UNPKG

14.2 kBJavaScriptView Raw
1'use strict';
2
3const ionicGlobal = require('./ionic-global-06f21c1a.js');
4const hardwareBackButton = require('./hardware-back-button-148ce546.js');
5const helpers = require('./helpers-d381ec4d.js');
6
7let lastId = 0;
8const activeAnimations = new WeakMap();
9const createController = (tagName) => {
10 return {
11 create(options) {
12 return createOverlay(tagName, options);
13 },
14 dismiss(data, role, id) {
15 return dismissOverlay(document, data, role, tagName, id);
16 },
17 async getTop() {
18 return getOverlay(document, tagName);
19 }
20 };
21};
22const alertController = /*@__PURE__*/ createController('ion-alert');
23const actionSheetController = /*@__PURE__*/ createController('ion-action-sheet');
24const loadingController = /*@__PURE__*/ createController('ion-loading');
25const modalController = /*@__PURE__*/ createController('ion-modal');
26const pickerController = /*@__PURE__*/ createController('ion-picker');
27const popoverController = /*@__PURE__*/ createController('ion-popover');
28const toastController = /*@__PURE__*/ createController('ion-toast');
29const prepareOverlay = (el) => {
30 /* tslint:disable-next-line */
31 if (typeof document !== 'undefined') {
32 connectListeners(document);
33 }
34 const overlayIndex = lastId++;
35 el.overlayIndex = overlayIndex;
36 if (!el.hasAttribute('id')) {
37 el.id = `ion-overlay-${overlayIndex}`;
38 }
39};
40const createOverlay = (tagName, opts) => {
41 /* tslint:disable-next-line */
42 if (typeof customElements !== 'undefined') {
43 return customElements.whenDefined(tagName).then(() => {
44 const element = document.createElement(tagName);
45 element.classList.add('overlay-hidden');
46 // convert the passed in overlay options into props
47 // that get passed down into the new overlay
48 Object.assign(element, opts);
49 // append the overlay element to the document body
50 getAppRoot(document).appendChild(element);
51 return new Promise(resolve => helpers.componentOnReady(element, resolve));
52 });
53 }
54 return Promise.resolve();
55};
56const focusableQueryString = '[tabindex]:not([tabindex^="-"]), input:not([type=hidden]):not([tabindex^="-"]), textarea:not([tabindex^="-"]), button:not([tabindex^="-"]), select:not([tabindex^="-"]), .ion-focusable:not([tabindex^="-"])';
57const innerFocusableQueryString = 'input:not([type=hidden]), textarea, button, select';
58const focusFirstDescendant = (ref, overlay) => {
59 let firstInput = ref.querySelector(focusableQueryString);
60 const shadowRoot = firstInput && firstInput.shadowRoot;
61 if (shadowRoot) {
62 // If there are no inner focusable elements, just focus the host element.
63 firstInput = shadowRoot.querySelector(innerFocusableQueryString) || firstInput;
64 }
65 if (firstInput) {
66 firstInput.focus();
67 }
68 else {
69 // Focus overlay instead of letting focus escape
70 overlay.focus();
71 }
72};
73const focusLastDescendant = (ref, overlay) => {
74 const inputs = Array.from(ref.querySelectorAll(focusableQueryString));
75 let lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
76 const shadowRoot = lastInput && lastInput.shadowRoot;
77 if (shadowRoot) {
78 // If there are no inner focusable elements, just focus the host element.
79 lastInput = shadowRoot.querySelector(innerFocusableQueryString) || lastInput;
80 }
81 if (lastInput) {
82 lastInput.focus();
83 }
84 else {
85 // Focus overlay instead of letting focus escape
86 overlay.focus();
87 }
88};
89/**
90 * Traps keyboard focus inside of overlay components.
91 * Based on https://w3c.github.io/aria-practices/examples/dialog-modal/alertdialog.html
92 * This includes the following components: Action Sheet, Alert, Loading, Modal,
93 * Picker, and Popover.
94 * Should NOT include: Toast
95 */
96const trapKeyboardFocus = (ev, doc) => {
97 const lastOverlay = getOverlay(doc);
98 const target = ev.target;
99 // If no active overlay, ignore this event
100 if (!lastOverlay || !target) {
101 return;
102 }
103 /**
104 * If we are focusing the overlay, clear
105 * the last focused element so that hitting
106 * tab activates the first focusable element
107 * in the overlay wrapper.
108 */
109 if (lastOverlay === target) {
110 lastOverlay.lastFocus = undefined;
111 /**
112 * Otherwise, we must be focusing an element
113 * inside of the overlay. The two possible options
114 * here are an input/button/etc or the ion-focus-trap
115 * element. The focus trap element is used to prevent
116 * the keyboard focus from leaving the overlay when
117 * using Tab or screen assistants.
118 */
119 }
120 else {
121 /**
122 * We do not want to focus the traps, so get the overlay
123 * wrapper element as the traps live outside of the wrapper.
124 */
125 const overlayRoot = helpers.getElementRoot(lastOverlay);
126 if (!overlayRoot.contains(target)) {
127 return;
128 }
129 const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper');
130 if (!overlayWrapper) {
131 return;
132 }
133 /**
134 * If the target is inside the wrapper, let the browser
135 * focus as normal and keep a log of the last focused element.
136 */
137 if (overlayWrapper.contains(target)) {
138 lastOverlay.lastFocus = target;
139 }
140 else {
141 /**
142 * Otherwise, we must have focused one of the focus traps.
143 * We need to wrap the focus to either the first element
144 * or the last element.
145 */
146 /**
147 * Once we call `focusFirstDescendant` and focus the first
148 * descendant, another focus event will fire which will
149 * cause `lastOverlay.lastFocus` to be updated before
150 * we can run the code after that. We will cache the value
151 * here to avoid that.
152 */
153 const lastFocus = lastOverlay.lastFocus;
154 // Focus the first element in the overlay wrapper
155 focusFirstDescendant(overlayWrapper, lastOverlay);
156 /**
157 * If the cached last focused element is the
158 * same as the active element, then we need
159 * to wrap focus to the last descendant. This happens
160 * when the first descendant is focused, and the user
161 * presses Shift + Tab. The previous line will focus
162 * the same descendant again (the first one), causing
163 * last focus to equal the active element.
164 */
165 if (lastFocus === doc.activeElement) {
166 focusLastDescendant(overlayWrapper, lastOverlay);
167 }
168 lastOverlay.lastFocus = doc.activeElement;
169 }
170 }
171};
172const connectListeners = (doc) => {
173 if (lastId === 0) {
174 lastId = 1;
175 doc.addEventListener('focus', ev => trapKeyboardFocus(ev, doc), true);
176 // handle back-button click
177 doc.addEventListener('ionBackButton', ev => {
178 const lastOverlay = getOverlay(doc);
179 if (lastOverlay && lastOverlay.backdropDismiss) {
180 ev.detail.register(hardwareBackButton.OVERLAY_BACK_BUTTON_PRIORITY, () => {
181 return lastOverlay.dismiss(undefined, BACKDROP);
182 });
183 }
184 });
185 // handle ESC to close overlay
186 doc.addEventListener('keyup', ev => {
187 if (ev.key === 'Escape') {
188 const lastOverlay = getOverlay(doc);
189 if (lastOverlay && lastOverlay.backdropDismiss) {
190 lastOverlay.dismiss(undefined, BACKDROP);
191 }
192 }
193 });
194 }
195};
196const dismissOverlay = (doc, data, role, overlayTag, id) => {
197 const overlay = getOverlay(doc, overlayTag, id);
198 if (!overlay) {
199 return Promise.reject('overlay does not exist');
200 }
201 return overlay.dismiss(data, role);
202};
203const getOverlays = (doc, selector) => {
204 if (selector === undefined) {
205 selector = 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker,ion-popover,ion-toast';
206 }
207 return Array.from(doc.querySelectorAll(selector))
208 .filter(c => c.overlayIndex > 0);
209};
210const getOverlay = (doc, overlayTag, id) => {
211 const overlays = getOverlays(doc, overlayTag);
212 return (id === undefined)
213 ? overlays[overlays.length - 1]
214 : overlays.find(o => o.id === id);
215};
216/**
217 * When an overlay is presented, the main
218 * focus is the overlay not the page content.
219 * We need to remove the page content from the
220 * accessibility tree otherwise when
221 * users use "read screen from top" gestures with
222 * TalkBack and VoiceOver, the screen reader will begin
223 * to read the content underneath the overlay.
224 *
225 * We need a container where all page components
226 * exist that is separate from where the overlays
227 * are added in the DOM. For most apps, this element
228 * is the top most ion-router-outlet. In the event
229 * that devs are not using a router,
230 * they will need to add the "ion-view-container-root"
231 * id to the element that contains all of their views.
232 *
233 * TODO: If Framework supports having multiple top
234 * level router outlets we would need to update this.
235 * Example: One outlet for side menu and one outlet
236 * for main content.
237 */
238const setRootAriaHidden = (hidden = false) => {
239 const root = getAppRoot(document);
240 const viewContainer = root.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root');
241 if (!viewContainer) {
242 return;
243 }
244 if (hidden) {
245 viewContainer.setAttribute('aria-hidden', 'true');
246 }
247 else {
248 viewContainer.removeAttribute('aria-hidden');
249 }
250};
251const present = async (overlay, name, iosEnterAnimation, mdEnterAnimation, opts) => {
252 if (overlay.presented) {
253 return;
254 }
255 setRootAriaHidden(true);
256 overlay.presented = true;
257 overlay.willPresent.emit();
258 const mode = ionicGlobal.getIonMode(overlay);
259 // get the user's animation fn if one was provided
260 const animationBuilder = (overlay.enterAnimation)
261 ? overlay.enterAnimation
262 : ionicGlobal.config.get(name, mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);
263 const completed = await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
264 if (completed) {
265 overlay.didPresent.emit();
266 }
267 /**
268 * When an overlay that steals focus
269 * is dismissed, focus should be returned
270 * to the element that was focused
271 * prior to the overlay opening. Toast
272 * does not steal focus and is excluded
273 * from returning focus as a result.
274 */
275 if (overlay.el.tagName !== 'ION-TOAST') {
276 focusPreviousElementOnDismiss(overlay.el);
277 }
278 if (overlay.keyboardClose) {
279 overlay.el.focus();
280 }
281};
282/**
283 * When an overlay component is dismissed,
284 * focus should be returned to the element
285 * that presented the overlay. Otherwise
286 * focus will be set on the body which
287 * means that people using screen readers
288 * or tabbing will need to re-navigate
289 * to where they were before they
290 * opened the overlay.
291 */
292const focusPreviousElementOnDismiss = async (overlayEl) => {
293 let previousElement = document.activeElement;
294 if (!previousElement) {
295 return;
296 }
297 const shadowRoot = previousElement && previousElement.shadowRoot;
298 if (shadowRoot) {
299 // If there are no inner focusable elements, just focus the host element.
300 previousElement = shadowRoot.querySelector(innerFocusableQueryString) || previousElement;
301 }
302 await overlayEl.onDidDismiss();
303 previousElement.focus();
304};
305const dismiss = async (overlay, data, role, name, iosLeaveAnimation, mdLeaveAnimation, opts) => {
306 if (!overlay.presented) {
307 return false;
308 }
309 setRootAriaHidden(false);
310 overlay.presented = false;
311 try {
312 // Overlay contents should not be clickable during dismiss
313 overlay.el.style.setProperty('pointer-events', 'none');
314 overlay.willDismiss.emit({ data, role });
315 const mode = ionicGlobal.getIonMode(overlay);
316 const animationBuilder = (overlay.leaveAnimation)
317 ? overlay.leaveAnimation
318 : ionicGlobal.config.get(name, mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation);
319 // If dismissed via gesture, no need to play leaving animation again
320 if (role !== 'gesture') {
321 await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
322 }
323 overlay.didDismiss.emit({ data, role });
324 activeAnimations.delete(overlay);
325 }
326 catch (err) {
327 console.error(err);
328 }
329 overlay.el.remove();
330 return true;
331};
332const getAppRoot = (doc) => {
333 return doc.querySelector('ion-app') || doc.body;
334};
335const overlayAnimation = async (overlay, animationBuilder, baseEl, opts) => {
336 // Make overlay visible in case it's hidden
337 baseEl.classList.remove('overlay-hidden');
338 const aniRoot = baseEl.shadowRoot || overlay.el;
339 const animation = animationBuilder(aniRoot, opts);
340 if (!overlay.animated || !ionicGlobal.config.getBoolean('animated', true)) {
341 animation.duration(0);
342 }
343 if (overlay.keyboardClose) {
344 animation.beforeAddWrite(() => {
345 const activeElement = baseEl.ownerDocument.activeElement;
346 if (activeElement && activeElement.matches('input, ion-input, ion-textarea')) {
347 activeElement.blur();
348 }
349 });
350 }
351 const activeAni = activeAnimations.get(overlay) || [];
352 activeAnimations.set(overlay, [...activeAni, animation]);
353 await animation.play();
354 return true;
355};
356const eventMethod = (element, eventName) => {
357 let resolve;
358 const promise = new Promise(r => resolve = r);
359 onceEvent(element, eventName, (event) => {
360 resolve(event.detail);
361 });
362 return promise;
363};
364const onceEvent = (element, eventName, callback) => {
365 const handler = (ev) => {
366 helpers.removeEventListener(element, eventName, handler);
367 callback(ev);
368 };
369 helpers.addEventListener(element, eventName, handler);
370};
371const isCancel = (role) => {
372 return role === 'cancel' || role === BACKDROP;
373};
374const defaultGate = (h) => h();
375const safeCall = (handler, arg) => {
376 if (typeof handler === 'function') {
377 const jmp = ionicGlobal.config.get('_zoneGate', defaultGate);
378 return jmp(() => {
379 try {
380 return handler(arg);
381 }
382 catch (e) {
383 console.error(e);
384 }
385 });
386 }
387 return undefined;
388};
389const BACKDROP = 'backdrop';
390
391exports.BACKDROP = BACKDROP;
392exports.actionSheetController = actionSheetController;
393exports.activeAnimations = activeAnimations;
394exports.alertController = alertController;
395exports.dismiss = dismiss;
396exports.eventMethod = eventMethod;
397exports.isCancel = isCancel;
398exports.loadingController = loadingController;
399exports.modalController = modalController;
400exports.pickerController = pickerController;
401exports.popoverController = popoverController;
402exports.prepareOverlay = prepareOverlay;
403exports.present = present;
404exports.safeCall = safeCall;
405exports.toastController = toastController;