octicon-rss(16/)
You've already forked ionic-framework
mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 00:27:41 +08:00
In https://github.com/ionic-team/ionic-framework/issues/28981 there was some confusion surrounding how to remove an overlay from the DOM if it was never presented. The `dismiss` method will remove the overlay from the DOM, but only if the overlay is visible. Otherwise, it's a no-op. This PR updates the `dismiss` method docs for each overlay component to note that developers can use the browser's remove method to remove the element from the DOM.
742 lines
22 KiB
TypeScript
742 lines
22 KiB
TypeScript
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
|
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
|
import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate';
|
|
import { addEventListener, raf, hasLazyBuild } from '@utils/helpers';
|
|
import { createLockController } from '@utils/lock-controller';
|
|
import { printIonWarning } from '@utils/logging';
|
|
import {
|
|
BACKDROP,
|
|
dismiss,
|
|
eventMethod,
|
|
focusFirstDescendant,
|
|
prepareOverlay,
|
|
present,
|
|
setOverlayId,
|
|
} from '@utils/overlays';
|
|
import { isPlatform } from '@utils/platform';
|
|
import { getClassMap } from '@utils/theme';
|
|
import { deepReady, waitForMount } from '@utils/transition';
|
|
|
|
import { getIonMode } from '../../global/ionic-global';
|
|
import type { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate } from '../../interface';
|
|
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
|
|
|
import { iosEnterAnimation } from './animations/ios.enter';
|
|
import { iosLeaveAnimation } from './animations/ios.leave';
|
|
import { mdEnterAnimation } from './animations/md.enter';
|
|
import { mdLeaveAnimation } from './animations/md.leave';
|
|
import type {
|
|
PopoverInterface,
|
|
PopoverSize,
|
|
PositionAlign,
|
|
PositionReference,
|
|
PositionSide,
|
|
TriggerAction,
|
|
} from './popover-interface';
|
|
import { configureDismissInteraction, configureKeyboardInteraction, configureTriggerInteraction } from './utils';
|
|
|
|
// TODO(FW-2832): types
|
|
|
|
/**
|
|
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
|
*
|
|
* @slot - Content is placed inside of the `.popover-content` element.
|
|
*
|
|
* @part backdrop - The `ion-backdrop` element.
|
|
* @part arrow - The arrow that points to the reference element. Only applies on `ios` mode.
|
|
* @part content - The wrapper element for the default slot.
|
|
*/
|
|
@Component({
|
|
tag: 'ion-popover',
|
|
styleUrls: {
|
|
ios: 'popover.ios.scss',
|
|
md: 'popover.md.scss',
|
|
},
|
|
shadow: true,
|
|
})
|
|
export class Popover implements ComponentInterface, PopoverInterface {
|
|
private usersElement?: HTMLElement;
|
|
private triggerEl?: HTMLElement | null;
|
|
private parentPopover: HTMLIonPopoverElement | null = null;
|
|
private coreDelegate: FrameworkDelegate = CoreDelegate();
|
|
private readonly lockController = createLockController();
|
|
private destroyTriggerInteraction?: () => void;
|
|
private destroyKeyboardInteraction?: () => void;
|
|
private destroyDismissInteraction?: () => void;
|
|
|
|
private inline = false;
|
|
private workingDelegate?: FrameworkDelegate;
|
|
|
|
private focusDescendantOnPresent = false;
|
|
|
|
lastFocus?: HTMLElement;
|
|
|
|
@State() presented = false;
|
|
|
|
@Element() el!: HTMLIonPopoverElement;
|
|
|
|
/** @internal */
|
|
@Prop() hasController = false;
|
|
|
|
/** @internal */
|
|
@Prop() delegate?: FrameworkDelegate;
|
|
|
|
/** @internal */
|
|
@Prop() overlayIndex!: number;
|
|
|
|
/**
|
|
* Animation to use when the popover is presented.
|
|
*/
|
|
@Prop() enterAnimation?: AnimationBuilder;
|
|
|
|
/**
|
|
* Animation to use when the popover is dismissed.
|
|
*/
|
|
@Prop() leaveAnimation?: AnimationBuilder;
|
|
|
|
/**
|
|
* The component to display inside of the popover.
|
|
* You only need to use this if you are not using
|
|
* a JavaScript framework. Otherwise, you can just
|
|
* slot your component inside of `ion-popover`.
|
|
*/
|
|
@Prop() component?: ComponentRef;
|
|
|
|
/**
|
|
* The data to pass to the popover component.
|
|
* You only need to use this if you are not using
|
|
* a JavaScript framework. Otherwise, you can just
|
|
* set the props directly on your component.
|
|
*/
|
|
@Prop() componentProps?: ComponentProps;
|
|
|
|
/**
|
|
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
|
*/
|
|
@Prop() keyboardClose = true;
|
|
|
|
/**
|
|
* Additional classes to apply for custom CSS. If multiple classes are
|
|
* provided they should be separated by spaces.
|
|
* @internal
|
|
*/
|
|
@Prop() cssClass?: string | string[];
|
|
|
|
/**
|
|
* If `true`, the popover will be dismissed when the backdrop is clicked.
|
|
*/
|
|
@Prop() backdropDismiss = true;
|
|
|
|
/**
|
|
* The event to pass to the popover animation.
|
|
*/
|
|
@Prop() event: any;
|
|
|
|
/**
|
|
* If `true`, a backdrop will be displayed behind the popover.
|
|
* This property controls whether or not the backdrop
|
|
* darkens the screen when the popover is presented.
|
|
* It does not control whether or not the backdrop
|
|
* is active or present in the DOM.
|
|
*/
|
|
@Prop() showBackdrop = true;
|
|
|
|
/**
|
|
* If `true`, the popover will be translucent.
|
|
* Only applies when the mode is `"ios"` and the device supports
|
|
* [`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility).
|
|
*/
|
|
@Prop() translucent = false;
|
|
|
|
/**
|
|
* If `true`, the popover will animate.
|
|
*/
|
|
@Prop() animated = true;
|
|
|
|
/**
|
|
* Additional attributes to pass to the popover.
|
|
*/
|
|
@Prop() htmlAttributes?: { [key: string]: any };
|
|
|
|
/**
|
|
* Describes what kind of interaction with the trigger that
|
|
* should cause the popover to open. Does not apply when the `trigger`
|
|
* property is `undefined`.
|
|
* If `"click"`, the popover will be presented when the trigger is left clicked.
|
|
* If `"hover"`, the popover will be presented when a pointer hovers over the trigger.
|
|
* If `"context-menu"`, the popover will be presented when the trigger is right
|
|
* clicked on desktop and long pressed on mobile. This will also prevent your
|
|
* device's normal context menu from appearing.
|
|
*/
|
|
@Prop() triggerAction: TriggerAction = 'click';
|
|
|
|
/**
|
|
* An ID corresponding to the trigger element that
|
|
* causes the popover to open. Use the `trigger-action`
|
|
* property to customize the interaction that results in
|
|
* the popover opening.
|
|
*/
|
|
@Prop() trigger: string | undefined;
|
|
|
|
/**
|
|
* Describes how to calculate the popover width.
|
|
* If `"cover"`, the popover width will match the width of the trigger.
|
|
* If `"auto"`, the popover width will be set to a static default value.
|
|
*/
|
|
@Prop() size: PopoverSize = 'auto';
|
|
|
|
/**
|
|
* If `true`, the popover will be automatically
|
|
* dismissed when the content has been clicked.
|
|
*/
|
|
@Prop() dismissOnSelect = false;
|
|
|
|
/**
|
|
* Describes what to position the popover relative to.
|
|
* If `"trigger"`, the popover will be positioned relative
|
|
* to the trigger button. If passing in an event, this is
|
|
* determined via event.target.
|
|
* If `"event"`, the popover will be positioned relative
|
|
* to the x/y coordinates of the trigger action. If passing
|
|
* in an event, this is determined via event.clientX and event.clientY.
|
|
*/
|
|
@Prop() reference: PositionReference = 'trigger';
|
|
|
|
/**
|
|
* Describes which side of the `reference` point to position
|
|
* the popover on. The `"start"` and `"end"` values are RTL-aware,
|
|
* and the `"left"` and `"right"` values are not.
|
|
*/
|
|
@Prop() side: PositionSide = 'bottom';
|
|
|
|
/**
|
|
* Describes how to align the popover content with the `reference` point.
|
|
* Defaults to `"center"` for `ios` mode, and `"start"` for `md` mode.
|
|
*/
|
|
@Prop({ mutable: true }) alignment?: PositionAlign;
|
|
|
|
/**
|
|
* If `true`, the popover will display an arrow that points at the
|
|
* `reference` when running in `ios` mode. Does not apply in `md` mode.
|
|
*/
|
|
@Prop() arrow = true;
|
|
|
|
/**
|
|
* If `true`, the popover will open. If `false`, the popover will close.
|
|
* Use this if you need finer grained control over presentation, otherwise
|
|
* just use the popoverController or the `trigger` property.
|
|
* Note: `isOpen` will not automatically be set back to `false` when
|
|
* the popover dismisses. You will need to do that in your code.
|
|
*/
|
|
@Prop() isOpen = false;
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* If `true` the popover will not register its own keyboard event handlers.
|
|
* This allows the contents of the popover to handle their own keyboard interactions.
|
|
*
|
|
* If `false`, the popover will register its own keyboard event handlers for
|
|
* navigating `ion-list` items within a popover (up/down/home/end/etc.).
|
|
* This will also cancel browser keyboard event bindings to prevent scroll
|
|
* behavior in a popover using a list of items.
|
|
*/
|
|
@Prop() keyboardEvents = false;
|
|
|
|
@Watch('trigger')
|
|
@Watch('triggerAction')
|
|
onTriggerChange() {
|
|
this.configureTriggerInteraction();
|
|
}
|
|
|
|
@Watch('isOpen')
|
|
onIsOpenChange(newValue: boolean, oldValue: boolean) {
|
|
if (newValue === true && oldValue === false) {
|
|
this.present();
|
|
} else if (newValue === false && oldValue === true) {
|
|
this.dismiss();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If `true`, the component passed into `ion-popover` will
|
|
* automatically be mounted when the popover is created. The
|
|
* component will remain mounted even when the popover is dismissed.
|
|
* However, the component will be destroyed when the popover is
|
|
* destroyed. This property is not reactive and should only be
|
|
* used when initially creating a popover.
|
|
*
|
|
* Note: This feature only applies to inline popovers in JavaScript
|
|
* frameworks such as Angular, React, and Vue.
|
|
*/
|
|
@Prop() keepContentsMounted = false;
|
|
|
|
/**
|
|
* Emitted after the popover has presented.
|
|
*/
|
|
@Event({ eventName: 'ionPopoverDidPresent' }) didPresent!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the popover has presented.
|
|
*/
|
|
@Event({ eventName: 'ionPopoverWillPresent' }) willPresent!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the popover has dismissed.
|
|
*/
|
|
@Event({ eventName: 'ionPopoverWillDismiss' }) willDismiss!: EventEmitter<OverlayEventDetail>;
|
|
|
|
/**
|
|
* Emitted after the popover has dismissed.
|
|
*/
|
|
@Event({ eventName: 'ionPopoverDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
|
|
|
/**
|
|
* Emitted after the popover has presented.
|
|
* Shorthand for ionPopoverWillDismiss.
|
|
*/
|
|
@Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the popover has presented.
|
|
* Shorthand for ionPopoverWillPresent.
|
|
*/
|
|
@Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the popover has dismissed.
|
|
* Shorthand for ionPopoverWillDismiss.
|
|
*/
|
|
@Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
|
|
|
/**
|
|
* Emitted after the popover has dismissed.
|
|
* Shorthand for ionPopoverDidDismiss.
|
|
*/
|
|
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
|
|
|
/**
|
|
* Emitted before the popover has presented, but after the component
|
|
* has been mounted in the DOM.
|
|
* This event exists for ion-popover to resolve an issue with the
|
|
* popover and the lazy build, that the transition is unable to get
|
|
* the correct dimensions of the popover with auto sizing.
|
|
* This is not required for other overlays, since the existing
|
|
* overlay transitions are not effected by auto sizing content.
|
|
*
|
|
* @internal
|
|
*/
|
|
@Event() ionMount!: EventEmitter<void>;
|
|
|
|
connectedCallback() {
|
|
const { configureTriggerInteraction, el } = this;
|
|
|
|
prepareOverlay(el);
|
|
configureTriggerInteraction();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
const { destroyTriggerInteraction } = this;
|
|
|
|
if (destroyTriggerInteraction) {
|
|
destroyTriggerInteraction();
|
|
}
|
|
}
|
|
|
|
componentWillLoad() {
|
|
const { el } = this;
|
|
const popoverId = setOverlayId(el);
|
|
|
|
this.parentPopover = el.closest(`ion-popover:not(#${popoverId})`) as HTMLIonPopoverElement | null;
|
|
|
|
if (this.alignment === undefined) {
|
|
this.alignment = getIonMode(this) === 'ios' ? 'center' : 'start';
|
|
}
|
|
}
|
|
|
|
componentDidLoad() {
|
|
const { parentPopover, isOpen } = this;
|
|
|
|
/**
|
|
* If popover was rendered with isOpen="true"
|
|
* then we should open popover immediately.
|
|
*/
|
|
if (isOpen === true) {
|
|
raf(() => this.present());
|
|
}
|
|
|
|
if (parentPopover) {
|
|
addEventListener(parentPopover, 'ionPopoverWillDismiss', () => {
|
|
this.dismiss(undefined, undefined, false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When binding values in frameworks such as Angular
|
|
* it is possible for the value to be set after the Web Component
|
|
* initializes but before the value watcher is set up in Stencil.
|
|
* As a result, the watcher callback may not be fired.
|
|
* We work around this by manually calling the watcher
|
|
* callback when the component has loaded and the watcher
|
|
* is configured.
|
|
*/
|
|
this.configureTriggerInteraction();
|
|
}
|
|
|
|
/**
|
|
* When opening a popover from a trigger, we should not be
|
|
* modifying the `event` prop from inside the component.
|
|
* Additionally, when pressing the "Right" arrow key, we need
|
|
* to shift focus to the first descendant in the newly presented
|
|
* popover.
|
|
*
|
|
* @internal
|
|
*/
|
|
@Method()
|
|
async presentFromTrigger(event?: any, focusDescendant = false) {
|
|
this.focusDescendantOnPresent = focusDescendant;
|
|
|
|
await this.present(event);
|
|
|
|
this.focusDescendantOnPresent = false;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private getDelegate(force = false) {
|
|
if (this.workingDelegate && !force) {
|
|
return {
|
|
delegate: this.workingDelegate,
|
|
inline: this.inline,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 = this.el.parentNode as HTMLElement | null;
|
|
const inline = (this.inline = parentEl !== null && !this.hasController);
|
|
const delegate = (this.workingDelegate = inline ? this.delegate || this.coreDelegate : this.delegate);
|
|
|
|
return { inline, delegate };
|
|
}
|
|
|
|
/**
|
|
* Present the popover overlay after it has been created.
|
|
* Developers can pass a mouse, touch, or pointer event
|
|
* to position the popover relative to where that event
|
|
* was dispatched.
|
|
*/
|
|
@Method()
|
|
async present(event?: MouseEvent | TouchEvent | PointerEvent | CustomEvent): Promise<void> {
|
|
const unlock = await this.lockController.lock();
|
|
|
|
if (this.presented) {
|
|
unlock();
|
|
return;
|
|
}
|
|
|
|
const { el } = this;
|
|
|
|
const { inline, delegate } = this.getDelegate(true);
|
|
|
|
/**
|
|
* Emit ionMount so JS Frameworks have an opportunity
|
|
* to add the child component to the DOM. The child
|
|
* component will be assigned to this.usersElement below.
|
|
*/
|
|
this.ionMount.emit();
|
|
|
|
this.usersElement = await attachComponent(
|
|
delegate,
|
|
el,
|
|
this.component,
|
|
['popover-viewport'],
|
|
this.componentProps,
|
|
inline
|
|
);
|
|
|
|
if (!this.keyboardEvents) {
|
|
this.configureKeyboardInteraction();
|
|
}
|
|
this.configureDismissInteraction();
|
|
|
|
/**
|
|
* When using the lazy loaded build of Stencil, we need to wait
|
|
* for every Stencil component instance to be ready before presenting
|
|
* otherwise there can be a flash of unstyled content. With the
|
|
* custom elements bundle we need to wait for the JS framework
|
|
* mount the inner contents of the overlay otherwise WebKit may
|
|
* get the transition incorrect.
|
|
*/
|
|
if (hasLazyBuild(el)) {
|
|
await deepReady(this.usersElement);
|
|
/**
|
|
* If keepContentsMounted="true" then the
|
|
* JS Framework has already mounted the inner
|
|
* contents so there is no need to wait.
|
|
* Otherwise, we need to wait for the JS
|
|
* Framework to mount the inner contents
|
|
* of this component.
|
|
*/
|
|
} else if (!this.keepContentsMounted) {
|
|
await waitForMount();
|
|
}
|
|
|
|
await present<PopoverPresentOptions>(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, {
|
|
event: event || this.event,
|
|
size: this.size,
|
|
trigger: this.triggerEl,
|
|
reference: this.reference,
|
|
side: this.side,
|
|
align: this.alignment,
|
|
});
|
|
|
|
/**
|
|
* If popover is nested and was
|
|
* presented using the "Right" arrow key,
|
|
* we need to move focus to the first
|
|
* descendant inside of the popover.
|
|
*/
|
|
if (this.focusDescendantOnPresent) {
|
|
focusFirstDescendant(this.el, this.el);
|
|
}
|
|
|
|
unlock();
|
|
}
|
|
|
|
/**
|
|
* Dismiss the popover overlay after it has been presented.
|
|
*
|
|
* @param data Any data to emit in the dismiss events.
|
|
* @param role The role of the element that is dismissing the popover. For example, 'cancel' or 'backdrop'.
|
|
* @param dismissParentPopover If `true`, dismissing this popover will also dismiss
|
|
* a parent popover if this popover is nested. Defaults to `true`.
|
|
*
|
|
* This is a no-op if the overlay has not been presented yet. If you want
|
|
* to remove an overlay from the DOM that was never presented, use the
|
|
* [remove](https://developer.mozilla.org/en-US/docs/Web/API/Element/remove) method.
|
|
*/
|
|
@Method()
|
|
async dismiss(data?: any, role?: string, dismissParentPopover = true): Promise<boolean> {
|
|
const unlock = await this.lockController.lock();
|
|
|
|
const { destroyKeyboardInteraction, destroyDismissInteraction } = this;
|
|
if (dismissParentPopover && this.parentPopover) {
|
|
this.parentPopover.dismiss(data, role, dismissParentPopover);
|
|
}
|
|
|
|
const shouldDismiss = await dismiss<PopoverDismissOptions>(
|
|
this,
|
|
data,
|
|
role,
|
|
'popoverLeave',
|
|
iosLeaveAnimation,
|
|
mdLeaveAnimation,
|
|
this.event
|
|
);
|
|
|
|
if (shouldDismiss) {
|
|
if (destroyKeyboardInteraction) {
|
|
destroyKeyboardInteraction();
|
|
this.destroyKeyboardInteraction = undefined;
|
|
}
|
|
if (destroyDismissInteraction) {
|
|
destroyDismissInteraction();
|
|
this.destroyDismissInteraction = undefined;
|
|
}
|
|
|
|
/**
|
|
* If using popover inline
|
|
* we potentially need to use the coreDelegate
|
|
* so that this works in vanilla JS apps
|
|
*/
|
|
const { delegate } = this.getDelegate();
|
|
await detachComponent(delegate, this.usersElement);
|
|
}
|
|
|
|
unlock();
|
|
|
|
return shouldDismiss;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
@Method()
|
|
async getParentPopover(): Promise<HTMLIonPopoverElement | null> {
|
|
return this.parentPopover;
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when the popover did dismiss.
|
|
*/
|
|
@Method()
|
|
onDidDismiss<T = any>(): Promise<OverlayEventDetail<T>> {
|
|
return eventMethod(this.el, 'ionPopoverDidDismiss');
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when the popover will dismiss.
|
|
*/
|
|
@Method()
|
|
onWillDismiss<T = any>(): Promise<OverlayEventDetail<T>> {
|
|
return eventMethod(this.el, 'ionPopoverWillDismiss');
|
|
}
|
|
|
|
private onBackdropTap = () => {
|
|
this.dismiss(undefined, BACKDROP);
|
|
};
|
|
|
|
private onLifecycle = (modalEvent: CustomEvent) => {
|
|
const el = this.usersElement;
|
|
const name = LIFECYCLE_MAP[modalEvent.type];
|
|
if (el && name) {
|
|
const event = new CustomEvent(name, {
|
|
bubbles: false,
|
|
cancelable: false,
|
|
detail: modalEvent.detail,
|
|
});
|
|
el.dispatchEvent(event);
|
|
}
|
|
};
|
|
|
|
private configureTriggerInteraction = () => {
|
|
const { trigger, triggerAction, el, destroyTriggerInteraction } = this;
|
|
|
|
if (destroyTriggerInteraction) {
|
|
destroyTriggerInteraction();
|
|
}
|
|
|
|
if (trigger === undefined) {
|
|
return;
|
|
}
|
|
|
|
const triggerEl = (this.triggerEl = trigger !== undefined ? document.getElementById(trigger) : null);
|
|
if (!triggerEl) {
|
|
printIonWarning(
|
|
`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 ion-popover.`,
|
|
this.el
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.destroyTriggerInteraction = configureTriggerInteraction(triggerEl, triggerAction, el);
|
|
};
|
|
|
|
private configureKeyboardInteraction = () => {
|
|
const { destroyKeyboardInteraction, el } = this;
|
|
|
|
if (destroyKeyboardInteraction) {
|
|
destroyKeyboardInteraction();
|
|
}
|
|
|
|
this.destroyKeyboardInteraction = configureKeyboardInteraction(el);
|
|
};
|
|
|
|
private configureDismissInteraction = () => {
|
|
const { destroyDismissInteraction, parentPopover, triggerAction, triggerEl, el } = this;
|
|
|
|
if (!parentPopover || !triggerEl) {
|
|
return;
|
|
}
|
|
|
|
if (destroyDismissInteraction) {
|
|
destroyDismissInteraction();
|
|
}
|
|
|
|
this.destroyDismissInteraction = configureDismissInteraction(triggerEl, triggerAction, el, parentPopover);
|
|
};
|
|
|
|
render() {
|
|
const mode = getIonMode(this);
|
|
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this;
|
|
const desktop = isPlatform('desktop');
|
|
const enableArrow = arrow && !parentPopover;
|
|
|
|
return (
|
|
<Host
|
|
aria-modal="true"
|
|
no-router
|
|
tabindex="-1"
|
|
{...(htmlAttributes as any)}
|
|
style={{
|
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
}}
|
|
class={{
|
|
...getClassMap(this.cssClass),
|
|
[mode]: true,
|
|
'popover-translucent': this.translucent,
|
|
'overlay-hidden': true,
|
|
'popover-desktop': desktop,
|
|
[`popover-side-${side}`]: true,
|
|
'popover-nested': !!parentPopover,
|
|
}}
|
|
onIonPopoverDidPresent={onLifecycle}
|
|
onIonPopoverWillPresent={onLifecycle}
|
|
onIonPopoverWillDismiss={onLifecycle}
|
|
onIonPopoverDidDismiss={onLifecycle}
|
|
onIonBackdropTap={this.onBackdropTap}
|
|
>
|
|
{!parentPopover && <ion-backdrop tappable={this.backdropDismiss} visible={this.showBackdrop} part="backdrop" />}
|
|
|
|
<div class="popover-wrapper ion-overlay-wrapper" onClick={dismissOnSelect ? () => this.dismiss() : undefined}>
|
|
{enableArrow && <div class="popover-arrow" part="arrow"></div>}
|
|
<div class="popover-content" part="content">
|
|
<slot></slot>
|
|
</div>
|
|
</div>
|
|
</Host>
|
|
);
|
|
}
|
|
}
|
|
|
|
const LIFECYCLE_MAP: any = {
|
|
ionPopoverDidPresent: 'ionViewDidEnter',
|
|
ionPopoverWillPresent: 'ionViewWillEnter',
|
|
ionPopoverWillDismiss: 'ionViewWillLeave',
|
|
ionPopoverDidDismiss: 'ionViewDidLeave',
|
|
};
|
|
|
|
interface PopoverPresentOptions {
|
|
/**
|
|
* The original target event that presented the popover.
|
|
*/
|
|
event: Event;
|
|
/**
|
|
* Describes how to calculate the popover width.
|
|
*/
|
|
size: PopoverSize;
|
|
/**
|
|
* The element that causes the popover to open.
|
|
*/
|
|
trigger?: HTMLElement | null;
|
|
/**
|
|
* Describes what to position the popover relative to.
|
|
*/
|
|
reference: PositionReference;
|
|
/**
|
|
* Side of the `reference` point to position the popover on.
|
|
*/
|
|
side: PositionSide;
|
|
/**
|
|
* Describes how to align the popover content with the `reference` point.
|
|
*/
|
|
align?: PositionAlign;
|
|
}
|
|
|
|
type PopoverDismissOptions = Event;
|