overlays-4c291a05.js 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. 'use strict';
  5. const index$1 = require('./index-c8d52405.js');
  6. const helpers = require('./helpers-8a48fdea.js');
  7. const hardwareBackButton = require('./hardware-back-button-3d2b1004.js');
  8. const index = require('./index-cc858e97.js');
  9. const ionicGlobal = require('./ionic-global-6dea5a96.js');
  10. const frameworkDelegate = require('./framework-delegate-862d9d00.js');
  11. const gestureController = require('./gesture-controller-9436f482.js');
  12. /**
  13. * This query string selects elements that
  14. * are eligible to receive focus. We select
  15. * interactive elements that meet the following
  16. * criteria:
  17. * 1. Element does not have a negative tabindex
  18. * 2. Element does not have `hidden`
  19. * 3. Element does not have `disabled` for non-Ionic components.
  20. * 4. Element does not have `disabled` or `disabled="true"` for Ionic components.
  21. * Note: We need this distinction because `disabled="false"` is
  22. * valid usage for the disabled property on ion-button.
  23. */
  24. const focusableQueryString = '[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-checkbox:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-radio:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
  25. /**
  26. * Focuses the first descendant in a context
  27. * that can receive focus. If none exists,
  28. * a fallback element will be focused.
  29. * This fallback is typically an ancestor
  30. * container such as a menu or overlay so focus does not
  31. * leave the container we are trying to trap focus in.
  32. *
  33. * If no fallback is specified then we focus the container itself.
  34. */
  35. const focusFirstDescendant = (ref, fallbackElement) => {
  36. const firstInput = ref.querySelector(focusableQueryString);
  37. focusElementInContext(firstInput, fallbackElement !== null && fallbackElement !== void 0 ? fallbackElement : ref);
  38. };
  39. /**
  40. * Focuses the last descendant in a context
  41. * that can receive focus. If none exists,
  42. * a fallback element will be focused.
  43. * This fallback is typically an ancestor
  44. * container such as a menu or overlay so focus does not
  45. * leave the container we are trying to trap focus in.
  46. *
  47. * If no fallback is specified then we focus the container itself.
  48. */
  49. const focusLastDescendant = (ref, fallbackElement) => {
  50. const inputs = Array.from(ref.querySelectorAll(focusableQueryString));
  51. const lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
  52. focusElementInContext(lastInput, fallbackElement !== null && fallbackElement !== void 0 ? fallbackElement : ref);
  53. };
  54. /**
  55. * Focuses a particular element in a context. If the element
  56. * doesn't have anything focusable associated with it then
  57. * a fallback element will be focused.
  58. *
  59. * This fallback is typically an ancestor
  60. * container such as a menu or overlay so focus does not
  61. * leave the container we are trying to trap focus in.
  62. * This should be used instead of the focus() method
  63. * on most elements because the focusable element
  64. * may not be the host element.
  65. *
  66. * For example, if an ion-button should be focused
  67. * then we should actually focus the native <button>
  68. * element inside of ion-button's shadow root, not
  69. * the host element itself.
  70. */
  71. const focusElementInContext = (hostToFocus, fallbackElement) => {
  72. let elementToFocus = hostToFocus;
  73. const shadowRoot = hostToFocus === null || hostToFocus === void 0 ? void 0 : hostToFocus.shadowRoot;
  74. if (shadowRoot) {
  75. // If there are no inner focusable elements, just focus the host element.
  76. elementToFocus = shadowRoot.querySelector(focusableQueryString) || hostToFocus;
  77. }
  78. if (elementToFocus) {
  79. const radioGroup = elementToFocus.closest('ion-radio-group');
  80. if (radioGroup) {
  81. radioGroup.setFocus();
  82. }
  83. else {
  84. helpers.focusVisibleElement(elementToFocus);
  85. }
  86. }
  87. else {
  88. // Focus fallback element instead of letting focus escape
  89. fallbackElement.focus();
  90. }
  91. };
  92. let lastOverlayIndex = 0;
  93. let lastId = 0;
  94. const activeAnimations = new WeakMap();
  95. const createController = (tagName) => {
  96. return {
  97. create(options) {
  98. return createOverlay(tagName, options);
  99. },
  100. dismiss(data, role, id) {
  101. return dismissOverlay(document, data, role, tagName, id);
  102. },
  103. async getTop() {
  104. return getPresentedOverlay(document, tagName);
  105. },
  106. };
  107. };
  108. const alertController = /*@__PURE__*/ createController('ion-alert');
  109. const actionSheetController = /*@__PURE__*/ createController('ion-action-sheet');
  110. const loadingController = /*@__PURE__*/ createController('ion-loading');
  111. const modalController = /*@__PURE__*/ createController('ion-modal');
  112. /**
  113. * @deprecated Use the inline ion-picker component instead.
  114. */
  115. const pickerController = /*@__PURE__*/ createController('ion-picker-legacy');
  116. const popoverController = /*@__PURE__*/ createController('ion-popover');
  117. const toastController = /*@__PURE__*/ createController('ion-toast');
  118. /**
  119. * Prepares the overlay element to be presented.
  120. */
  121. const prepareOverlay = (el) => {
  122. if (typeof document !== 'undefined') {
  123. /**
  124. * Adds a single instance of event listeners for application behaviors:
  125. *
  126. * - Escape Key behavior to dismiss an overlay
  127. * - Trapping focus within an overlay
  128. * - Back button behavior to dismiss an overlay
  129. *
  130. * This only occurs when the first overlay is created.
  131. */
  132. connectListeners(document);
  133. }
  134. const overlayIndex = lastOverlayIndex++;
  135. /**
  136. * overlayIndex is used in the overlay components to set a zIndex.
  137. * This ensures that the most recently presented overlay will be
  138. * on top.
  139. */
  140. el.overlayIndex = overlayIndex;
  141. };
  142. /**
  143. * Assigns an incrementing id to an overlay element, that does not
  144. * already have an id assigned to it.
  145. *
  146. * Used to track unique instances of an overlay element.
  147. */
  148. const setOverlayId = (el) => {
  149. if (!el.hasAttribute('id')) {
  150. el.id = `ion-overlay-${++lastId}`;
  151. }
  152. return el.id;
  153. };
  154. const createOverlay = (tagName, opts) => {
  155. // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
  156. if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined') {
  157. return window.customElements.whenDefined(tagName).then(() => {
  158. const element = document.createElement(tagName);
  159. element.classList.add('overlay-hidden');
  160. /**
  161. * Convert the passed in overlay options into props
  162. * that get passed down into the new overlay.
  163. */
  164. Object.assign(element, Object.assign(Object.assign({}, opts), { hasController: true }));
  165. // append the overlay element to the document body
  166. getAppRoot(document).appendChild(element);
  167. return new Promise((resolve) => helpers.componentOnReady(element, resolve));
  168. });
  169. }
  170. return Promise.resolve();
  171. };
  172. const isOverlayHidden = (overlay) => overlay.classList.contains('overlay-hidden');
  173. /**
  174. * Focuses a particular element in an overlay. If the element
  175. * doesn't have anything focusable associated with it then
  176. * the overlay itself will be focused.
  177. * This should be used instead of the focus() method
  178. * on most elements because the focusable element
  179. * may not be the host element.
  180. *
  181. * For example, if an ion-button should be focused
  182. * then we should actually focus the native <button>
  183. * element inside of ion-button's shadow root, not
  184. * the host element itself.
  185. */
  186. const focusElementInOverlay = (hostToFocus, overlay) => {
  187. let elementToFocus = hostToFocus;
  188. const shadowRoot = hostToFocus === null || hostToFocus === void 0 ? void 0 : hostToFocus.shadowRoot;
  189. if (shadowRoot) {
  190. // If there are no inner focusable elements, just focus the host element.
  191. elementToFocus = shadowRoot.querySelector(focusableQueryString) || hostToFocus;
  192. }
  193. if (elementToFocus) {
  194. helpers.focusVisibleElement(elementToFocus);
  195. }
  196. else {
  197. // Focus overlay instead of letting focus escape
  198. overlay.focus();
  199. }
  200. };
  201. /**
  202. * Traps keyboard focus inside of overlay components.
  203. * Based on https://w3c.github.io/aria-practices/examples/dialog-modal/alertdialog.html
  204. * This includes the following components: Action Sheet, Alert, Loading, Modal,
  205. * Picker, and Popover.
  206. * Should NOT include: Toast
  207. */
  208. const trapKeyboardFocus = (ev, doc) => {
  209. const lastOverlay = getPresentedOverlay(doc, 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker-legacy,ion-popover');
  210. const target = ev.target;
  211. /**
  212. * If no active overlay, ignore this event.
  213. *
  214. * If this component uses the shadow dom,
  215. * this global listener is pointless
  216. * since it will not catch the focus
  217. * traps as they are inside the shadow root.
  218. * We need to add a listener to the shadow root
  219. * itself to ensure the focus trap works.
  220. */
  221. if (!lastOverlay || !target) {
  222. return;
  223. }
  224. /**
  225. * If the ion-disable-focus-trap class
  226. * is present on an overlay, then this component
  227. * instance has opted out of focus trapping.
  228. * An example of this is when the sheet modal
  229. * has a backdrop that is disabled. The content
  230. * behind the sheet should be focusable until
  231. * the backdrop is enabled.
  232. */
  233. if (lastOverlay.classList.contains(FOCUS_TRAP_DISABLE_CLASS)) {
  234. return;
  235. }
  236. const trapScopedFocus = () => {
  237. /**
  238. * If we are focusing the overlay, clear
  239. * the last focused element so that hitting
  240. * tab activates the first focusable element
  241. * in the overlay wrapper.
  242. */
  243. if (lastOverlay === target) {
  244. lastOverlay.lastFocus = undefined;
  245. /**
  246. * Toasts can be presented from an overlay.
  247. * However, focus should still be returned to
  248. * the overlay when clicking a toast. Normally,
  249. * focus would be returned to the last focusable
  250. * descendant in the overlay which may not always be
  251. * the button that the toast was presented from. In this case,
  252. * the focus may be returned to an unexpected element.
  253. * To account for this, we make sure to return focus to the
  254. * last focused element in the overlay if focus is
  255. * moved to the toast.
  256. */
  257. }
  258. else if (target.tagName === 'ION-TOAST') {
  259. focusElementInOverlay(lastOverlay.lastFocus, lastOverlay);
  260. /**
  261. * Otherwise, we must be focusing an element
  262. * inside of the overlay. The two possible options
  263. * here are an input/button/etc or the ion-focus-trap
  264. * element. The focus trap element is used to prevent
  265. * the keyboard focus from leaving the overlay when
  266. * using Tab or screen assistants.
  267. */
  268. }
  269. else {
  270. /**
  271. * We do not want to focus the traps, so get the overlay
  272. * wrapper element as the traps live outside of the wrapper.
  273. */
  274. const overlayRoot = helpers.getElementRoot(lastOverlay);
  275. if (!overlayRoot.contains(target)) {
  276. return;
  277. }
  278. const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper');
  279. if (!overlayWrapper) {
  280. return;
  281. }
  282. /**
  283. * If the target is inside the wrapper, let the browser
  284. * focus as normal and keep a log of the last focused element.
  285. * Additionally, if the backdrop was tapped we should not
  286. * move focus back inside the wrapper as that could cause
  287. * an interactive elements focus state to activate.
  288. */
  289. if (overlayWrapper.contains(target) || target === overlayRoot.querySelector('ion-backdrop')) {
  290. lastOverlay.lastFocus = target;
  291. }
  292. else {
  293. /**
  294. * Otherwise, we must have focused one of the focus traps.
  295. * We need to wrap the focus to either the first element
  296. * or the last element.
  297. */
  298. /**
  299. * Once we call `focusFirstDescendant` and focus the first
  300. * descendant, another focus event will fire which will
  301. * cause `lastOverlay.lastFocus` to be updated before
  302. * we can run the code after that. We will cache the value
  303. * here to avoid that.
  304. */
  305. const lastFocus = lastOverlay.lastFocus;
  306. // Focus the first element in the overlay wrapper
  307. focusFirstDescendant(overlayWrapper, lastOverlay);
  308. /**
  309. * If the cached last focused element is the
  310. * same as the active element, then we need
  311. * to wrap focus to the last descendant. This happens
  312. * when the first descendant is focused, and the user
  313. * presses Shift + Tab. The previous line will focus
  314. * the same descendant again (the first one), causing
  315. * last focus to equal the active element.
  316. */
  317. if (lastFocus === doc.activeElement) {
  318. focusLastDescendant(overlayWrapper, lastOverlay);
  319. }
  320. lastOverlay.lastFocus = doc.activeElement;
  321. }
  322. }
  323. };
  324. const trapShadowFocus = () => {
  325. /**
  326. * If the target is inside the wrapper, let the browser
  327. * focus as normal and keep a log of the last focused element.
  328. */
  329. if (lastOverlay.contains(target)) {
  330. lastOverlay.lastFocus = target;
  331. /**
  332. * Toasts can be presented from an overlay.
  333. * However, focus should still be returned to
  334. * the overlay when clicking a toast. Normally,
  335. * focus would be returned to the last focusable
  336. * descendant in the overlay which may not always be
  337. * the button that the toast was presented from. In this case,
  338. * the focus may be returned to an unexpected element.
  339. * To account for this, we make sure to return focus to the
  340. * last focused element in the overlay if focus is
  341. * moved to the toast.
  342. */
  343. }
  344. else if (target.tagName === 'ION-TOAST') {
  345. focusElementInOverlay(lastOverlay.lastFocus, lastOverlay);
  346. }
  347. else {
  348. /**
  349. * Otherwise, we are about to have focus
  350. * go out of the overlay. We need to wrap
  351. * the focus to either the first element
  352. * or the last element.
  353. */
  354. /**
  355. * Once we call `focusFirstDescendant` and focus the first
  356. * descendant, another focus event will fire which will
  357. * cause `lastOverlay.lastFocus` to be updated before
  358. * we can run the code after that. We will cache the value
  359. * here to avoid that.
  360. */
  361. const lastFocus = lastOverlay.lastFocus;
  362. // Focus the first element in the overlay wrapper
  363. focusFirstDescendant(lastOverlay);
  364. /**
  365. * If the cached last focused element is the
  366. * same as the active element, then we need
  367. * to wrap focus to the last descendant. This happens
  368. * when the first descendant is focused, and the user
  369. * presses Shift + Tab. The previous line will focus
  370. * the same descendant again (the first one), causing
  371. * last focus to equal the active element.
  372. */
  373. if (lastFocus === doc.activeElement) {
  374. focusLastDescendant(lastOverlay);
  375. }
  376. lastOverlay.lastFocus = doc.activeElement;
  377. }
  378. };
  379. if (lastOverlay.shadowRoot) {
  380. trapShadowFocus();
  381. }
  382. else {
  383. trapScopedFocus();
  384. }
  385. };
  386. const connectListeners = (doc) => {
  387. if (lastOverlayIndex === 0) {
  388. lastOverlayIndex = 1;
  389. doc.addEventListener('focus', (ev) => {
  390. trapKeyboardFocus(ev, doc);
  391. }, true);
  392. // handle back-button click
  393. doc.addEventListener('ionBackButton', (ev) => {
  394. const lastOverlay = getPresentedOverlay(doc);
  395. if (lastOverlay === null || lastOverlay === void 0 ? void 0 : lastOverlay.backdropDismiss) {
  396. ev.detail.register(hardwareBackButton.OVERLAY_BACK_BUTTON_PRIORITY, () => {
  397. /**
  398. * Do not return this promise otherwise
  399. * the hardware back button utility will
  400. * be blocked until the overlay dismisses.
  401. * This is important for a modal with canDismiss.
  402. * If the application presents a confirmation alert
  403. * in the "canDismiss" callback, then it will be impossible
  404. * to use the hardware back button to dismiss the alert
  405. * dialog because the hardware back button utility
  406. * is blocked on waiting for the modal to dismiss.
  407. */
  408. lastOverlay.dismiss(undefined, BACKDROP);
  409. });
  410. }
  411. });
  412. /**
  413. * Handle ESC to close overlay.
  414. * CloseWatcher also handles pressing the Esc
  415. * key, so if a browser supports CloseWatcher then
  416. * this behavior will be handled via the ionBackButton
  417. * event.
  418. */
  419. if (!hardwareBackButton.shouldUseCloseWatcher()) {
  420. doc.addEventListener('keydown', (ev) => {
  421. if (ev.key === 'Escape') {
  422. const lastOverlay = getPresentedOverlay(doc);
  423. if (lastOverlay === null || lastOverlay === void 0 ? void 0 : lastOverlay.backdropDismiss) {
  424. lastOverlay.dismiss(undefined, BACKDROP);
  425. }
  426. }
  427. });
  428. }
  429. }
  430. };
  431. const dismissOverlay = (doc, data, role, overlayTag, id) => {
  432. const overlay = getPresentedOverlay(doc, overlayTag, id);
  433. if (!overlay) {
  434. return Promise.reject('overlay does not exist');
  435. }
  436. return overlay.dismiss(data, role);
  437. };
  438. /**
  439. * Returns a list of all overlays in the DOM even if they are not presented.
  440. */
  441. const getOverlays = (doc, selector) => {
  442. if (selector === undefined) {
  443. selector = 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker-legacy,ion-popover,ion-toast';
  444. }
  445. return Array.from(doc.querySelectorAll(selector)).filter((c) => c.overlayIndex > 0);
  446. };
  447. /**
  448. * Returns a list of all presented overlays.
  449. * Inline overlays can exist in the DOM but not be presented,
  450. * so there are times when we want to exclude those.
  451. * @param doc The document to find the element within.
  452. * @param overlayTag The selector for the overlay, defaults to Ionic overlay components.
  453. */
  454. const getPresentedOverlays = (doc, overlayTag) => {
  455. return getOverlays(doc, overlayTag).filter((o) => !isOverlayHidden(o));
  456. };
  457. /**
  458. * Returns a presented overlay element.
  459. * @param doc The document to find the element within.
  460. * @param overlayTag The selector for the overlay, defaults to Ionic overlay components.
  461. * @param id The unique identifier for the overlay instance.
  462. * @returns The overlay element or `undefined` if no overlay element is found.
  463. */
  464. const getPresentedOverlay = (doc, overlayTag, id) => {
  465. const overlays = getPresentedOverlays(doc, overlayTag);
  466. return id === undefined ? overlays[overlays.length - 1] : overlays.find((o) => o.id === id);
  467. };
  468. /**
  469. * When an overlay is presented, the main
  470. * focus is the overlay not the page content.
  471. * We need to remove the page content from the
  472. * accessibility tree otherwise when
  473. * users use "read screen from top" gestures with
  474. * TalkBack and VoiceOver, the screen reader will begin
  475. * to read the content underneath the overlay.
  476. *
  477. * We need a container where all page components
  478. * exist that is separate from where the overlays
  479. * are added in the DOM. For most apps, this element
  480. * is the top most ion-router-outlet. In the event
  481. * that devs are not using a router,
  482. * they will need to add the "ion-view-container-root"
  483. * id to the element that contains all of their views.
  484. *
  485. * TODO: If Framework supports having multiple top
  486. * level router outlets we would need to update this.
  487. * Example: One outlet for side menu and one outlet
  488. * for main content.
  489. */
  490. const setRootAriaHidden = (hidden = false) => {
  491. const root = getAppRoot(document);
  492. const viewContainer = root.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root');
  493. if (!viewContainer) {
  494. return;
  495. }
  496. if (hidden) {
  497. viewContainer.setAttribute('aria-hidden', 'true');
  498. }
  499. else {
  500. viewContainer.removeAttribute('aria-hidden');
  501. }
  502. };
  503. const present = async (overlay, name, iosEnterAnimation, mdEnterAnimation, opts) => {
  504. var _a, _b;
  505. if (overlay.presented) {
  506. return;
  507. }
  508. /**
  509. * Due to accessibility guidelines, toasts do not have
  510. * focus traps.
  511. *
  512. * All other overlays should have focus traps to prevent
  513. * the keyboard focus from leaving the overlay.
  514. */
  515. if (overlay.el.tagName !== 'ION-TOAST') {
  516. setRootAriaHidden(true);
  517. document.body.classList.add(gestureController.BACKDROP_NO_SCROLL);
  518. }
  519. hideUnderlyingOverlaysFromScreenReaders(overlay.el);
  520. hideAnimatingOverlayFromScreenReaders(overlay.el);
  521. overlay.presented = true;
  522. overlay.willPresent.emit();
  523. (_a = overlay.willPresentShorthand) === null || _a === void 0 ? void 0 : _a.emit();
  524. const mode = ionicGlobal.getIonMode(overlay);
  525. // get the user's animation fn if one was provided
  526. const animationBuilder = overlay.enterAnimation
  527. ? overlay.enterAnimation
  528. : index.config.get(name, mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);
  529. const completed = await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
  530. if (completed) {
  531. overlay.didPresent.emit();
  532. (_b = overlay.didPresentShorthand) === null || _b === void 0 ? void 0 : _b.emit();
  533. }
  534. /**
  535. * When an overlay that steals focus
  536. * is dismissed, focus should be returned
  537. * to the element that was focused
  538. * prior to the overlay opening. Toast
  539. * does not steal focus and is excluded
  540. * from returning focus as a result.
  541. */
  542. if (overlay.el.tagName !== 'ION-TOAST') {
  543. restoreElementFocus(overlay.el);
  544. }
  545. /**
  546. * If the focused element is already
  547. * inside the overlay component then
  548. * focus should not be moved from that
  549. * to the overlay container.
  550. */
  551. if (overlay.keyboardClose && (document.activeElement === null || !overlay.el.contains(document.activeElement))) {
  552. overlay.el.focus();
  553. }
  554. /**
  555. * If this overlay was previously dismissed without being
  556. * the topmost one (such as by manually calling dismiss()),
  557. * it would still have aria-hidden on being presented again.
  558. * Removing it here ensures the overlay is visible to screen
  559. * readers.
  560. *
  561. * If this overlay was being presented, then it was hidden
  562. * from screen readers during the animation. Now that the
  563. * animation is complete, we can reveal the overlay to
  564. * screen readers.
  565. */
  566. overlay.el.removeAttribute('aria-hidden');
  567. };
  568. /**
  569. * When an overlay component is dismissed,
  570. * focus should be returned to the element
  571. * that presented the overlay. Otherwise
  572. * focus will be set on the body which
  573. * means that people using screen readers
  574. * or tabbing will need to re-navigate
  575. * to where they were before they
  576. * opened the overlay.
  577. */
  578. const restoreElementFocus = async (overlayEl) => {
  579. let previousElement = document.activeElement;
  580. if (!previousElement) {
  581. return;
  582. }
  583. const shadowRoot = previousElement === null || previousElement === void 0 ? void 0 : previousElement.shadowRoot;
  584. if (shadowRoot) {
  585. // If there are no inner focusable elements, just focus the host element.
  586. previousElement = shadowRoot.querySelector(focusableQueryString) || previousElement;
  587. }
  588. await overlayEl.onDidDismiss();
  589. /**
  590. * After onDidDismiss, the overlay loses focus
  591. * because it is removed from the document
  592. *
  593. * > An element will also lose focus [...]
  594. * > if the element is removed from the document)
  595. *
  596. * https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
  597. *
  598. * Additionally, `document.activeElement` returns:
  599. *
  600. * > The Element which currently has focus,
  601. * > `<body>` or null if there is
  602. * > no focused element.
  603. *
  604. * https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement#value
  605. *
  606. * However, if the user has already focused
  607. * an element sometime between onWillDismiss
  608. * and onDidDismiss (for example, focusing a
  609. * text box after tapping a button in an
  610. * action sheet) then don't restore focus to
  611. * previous element
  612. */
  613. if (document.activeElement === null || document.activeElement === document.body) {
  614. previousElement.focus();
  615. }
  616. };
  617. const dismiss = async (overlay, data, role, name, iosLeaveAnimation, mdLeaveAnimation, opts) => {
  618. var _a, _b;
  619. if (!overlay.presented) {
  620. return false;
  621. }
  622. const presentedOverlays = index$1.doc !== undefined ? getPresentedOverlays(index$1.doc) : [];
  623. /**
  624. * For accessibility, toasts lack focus traps and don't receive
  625. * `aria-hidden` on the root element when presented.
  626. *
  627. * All other overlays use focus traps to keep keyboard focus
  628. * within the overlay, setting `aria-hidden` on the root element
  629. * to enhance accessibility.
  630. *
  631. * Therefore, we must remove `aria-hidden` from the root element
  632. * when the last non-toast overlay is dismissed.
  633. */
  634. const overlaysNotToast = presentedOverlays.filter((o) => o.tagName !== 'ION-TOAST');
  635. const lastOverlayNotToast = overlaysNotToast.length === 1 && overlaysNotToast[0].id === overlay.el.id;
  636. /**
  637. * If this is the last visible overlay that is not a toast
  638. * then we want to re-add the root to the accessibility tree.
  639. */
  640. if (lastOverlayNotToast) {
  641. setRootAriaHidden(false);
  642. document.body.classList.remove(gestureController.BACKDROP_NO_SCROLL);
  643. }
  644. overlay.presented = false;
  645. try {
  646. /**
  647. * There is no need to show the overlay to screen readers during
  648. * the dismiss animation. This is because the overlay will be removed
  649. * from the DOM after the animation is complete.
  650. */
  651. hideAnimatingOverlayFromScreenReaders(overlay.el);
  652. // Overlay contents should not be clickable during dismiss
  653. overlay.el.style.setProperty('pointer-events', 'none');
  654. overlay.willDismiss.emit({ data, role });
  655. (_a = overlay.willDismissShorthand) === null || _a === void 0 ? void 0 : _a.emit({ data, role });
  656. const mode = ionicGlobal.getIonMode(overlay);
  657. const animationBuilder = overlay.leaveAnimation
  658. ? overlay.leaveAnimation
  659. : index.config.get(name, mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation);
  660. // If dismissed via gesture, no need to play leaving animation again
  661. if (role !== GESTURE) {
  662. await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
  663. }
  664. overlay.didDismiss.emit({ data, role });
  665. (_b = overlay.didDismissShorthand) === null || _b === void 0 ? void 0 : _b.emit({ data, role });
  666. // Get a reference to all animations currently assigned to this overlay
  667. // Then tear them down to return the overlay to its initial visual state
  668. const animations = activeAnimations.get(overlay) || [];
  669. animations.forEach((ani) => ani.destroy());
  670. activeAnimations.delete(overlay);
  671. /**
  672. * Make overlay hidden again in case it is being reused.
  673. * We can safely remove pointer-events: none as
  674. * overlay-hidden will set display: none.
  675. */
  676. overlay.el.classList.add('overlay-hidden');
  677. overlay.el.style.removeProperty('pointer-events');
  678. /**
  679. * Clear any focus trapping references
  680. * when the overlay is dismissed.
  681. */
  682. if (overlay.el.lastFocus !== undefined) {
  683. overlay.el.lastFocus = undefined;
  684. }
  685. }
  686. catch (err) {
  687. index.printIonError(`[${overlay.el.tagName.toLowerCase()}] - `, err);
  688. }
  689. overlay.el.remove();
  690. revealOverlaysToScreenReaders();
  691. return true;
  692. };
  693. const getAppRoot = (doc) => {
  694. return doc.querySelector('ion-app') || doc.body;
  695. };
  696. const overlayAnimation = async (overlay, animationBuilder, baseEl, opts) => {
  697. // Make overlay visible in case it's hidden
  698. baseEl.classList.remove('overlay-hidden');
  699. const aniRoot = overlay.el;
  700. const animation = animationBuilder(aniRoot, opts);
  701. if (!overlay.animated || !index.config.getBoolean('animated', true)) {
  702. animation.duration(0);
  703. }
  704. if (overlay.keyboardClose) {
  705. animation.beforeAddWrite(() => {
  706. const activeElement = baseEl.ownerDocument.activeElement;
  707. if (activeElement === null || activeElement === void 0 ? void 0 : activeElement.matches('input,ion-input, ion-textarea')) {
  708. activeElement.blur();
  709. }
  710. });
  711. }
  712. const activeAni = activeAnimations.get(overlay) || [];
  713. activeAnimations.set(overlay, [...activeAni, animation]);
  714. await animation.play();
  715. return true;
  716. };
  717. const eventMethod = (element, eventName) => {
  718. let resolve;
  719. const promise = new Promise((r) => (resolve = r));
  720. onceEvent(element, eventName, (event) => {
  721. resolve(event.detail);
  722. });
  723. return promise;
  724. };
  725. const onceEvent = (element, eventName, callback) => {
  726. const handler = (ev) => {
  727. helpers.removeEventListener(element, eventName, handler);
  728. callback(ev);
  729. };
  730. helpers.addEventListener(element, eventName, handler);
  731. };
  732. const isCancel = (role) => {
  733. return role === 'cancel' || role === BACKDROP;
  734. };
  735. const defaultGate = (h) => h();
  736. /**
  737. * Calls a developer provided method while avoiding
  738. * Angular Zones. Since the handler is provided by
  739. * the developer, we should throw any errors
  740. * received so that developer-provided bug
  741. * tracking software can log it.
  742. */
  743. const safeCall = (handler, arg) => {
  744. if (typeof handler === 'function') {
  745. const jmp = index.config.get('_zoneGate', defaultGate);
  746. return jmp(() => {
  747. try {
  748. return handler(arg);
  749. }
  750. catch (e) {
  751. throw e;
  752. }
  753. });
  754. }
  755. return undefined;
  756. };
  757. const BACKDROP = 'backdrop';
  758. const GESTURE = 'gesture';
  759. const OVERLAY_GESTURE_PRIORITY = 39;
  760. /**
  761. * Creates a delegate controller.
  762. *
  763. * Requires that the component has the following properties:
  764. * - `el: HTMLElement`
  765. * - `hasController: boolean`
  766. * - `delegate?: FrameworkDelegate`
  767. *
  768. * @param ref The component class instance.
  769. */
  770. const createDelegateController = (ref) => {
  771. let inline = false;
  772. let workingDelegate;
  773. const coreDelegate = frameworkDelegate.CoreDelegate();
  774. /**
  775. * Determines whether or not an overlay is being used
  776. * inline or via a controller/JS and returns the correct delegate.
  777. * By default, subsequent calls to getDelegate will use
  778. * a cached version of the delegate.
  779. * This is useful for calling dismiss after present,
  780. * so that the correct delegate is given.
  781. * @param force `true` to force the non-cached version of the delegate.
  782. * @returns The delegate to use and whether or not the overlay is inline.
  783. */
  784. const getDelegate = (force = false) => {
  785. if (workingDelegate && !force) {
  786. return {
  787. delegate: workingDelegate,
  788. inline,
  789. };
  790. }
  791. const { el, hasController, delegate } = ref;
  792. /**
  793. * If using overlay inline
  794. * we potentially need to use the coreDelegate
  795. * so that this works in vanilla JS apps.
  796. * If a developer has presented this component
  797. * via a controller, then we can assume
  798. * the component is already in the
  799. * correct place.
  800. */
  801. const parentEl = el.parentNode;
  802. inline = parentEl !== null && !hasController;
  803. workingDelegate = inline ? delegate || coreDelegate : delegate;
  804. return { inline, delegate: workingDelegate };
  805. };
  806. /**
  807. * Attaches a component in the DOM. Teleports the component
  808. * to the root of the app.
  809. * @param component The component to optionally construct and append to the element.
  810. */
  811. const attachViewToDom = async (component) => {
  812. const { delegate } = getDelegate(true);
  813. if (delegate) {
  814. return await delegate.attachViewToDom(ref.el, component);
  815. }
  816. const { hasController } = ref;
  817. if (hasController && component !== undefined) {
  818. throw new Error('framework delegate is missing');
  819. }
  820. return null;
  821. };
  822. /**
  823. * Moves a component back to its original location in the DOM.
  824. */
  825. const removeViewFromDom = () => {
  826. const { delegate } = getDelegate();
  827. if (delegate && ref.el !== undefined) {
  828. delegate.removeViewFromDom(ref.el.parentElement, ref.el);
  829. }
  830. };
  831. return {
  832. attachViewToDom,
  833. removeViewFromDom,
  834. };
  835. };
  836. /**
  837. * Constructs a trigger interaction for an overlay.
  838. * Presents an overlay when the trigger is clicked.
  839. *
  840. * Usage:
  841. * ```ts
  842. * triggerController = createTriggerController();
  843. * triggerController.addClickListener(el, trigger);
  844. * ```
  845. */
  846. const createTriggerController = () => {
  847. let destroyTriggerInteraction;
  848. /**
  849. * Removes the click listener from the trigger element.
  850. */
  851. const removeClickListener = () => {
  852. if (destroyTriggerInteraction) {
  853. destroyTriggerInteraction();
  854. destroyTriggerInteraction = undefined;
  855. }
  856. };
  857. /**
  858. * Adds a click listener to the trigger element.
  859. * Presents the overlay when the trigger is clicked.
  860. * @param el The overlay element.
  861. * @param trigger The ID of the element to add a click listener to.
  862. */
  863. const addClickListener = (el, trigger) => {
  864. removeClickListener();
  865. const triggerEl = trigger !== undefined ? document.getElementById(trigger) : null;
  866. if (!triggerEl) {
  867. index.printIonWarning(`[${el.tagName.toLowerCase()}] - A trigger element with the ID "${trigger}" was not found in the DOM. The trigger element must be in the DOM when the "trigger" property is set on an overlay component.`, el);
  868. return;
  869. }
  870. const configureTriggerInteraction = (targetEl, overlayEl) => {
  871. const openOverlay = () => {
  872. overlayEl.present();
  873. };
  874. targetEl.addEventListener('click', openOverlay);
  875. return () => {
  876. targetEl.removeEventListener('click', openOverlay);
  877. };
  878. };
  879. destroyTriggerInteraction = configureTriggerInteraction(triggerEl, el);
  880. };
  881. return {
  882. addClickListener,
  883. removeClickListener,
  884. };
  885. };
  886. /**
  887. * The overlay that is being animated also needs to hide from screen
  888. * readers during its animation. This ensures that assistive technologies
  889. * like TalkBack do not announce or interact with the content until the
  890. * animation is complete, avoiding confusion for users.
  891. *
  892. * When the overlay is presented on an Android device, TalkBack's focus rings
  893. * may appear in the wrong position due to the transition (specifically
  894. * `transform` styles). This occurs because the focus rings are initially
  895. * displayed at the starting position of the elements before the transition
  896. * begins. This workaround ensures the focus rings do not appear in the
  897. * incorrect location.
  898. *
  899. * If this solution is applied to iOS devices, then it leads to a bug where
  900. * the overlays cannot be accessed by screen readers. This is due to
  901. * VoiceOver not being able to update the accessibility tree when the
  902. * `aria-hidden` is removed.
  903. *
  904. * @param overlay - The overlay that is being animated.
  905. */
  906. const hideAnimatingOverlayFromScreenReaders = (overlay) => {
  907. if (index$1.doc === undefined)
  908. return;
  909. if (ionicGlobal.isPlatform('android')) {
  910. /**
  911. * Once the animation is complete, this attribute will be removed.
  912. * This is done at the end of the `present` method.
  913. */
  914. overlay.setAttribute('aria-hidden', 'true');
  915. }
  916. };
  917. /**
  918. * Ensure that underlying overlays have aria-hidden if necessary so that screen readers
  919. * cannot move focus to these elements. Note that we cannot rely on focus/focusin/focusout
  920. * events here because those events do not fire when the screen readers moves to a non-focusable
  921. * element such as text.
  922. * Without this logic screen readers would be able to move focus outside of the top focus-trapped overlay.
  923. *
  924. * @param newTopMostOverlay - The overlay that is being presented. Since the overlay has not been
  925. * fully presented yet at the time this function is called it will not be included in the getPresentedOverlays result.
  926. */
  927. const hideUnderlyingOverlaysFromScreenReaders = (newTopMostOverlay) => {
  928. var _a;
  929. if (index$1.doc === undefined)
  930. return;
  931. const overlays = getPresentedOverlays(index$1.doc);
  932. for (let i = overlays.length - 1; i >= 0; i--) {
  933. const presentedOverlay = overlays[i];
  934. const nextPresentedOverlay = (_a = overlays[i + 1]) !== null && _a !== void 0 ? _a : newTopMostOverlay;
  935. /**
  936. * If next overlay has aria-hidden then all remaining overlays will have it too.
  937. * Or, if the next overlay is a Toast that does not have aria-hidden then current overlay
  938. * should not have aria-hidden either so focus can remain in the current overlay.
  939. */
  940. if (nextPresentedOverlay.hasAttribute('aria-hidden') || nextPresentedOverlay.tagName !== 'ION-TOAST') {
  941. presentedOverlay.setAttribute('aria-hidden', 'true');
  942. }
  943. }
  944. };
  945. /**
  946. * When dismissing an overlay we need to reveal the new top-most overlay to screen readers.
  947. * If the top-most overlay is a Toast we potentially need to reveal more overlays since
  948. * focus is never automatically moved to the Toast.
  949. */
  950. const revealOverlaysToScreenReaders = () => {
  951. if (index$1.doc === undefined)
  952. return;
  953. const overlays = getPresentedOverlays(index$1.doc);
  954. for (let i = overlays.length - 1; i >= 0; i--) {
  955. const currentOverlay = overlays[i];
  956. /**
  957. * If the current we are looking at is a Toast then we can remove aria-hidden.
  958. * However, we potentially need to keep looking at the overlay stack because there
  959. * could be more Toasts underneath. Additionally, we need to unhide the closest non-Toast
  960. * overlay too so focus can move there since focus is never automatically moved to the Toast.
  961. */
  962. currentOverlay.removeAttribute('aria-hidden');
  963. /**
  964. * If we found a non-Toast element then we can just remove aria-hidden and stop searching entirely
  965. * since this overlay should always receive focus. As a result, all underlying overlays should still
  966. * be hidden from screen readers.
  967. */
  968. if (currentOverlay.tagName !== 'ION-TOAST') {
  969. break;
  970. }
  971. }
  972. };
  973. const FOCUS_TRAP_DISABLE_CLASS = 'ion-disable-focus-trap';
  974. exports.BACKDROP = BACKDROP;
  975. exports.FOCUS_TRAP_DISABLE_CLASS = FOCUS_TRAP_DISABLE_CLASS;
  976. exports.GESTURE = GESTURE;
  977. exports.OVERLAY_GESTURE_PRIORITY = OVERLAY_GESTURE_PRIORITY;
  978. exports.actionSheetController = actionSheetController;
  979. exports.alertController = alertController;
  980. exports.createDelegateController = createDelegateController;
  981. exports.createTriggerController = createTriggerController;
  982. exports.dismiss = dismiss;
  983. exports.eventMethod = eventMethod;
  984. exports.focusFirstDescendant = focusFirstDescendant;
  985. exports.focusLastDescendant = focusLastDescendant;
  986. exports.getPresentedOverlay = getPresentedOverlay;
  987. exports.isCancel = isCancel;
  988. exports.loadingController = loadingController;
  989. exports.modalController = modalController;
  990. exports.pickerController = pickerController;
  991. exports.popoverController = popoverController;
  992. exports.prepareOverlay = prepareOverlay;
  993. exports.present = present;
  994. exports.safeCall = safeCall;
  995. exports.setOverlayId = setOverlayId;
  996. exports.toastController = toastController;