├── docs ├── app │ ├── components │ │ └── .gitkeep │ ├── helpers │ │ └── .gitkeep │ ├── models │ │ └── .gitkeep │ ├── routes │ │ └── .gitkeep │ ├── controllers │ │ ├── .gitkeep │ │ └── index.js │ ├── templates │ │ ├── application.hbs │ │ ├── not-found.hbs │ │ ├── docs │ │ │ ├── index.md │ │ │ ├── quickstart.md │ │ │ ├── usage.md │ │ │ └── components │ │ │ │ ├── menu.md │ │ │ │ └── menu-wrapper.md │ │ ├── docs.hbs │ │ └── index.hbs │ ├── app.js │ ├── router.js │ ├── index.html │ └── styles │ │ └── app.scss ├── .watchmanconfig ├── public │ └── robots.txt ├── .prettierrc.js ├── .template-lintrc.js ├── config │ ├── optional-features.json │ ├── targets.js │ ├── addon-docs.js │ ├── deploy.js │ ├── ember-try.js │ └── environment.js ├── .ember-cli ├── tests │ ├── test-helper.js │ ├── acceptance │ │ └── smoke-test.js │ └── index.html ├── .eslintignore ├── .prettierignore ├── ember-cli-build.js ├── .gitignore ├── testem.js ├── .npmignore ├── eslint.config.mjs └── package.json ├── test-app ├── app │ ├── models │ │ └── .gitkeep │ ├── routes │ │ └── .gitkeep │ ├── components │ │ └── .gitkeep │ ├── controllers │ │ └── .gitkeep │ ├── helpers │ │ └── .gitkeep │ ├── templates │ │ └── application.hbs │ ├── styles │ │ └── app.css │ ├── router.js │ ├── app.js │ └── config │ │ └── environment.js ├── tests │ ├── helpers │ │ └── .gitkeep │ ├── integration │ │ ├── .gitkeep │ │ └── components │ │ │ ├── mobile-menu │ │ │ ├── mask-test.js │ │ │ └── tray-test.js │ │ │ ├── mobile-menu-toggle-test.js │ │ │ ├── mobile-menu-test.js │ │ │ └── mobile-menu-wrapper-test.js │ ├── test-helper.js │ └── index.html ├── .watchmanconfig ├── public │ └── robots.txt ├── .template-lintrc.js ├── .stylelintrc.js ├── .stylelintignore ├── .prettierignore ├── config │ ├── targets.js │ ├── optional-features.json │ ├── ember-cli-update.json │ ├── environment.js │ └── ember-try.js ├── .ember-cli ├── .env.development ├── vite.config.mjs ├── .prettierrc.js ├── .editorconfig ├── .gitignore ├── ember-cli-build.js ├── testem.js ├── tsconfig.json ├── index.html ├── README.md ├── babel.config.cjs ├── eslint.config.mjs └── package.json ├── pnpm-workspace.yaml ├── ember-mobile-menu ├── src │ ├── components │ │ ├── mobile-menu-toggle.css │ │ ├── mobile-menu │ │ │ ├── tray.css │ │ │ ├── mask.css │ │ │ ├── mask.gjs │ │ │ └── tray.gjs │ │ ├── utils.js │ │ ├── mobile-menu-toggle.gjs │ │ ├── mobile-menu-wrapper.css │ │ ├── mobile-menu-wrapper │ │ │ └── content.gjs │ │ ├── mobile-menu.css │ │ ├── mobile-menu.gjs │ │ └── mobile-menu-wrapper.gjs │ ├── utils │ │ ├── normalize-coordinates.js │ │ └── body-scroll-lock.js │ ├── themes │ │ └── android.css │ └── spring.js ├── addon-main.cjs ├── .prettierignore ├── .template-lintrc.cjs ├── .prettierrc.cjs ├── .gitignore ├── babel.config.json ├── eslint.config.mjs ├── rollup.config.mjs └── package.json ├── .prettierrc.cjs ├── .prettierignore ├── .npmrc ├── .editorconfig ├── .gitignore ├── config └── ember-cli-update.json ├── patches └── ember-app-scheduler.patch ├── renovate.json ├── CONTRIBUTING.md ├── LICENSE.md ├── .github └── workflows │ ├── addon-docs.yml │ ├── publish.yml │ ├── plan-release.yml │ └── ci.yml ├── RELEASE.md ├── README.md ├── package.json ├── .release-plan.json └── CHANGELOG.md /docs/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{outlet}} -------------------------------------------------------------------------------- /test-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ember-mobile-menu 3 | - docs 4 | - test-app 5 | -------------------------------------------------------------------------------- /test-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu-toggle.css: -------------------------------------------------------------------------------- 1 | .mobile-menu__toggle { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /test-app/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-ember-template-tag'], 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /test-app/app/styles/app.css: -------------------------------------------------------------------------------- 1 | /* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ 2 | -------------------------------------------------------------------------------- /docs/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "ember-mobile-menu"}} 2 | 3 |
4 | 5 | {{outlet}} 6 |
-------------------------------------------------------------------------------- /test-app/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], 5 | }; 6 | -------------------------------------------------------------------------------- /ember-mobile-menu/addon-main.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { addonV1Shim } = require('@embroider/addon-shim'); 4 | module.exports = addonV1Shim(__dirname); 5 | -------------------------------------------------------------------------------- /test-app/.stylelintignore: -------------------------------------------------------------------------------- 1 | # unconventional files 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # addons 8 | /.node_modules.ember-try/ 9 | -------------------------------------------------------------------------------- /docs/app/templates/not-found.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Not found

3 |

This page doesn't exist. Head home?

4 |
-------------------------------------------------------------------------------- /ember-mobile-menu/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | /declarations/ 7 | 8 | # misc 9 | /coverage/ 10 | -------------------------------------------------------------------------------- /ember-mobile-menu/.template-lintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | rules: { 6 | 'require-strict-mode': 'error', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /test-app/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | 12 | # ember-try 13 | /.node_modules.ember-try/ 14 | -------------------------------------------------------------------------------- /docs/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /test-app/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /test-app/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true, 6 | "no-implicit-route-model": true 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Prettier is also run from each package, so the ignores here 2 | # protect against files that may not be within a package 3 | 4 | # misc 5 | !.* 6 | .lint-todo/ 7 | 8 | # ember-try 9 | /.node_modules.ember-try/ 10 | /pnpm-lock.ember-try.yaml 11 | -------------------------------------------------------------------------------- /docs/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default class IndexController extends Controller { 4 | mask = true; 5 | shadow = true; 6 | type = 'left'; 7 | mode = 'default'; 8 | openDetectionWidth = -1; 9 | } 10 | -------------------------------------------------------------------------------- /test-app/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript 4 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available. 5 | */ 6 | "isTypeScriptProject": false 7 | } 8 | -------------------------------------------------------------------------------- /test-app/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'test-app/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () {}); 10 | -------------------------------------------------------------------------------- /docs/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /docs/config/addon-docs.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | const AddonDocsConfig = require('ember-cli-addon-docs/lib/config'); 5 | 6 | module.exports = class extends AddonDocsConfig { 7 | // See https://ember-learn.github.io/ember-cli-addon-docs/docs/deploying 8 | // for details on configuration you can override here. 9 | }; 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Docs: https://pnpm.io/npmrc 2 | # https://github.com/emberjs/rfcs/pull/907 3 | 4 | # we don't want addons to be bad citizens of the ecosystem 5 | auto-install-peers=false 6 | 7 | # we want true isolation, 8 | # if a dependency is not declared, we want an error 9 | resolve-peers-from-workspace-root=false 10 | 11 | use-node-version=20.18.3 12 | -------------------------------------------------------------------------------- /ember-mobile-menu/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-ember-template-tag'], 5 | overrides: [ 6 | { 7 | files: '*.{js,gjs,ts,gts,mjs,mts,cjs,cts}', 8 | options: { 9 | singleQuote: true, 10 | templateSingleQuote: false, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /test-app/.env.development: -------------------------------------------------------------------------------- 1 | # This file is committed to git and should not contain any secrets. 2 | # 3 | # Vite recommends using .env.local or .env.[mode].local if you need to manage secrets 4 | # SEE: https://vite.dev/guide/env-and-mode.html#env-files for more information. 5 | 6 | 7 | # Default NODE_ENV with vite build --mode=test is production 8 | NODE_ENV=development 9 | -------------------------------------------------------------------------------- /ember-mobile-menu/.gitignore: -------------------------------------------------------------------------------- 1 | # The authoritative copies of these live in the monorepo root (because they're 2 | # more useful on github that way), but the build copies them into here so they 3 | # will also appear in published NPM packages. 4 | /README.md 5 | /LICENSE.md 6 | 7 | # compiled output 8 | /dist 9 | /declarations 10 | 11 | # npm/pnpm/yarn pack output 12 | *.tgz 13 | -------------------------------------------------------------------------------- /docs/app/templates/docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | An [ember-cli](http://www.ember-cli.com) addon providing a draggable sidebar tailored to mobile devices. 4 | 5 | Both a left and a right menu are supported. Dragging is supported through touch events as supported by any modern (mobile) browser. The sidebar provides an empty canvas suitable for any content. An optional Android theme is supplied. 6 | -------------------------------------------------------------------------------- /docs/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'docs/app'; 2 | import config from 'docs/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /test-app/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { extensions, classicEmberSupport, ember } from '@embroider/vite'; 3 | import { babel } from '@rollup/plugin-babel'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | classicEmberSupport(), 8 | ember(), 9 | // extra plugins here 10 | babel({ 11 | babelHelpers: 'runtime', 12 | extensions, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /test-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-ember-template-tag'], 5 | overrides: [ 6 | { 7 | files: '*.{js,gjs,ts,gts,mjs,mts,cjs,cts}', 8 | options: { 9 | singleQuote: true, 10 | }, 11 | }, 12 | { 13 | files: '*.{gjs,gts}', 14 | options: { 15 | singleQuote: true, 16 | templateSingleQuote: false, 17 | }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /docs/.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /npm-shrinkwrap.json.ember-try 23 | /package.json.ember-try 24 | /package-lock.json.ember-try 25 | /yarn.lock.ember-try 26 | -------------------------------------------------------------------------------- /test-app/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'test-app/app'; 2 | import config from 'test-app/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start as qunitStart } from 'ember-qunit'; 7 | 8 | export function start() { 9 | setApplication(Application.create(config.APP)); 10 | 11 | setup(QUnit.assert); 12 | 13 | qunitStart(); 14 | } 15 | -------------------------------------------------------------------------------- /test-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /docs/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | .lint-todo/ 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /npm-shrinkwrap.json.ember-try 23 | /package.json.ember-try 24 | /package-lock.json.ember-try 25 | /yarn.lock.ember-try 26 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu/tray.css: -------------------------------------------------------------------------------- 1 | .mobile-menu__tray { 2 | position: absolute; 3 | top: 0; 4 | height: var(--mobile-menu-height); 5 | /* Avoid Chrome to see Safari hack */ 6 | overflow-y: auto; 7 | touch-action: pan-y; 8 | background: #fff; 9 | will-change: transform; 10 | } 11 | @supports (-webkit-touch-callout: none) { 12 | .mobile-menu__tray { 13 | /* The hack for Safari */ 14 | height: -webkit-fill-available; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-app/.gitignore: -------------------------------------------------------------------------------- 1 | /tmp/ 2 | 3 | # compiled output 4 | /dist/ 5 | /declarations/ 6 | 7 | # dependencies 8 | /node_modules/ 9 | 10 | # misc 11 | /.env* 12 | /.pnp* 13 | /.eslintcache 14 | /coverage/ 15 | /npm-debug.log* 16 | /testem.log 17 | /yarn-error.log 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /npm-shrinkwrap.json.ember-try 22 | /package.json.ember-try 23 | /package-lock.json.ember-try 24 | /yarn.lock.ember-try 25 | 26 | # broccoli-debug 27 | /DEBUG/ 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # misc 7 | .env* 8 | .pnp* 9 | .pnpm-debug.log 10 | .sass-cache 11 | .eslintcache 12 | coverage/ 13 | npm-debug.log* 14 | yarn-error.log 15 | 16 | # ember-try 17 | /.node_modules.ember-try/ 18 | /package.json.ember-try 19 | /package-lock.json.ember-try 20 | /yarn.lock.ember-try 21 | /pnpm-lock.ember-try.yaml 22 | 23 | 24 | .DS_Store 25 | /.idea/ 26 | -------------------------------------------------------------------------------- /ember-mobile-menu/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@embroider/addon-dev/template-colocation-plugin", 4 | "ember-concurrency/async-arrow-task-transform", 5 | [ 6 | "babel-plugin-ember-template-compilation", 7 | { 8 | "targetFormat": "hbs", 9 | "transforms": [] 10 | } 11 | ], 12 | [ 13 | "module:decorator-transforms", 14 | { "runtime": { "import": "decorator-transforms/runtime" } } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /docs/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'docs/config/environment'; 5 | import 'ember-mobile-menu/themes/android'; 6 | 7 | export default class App extends Application { 8 | modulePrefix = config.modulePrefix; 9 | podModulePrefix = config.podModulePrefix; 10 | Resolver = Resolver; 11 | } 12 | 13 | loadInitializers(App, config.modulePrefix); 14 | -------------------------------------------------------------------------------- /config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "projectName": "ember-mobile-menu", 4 | "packages": [ 5 | { 6 | "name": "@embroider/addon-blueprint", 7 | "version": "4.0.0", 8 | "blueprints": [ 9 | { 10 | "name": "@embroider/addon-blueprint", 11 | "isBaseBlueprint": true, 12 | "options": [ 13 | "--ci-provider=github", 14 | "--pnpm" 15 | ] 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /docs/tests/acceptance/smoke-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { visit } from '@ember/test-helpers'; 3 | import { setupApplicationTest } from 'ember-qunit'; 4 | 5 | module('Acceptance | smoke test', function (hooks) { 6 | setupApplicationTest(hooks); 7 | 8 | test('visiting /', async function (assert) { 9 | await visit('/'); 10 | 11 | assert.dom('h1').exists(); 12 | assert.dom('h1').hasText('Ember MobileMenu'); 13 | assert.dom('.mobile-menu').exists(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test-app/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "5.1.0", 7 | "blueprints": [ 8 | { 9 | "name": "app", 10 | "outputRepo": "https://github.com/ember-cli/ember-new-output", 11 | "codemodsSource": "ember-app-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--no-welcome" 15 | ] 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test-app/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from "./config/environment"; 5 | 6 | import compatModules from "@embroider/virtual/compat-modules"; 7 | 8 | export default class App extends Application { 9 | modulePrefix = config.modulePrefix; 10 | podModulePrefix = config.podModulePrefix; 11 | Resolver = Resolver.withModules(compatModules); 12 | } 13 | 14 | loadInitializers(App, config.modulePrefix, compatModules); 15 | -------------------------------------------------------------------------------- /patches/ember-app-scheduler.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index d35c583d22f08797101030ba0e047284836e7e5d..e99149e2c9ea1e34b7cb809af953d94b111b606f 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -44,9 +44,10 @@ 6 | ] 7 | }, 8 | "dependencies": { 9 | - "@ember/test-waiters": "^3.0.0", 10 | + "@ember/test-waiters": "*", 11 | "@types/ember": "^3.16.5", 12 | "@types/rsvp": "^4.0.4", 13 | + "ember-auto-import": "*", 14 | "ember-cli-babel": "^7.26.6", 15 | "ember-cli-typescript": "^4.2.1", 16 | "ember-compatibility-helpers": "^1.2.5", 17 | -------------------------------------------------------------------------------- /docs/app/router.js: -------------------------------------------------------------------------------- 1 | import AddonDocsRouter, { docsRoute } from 'ember-cli-addon-docs/router'; 2 | import config from 'docs/config/environment'; 3 | 4 | export default class Router extends AddonDocsRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () { 10 | docsRoute(this, function () { 11 | this.route('usage'); 12 | this.route('quickstart'); 13 | 14 | this.route('components', function () { 15 | this.route('menu-wrapper'); 16 | this.route('menu'); 17 | }); 18 | }); 19 | this.route('not-found', { path: '/*path' }); 20 | }); 21 | -------------------------------------------------------------------------------- /test-app/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 3 | 4 | const { compatBuild } = require('@embroider/compat'); 5 | 6 | const needsOwnerPolyfill = process.env.NEEDS_OWNER_POLYFILL === 'true'; 7 | 8 | console.log(`Needs owner polyfill? ${needsOwnerPolyfill}`); 9 | 10 | module.exports = async function (defaults) { 11 | const { buildOnce } = await import('@embroider/vite'); 12 | 13 | let app = new EmberApp(defaults, { 14 | autoImport: { 15 | watchDependencies: ['ember-mobile-menu'], 16 | }, 17 | }); 18 | 19 | return compatBuild(app, buildOnce); 20 | }; 21 | -------------------------------------------------------------------------------- /docs/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberApp(defaults, { 7 | 'ember-cli-addon-docs': { 8 | documentingAddonAt: '../ember-mobile-menu', 9 | }, 10 | }); 11 | 12 | /* 13 | This build file specifies the options for the dummy test app of this 14 | addon, located in `/docs` 15 | This build file does *not* influence how the addon or the app using it 16 | behave. You most likely want to be modifying `./index.js` or app's build file 17 | */ 18 | return app.toTree(); 19 | }; 20 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /npm-shrinkwrap.json.ember-try 27 | /package.json.ember-try 28 | /package-lock.json.ember-try 29 | /yarn.lock.ember-try 30 | 31 | /.idea/ 32 | /vendor/ember-mobile-menu.css 33 | -------------------------------------------------------------------------------- /docs/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu/mask.css: -------------------------------------------------------------------------------- 1 | .mobile-menu__mask { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | border: none; 6 | border-radius: 0; 7 | margin: 0; 8 | padding: 0; 9 | width: 100vw; 10 | height: 100vh; 11 | /* Avoid Chrome to see Safari hack */ 12 | background: rgba(0, 0, 0, 0.3); 13 | opacity: 0; 14 | transition: none; 15 | touch-action: pan-y; 16 | will-change: opacity; 17 | visibility: hidden; 18 | outline: none; 19 | -webkit-tap-highlight-color: transparent; 20 | } 21 | @supports (-webkit-touch-callout: none) { 22 | .mobile-menu__mask { 23 | /* The hack for Safari */ 24 | height: -webkit-fill-available; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/app/templates/docs.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | {{outlet}} 18 |
19 |
20 |
21 | 22 |
-------------------------------------------------------------------------------- /ember-mobile-menu/src/components/utils.js: -------------------------------------------------------------------------------- 1 | import { waitForPromise } from '@ember/test-waiters'; 2 | /** 3 | * See also: https://reactive.nullvoxpopuli.com/functions/sync.sync.html 4 | * 5 | * This version ties in to the waiter system 6 | */ 7 | export function effect(fn, ...args) { 8 | waitForPromise( 9 | (async () => { 10 | /** 11 | * Detaches from auto-tracking so that mutations here doen't cause 12 | * infinite re-render loops (which would run this effect) 13 | * 14 | * Infinite re-render loops are still possible if then some other effect 15 | * causes this effect to change. 16 | */ 17 | await 0; 18 | await fn(...args); 19 | })(), 20 | ); 21 | 22 | return; 23 | } 24 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:js-lib", 4 | ":automergeLinters", 5 | ":automergeTesters", 6 | ":dependencyDashboard", 7 | ":maintainLockFilesWeekly", 8 | ":semanticCommitsDisabled", 9 | "github>Turbo87/renovate-config:automergeCaretConstraint" 10 | ], 11 | "customManagers": [ 12 | { 13 | "customType": "regex", 14 | "fileMatch": ["^.github/workflows/[^\\.]+\\.ya?ml$"], 15 | "matchStrings": ["PNPM_VERSION:\\s*(?.*?)\n"], 16 | "depNameTemplate": "pnpm", 17 | "datasourceTemplate": "npm" 18 | } 19 | ], 20 | "packageRules": [ 21 | { 22 | "matchPackageNames": ["ember-cli", "ember-data", "ember-source"], 23 | "separateMinorPatch": true 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-mobile-menu` 7 | * `pnpm install` 8 | 9 | ## Linting 10 | 11 | * `pnpm lint` 12 | * `pnpm lint:fix` 13 | 14 | ## Running tests 15 | 16 | * `cd test-app` – Move to the test-app directory 17 | * `ember test` – Runs the test suite on the current Ember version 18 | * `ember test --server` – Runs the test suite in "watch mode" 19 | * `ember try:each` – Runs the test suite against multiple Ember versions 20 | 21 | ## Running the dummy application 22 | 23 | * `ember serve` 24 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 25 | 26 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 27 | -------------------------------------------------------------------------------- /test-app/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict';; 2 | if (typeof module !== "undefined") { 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /docs/.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.github/ 18 | /.gitignore 19 | /.prettierignore 20 | /.prettierrc.js 21 | /.template-lintrc.js 22 | /.travis.yml 23 | /.watchmanconfig 24 | /bower.json 25 | /config/ember-try-typescript.js 26 | /config/ember-try.js 27 | /CONTRIBUTING.md 28 | /ember-cli-build.js 29 | /testem.js 30 | /tests/ 31 | /yarn-error.log 32 | /yarn.lock 33 | .gitkeep 34 | 35 | # ember-try 36 | /.node_modules.ember-try/ 37 | /bower.json.ember-try 38 | /npm-shrinkwrap.json.ember-try 39 | /package.json.ember-try 40 | /package-lock.json.ember-try 41 | /yarn.lock.ember-try 42 | -------------------------------------------------------------------------------- /test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember/tsconfig.json", 3 | "glint": { 4 | "environment": ["ember-loose", "ember-template-imports"] 5 | }, 6 | "compilerOptions": { 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "noEmitOnError": false, 10 | "declaration": false, 11 | "declarationMap": false, 12 | // The combination of `baseUrl` with `paths` allows Ember's classic package 13 | // layout, which is not resolvable with the Node resolution algorithm, to 14 | // work with TypeScript. 15 | "baseUrl": ".", 16 | "paths": { 17 | "test-app/tests/*": ["tests/*"], 18 | "test-app/*": ["app/*"], 19 | "*": ["types/*"] 20 | }, 21 | "types": [ 22 | "ember-source/types", 23 | "@embroider/core/virtual", 24 | "vite/client" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/app/templates/docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | To get started the `` component needs to be placed high in the dom so that it wraps the entirety of your app's content on which the gestures need to be detected. From this component you can then yield a content, toggle and menu component to which you can pass a block of content. An extended `` component is available which closes the menu on click. The content component should wrap your pages content. It will react to gestures and adjust its styles as needed. 4 | 5 | ```handlebars 6 | 7 | 8 | Home 9 | 10 | 11 | 12 | Menu 13 | 14 | 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ember-mobile-menu documentation 7 | 8 | 9 | 10 | 11 | {{content-for "head"}} 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | 18 | 19 | {{content-for "body"}} 20 | 21 | 22 | 23 | 24 | {{content-for "body-footer"}} 25 | 26 | 27 | -------------------------------------------------------------------------------- /test-app/app/config/environment.js: -------------------------------------------------------------------------------- 1 | import loadConfigFromMeta from '@embroider/config-meta-loader'; 2 | import { assert } from '@ember/debug'; 3 | 4 | const config = loadConfigFromMeta('test-app'); 5 | 6 | assert( 7 | 'config is not an object', 8 | typeof config === 'object' && config !== null, 9 | ); 10 | assert( 11 | 'modulePrefix was not detected on your config', 12 | 'modulePrefix' in config && typeof config.modulePrefix === 'string', 13 | ); 14 | assert( 15 | 'locationType was not detected on your config', 16 | 'locationType' in config && typeof config.locationType === 'string', 17 | ); 18 | assert( 19 | 'rootURL was not detected on your config', 20 | 'rootURL' in config && typeof config.rootURL === 'string', 21 | ); 22 | assert( 23 | 'APP was not detected on your config', 24 | 'APP' in config && typeof config.APP === 'object', 25 | ); 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /docs/config/deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function (deployTarget) { 5 | let ENV = { 6 | build: {}, 7 | // include other plugin configuration that applies to all deploy targets here 8 | }; 9 | 10 | if (deployTarget === 'development') { 11 | ENV.build.environment = 'development'; 12 | // configure other plugins for development deploy target here 13 | } 14 | 15 | if (deployTarget === 'staging') { 16 | ENV.build.environment = 'production'; 17 | // configure other plugins for staging deploy target here 18 | } 19 | 20 | if (deployTarget === 'production') { 21 | ENV.build.environment = 'production'; 22 | // configure other plugins for production deploy target here 23 | } 24 | 25 | // Note: if you need to build some configuration asynchronously, you can return 26 | // a promise that resolves with the ENV object instead of returning the 27 | // ENV object synchronously. 28 | return ENV; 29 | }; 30 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu-toggle.gjs: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import './mobile-menu-toggle.css'; 3 | import { on } from '@ember/modifier'; 4 | 5 | const _fn = () => {}; 6 | 7 | /** 8 | * A toggle component to open or close a menu. 9 | * 10 | * @class MobileMenuToggle 11 | * @public 12 | */ 13 | export default class MobileMenuToggle extends Component { 14 | /** 15 | * Target menu for the toggle 16 | * 17 | * @argument target 18 | * @type String 'left' or 'right' 19 | */ 20 | 21 | /** 22 | * Hook fired when the toggle is clicked. You can pass in an action. 23 | * 24 | * @argument onClick 25 | * @type function 26 | */ 27 | get onClick() { 28 | return () => this.args.onClick(this.args.target) ?? _fn; 29 | } 30 | 31 | 41 | } 42 | -------------------------------------------------------------------------------- /test-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ember-mobile-menu 7 | 8 | 9 | 10 | 11 | {{content-for "head"}} 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | 18 | 19 | {{content-for "body"}} 20 | 21 | 22 | 28 | 29 | {{content-for "body-footer"}} 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /docs/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | //@import "ember-mobile-menu"; 2 | //@import "ember-mobile-menu/themes/android"; 3 | 4 | .demo-height { 5 | height: 500px; 6 | 7 | .mobile-menu-wrapper { 8 | height: 100%; 9 | } 10 | } 11 | 12 | .mobile-menu-example-options { 13 | margin-top: 2rem; 14 | 15 | h3 { 16 | font-weight: bold; 17 | text-transform: uppercase; 18 | font-size: 0.9em; 19 | } 20 | 21 | label { 22 | input { 23 | margin-right: 0.5rem; 24 | } 25 | } 26 | } 27 | 28 | .mobile-menu-demo { 29 | background: #FFF; 30 | box-shadow: 0 25px 50px rgba(0,0,0,0.3); 31 | border-width: 2px; 32 | 33 | > .example { 34 | padding: 0; 35 | border-radius: 3px; 36 | overflow: hidden; 37 | } 38 | 39 | .mobile-menu-wrapper { 40 | height: 500px; 41 | } 42 | 43 | .mobile-menu__header { 44 | background: lighten(#E04E39, 5%); 45 | } 46 | } 47 | 48 | table { 49 | margin-bottom: 1rem; 50 | 51 | th { 52 | font-weight: bold; 53 | white-space: nowrap; 54 | } 55 | 56 | th, td { 57 | border: 1px solid #6c757d; 58 | padding: 0.25rem 0.5rem; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test-app/tests/integration/components/mobile-menu/mask-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, click } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | mobile-menu/mask', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | await render(hbs``); 11 | 12 | assert.strictEqual(this.element.textContent.trim(), ''); 13 | }); 14 | 15 | test('it triggers the onClick hook when clicked', async function (assert) { 16 | this.handleClick = (...args) => { 17 | assert.strictEqual(args.length, 1); 18 | assert.ok(args[0] instanceof MouseEvent); 19 | assert.step(`handleClick`); 20 | }; 21 | 22 | // Template block usage: 23 | await render(hbs` 24 | 25 | My Link 26 | 27 | `); 28 | 29 | await click('.mobile-menu__mask'); 30 | 31 | assert.verifySteps(['handleClick']); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/utils/normalize-coordinates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalizes coordinates in the passed TouchData to the BoundingClientRect of the passed element 3 | * 4 | * @function normalizeCoordinates 5 | * @param e {Object} TouchData as generated by ember-mobile-core 6 | * @param bcr {DOMRect} The DOMRect of the element to which the coordinates need to be normalized. 7 | * @return {Object} Returns a TouchData object 8 | * @private 9 | */ 10 | export default function normalizeCoordinates(e, bcr) { 11 | return { 12 | ...e, 13 | initial: { 14 | ...e.initial, 15 | x: e.initial.x - bcr.x, 16 | y: e.initial.x - bcr.x, 17 | }, 18 | current: { 19 | ...e.current, 20 | x: e.current.x - bcr.x, 21 | y: e.current.x - bcr.x, 22 | }, 23 | }; 24 | } 25 | 26 | export function scaleCorrection(e, scaleX, scaleY) { 27 | // TODO: convert rest of API 28 | return { 29 | ...e, 30 | current: { 31 | ...e.current, 32 | distance: e.current.distance / ((scaleX + scaleY) / 2), 33 | distanceX: e.current.distanceX / scaleX, 34 | distanceY: e.current.distanceY / scaleY, 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /docs/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | usePnpm: true, 9 | scenarios: [ 10 | { 11 | name: 'ember-lts-3.28', 12 | npm: { 13 | devDependencies: { 14 | 'ember-source': '~3.28.0', 15 | }, 16 | }, 17 | }, 18 | { 19 | name: 'ember-release', 20 | npm: { 21 | devDependencies: { 22 | 'ember-source': await getChannelURL('release'), 23 | }, 24 | }, 25 | }, 26 | { 27 | name: 'ember-beta', 28 | npm: { 29 | devDependencies: { 30 | 'ember-source': await getChannelURL('beta'), 31 | }, 32 | }, 33 | }, 34 | { 35 | name: 'ember-canary', 36 | npm: { 37 | devDependencies: { 38 | 'ember-source': await getChannelURL('canary'), 39 | }, 40 | }, 41 | }, 42 | embroiderSafe(), 43 | embroiderOptimized(), 44 | ], 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /docs/app/templates/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | ## Installation 3 | From your application directory run: 4 | 5 | ```sh 6 | ember install ember-mobile-menu 7 | ``` 8 | 9 | **NOTE:** the minimum supported Ember version is v3.28. 10 | 11 | ## Styles 12 | The addon will automatically import its required CSS. Some CSS variables are available for customization, should it be necessary. 13 | 14 | ```css 15 | :root { 16 | --mobile-menu-wrapper-width: 100%; 17 | --mobile-menu-wrapper-min-height: 100vh; 18 | 19 | --mobile-menu-height: 100vh; 20 | --mobile-menu-z-index: 2000; 21 | } 22 | ``` 23 | 24 | An optional `android` theme is also available for the sidebar (it's used in this documentation). This can be imported in your app with ember-auto-import. This can also be `app.js`. 25 | 26 | ```javascript 27 | import 'ember-mobile-menu/themes/android' 28 | ``` 29 | 30 | It also has CSS variables available for customization. 31 | 32 | ```css 33 | :root { 34 | --mobile-menu-header-bg: #E04E39; 35 | 36 | --mobile-menu-item-color: #333; 37 | --mobile-menu-item-active-bg: #EEE; 38 | --mobile-menu-item-link-disabled-color: #6C757D; 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /test-app/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | const ENV = { 5 | modulePrefix: 'test-app', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'history', 9 | EmberENV: { 10 | EXTEND_PROTOTYPES: false, 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 14 | }, 15 | }, 16 | 17 | APP: { 18 | // Here you can pass flags/options to your application instance 19 | // when it is created 20 | }, 21 | }; 22 | 23 | if (environment === 'development') { 24 | // ENV.APP.LOG_RESOLVER = true; 25 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 26 | // ENV.APP.LOG_TRANSITIONS = true; 27 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 28 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 29 | } 30 | 31 | if (environment === 'test') { 32 | // Testem prefers this... 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | ENV.APP.autoboot = false; 41 | } 42 | 43 | if (environment === 'production') { 44 | // here you can enable a production-specific feature 45 | } 46 | 47 | return ENV; 48 | }; 49 | -------------------------------------------------------------------------------- /docs/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ember-mobile-menu Docs Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu-wrapper.css: -------------------------------------------------------------------------------- 1 | @layer ember-mobile-menu { 2 | :root { 3 | --mobile-menu-wrapper-width: 100%; 4 | --mobile-menu-wrapper-min-height: 100vh; 5 | 6 | --mobile-menu-height: 100vh; 7 | --mobile-menu-z-index: 2000; 8 | } 9 | } 10 | 11 | body.mobile-menu--prevent-scroll { 12 | overflow: hidden; 13 | } 14 | 15 | .mobile-menu-wrapper { 16 | overflow: hidden; 17 | width: var(--mobile-menu-wrapper-width); 18 | min-height: var(--mobile-menu-wrapper-min-height); 19 | /* Avoid Chrome to see Safari hack */ 20 | } 21 | @supports (-webkit-touch-callout: none) { 22 | .mobile-menu-wrapper { 23 | /* The hack for Safari */ 24 | min-height: -webkit-fill-available; 25 | } 26 | } 27 | 28 | .mobile-menu-wrapper--embedded { 29 | position: relative; 30 | min-height: 100%; 31 | min-width: 100%; 32 | overflow: hidden; 33 | } 34 | .mobile-menu-wrapper--embedded .mobile-menu-wrapper__content { 35 | min-height: 100%; 36 | } 37 | 38 | .mobile-menu-wrapper__content { 39 | min-height: var(--mobile-menu-wrapper-min-height); 40 | position: relative; 41 | background: #fff; 42 | will-change: transform, margin-left, margin-right; 43 | z-index: 1; 44 | touch-action: pan-y; 45 | } 46 | .mobile-menu-wrapper__content--shadow { 47 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 48 | } 49 | .mobile-menu-wrapper__content--ios, 50 | .mobile-menu-wrapper__content--reveal, 51 | .mobile-menu-wrapper__content--squeeze-reveal { 52 | z-index: 2; 53 | } 54 | -------------------------------------------------------------------------------- /test-app/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ember-mobile-menu Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | {{content-for "body-footer"}} 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/themes/android.css: -------------------------------------------------------------------------------- 1 | @layer ember-mobile-menu { 2 | :root { 3 | --mobile-menu-header-bg: #e04e39; 4 | 5 | --mobile-menu-item-color: #333; 6 | --mobile-menu-item-active-bg: #eee; 7 | --mobile-menu-item-link-disabled-color: #6c757d; 8 | } 9 | } 10 | 11 | .mobile-menu__tray .mobile-menu__header { 12 | min-height: 150px; 13 | background: var(--mobile-menu-header-bg); 14 | color: #fff; 15 | margin-bottom: 8px; 16 | } 17 | .mobile-menu__tray .mobile-menu__header .header__text { 18 | padding: 16px; 19 | } 20 | .mobile-menu__tray .mobile-menu__header .header__btn { 21 | padding: 16px; 22 | color: #fff; 23 | text-decoration: none; 24 | } 25 | 26 | .mobile-menu__tray .mobile-menu__nav { 27 | list-style: none; 28 | padding: 0; 29 | margin: 0; 30 | } 31 | .mobile-menu__tray .mobile-menu__nav .mobile-menu__nav-item a { 32 | display: block; 33 | font-size: 12px; 34 | font-weight: bold; 35 | color: var(--mobile-menu-item-color); 36 | line-height: 1.5; 37 | text-decoration: none !important; 38 | 39 | padding: 12px; 40 | } 41 | .mobile-menu__tray 42 | .mobile-menu__nav 43 | .mobile-menu__nav-item 44 | a.mobile-menu__nav-link.disabled { 45 | color: var(--mobile-menu-item-link-disabled-color); 46 | } 47 | .mobile-menu__tray .mobile-menu__nav .mobile-menu__nav-item a.active { 48 | background: var(--mobile-menu-item-active-bg); 49 | } 50 | .mobile-menu__tray .mobile-menu__nav .mobile-menu__nav-divider { 51 | margin: 8px 0; 52 | height: 0; 53 | border-bottom: 1px solid var(--mobile-menu-item-active-bg); 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/addon-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Addon Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | tags: 9 | - "**" 10 | jobs: 11 | build: 12 | env: 13 | DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - name: Check for tags and set short-circuit condition 21 | id: check-tags 22 | run: | 23 | # Fetch tags pointing to the current commit 24 | TAGS=$(git tag --points-at $GITHUB_SHA) 25 | echo "Tags found: $TAGS" 26 | 27 | # Check if a tag exists and if the ref is 'refs/heads/main' or 'refs/heads/master' 28 | if [ -n "$TAGS" ] && ([[ "${GITHUB_REF}" == "refs/heads/main" ]] || [[ "${GITHUB_REF}" == "refs/heads/master" ]]); then 29 | echo "SHORT_CIRCUIT=true" >> $GITHUB_ENV 30 | else 31 | echo "SHORT_CIRCUIT=false" >> $GITHUB_ENV 32 | fi 33 | - uses: pnpm/action-setup@v4 34 | if: env.SHORT_CIRCUIT == 'false' 35 | - uses: actions/setup-node@v4 36 | if: env.SHORT_CIRCUIT == 'false' 37 | with: 38 | node-version: 18 39 | cache: pnpm 40 | - name: Install Dependencies 41 | if: env.SHORT_CIRCUIT == 'false' 42 | run: pnpm install --no-lockfile 43 | - name: Deploy Docs 44 | if: env.SHORT_CIRCUIT == 'false' 45 | run: cd docs && pnpm ember deploy production 46 | -------------------------------------------------------------------------------- /docs/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'docs', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'history', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // Allow ember-cli-addon-docs to update the rootURL in compiled assets 48 | ENV.rootURL = 'ADDON_DOCS_ROOT_URL'; 49 | // here you can enable a production-specific feature 50 | } 51 | 52 | return ENV; 53 | }; 54 | -------------------------------------------------------------------------------- /test-app/tests/integration/components/mobile-menu/tray-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | mobile-menu/tray', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | await render(hbs``); 11 | 12 | assert.strictEqual(this.element.textContent.trim(), ''); 13 | 14 | // Template block usage: 15 | await render(hbs` 16 | 17 | template block text 18 | 19 | `); 20 | 21 | assert.strictEqual(this.element.textContent.trim(), 'template block text'); 22 | }); 23 | 24 | test('it cleans up body-scroll-lock after undrendering', async function (assert) { 25 | this.set('showMenu', true); 26 | this.set('isClosed', true); 27 | 28 | await render( 29 | hbs`{{#if this.showMenu}}{{/if}}`, 30 | ); 31 | assert.strictEqual(document.body.style.overflow, ''); 32 | 33 | this.set('isClosed', false); 34 | assert.strictEqual(document.body.style.overflow, 'hidden'); 35 | 36 | this.set('isClosed', true); 37 | assert.strictEqual(document.body.style.overflow, ''); 38 | 39 | this.set('isClosed', false); 40 | assert.strictEqual(document.body.style.overflow, 'hidden'); 41 | 42 | this.set('showMenu', false); 43 | assert.strictEqual(document.body.style.overflow, ''); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test-app/README.md: -------------------------------------------------------------------------------- 1 | # test-app 2 | 3 | This README outlines the details of collaborating on this Ember application. 4 | A short introduction of this app could easily go here. 5 | 6 | ## Prerequisites 7 | 8 | You will need the following things properly installed on your computer. 9 | 10 | - [Git](https://git-scm.com/) 11 | - [Node.js](https://nodejs.org/) 12 | - [pnpm](https://pnpm.io/) 13 | - [Ember CLI](https://cli.emberjs.com/release/) 14 | - [Google Chrome](https://google.com/chrome/) 15 | 16 | ## Installation 17 | 18 | - `git clone ` this repository 19 | - `cd test-app` 20 | - `pnpm install` 21 | 22 | ## Running / Development 23 | 24 | - `pnpm start` 25 | - Visit your app at [http://localhost:4200](http://localhost:4200). 26 | - Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). 27 | 28 | ### Code Generators 29 | 30 | Make use of the many generators for code, try `ember help generate` for more details 31 | 32 | ### Running Tests 33 | 34 | - `pnpm test` 35 | - `pnpm test:ember --server` 36 | 37 | ### Linting 38 | 39 | - `pnpm lint` 40 | - `pnpm lint:fix` 41 | 42 | ### Building 43 | 44 | - `pnpm ember build` (development) 45 | - `pnpm build` (production) 46 | 47 | ### Deploying 48 | 49 | Specify what it takes to deploy your app. 50 | 51 | ## Further Reading / Useful Links 52 | 53 | - [ember.js](https://emberjs.com/) 54 | - [ember-cli](https://cli.emberjs.com/release/) 55 | - Development Browser Extensions 56 | - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) 57 | - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) 58 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/spring.js: -------------------------------------------------------------------------------- 1 | import { Spring as Wobble } from 'wobble'; 2 | 3 | /** 4 | * A thin wrapper around the `wobble` package which provides spring physics (damped harmonic oscillator). 5 | * 6 | * @class Spring 7 | * @private 8 | */ 9 | export default class Spring { 10 | spring; 11 | 12 | /** 13 | * @method constructor 14 | * @param {function} callback Called whenever the spring updates 15 | * @param {object} options See: https://github.com/skevy/wobble#api 16 | */ 17 | constructor(callback = () => {}, options = {}) { 18 | const { onStop = () => {}, ..._options } = options; 19 | 20 | const config = { 21 | stiffness: 100, 22 | damping: 10, 23 | mass: 1, 24 | restVelocityThreshold: 0.01, 25 | restDisplacementThreshold: 0.01, 26 | ..._options, 27 | }; 28 | 29 | this.spring = new Wobble(config); 30 | this.spring.onUpdate(callback); 31 | this.spring.onStop(() => { 32 | this.promise.resolve(); 33 | onStop(); 34 | }); 35 | } 36 | 37 | start() { 38 | this.promise = null; 39 | 40 | return new Promise((resolve) => { 41 | this.promise = { resolve }; 42 | 43 | const { fromValue, toValue, initialVelocity } = this.spring._config; 44 | 45 | // This is the same check as is done in wobble itself. It's needed to ensure our promise always resolves. 46 | if (fromValue !== toValue || initialVelocity !== 0) { 47 | this.spring.start(); 48 | } else { 49 | this.promise.resolve(); 50 | } 51 | }); 52 | } 53 | 54 | stop() { 55 | this.spring.stop(); 56 | } 57 | 58 | get currentVelocity() { 59 | return this.spring.currentVelocity; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged. 4 | 5 | ## Preparation 6 | 7 | Since the majority of the actual release process is automated, the remaining tasks before releasing are: 8 | 9 | - correctly labeling **all** pull requests that have been merged since the last release 10 | - updating pull request titles so they make sense to our users 11 | 12 | Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall 13 | guiding principle here is that changelogs are for humans, not machines. 14 | 15 | When reviewing merged PR's the labels to be used are: 16 | 17 | * breaking - Used when the PR is considered a breaking change. 18 | * enhancement - Used when the PR adds a new feature or enhancement. 19 | * bug - Used when the PR fixes a bug included in a previous release. 20 | * documentation - Used when the PR adds or updates documentation. 21 | * internal - Internal changes or things that don't fit in any other category. 22 | 23 | **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` 24 | 25 | ## Release 26 | 27 | Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/nickschot/ember-mobile-menu/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR 28 | -------------------------------------------------------------------------------- /test-app/babel.config.cjs: -------------------------------------------------------------------------------- 1 | const { 2 | babelCompatSupport, 3 | templateCompatSupport, 4 | } = require('@embroider/compat/babel'); 5 | 6 | const needsOwnerPolyfill = process.env.NEEDS_OWNER_POLYFILL === 'true'; 7 | const needsServicePolyfill = process.env.NEEDS_SERVICE_POLYFILL === 'true'; 8 | 9 | module.exports = { 10 | plugins: [ 11 | [ 12 | '@babel/plugin-transform-typescript', 13 | { 14 | allExtensions: true, 15 | onlyRemoveTypeImports: true, 16 | allowDeclareFields: true, 17 | }, 18 | ], 19 | needsOwnerPolyfill 20 | ? 'babel-plugin-ember-polyfill-get-and-set-owner-from-ember-owner' 21 | : null, 22 | needsServicePolyfill 23 | ? 'babel-plugin-undeprecate-inject-from-at-ember-service' 24 | : null, 25 | [ 26 | 'babel-plugin-ember-template-compilation', 27 | { 28 | compilerPath: 'ember-source/dist/ember-template-compiler.js', 29 | enableLegacyModules: [ 30 | 'ember-cli-htmlbars', 31 | 'ember-cli-htmlbars-inline-precompile', 32 | 'htmlbars-inline-precompile', 33 | ], 34 | transforms: [...templateCompatSupport()], 35 | }, 36 | ], 37 | [ 38 | 'module:decorator-transforms', 39 | { 40 | runtime: { 41 | import: require.resolve('decorator-transforms/runtime-esm'), 42 | }, 43 | }, 44 | ], 45 | [ 46 | '@babel/plugin-transform-runtime', 47 | { 48 | absoluteRuntime: __dirname, 49 | useESModules: true, 50 | regenerator: false, 51 | }, 52 | ], 53 | ...babelCompatSupport(), 54 | ].filter(Boolean), 55 | 56 | generatorOpts: { 57 | compact: false, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /test-app/tests/integration/components/mobile-menu-toggle-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, click } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | mobile-menu-toggle', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.set('myAction', function(val) { ... }); 12 | 13 | await render(hbs``); 14 | 15 | assert.strictEqual(this.element.textContent.trim(), ''); 16 | 17 | // Template block usage: 18 | await render(hbs` 19 | 20 | template block text 21 | 22 | `); 23 | 24 | assert.strictEqual(this.element.textContent.trim(), 'template block text'); 25 | }); 26 | 27 | test('it fires the `onClick` hook with no argument when clicked', async function (assert) { 28 | this.handleClick = (...args) => { 29 | assert.step(`handleClick: ${args}`); 30 | }; 31 | 32 | await render(hbs``); 33 | await click('.mobile-menu__toggle'); 34 | 35 | assert.verifySteps(['handleClick: ']); 36 | }); 37 | 38 | test('it fires the `onClick` hook with the passed target when clicked', async function (assert) { 39 | this.handleClick = (...args) => { 40 | assert.step(`handleClick: ${args}`); 41 | }; 42 | 43 | await render( 44 | hbs``, 45 | ); 46 | await click('.mobile-menu__toggle'); 47 | 48 | assert.verifySteps(['handleClick: right']); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ember-mobile-menu 2 | ============================================================================== 3 | 4 | [![Ember Observer Score](https://emberobserver.com/badges/ember-mobile-menu.svg)](https://emberobserver.com/addons/ember-mobile-menu) 5 | 6 | An [ember-cli](http://www.ember-cli.com) addon providing a draggable sidebar specifically tailored to mobile devices. 7 | 8 | Both a left and a right menu are supported. Dragging is supported through touch events as supported by any modern (mobile) browser. The sidebar provides an empty canvas suitable for any content. 9 | 10 | Requirements 11 | ------------------------------------------------------------------------------ 12 | * Ember.js v3.28 or above 13 | * Ember CLI v3.12 or above 14 | * ember-concurrency v3.x or v4.x 15 | * tracked-built-ins v3.x 16 | * ember-auto-import v2.x 17 | 18 | **NOTE:** This addon utilizes ResizeObservers. If you require support for older browser you can install a ResizeObserver polyfill like [ember-resize-observer-polyfill](https://github.com/PrecisionNutrition/ember-resize-observer-polyfill). 19 | 20 | Installation 21 | ------------------------------------------------------------------------------ 22 | 23 | From your application directory run: 24 | 25 | `ember install ember-mobile-menu` 26 | 27 | Documentation 28 | ------------------------------------------------------------------------------ 29 | https://nickschot.github.io/ember-mobile-menu 30 | 31 | Contributing 32 | ------------------------------------------------------------------------------ 33 | 34 | See the [Contributing](CONTRIBUTING.md) guide for details. 35 | 36 | Copyright and license 37 | ------------------------------------------------------------------------------ 38 | 39 | Code and documentation copyright 2018 Nick Schot. The code is released under [the MIT license](LICENSE.md). 40 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu-wrapper/content.gjs: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { htmlSafe } from '@ember/template'; 3 | import didPan from 'ember-gesture-modifiers/modifiers/did-pan'; 4 | import MaskComponent from '../mobile-menu/mask.gjs'; 5 | 6 | const MODES = new Map([ 7 | ['default', () => ''], 8 | ['push', (p) => `transform: translateX(${p}px);`], 9 | ['reveal', (p) => `transform: translateX(${p}px);`], 10 | ['ios', (p) => `transform: translateX(${p}px);`], 11 | ['squeeze', (p, side) => `margin-${side}: ${Math.abs(p)}px;`], 12 | ['squeeze-reveal', (p, side) => `margin-${side}: ${Math.abs(p)}px;`], 13 | ]); 14 | 15 | /** 16 | * @class ContentComponent 17 | * @private 18 | */ 19 | export default class ContentComponent extends Component { 20 | /** 21 | * @argument mode 22 | * @type string 23 | * @protected 24 | */ 25 | get mode() { 26 | return this.args.mode ?? 'default'; 27 | } 28 | 29 | get style() { 30 | let styles = ''; 31 | if (this.args.position > 0) { 32 | styles = MODES.get(this.mode)(this.args.position, 'left'); 33 | } else if (this.args.position < 0) { 34 | styles = MODES.get(this.mode)(this.args.position, 'right'); 35 | } 36 | 37 | return htmlSafe(styles); 38 | } 39 | 40 | get mask() { 41 | return ['reveal', 'ios'].includes(this.mode); 42 | } 43 | 44 | 66 | } 67 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu.css: -------------------------------------------------------------------------------- 1 | .mobile-menu { 2 | position: fixed; 3 | top: 0; 4 | width: 0; 5 | } 6 | 7 | .mobile-menu.mobile-menu--left { 8 | left: 0; 9 | } 10 | .mobile-menu.mobile-menu--right { 11 | right: 0; 12 | } 13 | 14 | /* variants */ 15 | .mobile-menu--default { 16 | z-index: var(--mobile-menu-z-index); 17 | } 18 | .mobile-menu--squeeze, 19 | .mobile-menu--push { 20 | z-index: 2; 21 | } 22 | .mobile-menu--ios, 23 | .mobile-menu--reveal, 24 | .mobile-menu--squeeze-reveal { 25 | display: none; 26 | z-index: -1; 27 | } 28 | .mobile-menu--ios.mobile-menu--dragging, 29 | .mobile-menu--ios.mobile-menu--transitioning, 30 | .mobile-menu--ios.mobile-menu--open, 31 | .mobile-menu--reveal.mobile-menu--dragging, 32 | .mobile-menu--reveal.mobile-menu--transitioning, 33 | .mobile-menu--reveal.mobile-menu--open, 34 | .mobile-menu--squeeze-reveal.mobile-menu--dragging, 35 | .mobile-menu--squeeze-reveal.mobile-menu--transitioning, 36 | .mobile-menu--squeeze-reveal.mobile-menu--open { 37 | display: block; 38 | z-index: unset; 39 | } 40 | .mobile-menu--ios .mobile-menu__mask, 41 | .mobile-menu--reveal .mobile-menu__mask, 42 | .mobile-menu--squeeze-reveal .mobile-menu__mask { 43 | z-index: 1; 44 | } 45 | .mobile-menu--ios.mobile-menu--open .mobile-menu__mask, 46 | .mobile-menu--reveal.mobile-menu--open .mobile-menu__mask, 47 | .mobile-menu--squeeze-reveal.mobile-menu--open .mobile-menu__mask { 48 | display: none; 49 | } 50 | 51 | .mobile-menu-wrapper--embedded .mobile-menu { 52 | position: absolute; 53 | } 54 | .mobile-menu-wrapper--embedded .mobile-menu__mask, 55 | .mobile-menu-wrapper--embedded .mobile-menu.mobile-menu--open, 56 | .mobile-menu-wrapper--embedded .mobile-menu.mobile-menu--transitioning, 57 | .mobile-menu-wrapper--embedded .mobile-menu.mobile-menu--dragging { 58 | width: 100%; 59 | } 60 | .mobile-menu-wrapper--embedded .mobile-menu, 61 | .mobile-menu-wrapper--embedded .mobile-menu__mask, 62 | .mobile-menu-wrapper--embedded .mobile-menu .mobile-menu__tray { 63 | height: var(--mobile-menu-height); 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # For every push to the master branch, this checks if the release-plan was 2 | # updated and if it was it will publish stable npm packages based on the 3 | # release plan 4 | 5 | name: Publish Stable 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | 14 | concurrency: 15 | group: publish-${{ github.head_ref || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | check-plan: 20 | name: "Check Release Plan" 21 | runs-on: ubuntu-latest 22 | outputs: 23 | command: ${{ steps.check-release.outputs.command }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | ref: 'master' 30 | # This will only cause the `check-plan` job to have a result of `success` 31 | # when the .release-plan.json file was changed on the last commit. This 32 | # plus the fact that this action only runs on main will be enough of a guard 33 | - id: check-release 34 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 35 | 36 | publish: 37 | name: "NPM Publish" 38 | runs-on: ubuntu-latest 39 | needs: check-plan 40 | if: needs.check-plan.outputs.command == 'release' 41 | permissions: 42 | contents: write 43 | pull-requests: write 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-node@v4 48 | with: 49 | node-version: 18 50 | # This creates an .npmrc that reads the NODE_AUTH_TOKEN environment variable 51 | registry-url: 'https://registry.npmjs.org' 52 | 53 | - uses: pnpm/action-setup@v2 54 | with: 55 | version: 10 56 | - run: pnpm install --frozen-lockfile 57 | - name: npm publish 58 | run: pnpm release-plan publish 59 | 60 | env: 61 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 62 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.0.0", 3 | "private": true, 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/nickschot/ember-mobile-menu" 7 | }, 8 | "workspaces": [ 9 | "ember-mobile-menu", 10 | "docs", 11 | "test-app" 12 | ], 13 | "scripts": { 14 | "build": "pnpm --filter ember-mobile-menu build", 15 | "changelog": "npx lerna-changelog --from=v0.1.0-alpha.5 > CHANGELOG.md", 16 | "lint": "pnpm --filter '!docs' lint", 17 | "lint:fix": "pnpm --filter '!docs' lint:fix", 18 | "prepare": "pnpm build", 19 | "start": "concurrently 'pnpm:start:*' --restart-after 5000 --prefixColors auto", 20 | "start:addon": "pnpm --filter ember-mobile-menu start --no-watch.clearScreen", 21 | "start:docs": "pnpm --filter docs start", 22 | "start:test-app": "pnpm --filter test-app start", 23 | "test": "pnpm --filter '*' test", 24 | "test:ember": "pnpm --filter '*' test:ember" 25 | }, 26 | "pnpm": { 27 | "overrides": { 28 | "@handlebars/parser": "2.0.0", 29 | "@ember/test-helpers": "5.4.1", 30 | "@ember/test-waiters": "4.1.1" 31 | }, 32 | "patchedDependencies": { 33 | "ember-app-scheduler": "patches/ember-app-scheduler.patch" 34 | }, 35 | "packageExtensions": { 36 | "ember-app-scheduler": { 37 | "dependencies": { 38 | "@ember/test-waiters": "*", 39 | "ember-auto-import": "*" 40 | } 41 | }, 42 | "ember-tether": { 43 | "peerDependencies": { 44 | "ember-source": "*" 45 | } 46 | }, 47 | "@ember/render-modifiers": { 48 | "peerDependencies": { 49 | "ember-source": "*" 50 | } 51 | } 52 | } 53 | }, 54 | "devDependencies": { 55 | "concurrently": "9.2.1", 56 | "release-plan": "0.17.2", 57 | "prettier": "3.6.2", 58 | "prettier-plugin-ember-template-tag": "2.1.0" 59 | }, 60 | "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd", 61 | "publishConfig": { 62 | "registry": "https://registry.npmjs.org" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/app/templates/docs/components/menu.md: -------------------------------------------------------------------------------- 1 | # Mobile Menu 2 | This component is yielded from `mobile-menu-wrapper`. It represents a menu instance. 3 | 4 | ## Modes 5 | A menu component takes an `@mode` argument which represents how it will function. 6 | 7 | | Mode | Primary use case | Description | 8 | | ---------------- | --------------------- | --------------------------------------------------------------------------- | 9 | | `default` | mobile, small screens | Default overlay menu. | 10 | | `push` | mobile | Pushes the content. | 11 | | `reveal` | mobile | Content is dragged away, revealing the menu. | 12 | | `ios` | mobile | Similar to `reveal` but the menu has 1/3 the translation of the user's pan. | 13 | | `squeeze` | tablet, desktop | A push style menu which squeezes the content keeping everything in view. | 14 | | `squeeze-reveal` | tablet, desktop | Similar to `squeeze` but with a `reveal` style menu. | 15 | 16 | If you have two menu instances `left` and `right` they can have distinct modes. 17 | 18 | ## Shadow 19 | The `@shadowEnabled` argument enables or disables a dynamic shadow, which gets stronger as the menu is opened, for menu's which "overlay" the content. These are `default`, `squeeze` and `push` modes. For the other modes this argument will apply a fixed shadow to the `Content` component. 20 | 21 | ## Mask 22 | The `@maskEnabled` argument enables or disables a mask which will overlay the content. By default it's opacity is linked to the current progress of the pan. The background color of the mask can be set through CSS. Clicking the mask will close the menu. 23 | 24 | ## Menu width 25 | The width of the menu can be controller by two arguments: `@width` and `@maxWidth`. `@width` is as a percentage of the width of the `` component. `@maxWidth` is a maximum width passed in as pixels. It can be set to -1 to disable the maximum width. This can be used to for example create a full width iOS style menu. 26 | -------------------------------------------------------------------------------- /test-app/tests/integration/components/mobile-menu-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | mobile-menu', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | hooks.before(function () { 10 | this.emptyObject = {}; 11 | this.register = () => {}; 12 | this.unregister = () => {}; 13 | }); 14 | 15 | test('it renders', async function (assert) { 16 | await render( 17 | hbs``, 18 | ); 19 | 20 | assert.strictEqual(this.element.textContent.trim(), ''); 21 | 22 | // Template block usage: 23 | await render(hbs` 24 | 25 | template block text 26 | 27 | `); 28 | 29 | assert.strictEqual(this.element.textContent.trim(), 'template block text'); 30 | }); 31 | 32 | test('it is a left menu by default', async function (assert) { 33 | await render( 34 | hbs``, 35 | ); 36 | assert.dom('.mobile-menu').hasClass('mobile-menu--left'); 37 | }); 38 | 39 | test(`it is a right menu if type is 'right'`, async function (assert) { 40 | await render( 41 | hbs``, 42 | ); 43 | assert.dom('.mobile-menu').hasClass('mobile-menu--right'); 44 | }); 45 | 46 | test(`it adds a mask by default`, async function (assert) { 47 | await render( 48 | hbs``, 49 | ); 50 | assert.dom('.mobile-menu__mask').exists({ count: 1 }); 51 | }); 52 | 53 | test(`it doesn't add a mask by if maskEnabled=false`, async function (assert) { 54 | await render( 55 | hbs` @parentBoundingClientRect={{hash}}`, 56 | ); 57 | assert.dom('.mobile-menu__mask').doesNotExist(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test-app/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Debugging: 3 | * https://eslint.org/docs/latest/use/configure/debug 4 | * ---------------------------------------------------- 5 | * 6 | * Print a file's calculated configuration 7 | * 8 | * npx eslint --print-config path/to/file.js 9 | * 10 | * Inspecting the config 11 | * 12 | * npx eslint --inspect-config 13 | * 14 | */ 15 | import globals from 'globals'; 16 | import js from '@eslint/js'; 17 | 18 | import ember from 'eslint-plugin-ember/recommended'; 19 | import eslintConfigPrettier from 'eslint-config-prettier'; 20 | import qunit from 'eslint-plugin-qunit'; 21 | import n from 'eslint-plugin-n'; 22 | 23 | import babelParser from '@babel/eslint-parser'; 24 | 25 | const esmParserOptions = { 26 | ecmaFeatures: { modules: true }, 27 | ecmaVersion: 'latest', 28 | }; 29 | 30 | export default [ 31 | js.configs.recommended, 32 | eslintConfigPrettier, 33 | ember.configs.base, 34 | ember.configs.gjs, 35 | /** 36 | * Ignores must be in their own object 37 | * https://eslint.org/docs/latest/use/configure/ignore 38 | */ 39 | { 40 | ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], 41 | }, 42 | /** 43 | * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options 44 | */ 45 | { 46 | linterOptions: { 47 | reportUnusedDisableDirectives: 'error', 48 | }, 49 | }, 50 | { 51 | files: ['**/*.js'], 52 | languageOptions: { 53 | parser: babelParser, 54 | }, 55 | }, 56 | { 57 | files: ['**/*.{js,gjs}'], 58 | languageOptions: { 59 | parserOptions: esmParserOptions, 60 | globals: { 61 | ...globals.browser, 62 | }, 63 | }, 64 | }, 65 | { 66 | files: ['tests/**/*-test.{js,gjs}'], 67 | plugins: { 68 | qunit, 69 | }, 70 | }, 71 | /** 72 | * CJS node files 73 | */ 74 | { 75 | files: [ 76 | '**/*.cjs', 77 | 'config/**/*.js', 78 | 'testem.js', 79 | 'testem*.js', 80 | '.prettierrc.js', 81 | '.stylelintrc.js', 82 | '.template-lintrc.js', 83 | 'ember-cli-build.js', 84 | ], 85 | plugins: { 86 | n, 87 | }, 88 | 89 | languageOptions: { 90 | sourceType: 'script', 91 | ecmaVersion: 'latest', 92 | globals: { 93 | ...globals.node, 94 | }, 95 | }, 96 | }, 97 | /** 98 | * ESM node files 99 | */ 100 | { 101 | files: ['**/*.mjs'], 102 | plugins: { 103 | n, 104 | }, 105 | 106 | languageOptions: { 107 | sourceType: 'module', 108 | ecmaVersion: 'latest', 109 | parserOptions: esmParserOptions, 110 | globals: { 111 | ...globals.node, 112 | }, 113 | }, 114 | }, 115 | ]; 116 | -------------------------------------------------------------------------------- /ember-mobile-menu/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Debugging: 3 | * https://eslint.org/docs/latest/use/configure/debug 4 | * ---------------------------------------------------- 5 | * 6 | * Print a file's calculated configuration 7 | * 8 | * npx eslint --print-config path/to/file.js 9 | * 10 | * Inspecting the config 11 | * 12 | * npx eslint --inspect-config 13 | * 14 | */ 15 | import babelParser from '@babel/eslint-parser'; 16 | import js from '@eslint/js'; 17 | import prettier from 'eslint-config-prettier'; 18 | import ember from 'eslint-plugin-ember/recommended'; 19 | import importPlugin from 'eslint-plugin-import'; 20 | import n from 'eslint-plugin-n'; 21 | import globals from 'globals'; 22 | 23 | const esmParserOptions = { 24 | ecmaFeatures: { modules: true }, 25 | ecmaVersion: 'latest', 26 | }; 27 | 28 | export default [ 29 | js.configs.recommended, 30 | prettier, 31 | ember.configs.base, 32 | ember.configs.gjs, 33 | /** 34 | * Ignores must be in their own object 35 | * https://eslint.org/docs/latest/use/configure/ignore 36 | */ 37 | { 38 | ignores: ['dist/', 'declarations/', 'node_modules/', 'coverage/', '!**/.*'], 39 | }, 40 | /** 41 | * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options 42 | */ 43 | { 44 | linterOptions: { 45 | reportUnusedDisableDirectives: 'error', 46 | }, 47 | }, 48 | { 49 | files: ['**/*.js'], 50 | languageOptions: { 51 | parser: babelParser, 52 | }, 53 | }, 54 | { 55 | files: ['**/*.{js,gjs}'], 56 | languageOptions: { 57 | parserOptions: esmParserOptions, 58 | globals: { 59 | ...globals.browser, 60 | }, 61 | }, 62 | }, 63 | { 64 | files: ['src/**/*'], 65 | plugins: { 66 | import: importPlugin, 67 | }, 68 | rules: { 69 | // require relative imports use full extensions 70 | 'import/extensions': ['error', 'always', { ignorePackages: true }], 71 | }, 72 | }, 73 | /** 74 | * CJS node files 75 | */ 76 | { 77 | files: [ 78 | '**/*.cjs', 79 | '.prettierrc.js', 80 | '.stylelintrc.js', 81 | '.template-lintrc.js', 82 | 'addon-main.cjs', 83 | ], 84 | plugins: { 85 | n, 86 | }, 87 | 88 | languageOptions: { 89 | sourceType: 'script', 90 | ecmaVersion: 'latest', 91 | globals: { 92 | ...globals.node, 93 | }, 94 | }, 95 | }, 96 | /** 97 | * ESM node files 98 | */ 99 | { 100 | files: ['**/*.mjs'], 101 | plugins: { 102 | n, 103 | }, 104 | 105 | languageOptions: { 106 | sourceType: 'module', 107 | ecmaVersion: 'latest', 108 | parserOptions: esmParserOptions, 109 | globals: { 110 | ...globals.node, 111 | }, 112 | }, 113 | }, 114 | ]; 115 | -------------------------------------------------------------------------------- /ember-mobile-menu/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import copy from 'rollup-plugin-copy'; 3 | import { Addon } from '@embroider/addon-dev/rollup'; 4 | 5 | const addon = new Addon({ 6 | srcDir: 'src', 7 | destDir: 'dist', 8 | }); 9 | 10 | export default { 11 | // This provides defaults that work well alongside `publicEntrypoints` below. 12 | // You can augment this if you need to. 13 | output: addon.output(), 14 | 15 | plugins: [ 16 | // These are the modules that users should be able to import from your 17 | // addon. Anything not listed here may get optimized away. 18 | // By default all your JavaScript modules (**/*.js) will be importable. 19 | // But you are encouraged to tweak this to only cover the modules that make 20 | // up your addon's public API. Also make sure your package.json#exports 21 | // is aligned to the config here. 22 | // See https://github.com/embroider-build/embroider/blob/main/docs/v2-faq.md#how-can-i-define-the-public-exports-of-my-addon 23 | addon.publicEntrypoints(['**/*.js', 'index.js']), 24 | 25 | // These are the modules that should get reexported into the traditional 26 | // "app" tree. Things in here should also be in publicEntrypoints above, but 27 | // not everything in publicEntrypoints necessarily needs to go here. 28 | addon.appReexports([ 29 | 'components/**/*.js', 30 | 'helpers/**/*.js', 31 | 'modifiers/**/*.js', 32 | 'services/**/*.js', 33 | ]), 34 | 35 | // Follow the V2 Addon rules about dependencies. Your code can import from 36 | // `dependencies` and `peerDependencies` as well as standard Ember-provided 37 | // package names. 38 | addon.dependencies(), 39 | 40 | // This babel config should *not* apply presets or compile away ES modules. 41 | // It exists only to provide development niceties for you, like automatic 42 | // template colocation. 43 | // 44 | // By default, this will load the actual babel config from the file 45 | // babel.config.json. 46 | babel({ 47 | extensions: ['.js', '.gjs'], 48 | babelHelpers: 'bundled', 49 | }), 50 | 51 | // Ensure that standalone .hbs files are properly integrated as Javascript. 52 | addon.hbs(), 53 | 54 | // Ensure that .gjs files are properly integrated as Javascript 55 | addon.gjs(), 56 | 57 | // addons are allowed to contain imports of .css files, which we want rollup 58 | // to leave alone and keep in the published output. 59 | addon.keepAssets(['**/*.css']), 60 | 61 | // Remove leftover build artifacts when starting a new build. 62 | addon.clean(), 63 | 64 | // Copy Readme and License into published package 65 | copy({ 66 | targets: [ 67 | { src: '../README.md', dest: '.' }, 68 | { src: '../LICENSE.md', dest: '.' }, 69 | { src: './src/themes/**/*.css', dest: './dist' }, 70 | ], 71 | }), 72 | ], 73 | }; 74 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu/mask.gjs: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { htmlSafe } from '@ember/template'; 3 | import './mask.css'; 4 | import { on } from '@ember/modifier'; 5 | import didPan from 'ember-gesture-modifiers/modifiers/did-pan'; 6 | 7 | const _fn = () => {}; 8 | 9 | /** 10 | * A mask component. 11 | * 12 | * You usually do not need to use this component yourself. Whether or not a mask is present can be set by passing an 13 | * argument to the MobileMenu component. 14 | * 15 | * @class Mask 16 | * @private 17 | */ 18 | export default class MaskComponent extends Component { 19 | /** 20 | * Offset (or "deadzone") used when calculating what opacity the mask should 21 | * currently be. 22 | * 23 | * Example: the default value is 0.1. This means the mask will only become 24 | * visible after the position is over 10% of the screen width. 25 | * 26 | * @argument maskOpacityOffset 27 | * @type number 28 | * @default 0.1 29 | */ 30 | get maskOpacityOffset() { 31 | return this.args.maskOpacityOffset ?? 0.1; 32 | } 33 | 34 | /** 35 | * @argument invertOpacity 36 | * @type boolean 37 | * @default undefined 38 | * @protected 39 | */ 40 | 41 | /** 42 | * @argument isOpen 43 | * @type boolean 44 | * @default false 45 | * @protected 46 | */ 47 | get isOpen() { 48 | return this.args.isOpen ?? false; 49 | } 50 | 51 | /** 52 | * @argument position 53 | * @type number 54 | * @default 0 55 | * @protected 56 | */ 57 | get position() { 58 | return this.args.position ?? 0; 59 | } 60 | 61 | /** 62 | * @argument onClick 63 | * @type function 64 | * @default function(){} 65 | * @protected 66 | */ 67 | get onClick() { 68 | return this.args.onClick ?? _fn; 69 | } 70 | 71 | get style() { 72 | let style = ''; 73 | 74 | style += 75 | !this.isOpen && this.position === 0 76 | ? 'visibility: hidden;' 77 | : 'visibility: visible;'; 78 | 79 | let opacity = 80 | this.position > this.maskOpacityOffset 81 | ? (this.position - this.maskOpacityOffset) / 82 | (1 - this.maskOpacityOffset) 83 | : 0; 84 | 85 | if (this.args.invertOpacity) { 86 | opacity = 1 - opacity; 87 | } 88 | 89 | style += `opacity: ${opacity};`; 90 | 91 | return htmlSafe(style); 92 | } 93 | 94 | 111 | } 112 | -------------------------------------------------------------------------------- /docs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Debugging: 3 | * https://eslint.org/docs/latest/use/configure/debug 4 | * ---------------------------------------------------- 5 | * 6 | * Print a file's calculated configuration 7 | * 8 | * npx eslint --print-config path/to/file.js 9 | * 10 | * Inspecting the config 11 | * 12 | * npx eslint --inspect-config 13 | * 14 | */ 15 | import globals from 'globals'; 16 | import js from '@eslint/js'; 17 | 18 | import ember from 'eslint-plugin-ember/recommended'; 19 | import prettier from 'eslint-plugin-prettier/recommended'; 20 | import qunit from 'eslint-plugin-qunit'; 21 | import n from 'eslint-plugin-n'; 22 | 23 | import babelParser from '@babel/eslint-parser'; 24 | 25 | const esmParserOptions = { 26 | ecmaFeatures: { modules: true }, 27 | ecmaVersion: 'latest', 28 | requireConfigFile: false, 29 | babelOptions: { 30 | plugins: [ 31 | ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], 32 | ], 33 | }, 34 | }; 35 | 36 | export default [ 37 | js.configs.recommended, 38 | prettier, 39 | ember.configs.base, 40 | ember.configs.gjs, 41 | /** 42 | * Ignores must be in their own object 43 | * https://eslint.org/docs/latest/use/configure/ignore 44 | */ 45 | { 46 | ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], 47 | }, 48 | /** 49 | * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options 50 | */ 51 | { 52 | linterOptions: { 53 | reportUnusedDisableDirectives: 'error', 54 | }, 55 | }, 56 | { 57 | files: ['**/*.js'], 58 | languageOptions: { 59 | parser: babelParser, 60 | }, 61 | }, 62 | { 63 | files: ['**/*.{js,gjs}'], 64 | languageOptions: { 65 | parserOptions: esmParserOptions, 66 | globals: { 67 | ...globals.browser, 68 | }, 69 | }, 70 | }, 71 | { 72 | files: ['tests/**/*-test.{js,gjs}'], 73 | plugins: { 74 | qunit, 75 | }, 76 | }, 77 | /** 78 | * CJS node files 79 | */ 80 | { 81 | files: [ 82 | '**/*.cjs', 83 | 'config/**/*.js', 84 | 'testem.js', 85 | 'testem*.js', 86 | '.prettierrc.js', 87 | '.stylelintrc.js', 88 | '.template-lintrc.js', 89 | 'ember-cli-build.js', 90 | ], 91 | plugins: { 92 | n, 93 | }, 94 | 95 | languageOptions: { 96 | sourceType: 'script', 97 | ecmaVersion: 'latest', 98 | globals: { 99 | ...globals.node, 100 | }, 101 | }, 102 | }, 103 | /** 104 | * ESM node files 105 | */ 106 | { 107 | files: ['**/*.mjs'], 108 | plugins: { 109 | n, 110 | }, 111 | 112 | languageOptions: { 113 | sourceType: 'module', 114 | ecmaVersion: 'latest', 115 | parserOptions: esmParserOptions, 116 | globals: { 117 | ...globals.node, 118 | }, 119 | }, 120 | }, 121 | ]; 122 | -------------------------------------------------------------------------------- /.github/workflows/plan-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Plan Review 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | types: 9 | - labeled 10 | 11 | concurrency: 12 | group: plan-release # only the latest one of these should ever be running 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | check-plan: 17 | name: "Check Release Plan" 18 | runs-on: ubuntu-latest 19 | outputs: 20 | command: ${{ steps.check-release.outputs.command }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | ref: 'master' 27 | # This will only cause the `check-plan` job to have a "command" of `release` 28 | # when the .release-plan.json file was changed on the last commit. 29 | - id: check-release 30 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 31 | 32 | prepare_release_notes: 33 | name: Prepare Release Notes 34 | runs-on: ubuntu-latest 35 | timeout-minutes: 5 36 | needs: check-plan 37 | permissions: 38 | contents: write 39 | pull-requests: write 40 | outputs: 41 | explanation: ${{ steps.explanation.outputs.text }} 42 | # only run on push event if plan wasn't updated (don't create a release plan when we're releasing) 43 | # only run on labeled event if the PR has already been merged 44 | if: (github.event_name == 'push' && needs.check-plan.outputs.command != 'release') || (github.event_name == 'pull_request' && github.event.pull_request.merged == true) 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | # We need to download lots of history so that 49 | # lerna-changelog can discover what's changed since the last release 50 | with: 51 | fetch-depth: 0 52 | - uses: actions/setup-node@v4 53 | with: 54 | node-version: 18 55 | 56 | - uses: pnpm/action-setup@v2 57 | with: 58 | version: 10 59 | - run: pnpm install --frozen-lockfile 60 | 61 | - name: "Generate Explanation and Prep Changelogs" 62 | id: explanation 63 | run: | 64 | set -x 65 | 66 | pnpm release-plan prepare 67 | 68 | echo 'text<> $GITHUB_OUTPUT 69 | jq .description .release-plan.json -r >> $GITHUB_OUTPUT 70 | echo 'EOF' >> $GITHUB_OUTPUT 71 | env: 72 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - uses: peter-evans/create-pull-request@v7 75 | with: 76 | commit-message: "Prepare Release using 'release-plan'" 77 | labels: "internal" 78 | branch: release-preview 79 | title: Prepare Release 80 | body: | 81 | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 82 | 83 | ----------------------------------------- 84 | 85 | ${{ steps.explanation.outputs.text }} 86 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "4.0.0", 4 | "private": true, 5 | "description": "Docs app for ember-mobile-menu.", 6 | "keywords": [], 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/nickschot/ember-mobile-menu" 10 | }, 11 | "license": "MIT", 12 | "author": "Nick Schot ", 13 | "directories": { 14 | "doc": "doc", 15 | "test": "tests" 16 | }, 17 | "scripts": { 18 | "build": "ember build --environment=production", 19 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 20 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 21 | "lint:hbs": "ember-template-lint .", 22 | "lint:hbs:fix": "ember-template-lint . --fix", 23 | "lint:js": "eslint . --cache", 24 | "lint:js:fix": "eslint . --fix", 25 | "start": "ember serve --port=4300", 26 | "test": "npm-run-all lint test:*", 27 | "test:ember": "ember test --port=4300", 28 | "test:ember-compatibility": "ember try:each --port=4300" 29 | }, 30 | "devDependencies": { 31 | "@babel/eslint-parser": "7.28.5", 32 | "@ember/optional-features": "2.2.0", 33 | "@ember/string": "4.0.1", 34 | "@ember/test-helpers": "5.4.1", 35 | "@ember/test-waiters": "4.1.1", 36 | "@eslint/js": "9.39.1", 37 | "@glimmer/component": "1.1.2", 38 | "@glimmer/tracking": "1.1.2", 39 | "broccoli-asset-rev": "3.0.0", 40 | "ember-auto-import": "2.11.2", 41 | "ember-cli": "6.8.0", 42 | "ember-cli-addon-docs": "8.0.8", 43 | "ember-cli-addon-docs-yuidoc": "1.1.0", 44 | "ember-cli-babel": "8.2.0", 45 | "ember-cli-dependency-checker": "3.3.3", 46 | "ember-cli-deploy": "2.0.0", 47 | "ember-cli-deploy-build": "3.0.0", 48 | "ember-cli-deploy-git": "1.3.4", 49 | "ember-cli-deploy-git-ci": "1.0.1", 50 | "ember-cli-htmlbars": "6.3.0", 51 | "ember-cli-inject-live-reload": "2.1.0", 52 | "ember-cli-sass": "11.0.1", 53 | "ember-cli-sri": "2.1.1", 54 | "ember-cli-terser": "4.0.2", 55 | "ember-data": "5.8.0", 56 | "ember-disable-prototype-extensions": "1.1.3", 57 | "ember-load-initializers": "3.0.1", 58 | "ember-mobile-menu": "workspace:*", 59 | "ember-page-title": "9.0.3", 60 | "ember-qunit": "8.1.1", 61 | "ember-resolver": "13.1.1", 62 | "ember-source": "6.8.2", 63 | "ember-source-channel-url": "3.0.0", 64 | "ember-template-lint": "7.9.3", 65 | "ember-try": "4.0.0", 66 | "eslint": "9.39.1", 67 | "eslint-config-prettier": "10.1.8", 68 | "eslint-plugin-ember": "12.7.5", 69 | "eslint-plugin-n": "17.23.1", 70 | "eslint-plugin-prettier": "5.5.4", 71 | "eslint-plugin-qunit": "8.2.5", 72 | "globals": "16.5.0", 73 | "loader.js": "4.7.0", 74 | "npm-run-all2": "8.0.4", 75 | "prettier": "3.6.2", 76 | "qunit": "2.24.3", 77 | "qunit-dom": "3.5.0", 78 | "sass": "1.93.3", 79 | "tracked-built-ins": "4.0.0", 80 | "webpack": "5.102.1" 81 | }, 82 | "engines": { 83 | "node": "18.* || >= 20" 84 | }, 85 | "ember": { 86 | "edition": "octane" 87 | }, 88 | "homepage": "https://nickschot.github.io/ember-mobile-menu" 89 | } 90 | -------------------------------------------------------------------------------- /docs/app/templates/docs/components/menu-wrapper.md: -------------------------------------------------------------------------------- 1 | # Mobile Menu Wrapper 2 | This component manages the state of the menus and does the initial pan recognition. It is the main entry point for using this addon. 3 | 4 | By default it is set up to detect a pan from respectively the left or the right edge depending on the chosen menu(s). 5 | 6 | ```handlebars 7 | 8 | 9 | Home 10 | 11 | 12 | 13 | Menu 14 | 15 | 16 | ``` 17 | 18 | ## Open detection width 19 | The `@openDetectionWidth` argument controls the size in px of the area that will be used for dragging from an "edge" of the content. If set to `-1` the full width of the content can be used to drag open the menu. 20 | 21 | ```handlebars 22 | 23 | ... 24 | 25 | ``` 26 | 27 | ## Left & Right menus 28 | By default the menu is setup to be a left menu. By passing `type=right` to the menu you can make the menu slide in from the right. 29 | 30 | ```handlebars 31 | 32 | 33 | Home 34 | 35 | 36 | 37 | Menu 38 | 39 | 40 | ``` 41 | 42 | ## Multiple menus 43 | You can also use both a left and a right menu. A `target` option is available on the toggle component to target a specific menu (defaults to `left` or the only available menu if there is just one). 44 | 45 | ```handlebars 46 | 47 | 48 | Home 49 | 50 | 51 | 52 | Left Menu 53 | Right Menu 54 | 55 | 56 | 57 | Home 58 | 59 | 60 | ``` 61 | 62 | ## Embedded menu 63 | The menu can also be used embedded on a page by passing `embed=true` to the `` component. This means the menus will stay within the boundaries of the `` component which can be useful for more complicated desktop layouts. 64 | 65 | If a menu is _not_ embedded, the assumption is made that the `Content` component takes the _full width_ of the viewport. 66 | 67 | 68 | 69 | 70 | 71 | Home 72 | 73 | 74 | 75 | Menu 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Test app for ember-mobile-menu addon", 6 | "repository": "", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "vite build", 15 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", 16 | "lint:css": "stylelint \"**/*.css\"", 17 | "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"", 18 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto", 19 | "lint:hbs": "ember-template-lint .", 20 | "lint:hbs:fix": "ember-template-lint . --fix", 21 | "lint:js": "eslint . --cache", 22 | "lint:js:fix": "eslint . --fix", 23 | "start": "vite", 24 | "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\" --prefixColors auto", 25 | "test:ember": "vite build --mode development && ember test --path dist" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "7.28.5", 29 | "@babel/eslint-parser": "7.28.5", 30 | "@babel/plugin-transform-runtime": "7.28.5", 31 | "@babel/plugin-transform-typescript": "7.28.5", 32 | "@ember/optional-features": "2.2.0", 33 | "@ember/string": "4.0.1", 34 | "@ember/test-helpers": "5.4.1", 35 | "@ember/test-waiters": "4.1.1", 36 | "@embroider/compat": "4.1.10", 37 | "@embroider/config-meta-loader": "1.0.0", 38 | "@embroider/core": "4.2.8", 39 | "@embroider/test-setup": "4.0.0", 40 | "@embroider/vite": "1.4.2", 41 | "@eslint/js": "9.39.1", 42 | "@glimmer/component": "2.0.0", 43 | "@rollup/plugin-babel": "6.1.0", 44 | "babel-plugin-ember-template-compilation": "2.4.1", 45 | "concurrently": "9.2.1", 46 | "decorator-transforms": "2.3.0", 47 | "ember-cli": "6.8.0", 48 | "ember-cli-babel": "8.2.0", 49 | "ember-cli-htmlbars": "6.3.0", 50 | "ember-functions-as-helper-polyfill": "2.1.3", 51 | "ember-load-initializers": "3.0.1", 52 | "ember-mobile-menu": "workspace:*", 53 | "ember-modifier": "4.2.2", 54 | "ember-qunit": "9.0.4", 55 | "ember-resolver": "13.1.1", 56 | "ember-source": "6.8.2", 57 | "ember-source-channel-url": "3.0.0", 58 | "ember-template-lint": "7.9.3", 59 | "ember-try": "4.0.0", 60 | "eslint": "9.39.1", 61 | "eslint-config-prettier": "10.1.8", 62 | "eslint-plugin-ember": "12.7.5", 63 | "eslint-plugin-n": "17.23.1", 64 | "eslint-plugin-prettier": "5.5.4", 65 | "eslint-plugin-qunit": "8.2.5", 66 | "globals": "16.5.0", 67 | "prettier": "3.6.2", 68 | "prettier-plugin-ember-template-tag": "2.1.0", 69 | "qunit": "2.24.3", 70 | "qunit-dom": "3.5.0", 71 | "stylelint": "16.25.0", 72 | "stylelint-config-standard": "38.0.0", 73 | "stylelint-prettier": "5.0.3", 74 | "tracked-built-ins": "4.0.0", 75 | "vite": "7.2.6" 76 | }, 77 | "engines": { 78 | "node": ">= 18" 79 | }, 80 | "ember": { 81 | "edition": "octane" 82 | }, 83 | "ember-addon": { 84 | "type": "app", 85 | "version": 2 86 | }, 87 | "exports": { 88 | "./tests/*": "./tests/*", 89 | "./*": "./app/*" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.release-plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "ember-mobile-menu": { 4 | "impact": "major", 5 | "oldVersion": "5.3.0", 6 | "newVersion": "6.0.0", 7 | "tagName": "latest", 8 | "constraints": [ 9 | { 10 | "impact": "major", 11 | "reason": "Appears in changelog section :boom: Breaking Change" 12 | }, 13 | { 14 | "impact": "minor", 15 | "reason": "Appears in changelog section :rocket: Enhancement" 16 | }, 17 | { 18 | "impact": "patch", 19 | "reason": "Appears in changelog section :house: Internal" 20 | } 21 | ], 22 | "pkgJSONPath": "./ember-mobile-menu/package.json" 23 | } 24 | }, 25 | "description": "## Release (2025-12-01)\n\n* ember-mobile-menu 6.0.0 (major)\n\n#### :boom: Breaking Change\n* `ember-mobile-menu`\n * [#1160](https://github.com/nickschot/ember-mobile-menu/pull/1160) Remove @ember/render-modifiers ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :rocket: Enhancement\n* `ember-mobile-menu`\n * [#1156](https://github.com/nickschot/ember-mobile-menu/pull/1156) Remove v1 addons: test-waiters and cached-decorator-polyfill ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n * [#1270](https://github.com/nickschot/ember-mobile-menu/pull/1270) Replace ember-on-resize-modifier with ember-primitives (v2) version ([@nickschot](https://github.com/nickschot))\n * [#1269](https://github.com/nickschot/ember-mobile-menu/pull/1269) Replace ember-set-body-class with ember-primitives (v2) version ([@nickschot](https://github.com/nickschot))\n * [#1245](https://github.com/nickschot/ember-mobile-menu/pull/1245) Deprecate decorator syntax of ember-concurrency ([@johanrd](https://github.com/johanrd))\n * [#1158](https://github.com/nickschot/ember-mobile-menu/pull/1158) Remove unneeded peers ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n* Other\n * [#1310](https://github.com/nickschot/ember-mobile-menu/pull/1310) Add 6.4 and 6.8 to LTS testing ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :memo: Documentation\n* [#1244](https://github.com/nickschot/ember-mobile-menu/pull/1244) Update CONTRIBUTING.md to reflect practice in v2 addon ([@johanrd](https://github.com/johanrd))\n\n#### :house: Internal\n* `ember-mobile-menu`\n * [#1303](https://github.com/nickschot/ember-mobile-menu/pull/1303) Update test-app to Vite ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n* Other\n * [#1161](https://github.com/nickschot/ember-mobile-menu/pull/1161) Fix floating dependencies test ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n * [#1129](https://github.com/nickschot/ember-mobile-menu/pull/1129) Docs smoke test & lint config fixes ([@nickschot](https://github.com/nickschot))\n * [#1128](https://github.com/nickschot/ember-mobile-menu/pull/1128) Fix addon-docs deployment ([@nickschot](https://github.com/nickschot))\n * [#1124](https://github.com/nickschot/ember-mobile-menu/pull/1124) Get rid of dependenciesMeta injected and use pnpm flags only when needed in CI ([@nickschot](https://github.com/nickschot))\n\n#### Committers: 3\n- Nick Schot ([@nickschot](https://github.com/nickschot))\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n- [@johanrd](https://github.com/johanrd)\n" 26 | } 27 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu/tray.gjs: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { htmlSafe } from '@ember/template'; 3 | import { modifier as eModifier } from 'ember-modifier'; 4 | import { 5 | disableBodyScroll, 6 | enableBodyScroll, 7 | } from '../../utils/body-scroll-lock.js'; 8 | import './tray.css'; 9 | import didPan from 'ember-gesture-modifiers/modifiers/did-pan'; 10 | 11 | /** 12 | * The tray that resides within the menu. Menu content is placed in here. 13 | * 14 | * You usually do not need to use this component yourself. 15 | * 16 | * @class Tray 17 | * @private 18 | * @hide 19 | */ 20 | export default class TrayComponent extends Component { 21 | /** 22 | * Width of the menu in px. 23 | * 24 | * @property width 25 | * @type Number 26 | * @default 300 27 | * @private 28 | */ 29 | get width() { 30 | return this.args.width ?? 300; 31 | } 32 | 33 | /** 34 | * Whether the menu is a left menu (otherwise it's a right menu) 35 | * 36 | * @property isLeft 37 | * @type Boolean 38 | * @default true 39 | * @private 40 | */ 41 | get isLeft() { 42 | return this.args.isLeft ?? true; 43 | } 44 | 45 | /** 46 | * Current relative position of the menu in px. 47 | * 48 | * @property position 49 | * @type Number 50 | * @default 0 51 | * @private 52 | */ 53 | get position() { 54 | return this.args.position ?? 0; 55 | } 56 | 57 | get progress() { 58 | return Math.abs(this.position) / this.width; 59 | } 60 | 61 | get style() { 62 | let style = `width: ${this.width}px;`; 63 | 64 | let offset = this.width; 65 | let translation = this.position; 66 | if (this.args.mode === 'ios') { 67 | offset /= 3; 68 | translation /= 3; 69 | } else if (['reveal', 'squeeze-reveal'].includes(this.args.mode)) { 70 | offset = 0; 71 | translation = 0; 72 | } 73 | 74 | style += this.isLeft 75 | ? `left: -${offset}px; transform: translateX(${translation}px);` 76 | : `right: -${offset}px; transform: translateX(${translation}px);`; 77 | 78 | if ( 79 | this.args.shadowEnabled && 80 | ['default', 'push', 'squeeze'].includes(this.args.mode) && 81 | this.progress > 0 82 | ) { 83 | style += `box-shadow: 0 0 10px rgba(0,0,0,${0.3 * this.progress});`; 84 | } 85 | 86 | return htmlSafe(style); 87 | } 88 | 89 | lockBodyScroll = eModifier((element) => { 90 | let { isClosed, preventScroll, embed } = this.args; 91 | 92 | if (preventScroll && !embed) { 93 | if (isClosed) { 94 | enableBodyScroll(element); 95 | } else { 96 | disableBodyScroll(element); 97 | } 98 | } 99 | 100 | return () => { 101 | if (preventScroll && !embed) { 102 | enableBodyScroll(element); 103 | } 104 | }; 105 | }); 106 | 107 | 124 | } 125 | -------------------------------------------------------------------------------- /ember-mobile-menu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-mobile-menu", 3 | "version": "6.0.0", 4 | "description": "A draggable sidebar menu for Ember.", 5 | "keywords": [ 6 | "ember-addon", 7 | "mobile", 8 | "menu", 9 | "sidebar", 10 | "hamburger-menu" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/nickschot/ember-mobile-menu" 15 | }, 16 | "license": "MIT", 17 | "author": "Nick Schot ", 18 | "homepage": "https://nickschot.github.io/ember-mobile-menu", 19 | "exports": { 20 | ".": "./dist/index.js", 21 | "./*": "./dist/*.js", 22 | "./addon-main.js": "./addon-main.cjs", 23 | "./themes/*": "./dist/*.css" 24 | }, 25 | "files": [ 26 | "addon-main.cjs", 27 | "declarations", 28 | "dist" 29 | ], 30 | "scripts": { 31 | "build": "rollup --config", 32 | "format": "prettier . --cache --write", 33 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", 34 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm run format", 35 | "lint:format": "prettier . --cache --check", 36 | "lint:hbs": "ember-template-lint . --no-error-on-unmatched-pattern", 37 | "lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern", 38 | "lint:js": "eslint . --cache", 39 | "lint:js:fix": "eslint . --fix", 40 | "prepack": "rollup --config", 41 | "start": "rollup --config --watch", 42 | "test": "echo 'A v2 addon does not have tests, run tests in test-app'" 43 | }, 44 | "dependencies": { 45 | "@ember/test-waiters": "^3.0.0 || ^4.0.0", 46 | "@embroider/addon-shim": "^1.8.9", 47 | "@glimmer/component": "^1.0.4 || >=2.0.0", 48 | "decorator-transforms": "^2.2.2", 49 | "ember-concurrency": "^3.0.0 || ^4.0.0", 50 | "ember-gesture-modifiers": "^5.0.0 || ^6.0.0", 51 | "ember-modifier": "^4.2.2", 52 | "ember-primitives": "^0.46.0", 53 | "tracked-built-ins": "^3.0.0 || ^4.0.0", 54 | "wobble": "^1.5.1" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "7.28.5", 58 | "@babel/eslint-parser": "7.28.5", 59 | "@babel/runtime": "7.28.4", 60 | "@embroider/addon-dev": "8.1.1", 61 | "@eslint/js": "9.39.1", 62 | "@rollup/plugin-babel": "6.1.0", 63 | "babel-plugin-ember-template-compilation": "2.4.1", 64 | "concurrently": "9.2.1", 65 | "ember-source": "6.8.2", 66 | "ember-template-lint": "7.9.3", 67 | "eslint": "9.39.1", 68 | "eslint-config-prettier": "10.1.8", 69 | "eslint-plugin-ember": "12.7.5", 70 | "eslint-plugin-import": "2.32.0", 71 | "eslint-plugin-n": "17.23.1", 72 | "globals": "16.5.0", 73 | "prettier": "3.6.2", 74 | "prettier-plugin-ember-template-tag": "2.1.0", 75 | "rollup": "4.52.5", 76 | "rollup-plugin-copy": "3.5.0" 77 | }, 78 | "publishConfig": { 79 | "registry": "https://registry.npmjs.org" 80 | }, 81 | "ember": { 82 | "edition": "octane" 83 | }, 84 | "ember-addon": { 85 | "version": 2, 86 | "type": "addon", 87 | "main": "addon-main.cjs", 88 | "app-js": { 89 | "./components/mobile-menu-toggle.js": "./dist/_app_/components/mobile-menu-toggle.js", 90 | "./components/mobile-menu-wrapper.js": "./dist/_app_/components/mobile-menu-wrapper.js", 91 | "./components/mobile-menu-wrapper/content.js": "./dist/_app_/components/mobile-menu-wrapper/content.js", 92 | "./components/mobile-menu.js": "./dist/_app_/components/mobile-menu.js", 93 | "./components/mobile-menu/mask.js": "./dist/_app_/components/mobile-menu/mask.js", 94 | "./components/mobile-menu/tray.js": "./dist/_app_/components/mobile-menu/tray.js", 95 | "./components/utils.js": "./dist/_app_/components/utils.js" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test-app/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | usePnpm: true, 9 | scenarios: [ 10 | { 11 | name: 'ember-lts-3.28', 12 | npm: { 13 | devDependencies: { 14 | 'ember-cli': '~4.12.0', 15 | 'ember-source': '~3.28.0', 16 | 'babel-plugin-ember-polyfill-get-and-set-owner-from-ember-owner': 17 | '^1.0.0', 18 | 'babel-plugin-undeprecate-inject-from-at-ember-service': '^1.0.0', 19 | }, 20 | }, 21 | env: { 22 | NEEDS_OWNER_POLYFILL: 'true', 23 | NEEDS_SERVICE_POLYFILL: 'true', 24 | }, 25 | }, 26 | { 27 | name: 'ember-lts-4.4', 28 | npm: { 29 | devDependencies: { 30 | 'ember-source': '~4.4.0', 31 | 'babel-plugin-ember-polyfill-get-and-set-owner-from-ember-owner': 32 | '^1.0.0', 33 | 'babel-plugin-undeprecate-inject-from-at-ember-service': '^1.0.0', 34 | }, 35 | }, 36 | env: { 37 | NEEDS_OWNER_POLYFILL: 'true', 38 | NEEDS_SERVICE_POLYFILL: 'true', 39 | }, 40 | }, 41 | { 42 | name: 'ember-lts-4.8', 43 | npm: { 44 | devDependencies: { 45 | 'ember-source': '~4.8.0', 46 | 'babel-plugin-ember-polyfill-get-and-set-owner-from-ember-owner': 47 | '^1.0.0', 48 | 'babel-plugin-undeprecate-inject-from-at-ember-service': '^1.0.0', 49 | }, 50 | }, 51 | env: { 52 | NEEDS_OWNER_POLYFILL: 'true', 53 | NEEDS_SERVICE_POLYFILL: 'true', 54 | }, 55 | }, 56 | { 57 | name: 'ember-lts-4.12', 58 | npm: { 59 | devDependencies: { 60 | 'ember-source': '~4.12.0', 61 | }, 62 | }, 63 | }, 64 | { 65 | name: 'ember-lts-5.4', 66 | npm: { 67 | devDependencies: { 68 | 'ember-source': '~5.4.0', 69 | }, 70 | }, 71 | }, 72 | { 73 | name: 'ember-lts-5.8', 74 | npm: { 75 | devDependencies: { 76 | 'ember-source': '~5.8.0', 77 | }, 78 | }, 79 | }, 80 | { 81 | name: 'ember-lts-5.12', 82 | npm: { 83 | devDependencies: { 84 | 'ember-source': '~5.12.0', 85 | }, 86 | }, 87 | }, 88 | { 89 | name: 'ember-lts-6.4', 90 | npm: { 91 | devDependencies: { 92 | 'ember-source': '~6.4.0', 93 | }, 94 | }, 95 | }, 96 | { 97 | name: 'ember-lts-6.8', 98 | npm: { 99 | devDependencies: { 100 | 'ember-source': '~6.8.0', 101 | }, 102 | }, 103 | }, 104 | { 105 | name: 'ember-release', 106 | npm: { 107 | devDependencies: { 108 | 'ember-source': await getChannelURL('release'), 109 | }, 110 | }, 111 | }, 112 | { 113 | name: 'ember-beta', 114 | npm: { 115 | devDependencies: { 116 | 'ember-source': await getChannelURL('beta'), 117 | }, 118 | }, 119 | }, 120 | { 121 | name: 'ember-canary', 122 | npm: { 123 | devDependencies: { 124 | 'ember-source': await getChannelURL('canary'), 125 | }, 126 | }, 127 | }, 128 | { 129 | name: 'ember-default-with-jquery', 130 | env: { 131 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 132 | 'jquery-integration': true, 133 | }), 134 | }, 135 | npm: { 136 | devDependencies: { 137 | 'ember-cli': '~4.12.0', 138 | 'ember-source': '~3.28.0', 139 | '@ember/jquery': '^1.1.0', 140 | 'ember-resolver': '^10.0.0', 141 | 'ember-load-initializers': '^2.1.2', 142 | }, 143 | }, 144 | }, 145 | { 146 | name: 'ember-classic', 147 | env: { 148 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 149 | 'application-template-wrapper': true, 150 | 'default-async-observers': false, 151 | 'template-only-glimmer-components': false, 152 | }), 153 | }, 154 | npm: { 155 | devDependencies: { 156 | 'ember-cli': '~4.12.0', 157 | 'ember-source': '~3.28.0', 158 | 'ember-resolver': '^10.0.0', 159 | 'ember-load-initializers': '^2.1.2', 160 | }, 161 | ember: { 162 | edition: 'classic', 163 | }, 164 | }, 165 | }, 166 | embroiderSafe(), 167 | embroiderOptimized(), 168 | ], 169 | }; 170 | }; 171 | -------------------------------------------------------------------------------- /docs/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 | Nick Schot 18 | 19 | info@example.com 20 | 21 |
22 |
23 |
24 |
25 |
    26 |
  • 27 | Home 28 |
  • 29 |
30 |
31 |
32 | 33 | 34 | Menu 35 | 36 |

37 | Open your dev tools and switch to responsive mode to try the gestures! 38 |

39 | 40 | {{#if this.configure}} 41 |
42 |
43 |
44 |

Mode

45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 | 55 |
56 |

Type

57 |
58 | 59 | 60 |
61 | 62 |

Open detection width

63 |
64 | 65 | 66 |
67 | 68 |

Other options

69 |
70 | 71 | 72 |
73 |
74 |
75 |
76 | {{else}} 77 |
78 | 85 |
86 | {{/if}} 87 |
88 |
89 |
90 |
91 |
92 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: {} 9 | 10 | concurrency: 11 | group: ci-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | dist: ember-mobile-menu/dist 16 | 17 | jobs: 18 | build: 19 | name: "Build" 20 | runs-on: ubuntu-latest 21 | timeout-minutes: 10 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | - name: Install Node 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20.x 30 | cache: pnpm 31 | 32 | - name: Install Dependencies 33 | # Runs build via "prepare" in root package.json 34 | run: pnpm install 35 | - name: Upload dist assets to cache 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: dist 39 | path: ${{ env.dist }} 40 | 41 | lint: 42 | name: "Lint" 43 | runs-on: ubuntu-latest 44 | timeout-minutes: 10 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: pnpm/action-setup@v4 49 | - name: Install Node 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: 20.x 53 | cache: pnpm 54 | - name: Install Dependencies 55 | run: pnpm install --frozen-lockfile --ignore-scripts 56 | - name: Lint 57 | run: pnpm lint 58 | 59 | test: 60 | name: "Tests" 61 | runs-on: ubuntu-latest 62 | timeout-minutes: 10 63 | needs: ["build"] 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: pnpm/action-setup@v4 68 | - name: Install Node 69 | uses: actions/setup-node@v4 70 | with: 71 | node-version: 20.x 72 | cache: pnpm 73 | - name: Install Dependencies 74 | run: pnpm install --frozen-lockfile --ignore-scripts 75 | - name: Download built package from cache 76 | uses: actions/download-artifact@v4 77 | with: 78 | name: dist 79 | path: ${{ env.dist }} 80 | - name: Run Tests 81 | run: pnpm test:ember 82 | 83 | floating: 84 | name: "Floating Dependencies" 85 | runs-on: ubuntu-latest 86 | timeout-minutes: 10 87 | needs: ["build"] 88 | 89 | steps: 90 | - uses: actions/checkout@v4 91 | - uses: pnpm/action-setup@v4 92 | - uses: actions/setup-node@v4 93 | with: 94 | node-version: 20.x 95 | cache: pnpm 96 | - name: Remove lock file 97 | shell: bash 98 | run: rm pnpm-lock.yaml 99 | - name: Install Dependencies 100 | run: pnpm install --ignore-scripts 101 | - name: Download built package from cache 102 | uses: actions/download-artifact@v4 103 | with: 104 | name: dist 105 | path: ${{ env.dist }} 106 | - name: Run Tests 107 | run: pnpm test:ember 108 | 109 | try-scenarios: 110 | name: ${{ matrix.try-scenario }} 111 | runs-on: ubuntu-latest 112 | needs: ["test", "build"] 113 | timeout-minutes: 10 114 | 115 | strategy: 116 | fail-fast: false 117 | matrix: 118 | try-scenario: 119 | - ember-lts-3.28 120 | - ember-lts-4.4 121 | - ember-lts-4.8 122 | - ember-lts-4.12 123 | - ember-lts-5.4 124 | - ember-lts-5.8 125 | - ember-lts-5.12 126 | - ember-lts-6.4 127 | - ember-lts-6.8 128 | - ember-release 129 | 130 | steps: 131 | - uses: actions/checkout@v4 132 | - uses: pnpm/action-setup@v4 133 | - name: Install Node 134 | uses: actions/setup-node@v4 135 | with: 136 | node-version: 20.x 137 | cache: pnpm 138 | - run: echo "inject-workspace-packages=true" >> .npmrc 139 | - run: echo "sync-injected-deps-after-scripts[]=build" >> .npmrc 140 | - run: cat .npmrc 141 | 142 | - name: Download built package from cache 143 | uses: actions/download-artifact@v4 144 | with: 145 | name: dist 146 | path: ${{ env.dist }} 147 | 148 | - name: Install Dependencies 149 | run: pnpm install --no-frozen-lockfile --ignore-scripts 150 | - name: Run Tests 151 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} --skip-cleanup 152 | working-directory: test-app 153 | 154 | allowed-fail-try-scenarios: 155 | name: ${{ matrix.try-scenario }} - Allowed to fail 156 | runs-on: ubuntu-latest 157 | needs: ["test", "build"] 158 | permissions: 159 | pull-requests: write 160 | 161 | strategy: 162 | fail-fast: false 163 | matrix: 164 | try-scenario: 165 | - ember-beta 166 | - ember-canary 167 | 168 | steps: 169 | - uses: actions/checkout@v4 170 | - uses: pnpm/action-setup@v4 171 | - name: Install Node 172 | uses: actions/setup-node@v4 173 | with: 174 | node-version: 20.x 175 | cache: pnpm 176 | - run: echo "inject-workspace-packages=true" >> .npmrc 177 | - run: echo "sync-injected-deps-after-scripts[]=build" >> .npmrc 178 | - run: cat .npmrc 179 | 180 | - name: Download built package from cache 181 | uses: actions/download-artifact@v4 182 | with: 183 | name: dist 184 | path: ${{ env.dist }} 185 | 186 | - name: Install Dependencies 187 | run: pnpm install --no-frozen-lockfile --ignore-scripts 188 | - name: Run Tests 189 | id: tests 190 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} --skip-cleanup 191 | continue-on-error: true 192 | working-directory: test-app 193 | - uses: mainmatter/continue-on-error-comment@v1 194 | with: 195 | repo-token: ${{ secrets.GITHUB_TOKEN }} 196 | outcome: ${{ steps.tests.outcome }} 197 | test-id: ${{ matrix.try-scenario }} 198 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu.gjs: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | import { assert } from '@ember/debug'; 5 | import { htmlSafe } from '@ember/template'; 6 | import './mobile-menu.css'; 7 | import { registerDestructor } from '@ember/destroyable'; 8 | import MaskComponent from './mobile-menu/mask.gjs'; 9 | import TrayComponent from './mobile-menu/tray.gjs'; 10 | import { hash } from '@ember/helper'; 11 | import { effect } from './utils.js'; 12 | 13 | /** 14 | * NOTE: @cached isn't supported in ember < 4.1, 15 | * and we don't want to use the polyfill, because it's a v1 addon. 16 | * So to have broad ember support *and* strive for avoiding v1 addons for the most modern of ember apps, 17 | * we need to implement "cached" ourselves 18 | * 19 | * These APIs are technically private (intimate?), and shouldn't be used. 20 | * However, they are what the @cached decorator is built on 21 | */ 22 | import { createCache, getValue } from '@glimmer/validator'; 23 | 24 | const _fn = function () {}; 25 | class StateResource { 26 | @tracked _open = false; 27 | @tracked _closed = true; 28 | @tracked _dragging = false; 29 | @tracked _transitioning = false; 30 | 31 | _useState; 32 | 33 | constructor(owner, useState = () => {}) { 34 | this._useState = useState; 35 | 36 | registerDestructor(owner, () => { 37 | this._useState = undefined; 38 | }); 39 | } 40 | 41 | currentCache = createCache(() => { 42 | let state = this._useState(); 43 | return this.calculateCurrent(...state); 44 | }); 45 | 46 | get current() { 47 | return getValue(this.currentCache); 48 | } 49 | 50 | calculateCurrent(position, isDragging, width) { 51 | this._dragging = position !== 0 && isDragging; 52 | let open = !this._dragging && Math.abs(position) === width; 53 | let closed = !this._dragging && position === 0; 54 | 55 | effect(() => { 56 | this.maybeToggle(open, closed); 57 | }); 58 | this._transitioning = !this._dragging && !this._open && !this._closed; 59 | 60 | return { 61 | open: this._open, 62 | closed: this._closed, 63 | dragging: this._dragging, 64 | transitioning: this._transitioning, 65 | }; 66 | } 67 | 68 | get open() { 69 | return this.current.open; 70 | } 71 | get closed() { 72 | return this.current.closed; 73 | } 74 | get dragging() { 75 | return this.current.dragging; 76 | } 77 | get transitioning() { 78 | return this.current.transitioning; 79 | } 80 | 81 | maybeToggle(open, closed) { 82 | if (this._open !== open) { 83 | this._open = open; 84 | } else if (this.closed !== closed) { 85 | this._closed = closed; 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Menu component 92 | * 93 | * @class MobileMenu 94 | * @public 95 | */ 96 | export default class MobileMenu extends Component { 97 | state = new StateResource(this, () => [ 98 | this.position, 99 | this.args.isDragging, 100 | this._width, 101 | ]); 102 | 103 | /** 104 | * The type of menu. Currently 'left' and 'right' are supported. 105 | * 106 | * @argument type 107 | * @type String 108 | * @default 'left' 109 | */ 110 | get type() { 111 | return this.args.type ?? 'left'; 112 | } 113 | 114 | /** 115 | * Sets the mode of the menu. Currently 'default', 'push', 'ios', 'reveal', 'squeeze' and 'squeeze-reveal' are supported. 116 | * 117 | * @argument mode 118 | * @type string 119 | * @default 'default' 120 | */ 121 | get mode() { 122 | return this.args.mode ?? 'default'; 123 | } 124 | 125 | /** 126 | * The percentage of the screen the menu will take when opened. 127 | * 128 | * @argument width 129 | * @type Number [0-100] 130 | * @default 85 131 | */ 132 | get width() { 133 | return this.args.width ?? 85; 134 | } 135 | 136 | /** 137 | * The maximum width of the menu in pixels. Set to -1 to disable; 138 | * 139 | * @argument maxWidth 140 | * @type Number 141 | * @default 300 142 | */ 143 | get maxWidth() { 144 | return this.args.maxWidth ?? 300; 145 | } 146 | 147 | /** 148 | * Whether or not a mask is added when the menu is opened. 149 | * 150 | * @argument maskEnabled 151 | * @type Boolean 152 | * @default true 153 | */ 154 | get maskEnabled() { 155 | return this.args.maskEnabled ?? true; 156 | } 157 | 158 | /** 159 | * Whether or not a shadow is added to the menu. 160 | * 161 | * @argument shadowEnabled 162 | * @type Boolean 163 | * @default true 164 | */ 165 | get shadowEnabled() { 166 | return this.args.shadowEnabled ?? true; 167 | } 168 | 169 | /** 170 | * The default swipe velocity needed to fully open the menu. 171 | * 172 | * @argument triggerVelocity 173 | * @type Number 174 | * @default 0.3 175 | */ 176 | get triggerVelocity() { 177 | return this.args.triggerVelocity ?? 0.3; 178 | } 179 | 180 | /** 181 | * @argument isOpen 182 | * @type boolean 183 | * @default false 184 | */ 185 | 186 | /** 187 | * Hook which is called after the transition with the new menu isOpen state. 188 | * 189 | * @argument onToggle 190 | * @type Function 191 | */ 192 | get onToggle() { 193 | return this.args.onToggle ?? _fn; 194 | } 195 | 196 | /** 197 | * @argument embed 198 | * @type boolean 199 | * @default false 200 | * @protected 201 | */ 202 | get embed() { 203 | return this.args.embed ?? false; 204 | } 205 | 206 | /** 207 | * Hook fired when the menu is opened. You can pass in an action. The menu instance will be passed to the action. 208 | * 209 | * @argument onOpen 210 | * @type Function 211 | * @protected 212 | */ 213 | get onOpen() { 214 | return this.args.onOpen ?? _fn; 215 | } 216 | 217 | /** 218 | * Hook fired when the menu is closed. You can pass in an action. The menu instance will be passed to the action. 219 | * 220 | * @argument onClose 221 | * @type Action 222 | * @protected 223 | */ 224 | get onClose() { 225 | return this.args.onClose ?? _fn; 226 | } 227 | 228 | /** 229 | * @argument position 230 | * @type number 231 | * @protected 232 | */ 233 | get position() { 234 | return (this.isLeft && this.args.position > 0) || 235 | (this.isRight && this.args.position < 0) 236 | ? this.args.position 237 | : 0; 238 | } 239 | 240 | /** 241 | * @argument isDragging 242 | * @type boolean 243 | * @protected 244 | */ 245 | 246 | constructor() { 247 | super(...arguments); 248 | 249 | assert( 250 | 'register function argument not passed. You should not be using directly.', 251 | typeof this.args.register === 'function', 252 | ); 253 | assert( 254 | 'unregister function argument not passed. You should not be using directly.', 255 | typeof this.args.unregister === 'function', 256 | ); 257 | 258 | if (this.args.parent?.isFastBoot && this.args.isOpen) { 259 | this.args.parent._activeMenu = this; 260 | this.open(false); 261 | } 262 | } 263 | 264 | willDestroy() { 265 | this.args.unregister(this); 266 | super.willDestroy(...arguments); 267 | } 268 | 269 | get renderMenu() { 270 | return this.args.parent?.isFastBoot || this.args.parentBoundingClientRect; 271 | } 272 | 273 | get classNames() { 274 | let classes = `mobile-menu mobile-menu--${this.mode}`; 275 | if (this.isLeft) classes += ' mobile-menu--left'; 276 | if (this.isRight) classes += ' mobile-menu--right'; 277 | if (this.state.dragging) classes += ' mobile-menu--dragging'; 278 | if (this.state.open) classes += ' mobile-menu--open'; 279 | if (this.state.transitioning) classes += ' mobile-menu--transitioning'; 280 | return classes; 281 | } 282 | 283 | get isLeft() { 284 | return this.type === 'left'; 285 | } 286 | 287 | get isRight() { 288 | return this.type === 'right'; 289 | } 290 | 291 | get relativePosition() { 292 | return Math.abs(this.position) / this._width; 293 | } 294 | 295 | get invertOpacity() { 296 | return ['ios', 'reveal', 'squeeze-reveal'].includes(this.args.mode); 297 | } 298 | 299 | /** 300 | * Current menu width in px 301 | * 302 | * @property _width 303 | * @return {Boolean} 304 | * @private 305 | */ 306 | get _width() { 307 | const width = this.args.parentBoundingClientRect 308 | ? (this.width / 100) * this.args.parentBoundingClientRect.width 309 | : this.maxWidth; 310 | 311 | return this.maxWidth === -1 ? width : Math.min(width, this.maxWidth); 312 | } 313 | 314 | get style() { 315 | let styles = ''; 316 | if (!this.maskEnabled && this.state.open) { 317 | styles = `width: ${this._width}px;`; 318 | } 319 | return htmlSafe(styles); 320 | } 321 | 322 | @action 323 | open(animate) { 324 | this.onOpen(this, 0, animate); 325 | } 326 | 327 | @action 328 | close(animate) { 329 | if (!this.hasRendered) return; 330 | 331 | this.onClose(this, 0, animate); 332 | } 333 | 334 | hasRendered = false; 335 | 336 | @action 337 | openOrClose(open) { 338 | let animate = this.hasRendered; 339 | 340 | if (open) { 341 | this.open(animate); 342 | } else { 343 | this.close(animate); 344 | } 345 | 346 | this.onToggle(open); 347 | } 348 | 349 | @action 350 | closeFromLinkTo() { 351 | if (!['squeeze', 'squeeze-reveal'].includes(this.mode)) { 352 | this.close(); 353 | } 354 | } 355 | 356 | @action setRendered() { 357 | if (!this.hasRendered) { 358 | this.hasRendered = true; 359 | } 360 | } 361 | 362 | 408 | } 409 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/utils/body-scroll-lock.js: -------------------------------------------------------------------------------- 1 | // Adopted and modified from https://github.com/willmcpo/body-scroll-lock 2 | 3 | // MIT License 4 | // 5 | // Copyright (c) 2018 Will Po 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | // Older browsers don't support event options, feature detect it. 26 | 27 | // Adopted and modified solution from Bohdan Didukh (2017) 28 | // https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi 29 | 30 | let hasPassiveEvents = false; 31 | if (typeof window !== 'undefined' && typeof FastBoot === 'undefined') { 32 | const passiveTestOptions = { 33 | get passive() { 34 | hasPassiveEvents = true; 35 | return undefined; 36 | }, 37 | }; 38 | window.addEventListener('testPassive', null, passiveTestOptions); 39 | window.removeEventListener('testPassive', null, passiveTestOptions); 40 | } 41 | 42 | const isIosDevice = 43 | typeof window !== 'undefined' && 44 | typeof FastBoot === 'undefined' && 45 | window.navigator && 46 | window.navigator.platform && 47 | (/iP(ad|hone|od)/.test(window.navigator.platform) || 48 | (window.navigator.platform === 'MacIntel' && 49 | window.navigator.maxTouchPoints > 1)); 50 | 51 | let locks = []; 52 | let documentListenerAdded = false; 53 | let initialClientY = -1; 54 | let previousBodyOverflowSetting; 55 | let previousBodyPosition; 56 | let previousBodyPaddingRight; 57 | 58 | // returns true if `el` should be allowed to receive touchmove events. 59 | const allowTouchMove = (el) => 60 | locks.some((lock) => { 61 | if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) { 62 | return true; 63 | } 64 | 65 | return false; 66 | }); 67 | 68 | const preventDefault = (rawEvent) => { 69 | const e = rawEvent || window.event; 70 | 71 | // For the case whereby consumers adds a touchmove event listener to document. 72 | // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false }) 73 | // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then 74 | // the touchmove event on document will break. 75 | if (allowTouchMove(e.target)) { 76 | return true; 77 | } 78 | 79 | // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom). 80 | if (e.touches.length > 1) return true; 81 | 82 | if (e.preventDefault) e.preventDefault(); 83 | 84 | return false; 85 | }; 86 | 87 | const setOverflowHidden = (options) => { 88 | // If previousBodyPaddingRight is already set, don't set it again. 89 | if (previousBodyPaddingRight === undefined) { 90 | const reserveScrollBarGap = 91 | !!options && options.reserveScrollBarGap === true; 92 | const scrollBarGap = 93 | window.innerWidth - document.documentElement.clientWidth; 94 | 95 | if (reserveScrollBarGap && scrollBarGap > 0) { 96 | const computedBodyPaddingRight = parseInt( 97 | window 98 | .getComputedStyle(document.body) 99 | .getPropertyValue('padding-right'), 100 | 10, 101 | ); 102 | previousBodyPaddingRight = document.body.style.paddingRight; 103 | document.body.style.paddingRight = `${computedBodyPaddingRight + scrollBarGap}px`; 104 | } 105 | } 106 | 107 | // If previousBodyOverflowSetting is already set, don't set it again. 108 | if (previousBodyOverflowSetting === undefined) { 109 | previousBodyOverflowSetting = document.body.style.overflow; 110 | document.body.style.overflow = 'hidden'; 111 | } 112 | }; 113 | 114 | const restoreOverflowSetting = () => { 115 | if (previousBodyPaddingRight !== undefined) { 116 | document.body.style.paddingRight = previousBodyPaddingRight; 117 | 118 | // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it 119 | // can be set again. 120 | previousBodyPaddingRight = undefined; 121 | } 122 | 123 | if (previousBodyOverflowSetting !== undefined) { 124 | document.body.style.overflow = previousBodyOverflowSetting; 125 | 126 | // Restore previousBodyOverflowSetting to undefined 127 | // so setOverflowHidden knows it can be set again. 128 | previousBodyOverflowSetting = undefined; 129 | } 130 | }; 131 | 132 | const setPositionFixed = () => 133 | window.requestAnimationFrame(() => { 134 | // If previousBodyPosition is already set, don't set it again. 135 | if (previousBodyPosition === undefined) { 136 | previousBodyPosition = { 137 | position: document.body.style.position, 138 | top: document.body.style.top, 139 | left: document.body.style.left, 140 | }; 141 | 142 | // Update the dom inside an animation frame 143 | const { scrollY, scrollX, innerHeight } = window; 144 | document.body.style.position = 'fixed'; 145 | document.body.style.top = `${-scrollY}px`; 146 | document.body.style.left = `${-scrollX}px`; 147 | 148 | setTimeout( 149 | () => 150 | window.requestAnimationFrame(() => { 151 | // Attempt to check if the bottom bar appeared due to the position change 152 | const bottomBarHeight = innerHeight - window.innerHeight; 153 | if (bottomBarHeight && scrollY >= innerHeight) { 154 | // Move the content further up so that the bottom bar doesn't hide it 155 | document.body.style.top = `-${scrollY + bottomBarHeight}px`; 156 | } 157 | }), 158 | 300, 159 | ); 160 | } 161 | }); 162 | 163 | const restorePositionSetting = () => { 164 | if (previousBodyPosition !== undefined) { 165 | // Convert the position from "px" to Int 166 | const y = -parseInt(document.body.style.top, 10); 167 | const x = -parseInt(document.body.style.left, 10); 168 | 169 | // Restore styles 170 | document.body.style.position = previousBodyPosition.position; 171 | document.body.style.top = previousBodyPosition.top; 172 | document.body.style.left = previousBodyPosition.left; 173 | 174 | // Restore scroll 175 | window.scrollTo(x, y); 176 | 177 | previousBodyPosition = undefined; 178 | } 179 | }; 180 | 181 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions 182 | const isTargetElementTotallyScrolled = (targetElement) => 183 | targetElement 184 | ? targetElement.scrollHeight - targetElement.scrollTop <= 185 | targetElement.clientHeight 186 | : false; 187 | 188 | const handleScroll = (event, targetElement) => { 189 | const clientY = event.targetTouches[0].clientY - initialClientY; 190 | 191 | if (allowTouchMove(event.target)) { 192 | return false; 193 | } 194 | 195 | if (targetElement && targetElement.scrollTop === 0 && clientY > 0) { 196 | // element is at the top of its scroll. 197 | return preventDefault(event); 198 | } 199 | 200 | if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { 201 | // element is at the bottom of its scroll. 202 | return preventDefault(event); 203 | } 204 | 205 | event.stopPropagation(); 206 | return true; 207 | }; 208 | 209 | export const disableBodyScroll = (targetElement, options) => { 210 | // targetElement must be provided 211 | if (!targetElement) { 212 | console.error( 213 | 'disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.', 214 | ); 215 | return; 216 | } 217 | 218 | // disableBodyScroll must not have been called on this targetElement before 219 | if (locks.some((lock) => lock.targetElement === targetElement)) { 220 | return; 221 | } 222 | 223 | const lock = { 224 | targetElement, 225 | options: options || {}, 226 | }; 227 | 228 | locks = [...locks, lock]; 229 | 230 | if (isIosDevice) { 231 | setPositionFixed(); 232 | } else { 233 | setOverflowHidden(options); 234 | } 235 | 236 | if (isIosDevice) { 237 | targetElement.ontouchstart = (event) => { 238 | if (event.targetTouches.length === 1) { 239 | // detect single touch. 240 | initialClientY = event.targetTouches[0].clientY; 241 | } 242 | }; 243 | targetElement.ontouchmove = (event) => { 244 | if (event.targetTouches.length === 1) { 245 | // detect single touch. 246 | handleScroll(event, targetElement); 247 | } 248 | }; 249 | 250 | if (!documentListenerAdded) { 251 | document.addEventListener( 252 | 'touchmove', 253 | preventDefault, 254 | hasPassiveEvents ? { passive: false } : undefined, 255 | ); 256 | documentListenerAdded = true; 257 | } 258 | } 259 | }; 260 | 261 | export const clearAllBodyScrollLocks = () => { 262 | if (isIosDevice) { 263 | // Clear all locks ontouchstart/ontouchmove handlers, and the references. 264 | locks.forEach((lock) => { 265 | lock.targetElement.ontouchstart = null; 266 | lock.targetElement.ontouchmove = null; 267 | }); 268 | 269 | if (documentListenerAdded) { 270 | document.removeEventListener( 271 | 'touchmove', 272 | preventDefault, 273 | hasPassiveEvents ? { passive: false } : undefined, 274 | ); 275 | documentListenerAdded = false; 276 | } 277 | 278 | // Reset initial clientY. 279 | initialClientY = -1; 280 | } 281 | 282 | if (isIosDevice) { 283 | restorePositionSetting(); 284 | } else { 285 | restoreOverflowSetting(); 286 | } 287 | 288 | locks = []; 289 | }; 290 | 291 | export const enableBodyScroll = (targetElement) => { 292 | if (!targetElement) { 293 | console.error( 294 | 'enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.', 295 | ); 296 | return; 297 | } 298 | 299 | locks = locks.filter((lock) => lock.targetElement !== targetElement); 300 | 301 | if (isIosDevice) { 302 | targetElement.ontouchstart = null; 303 | targetElement.ontouchmove = null; 304 | 305 | if (documentListenerAdded && locks.length === 0) { 306 | document.removeEventListener( 307 | 'touchmove', 308 | preventDefault, 309 | hasPassiveEvents ? { passive: false } : undefined, 310 | ); 311 | documentListenerAdded = false; 312 | } 313 | } 314 | 315 | if (isIosDevice) { 316 | restorePositionSetting(); 317 | } else { 318 | restoreOverflowSetting(); 319 | } 320 | }; 321 | -------------------------------------------------------------------------------- /test-app/tests/integration/components/mobile-menu-wrapper-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, click, waitFor, settled } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | import { pan } from 'ember-gesture-modifiers/test-support'; 6 | 7 | module('Integration | Component | mobile-menu-wrapper', function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | test('it renders', async function (assert) { 11 | await render(hbs``); 12 | 13 | assert.strictEqual(this.element.textContent.trim(), ''); 14 | 15 | // Template block usage: 16 | await render(hbs` 17 | 18 | template block text 19 | 20 | `); 21 | 22 | assert.strictEqual(this.element.textContent.trim(), 'template block text'); 23 | }); 24 | 25 | test('it renders a "left" menu by default', async function (assert) { 26 | await render(hbs` 27 | 28 | 29 | Home 30 | 31 | 32 | `); 33 | 34 | assert.dom('.mobile-menu').hasClass('mobile-menu--left'); 35 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--right'); 36 | }); 37 | 38 | test('it renders a "right" menu', async function (assert) { 39 | await render(hbs` 40 | 41 | 42 | Home 43 | 44 | 45 | `); 46 | 47 | assert.dom('.mobile-menu').hasClass('mobile-menu--right'); 48 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--left'); 49 | }); 50 | 51 | test('it opens the menu when the toggle is clicked', async function (assert) { 52 | await render(hbs` 53 | 54 | 55 | Home 56 | 57 | 58 | 59 | Menu 60 | 61 | 62 | `); 63 | 64 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 65 | assert.dom('.mobile-menu').hasAttribute('aria-hidden', 'true'); 66 | click('.mobile-menu__toggle'); 67 | 68 | await waitFor('.mobile-menu--transitioning'); 69 | assert.dom('.mobile-menu').hasClass('mobile-menu--transitioning'); 70 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 71 | assert.dom('.mobile-menu').doesNotHaveAttribute('aria-hidden'); 72 | 73 | await settled(); 74 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--transitioning'); 75 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 76 | assert.dom('.mobile-menu').doesNotHaveAttribute('aria-hidden'); 77 | }); 78 | 79 | test('it toggles the right menu when it is the only one', async function (assert) { 80 | await render(hbs` 81 | 82 | 83 | Home 84 | 85 | 86 | 87 | Menu 88 | 89 | 90 | `); 91 | 92 | assert.dom('.mobile-menu--right').doesNotHaveClass('mobile-menu--open'); 93 | await click('.mobile-menu__toggle'); 94 | 95 | assert.dom('.mobile-menu--right').hasClass('mobile-menu--open'); 96 | }); 97 | 98 | test('it toggles the targetted menu', async function (assert) { 99 | this.set('target', 'right'); 100 | 101 | await render(hbs` 102 | 103 | 104 | Home 105 | 106 | 107 | Home 108 | 109 | 110 | 111 | Menu 112 | 113 | 114 | `); 115 | 116 | assert.dom('.mobile-menu--left').doesNotHaveClass('mobile-menu--open'); 117 | assert.dom('.mobile-menu--right').doesNotHaveClass('mobile-menu--open'); 118 | await click('.mobile-menu__toggle'); 119 | 120 | assert.dom('.mobile-menu--left').doesNotHaveClass('mobile-menu--open'); 121 | assert.dom('.mobile-menu--right').hasClass('mobile-menu--open'); 122 | 123 | await click('.mobile-menu__toggle'); 124 | this.set('target', 'left'); 125 | 126 | assert.dom('.mobile-menu--left').doesNotHaveClass('mobile-menu--open'); 127 | assert.dom('.mobile-menu--right').doesNotHaveClass('mobile-menu--open'); 128 | await click('.mobile-menu__toggle'); 129 | 130 | assert.dom('.mobile-menu--left').hasClass('mobile-menu--open'); 131 | assert.dom('.mobile-menu--right').doesNotHaveClass('mobile-menu--open'); 132 | }); 133 | 134 | test('it closes the menu when the mask is clicked', async function (assert) { 135 | await render(hbs` 136 | 137 | 138 | Home 139 | 140 | 141 | 142 | Menu 143 | 144 | 145 | `); 146 | 147 | await click('.mobile-menu__toggle'); 148 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 149 | 150 | click('.mobile-menu__mask'); 151 | await waitFor('.mobile-menu--transitioning'); 152 | assert.dom('.mobile-menu').hasClass('mobile-menu--transitioning'); 153 | 154 | await settled(); 155 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--transitioning'); 156 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 157 | }); 158 | 159 | test('it opens the menu when dragged', async function (assert) { 160 | await render(hbs` 161 | 162 | 163 | Home 164 | 165 | 166 | 167 | Menu 168 | 169 | 170 | `); 171 | 172 | await pan('.mobile-menu-wrapper__content', 'right'); 173 | await settled(); 174 | 175 | assert.dom('.mobile-menu').hasClass('mobile-menu--left'); 176 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 177 | }); 178 | 179 | test('it opens the "right" menu when dragged', async function (assert) { 180 | await render(hbs` 181 | 182 | 183 | Home 184 | 185 | 186 | 187 | Menu 188 | 189 | 190 | `); 191 | 192 | await pan('.mobile-menu-wrapper__content', 'left'); 193 | await settled(); 194 | 195 | assert.dom('.mobile-menu').hasClass('mobile-menu--right'); 196 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 197 | }); 198 | 199 | test('it does not open the menu when dragged in the wrong direction', async function (assert) { 200 | await render(hbs` 201 | 202 | 203 | Home 204 | 205 | 206 | 207 | Menu 208 | 209 | 210 | `); 211 | 212 | await pan('.mobile-menu-wrapper__content', 'left'); 213 | await settled(); 214 | 215 | assert.dom('.mobile-menu').hasClass('mobile-menu--left'); 216 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 217 | }); 218 | 219 | test('it closes the menu when dragged from outside the menu', async function (assert) { 220 | await render(hbs` 221 | 222 | 223 | Home 224 | 225 | 226 | 227 | Menu 228 | 229 | 230 | `); 231 | 232 | await click('.mobile-menu__toggle'); 233 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 234 | 235 | await pan('.mobile-menu__mask', 'left'); 236 | await settled(); 237 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 238 | }); 239 | 240 | test('it closes the menu when dragged on the menu itself', async function (assert) { 241 | await render(hbs` 242 | 243 | 244 | Home 245 | 246 | 247 | 248 | Menu 249 | 250 | 251 | `); 252 | 253 | await click('.mobile-menu__toggle'); 254 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 255 | 256 | await pan('.mobile-menu__tray', 'left'); 257 | await settled(); 258 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 259 | }); 260 | 261 | test('it opens the embedded menu when dragged', async function (assert) { 262 | await render(hbs` 263 | {{! template-lint-disable no-inline-styles }} 264 |
265 | 266 | 267 | Home 268 | 269 | 270 | 271 | Menu 272 | 273 | 274 |
275 | `); 276 | 277 | await pan('.root-div', 'right'); 278 | await settled(); 279 | 280 | assert.dom('.mobile-menu').hasClass('mobile-menu--left'); 281 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 282 | 283 | await pan( 284 | '.mobile-menu-wrapper--embedded .mobile-menu-wrapper__content', 285 | 'right', 286 | ); 287 | await settled(); 288 | 289 | assert.dom('.mobile-menu').hasClass('mobile-menu--left'); 290 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 291 | }); 292 | 293 | test('it opens the "right" embedded menu when dragged', async function (assert) { 294 | await render(hbs` 295 | {{! template-lint-disable no-inline-styles }} 296 |
297 | 298 | 299 | Home 300 | 301 | 302 | 303 | Menu 304 | 305 | 306 |
307 | `); 308 | 309 | await pan('.root-div', 'left'); 310 | await settled(); 311 | 312 | assert.dom('.mobile-menu').hasClass('mobile-menu--right'); 313 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 314 | 315 | await pan( 316 | '.mobile-menu-wrapper--embedded .mobile-menu-wrapper__content', 317 | 'left', 318 | ); 319 | await settled(); 320 | 321 | assert.dom('.mobile-menu').hasClass('mobile-menu--right'); 322 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 323 | }); 324 | 325 | test('it closes the embedded menu when dragged from outside the menu', async function (assert) { 326 | await render(hbs` 327 | 328 | 329 | Home 330 | 331 | 332 | 333 | Menu 334 | 335 | 336 | `); 337 | 338 | await click('.mobile-menu__toggle'); 339 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 340 | 341 | await pan('.mobile-menu__mask', 'left'); 342 | await settled(); 343 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 344 | }); 345 | 346 | test('it closes the embedded menu when dragged on the menu itself', async function (assert) { 347 | await render(hbs` 348 | 349 | 350 | Home 351 | 352 | 353 | 354 | Menu 355 | 356 | 357 | `); 358 | 359 | await click('.mobile-menu__toggle'); 360 | assert.dom('.mobile-menu').hasClass('mobile-menu--open'); 361 | 362 | await pan('.mobile-menu__tray', 'left'); 363 | await settled(); 364 | assert.dom('.mobile-menu').doesNotHaveClass('mobile-menu--open'); 365 | }); 366 | 367 | test('it renders the menu with the correct width/maxWidth', async function (assert) { 368 | this.set('maxWidth', -1); 369 | 370 | await render(hbs` 371 | 372 | 373 | Home 374 | 375 | 376 | 377 | Menu 378 | 379 | 380 | `); 381 | 382 | const containerWidth = document 383 | .querySelector('.mobile-menu-wrapper') 384 | .getBoundingClientRect().width; 385 | 386 | await click('.mobile-menu__toggle'); 387 | assert.dom('.mobile-menu__tray').hasStyle({ 388 | width: `${containerWidth}px`, 389 | }); 390 | 391 | this.set('maxWidth', 100); 392 | assert.dom('.mobile-menu__tray').hasStyle({ 393 | width: `100px`, 394 | }); 395 | }); 396 | 397 | test('it opens/closes the menu according to the @isOpen argument and calls the accompanying @onToggle hook', async function (assert) { 398 | this.set('isOpen', true); 399 | this.set('onToggle', (isOpen) => { 400 | assert.strictEqual( 401 | this.isOpen, 402 | isOpen, 403 | `onToggle called with the same value (${isOpen}) is this.isOpen (${this.isOpen})`, 404 | ); 405 | }); 406 | 407 | await render(hbs` 408 | 409 | 410 | Home 411 | 412 | 413 | 414 | Menu 415 | 416 | 417 | `); 418 | 419 | assert.dom('.mobile-menu--open').exists(); 420 | 421 | this.set('isOpen', false); 422 | await settled(); 423 | assert.dom('.mobile-menu--open').doesNotExist(); 424 | 425 | this.set('isOpen', true); 426 | await settled(); 427 | assert.dom('.mobile-menu--open').exists(); 428 | }); 429 | }); 430 | -------------------------------------------------------------------------------- /ember-mobile-menu/src/components/mobile-menu-wrapper.gjs: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { tracked } from '@glimmer/tracking'; 3 | 4 | import { modifier as eModifier } from 'ember-modifier'; 5 | import { action } from '@ember/object'; 6 | import { TrackedSet } from 'tracked-built-ins'; 7 | 8 | import MobileMenu from './mobile-menu.gjs'; 9 | import normalizeCoordinates, { 10 | scaleCorrection, 11 | } from '../utils/normalize-coordinates.js'; 12 | 13 | import { getOwner } from '@ember/application'; 14 | import { assert } from '@ember/debug'; 15 | import { waitFor } from '@ember/test-waiters'; 16 | import { restartableTask } from 'ember-concurrency'; 17 | import Spring from '../spring.js'; 18 | import './mobile-menu-wrapper.css'; 19 | import { onResize } from 'ember-primitives/on-resize'; 20 | import { bodyClass } from 'ember-primitives/helpers/body-class'; 21 | import { hash } from '@ember/helper'; 22 | 23 | import MobileMenuComponent from './mobile-menu.gjs'; 24 | import ToggleComponent from './mobile-menu-toggle.gjs'; 25 | import ContentComponent from './mobile-menu-wrapper/content.gjs'; 26 | 27 | const isIOSDevice = 28 | typeof window !== 'undefined' && 29 | window.navigator?.platform && 30 | (/iP(ad|hone|od)/.test(window.navigator.platform) || 31 | (window.navigator.platform === 'MacIntel' && 32 | window.navigator.maxTouchPoints > 1)); 33 | 34 | /** 35 | * Wrapper component for menu's. Provides pan recognition and management. 36 | * 37 | * @class MobileMenuWrapper 38 | * @yield {Hash} wrapper 39 | * @yield {MobileMenu component} wrapper.MobileMenu 40 | * @yield {Content component} wrapper.Content 41 | * @yield {MobileMenuToggle component} wrapper.Toggle 42 | * @yield {Hash} wrapper.actions 43 | * @yield {number} position Current position of the active menu in px. 44 | * @yield {number} relativePosition Current position of the active menu between 0 and 1. 45 | * @yield {Action} wrapper.actions.toggle 46 | * @yield {Action} wrapper.actions.close 47 | * @public 48 | */ 49 | export default class MobileMenuWrapper extends Component { 50 | get fastboot() { 51 | return getOwner(this).lookup('service:fastboot'); 52 | } 53 | get isFastBoot() { 54 | return !!this.fastboot?.isFastBoot; 55 | } 56 | 57 | /** 58 | * Current BoundingClientRect of the mobile menu wrapper root element 59 | * 60 | * @property boundingClientRect 61 | * @type {DOMRect} 62 | * @default null 63 | * @private 64 | */ 65 | @tracked boundingClientRect = null; 66 | 67 | @tracked children = new TrackedSet(); 68 | @tracked position = 0; 69 | @tracked dragging = false; 70 | fromPosition = 0; 71 | fromOpen = false; 72 | defaultMenuDx = 0; 73 | preservedVelocity = 0; 74 | _activeMenu = null; // used only in case a menu is set to open in a fastboot environment 75 | 76 | /** 77 | * Horizontal width of the detection zone in pixels. Set to -1 to use full width. 78 | * 79 | * @argument openDetectionWidth 80 | * @type Number 81 | * @default 15 82 | */ 83 | get openDetectionWidth() { 84 | return this.args.openDetectionWidth ?? 15; 85 | } 86 | 87 | /** 88 | * If true the capture phase will be used for the event, giving it precedence over events in the (default) 89 | * bubble phase. This is handy for menus as they are usually defined high in the dom, are opened with edge gestures 90 | * and thus must take precedence over deeper nested elements by using the capture phase. 91 | * 92 | * See for more details. 93 | * 94 | * @argument capture 95 | * @type Boolean 96 | * @default true 97 | */ 98 | get capture() { 99 | return this.args.capture ?? true; 100 | } 101 | 102 | /** 103 | * If true, the component tries to prevent scroll when a menu is open 104 | * 105 | * @argument preventScroll 106 | * @type Boolean 107 | * @default false 108 | */ 109 | get preventScroll() { 110 | return this.args.preventScroll ?? true; 111 | } 112 | 113 | /** 114 | * @argument embed 115 | * @type Boolean 116 | * @default false 117 | */ 118 | get embed() { 119 | return this.args.embed ?? false; 120 | } 121 | 122 | get triggerVelocity() { 123 | return this.args.triggerVelocity ?? 0.3; 124 | } 125 | 126 | /** 127 | * The currently active menu component. 128 | * 129 | * @property activeMenu 130 | * @type MobileMenu 131 | * @default null 132 | * @private 133 | */ 134 | get activeMenu() { 135 | if (this.isFastBoot && !this.children.length && this._activeMenu) { 136 | return this._activeMenu; 137 | } 138 | 139 | if (this.leftMenu && this.position > 0) { 140 | return this.leftMenu; 141 | } else if (this.rightMenu && this.position < 0) { 142 | return this.rightMenu; 143 | } else { 144 | return null; 145 | } 146 | } 147 | 148 | get isOpen() { 149 | return !!this.activeMenu?.state.open; 150 | } 151 | 152 | get isNotClosed() { 153 | return this.activeMenu && !this.activeMenu.state.closed; 154 | } 155 | 156 | get mode() { 157 | return this.activeMenu?.mode; 158 | } 159 | 160 | get contentShadowEnabled() { 161 | return ( 162 | this.activeMenu?.shadowEnabled && 163 | ['reveal', 'ios', 'squeeze-reveal'].includes(this.mode) 164 | ); 165 | } 166 | 167 | get requiresUpdatedPosition() { 168 | return this.mode !== 'default'; 169 | } 170 | 171 | @action 172 | registerChild(component) { 173 | assert( 174 | 'component was already registered as a child', 175 | !this.children.has(component), 176 | ); 177 | 178 | this.children.add(component); 179 | } 180 | 181 | @action 182 | unregisterChild(component) { 183 | this.children.delete(component); 184 | } 185 | 186 | get childMenus() { 187 | return [...this.children].filter((view) => view instanceof MobileMenu); 188 | } 189 | 190 | get leftMenu() { 191 | return this.childMenus.find((menu) => menu.isLeft); 192 | } 193 | 194 | get rightMenu() { 195 | return this.childMenus.find((menu) => menu.isRight); 196 | } 197 | 198 | get preventBodyScroll() { 199 | return ( 200 | this.preventScroll && 201 | !this.embed && 202 | this.isNotClosed && 203 | this.activeMenu?.maskEnabled 204 | ); 205 | } 206 | 207 | get relativePosition() { 208 | return this.activeMenu 209 | ? Math.abs(this.position) / this.activeMenu._width 210 | : 0; 211 | } 212 | 213 | @action 214 | toggle(target) { 215 | let targetMenu = this.leftMenu; 216 | 217 | if (target === 'right') { 218 | targetMenu = this.rightMenu; 219 | } else if (target === 'left') { 220 | targetMenu = this.leftMenu; 221 | } else if (this.rightMenu && !this.leftMenu) { 222 | targetMenu = this.rightMenu; 223 | } 224 | 225 | if (targetMenu) { 226 | this.close(); 227 | if (this.activeMenu !== targetMenu) { 228 | this.open(targetMenu); 229 | } 230 | } 231 | } 232 | 233 | @action 234 | updatePosition(pan) { 235 | const { 236 | initial: { x: initialX }, 237 | current: { distanceX }, 238 | } = pan; 239 | 240 | let distance = distanceX + this.fromPosition; 241 | if (this.dragging && this.fromOpen) { 242 | const menu = this.fromMenu; 243 | 244 | // default menu dx correction 245 | if (this.mode === 'default') { 246 | if (menu.isLeft && initialX > menu._width) { 247 | this.defaultMenuDx = initialX - menu._width; 248 | if (initialX + distanceX > menu._width) { 249 | return; 250 | } 251 | } else if ( 252 | menu.isRight && 253 | initialX < this.boundingClientRect.width - menu._width 254 | ) { 255 | this.defaultMenuDx = 256 | initialX - (this.boundingClientRect.width - menu._width); 257 | if ( 258 | initialX + distanceX < 259 | this.boundingClientRect.width - menu._width 260 | ) { 261 | return; 262 | } 263 | } else { 264 | this.defaultMenuDx = 0; 265 | } 266 | 267 | distance += this.defaultMenuDx; 268 | } 269 | 270 | if (menu.isLeft) { 271 | this.position = Math.min(Math.max(distance, 0), menu._width); 272 | } else { 273 | this.position = Math.max(Math.min(distance, 0), -1 * menu._width); 274 | } 275 | } else if ( 276 | this.dragging && 277 | ((this.leftMenu && distance > 0) || (this.rightMenu && distance < 0)) 278 | ) { 279 | const menu = distance > 0 ? this.leftMenu : this.rightMenu; 280 | this.position = 281 | Math.min(Math.max(Math.abs(distance), 0), menu._width) * 282 | (distance > 0 ? 1 : -1); 283 | } else if (this.position !== 0) { 284 | this.position = 0; 285 | } 286 | } 287 | 288 | @action 289 | didPanStart(e) { 290 | if (this.finishTransitionTask.isRunning) { 291 | this.finishTransitionTask.cancelAll(); 292 | this.preservedVelocity = 0; 293 | } 294 | 295 | // don't conflict with iOS browser's drag to go back/forward functionality 296 | if ( 297 | this._isIOSbrowser && 298 | (e.initial.x < 15 || e.initial.x > this._windowWidth - 15) 299 | ) { 300 | return; 301 | } 302 | 303 | const fromOpen = this.isOpen; 304 | const pan = scaleCorrection( 305 | normalizeCoordinates(e, this.boundingClientRect), 306 | this.scaleX, 307 | this.scaleY, 308 | ); 309 | 310 | if ( 311 | fromOpen || 312 | this.openDetectionWidth < 0 || 313 | (this.leftMenu && pan.initial.x <= this.openDetectionWidth) || 314 | (this.rightMenu && 315 | pan.initial.x >= 316 | this.boundingClientRect.width - this.openDetectionWidth) 317 | ) { 318 | this.fromOpen = fromOpen; 319 | this.fromMenu = this.activeMenu; 320 | this.fromPosition = this.position; 321 | this.dragging = true; 322 | this.updatePosition(pan); 323 | } 324 | } 325 | 326 | @action 327 | didPan(e) { 328 | if (this.dragging) { 329 | this.updatePosition( 330 | scaleCorrection( 331 | normalizeCoordinates(e, this.boundingClientRect), 332 | this.scaleX, 333 | this.scaleY, 334 | ), 335 | ); 336 | } 337 | } 338 | 339 | @action 340 | didPanEnd(e) { 341 | if (this.dragging) { 342 | this.dragging = false; 343 | const pan = scaleCorrection( 344 | normalizeCoordinates(e, this.boundingClientRect), 345 | this.scaleX, 346 | this.scaleY, 347 | ); 348 | const menu = this.activeMenu; 349 | 350 | if (menu) { 351 | const { 352 | current: { distanceX, velocityX }, 353 | } = pan; 354 | 355 | const isLeft = menu.isLeft; 356 | const width = menu._width; 357 | 358 | const condition = 359 | (isLeft && !this.fromOpen) || (this.fromOpen && !isLeft); 360 | const vx = condition ? velocityX : -velocityX; 361 | let dx = condition ? distanceX : -distanceX; 362 | 363 | // default menu dx correction 364 | if (this.fromOpen && this.mode === 'default') { 365 | if (isLeft) { 366 | dx -= this.defaultMenuDx; 367 | } else { 368 | dx += this.defaultMenuDx; 369 | } 370 | } 371 | 372 | // the pan action is over, cleanup and set the correct final menu position 373 | if (!this.fromOpen) { 374 | if (vx > this.triggerVelocity || dx > width / 2) { 375 | this.open(menu, velocityX); 376 | } else { 377 | this.close(menu, velocityX); 378 | } 379 | } else { 380 | if ( 381 | this.mode === 'default' 382 | ? (vx > this.triggerVelocity && dx > 0) || dx > width / 2 383 | : vx > this.triggerVelocity || dx > width / 2 384 | ) { 385 | this.close(menu, velocityX); 386 | } else { 387 | this.open(menu, velocityX); 388 | } 389 | } 390 | } 391 | } 392 | } 393 | 394 | finishTransitionTask = restartableTask( 395 | waitFor( 396 | async ( 397 | menu, 398 | targetPosition = 'open', 399 | currentVelocity = 0, 400 | animate = true, 401 | ) => { 402 | const fromValue = this.position; 403 | const toValue = 404 | targetPosition === 'close' ? 0 : (menu.isLeft ? 1 : -1) * menu._width; 405 | 406 | if (fromValue !== toValue && animate) { 407 | const spring = new Spring((s) => (this.position = s.currentValue), { 408 | stiffness: 1000, 409 | mass: 3, 410 | damping: 500, 411 | overshootClamping: true, 412 | 413 | fromValue, 414 | toValue, 415 | initialVelocity: this.preservedVelocity || currentVelocity, 416 | }); 417 | 418 | try { 419 | await spring.start(); 420 | } finally { 421 | spring.stop(); 422 | this.preservedVelocity = spring.currentVelocity; 423 | } 424 | } else { 425 | this.position = toValue; 426 | this.preservedVelocity = 0; 427 | } 428 | }, 429 | ), 430 | ); 431 | 432 | @action 433 | open(menu = this.activeMenu, currentVelocity, animate) { 434 | this.finishTransitionTask.perform(menu, 'open', currentVelocity, animate); 435 | } 436 | 437 | @action 438 | close(menu = this.activeMenu, currentVelocity, animate) { 439 | this.finishTransitionTask.perform(menu, 'close', currentVelocity, animate); 440 | } 441 | 442 | scaleX = 1; 443 | scaleY = 1; 444 | 445 | @action 446 | onResize({ target }) { 447 | this.boundingClientRect = target.getBoundingClientRect(); 448 | this.updateScale(target); 449 | } 450 | 451 | @action 452 | updateScale(element) { 453 | this.scaleX = this.boundingClientRect.width / element.clientWidth; 454 | this.scaleY = this.boundingClientRect.height / element.clientHeight; 455 | } 456 | 457 | /** 458 | * Detect if the user is using the app from a browser on iOS 459 | * 460 | * @method _isIOSbrowser 461 | * @return {Boolean} Returns true when the user is using iOS and is inside a browser 462 | * @private 463 | */ 464 | get _isIOSbrowser() { 465 | return isIOSDevice && !window.navigator.standalone; 466 | } 467 | 468 | get _windowWidth() { 469 | return window.innerWidth; 470 | } 471 | 472 | updateBounds = eModifier((element) => { 473 | this.boundingClientRect = element.getBoundingClientRect(); 474 | this.updateScale(element); 475 | }); 476 | 477 | 530 | } 531 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Release (2025-12-01) 4 | 5 | * ember-mobile-menu 6.0.0 (major) 6 | 7 | #### :boom: Breaking Change 8 | * `ember-mobile-menu` 9 | * [#1160](https://github.com/nickschot/ember-mobile-menu/pull/1160) Remove @ember/render-modifiers ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 10 | 11 | #### :rocket: Enhancement 12 | * `ember-mobile-menu` 13 | * [#1156](https://github.com/nickschot/ember-mobile-menu/pull/1156) Remove v1 addons: test-waiters and cached-decorator-polyfill ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 14 | * [#1270](https://github.com/nickschot/ember-mobile-menu/pull/1270) Replace ember-on-resize-modifier with ember-primitives (v2) version ([@nickschot](https://github.com/nickschot)) 15 | * [#1269](https://github.com/nickschot/ember-mobile-menu/pull/1269) Replace ember-set-body-class with ember-primitives (v2) version ([@nickschot](https://github.com/nickschot)) 16 | * [#1245](https://github.com/nickschot/ember-mobile-menu/pull/1245) Deprecate decorator syntax of ember-concurrency ([@johanrd](https://github.com/johanrd)) 17 | * [#1158](https://github.com/nickschot/ember-mobile-menu/pull/1158) Remove unneeded peers ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 18 | * Other 19 | * [#1310](https://github.com/nickschot/ember-mobile-menu/pull/1310) Add 6.4 and 6.8 to LTS testing ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 20 | 21 | #### :memo: Documentation 22 | * [#1244](https://github.com/nickschot/ember-mobile-menu/pull/1244) Update CONTRIBUTING.md to reflect practice in v2 addon ([@johanrd](https://github.com/johanrd)) 23 | 24 | #### :house: Internal 25 | * `ember-mobile-menu` 26 | * [#1303](https://github.com/nickschot/ember-mobile-menu/pull/1303) Update test-app to Vite ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 27 | * Other 28 | * [#1161](https://github.com/nickschot/ember-mobile-menu/pull/1161) Fix floating dependencies test ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 29 | * [#1129](https://github.com/nickschot/ember-mobile-menu/pull/1129) Docs smoke test & lint config fixes ([@nickschot](https://github.com/nickschot)) 30 | * [#1128](https://github.com/nickschot/ember-mobile-menu/pull/1128) Fix addon-docs deployment ([@nickschot](https://github.com/nickschot)) 31 | * [#1124](https://github.com/nickschot/ember-mobile-menu/pull/1124) Get rid of dependenciesMeta injected and use pnpm flags only when needed in CI ([@nickschot](https://github.com/nickschot)) 32 | 33 | #### Committers: 3 34 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 35 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli) 36 | - [@johanrd](https://github.com/johanrd) 37 | 38 | ## Release (2025-02-25) 39 | 40 | ember-mobile-menu 5.3.0 (minor) 41 | 42 | #### :rocket: Enhancement 43 | * `ember-mobile-menu` 44 | * [#1084](https://github.com/nickschot/ember-mobile-menu/pull/1084) Convert to GJS components ([@nickschot](https://github.com/nickschot)) 45 | 46 | #### Committers: 1 47 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 48 | 49 | ## Release (2025-02-23) 50 | 51 | ember-mobile-menu 5.2.0 (minor) 52 | 53 | #### :rocket: Enhancement 54 | * `ember-mobile-menu`, `test-app` 55 | * [#1080](https://github.com/nickschot/ember-mobile-menu/pull/1080) Add support for @ember/test-waiters v4 ([@nickschot](https://github.com/nickschot)) 56 | * `ember-mobile-menu` 57 | * [#1077](https://github.com/nickschot/ember-mobile-menu/pull/1077) Add support for tracked-built-ins v4 ([@nickschot](https://github.com/nickschot)) 58 | * [#1064](https://github.com/nickschot/ember-mobile-menu/pull/1064) Make @glimmer/component & @glimmer/component peerDependency instead of dependency & support 2.0.0 ([@nickschot](https://github.com/nickschot)) 59 | * [#1073](https://github.com/nickschot/ember-mobile-menu/pull/1073) Replace ember-resources & friends with native code ([@nickschot](https://github.com/nickschot)) 60 | 61 | #### :bug: Bug Fix 62 | * `ember-mobile-menu` 63 | * [#1064](https://github.com/nickschot/ember-mobile-menu/pull/1064) Make @glimmer/component & @glimmer/component peerDependency instead of dependency & support 2.0.0 ([@nickschot](https://github.com/nickschot)) 64 | 65 | #### :house: Internal 66 | * `docs`, `ember-mobile-menu`, `test-app` 67 | * [#1072](https://github.com/nickschot/ember-mobile-menu/pull/1072) Upgrade to latest v2 blueprint structure ([@nickschot](https://github.com/nickschot)) 68 | * `test-app` 69 | * [#1071](https://github.com/nickschot/ember-mobile-menu/pull/1071) Add all ember v5 LTS versions to ember-try ([@nickschot](https://github.com/nickschot)) 70 | * Other 71 | * [#898](https://github.com/nickschot/ember-mobile-menu/pull/898) Prepare Release ([@github-actions[bot]](https://github.com/apps/github-actions)) 72 | * [#897](https://github.com/nickschot/ember-mobile-menu/pull/897) Fix docs deploy ([@nickschot](https://github.com/nickschot)) 73 | 74 | #### Committers: 2 75 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 76 | - [@github-actions[bot]](https://github.com/apps/github-actions) 77 | 78 | ## Release (2024-02-16) 79 | 80 | 81 | 82 | #### :house: Internal 83 | * [#896](https://github.com/nickschot/ember-mobile-menu/pull/896) Update dependency release-plan to v0.8.0 ([@renovate[bot]](https://github.com/apps/renovate)) 84 | * [#897](https://github.com/nickschot/ember-mobile-menu/pull/897) Fix docs deploy ([@nickschot](https://github.com/nickschot)) 85 | 86 | #### Committers: 1 87 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 88 | 89 | ## Release (2024-02-16) 90 | 91 | ember-mobile-menu 5.1.0 (minor) 92 | 93 | #### :rocket: Enhancement 94 | * `ember-mobile-menu` 95 | * [#891](https://github.com/nickschot/ember-mobile-menu/pull/891) Inline body scroll lock library, apply small fixes & (unofficial) fastboot support ([@nickschot](https://github.com/nickschot)) 96 | 97 | #### :house: Internal 98 | * [#892](https://github.com/nickschot/ember-mobile-menu/pull/892) Setup release-plan ([@nickschot](https://github.com/nickschot)) 99 | 100 | #### Committers: 1 101 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 102 | 103 | ## v5.0.0 (2024-02-09) 104 | 105 | #### :boom: Breaking Change 106 | * [#877](https://github.com/nickschot/ember-mobile-menu/pull/877) Require ember-resources >=v6.4.0 (inc. v7) ([@nickschot](https://github.com/nickschot)) 107 | * [#821](https://github.com/nickschot/ember-mobile-menu/pull/821) Drop support for mm.LinkTo component, use LinkTo with on click modifier instead ([@nickschot](https://github.com/nickschot)) 108 | * [#779](https://github.com/nickschot/ember-mobile-menu/pull/779) Replace SCSS with plain CSS approach ([@nickschot](https://github.com/nickschot)) 109 | * [#778](https://github.com/nickschot/ember-mobile-menu/pull/778) Convert to v2 addon ([@nickschot](https://github.com/nickschot)) 110 | * [#744](https://github.com/nickschot/ember-mobile-menu/pull/744) Replace tracked-maps-and-sets with tracked-built-ins ([@nickschot](https://github.com/nickschot)) 111 | * [#742](https://github.com/nickschot/ember-mobile-menu/pull/742) Drop ember-source 3.28 ([@nickschot](https://github.com/nickschot)) 112 | 113 | #### :rocket: Enhancement 114 | * [#878](https://github.com/nickschot/ember-mobile-menu/pull/878) Allow ember-concurrency v4 ([@nickschot](https://github.com/nickschot)) 115 | * [#855](https://github.com/nickschot/ember-mobile-menu/pull/855) Implement basic scale correction for gestures ([@nickschot](https://github.com/nickschot)) 116 | * [#827](https://github.com/nickschot/ember-mobile-menu/pull/827) Add ember-gesture-modifiers v6 to allowed range ([@nickschot](https://github.com/nickschot)) 117 | * [#804](https://github.com/nickschot/ember-mobile-menu/pull/804) Re-add support for ember-source v3.28.0 ([@nickschot](https://github.com/nickschot)) 118 | * [#798](https://github.com/nickschot/ember-mobile-menu/pull/798) Replace ember-could-get-used-to-this with ember-resources (by @johanrd ) ([@nickschot](https://github.com/nickschot)) 119 | * [#779](https://github.com/nickschot/ember-mobile-menu/pull/779) Replace SCSS with plain CSS approach ([@nickschot](https://github.com/nickschot)) 120 | 121 | #### :memo: Documentation 122 | * [#822](https://github.com/nickschot/ember-mobile-menu/pull/822) Update docs with style customization section ([@nickschot](https://github.com/nickschot)) 123 | 124 | #### :house: Internal 125 | * [#820](https://github.com/nickschot/ember-mobile-menu/pull/820) Fix workspaces reference in package.json ([@nickschot](https://github.com/nickschot)) 126 | * [#819](https://github.com/nickschot/ember-mobile-menu/pull/819) Remove prepare from root package.json ([@nickschot](https://github.com/nickschot)) 127 | * [#804](https://github.com/nickschot/ember-mobile-menu/pull/804) Re-add support for ember-source v3.28.0 ([@nickschot](https://github.com/nickschot)) 128 | * [#816](https://github.com/nickschot/ember-mobile-menu/pull/816) Add npmrc w/ proper peerDep config, fix monorepo resolution ([@nickschot](https://github.com/nickschot)) 129 | * [#810](https://github.com/nickschot/ember-mobile-menu/pull/810) Downgrade to actions/setup-node@v3 due to performance bug with v4 ([@nickschot](https://github.com/nickschot)) 130 | * [#788](https://github.com/nickschot/ember-mobile-menu/pull/788) Update dependency rollup to v4 ([@renovate[bot]](https://github.com/apps/renovate)) 131 | * [#800](https://github.com/nickschot/ember-mobile-menu/pull/800) Specify ember-source >=v4 as a peer dependency ([@nickschot](https://github.com/nickschot)) 132 | * [#791](https://github.com/nickschot/ember-mobile-menu/pull/791) Enable readme/license copying to published addon ([@nickschot](https://github.com/nickschot)) 133 | * [#790](https://github.com/nickschot/ember-mobile-menu/pull/790) Remove Travis CI badge ([@nickschot](https://github.com/nickschot)) 134 | * [#789](https://github.com/nickschot/ember-mobile-menu/pull/789) Remove v1 addon/cleanup ([@nickschot](https://github.com/nickschot)) 135 | * [#745](https://github.com/nickschot/ember-mobile-menu/pull/745) Remove reliance on sinon ([@nickschot](https://github.com/nickschot)) 136 | 137 | #### Committers: 1 138 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 139 | - johanrd ([@johanrd](https://github.com/johanrd)) 140 | 141 | ## v5.0.0-2 (2023-12-01) 142 | 143 | #### :boom: Breaking Change 144 | * [#821](https://github.com/nickschot/ember-mobile-menu/pull/821) Drop support for mm.LinkTo component, use LinkTo with on click modifier instead ([@nickschot](https://github.com/nickschot)) 145 | 146 | #### :memo: Documentation 147 | * [#822](https://github.com/nickschot/ember-mobile-menu/pull/822) Update docs with style customization section ([@nickschot](https://github.com/nickschot)) 148 | 149 | #### :house: Internal 150 | * [#820](https://github.com/nickschot/ember-mobile-menu/pull/820) Fix workspaces reference in package.json ([@nickschot](https://github.com/nickschot)) 151 | * [#819](https://github.com/nickschot/ember-mobile-menu/pull/819) Remove prepare from root package.json ([@nickschot](https://github.com/nickschot)) 152 | 153 | #### Committers: 1 154 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 155 | 156 | ## v5.0.0-1 (2023-12-01) 157 | 158 | #### :rocket: Enhancement 159 | * [#804](https://github.com/nickschot/ember-mobile-menu/pull/804) Re-add support for ember-source v3.28.0 ([@nickschot](https://github.com/nickschot)) 160 | * [#798](https://github.com/nickschot/ember-mobile-menu/pull/798) Replace ember-could-get-used-to-this with ember-resources (by @johanrd ) ([@nickschot](https://github.com/nickschot)) 161 | 162 | #### :house: Internal 163 | * [#804](https://github.com/nickschot/ember-mobile-menu/pull/804) Re-add support for ember-source v3.28.0 ([@nickschot](https://github.com/nickschot)) 164 | * [#816](https://github.com/nickschot/ember-mobile-menu/pull/816) Add npmrc w/ proper peerDep config, fix monorepo resolution ([@nickschot](https://github.com/nickschot)) 165 | * [#810](https://github.com/nickschot/ember-mobile-menu/pull/810) Downgrade to actions/setup-node@v3 due to performance bug with v4 ([@nickschot](https://github.com/nickschot)) 166 | * [#788](https://github.com/nickschot/ember-mobile-menu/pull/788) Update dependency rollup to v4 ([@renovate[bot]](https://github.com/apps/renovate)) 167 | * [#800](https://github.com/nickschot/ember-mobile-menu/pull/800) Specify ember-source >=v4 as a peer dependency ([@nickschot](https://github.com/nickschot)) 168 | 169 | #### Committers: 1 170 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 171 | 172 | ## v5.0.0-0 (2023-11-17) 173 | 174 | #### :boom: Breaking Change 175 | * [#779](https://github.com/nickschot/ember-mobile-menu/pull/779) Replace SCSS with plain CSS approach ([@nickschot](https://github.com/nickschot)) 176 | * [#778](https://github.com/nickschot/ember-mobile-menu/pull/778) Convert to v2 addon ([@nickschot](https://github.com/nickschot)) 177 | * [#744](https://github.com/nickschot/ember-mobile-menu/pull/744) Replace tracked-maps-and-sets with tracked-built-ins ([@nickschot](https://github.com/nickschot)) 178 | * [#742](https://github.com/nickschot/ember-mobile-menu/pull/742) Drop ember-source 3.28 ([@nickschot](https://github.com/nickschot)) 179 | 180 | #### :rocket: Enhancement 181 | * [#779](https://github.com/nickschot/ember-mobile-menu/pull/779) Replace SCSS with plain CSS approach ([@nickschot](https://github.com/nickschot)) 182 | 183 | #### :house: Internal 184 | * [#791](https://github.com/nickschot/ember-mobile-menu/pull/791) Enable readme/license copying to published addon ([@nickschot](https://github.com/nickschot)) 185 | * [#790](https://github.com/nickschot/ember-mobile-menu/pull/790) Remove Travis CI badge ([@nickschot](https://github.com/nickschot)) 186 | * [#789](https://github.com/nickschot/ember-mobile-menu/pull/789) Remove v1 addon/cleanup ([@nickschot](https://github.com/nickschot)) 187 | * [#745](https://github.com/nickschot/ember-mobile-menu/pull/745) Remove reliance on sinon ([@nickschot](https://github.com/nickschot)) 188 | 189 | #### Committers: 1 190 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 191 | 192 | ## v4.0.0 (2023-10-20) 193 | 194 | #### :boom: Breaking Change 195 | * [#734](https://github.com/nickschot/ember-mobile-menu/pull/734) Drop Node 16 support ([@nickschot](https://github.com/nickschot)) 196 | 197 | #### Committers: 1 198 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 199 | 200 | ## v4.0.0-beta.0 (2023-08-11) 201 | 202 | #### :boom: Breaking Change 203 | * [#687](https://github.com/nickschot/ember-mobile-menu/pull/687) Drop support for node v14 ([@nickschot](https://github.com/nickschot)) 204 | * [#685](https://github.com/nickschot/ember-mobile-menu/pull/685) Drop support for Ember 3.24 ([@nickschot](https://github.com/nickschot)) 205 | * [#631](https://github.com/nickschot/ember-mobile-menu/pull/631) Update dependency ember-on-resize-modifier to v2 ([@renovate[bot]](https://github.com/apps/renovate)) 206 | * [#576](https://github.com/nickschot/ember-mobile-menu/pull/576) Update dependency ember-gesture-modifiers to v5 ([@renovate[bot]](https://github.com/apps/renovate)) 207 | 208 | #### :rocket: Enhancement 209 | * [#688](https://github.com/nickschot/ember-mobile-menu/pull/688) Allow ember-concurrency v3 ([@nickschot](https://github.com/nickschot)) 210 | 211 | #### :bug: Bug Fix 212 | * [#633](https://github.com/nickschot/ember-mobile-menu/pull/633) Set correct supported ember-source as peerDependency ([@nickschot](https://github.com/nickschot)) 213 | * [#682](https://github.com/nickschot/ember-mobile-menu/pull/682) Fix ember-cli-sass check not working under ember-cli 5/embroider ([@nickschot](https://github.com/nickschot)) 214 | 215 | #### :house: Internal 216 | * [#686](https://github.com/nickschot/ember-mobile-menu/pull/686) Add LTS 4.4, 4.8 and 4.12 to test-matrix ([@nickschot](https://github.com/nickschot)) 217 | * [#681](https://github.com/nickschot/ember-mobile-menu/pull/681) Update test-app to ember v5, CI node to 16 ([@nickschot](https://github.com/nickschot)) 218 | * [#679](https://github.com/nickschot/ember-mobile-menu/pull/679) Update to stable lerna-changelog package ([@nickschot](https://github.com/nickschot)) 219 | * [#647](https://github.com/nickschot/ember-mobile-menu/pull/647) Allow ember-beta and ember-canary to fail ([@nickschot](https://github.com/nickschot)) 220 | * [#575](https://github.com/nickschot/ember-mobile-menu/pull/575) Add missing peerDeps ([@nickschot](https://github.com/nickschot)) 221 | * [#574](https://github.com/nickschot/ember-mobile-menu/pull/574) Upgrade to eslint v8 ([@nickschot](https://github.com/nickschot)) 222 | 223 | #### Committers: 1 224 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 225 | 226 | ## v3.0.3 (2022-07-15) 227 | 228 | ## v3.0.2 (2022-07-15) 229 | 230 | #### :house: Internal 231 | * [#347](https://github.com/nickschot/ember-mobile-menu/pull/347) Update dependency ember-cli-addon-docs to v5 ([@renovate[bot]](https://github.com/apps/renovate)) 232 | 233 | ## v3.0.1 (2022-07-15) 234 | 235 | #### :memo: Documentation 236 | * [#365](https://github.com/nickschot/ember-mobile-menu/pull/365) Move README, CONTRIBUTING and LICENSE.md to addon folder ([@nickschot](https://github.com/nickschot)) 237 | 238 | #### Committers: 1 239 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 240 | 241 | ## v3.0.0 (2022-07-15) 242 | 243 | #### :boom: Breaking Change 244 | * [#356](https://github.com/nickschot/ember-mobile-menu/pull/356) Drop node 12 support ([@nickschot](https://github.com/nickschot)) 245 | 246 | #### :house: Internal 247 | * [#361](https://github.com/nickschot/ember-mobile-menu/pull/361) remove ember-cli-htmlbars override now that 6.1.0 has been released ([@nickschot](https://github.com/nickschot)) 248 | * [#358](https://github.com/nickschot/ember-mobile-menu/pull/358) Upgrade to pnpm v7 ([@nickschot](https://github.com/nickschot)) 249 | * [#357](https://github.com/nickschot/ember-mobile-menu/pull/357) declare ember-source peer dependency for addon ([@nickschot](https://github.com/nickschot)) 250 | 251 | #### Committers: 1 252 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 253 | 254 | ## v3.0.0-beta.3 (2022-04-03) 255 | 256 | #### :house: Internal 257 | * [#238](https://github.com/nickschot/ember-mobile-menu/pull/238) Revert "Prevent open/close from being triggered a huge amount of times when unnecessary" ([@nickschot](https://github.com/nickschot)) 258 | 259 | #### Committers: 1 260 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 261 | 262 | ## v3.0.0-beta.2 (2022-04-03) 263 | 264 | #### :house: Internal 265 | * [#261](https://github.com/nickschot/ember-mobile-menu/pull/261) Move npmignore to addon package ([@nickschot](https://github.com/nickschot)) 266 | 267 | #### Committers: 1 268 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 269 | 270 | ## v3.0.0-beta.1 (2022-04-03) 271 | 272 | #### :boom: Breaking Change 273 | * [#234](https://github.com/nickschot/ember-mobile-menu/pull/234) Update dependency ember-gesture-modifiers to v3 ([@renovate[bot]](https://github.com/apps/renovate)) 274 | * [#235](https://github.com/nickschot/ember-mobile-menu/pull/235) Update dependency ember-on-resize-modifier to v1 ([@renovate[bot]](https://github.com/apps/renovate)) 275 | * [#243](https://github.com/nickschot/ember-mobile-menu/pull/243) Drop Ember 3.23 support ([@nickschot](https://github.com/nickschot)) 276 | * [#109](https://github.com/nickschot/ember-mobile-menu/pull/109) replace ember-usable with ember-could-get-used-to-this ([@nickschot](https://github.com/nickschot)) 277 | * [#155](https://github.com/nickschot/ember-mobile-menu/pull/155) drop support for Ember < 3.23 ([@nickschot](https://github.com/nickschot)) 278 | * [#147](https://github.com/nickschot/ember-mobile-menu/pull/147) put splattributes on the Tray instead of the (less useful) wrapper div of the MobileMenu component ([@nickschot](https://github.com/nickschot)) 279 | * [#150](https://github.com/nickschot/ember-mobile-menu/pull/150) upgrade to ember-auto-import v2 ([@nickschot](https://github.com/nickschot)) 280 | * [#103](https://github.com/nickschot/ember-mobile-menu/pull/103) upgrade to ember-gesture-modifiers v1 & PointerEvents ([@nickschot](https://github.com/nickschot)) 281 | * [#136](https://github.com/nickschot/ember-mobile-menu/pull/136) drope node 10 support ([@nickschot](https://github.com/nickschot)) 282 | * [#120](https://github.com/nickschot/ember-mobile-menu/pull/120) drop support for ember-concurrency v1 ([@nickschot](https://github.com/nickschot)) 283 | 284 | #### :bug: Bug Fix 285 | * [#200](https://github.com/nickschot/ember-mobile-menu/pull/200) Prevent open/close from being triggered a huge amount of times when unnecessary ([@nickschot](https://github.com/nickschot)) 286 | * [#188](https://github.com/nickschot/ember-mobile-menu/pull/188) fix import path for htmlSafe ([@nickschot](https://github.com/nickschot)) 287 | * [#190](https://github.com/nickschot/ember-mobile-menu/pull/190) fix service injection deprecation ([@nickschot](https://github.com/nickschot)) 288 | 289 | #### :house: Internal 290 | * [#259](https://github.com/nickschot/ember-mobile-menu/pull/259) Setup release-it ([@nickschot](https://github.com/nickschot)) 291 | * [#254](https://github.com/nickschot/ember-mobile-menu/pull/254) Re-enable embroider test-support on CI ([@nickschot](https://github.com/nickschot)) 292 | * [#256](https://github.com/nickschot/ember-mobile-menu/pull/256) Convert addon to monorepo ([@nickschot](https://github.com/nickschot)) 293 | * [#255](https://github.com/nickschot/ember-mobile-menu/pull/255) Switch to pnpm ([@nickschot](https://github.com/nickschot)) 294 | * [#242](https://github.com/nickschot/ember-mobile-menu/pull/242) Temporarily disable Embroider tests in CI ([@nickschot](https://github.com/nickschot)) 295 | * [#216](https://github.com/nickschot/ember-mobile-menu/pull/216) add ember-keyboard v7 resolution to fix ember 4+ CI ([@nickschot](https://github.com/nickschot)) 296 | * [#210](https://github.com/nickschot/ember-mobile-menu/pull/210) Fix {{hash}} deprecation in tests ([@nickschot](https://github.com/nickschot)) 297 | * [#208](https://github.com/nickschot/ember-mobile-menu/pull/208) Add ember-href-to resolution ([@nickschot](https://github.com/nickschot)) 298 | * [#209](https://github.com/nickschot/ember-mobile-menu/pull/209) Update github CI to latest ember-cli blueprint ([@nickschot](https://github.com/nickschot)) 299 | * [#189](https://github.com/nickschot/ember-mobile-menu/pull/189) update to addon-docs v4 ([@nickschot](https://github.com/nickschot)) 300 | * [#152](https://github.com/nickschot/ember-mobile-menu/pull/152) upgrade to ember-cli 3.27 blueprint ([@nickschot](https://github.com/nickschot)) 301 | * [#121](https://github.com/nickschot/ember-mobile-menu/pull/121) use ember-concurrency 2 task syntax ([@nickschot](https://github.com/nickschot)) 302 | 303 | #### Committers: 1 304 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 305 | 306 | ## v2.1.1 (2020-12-04) 307 | 308 | #### :memo: Documentation 309 | * [#108](https://github.com/nickschot/ember-mobile-menu/pull/108) manually add application wrapper to dummy app as addon docs styles expect it ([@nickschot](https://github.com/nickschot)) 310 | 311 | #### Committers: 1 312 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 313 | 314 | 315 | ## v2.1.0 (2020-12-03) 316 | 317 | #### :rocket: Enhancement 318 | * [#102](https://github.com/nickschot/ember-mobile-menu/pull/102) upgrade to ember-cli 3.22 & update dependencies ([@nickschot](https://github.com/nickschot)) 319 | 320 | #### :bug: Bug Fix 321 | * [#105](https://github.com/nickschot/ember-mobile-menu/pull/105) properly cleanup body-scroll-lock when the tray component is destroyed ([@nickschot](https://github.com/nickschot)) 322 | * [#100](https://github.com/nickschot/ember-mobile-menu/pull/100) remove old service re-export (fixes embroider build) ([@johanrd](https://github.com/johanrd)) 323 | * [#98](https://github.com/nickschot/ember-mobile-menu/pull/98) fix ember-usable package.json ref (for yarn 2) ([@bartocc](https://github.com/bartocc)) 324 | 325 | #### :house: Internal 326 | * [#107](https://github.com/nickschot/ember-mobile-menu/pull/107) add ember-data dev dependency ([@nickschot](https://github.com/nickschot)) 327 | * [#106](https://github.com/nickschot/ember-mobile-menu/pull/106) Github CI: allow canary tests to fail, remove job dependencies ([@nickschot](https://github.com/nickschot)) 328 | * [#104](https://github.com/nickschot/ember-mobile-menu/pull/104) switch to github CI ([@nickschot](https://github.com/nickschot)) 329 | 330 | #### Committers: 3 331 | - Julien Palmas ([@bartocc](https://github.com/bartocc)) 332 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 333 | - [@johanrd](https://github.com/johanrd) 334 | 335 | 336 | ## v2.0.5 (2020-08-26) 337 | 338 | #### :bug: Bug Fix 339 | * [#91](https://github.com/nickschot/ember-mobile-menu/pull/91) fix build issue by upgrading to ember-modifier 2.1.0 ([@nickschot](https://github.com/nickschot)) 340 | 341 | #### :memo: Documentation 342 | * [#93](https://github.com/nickschot/ember-mobile-menu/pull/93) upgrade ember-cli-addon-docs to 0.9.0 ([@nickschot](https://github.com/nickschot)) 343 | 344 | #### Committers: 1 345 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 346 | 347 | 348 | ## v2.0.4 (2020-08-23) 349 | 350 | #### :bug: Bug Fix 351 | * [#92](https://github.com/nickschot/ember-mobile-menu/pull/92) fix horizontal scrollbar appearing in certain situations ([@nickschot](https://github.com/nickschot)) 352 | 353 | #### :house: Internal 354 | * [#90](https://github.com/nickschot/ember-mobile-menu/pull/90) upgrade to ember-gesture-modifiers 0.2.0 ([@nickschot](https://github.com/nickschot)) 355 | 356 | #### Committers: 1 357 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 358 | 359 | 360 | ## v2.0.3 (2020-08-04) 361 | 362 | #### :bug: Bug Fix 363 | * [#89](https://github.com/nickschot/ember-mobile-menu/pull/89) don't require @glimmer/{component,tracking} 1.0.1+ ([@nickschot](https://github.com/nickschot)) 364 | 365 | #### Committers: 1 366 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 367 | 368 | 369 | ## v2.0.2 (2020-08-03) 370 | 371 | #### :bug: Bug Fix 372 | * [#86](https://github.com/nickschot/ember-mobile-menu/pull/86) set aria-hidden="true" on closed MobileMenus ([@nickschot](https://github.com/nickschot)) 373 | * [#85](https://github.com/nickschot/ember-mobile-menu/pull/85) make squeeze(-reveal) menus listen to the preventScroll argument ([@nickschot](https://github.com/nickschot)) 374 | 375 | #### Committers: 1 376 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 377 | 378 | 379 | ## v2.0.1 (2020-08-02) 380 | 381 | #### :bug: Bug Fix 382 | * [#83](https://github.com/nickschot/ember-mobile-menu/pull/83) hide secondary mask for push style menus ([@nickschot](https://github.com/nickschot)) 383 | * [#82](https://github.com/nickschot/ember-mobile-menu/pull/82) hide MobileMenuWrapper overflow & add prevent scroll class as soon as the menu is not closed ([@nickschot](https://github.com/nickschot)) 384 | 385 | #### :house: Internal 386 | * [#81](https://github.com/nickschot/ember-mobile-menu/pull/81) tweak spring to iOS configuration ([@nickschot](https://github.com/nickschot)) 387 | 388 | #### Committers: 1 389 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 390 | 391 | 392 | ## v2.0.0 (2020-08-01) 393 | 394 | #### :boom: Breaking Change 395 | * [#75](https://github.com/nickschot/ember-mobile-menu/pull/75) switch to a ResizeObserver based on-resize modifier ([@nickschot](https://github.com/nickschot)) 396 | 397 | #### :rocket: Enhancement 398 | * [#79](https://github.com/nickschot/ember-mobile-menu/pull/79) add isOpen & onToggle arguments to MobileMenu component (w/ Fastboot support) ([@nickschot](https://github.com/nickschot)) 399 | * [#74](https://github.com/nickschot/ember-mobile-menu/pull/74) spring physics implementation to replace basic tween ([@nickschot](https://github.com/nickschot)) 400 | 401 | #### :bug: Bug Fix 402 | * [#78](https://github.com/nickschot/ember-mobile-menu/pull/78) don't prevent body scroll when the menu is in embed mode ([@nickschot](https://github.com/nickschot)) 403 | * [#77](https://github.com/nickschot/ember-mobile-menu/pull/77) prevent body scroll as soon as the menu is not closed ([@nickschot](https://github.com/nickschot)) 404 | * [#76](https://github.com/nickschot/ember-mobile-menu/pull/76) fix iOS safari menu overflow when browser chrome is visible ([@nickschot](https://github.com/nickschot)) 405 | 406 | #### :house: Internal 407 | * [#80](https://github.com/nickschot/ember-mobile-menu/pull/80) upgrade to ember-cli 3.20 & latest dependencies ([@nickschot](https://github.com/nickschot)) 408 | 409 | #### Committers: 1 410 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 411 | 412 | 413 | ## v2.0.0-beta.4 (2020-03-29) 414 | 415 | #### :rocket: Enhancement 416 | * [#68](https://github.com/nickschot/ember-mobile-menu/pull/68) yield absolute position of active menu from MobileMenuWrapper component ([@nickschot](https://github.com/nickschot)) 417 | * [#67](https://github.com/nickschot/ember-mobile-menu/pull/67) yield relative position of active menu from MobileMenuWrapper component ([@nickschot](https://github.com/nickschot)) 418 | 419 | #### :house: Internal 420 | * [#66](https://github.com/nickschot/ember-mobile-menu/pull/66) remove reliance on ember-useragent for detecting iOS ([@nickschot](https://github.com/nickschot)) 421 | 422 | #### Committers: 1 423 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 424 | 425 | 426 | ## v2.0.0-beta.3 (2020-03-27) 427 | 428 | #### :rocket: Enhancement 429 | * [#64](https://github.com/nickschot/ember-mobile-menu/pull/64) more rigorously prevent body scroll when non-squeeze menus are open ([@nickschot](https://github.com/nickschot)) 430 | * [#61](https://github.com/nickschot/ember-mobile-menu/pull/61) add invisible content mask for reveal/ios/push menus ([@nickschot](https://github.com/nickschot)) 431 | 432 | #### :bug: Bug Fix 433 | * [#62](https://github.com/nickschot/ember-mobile-menu/pull/62) don't close menu when clicking on a link in one of the squeeze modes ([@nickschot](https://github.com/nickschot)) 434 | * [#58](https://github.com/nickschot/ember-mobile-menu/pull/58) add missing ember-concurrency dependency ([@nickschot](https://github.com/nickschot)) 435 | * [#59](https://github.com/nickschot/ember-mobile-menu/pull/59) fix css not included in host app if ember-cli-sass isn't used ([@nickschot](https://github.com/nickschot)) 436 | * [#60](https://github.com/nickschot/ember-mobile-menu/pull/60) fix minor css issues ([@nickschot](https://github.com/nickschot)) 437 | 438 | #### Committers: 1 439 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 440 | 441 | 442 | ## v2.0.0-beta.2 (2020-03-24) 443 | 444 | #### :boom: Breaking Change 445 | * [#57](https://github.com/nickschot/ember-mobile-menu/pull/57) link body scroll behaviour to menu settings & enable preventScroll by default ([@nickschot](https://github.com/nickschot)) 446 | 447 | #### :house: Internal 448 | * [#56](https://github.com/nickschot/ember-mobile-menu/pull/56) fix childmenus tracking & registration ([@nickschot](https://github.com/nickschot)) 449 | 450 | #### Committers: 1 451 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 452 | 453 | 454 | ## v2.0.0-beta.1 (2020-03-23) 455 | 456 | #### :boom: Breaking Change 457 | * [#42](https://github.com/nickschot/ember-mobile-menu/pull/42) implement multiple modes (inc better UX for desktop) ([@nickschot](https://github.com/nickschot)) 458 | * [#36](https://github.com/nickschot/ember-mobile-menu/pull/36) set minimum supported version to ember-cli 3.12 + ember-source 3.13 ([@nickschot](https://github.com/nickschot)) 459 | * [#32](https://github.com/nickschot/ember-mobile-menu/pull/32) migrate to glimmer components ([@nickschot](https://github.com/nickschot)) 460 | * [#31](https://github.com/nickschot/ember-mobile-menu/pull/31) bump minimum node to 10 ([@nickschot](https://github.com/nickschot)) 461 | 462 | #### :rocket: Enhancement 463 | * [#52](https://github.com/nickschot/ember-mobile-menu/pull/52) ignore pan events that start near the edge on non-standalone iOS browsers ([@nickschot](https://github.com/nickschot)) 464 | * [#44](https://github.com/nickschot/ember-mobile-menu/pull/44) add support for different mode per menu ([@nickschot](https://github.com/nickschot)) 465 | * [#43](https://github.com/nickschot/ember-mobile-menu/pull/43) rework pan handling ([@nickschot](https://github.com/nickschot)) 466 | * [#42](https://github.com/nickschot/ember-mobile-menu/pull/42) implement multiple modes (inc better UX for desktop) ([@nickschot](https://github.com/nickschot)) 467 | * [#40](https://github.com/nickschot/ember-mobile-menu/pull/40) make maxWidth optional (enables 100% width menus) ([@nickschot](https://github.com/nickschot)) 468 | * [#34](https://github.com/nickschot/ember-mobile-menu/pull/34) automatically toggle the only available menu if no target is passed ([@nickschot](https://github.com/nickschot)) 469 | * [#33](https://github.com/nickschot/ember-mobile-menu/pull/33) add dynamic shadow feature (intensity based on drag position) ([@nickschot](https://github.com/nickschot)) 470 | * [#29](https://github.com/nickschot/ember-mobile-menu/pull/29) add support for embedding a menu in another element ([@nickschot](https://github.com/nickschot)) 471 | 472 | #### :bug: Bug Fix 473 | * [#48](https://github.com/nickschot/ember-mobile-menu/pull/48) fix mode reverting to default when dragging non-default menu to a close ([@nickschot](https://github.com/nickschot)) 474 | 475 | #### :memo: Documentation 476 | * [#50](https://github.com/nickschot/ember-mobile-menu/pull/50) update & extend documentation ([@nickschot](https://github.com/nickschot)) 477 | * [#47](https://github.com/nickschot/ember-mobile-menu/pull/47) add configurable landing page demo ([@nickschot](https://github.com/nickschot)) 478 | * [#30](https://github.com/nickschot/ember-mobile-menu/pull/30) improve documentation with left/right/multiple menu setups ([@nickschot](https://github.com/nickschot)) 479 | 480 | #### :house: Internal 481 | * [#51](https://github.com/nickschot/ember-mobile-menu/pull/51) inline the micro tween engine from ember-mobile-core ([@nickschot](https://github.com/nickschot)) 482 | * [#49](https://github.com/nickschot/ember-mobile-menu/pull/49) update ember-source & ember-try config ([@nickschot](https://github.com/nickschot)) 483 | * [#38](https://github.com/nickschot/ember-mobile-menu/pull/38) minimize calls to getBoundingClientRect ([@nickschot](https://github.com/nickschot)) 484 | 485 | #### Committers: 1 486 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 487 | 488 | 489 | ## v0.1.0-beta.2 (2019-12-13) 490 | 491 | #### :rocket: Enhancement 492 | * [#28](https://github.com/nickschot/ember-mobile-menu/pull/28) upgrade to ember-cli 3.12.0 & upgrade dependencies ([@nickschot](https://github.com/nickschot)) 493 | 494 | #### :house: Internal 495 | * [#24](https://github.com/nickschot/ember-mobile-menu/pull/24) Add drag tests & pan simulator ([@nickschot](https://github.com/nickschot)) 496 | 497 | #### Committers: 1 498 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 499 | 500 | 501 | ## v0.1.0-beta.1 (2019-03-29) 502 | 503 | #### :boom: Breaking Change 504 | * [#22](https://github.com/nickschot/ember-mobile-menu/pull/22) use ember-concurrency for opening/closing menu ([@nickschot](https://github.com/nickschot)) 505 | 506 | #### :rocket: Enhancement 507 | * [#19](https://github.com/nickschot/ember-mobile-menu/pull/19) ember-cli 3.8 & dependency updates ([@nickschot](https://github.com/nickschot)) 508 | * [#18](https://github.com/nickschot/ember-mobile-menu/pull/18) upgrade ember-useragent to 0.9.0 ([@nickschot](https://github.com/nickschot)) 509 | * [#17](https://github.com/nickschot/ember-mobile-menu/pull/17) Precompile CSS & include precompiled CSS if ember-cli-sass is not installed ([@nickschot](https://github.com/nickschot)) 510 | 511 | #### :memo: Documentation 512 | * [#20](https://github.com/nickschot/ember-mobile-menu/pull/20) Documentation improvements & cleanup ([@nickschot](https://github.com/nickschot)) 513 | 514 | #### :house: Internal 515 | * [#21](https://github.com/nickschot/ember-mobile-menu/pull/21) improve test suite ([@nickschot](https://github.com/nickschot)) 516 | * [#23](https://github.com/nickschot/ember-mobile-menu/pull/23) pin nwsapi to release that supports node 6 ([@nickschot](https://github.com/nickschot)) 517 | 518 | #### Committers: 1 519 | - Nick Schot ([@nickschot](https://github.com/nickschot)) 520 | --------------------------------------------------------------------------------