index2.js 13 KB

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