ion-menu.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { proxyCustomElement, HTMLElement, createEvent, Build, h, Host } from '@stencil/core/internal/client';
  5. import { g as getTimeGivenProgression } from './cubic-bezier.js';
  6. import { o as getPresentedOverlay, B as BACKDROP, n as focusFirstDescendant, q as focusLastDescendant, G as GESTURE } from './overlays.js';
  7. import { G as GESTURE_CONTROLLER } from './gesture-controller.js';
  8. import { shouldUseCloseWatcher } from './hardware-back-button.js';
  9. import { m as isEndSide, i as inheritAriaAttributes, n as assert, k as clamp } from './helpers.js';
  10. import { c as config, p as printIonError } from './index4.js';
  11. import { m as menuController } from './index5.js';
  12. import { b as getIonMode, a as isPlatform } from './ionic-global.js';
  13. import { h as hostContext } from './theme.js';
  14. import { d as defineCustomElement$2 } from './backdrop.js';
  15. const menuIosCss = ":host{--width:304px;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--background:var(--ion-background-color, #fff);left:0;right:0;top:0;bottom:0;display:none;position:absolute;contain:strict}:host(.show-menu){display:block}.menu-inner{-webkit-transform:translateX(-9999px);transform:translateX(-9999px);display:-ms-flexbox;display:flex;position:absolute;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:justify;justify-content:space-between;width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);background:var(--background);contain:strict}:host(.menu-side-start) .menu-inner{--ion-safe-area-right:0px;top:0;bottom:0}:host(.menu-side-start) .menu-inner{inset-inline-start:0;inset-inline-end:auto}:host-context([dir=rtl]):host(.menu-side-start) .menu-inner,:host-context([dir=rtl]).menu-side-start .menu-inner{--ion-safe-area-right:unset;--ion-safe-area-left:0px}@supports selector(:dir(rtl)){:host(.menu-side-start:dir(rtl)) .menu-inner{--ion-safe-area-right:unset;--ion-safe-area-left:0px}}:host(.menu-side-end) .menu-inner{--ion-safe-area-left:0px;top:0;bottom:0}:host(.menu-side-end) .menu-inner{inset-inline-start:auto;inset-inline-end:0}:host-context([dir=rtl]):host(.menu-side-end) .menu-inner,:host-context([dir=rtl]).menu-side-end .menu-inner{--ion-safe-area-left:unset;--ion-safe-area-right:0px}@supports selector(:dir(rtl)){:host(.menu-side-end:dir(rtl)) .menu-inner{--ion-safe-area-left:unset;--ion-safe-area-right:0px}}ion-backdrop{display:none;opacity:0.01;z-index:-1}@media (max-width: 340px){.menu-inner{--width:264px}}:host(.menu-type-reveal){z-index:0}:host(.menu-type-reveal.show-menu) .menu-inner{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}:host(.menu-type-overlay){z-index:1000}:host(.menu-type-overlay) .show-backdrop{display:block;cursor:pointer}:host(.menu-pane-visible){-ms-flex:0 1 auto;flex:0 1 auto;width:var(--side-width, var(--width));min-width:var(--side-min-width, var(--min-width));max-width:var(--side-max-width, var(--max-width))}:host(.menu-pane-visible.split-pane-side){left:0;right:0;top:0;bottom:0;position:relative;-webkit-box-shadow:none;box-shadow:none;z-index:0}:host(.menu-pane-visible.split-pane-side.menu-enabled){display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0}:host(.menu-pane-visible.split-pane-side){-ms-flex-order:-1;order:-1}:host(.menu-pane-visible.split-pane-side[side=end]){-ms-flex-order:1;order:1}:host(.menu-pane-visible) .menu-inner{left:0;right:0;width:auto;-webkit-transform:none;transform:none;-webkit-box-shadow:none;box-shadow:none}:host(.menu-pane-visible) ion-backdrop{display:hidden !important}:host(.menu-pane-visible.split-pane-side){-webkit-border-start:0;border-inline-start:0;-webkit-border-end:var(--border);border-inline-end:var(--border);border-top:0;border-bottom:0;min-width:var(--side-min-width);max-width:var(--side-max-width)}:host(.menu-pane-visible.split-pane-side[side=end]){-webkit-border-start:var(--border);border-inline-start:var(--border);-webkit-border-end:0;border-inline-end:0;border-top:0;border-bottom:0;min-width:var(--side-min-width);max-width:var(--side-max-width)}:host(.menu-type-push){z-index:1000}:host(.menu-type-push) .show-backdrop{display:block}";
  16. const IonMenuIosStyle0 = menuIosCss;
  17. const menuMdCss = ":host{--width:304px;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--background:var(--ion-background-color, #fff);left:0;right:0;top:0;bottom:0;display:none;position:absolute;contain:strict}:host(.show-menu){display:block}.menu-inner{-webkit-transform:translateX(-9999px);transform:translateX(-9999px);display:-ms-flexbox;display:flex;position:absolute;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:justify;justify-content:space-between;width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);background:var(--background);contain:strict}:host(.menu-side-start) .menu-inner{--ion-safe-area-right:0px;top:0;bottom:0}:host(.menu-side-start) .menu-inner{inset-inline-start:0;inset-inline-end:auto}:host-context([dir=rtl]):host(.menu-side-start) .menu-inner,:host-context([dir=rtl]).menu-side-start .menu-inner{--ion-safe-area-right:unset;--ion-safe-area-left:0px}@supports selector(:dir(rtl)){:host(.menu-side-start:dir(rtl)) .menu-inner{--ion-safe-area-right:unset;--ion-safe-area-left:0px}}:host(.menu-side-end) .menu-inner{--ion-safe-area-left:0px;top:0;bottom:0}:host(.menu-side-end) .menu-inner{inset-inline-start:auto;inset-inline-end:0}:host-context([dir=rtl]):host(.menu-side-end) .menu-inner,:host-context([dir=rtl]).menu-side-end .menu-inner{--ion-safe-area-left:unset;--ion-safe-area-right:0px}@supports selector(:dir(rtl)){:host(.menu-side-end:dir(rtl)) .menu-inner{--ion-safe-area-left:unset;--ion-safe-area-right:0px}}ion-backdrop{display:none;opacity:0.01;z-index:-1}@media (max-width: 340px){.menu-inner{--width:264px}}:host(.menu-type-reveal){z-index:0}:host(.menu-type-reveal.show-menu) .menu-inner{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}:host(.menu-type-overlay){z-index:1000}:host(.menu-type-overlay) .show-backdrop{display:block;cursor:pointer}:host(.menu-pane-visible){-ms-flex:0 1 auto;flex:0 1 auto;width:var(--side-width, var(--width));min-width:var(--side-min-width, var(--min-width));max-width:var(--side-max-width, var(--max-width))}:host(.menu-pane-visible.split-pane-side){left:0;right:0;top:0;bottom:0;position:relative;-webkit-box-shadow:none;box-shadow:none;z-index:0}:host(.menu-pane-visible.split-pane-side.menu-enabled){display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0}:host(.menu-pane-visible.split-pane-side){-ms-flex-order:-1;order:-1}:host(.menu-pane-visible.split-pane-side[side=end]){-ms-flex-order:1;order:1}:host(.menu-pane-visible) .menu-inner{left:0;right:0;width:auto;-webkit-transform:none;transform:none;-webkit-box-shadow:none;box-shadow:none}:host(.menu-pane-visible) ion-backdrop{display:hidden !important}:host(.menu-pane-visible.split-pane-side){-webkit-border-start:0;border-inline-start:0;-webkit-border-end:var(--border);border-inline-end:var(--border);border-top:0;border-bottom:0;min-width:var(--side-min-width);max-width:var(--side-max-width)}:host(.menu-pane-visible.split-pane-side[side=end]){-webkit-border-start:var(--border);border-inline-start:var(--border);-webkit-border-end:0;border-inline-end:0;border-top:0;border-bottom:0;min-width:var(--side-min-width);max-width:var(--side-max-width)}:host(.menu-type-overlay) .menu-inner{-webkit-box-shadow:4px 0px 16px rgba(0, 0, 0, 0.18);box-shadow:4px 0px 16px rgba(0, 0, 0, 0.18)}";
  18. const IonMenuMdStyle0 = menuMdCss;
  19. const iosEasing = 'cubic-bezier(0.32,0.72,0,1)';
  20. const mdEasing = 'cubic-bezier(0.0,0.0,0.2,1)';
  21. const iosEasingReverse = 'cubic-bezier(1, 0, 0.68, 0.28)';
  22. const mdEasingReverse = 'cubic-bezier(0.4, 0, 0.6, 1)';
  23. const Menu = /*@__PURE__*/ proxyCustomElement(class Menu extends HTMLElement {
  24. constructor() {
  25. super();
  26. this.__registerHost();
  27. this.__attachShadow();
  28. this.ionWillOpen = createEvent(this, "ionWillOpen", 7);
  29. this.ionWillClose = createEvent(this, "ionWillClose", 7);
  30. this.ionDidOpen = createEvent(this, "ionDidOpen", 7);
  31. this.ionDidClose = createEvent(this, "ionDidClose", 7);
  32. this.ionMenuChange = createEvent(this, "ionMenuChange", 7);
  33. this.lastOnEnd = 0;
  34. this.blocker = GESTURE_CONTROLLER.createBlocker({ disableScroll: true });
  35. this.didLoad = false;
  36. /**
  37. * Flag used to determine if an open/close
  38. * operation was cancelled. For example, if
  39. * an app calls "menu.open" then disables the menu
  40. * part way through the animation, then this would
  41. * be considered a cancelled operation.
  42. */
  43. this.operationCancelled = false;
  44. this.isAnimating = false;
  45. this._isOpen = false;
  46. this.inheritedAttributes = {};
  47. this.handleFocus = (ev) => {
  48. /**
  49. * Overlays have their own focus trapping listener
  50. * so we do not want the two listeners to conflict
  51. * with each other. If the top-most overlay that is
  52. * open does not contain this ion-menu, then ion-menu's
  53. * focus trapping should not run.
  54. */
  55. const lastOverlay = getPresentedOverlay(document);
  56. if (lastOverlay && !lastOverlay.contains(this.el)) {
  57. return;
  58. }
  59. this.trapKeyboardFocus(ev, document);
  60. };
  61. this.isPaneVisible = false;
  62. this.isEndSide = false;
  63. this.contentId = undefined;
  64. this.menuId = undefined;
  65. this.type = undefined;
  66. this.disabled = false;
  67. this.side = 'start';
  68. this.swipeGesture = true;
  69. this.maxEdgeStart = 50;
  70. }
  71. typeChanged(type, oldType) {
  72. const contentEl = this.contentEl;
  73. if (contentEl) {
  74. if (oldType !== undefined) {
  75. contentEl.classList.remove(`menu-content-${oldType}`);
  76. }
  77. contentEl.classList.add(`menu-content-${type}`);
  78. contentEl.removeAttribute('style');
  79. }
  80. if (this.menuInnerEl) {
  81. // Remove effects of previous animations
  82. this.menuInnerEl.removeAttribute('style');
  83. }
  84. this.animation = undefined;
  85. }
  86. disabledChanged() {
  87. this.updateState();
  88. this.ionMenuChange.emit({
  89. disabled: this.disabled,
  90. open: this._isOpen,
  91. });
  92. }
  93. sideChanged() {
  94. this.isEndSide = isEndSide(this.side);
  95. /**
  96. * Menu direction animation is calculated based on the document direction.
  97. * If the document direction changes, we need to create a new animation.
  98. */
  99. this.animation = undefined;
  100. }
  101. swipeGestureChanged() {
  102. this.updateState();
  103. }
  104. async connectedCallback() {
  105. // TODO: connectedCallback is fired in CE build
  106. // before WC is defined. This needs to be fixed in Stencil.
  107. if (typeof customElements !== 'undefined' && customElements != null) {
  108. await customElements.whenDefined('ion-menu');
  109. }
  110. if (this.type === undefined) {
  111. this.type = config.get('menuType', 'overlay');
  112. }
  113. if (!Build.isBrowser) {
  114. return;
  115. }
  116. const content = this.contentId !== undefined ? document.getElementById(this.contentId) : null;
  117. if (content === null) {
  118. printIonError('[ion-menu] - Must have a "content" element to listen for drag events on.');
  119. return;
  120. }
  121. if (this.el.contains(content)) {
  122. printIonError(`[ion-menu] - The "contentId" should refer to the main view's ion-content, not the ion-content inside of the ion-menu.`);
  123. }
  124. this.contentEl = content;
  125. // add menu's content classes
  126. content.classList.add('menu-content');
  127. this.typeChanged(this.type, undefined);
  128. this.sideChanged();
  129. // register this menu with the app's menu controller
  130. menuController._register(this);
  131. this.menuChanged();
  132. this.gesture = (await import('./index3.js')).createGesture({
  133. el: document,
  134. gestureName: 'menu-swipe',
  135. gesturePriority: 30,
  136. threshold: 10,
  137. blurOnStart: true,
  138. canStart: (ev) => this.canStart(ev),
  139. onWillStart: () => this.onWillStart(),
  140. onStart: () => this.onStart(),
  141. onMove: (ev) => this.onMove(ev),
  142. onEnd: (ev) => this.onEnd(ev),
  143. });
  144. this.updateState();
  145. }
  146. componentWillLoad() {
  147. this.inheritedAttributes = inheritAriaAttributes(this.el);
  148. }
  149. async componentDidLoad() {
  150. this.didLoad = true;
  151. /**
  152. * A menu inside of a split pane is assumed
  153. * to be a side pane.
  154. *
  155. * When the menu is loaded it needs to
  156. * see if it should be considered visible inside
  157. * of the split pane. If the split pane is
  158. * hidden then the menu should be too.
  159. */
  160. const splitPane = this.el.closest('ion-split-pane');
  161. if (splitPane !== null) {
  162. this.isPaneVisible = await splitPane.isVisible();
  163. }
  164. this.menuChanged();
  165. this.updateState();
  166. }
  167. menuChanged() {
  168. /**
  169. * Inform dependent components such as ion-menu-button
  170. * that the menu is ready. Note that we only want to do this
  171. * once the menu has been rendered which is why we check for didLoad.
  172. */
  173. if (this.didLoad) {
  174. this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
  175. }
  176. }
  177. async disconnectedCallback() {
  178. /**
  179. * The menu should be closed when it is
  180. * unmounted from the DOM.
  181. * This is an async call, so we need to wait for
  182. * this to finish otherwise contentEl
  183. * will not have MENU_CONTENT_OPEN removed.
  184. */
  185. await this.close(false);
  186. this.blocker.destroy();
  187. menuController._unregister(this);
  188. if (this.animation) {
  189. this.animation.destroy();
  190. }
  191. if (this.gesture) {
  192. this.gesture.destroy();
  193. this.gesture = undefined;
  194. }
  195. this.animation = undefined;
  196. this.contentEl = undefined;
  197. }
  198. onSplitPaneChanged(ev) {
  199. const closestSplitPane = this.el.closest('ion-split-pane');
  200. if (closestSplitPane !== null && closestSplitPane === ev.target) {
  201. this.isPaneVisible = ev.detail.visible;
  202. this.updateState();
  203. }
  204. }
  205. onBackdropClick(ev) {
  206. // TODO(FW-2832): type (CustomEvent triggers errors which should be sorted)
  207. if (this._isOpen && this.lastOnEnd < ev.timeStamp - 100) {
  208. const shouldClose = ev.composedPath ? !ev.composedPath().includes(this.menuInnerEl) : false;
  209. if (shouldClose) {
  210. ev.preventDefault();
  211. ev.stopPropagation();
  212. this.close(undefined, BACKDROP);
  213. }
  214. }
  215. }
  216. onKeydown(ev) {
  217. if (ev.key === 'Escape') {
  218. this.close(undefined, BACKDROP);
  219. }
  220. }
  221. /**
  222. * Returns `true` is the menu is open.
  223. */
  224. isOpen() {
  225. return Promise.resolve(this._isOpen);
  226. }
  227. /**
  228. * Returns `true` is the menu is active.
  229. *
  230. * A menu is active when it can be opened or closed, meaning it's enabled
  231. * and it's not part of a `ion-split-pane`.
  232. */
  233. isActive() {
  234. return Promise.resolve(this._isActive());
  235. }
  236. /**
  237. * Opens the menu. If the menu is already open or it can't be opened,
  238. * it returns `false`.
  239. */
  240. open(animated = true) {
  241. return this.setOpen(true, animated);
  242. }
  243. /**
  244. * Closes the menu. If the menu is already closed or it can't be closed,
  245. * it returns `false`.
  246. */
  247. close(animated = true, role) {
  248. return this.setOpen(false, animated, role);
  249. }
  250. /**
  251. * Toggles the menu. If the menu is already open, it will try to close, otherwise it will try to open it.
  252. * If the operation can't be completed successfully, it returns `false`.
  253. */
  254. toggle(animated = true) {
  255. return this.setOpen(!this._isOpen, animated);
  256. }
  257. /**
  258. * Opens or closes the button.
  259. * If the operation can't be completed successfully, it returns `false`.
  260. */
  261. setOpen(shouldOpen, animated = true, role) {
  262. return menuController._setOpen(this, shouldOpen, animated, role);
  263. }
  264. trapKeyboardFocus(ev, doc) {
  265. const target = ev.target;
  266. if (!target) {
  267. return;
  268. }
  269. /**
  270. * If the target is inside the menu contents, let the browser
  271. * focus as normal and keep a log of the last focused element.
  272. */
  273. if (this.el.contains(target)) {
  274. this.lastFocus = target;
  275. }
  276. else {
  277. /**
  278. * Otherwise, we are about to have focus go out of the menu.
  279. * Wrap the focus to either the first or last element.
  280. */
  281. const { el } = this;
  282. /**
  283. * Once we call `focusFirstDescendant`, another focus event
  284. * will fire, which will cause `lastFocus` to be updated
  285. * before we can run the code after that. We cache the value
  286. * here to avoid that.
  287. */
  288. focusFirstDescendant(el);
  289. /**
  290. * If the cached last focused element is the same as the now-
  291. * active element, that means the user was on the first element
  292. * already and pressed Shift + Tab, so we need to wrap to the
  293. * last descendant.
  294. */
  295. if (this.lastFocus === doc.activeElement) {
  296. focusLastDescendant(el);
  297. }
  298. }
  299. }
  300. async _setOpen(shouldOpen, animated = true, role) {
  301. // If the menu is disabled or it is currently being animated, let's do nothing
  302. if (!this._isActive() || this.isAnimating || shouldOpen === this._isOpen) {
  303. return false;
  304. }
  305. this.beforeAnimation(shouldOpen, role);
  306. await this.loadAnimation();
  307. await this.startAnimation(shouldOpen, animated);
  308. /**
  309. * If the animation was cancelled then
  310. * return false because the operation
  311. * did not succeed.
  312. */
  313. if (this.operationCancelled) {
  314. this.operationCancelled = false;
  315. return false;
  316. }
  317. this.afterAnimation(shouldOpen, role);
  318. return true;
  319. }
  320. async loadAnimation() {
  321. // Menu swipe animation takes the menu's inner width as parameter,
  322. // If `offsetWidth` changes, we need to create a new animation.
  323. const width = this.menuInnerEl.offsetWidth;
  324. /**
  325. * Menu direction animation is calculated based on the document direction.
  326. * If the document direction changes, we need to create a new animation.
  327. */
  328. const isEndSide$1 = isEndSide(this.side);
  329. if (width === this.width && this.animation !== undefined && isEndSide$1 === this.isEndSide) {
  330. return;
  331. }
  332. this.width = width;
  333. this.isEndSide = isEndSide$1;
  334. // Destroy existing animation
  335. if (this.animation) {
  336. this.animation.destroy();
  337. this.animation = undefined;
  338. }
  339. // Create new animation
  340. const animation = (this.animation = await menuController._createAnimation(this.type, this));
  341. if (!config.getBoolean('animated', true)) {
  342. animation.duration(0);
  343. }
  344. animation.fill('both');
  345. }
  346. async startAnimation(shouldOpen, animated) {
  347. const isReversed = !shouldOpen;
  348. const mode = getIonMode(this);
  349. const easing = mode === 'ios' ? iosEasing : mdEasing;
  350. const easingReverse = mode === 'ios' ? iosEasingReverse : mdEasingReverse;
  351. const ani = this.animation
  352. .direction(isReversed ? 'reverse' : 'normal')
  353. .easing(isReversed ? easingReverse : easing);
  354. if (animated) {
  355. await ani.play();
  356. }
  357. else {
  358. ani.play({ sync: true });
  359. }
  360. /**
  361. * We run this after the play invocation
  362. * instead of using ani.onFinish so that
  363. * multiple onFinish callbacks do not get
  364. * run if an animation is played, stopped,
  365. * and then played again.
  366. */
  367. if (ani.getDirection() === 'reverse') {
  368. ani.direction('normal');
  369. }
  370. }
  371. _isActive() {
  372. return !this.disabled && !this.isPaneVisible;
  373. }
  374. canSwipe() {
  375. return this.swipeGesture && !this.isAnimating && this._isActive();
  376. }
  377. canStart(detail) {
  378. // Do not allow swipe gesture if a modal is open
  379. const isModalPresented = !!document.querySelector('ion-modal.show-modal');
  380. if (isModalPresented || !this.canSwipe()) {
  381. return false;
  382. }
  383. if (this._isOpen) {
  384. return true;
  385. }
  386. else if (menuController._getOpenSync()) {
  387. return false;
  388. }
  389. return checkEdgeSide(window, detail.currentX, this.isEndSide, this.maxEdgeStart);
  390. }
  391. onWillStart() {
  392. this.beforeAnimation(!this._isOpen, GESTURE);
  393. return this.loadAnimation();
  394. }
  395. onStart() {
  396. if (!this.isAnimating || !this.animation) {
  397. assert(false, 'isAnimating has to be true');
  398. return;
  399. }
  400. // the cloned animation should not use an easing curve during seek
  401. this.animation.progressStart(true, this._isOpen ? 1 : 0);
  402. }
  403. onMove(detail) {
  404. if (!this.isAnimating || !this.animation) {
  405. assert(false, 'isAnimating has to be true');
  406. return;
  407. }
  408. const delta = computeDelta(detail.deltaX, this._isOpen, this.isEndSide);
  409. const stepValue = delta / this.width;
  410. this.animation.progressStep(this._isOpen ? 1 - stepValue : stepValue);
  411. }
  412. onEnd(detail) {
  413. if (!this.isAnimating || !this.animation) {
  414. assert(false, 'isAnimating has to be true');
  415. return;
  416. }
  417. const isOpen = this._isOpen;
  418. const isEndSide = this.isEndSide;
  419. const delta = computeDelta(detail.deltaX, isOpen, isEndSide);
  420. const width = this.width;
  421. const stepValue = delta / width;
  422. const velocity = detail.velocityX;
  423. const z = width / 2.0;
  424. const shouldCompleteRight = velocity >= 0 && (velocity > 0.2 || detail.deltaX > z);
  425. const shouldCompleteLeft = velocity <= 0 && (velocity < -0.2 || detail.deltaX < -z);
  426. const shouldComplete = isOpen
  427. ? isEndSide
  428. ? shouldCompleteRight
  429. : shouldCompleteLeft
  430. : isEndSide
  431. ? shouldCompleteLeft
  432. : shouldCompleteRight;
  433. let shouldOpen = !isOpen && shouldComplete;
  434. if (isOpen && !shouldComplete) {
  435. shouldOpen = true;
  436. }
  437. this.lastOnEnd = detail.currentTime;
  438. // Account for rounding errors in JS
  439. let newStepValue = shouldComplete ? 0.001 : -0.001;
  440. /**
  441. * stepValue can sometimes return a negative
  442. * value, but you can't have a negative time value
  443. * for the cubic bezier curve (at least with web animations)
  444. */
  445. const adjustedStepValue = stepValue < 0 ? 0.01 : stepValue;
  446. /**
  447. * Animation will be reversed here, so need to
  448. * reverse the easing curve as well
  449. *
  450. * Additionally, we need to account for the time relative
  451. * to the new easing curve, as `stepValue` is going to be given
  452. * in terms of a linear curve.
  453. */
  454. newStepValue +=
  455. getTimeGivenProgression([0, 0], [0.4, 0], [0.6, 1], [1, 1], clamp(0, adjustedStepValue, 0.9999))[0] || 0;
  456. const playTo = this._isOpen ? !shouldComplete : shouldComplete;
  457. this.animation
  458. .easing('cubic-bezier(0.4, 0.0, 0.6, 1)')
  459. .onFinish(() => this.afterAnimation(shouldOpen, GESTURE), { oneTimeCallback: true })
  460. .progressEnd(playTo ? 1 : 0, this._isOpen ? 1 - newStepValue : newStepValue, 300);
  461. }
  462. beforeAnimation(shouldOpen, role) {
  463. assert(!this.isAnimating, '_before() should not be called while animating');
  464. /**
  465. * When the menu is presented on an Android device, TalkBack's focus rings
  466. * may appear in the wrong position due to the transition (specifically
  467. * `transform` styles). This occurs because the focus rings are initially
  468. * displayed at the starting position of the elements before the transition
  469. * begins. This workaround ensures the focus rings do not appear in the
  470. * incorrect location.
  471. *
  472. * If this solution is applied to iOS devices, then it leads to a bug where
  473. * the overlays cannot be accessed by screen readers. This is due to
  474. * VoiceOver not being able to update the accessibility tree when the
  475. * `aria-hidden` is removed.
  476. */
  477. if (isPlatform('android')) {
  478. this.el.setAttribute('aria-hidden', 'true');
  479. }
  480. // this places the menu into the correct location before it animates in
  481. // this css class doesn't actually kick off any animations
  482. this.el.classList.add(SHOW_MENU);
  483. /**
  484. * We add a tabindex here so that focus trapping
  485. * still works even if the menu does not have
  486. * any focusable elements slotted inside. The
  487. * focus trapping utility will fallback to focusing
  488. * the menu so focus does not leave when the menu
  489. * is open.
  490. */
  491. this.el.setAttribute('tabindex', '0');
  492. if (this.backdropEl) {
  493. this.backdropEl.classList.add(SHOW_BACKDROP);
  494. }
  495. // add css class and hide content behind menu from screen readers
  496. if (this.contentEl) {
  497. this.contentEl.classList.add(MENU_CONTENT_OPEN);
  498. /**
  499. * When the menu is open and overlaying the main
  500. * content, the main content should not be announced
  501. * by the screenreader as the menu is the main
  502. * focus. This is useful with screenreaders that have
  503. * "read from top" gestures that read the entire
  504. * page from top to bottom when activated.
  505. * This should be done before the animation starts
  506. * so that users cannot accidentally scroll
  507. * the content while dragging a menu open.
  508. */
  509. this.contentEl.setAttribute('aria-hidden', 'true');
  510. }
  511. this.blocker.block();
  512. this.isAnimating = true;
  513. if (shouldOpen) {
  514. this.ionWillOpen.emit();
  515. }
  516. else {
  517. this.ionWillClose.emit({ role });
  518. }
  519. }
  520. afterAnimation(isOpen, role) {
  521. var _a;
  522. // keep opening/closing the menu disabled for a touch more yet
  523. // only add listeners/css if it's enabled and isOpen
  524. // and only remove listeners/css if it's not open
  525. // emit opened/closed events
  526. this._isOpen = isOpen;
  527. this.isAnimating = false;
  528. if (!this._isOpen) {
  529. this.blocker.unblock();
  530. }
  531. if (isOpen) {
  532. /**
  533. * When the menu is presented on an Android device, TalkBack's focus rings
  534. * may appear in the wrong position due to the transition (specifically
  535. * `transform` styles). The menu is hidden from screen readers during the
  536. * transition to prevent this. Once the transition is complete, the menu
  537. * is shown again.
  538. */
  539. if (isPlatform('android')) {
  540. this.el.removeAttribute('aria-hidden');
  541. }
  542. // emit open event
  543. this.ionDidOpen.emit();
  544. /**
  545. * Move focus to the menu to prepare focus trapping, as long as
  546. * it isn't already focused. Use the host element instead of the
  547. * first descendant to avoid the scroll position jumping around.
  548. */
  549. const focusedMenu = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.closest('ion-menu');
  550. if (focusedMenu !== this.el) {
  551. this.el.focus();
  552. }
  553. // start focus trapping
  554. document.addEventListener('focus', this.handleFocus, true);
  555. }
  556. else {
  557. this.el.removeAttribute('aria-hidden');
  558. // remove css classes and unhide content from screen readers
  559. this.el.classList.remove(SHOW_MENU);
  560. /**
  561. * Remove tabindex from the menu component
  562. * so that is cannot be tabbed to.
  563. */
  564. this.el.removeAttribute('tabindex');
  565. if (this.contentEl) {
  566. this.contentEl.classList.remove(MENU_CONTENT_OPEN);
  567. /**
  568. * Remove aria-hidden so screen readers
  569. * can announce the main content again
  570. * now that the menu is not the main focus.
  571. */
  572. this.contentEl.removeAttribute('aria-hidden');
  573. }
  574. if (this.backdropEl) {
  575. this.backdropEl.classList.remove(SHOW_BACKDROP);
  576. }
  577. if (this.animation) {
  578. this.animation.stop();
  579. }
  580. // emit close event
  581. this.ionDidClose.emit({ role });
  582. // undo focus trapping so multiple menus don't collide
  583. document.removeEventListener('focus', this.handleFocus, true);
  584. }
  585. }
  586. updateState() {
  587. const isActive = this._isActive();
  588. if (this.gesture) {
  589. this.gesture.enable(isActive && this.swipeGesture);
  590. }
  591. /**
  592. * If the menu is disabled but it is still open
  593. * then we should close the menu immediately.
  594. * Additionally, if the menu is in the process
  595. * of animating {open, close} and the menu is disabled
  596. * then it should still be closed immediately.
  597. */
  598. if (!isActive) {
  599. /**
  600. * It is possible to disable the menu while
  601. * it is mid-animation. When this happens, we
  602. * need to set the operationCancelled flag
  603. * so that this._setOpen knows to return false
  604. * and not run the "afterAnimation" callback.
  605. */
  606. if (this.isAnimating) {
  607. this.operationCancelled = true;
  608. }
  609. /**
  610. * If the menu is disabled then we should
  611. * forcibly close the menu even if it is open.
  612. */
  613. this.afterAnimation(false, GESTURE);
  614. }
  615. }
  616. render() {
  617. const { type, disabled, el, isPaneVisible, inheritedAttributes, side } = this;
  618. const mode = getIonMode(this);
  619. /**
  620. * If the Close Watcher is enabled then
  621. * the ionBackButton listener in the menu controller
  622. * will handle closing the menu when Escape is pressed.
  623. */
  624. return (h(Host, { key: '0a2ba4ff5600b80b54d1b5b45124779c6aa0d2f2', onKeyDown: shouldUseCloseWatcher() ? null : this.onKeydown, role: "navigation", "aria-label": inheritedAttributes['aria-label'] || 'menu', class: {
  625. [mode]: true,
  626. [`menu-type-${type}`]: true,
  627. 'menu-enabled': !disabled,
  628. [`menu-side-${side}`]: true,
  629. 'menu-pane-visible': isPaneVisible,
  630. 'split-pane-side': hostContext('ion-split-pane', el),
  631. } }, h("div", { key: '40a222bcde4b959abc9939c44e89ea0cf8967aba', class: "menu-inner", part: "container", ref: (el) => (this.menuInnerEl = el) }, h("slot", { key: '6a7ec5583294bb314990ff4ce6f25045652c07cb' })), h("ion-backdrop", { key: '95f1e87237f3cc24845d91b744f935bad6bb460d', ref: (el) => (this.backdropEl = el), class: "menu-backdrop", tappable: false, stopPropagation: false, part: "backdrop" })));
  632. }
  633. get el() { return this; }
  634. static get watchers() { return {
  635. "type": ["typeChanged"],
  636. "disabled": ["disabledChanged"],
  637. "side": ["sideChanged"],
  638. "swipeGesture": ["swipeGestureChanged"]
  639. }; }
  640. static get style() { return {
  641. ios: IonMenuIosStyle0,
  642. md: IonMenuMdStyle0
  643. }; }
  644. }, [33, "ion-menu", {
  645. "contentId": [513, "content-id"],
  646. "menuId": [513, "menu-id"],
  647. "type": [1025],
  648. "disabled": [1028],
  649. "side": [513],
  650. "swipeGesture": [4, "swipe-gesture"],
  651. "maxEdgeStart": [2, "max-edge-start"],
  652. "isPaneVisible": [32],
  653. "isEndSide": [32],
  654. "isOpen": [64],
  655. "isActive": [64],
  656. "open": [64],
  657. "close": [64],
  658. "toggle": [64],
  659. "setOpen": [64]
  660. }, [[16, "ionSplitPaneVisible", "onSplitPaneChanged"], [2, "click", "onBackdropClick"]], {
  661. "type": ["typeChanged"],
  662. "disabled": ["disabledChanged"],
  663. "side": ["sideChanged"],
  664. "swipeGesture": ["swipeGestureChanged"]
  665. }]);
  666. const computeDelta = (deltaX, isOpen, isEndSide) => {
  667. return Math.max(0, isOpen !== isEndSide ? -deltaX : deltaX);
  668. };
  669. const checkEdgeSide = (win, posX, isEndSide, maxEdgeStart) => {
  670. if (isEndSide) {
  671. return posX >= win.innerWidth - maxEdgeStart;
  672. }
  673. else {
  674. return posX <= maxEdgeStart;
  675. }
  676. };
  677. const SHOW_MENU = 'show-menu';
  678. const SHOW_BACKDROP = 'show-backdrop';
  679. const MENU_CONTENT_OPEN = 'menu-content-open';
  680. function defineCustomElement$1() {
  681. if (typeof customElements === "undefined") {
  682. return;
  683. }
  684. const components = ["ion-menu", "ion-backdrop"];
  685. components.forEach(tagName => { switch (tagName) {
  686. case "ion-menu":
  687. if (!customElements.get(tagName)) {
  688. customElements.define(tagName, Menu);
  689. }
  690. break;
  691. case "ion-backdrop":
  692. if (!customElements.get(tagName)) {
  693. defineCustomElement$2();
  694. }
  695. break;
  696. } });
  697. }
  698. const IonMenu = Menu;
  699. const defineCustomElement = defineCustomElement$1;
  700. export { IonMenu, defineCustomElement };