mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(popover): popover can now be used inline (#23231)
BREAKING CHANGE: Converted `ion-popover` to use the Shadow DOM.
This commit is contained in:
38
core/src/components.d.ts
vendored
38
core/src/components.d.ts
vendored
@@ -1658,11 +1658,11 @@ export namespace Components {
|
||||
*/
|
||||
"backdropDismiss": boolean;
|
||||
/**
|
||||
* The component to display inside of the popover.
|
||||
* 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`.
|
||||
*/
|
||||
"component": ComponentRef;
|
||||
"component"?: ComponentRef;
|
||||
/**
|
||||
* The data to pass to the popover component.
|
||||
* 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.
|
||||
*/
|
||||
"componentProps"?: ComponentProps;
|
||||
/**
|
||||
@@ -1684,6 +1684,11 @@ export namespace Components {
|
||||
* The event to pass to the popover animation.
|
||||
*/
|
||||
"event": any;
|
||||
"inline": boolean;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
"isOpen": boolean;
|
||||
/**
|
||||
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
||||
*/
|
||||
@@ -4990,11 +4995,11 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"backdropDismiss"?: boolean;
|
||||
/**
|
||||
* The component to display inside of the popover.
|
||||
* 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`.
|
||||
*/
|
||||
"component": ComponentRef;
|
||||
"component"?: ComponentRef;
|
||||
/**
|
||||
* The data to pass to the popover component.
|
||||
* 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.
|
||||
*/
|
||||
"componentProps"?: ComponentProps;
|
||||
/**
|
||||
@@ -5010,6 +5015,11 @@ declare namespace LocalJSX {
|
||||
* The event to pass to the popover animation.
|
||||
*/
|
||||
"event"?: any;
|
||||
"inline"?: boolean;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
"isOpen"?: boolean;
|
||||
/**
|
||||
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
||||
*/
|
||||
@@ -5022,6 +5032,14 @@ declare namespace LocalJSX {
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* Emitted after the popover has dismissed. Shorthand for ionPopoverDidDismiss.
|
||||
*/
|
||||
"onDidDismiss"?: (event: CustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss.
|
||||
*/
|
||||
"onDidPresent"?: (event: CustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted after the popover has dismissed.
|
||||
*/
|
||||
@@ -5038,6 +5056,14 @@ declare namespace LocalJSX {
|
||||
* Emitted before the popover has presented.
|
||||
*/
|
||||
"onIonPopoverWillPresent"?: (event: CustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the popover has dismissed. Shorthand for ionPopoverWillDismiss.
|
||||
*/
|
||||
"onWillDismiss"?: (event: CustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted before the popover has presented. Shorthand for ionPopoverWillPresent.
|
||||
*/
|
||||
"onWillPresent"?: (event: CustomEvent<void>) => void;
|
||||
"overlayIndex": number;
|
||||
/**
|
||||
* If `true`, a backdrop will be displayed behind the popover.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* iOS Popover Enter Animation
|
||||
@@ -8,7 +9,8 @@ export const iosEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
let originY = 'top';
|
||||
let originX = 'left';
|
||||
|
||||
const contentEl = baseEl.querySelector('.popover-content') as HTMLElement;
|
||||
const root = getElementRoot(baseEl);
|
||||
const contentEl = root.querySelector('.popover-content') as HTMLElement;
|
||||
const contentDimentions = contentEl.getBoundingClientRect();
|
||||
const contentWidth = contentDimentions.width;
|
||||
const contentHeight = contentDimentions.height;
|
||||
@@ -24,7 +26,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
const targetWidth = (targetDim && targetDim.width) || 0;
|
||||
const targetHeight = (targetDim && targetDim.height) || 0;
|
||||
|
||||
const arrowEl = baseEl.querySelector('.popover-arrow') as HTMLElement;
|
||||
const arrowEl = root.querySelector('.popover-arrow') as HTMLElement;
|
||||
|
||||
const arrowDim = arrowEl.getBoundingClientRect();
|
||||
const arrowWidth = arrowDim.width;
|
||||
@@ -103,7 +105,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
backdropAnimation
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
|
||||
.beforeStyles({
|
||||
'pointer-events': 'none'
|
||||
@@ -111,11 +113,10 @@ export const iosEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
.afterClearStyles(['pointer-events']);
|
||||
|
||||
wrapperAnimation
|
||||
.addElement(baseEl.querySelector('.popover-wrapper')!)
|
||||
.addElement(root.querySelector('.popover-wrapper')!)
|
||||
.fromTo('opacity', 0.01, 1);
|
||||
|
||||
return baseAnimation
|
||||
.addElement(baseEl)
|
||||
.easing('ease')
|
||||
.duration(100)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* iOS Popover Leave Animation
|
||||
*/
|
||||
export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
|
||||
const root = getElementRoot(baseEl);
|
||||
const baseAnimation = createAnimation();
|
||||
const backdropAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
backdropAnimation
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 'var(--backdrop-opacity)', 0);
|
||||
|
||||
wrapperAnimation
|
||||
.addElement(baseEl.querySelector('.popover-wrapper')!)
|
||||
.addElement(root.querySelector('.popover-wrapper')!)
|
||||
.fromTo('opacity', 0.99, 0);
|
||||
|
||||
return baseAnimation
|
||||
.addElement(baseEl)
|
||||
.easing('ease')
|
||||
.duration(500)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* Md Popover Enter Animation
|
||||
@@ -12,7 +13,8 @@ export const mdEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
let originY = 'top';
|
||||
let originX = isRTL ? 'right' : 'left';
|
||||
|
||||
const contentEl = baseEl.querySelector('.popover-content') as HTMLElement;
|
||||
const root = getElementRoot(baseEl);
|
||||
const contentEl = root.querySelector('.popover-content') as HTMLElement;
|
||||
const contentDimentions = contentEl.getBoundingClientRect();
|
||||
const contentWidth = contentDimentions.width;
|
||||
const contentHeight = contentDimentions.height;
|
||||
@@ -85,7 +87,7 @@ export const mdEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
const viewportAnimation = createAnimation();
|
||||
|
||||
backdropAnimation
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
|
||||
.beforeStyles({
|
||||
'pointer-events': 'none'
|
||||
@@ -93,7 +95,7 @@ export const mdEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
.afterClearStyles(['pointer-events']);
|
||||
|
||||
wrapperAnimation
|
||||
.addElement(baseEl.querySelector('.popover-wrapper')!)
|
||||
.addElement(root.querySelector('.popover-wrapper')!)
|
||||
.fromTo('opacity', 0.01, 1);
|
||||
|
||||
contentAnimation
|
||||
@@ -106,11 +108,10 @@ export const mdEnterAnimation = (baseEl: HTMLElement, ev?: Event): Animation =>
|
||||
.fromTo('transform', 'scale(0.001)', 'scale(1)');
|
||||
|
||||
viewportAnimation
|
||||
.addElement(baseEl.querySelector('.popover-viewport')!)
|
||||
.addElement(root.querySelector('.popover-viewport')!)
|
||||
.fromTo('opacity', 0.01, 1);
|
||||
|
||||
return baseAnimation
|
||||
.addElement(baseEl)
|
||||
.easing('cubic-bezier(0.36,0.66,0.04,1)')
|
||||
.duration(300)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation, contentAnimation, viewportAnimation]);
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* Md Popover Leave Animation
|
||||
*/
|
||||
export const mdLeaveAnimation = (baseEl: HTMLElement): Animation => {
|
||||
const root = getElementRoot(baseEl);
|
||||
const baseAnimation = createAnimation();
|
||||
const backdropAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
backdropAnimation
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 'var(--backdrop-opacity)', 0);
|
||||
|
||||
wrapperAnimation
|
||||
.addElement(baseEl.querySelector('.popover-wrapper')!)
|
||||
.addElement(root.querySelector('.popover-wrapper')!)
|
||||
.fromTo('opacity', 0.99, 0);
|
||||
|
||||
return baseAnimation
|
||||
.addElement(baseEl)
|
||||
.easing('ease')
|
||||
.duration(500)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
color: $popover-text-color;
|
||||
|
||||
z-index: $z-index-overlay;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(.popover-interactive) .popover-content,
|
||||
:host(.popover-interactive) ion-backdrop {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:host(.overlay-hidden) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, h } from '@stencil/core';
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, OverlayEventDetail, OverlayInterface } from '../../interface';
|
||||
import { attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { raf } from '../../utils/helpers';
|
||||
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
import { deepReady } from '../../utils/transition';
|
||||
@@ -12,8 +13,36 @@ import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
import { mdEnterAnimation } from './animations/md.enter';
|
||||
import { mdLeaveAnimation } from './animations/md.leave';
|
||||
|
||||
const CoreDelegate = () => {
|
||||
let Cmp: any;
|
||||
const attachViewToDom = (parentElement: HTMLElement) => {
|
||||
Cmp = parentElement;
|
||||
const app = document.querySelector('ion-app') || document.body;
|
||||
if (app && Cmp) {
|
||||
app.appendChild(Cmp);
|
||||
}
|
||||
|
||||
return Cmp;
|
||||
}
|
||||
|
||||
const removeViewFromDom = () => {
|
||||
if (Cmp) {
|
||||
Cmp.remove();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return { attachViewToDom, removeViewFromDom }
|
||||
}
|
||||
|
||||
/**
|
||||
* @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',
|
||||
@@ -21,17 +50,25 @@ import { mdLeaveAnimation } from './animations/md.leave';
|
||||
ios: 'popover.ios.scss',
|
||||
md: 'popover.md.scss'
|
||||
},
|
||||
scoped: true
|
||||
shadow: true
|
||||
})
|
||||
export class Popover implements ComponentInterface, OverlayInterface {
|
||||
|
||||
private usersElement?: HTMLElement;
|
||||
private popoverIndex = popoverIds++;
|
||||
private popoverId?: string;
|
||||
private coreDelegate: FrameworkDelegate = CoreDelegate();
|
||||
private currentTransition?: Promise<any>;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@State() presented = false;
|
||||
|
||||
@Element() el!: HTMLIonPopoverElement;
|
||||
|
||||
/** @internal */
|
||||
@Prop() inline = true;
|
||||
|
||||
/** @internal */
|
||||
@Prop() delegate?: FrameworkDelegate;
|
||||
|
||||
@@ -50,11 +87,17 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@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;
|
||||
|
||||
@@ -66,6 +109,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
/**
|
||||
* Additional classes to apply for custom CSS. If multiple classes are
|
||||
* provided they should be separated by spaces.
|
||||
* @internal
|
||||
*/
|
||||
@Prop() cssClass?: string | string[];
|
||||
|
||||
@@ -96,6 +140,24 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() animated = 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;
|
||||
|
||||
@Watch('isOpen')
|
||||
onIsOpenChange(newValue: boolean, oldValue: boolean) {
|
||||
if (newValue === true && oldValue === false) {
|
||||
this.present();
|
||||
} else if (newValue === false && oldValue === true) {
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted after the popover has presented.
|
||||
*/
|
||||
@@ -116,10 +178,52 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@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>;
|
||||
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
/**
|
||||
* If user has custom ID set then we should
|
||||
* not assign the default incrementing ID.
|
||||
*/
|
||||
this.popoverId = (this.el.hasAttribute('id')) ? this.el.getAttribute('id')! : `ion-popover-${this.popoverIndex}`;
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
/**
|
||||
* If popover was rendered with isOpen="true"
|
||||
* then we should open popover immediately.
|
||||
*/
|
||||
if (this.isOpen === true) {
|
||||
raf(() => this.present());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the popover overlay after it has been created.
|
||||
*/
|
||||
@@ -128,17 +232,39 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
if (this.presented) {
|
||||
return;
|
||||
}
|
||||
const container = this.el.querySelector('.popover-content');
|
||||
if (!container) {
|
||||
throw new Error('container is undefined');
|
||||
|
||||
/**
|
||||
* When using an inline popover
|
||||
* and dismissing a popover it is possible to
|
||||
* quickly present the popover while it is
|
||||
* dismissing. We need to await any current
|
||||
* transition to allow the dismiss to finish
|
||||
* before presenting again.
|
||||
*/
|
||||
if (this.currentTransition !== undefined) {
|
||||
await this.currentTransition;
|
||||
}
|
||||
|
||||
const data = {
|
||||
...this.componentProps,
|
||||
popover: this.el
|
||||
};
|
||||
this.usersElement = await attachComponent(this.delegate, container, this.component, ['popover-viewport', (this.el as any)['s-sc']], data);
|
||||
|
||||
/**
|
||||
* If using popover inline
|
||||
* we potentially need to use the coreDelegate
|
||||
* so that this works in vanilla JS apps
|
||||
*/
|
||||
const delegate = (this.inline) ? this.delegate || this.coreDelegate : this.delegate;
|
||||
|
||||
this.usersElement = await attachComponent(delegate, this.el, this.component, ['popover-viewport'], data, this.inline);
|
||||
await deepReady(this.usersElement);
|
||||
return present(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, this.event);
|
||||
|
||||
this.currentTransition = present(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, this.event);
|
||||
|
||||
await this.currentTransition;
|
||||
|
||||
this.currentTransition = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,10 +275,26 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Method()
|
||||
async dismiss(data?: any, role?: string): Promise<boolean> {
|
||||
const shouldDismiss = await dismiss(this, data, role, 'popoverLeave', iosLeaveAnimation, mdLeaveAnimation, this.event);
|
||||
/**
|
||||
* When using an inline popover
|
||||
* and presenting a popover it is possible to
|
||||
* quickly dismiss the popover while it is
|
||||
* presenting. We need to await any current
|
||||
* transition to allow the present to finish
|
||||
* before dismissing again.
|
||||
*/
|
||||
if (this.currentTransition !== undefined) {
|
||||
await this.currentTransition;
|
||||
}
|
||||
|
||||
this.currentTransition = dismiss(this, data, role, 'popoverLeave', iosLeaveAnimation, mdLeaveAnimation, this.event);
|
||||
const shouldDismiss = await this.currentTransition;
|
||||
if (shouldDismiss) {
|
||||
await detachComponent(this.delegate, this.usersElement);
|
||||
}
|
||||
|
||||
this.currentTransition = undefined;
|
||||
|
||||
return shouldDismiss;
|
||||
}
|
||||
|
||||
@@ -198,7 +340,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
const { onLifecycle } = this;
|
||||
const { onLifecycle, presented, popoverId } = this;
|
||||
return (
|
||||
<Host
|
||||
aria-modal="true"
|
||||
@@ -207,10 +349,13 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
style={{
|
||||
zIndex: `${20000 + this.overlayIndex}`,
|
||||
}}
|
||||
id={popoverId}
|
||||
class={{
|
||||
...getClassMap(this.cssClass),
|
||||
[mode]: true,
|
||||
'popover-translucent': this.translucent
|
||||
'popover-translucent': this.translucent,
|
||||
'overlay-hidden': true,
|
||||
'popover-interactive': presented,
|
||||
}}
|
||||
onIonPopoverDidPresent={onLifecycle}
|
||||
onIonPopoverWillPresent={onLifecycle}
|
||||
@@ -219,16 +364,14 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
onIonDismiss={this.onDismiss}
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss} visible={this.showBackdrop}/>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<ion-backdrop part="backdrop" tappable={this.backdropDismiss} visible={this.showBackdrop}/>
|
||||
|
||||
<div class="popover-wrapper ion-overlay-wrapper">
|
||||
<div class="popover-arrow"></div>
|
||||
<div class="popover-content"></div>
|
||||
<div class="popover-arrow" part="arrow"></div>
|
||||
<div class="popover-content" part="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
@@ -240,3 +383,5 @@ const LIFECYCLE_MAP: any = {
|
||||
'ionPopoverWillDismiss': 'ionViewWillLeave',
|
||||
'ionPopoverDidDismiss': 'ionViewDidLeave',
|
||||
};
|
||||
|
||||
let popoverIds = 0;
|
||||
|
||||
@@ -2,9 +2,66 @@
|
||||
|
||||
A Popover is a dialog that appears on top of the current page. It can be used for anything, but generally it is used for overflow actions that don't fit in the navigation bar.
|
||||
|
||||
## Presenting
|
||||
There are two ways to use `ion-popover`: inline or via the `popoverController`. Each method comes with different considerations, so be sure to use the approach that best fits your use case.
|
||||
|
||||
To present a popover, call the `present` method on a popover instance. In order to position the popover relative to the element clicked, a click event needs to be passed into the options of the the `present` method. If the event is not passed, the popover will be positioned in the center of the viewport.
|
||||
## Inline Popovers
|
||||
|
||||
`ion-popover` can be used by writing the component directly in your template. This reduces the number of handlers you need to wire up in order to present the popover. See [Usage](#usage) for an example of how to write a popover inline.
|
||||
|
||||
When using `ion-popover` with Angular, React, or Vue, the component you pass in will be destroyed when the popover is dismissed. If you are not using a JavaScript Framework, you should use the `component` property to pass in the name of a Web Component. This Web Component will be destroyed when the popover is dismissed, and a new instance will be created if the popover is presented again.
|
||||
|
||||
### Angular
|
||||
|
||||
Since the component you passed in needs to be created when the popover is presented and destroyed when the popover is dismissed, we are unable to project the content using `<ng-content>` internally. Instead, we use `<ng-container>` which expects an `<ng-template>` to be passed in. As a result, when passing in your component you will need to wrap it in an `<ng-template>`:
|
||||
|
||||
```html
|
||||
<ion-popover [isOpen]="isPopoverOpen">
|
||||
<ng-template>
|
||||
<app-popover-content></app-popover-content>
|
||||
</ng-template>
|
||||
</ion-popover>
|
||||
```
|
||||
|
||||
Liam: Usage will be filled out via desktop popover PR.
|
||||
|
||||
### When to use
|
||||
|
||||
Liam: Will be filled out via desktop popover PR.
|
||||
|
||||
## Controller Popovers
|
||||
|
||||
`ion-popover` can also be presented programmatically by using the `popoverController` imported from Ionic Framework. This allows you to have complete control over when a popover is presented above and beyond the customization that inline popovers give you. See [Usage](#usage) for an example of how to use the `popoverController`.
|
||||
|
||||
Liam: Usage will be filled out via desktop popover PR.
|
||||
|
||||
|
||||
### When to use
|
||||
|
||||
Liam: Will be filled out via desktop popover PR.
|
||||
|
||||
## Interfaces
|
||||
|
||||
Below you will find all of the options available to you when using the `popoverController`. These options should be supplied when calling `popoverController.create()`.
|
||||
|
||||
```typescript
|
||||
interface PopoverOptions {
|
||||
component: any;
|
||||
componentProps?: { [key: string]: any };
|
||||
showBackdrop?: boolean;
|
||||
backdropDismiss?: boolean;
|
||||
translucent?: boolean;
|
||||
cssClass?: string | string[];
|
||||
event?: Event;
|
||||
animated?: boolean;
|
||||
|
||||
mode?: 'ios' | 'md';
|
||||
keyboardClose?: boolean;
|
||||
id?: string;
|
||||
|
||||
enterAnimation?: AnimationBuilder;
|
||||
leaveAnimation?: AnimationBuilder;
|
||||
}
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
@@ -360,30 +417,34 @@ export default defineComponent({
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- |
|
||||
| `animated` | `animated` | If `true`, the popover will animate. | `boolean` | `true` |
|
||||
| `backdropDismiss` | `backdrop-dismiss` | If `true`, the popover will be dismissed when the backdrop is clicked. | `boolean` | `true` |
|
||||
| `component` _(required)_ | `component` | The component to display inside of the popover. | `Function \| HTMLElement \| null \| string` | `undefined` |
|
||||
| `componentProps` | -- | The data to pass to the popover component. | `undefined \| { [key: string]: any; }` | `undefined` |
|
||||
| `cssClass` | `css-class` | Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces. | `string \| string[] \| undefined` | `undefined` |
|
||||
| `enterAnimation` | -- | Animation to use when the popover is presented. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
|
||||
| `event` | `event` | The event to pass to the popover animation. | `any` | `undefined` |
|
||||
| `keyboardClose` | `keyboard-close` | If `true`, the keyboard will be automatically dismissed when the overlay is presented. | `boolean` | `true` |
|
||||
| `leaveAnimation` | -- | Animation to use when the popover is dismissed. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
|
||||
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
|
||||
| `showBackdrop` | `show-backdrop` | If `true`, a backdrop will be displayed behind the popover. | `boolean` | `true` |
|
||||
| `translucent` | `translucent` | 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). | `boolean` | `false` |
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ----------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ----------- |
|
||||
| `animated` | `animated` | If `true`, the popover will animate. | `boolean` | `true` |
|
||||
| `backdropDismiss` | `backdrop-dismiss` | If `true`, the popover will be dismissed when the backdrop is clicked. | `boolean` | `true` |
|
||||
| `component` | `component` | 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`. | `Function \| HTMLElement \| null \| string \| undefined` | `undefined` |
|
||||
| `componentProps` | -- | 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. | `undefined \| { [key: string]: any; }` | `undefined` |
|
||||
| `enterAnimation` | -- | Animation to use when the popover is presented. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
|
||||
| `event` | `event` | The event to pass to the popover animation. | `any` | `undefined` |
|
||||
| `isOpen` | `is-open` | 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. | `boolean` | `false` |
|
||||
| `keyboardClose` | `keyboard-close` | If `true`, the keyboard will be automatically dismissed when the overlay is presented. | `boolean` | `true` |
|
||||
| `leaveAnimation` | -- | Animation to use when the popover is dismissed. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
|
||||
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
|
||||
| `showBackdrop` | `show-backdrop` | If `true`, a backdrop will be displayed behind the popover. | `boolean` | `true` |
|
||||
| `translucent` | `translucent` | 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). | `boolean` | `false` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| ----------------------- | ----------------------------------------- | -------------------------------------- |
|
||||
| `ionPopoverDidDismiss` | Emitted after the popover has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `ionPopoverDidPresent` | Emitted after the popover has presented. | `CustomEvent<void>` |
|
||||
| `ionPopoverWillDismiss` | Emitted before the popover has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `ionPopoverWillPresent` | Emitted before the popover has presented. | `CustomEvent<void>` |
|
||||
| Event | Description | Type |
|
||||
| ----------------------- | ------------------------------------------------------------------------------ | -------------------------------------- |
|
||||
| `didDismiss` | Emitted after the popover has dismissed. Shorthand for ionPopoverDidDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `didPresent` | Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss. | `CustomEvent<void>` |
|
||||
| `ionPopoverDidDismiss` | Emitted after the popover has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `ionPopoverDidPresent` | Emitted after the popover has presented. | `CustomEvent<void>` |
|
||||
| `ionPopoverWillDismiss` | Emitted before the popover has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `ionPopoverWillPresent` | Emitted before the popover has presented. | `CustomEvent<void>` |
|
||||
| `willDismiss` | Emitted before the popover has dismissed. Shorthand for ionPopoverWillDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `willPresent` | Emitted before the popover has presented. Shorthand for ionPopoverWillPresent. | `CustomEvent<void>` |
|
||||
|
||||
|
||||
## Methods
|
||||
@@ -429,6 +490,22 @@ Type: `Promise<void>`
|
||||
|
||||
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
| ---- | ----------------------------------------------------------- |
|
||||
| | Content is placed inside of the `.popover-content` element. |
|
||||
|
||||
|
||||
## Shadow Parts
|
||||
|
||||
| Part | Description |
|
||||
| ------------ | --------------------------------------------------------------------------- |
|
||||
| `"arrow"` | The arrow that points to the reference element. Only applies on `ios` mode. |
|
||||
| `"backdrop"` | The `ion-backdrop` element. |
|
||||
| `"content"` | The wrapper element for the default slot. |
|
||||
|
||||
|
||||
## CSS Custom Properties
|
||||
|
||||
| Name | Description |
|
||||
|
||||
38
core/src/components/popover/test/inline/e2e.ts
Normal file
38
core/src/components/popover/test/inline/e2e.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('popover: inline', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/popover/test/inline?ionic:_testing=true' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
await page.click('ion-button');
|
||||
await page.waitForSelector('ion-popover');
|
||||
|
||||
let popover = await page.find('ion-popover');
|
||||
|
||||
expect(popover).not.toBe(null);
|
||||
await popover.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await popover.callMethod('dismiss');
|
||||
await popover.waitForNotVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('dismiss'));
|
||||
|
||||
popover = await page.find('ion-popover');
|
||||
expect(popover).toBeNull();
|
||||
|
||||
await page.click('ion-button');
|
||||
await page.waitForSelector('ion-popover');
|
||||
|
||||
let popoverAgain = await page.find('ion-popover');
|
||||
|
||||
expect(popoverAgain).not.toBe(null);
|
||||
await popoverAgain.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
});
|
||||
45
core/src/components/popover/test/inline/index.html
Normal file
45
core/src/components/popover/test/inline/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Popover - Inline</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Popover - Inline</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button onclick="openPopover(event)">Open Popover</ion-button>
|
||||
|
||||
<ion-popover>
|
||||
<ion-content class="ion-padding">
|
||||
This is my inline popover content!
|
||||
</ion-content>
|
||||
</ion-popover>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const popover = document.querySelector('ion-popover');
|
||||
const openPopover = (ev) => {
|
||||
popover.isOpen = true;
|
||||
popover.event = ev;
|
||||
}
|
||||
|
||||
popover.addEventListener('didDismiss', () => {
|
||||
popover.isOpen = false;
|
||||
popover.event = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* iOS Toast Enter Animation
|
||||
*/
|
||||
export const iosEnterAnimation = (baseEl: ShadowRoot, position: string): Animation => {
|
||||
export const iosEnterAnimation = (baseEl: HTMLElement, position: string): Animation => {
|
||||
const baseAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
const hostEl = baseEl.host || baseEl;
|
||||
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;
|
||||
const root = getElementRoot(baseEl);
|
||||
const wrapperEl = root.querySelector('.toast-wrapper') as HTMLElement;
|
||||
|
||||
const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`;
|
||||
const top = `calc(10px + var(--ion-safe-area-top, 0px))`;
|
||||
@@ -22,7 +23,7 @@ export const iosEnterAnimation = (baseEl: ShadowRoot, position: string): Animati
|
||||
break;
|
||||
case 'middle':
|
||||
const topPosition = Math.floor(
|
||||
hostEl.clientHeight / 2 - wrapperEl.clientHeight / 2
|
||||
baseEl.clientHeight / 2 - wrapperEl.clientHeight / 2
|
||||
);
|
||||
wrapperEl.style.top = `${topPosition}px`;
|
||||
wrapperAnimation.fromTo('opacity', 0.01, 1);
|
||||
@@ -32,7 +33,6 @@ export const iosEnterAnimation = (baseEl: ShadowRoot, position: string): Animati
|
||||
break;
|
||||
}
|
||||
return baseAnimation
|
||||
.addElement(hostEl)
|
||||
.easing('cubic-bezier(.155,1.105,.295,1.12)')
|
||||
.duration(400)
|
||||
.addAnimation(wrapperAnimation);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* iOS Toast Leave Animation
|
||||
*/
|
||||
export const iosLeaveAnimation = (baseEl: ShadowRoot, position: string): Animation => {
|
||||
export const iosLeaveAnimation = (baseEl: HTMLElement, position: string): Animation => {
|
||||
const baseAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
const hostEl = baseEl.host || baseEl;
|
||||
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;
|
||||
const root = getElementRoot(baseEl);
|
||||
const wrapperEl = root.querySelector('.toast-wrapper') as HTMLElement;
|
||||
|
||||
const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`;
|
||||
const top = `calc(10px + var(--ion-safe-area-top, 0px))`;
|
||||
@@ -28,7 +29,6 @@ export const iosLeaveAnimation = (baseEl: ShadowRoot, position: string): Animati
|
||||
break;
|
||||
}
|
||||
return baseAnimation
|
||||
.addElement(hostEl)
|
||||
.easing('cubic-bezier(.36,.66,.04,1)')
|
||||
.duration(300)
|
||||
.addAnimation(wrapperAnimation);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* MD Toast Enter Animation
|
||||
*/
|
||||
export const mdEnterAnimation = (baseEl: ShadowRoot, position: string): Animation => {
|
||||
export const mdEnterAnimation = (baseEl: HTMLElement, position: string): Animation => {
|
||||
const baseAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
const hostEl = baseEl.host || baseEl;
|
||||
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;
|
||||
const root = getElementRoot(baseEl);
|
||||
const wrapperEl = root.querySelector('.toast-wrapper') as HTMLElement;
|
||||
|
||||
const bottom = `calc(8px + var(--ion-safe-area-bottom, 0px))`;
|
||||
const top = `calc(8px + var(--ion-safe-area-top, 0px))`;
|
||||
@@ -23,7 +24,7 @@ export const mdEnterAnimation = (baseEl: ShadowRoot, position: string): Animatio
|
||||
break;
|
||||
case 'middle':
|
||||
const topPosition = Math.floor(
|
||||
hostEl.clientHeight / 2 - wrapperEl.clientHeight / 2
|
||||
baseEl.clientHeight / 2 - wrapperEl.clientHeight / 2
|
||||
);
|
||||
wrapperEl.style.top = `${topPosition}px`;
|
||||
wrapperAnimation.fromTo('opacity', 0.01, 1);
|
||||
@@ -34,7 +35,6 @@ export const mdEnterAnimation = (baseEl: ShadowRoot, position: string): Animatio
|
||||
break;
|
||||
}
|
||||
return baseAnimation
|
||||
.addElement(hostEl)
|
||||
.easing('cubic-bezier(.36,.66,.04,1)')
|
||||
.duration(400)
|
||||
.addAnimation(wrapperAnimation);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* md Toast Leave Animation
|
||||
*/
|
||||
export const mdLeaveAnimation = (baseEl: ShadowRoot): Animation => {
|
||||
export const mdLeaveAnimation = (baseEl: HTMLElement): Animation => {
|
||||
const baseAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
const hostEl = baseEl.host || baseEl;
|
||||
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;
|
||||
const root = getElementRoot(baseEl);
|
||||
const wrapperEl = root.querySelector('.toast-wrapper') as HTMLElement;
|
||||
|
||||
wrapperAnimation
|
||||
.addElement(wrapperEl)
|
||||
.fromTo('opacity', 0.99, 0);
|
||||
|
||||
return baseAnimation
|
||||
.addElement(hostEl)
|
||||
.easing('cubic-bezier(.36,.66,.04,1)')
|
||||
.duration(300)
|
||||
.addAnimation(wrapperAnimation);
|
||||
|
||||
@@ -5,14 +5,15 @@ import { componentOnReady } from './helpers';
|
||||
export const attachComponent = async (
|
||||
delegate: FrameworkDelegate | undefined,
|
||||
container: Element,
|
||||
component: ComponentRef,
|
||||
component?: ComponentRef,
|
||||
cssClasses?: string[],
|
||||
componentProps?: { [key: string]: any }
|
||||
componentProps?: { [key: string]: any },
|
||||
inline?: boolean
|
||||
): Promise<HTMLElement> => {
|
||||
if (delegate) {
|
||||
return delegate.attachViewToDom(container, component, componentProps, cssClasses);
|
||||
}
|
||||
if (typeof component !== 'string' && !(component instanceof HTMLElement)) {
|
||||
if (!inline && typeof component !== 'string' && !(component instanceof HTMLElement)) {
|
||||
throw new Error('framework delegate is missing');
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ export const attachComponent = async (
|
||||
}
|
||||
|
||||
container.appendChild(el);
|
||||
|
||||
await new Promise(resolve => componentOnReady(el, resolve));
|
||||
|
||||
return el;
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface OverlayInterface {
|
||||
willDismiss: EventEmitter<OverlayEventDetail>;
|
||||
didDismiss: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
didPresentShorthand?: EventEmitter<void>;
|
||||
willPresentShorthand?: EventEmitter<void>;
|
||||
willDismissShorthand?: EventEmitter<OverlayEventDetail>;
|
||||
didDismissShorthand?: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
present(): Promise<void>;
|
||||
dismiss(data?: any, role?: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -50,9 +50,14 @@ export const createOverlay = <T extends HTMLIonOverlayElement>(tagName: string,
|
||||
const element = document.createElement(tagName) as HTMLIonOverlayElement;
|
||||
element.classList.add('overlay-hidden');
|
||||
|
||||
// convert the passed in overlay options into props
|
||||
// that get passed down into the new overlay
|
||||
Object.assign(element, opts);
|
||||
/**
|
||||
* Convert the passed in overlay options into props
|
||||
* that get passed down into the new overlay.
|
||||
* Inline is needed for ion-popover as it can
|
||||
* be presented via a controller or written
|
||||
* inline in a template.
|
||||
*/
|
||||
Object.assign(element, { ...opts, inline: false });
|
||||
|
||||
// append the overlay element to the document body
|
||||
getAppRoot(document).appendChild(element);
|
||||
@@ -112,48 +117,103 @@ const trapKeyboardFocus = (ev: Event, doc: Document) => {
|
||||
const lastOverlay = getOverlay(doc);
|
||||
const target = ev.target as HTMLElement | null;
|
||||
|
||||
// If no active overlay, ignore this event
|
||||
if (!lastOverlay || !target) { return; }
|
||||
|
||||
/**
|
||||
* 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 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) {
|
||||
lastOverlay.lastFocus = undefined;
|
||||
if (!lastOverlay || !target
|
||||
) { return; }
|
||||
|
||||
const trapScopedFocus = () => {
|
||||
/**
|
||||
* 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.
|
||||
* If we are focusing the overlay, clear
|
||||
* the last focused element so that hitting
|
||||
* tab activates the first focusable element
|
||||
* in the overlay wrapper.
|
||||
*/
|
||||
} 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; }
|
||||
if (lastOverlay === target) {
|
||||
lastOverlay.lastFocus = undefined;
|
||||
|
||||
const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper');
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
if (!overlayWrapper) { return; }
|
||||
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.
|
||||
*/
|
||||
if (overlayWrapper.contains(target)) {
|
||||
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 as HTMLElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 (overlayWrapper.contains(target)) {
|
||||
if (lastOverlay.contains(target)) {
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -167,7 +227,7 @@ const trapKeyboardFocus = (ev: Event, doc: Document) => {
|
||||
const lastFocus = lastOverlay.lastFocus;
|
||||
|
||||
// Focus the first element in the overlay wrapper
|
||||
focusFirstDescendant(overlayWrapper, lastOverlay);
|
||||
focusFirstDescendant(lastOverlay, lastOverlay);
|
||||
|
||||
/**
|
||||
* If the cached last focused element is the
|
||||
@@ -179,11 +239,17 @@ const trapKeyboardFocus = (ev: Event, doc: Document) => {
|
||||
* last focus to equal the active element.
|
||||
*/
|
||||
if (lastFocus === doc.activeElement) {
|
||||
focusLastDescendant(overlayWrapper, lastOverlay);
|
||||
focusLastDescendant(lastOverlay, lastOverlay);
|
||||
}
|
||||
lastOverlay.lastFocus = doc.activeElement as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOverlay.shadowRoot) {
|
||||
trapShadowFocus();
|
||||
} else {
|
||||
trapScopedFocus();
|
||||
}
|
||||
};
|
||||
|
||||
export const connectListeners = (doc: Document) => {
|
||||
@@ -248,6 +314,7 @@ export const present = async (
|
||||
}
|
||||
overlay.presented = true;
|
||||
overlay.willPresent.emit();
|
||||
overlay.willPresentShorthand?.emit();
|
||||
|
||||
const mode = getIonMode(overlay);
|
||||
// get the user's animation fn if one was provided
|
||||
@@ -258,6 +325,8 @@ export const present = async (
|
||||
const completed = await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
|
||||
if (completed) {
|
||||
overlay.didPresent.emit();
|
||||
overlay.didPresentShorthand?.emit();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,6 +388,8 @@ export const dismiss = async (
|
||||
// Overlay contents should not be clickable during dismiss
|
||||
overlay.el.style.setProperty('pointer-events', 'none');
|
||||
overlay.willDismiss.emit({ data, role });
|
||||
overlay.willDismissShorthand?.emit({ data, role });
|
||||
|
||||
const mode = getIonMode(overlay);
|
||||
const animationBuilder = (overlay.leaveAnimation)
|
||||
? overlay.leaveAnimation
|
||||
@@ -329,6 +400,7 @@ export const dismiss = async (
|
||||
await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
|
||||
}
|
||||
overlay.didDismiss.emit({ data, role });
|
||||
overlay.didDismissShorthand?.emit({ data, role });
|
||||
|
||||
activeAnimations.delete(overlay);
|
||||
|
||||
@@ -353,7 +425,7 @@ const overlayAnimation = async (
|
||||
// Make overlay visible in case it's hidden
|
||||
baseEl.classList.remove('overlay-hidden');
|
||||
|
||||
const aniRoot = baseEl.shadowRoot || overlay.el;
|
||||
const aniRoot = overlay.el;
|
||||
const animation = animationBuilder(aniRoot, opts);
|
||||
|
||||
if (!overlay.animated || !config.getBoolean('animated', true)) {
|
||||
@@ -363,7 +435,7 @@ const overlayAnimation = async (
|
||||
if (overlay.keyboardClose) {
|
||||
animation.beforeAddWrite(() => {
|
||||
const activeElement = baseEl.ownerDocument!.activeElement as HTMLElement;
|
||||
if (activeElement && activeElement.matches('input, ion-input, ion-textarea')) {
|
||||
if (activeElement && activeElement.matches('input,ion-input, ion-textarea')) {
|
||||
activeElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user