@ionic/core
Version:
Base components for Ionic
835 lines (834 loc) • 33.9 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { doc } from "./browser/index";
import { focusFirstDescendant, focusLastDescendant, focusableQueryString } from "./focus-trap";
import { shouldUseCloseWatcher } from "./hardware-back-button";
import { printIonError, printIonWarning } from "./logging/index";
import { config } from "../global/config";
import { getIonMode } from "../global/ionic-global";
import { CoreDelegate } from "./framework-delegate";
import { BACKDROP_NO_SCROLL } from "./gesture/gesture-controller";
import { OVERLAY_BACK_BUTTON_PRIORITY } from "./hardware-back-button";
import { addEventListener, componentOnReady, focusVisibleElement, getElementRoot, removeEventListener, } from "./helpers";
let lastOverlayIndex = 0;
let lastId = 0;
export const activeAnimations = new WeakMap();
/**
* Determines if the overlay's backdrop is always blocking (no background interaction).
* Returns false if showBackdrop=false or backdropBreakpoint > 0.
*/
const isBackdropAlwaysBlocking = (el) => {
var _a;
return el.showBackdrop !== false && !(((_a = el.backdropBreakpoint) !== null && _a !== void 0 ? _a : 0) > 0);
};
const createController = (tagName) => {
return {
create(options) {
return createOverlay(tagName, options);
},
dismiss(data, role, id) {
return dismissOverlay(document, data, role, tagName, id);
},
async getTop() {
return getPresentedOverlay(document, tagName);
},
};
};
export const alertController = /*@__PURE__*/ createController('ion-alert');
export const actionSheetController = /*@__PURE__*/ createController('ion-action-sheet');
export const loadingController = /*@__PURE__*/ createController('ion-loading');
export const modalController = /*@__PURE__*/ createController('ion-modal');
/**
* @deprecated Use the inline ion-picker component instead.
*/
export const pickerController = /*@__PURE__*/ createController('ion-picker-legacy');
export const popoverController = /*@__PURE__*/ createController('ion-popover');
export const toastController = /*@__PURE__*/ createController('ion-toast');
/**
* Prepares the overlay element to be presented.
*/
export const prepareOverlay = (el) => {
if (typeof document !== 'undefined') {
/**
* Adds a single instance of event listeners for application behaviors:
*
* - Escape Key behavior to dismiss an overlay
* - Trapping focus within an overlay
* - Back button behavior to dismiss an overlay
*
* This only occurs when the first overlay is created.
*/
connectListeners(document);
}
const overlayIndex = lastOverlayIndex++;
/**
* overlayIndex is used in the overlay components to set a zIndex.
* This ensures that the most recently presented overlay will be
* on top.
*/
el.overlayIndex = overlayIndex;
};
/**
* Assigns an incrementing id to an overlay element, that does not
* already have an id assigned to it.
*
* Used to track unique instances of an overlay element.
*/
export const setOverlayId = (el) => {
if (!el.hasAttribute('id')) {
el.id = `ion-overlay-${++lastId}`;
}
return el.id;
};
export const createOverlay = (tagName, opts) => {
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined') {
return window.customElements.whenDefined(tagName).then(() => {
const element = document.createElement(tagName);
element.classList.add('overlay-hidden');
/**
* Convert the passed in overlay options into props
* that get passed down into the new overlay.
*/
Object.assign(element, Object.assign(Object.assign({}, opts), { hasController: true }));
// append the overlay element to the document body
getAppRoot(document).appendChild(element);
return new Promise((resolve) => componentOnReady(element, resolve));
});
}
return Promise.resolve();
};
const isOverlayHidden = (overlay) => overlay.classList.contains('overlay-hidden');
/**
* Focuses a particular element in an overlay. If the element
* doesn't have anything focusable associated with it then
* the overlay itself will be focused.
* This should be used instead of the focus() method
* on most elements because the focusable element
* may not be the host element.
*
* For example, if an ion-button should be focused
* then we should actually focus the native <button>
* element inside of ion-button's shadow root, not
* the host element itself.
*/
const focusElementInOverlay = (hostToFocus, overlay) => {
let elementToFocus = hostToFocus;
const shadowRoot = hostToFocus === null || hostToFocus === void 0 ? void 0 : hostToFocus.shadowRoot;
if (shadowRoot) {
// If there are no inner focusable elements, just focus the host element.
elementToFocus = shadowRoot.querySelector(focusableQueryString) || hostToFocus;
}
if (elementToFocus) {
focusVisibleElement(elementToFocus);
}
else {
// Focus overlay instead of letting focus escape
overlay.focus();
}
};
/**
* Traps keyboard focus inside of overlay components.
* Based on https://w3c.github.io/aria-practices/examples/dialog-modal/alertdialog.html
* This includes the following components: Action Sheet, Alert, Loading, Modal,
* Picker, and Popover.
* Should NOT include: Toast
*/
const trapKeyboardFocus = (ev, doc) => {
const lastOverlay = getPresentedOverlay(doc, 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker-legacy,ion-popover');
const target = ev.target;
/**
* If no active overlay, ignore this event.
*
* If this component uses the shadow dom,
* this global listener is pointless
* since it will not catch the focus
* traps as they are inside the shadow root.
* We need to add a listener to the shadow root
* itself to ensure the focus trap works.
*/
if (!lastOverlay || !target) {
return;
}
/**
* If the ion-disable-focus-trap class
* is present on an overlay, then this component
* instance has opted out of focus trapping.
* An example of this is when the sheet modal
* has a backdrop that is disabled. The content
* behind the sheet should be focusable until
* the backdrop is enabled.
*/
if (lastOverlay.classList.contains(FOCUS_TRAP_DISABLE_CLASS)) {
return;
}
const trapScopedFocus = () => {
/**
* If we are focusing the overlay, clear
* the last focused element so that hitting
* tab activates the first focusable element
* in the overlay wrapper.
*/
if (lastOverlay === target) {
lastOverlay.lastFocus = undefined;
/**
* Toasts can be presented from an overlay.
* However, focus should still be returned to
* the overlay when clicking a toast. Normally,
* focus would be returned to the last focusable
* descendant in the overlay which may not always be
* the button that the toast was presented from. In this case,
* the focus may be returned to an unexpected element.
* To account for this, we make sure to return focus to the
* last focused element in the overlay if focus is
* moved to the toast.
*/
}
else if (target.tagName === 'ION-TOAST') {
focusElementInOverlay(lastOverlay.lastFocus, lastOverlay);
/**
* Otherwise, we must be focusing an element
* inside of the overlay. The two possible options
* here are an input/button/etc or the ion-focus-trap
* element. The focus trap element is used to prevent
* the keyboard focus from leaving the overlay when
* using Tab or screen assistants.
*/
}
else {
/**
* We do not want to focus the traps, so get the overlay
* wrapper element as the traps live outside of the wrapper.
*/
const overlayRoot = getElementRoot(lastOverlay);
if (!overlayRoot.contains(target)) {
return;
}
const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper');
if (!overlayWrapper) {
return;
}
/**
* If the target is inside the wrapper, let the browser
* focus as normal and keep a log of the last focused element.
* Additionally, if the backdrop was tapped we should not
* move focus back inside the wrapper as that could cause
* an interactive elements focus state to activate.
*/
if (overlayWrapper.contains(target) || target === overlayRoot.querySelector('ion-backdrop')) {
lastOverlay.lastFocus = target;
}
else {
/**
* Otherwise, we must have focused one of the focus traps.
* We need to wrap the focus to either the first element
* or the last element.
*/
/**
* Once we call `focusFirstDescendant` and focus the first
* descendant, another focus event will fire which will
* cause `lastOverlay.lastFocus` to be updated before
* we can run the code after that. We will cache the value
* here to avoid that.
*/
const lastFocus = lastOverlay.lastFocus;
// Focus the first element in the overlay wrapper
focusFirstDescendant(overlayWrapper, lastOverlay);
/**
* If the cached last focused element is the
* same as the active element, then we need
* to wrap focus to the last descendant. This happens
* when the first descendant is focused, and the user
* presses Shift + Tab. The previous line will focus
* the same descendant again (the first one), causing
* last focus to equal the active element.
*/
if (lastFocus === doc.activeElement) {
focusLastDescendant(overlayWrapper, lastOverlay);
}
lastOverlay.lastFocus = doc.activeElement;
}
}
};
const trapShadowFocus = () => {
/**
* If the target is inside the wrapper, let the browser
* focus as normal and keep a log of the last focused element.
*/
if (lastOverlay.contains(target)) {
lastOverlay.lastFocus = target;
/**
* Toasts can be presented from an overlay.
* However, focus should still be returned to
* the overlay when clicking a toast. Normally,
* focus would be returned to the last focusable
* descendant in the overlay which may not always be
* the button that the toast was presented from. In this case,
* the focus may be returned to an unexpected element.
* To account for this, we make sure to return focus to the
* last focused element in the overlay if focus is
* moved to the toast.
*/
}
else if (target.tagName === 'ION-TOAST') {
focusElementInOverlay(lastOverlay.lastFocus, lastOverlay);
}
else {
/**
* Otherwise, we are about to have focus
* go out of the overlay. We need to wrap
* the focus to either the first element
* or the last element.
*/
/**
* Once we call `focusFirstDescendant` and focus the first
* descendant, another focus event will fire which will
* cause `lastOverlay.lastFocus` to be updated before
* we can run the code after that. We will cache the value
* here to avoid that.
*/
const lastFocus = lastOverlay.lastFocus;
// Focus the first element in the overlay wrapper
focusFirstDescendant(lastOverlay);
/**
* If the cached last focused element is the
* same as the active element, then we need
* to wrap focus to the last descendant. This happens
* when the first descendant is focused, and the user
* presses Shift + Tab. The previous line will focus
* the same descendant again (the first one), causing
* last focus to equal the active element.
*/
if (lastFocus === doc.activeElement) {
focusLastDescendant(lastOverlay);
}
lastOverlay.lastFocus = doc.activeElement;
}
};
if (lastOverlay.shadowRoot) {
trapShadowFocus();
}
else {
trapScopedFocus();
}
};
const connectListeners = (doc) => {
if (lastOverlayIndex === 0) {
lastOverlayIndex = 1;
doc.addEventListener('focus', (ev) => {
trapKeyboardFocus(ev, doc);
}, true);
// handle back-button click
doc.addEventListener('ionBackButton', (ev) => {
const lastOverlay = getPresentedOverlay(doc);
if (lastOverlay === null || lastOverlay === void 0 ? void 0 : lastOverlay.backdropDismiss) {
ev.detail.register(OVERLAY_BACK_BUTTON_PRIORITY, () => {
/**
* Do not return this promise otherwise
* the hardware back button utility will
* be blocked until the overlay dismisses.
* This is important for a modal with canDismiss.
* If the application presents a confirmation alert
* in the "canDismiss" callback, then it will be impossible
* to use the hardware back button to dismiss the alert
* dialog because the hardware back button utility
* is blocked on waiting for the modal to dismiss.
*/
lastOverlay.dismiss(undefined, BACKDROP);
});
}
});
/**
* Handle ESC to close overlay.
* CloseWatcher also handles pressing the Esc
* key, so if a browser supports CloseWatcher then
* this behavior will be handled via the ionBackButton
* event.
*/
if (!shouldUseCloseWatcher()) {
doc.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') {
const lastOverlay = getPresentedOverlay(doc);
if (lastOverlay === null || lastOverlay === void 0 ? void 0 : lastOverlay.backdropDismiss) {
lastOverlay.dismiss(undefined, BACKDROP);
}
}
});
}
}
};
export const dismissOverlay = (doc, data, role, overlayTag, id) => {
const overlay = getPresentedOverlay(doc, overlayTag, id);
if (!overlay) {
return Promise.reject('overlay does not exist');
}
return overlay.dismiss(data, role);
};
/**
* Returns a list of all overlays in the DOM even if they are not presented.
*/
export const getOverlays = (doc, selector) => {
if (selector === undefined) {
selector = 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker-legacy,ion-popover,ion-toast';
}
return Array.from(doc.querySelectorAll(selector)).filter((c) => c.overlayIndex > 0);
};
/**
* Returns a list of all presented overlays.
* Inline overlays can exist in the DOM but not be presented,
* so there are times when we want to exclude those.
* @param doc The document to find the element within.
* @param overlayTag The selector for the overlay, defaults to Ionic overlay components.
*/
const getPresentedOverlays = (doc, overlayTag) => {
return getOverlays(doc, overlayTag).filter((o) => !isOverlayHidden(o));
};
/**
* Returns a presented overlay element.
* @param doc The document to find the element within.
* @param overlayTag The selector for the overlay, defaults to Ionic overlay components.
* @param id The unique identifier for the overlay instance.
* @returns The overlay element or `undefined` if no overlay element is found.
*/
export const getPresentedOverlay = (doc, overlayTag, id) => {
const overlays = getPresentedOverlays(doc, overlayTag);
// If no id is provided, return the last presented overlay
// Otherwise, return the last overlay with the given id
return (id === undefined ? overlays : overlays.filter((o) => o.id === id)).slice(-1)[0];
};
/**
* When an overlay is presented, the main
* focus is the overlay not the page content.
* We need to remove the page content from the
* accessibility tree otherwise when
* users use "read screen from top" gestures with
* TalkBack and VoiceOver, the screen reader will begin
* to read the content underneath the overlay.
*
* We need a container where all page components
* exist that is separate from where the overlays
* are added in the DOM. For most apps, this element
* is the top most ion-router-outlet. In the event
* that devs are not using a router,
* they will need to add the "ion-view-container-root"
* id to the element that contains all of their views.
*
* TODO: If Framework supports having multiple top
* level router outlets we would need to update this.
* Example: One outlet for side menu and one outlet
* for main content.
*/
export const setRootAriaHidden = (hidden = false) => {
const root = getAppRoot(document);
const viewContainer = root.querySelector('ion-router-outlet, #ion-view-container-root');
if (!viewContainer) {
return;
}
if (hidden) {
viewContainer.setAttribute('aria-hidden', 'true');
}
else {
viewContainer.removeAttribute('aria-hidden');
}
};
export const present = async (overlay, name, iosEnterAnimation, mdEnterAnimation, opts) => {
var _a, _b;
if (overlay.presented) {
return;
}
/**
* When an overlay that steals focus
* is dismissed, focus should be returned
* to the element that was focused
* prior to the overlay opening. Toast
* does not steal focus and is excluded
* from returning focus as a result.
*/
if (overlay.el.tagName !== 'ION-TOAST') {
restoreElementFocus(overlay.el);
}
/**
* Due to accessibility guidelines, toasts do not have
* focus traps.
*
* All other overlays should have focus traps to prevent
* the keyboard focus from leaving the overlay unless
* developers explicitly opt out (for example, sheet
* modals that should permit background interaction).
*
* Note: Some apps move inline overlays to a specific container
* during the willPresent lifecycle (e.g., React portals via
* onWillPresent). Defer applying aria-hidden/inert to the app
* root until after willPresent so we can detect where the
* overlay is finally inserted. If the overlay is inside the
* view container subtree, skip adding aria-hidden/inert there
* to avoid disabling the overlay.
*/
const overlayEl = overlay.el;
const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false;
const shouldLockRoot = shouldTrapFocus && isBackdropAlwaysBlocking(overlayEl);
overlay.presented = true;
overlay.willPresent.emit();
if (shouldLockRoot) {
const root = getAppRoot(document);
const viewContainer = root.querySelector('ion-router-outlet, #ion-view-container-root');
const overlayInsideViewContainer = viewContainer ? viewContainer.contains(overlayEl) : false;
if (!overlayInsideViewContainer) {
setRootAriaHidden(true);
}
document.body.classList.add(BACKDROP_NO_SCROLL);
}
(_a = overlay.willPresentShorthand) === null || _a === void 0 ? void 0 : _a.emit();
const mode = getIonMode(overlay);
// get the user's animation fn if one was provided
const animationBuilder = overlay.enterAnimation
? overlay.enterAnimation
: config.get(name, mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);
const completed = await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
if (completed) {
overlay.didPresent.emit();
(_b = overlay.didPresentShorthand) === null || _b === void 0 ? void 0 : _b.emit();
}
/**
* If the focused element is already
* inside the overlay component then
* focus should not be moved from that
* to the overlay container.
*/
if (overlay.keyboardClose && (document.activeElement === null || !overlay.el.contains(document.activeElement))) {
overlay.el.focus();
}
/**
* If this overlay was previously dismissed without being
* the topmost one (such as by manually calling dismiss()),
* it would still have aria-hidden on being presented again.
* Removing it here ensures the overlay is visible to screen
* readers.
*
* If this overlay was being presented, then it was hidden
* from screen readers during the animation. Now that the
* animation is complete, we can reveal the overlay to
* screen readers.
*/
overlay.el.removeAttribute('aria-hidden');
overlay.el.removeAttribute('inert');
};
/**
* When an overlay component is dismissed,
* focus should be returned to the element
* that presented the overlay. Otherwise
* focus will be set on the body which
* means that people using screen readers
* or tabbing will need to re-navigate
* to where they were before they
* opened the overlay.
*/
const restoreElementFocus = async (overlayEl) => {
let previousElement = document.activeElement;
if (!previousElement) {
return;
}
// Ensure active element is blurred to prevent a11y warning issues
previousElement.blur();
const shadowRoot = previousElement === null || previousElement === void 0 ? void 0 : previousElement.shadowRoot;
if (shadowRoot) {
// If there are no inner focusable elements, just focus the host element.
previousElement = shadowRoot.querySelector(focusableQueryString) || previousElement;
}
await overlayEl.onDidDismiss();
/**
* After onDidDismiss, the overlay loses focus
* because it is removed from the document
*
* > An element will also lose focus [...]
* > if the element is removed from the document)
*
* https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
*
* Additionally, `document.activeElement` returns:
*
* > The Element which currently has focus,
* > `<body>` or null if there is
* > no focused element.
*
* https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement#value
*
* However, if the user has already focused
* an element sometime between onWillDismiss
* and onDidDismiss (for example, focusing a
* text box after tapping a button in an
* action sheet) then don't restore focus to
* previous element
*/
if (document.activeElement === null || document.activeElement === document.body) {
previousElement.focus();
}
};
export const dismiss = async (overlay, data, role, name, iosLeaveAnimation, mdLeaveAnimation, opts) => {
var _a, _b;
if (!overlay.presented) {
return false;
}
const presentedOverlays = doc !== undefined ? getPresentedOverlays(doc) : [];
/**
* For accessibility, toasts lack focus traps and don't receive
* `aria-hidden` on the root element when presented.
*
* Overlays that opt into focus trapping set `aria-hidden`
* on the root element to keep keyboard focus and pointer
* events inside the overlay. We must remove `aria-hidden`
* from the root element when the last focus-trapping overlay
* is dismissed.
*/
const overlaysLockingRoot = presentedOverlays.filter((o) => {
const el = o;
return el.tagName !== 'ION-TOAST' && el.focusTrap !== false && isBackdropAlwaysBlocking(el);
});
const overlayEl = overlay.el;
const locksRoot = overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false && isBackdropAlwaysBlocking(overlayEl);
/**
* If this is the last visible overlay that is trapping focus
* then we want to re-add the root to the accessibility tree.
*/
const lastOverlayTrappingFocus = locksRoot && overlaysLockingRoot.length === 1 && overlaysLockingRoot[0].id === overlayEl.id;
if (lastOverlayTrappingFocus) {
setRootAriaHidden(false);
document.body.classList.remove(BACKDROP_NO_SCROLL);
}
overlay.presented = false;
try {
// Overlay contents should not be clickable during dismiss
overlay.el.style.setProperty('pointer-events', 'none');
overlay.willDismiss.emit({ data, role });
(_a = overlay.willDismissShorthand) === null || _a === void 0 ? void 0 : _a.emit({ data, role });
const mode = getIonMode(overlay);
const animationBuilder = overlay.leaveAnimation
? overlay.leaveAnimation
: config.get(name, mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation);
// If dismissed via gesture, no need to play leaving animation again
if (role !== GESTURE) {
await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
}
overlay.didDismiss.emit({ data, role });
(_b = overlay.didDismissShorthand) === null || _b === void 0 ? void 0 : _b.emit({ data, role });
// Get a reference to all animations currently assigned to this overlay
// Then tear them down to return the overlay to its initial visual state
const animations = activeAnimations.get(overlay) || [];
animations.forEach((ani) => ani.destroy());
activeAnimations.delete(overlay);
/**
* Make overlay hidden again in case it is being reused.
* We can safely remove pointer-events: none as
* overlay-hidden will set display: none.
*/
overlay.el.classList.add('overlay-hidden');
overlay.el.style.removeProperty('pointer-events');
/**
* Clear any focus trapping references
* when the overlay is dismissed.
*/
if (overlay.el.lastFocus !== undefined) {
overlay.el.lastFocus = undefined;
}
}
catch (err) {
printIonError(`[${overlay.el.tagName.toLowerCase()}] - `, err);
}
overlay.el.remove();
return true;
};
const getAppRoot = (doc) => {
return doc.querySelector('ion-app') || doc.body;
};
const overlayAnimation = async (overlay, animationBuilder, baseEl, opts) => {
// Make overlay visible in case it's hidden
baseEl.classList.remove('overlay-hidden');
const aniRoot = overlay.el;
const animation = animationBuilder(aniRoot, opts);
if (!overlay.animated || !config.getBoolean('animated', true)) {
animation.duration(0);
}
if (overlay.keyboardClose) {
animation.beforeAddWrite(() => {
const activeElement = baseEl.ownerDocument.activeElement;
if (activeElement === null || activeElement === void 0 ? void 0 : activeElement.matches('input,ion-input, ion-textarea')) {
activeElement.blur();
}
});
}
const activeAni = activeAnimations.get(overlay) || [];
activeAnimations.set(overlay, [...activeAni, animation]);
await animation.play();
return true;
};
export const eventMethod = (element, eventName) => {
let resolve;
const promise = new Promise((r) => (resolve = r));
onceEvent(element, eventName, (event) => {
resolve(event.detail);
});
return promise;
};
export const onceEvent = (element, eventName, callback) => {
const handler = (ev) => {
removeEventListener(element, eventName, handler);
callback(ev);
};
addEventListener(element, eventName, handler);
};
export const isCancel = (role) => {
return role === 'cancel' || role === BACKDROP;
};
const defaultGate = (h) => h();
/**
* Calls a developer provided method while avoiding
* Angular Zones. Since the handler is provided by
* the developer, we should throw any errors
* received so that developer-provided bug
* tracking software can log it.
*/
export const safeCall = (handler, arg) => {
if (typeof handler === 'function') {
const jmp = config.get('_zoneGate', defaultGate);
return jmp(() => {
try {
return handler(arg);
}
catch (e) {
throw e;
}
});
}
return undefined;
};
export const BACKDROP = 'backdrop';
export const GESTURE = 'gesture';
export const OVERLAY_GESTURE_PRIORITY = 39;
/**
* Creates a delegate controller.
*
* Requires that the component has the following properties:
* - `el: HTMLElement`
* - `hasController: boolean`
* - `delegate?: FrameworkDelegate`
*
* @param ref The component class instance.
*/
export const createDelegateController = (ref) => {
let inline = false;
let workingDelegate;
const coreDelegate = CoreDelegate();
/**
* Determines whether or not an overlay is being used
* inline or via a controller/JS and returns the correct delegate.
* By default, subsequent calls to getDelegate will use
* a cached version of the delegate.
* This is useful for calling dismiss after present,
* so that the correct delegate is given.
* @param force `true` to force the non-cached version of the delegate.
* @returns The delegate to use and whether or not the overlay is inline.
*/
const getDelegate = (force = false) => {
if (workingDelegate && !force) {
return {
delegate: workingDelegate,
inline,
};
}
const { el, hasController, delegate } = ref;
/**
* If using overlay inline
* we potentially need to use the coreDelegate
* so that this works in vanilla JS apps.
* If a developer has presented this component
* via a controller, then we can assume
* the component is already in the
* correct place.
*/
const parentEl = el.parentNode;
inline = parentEl !== null && !hasController;
workingDelegate = inline ? delegate || coreDelegate : delegate;
return { inline, delegate: workingDelegate };
};
/**
* Attaches a component in the DOM. Teleports the component
* to the root of the app.
* @param component The component to optionally construct and append to the element.
*/
const attachViewToDom = async (component) => {
const { delegate } = getDelegate(true);
if (delegate) {
return await delegate.attachViewToDom(ref.el, component);
}
const { hasController } = ref;
if (hasController && component !== undefined) {
throw new Error('framework delegate is missing');
}
return null;
};
/**
* Moves a component back to its original location in the DOM.
*/
const removeViewFromDom = () => {
const { delegate } = getDelegate();
if (delegate && ref.el !== undefined) {
delegate.removeViewFromDom(ref.el.parentElement, ref.el);
}
};
return {
attachViewToDom,
removeViewFromDom,
};
};
/**
* Constructs a trigger interaction for an overlay.
* Presents an overlay when the trigger is clicked.
*
* Usage:
* ```ts
* triggerController = createTriggerController();
* triggerController.addClickListener(el, trigger);
* ```
*/
export const createTriggerController = () => {
let destroyTriggerInteraction;
/**
* Removes the click listener from the trigger element.
*/
const removeClickListener = () => {
if (destroyTriggerInteraction) {
destroyTriggerInteraction();
destroyTriggerInteraction = undefined;
}
};
/**
* Adds a click listener to the trigger element.
* Presents the overlay when the trigger is clicked.
* @param el The overlay element.
* @param trigger The ID of the element to add a click listener to.
*/
const addClickListener = (el, trigger) => {
removeClickListener();
const triggerEl = trigger !== undefined ? document.getElementById(trigger) : null;
if (!triggerEl) {
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);
return;
}
const configureTriggerInteraction = (targetEl, overlayEl) => {
const openOverlay = () => {
overlayEl.present();
};
targetEl.addEventListener('click', openOverlay);
return () => {
targetEl.removeEventListener('click', openOverlay);
};
};
destroyTriggerInteraction = configureTriggerInteraction(triggerEl, el);
};
return {
addClickListener,
removeClickListener,
};
};
export const FOCUS_TRAP_DISABLE_CLASS = 'ion-disable-focus-trap';