index-1eff7149.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. 'use strict';
  5. const index = require('./index-cc858e97.js');
  6. const index$1 = require('./index-2e236a04.js');
  7. const helpers = require('./helpers-8a48fdea.js');
  8. const LIFECYCLE_WILL_ENTER = 'ionViewWillEnter';
  9. const LIFECYCLE_DID_ENTER = 'ionViewDidEnter';
  10. const LIFECYCLE_WILL_LEAVE = 'ionViewWillLeave';
  11. const LIFECYCLE_DID_LEAVE = 'ionViewDidLeave';
  12. const LIFECYCLE_WILL_UNLOAD = 'ionViewWillUnload';
  13. /**
  14. * Moves focus to a specified element. Note that we do not remove the tabindex
  15. * because that can result in an unintentional blur. Non-focusables can't be
  16. * focused, so the body will get focused again.
  17. */
  18. const moveFocus = (el) => {
  19. el.tabIndex = -1;
  20. el.focus();
  21. };
  22. /**
  23. * Elements that are hidden using `display: none` should not be focused even if
  24. * they are present in the DOM.
  25. */
  26. const isVisible = (el) => {
  27. return el.offsetParent !== null;
  28. };
  29. /**
  30. * The focus controller allows us to manage focus within a view so assistive
  31. * technologies can inform users of changes to the navigation state. Traditional
  32. * native apps have a way of informing assistive technology about a navigation
  33. * state change. Mobile browsers have this too, but only when doing a full page
  34. * load. In a single page app we do not do that, so we need to build this
  35. * integration ourselves.
  36. */
  37. const createFocusController = () => {
  38. const saveViewFocus = (referenceEl) => {
  39. const focusManagerEnabled = index.config.get('focusManagerPriority', false);
  40. /**
  41. * When going back to a previously visited page focus should typically be moved
  42. * back to the element that was last focused when the user was on this view.
  43. */
  44. if (focusManagerEnabled) {
  45. const activeEl = document.activeElement;
  46. if (activeEl !== null && (referenceEl === null || referenceEl === void 0 ? void 0 : referenceEl.contains(activeEl))) {
  47. activeEl.setAttribute(LAST_FOCUS, 'true');
  48. }
  49. }
  50. };
  51. const setViewFocus = (referenceEl) => {
  52. const focusManagerPriorities = index.config.get('focusManagerPriority', false);
  53. /**
  54. * If the focused element is a descendant of the referenceEl then it's possible
  55. * that the app developer manually moved focus, so we do not want to override that.
  56. * This can happen with inputs the are focused when a view transitions in.
  57. */
  58. if (Array.isArray(focusManagerPriorities) && !referenceEl.contains(document.activeElement)) {
  59. /**
  60. * When going back to a previously visited view focus should always be moved back
  61. * to the element that the user was last focused on when they were on this view.
  62. */
  63. const lastFocus = referenceEl.querySelector(`[${LAST_FOCUS}]`);
  64. if (lastFocus && isVisible(lastFocus)) {
  65. moveFocus(lastFocus);
  66. return;
  67. }
  68. for (const priority of focusManagerPriorities) {
  69. /**
  70. * For each recognized case (excluding the default case) make sure to return
  71. * so that the fallback focus behavior does not run.
  72. *
  73. * We intentionally query for specific roles/semantic elements so that the
  74. * transition manager can work with both Ionic and non-Ionic UI components.
  75. *
  76. * If new selectors are added, be sure to remove the outline ring by adding
  77. * new selectors to rule in core.scss.
  78. */
  79. switch (priority) {
  80. case 'content':
  81. const content = referenceEl.querySelector('main, [role="main"]');
  82. if (content && isVisible(content)) {
  83. moveFocus(content);
  84. return;
  85. }
  86. break;
  87. case 'heading':
  88. const headingOne = referenceEl.querySelector('h1, [role="heading"][aria-level="1"]');
  89. if (headingOne && isVisible(headingOne)) {
  90. moveFocus(headingOne);
  91. return;
  92. }
  93. break;
  94. case 'banner':
  95. const header = referenceEl.querySelector('header, [role="banner"]');
  96. if (header && isVisible(header)) {
  97. moveFocus(header);
  98. return;
  99. }
  100. break;
  101. default:
  102. index.printIonWarning(`Unrecognized focus manager priority value ${priority}`);
  103. break;
  104. }
  105. }
  106. /**
  107. * If there is nothing to focus then focus the page so focus at least moves to
  108. * the correct view. The browser will then determine where within the page to
  109. * move focus to.
  110. */
  111. moveFocus(referenceEl);
  112. }
  113. };
  114. return {
  115. saveViewFocus,
  116. setViewFocus,
  117. };
  118. };
  119. const LAST_FOCUS = 'ion-last-focus';
  120. const iosTransitionAnimation = () => Promise.resolve().then(function () { return require('./ios.transition-a131cd4d.js'); });
  121. const mdTransitionAnimation = () => Promise.resolve().then(function () { return require('./md.transition-e018ebe5.js'); });
  122. const focusController = createFocusController();
  123. // TODO(FW-2832): types
  124. const transition = (opts) => {
  125. return new Promise((resolve, reject) => {
  126. index$1.writeTask(() => {
  127. beforeTransition(opts);
  128. runTransition(opts).then((result) => {
  129. if (result.animation) {
  130. result.animation.destroy();
  131. }
  132. afterTransition(opts);
  133. resolve(result);
  134. }, (error) => {
  135. afterTransition(opts);
  136. reject(error);
  137. });
  138. });
  139. });
  140. };
  141. const beforeTransition = (opts) => {
  142. const enteringEl = opts.enteringEl;
  143. const leavingEl = opts.leavingEl;
  144. focusController.saveViewFocus(leavingEl);
  145. setZIndex(enteringEl, leavingEl, opts.direction);
  146. if (opts.showGoBack) {
  147. enteringEl.classList.add('can-go-back');
  148. }
  149. else {
  150. enteringEl.classList.remove('can-go-back');
  151. }
  152. setPageHidden(enteringEl, false);
  153. /**
  154. * When transitioning, the page should not
  155. * respond to click events. This resolves small
  156. * issues like users double tapping the ion-back-button.
  157. * These pointer events are removed in `afterTransition`.
  158. */
  159. enteringEl.style.setProperty('pointer-events', 'none');
  160. if (leavingEl) {
  161. setPageHidden(leavingEl, false);
  162. leavingEl.style.setProperty('pointer-events', 'none');
  163. }
  164. };
  165. const runTransition = async (opts) => {
  166. const animationBuilder = await getAnimationBuilder(opts);
  167. const ani = animationBuilder && index$1.Build.isBrowser ? animation(animationBuilder, opts) : noAnimation(opts); // fast path for no animation
  168. return ani;
  169. };
  170. const afterTransition = (opts) => {
  171. const enteringEl = opts.enteringEl;
  172. const leavingEl = opts.leavingEl;
  173. enteringEl.classList.remove('ion-page-invisible');
  174. enteringEl.style.removeProperty('pointer-events');
  175. if (leavingEl !== undefined) {
  176. leavingEl.classList.remove('ion-page-invisible');
  177. leavingEl.style.removeProperty('pointer-events');
  178. }
  179. focusController.setViewFocus(enteringEl);
  180. };
  181. const getAnimationBuilder = async (opts) => {
  182. if (!opts.leavingEl || !opts.animated || opts.duration === 0) {
  183. return undefined;
  184. }
  185. if (opts.animationBuilder) {
  186. return opts.animationBuilder;
  187. }
  188. const getAnimation = opts.mode === 'ios'
  189. ? (await iosTransitionAnimation()).iosTransitionAnimation
  190. : (await mdTransitionAnimation()).mdTransitionAnimation;
  191. return getAnimation;
  192. };
  193. const animation = async (animationBuilder, opts) => {
  194. await waitForReady(opts, true);
  195. const trans = animationBuilder(opts.baseEl, opts);
  196. fireWillEvents(opts.enteringEl, opts.leavingEl);
  197. const didComplete = await playTransition(trans, opts);
  198. if (opts.progressCallback) {
  199. opts.progressCallback(undefined);
  200. }
  201. if (didComplete) {
  202. fireDidEvents(opts.enteringEl, opts.leavingEl);
  203. }
  204. return {
  205. hasCompleted: didComplete,
  206. animation: trans,
  207. };
  208. };
  209. const noAnimation = async (opts) => {
  210. const enteringEl = opts.enteringEl;
  211. const leavingEl = opts.leavingEl;
  212. const focusManagerEnabled = index.config.get('focusManagerPriority', false);
  213. /**
  214. * If the focus manager is enabled then we need to wait for Ionic components to be
  215. * rendered otherwise the component to focus may not be focused because it is hidden.
  216. */
  217. await waitForReady(opts, focusManagerEnabled);
  218. fireWillEvents(enteringEl, leavingEl);
  219. fireDidEvents(enteringEl, leavingEl);
  220. return {
  221. hasCompleted: true,
  222. };
  223. };
  224. const waitForReady = async (opts, defaultDeep) => {
  225. const deep = opts.deepWait !== undefined ? opts.deepWait : defaultDeep;
  226. if (deep) {
  227. await Promise.all([deepReady(opts.enteringEl), deepReady(opts.leavingEl)]);
  228. }
  229. await notifyViewReady(opts.viewIsReady, opts.enteringEl);
  230. };
  231. const notifyViewReady = async (viewIsReady, enteringEl) => {
  232. if (viewIsReady) {
  233. await viewIsReady(enteringEl);
  234. }
  235. };
  236. const playTransition = (trans, opts) => {
  237. const progressCallback = opts.progressCallback;
  238. const promise = new Promise((resolve) => {
  239. trans.onFinish((currentStep) => resolve(currentStep === 1));
  240. });
  241. // cool, let's do this, start the transition
  242. if (progressCallback) {
  243. // this is a swipe to go back, just get the transition progress ready
  244. // kick off the swipe animation start
  245. trans.progressStart(true);
  246. progressCallback(trans);
  247. }
  248. else {
  249. // only the top level transition should actually start "play"
  250. // kick it off and let it play through
  251. // ******** DOM WRITE ****************
  252. trans.play();
  253. }
  254. // create a callback for when the animation is done
  255. return promise;
  256. };
  257. const fireWillEvents = (enteringEl, leavingEl) => {
  258. lifecycle(leavingEl, LIFECYCLE_WILL_LEAVE);
  259. lifecycle(enteringEl, LIFECYCLE_WILL_ENTER);
  260. };
  261. const fireDidEvents = (enteringEl, leavingEl) => {
  262. lifecycle(enteringEl, LIFECYCLE_DID_ENTER);
  263. lifecycle(leavingEl, LIFECYCLE_DID_LEAVE);
  264. };
  265. const lifecycle = (el, eventName) => {
  266. if (el) {
  267. const ev = new CustomEvent(eventName, {
  268. bubbles: false,
  269. cancelable: false,
  270. });
  271. el.dispatchEvent(ev);
  272. }
  273. };
  274. /**
  275. * Wait two request animation frame loops.
  276. * This allows the framework implementations enough time to mount
  277. * the user-defined contents. This is often needed when using inline
  278. * modals and popovers that accept user components. For popover,
  279. * the contents must be mounted for the popover to be sized correctly.
  280. * For modals, the contents must be mounted for iOS to run the
  281. * transition correctly.
  282. *
  283. * On Angular and React, a single raf is enough time, but for Vue
  284. * we need to wait two rafs. As a result we are using two rafs for
  285. * all frameworks to ensure contents are mounted.
  286. */
  287. const waitForMount = () => {
  288. return new Promise((resolve) => helpers.raf(() => helpers.raf(() => resolve())));
  289. };
  290. const deepReady = async (el) => {
  291. const element = el;
  292. if (element) {
  293. if (element.componentOnReady != null) {
  294. // eslint-disable-next-line custom-rules/no-component-on-ready-method
  295. const stencilEl = await element.componentOnReady();
  296. if (stencilEl != null) {
  297. return;
  298. }
  299. /**
  300. * Custom elements in Stencil will have __registerHost.
  301. */
  302. }
  303. else if (element.__registerHost != null) {
  304. /**
  305. * Non-lazy loaded custom elements need to wait
  306. * one frame for component to be loaded.
  307. */
  308. const waitForCustomElement = new Promise((resolve) => helpers.raf(resolve));
  309. await waitForCustomElement;
  310. return;
  311. }
  312. await Promise.all(Array.from(element.children).map(deepReady));
  313. }
  314. };
  315. const setPageHidden = (el, hidden) => {
  316. if (hidden) {
  317. el.setAttribute('aria-hidden', 'true');
  318. el.classList.add('ion-page-hidden');
  319. }
  320. else {
  321. el.hidden = false;
  322. el.removeAttribute('aria-hidden');
  323. el.classList.remove('ion-page-hidden');
  324. }
  325. };
  326. const setZIndex = (enteringEl, leavingEl, direction) => {
  327. if (enteringEl !== undefined) {
  328. enteringEl.style.zIndex = direction === 'back' ? '99' : '101';
  329. }
  330. if (leavingEl !== undefined) {
  331. leavingEl.style.zIndex = '100';
  332. }
  333. };
  334. const getIonPageElement = (element) => {
  335. if (element.classList.contains('ion-page')) {
  336. return element;
  337. }
  338. const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
  339. if (ionPage) {
  340. return ionPage;
  341. }
  342. // idk, return the original element so at least something animates and we don't have a null pointer
  343. return element;
  344. };
  345. exports.LIFECYCLE_DID_ENTER = LIFECYCLE_DID_ENTER;
  346. exports.LIFECYCLE_DID_LEAVE = LIFECYCLE_DID_LEAVE;
  347. exports.LIFECYCLE_WILL_ENTER = LIFECYCLE_WILL_ENTER;
  348. exports.LIFECYCLE_WILL_LEAVE = LIFECYCLE_WILL_LEAVE;
  349. exports.LIFECYCLE_WILL_UNLOAD = LIFECYCLE_WILL_UNLOAD;
  350. exports.deepReady = deepReady;
  351. exports.getIonPageElement = getIonPageElement;
  352. exports.lifecycle = lifecycle;
  353. exports.setPageHidden = setPageHidden;
  354. exports.transition = transition;
  355. exports.waitForMount = waitForMount;