├── 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 |
32 |
40 |
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 |
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 |
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 | [](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 |
45 |
59 | {{yield}}
60 |
61 | {{#if this.mask}}
62 |
63 | {{/if}}
64 |
65 |
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 |
95 |
110 |
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 |
108 |
123 |
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 |
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 |
363 | {{#if this.renderMenu}}
364 | {{effect @register this}}
365 | {{effect this.openOrClose @isOpen}}
366 | {{effect this.close this.type}}
367 | {{effect this.setRendered}}
368 |
369 |
374 | {{#if this.maskEnabled}}
375 |
386 | {{/if}}
387 |
388 |
403 | {{yield (hash actions=(hash open=this.open close=this.close))}}
404 |
405 |
406 | {{/if}}
407 |
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 |
478 | {{#if this.preventBodyScroll}}
479 | {{bodyClass "mobile-menu--prevent-scroll"}}
480 | {{/if}}
481 |
482 |
529 |
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 |
--------------------------------------------------------------------------------