From 3a85c51468a504adacf6ea05b2d2fc6225cf38e4 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Apr 2021 13:16:39 -0400 Subject: [PATCH 01/22] update popover to work inline --- core/api.txt | 3 +- core/src/components.d.ts | 14 ++- core/src/components/popover/popover.scss | 7 ++ core/src/components/popover/popover.tsx | 111 +++++++++++++++++++++-- core/src/components/popover/readme.md | 29 +++--- core/src/utils/overlays.ts | 11 ++- 6 files changed, 148 insertions(+), 27 deletions(-) diff --git a/core/api.txt b/core/api.txt index 326a803aa7b..b49bec88014 100644 --- a/core/api.txt +++ b/core/api.txt @@ -826,11 +826,12 @@ ion-picker,css-prop,--width ion-popover,scoped ion-popover,prop,animated,boolean,true,false,false ion-popover,prop,backdropDismiss,boolean,true,false,false -ion-popover,prop,component,Function | HTMLElement | null | string,undefined,true,false +ion-popover,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false ion-popover,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false ion-popover,prop,cssClass,string | string[] | undefined,undefined,false,false ion-popover,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false ion-popover,prop,event,any,undefined,false,false +ion-popover,prop,isOpen,boolean,false,false,false ion-popover,prop,keyboardClose,boolean,true,false,false ion-popover,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false ion-popover,prop,mode,"ios" | "md",undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index c352e052809..a12352575dc 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1660,7 +1660,7 @@ export namespace Components { /** * The component to display inside of the popover. */ - "component": ComponentRef; + "component"?: ComponentRef; /** * The data to pass to the popover component. */ @@ -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. */ @@ -4992,7 +4997,7 @@ declare namespace LocalJSX { /** * The component to display inside of the popover. */ - "component": ComponentRef; + "component"?: ComponentRef; /** * The data to pass to the popover component. */ @@ -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. */ diff --git a/core/src/components/popover/popover.scss b/core/src/components/popover/popover.scss index f538505f357..512de38dbb2 100644 --- a/core/src/components/popover/popover.scss +++ b/core/src/components/popover/popover.scss @@ -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) { diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 3584883e1af..75daf594908 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -1,4 +1,4 @@ -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'; @@ -12,6 +12,29 @@ 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.closest('ion-popover'); + 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. */ @@ -26,12 +49,20 @@ import { mdLeaveAnimation } from './animations/md.leave'; export class Popover implements ComponentInterface, OverlayInterface { private usersElement?: HTMLElement; + private popoverIndex = popoverIds++; + private popoverId?: string; + private coreDelegate: FrameworkDelegate = CoreDelegate(); + private currentTransition?: Promise; - presented = false; lastFocus?: HTMLElement; + @State() presented = false; + @Element() el!: HTMLIonPopoverElement; + /** @internal */ + @Prop() inline = true; + /** @internal */ @Prop() delegate?: FrameworkDelegate; @@ -51,7 +82,7 @@ export class Popover implements ComponentInterface, OverlayInterface { /** * The component to display inside of the popover. */ - @Prop() component!: ComponentRef; + @Prop() component?: ComponentRef; /** * The data to pass to the popover component. @@ -96,6 +127,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. */ @@ -120,6 +169,14 @@ export class Popover implements ComponentInterface, OverlayInterface { 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}`; + } + /** * Present the popover overlay after it has been created. */ @@ -128,6 +185,19 @@ export class Popover implements ComponentInterface, OverlayInterface { if (this.presented) { return; } + + /** + * 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 container = this.el.querySelector('.popover-content'); if (!container) { throw new Error('container is undefined'); @@ -138,7 +208,12 @@ export class Popover implements ComponentInterface, OverlayInterface { }; this.usersElement = await attachComponent(this.delegate, container, this.component, ['popover-viewport', (this.el as any)['s-sc']], data); 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 +224,26 @@ export class Popover implements ComponentInterface, OverlayInterface { */ @Method() async dismiss(data?: any, role?: string): Promise { - 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; } @@ -210,7 +301,9 @@ export class Popover implements ComponentInterface, OverlayInterface { class={{ ...getClassMap(this.cssClass), [mode]: true, - 'popover-translucent': this.translucent + 'popover-translucent': this.translucent, + 'overlay-hidden': true, + 'popover-interactive': presented, }} onIonPopoverDidPresent={onLifecycle} onIonPopoverWillPresent={onLifecycle} @@ -225,7 +318,9 @@ export class Popover implements ComponentInterface, OverlayInterface {
-
+
+ +
@@ -240,3 +335,5 @@ const LIFECYCLE_MAP: any = { 'ionPopoverWillDismiss': 'ionViewWillLeave', 'ionPopoverDidDismiss': 'ionViewDidLeave', }; + +let popoverIds = 0; diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index 910a21851c6..11995a972be 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -360,20 +360,21 @@ 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. | `Function \| HTMLElement \| null \| string \| undefined` | `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` | +| `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 diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 7e2395d341d..4cb9f4683d4 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -50,9 +50,14 @@ export const createOverlay = (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); From 4b96551354657b325f7ba67c273d0b47c3571262 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Apr 2021 13:36:21 -0400 Subject: [PATCH 02/22] add basic test, fix some bugs --- core/src/components/popover/popover.tsx | 13 ++++- core/src/components/popover/readme.md | 47 ++++++++++++++++++- .../src/components/popover/test/inline/e2e.ts | 38 +++++++++++++++ .../components/popover/test/inline/index.html | 45 ++++++++++++++++++ core/src/utils/framework-delegate.ts | 7 +-- 5 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 core/src/components/popover/test/inline/e2e.ts create mode 100644 core/src/components/popover/test/inline/index.html diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 75daf594908..61dc2d8cfe3 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -206,7 +206,15 @@ export class Popover implements ComponentInterface, OverlayInterface { ...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, container, this.component, ['popover-viewport', (this.el as any)['s-sc']], data, this.inline); await deepReady(this.usersElement); this.currentTransition = present(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, this.event); @@ -289,7 +297,7 @@ export class Popover implements ComponentInterface, OverlayInterface { render() { const mode = getIonMode(this); - const { onLifecycle } = this; + const { onLifecycle, presented, popoverId } = this; return ( { + 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(); + } +}); diff --git a/core/src/components/popover/test/inline/index.html b/core/src/components/popover/test/inline/index.html new file mode 100644 index 00000000000..0198d22fdfa --- /dev/null +++ b/core/src/components/popover/test/inline/index.html @@ -0,0 +1,45 @@ + + + + + Popover - Inline + + + + + + + + + + + Popover - Inline + + + + + Open Popover + + + + This is my inline popover content! + + + + + + + + + diff --git a/core/src/utils/framework-delegate.ts b/core/src/utils/framework-delegate.ts index ce0e6f1621f..11f358e5baf 100644 --- a/core/src/utils/framework-delegate.ts +++ b/core/src/utils/framework-delegate.ts @@ -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 => { 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'); } From cf2a79b644313ca8578e31a6145e72ccf56ff8b9 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Apr 2021 14:14:20 -0400 Subject: [PATCH 03/22] Add shorthand events --- core/src/components.d.ts | 16 +++++++++++++ core/src/components/popover/popover.tsx | 24 +++++++++++++++++++ .../components/popover/test/inline/index.html | 2 +- core/src/utils/overlays-interface.ts | 5 ++++ core/src/utils/overlays.ts | 6 +++++ 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index a12352575dc..ef2e88fa1b5 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -5032,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) => void; + /** + * Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss. + */ + "onDidPresent"?: (event: CustomEvent) => void; /** * Emitted after the popover has dismissed. */ @@ -5048,6 +5056,14 @@ declare namespace LocalJSX { * Emitted before the popover has presented. */ "onIonPopoverWillPresent"?: (event: CustomEvent) => void; + /** + * Emitted before the popover has dismissed. Shorthand for ionPopoverWillDismiss. + */ + "onWillDismiss"?: (event: CustomEvent) => void; + /** + * Emitted before the popover has presented. Shorthand for ionPopoverWillPresent. + */ + "onWillPresent"?: (event: CustomEvent) => void; "overlayIndex": number; /** * If `true`, a backdrop will be displayed behind the popover. diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 61dc2d8cfe3..8e019fc5456 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -165,6 +165,30 @@ export class Popover implements ComponentInterface, OverlayInterface { */ @Event({ eventName: 'ionPopoverDidDismiss' }) didDismiss!: EventEmitter; + /** + * Emitted after the popover has presented. + * Shorthand for ionPopoverWillDismiss. + */ + @Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter; + + /** + * Emitted before the popover has presented. + * Shorthand for ionPopoverWillPresent. + */ + @Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter; + + /** + * Emitted before the popover has dismissed. + * Shorthand for ionPopoverWillDismiss. + */ + @Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter; + + /** + * Emitted after the popover has dismissed. + * Shorthand for ionPopoverDidDismiss. + */ + @Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter; + connectedCallback() { prepareOverlay(this.el); } diff --git a/core/src/components/popover/test/inline/index.html b/core/src/components/popover/test/inline/index.html index 0198d22fdfa..2950693a107 100644 --- a/core/src/components/popover/test/inline/index.html +++ b/core/src/components/popover/test/inline/index.html @@ -34,7 +34,7 @@ popover.isOpen = true; popover.event = ev; - popover.addEventListener('ionPopoverDidDismiss', () => { + popover.addEventListener('didDismiss', () => { popover.isOpen = false; popover.event = undefined; }); diff --git a/core/src/utils/overlays-interface.ts b/core/src/utils/overlays-interface.ts index ace690ca521..48129871b7e 100644 --- a/core/src/utils/overlays-interface.ts +++ b/core/src/utils/overlays-interface.ts @@ -22,6 +22,11 @@ export interface OverlayInterface { willDismiss: EventEmitter; didDismiss: EventEmitter; + didPresentShorthand?: EventEmitter; + willPresentShorthand?: EventEmitter; + willDismissShorthand?: EventEmitter; + didDismissShorthand?: EventEmitter; + present(): Promise; dismiss(data?: any, role?: string): Promise; } diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 4cb9f4683d4..4d60015b63f 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -253,6 +253,7 @@ export const present = async ( } overlay.presented = true; overlay.willPresent.emit(); + overlay.willPresentShorthand && overlay.willPresentShorthand.emit(); const mode = getIonMode(overlay); // get the user's animation fn if one was provided @@ -263,6 +264,8 @@ export const present = async ( const completed = await overlayAnimation(overlay, animationBuilder, overlay.el, opts); if (completed) { overlay.didPresent.emit(); + overlay.didPresentShorthand && overlay.didPresentShorthand.emit(); + } /** @@ -324,6 +327,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 && overlay.willDismissShorthand.emit({ data, role }); + const mode = getIonMode(overlay); const animationBuilder = (overlay.leaveAnimation) ? overlay.leaveAnimation @@ -334,6 +339,7 @@ export const dismiss = async ( await overlayAnimation(overlay, animationBuilder, overlay.el, opts); } overlay.didDismiss.emit({ data, role }); + overlay.didDismissShorthand && overlay.didDismissShorthand.emit({ data, role }); activeAnimations.delete(overlay); From 046862682826ddf288c72f6780666b0b941a324e Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Apr 2021 14:17:46 -0400 Subject: [PATCH 04/22] add framework integrations --- angular/src/directives/proxies-list.txt | 1 + angular/src/directives/proxies.ts | 20 +++++++++++++++ core/api.txt | 4 +++ core/src/components/popover/readme.md | 16 +++++++----- core/stencil.config.ts | 2 -- packages/react/src/components/IonPopover.tsx | 12 --------- packages/react/src/components/index.ts | 1 - packages/react/src/components/proxies.ts | 4 +++ packages/vue/scripts/copy-overlays.js | 5 ---- packages/vue/src/proxies.ts | 27 ++++++++++++++++++++ 10 files changed, 66 insertions(+), 26 deletions(-) delete mode 100644 packages/react/src/components/IonPopover.tsx diff --git a/angular/src/directives/proxies-list.txt b/angular/src/directives/proxies-list.txt index 1d0a9e4bf2e..12a6fc4497f 100644 --- a/angular/src/directives/proxies-list.txt +++ b/angular/src/directives/proxies-list.txt @@ -47,6 +47,7 @@ export const DIRECTIVES = [ d.IonNav, d.IonNavLink, d.IonNote, + d.IonPopover, d.IonProgressBar, d.IonRadio, d.IonRadioGroup, diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index de592ce60bf..05220934825 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -542,6 +542,26 @@ export class IonNote { this.el = r.nativeElement; } } +export declare interface IonPopover extends Components.IonPopover { +} +@ProxyCmp({ inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"], "methods": ["present", "dismiss", "onDidDismiss", "onWillDismiss"] }) +@Component({ selector: "ion-popover", changeDetection: ChangeDetectionStrategy.OnPush, template: "", inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"] }) +export class IonPopover { + ionPopoverDidPresent!: EventEmitter; + ionPopoverWillPresent!: EventEmitter; + ionPopoverWillDismiss!: EventEmitter; + ionPopoverDidDismiss!: EventEmitter; + didPresent!: EventEmitter; + willPresent!: EventEmitter; + willDismiss!: EventEmitter; + didDismiss!: EventEmitter; + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ["ionPopoverDidPresent", "ionPopoverWillPresent", "ionPopoverWillDismiss", "ionPopoverDidDismiss", "didPresent", "willPresent", "willDismiss", "didDismiss"]); + } +} export declare interface IonProgressBar extends Components.IonProgressBar { } @ProxyCmp({ inputs: ["buffer", "color", "mode", "reversed", "type", "value"] }) diff --git a/core/api.txt b/core/api.txt index b49bec88014..f2f45d1806a 100644 --- a/core/api.txt +++ b/core/api.txt @@ -841,10 +841,14 @@ ion-popover,method,dismiss,dismiss(data?: any, role?: string | undefined) => Pro ion-popover,method,onDidDismiss,onDidDismiss() => Promise> ion-popover,method,onWillDismiss,onWillDismiss() => Promise> ion-popover,method,present,present() => Promise +ion-popover,event,didDismiss,OverlayEventDetail,true +ion-popover,event,didPresent,void,true ion-popover,event,ionPopoverDidDismiss,OverlayEventDetail,true ion-popover,event,ionPopoverDidPresent,void,true ion-popover,event,ionPopoverWillDismiss,OverlayEventDetail,true ion-popover,event,ionPopoverWillPresent,void,true +ion-popover,event,willDismiss,OverlayEventDetail,true +ion-popover,event,willPresent,void,true ion-popover,css-prop,--backdrop-opacity ion-popover,css-prop,--background ion-popover,css-prop,--box-shadow diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index f3b5ec53310..2a71533c59c 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -422,12 +422,16 @@ export default defineComponent({ ## Events -| Event | Description | Type | -| ----------------------- | ----------------------------------------- | -------------------------------------- | -| `ionPopoverDidDismiss` | Emitted after the popover has dismissed. | `CustomEvent>` | -| `ionPopoverDidPresent` | Emitted after the popover has presented. | `CustomEvent` | -| `ionPopoverWillDismiss` | Emitted before the popover has dismissed. | `CustomEvent>` | -| `ionPopoverWillPresent` | Emitted before the popover has presented. | `CustomEvent` | +| Event | Description | Type | +| ----------------------- | ------------------------------------------------------------------------------ | -------------------------------------- | +| `didDismiss` | Emitted after the popover has dismissed. Shorthand for ionPopoverDidDismiss. | `CustomEvent>` | +| `didPresent` | Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss. | `CustomEvent` | +| `ionPopoverDidDismiss` | Emitted after the popover has dismissed. | `CustomEvent>` | +| `ionPopoverDidPresent` | Emitted after the popover has presented. | `CustomEvent` | +| `ionPopoverWillDismiss` | Emitted before the popover has dismissed. | `CustomEvent>` | +| `ionPopoverWillPresent` | Emitted before the popover has presented. | `CustomEvent` | +| `willDismiss` | Emitted before the popover has dismissed. Shorthand for ionPopoverWillDismiss. | `CustomEvent>` | +| `willPresent` | Emitted before the popover has presented. Shorthand for ionPopoverWillPresent. | `CustomEvent` | ## Methods diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 71d7601ffb3..949ad216d62 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -82,7 +82,6 @@ export const config: Config = { 'ion-loading', 'ion-modal', 'ion-picker', - 'ion-popover', 'ion-toast', 'ion-app', @@ -155,7 +154,6 @@ export const config: Config = { 'ion-loading', 'ion-modal', 'ion-picker', - 'ion-popover', 'ion-toast', 'ion-toast', diff --git a/packages/react/src/components/IonPopover.tsx b/packages/react/src/components/IonPopover.tsx deleted file mode 100644 index 4684a1a2ea3..00000000000 --- a/packages/react/src/components/IonPopover.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PopoverOptions, popoverController } from '@ionic/core'; - -import { createOverlayComponent } from './createOverlayComponent'; - -export type ReactPopoverOptions = Omit & { - children: React.ReactNode; -}; - -export const IonPopover = /*@__PURE__*/ createOverlayComponent< - ReactPopoverOptions, - HTMLIonPopoverElement ->('IonPopover', popoverController); diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 0395ab811a5..771e7da3891 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -47,7 +47,6 @@ export { IonPicker } from './IonPicker'; // createOverlayComponent export * from './IonActionSheet'; export { IonModal } from './IonModal'; -export { IonPopover } from './IonPopover'; // Custom Components export { IonPage } from './IonPage'; diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index 21ea52aaa58..2fe818592ba 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -143,6 +143,10 @@ export const IonPickerColumn = /*@__PURE__*/ createReactComponent< JSX.IonPickerColumn, HTMLIonPickerColumnElement >('ion-picker-column'); +export const IonPopover = /*@__PURE__*/ createReactComponent< + JSX.IonPopover, + HTMLIonPopoverElement +>('ion-popover'); export const IonNav = /*@__PURE__*/ createReactComponent('ion-nav'); export const IonProgressBar = /*@__PURE__*/ createReactComponent< JSX.IonProgressBar, diff --git a/packages/vue/scripts/copy-overlays.js b/packages/vue/scripts/copy-overlays.js index 60c81de521b..37e8d949f5c 100644 --- a/packages/vue/scripts/copy-overlays.js +++ b/packages/vue/scripts/copy-overlays.js @@ -28,11 +28,6 @@ function generateOverlays() { controller: 'pickerController', name: 'IonPicker' }, - { - tag: 'ion-popover', - controller: 'popoverController', - name: 'IonPopover' - }, { tag: 'ion-toast', controller: 'toastController', diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 427568d9849..e329301870d 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -456,6 +456,33 @@ export const IonNote = /*@__PURE__*/ defineContainer('ion-note', [ ]); +export const IonPopover = /*@__PURE__*/ defineContainer('ion-popover', [ + 'inline', + 'delegate', + 'overlayIndex', + 'enterAnimation', + 'leaveAnimation', + 'component', + 'componentProps', + 'keyboardClose', + 'cssClass', + 'backdropDismiss', + 'event', + 'showBackdrop', + 'translucent', + 'animated', + 'isOpen', + 'ionPopoverDidPresent', + 'ionPopoverWillPresent', + 'ionPopoverWillDismiss', + 'ionPopoverDidDismiss', + 'didPresent', + 'willPresent', + 'willDismiss', + 'didDismiss' +]); + + export const IonProgressBar = /*@__PURE__*/ defineContainer('ion-progress-bar', [ 'type', 'reversed', From 753a1166a4461715ee68caa6b98de73c5583322f Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Apr 2021 14:56:09 -0400 Subject: [PATCH 05/22] add support for opening popover immediately --- core/src/components/popover/popover.tsx | 11 +++++++++++ core/src/components/popover/test/inline/index.html | 12 ++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 8e019fc5456..c742ffdf1cd 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -6,6 +6,7 @@ import { attachComponent, detachComponent } from '../../utils/framework-delegate import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays'; import { getClassMap } from '../../utils/theme'; import { deepReady } from '../../utils/transition'; +import { raf } from '../../utils/helpers'; import { iosEnterAnimation } from './animations/ios.enter'; import { iosLeaveAnimation } from './animations/ios.leave'; @@ -201,6 +202,16 @@ export class Popover implements ComponentInterface, OverlayInterface { 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. */ diff --git a/core/src/components/popover/test/inline/index.html b/core/src/components/popover/test/inline/index.html index 2950693a107..4b656a583b4 100644 --- a/core/src/components/popover/test/inline/index.html +++ b/core/src/components/popover/test/inline/index.html @@ -20,7 +20,7 @@ Open Popover - + This is my inline popover content! @@ -33,12 +33,12 @@ const openPopover = (ev) => { popover.isOpen = true; popover.event = ev; - - popover.addEventListener('didDismiss', () => { - popover.isOpen = false; - popover.event = undefined; - }); } + + popover.addEventListener('didDismiss', () => { + popover.isOpen = false; + popover.event = undefined; + }); From ff2a207b0158e0d98d2e01100c72cb6ffb220606 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Apr 2021 19:17:40 +0000 Subject: [PATCH 06/22] fix angular, core tests --- angular/src/ionic-module.ts | 3 ++- core/src/components/popover/popover.tsx | 2 +- .../src/components/popover/test/inline/index.html | 2 +- core/src/utils/overlays.ts | 8 ++++---- packages/vue/src/components/Overlays.ts | 3 --- packages/vue/test-app/src/views/Overlays.vue | 15 +++++++-------- packages/vue/test-app/tests/e2e/specs/overlays.js | 7 ++++--- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/angular/src/ionic-module.ts b/angular/src/ionic-module.ts index 8adfbab859c..66a5f52342d 100644 --- a/angular/src/ionic-module.ts +++ b/angular/src/ionic-module.ts @@ -13,7 +13,7 @@ import { IonRouterOutlet } from './directives/navigation/ion-router-outlet'; import { IonTabs } from './directives/navigation/ion-tabs'; import { NavDelegate } from './directives/navigation/nav-delegate'; import { RouterLinkDelegate } from './directives/navigation/router-link-delegate'; -import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies'; +import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonPopover, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies'; import { VirtualFooter } from './directives/virtual-scroll/virtual-footer'; import { VirtualHeader } from './directives/virtual-scroll/virtual-header'; import { VirtualItem } from './directives/virtual-scroll/virtual-item'; @@ -70,6 +70,7 @@ const DECLARATIONS = [ IonNav, IonNavLink, IonNote, + IonPopover, IonProgressBar, IonRadio, IonRadioGroup, diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index c742ffdf1cd..1166df062dd 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -3,10 +3,10 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth 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'; -import { raf } from '../../utils/helpers'; import { iosEnterAnimation } from './animations/ios.enter'; import { iosLeaveAnimation } from './animations/ios.leave'; diff --git a/core/src/components/popover/test/inline/index.html b/core/src/components/popover/test/inline/index.html index 4b656a583b4..29b28290ce0 100644 --- a/core/src/components/popover/test/inline/index.html +++ b/core/src/components/popover/test/inline/index.html @@ -20,7 +20,7 @@ Open Popover - + This is my inline popover content! diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 4d60015b63f..08361954d72 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -253,7 +253,7 @@ export const present = async ( } overlay.presented = true; overlay.willPresent.emit(); - overlay.willPresentShorthand && overlay.willPresentShorthand.emit(); + overlay.willPresentShorthand?.emit(); const mode = getIonMode(overlay); // get the user's animation fn if one was provided @@ -264,7 +264,7 @@ export const present = async ( const completed = await overlayAnimation(overlay, animationBuilder, overlay.el, opts); if (completed) { overlay.didPresent.emit(); - overlay.didPresentShorthand && overlay.didPresentShorthand.emit(); + overlay.didPresentShorthand?.emit(); } @@ -327,7 +327,7 @@ 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 && overlay.willDismissShorthand.emit({ data, role }); + overlay.willDismissShorthand?.emit({ data, role }); const mode = getIonMode(overlay); const animationBuilder = (overlay.leaveAnimation) @@ -339,7 +339,7 @@ export const dismiss = async ( await overlayAnimation(overlay, animationBuilder, overlay.el, opts); } overlay.didDismiss.emit({ data, role }); - overlay.didDismissShorthand && overlay.didDismissShorthand.emit({ data, role }); + overlay.didDismissShorthand?.emit({ data, role }); activeAnimations.delete(overlay); diff --git a/packages/vue/src/components/Overlays.ts b/packages/vue/src/components/Overlays.ts index d41c2586577..63cab9007c0 100644 --- a/packages/vue/src/components/Overlays.ts +++ b/packages/vue/src/components/Overlays.ts @@ -7,7 +7,6 @@ import { loadingController, modalController, pickerController, - popoverController, toastController } from '@ionic/core'; @@ -23,7 +22,5 @@ export const IonModal = /*@__PURE__*/defineOverlayContainer('ion-m export const IonPicker = /*@__PURE__*/defineOverlayContainer('ion-picker', ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop'], pickerController); -export const IonPopover = /*@__PURE__*/defineOverlayContainer('ion-popover', ['animated', 'backdropDismiss', 'component', 'componentProps', 'cssClass', 'enterAnimation', 'event', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', 'translucent'], popoverController); - export const IonToast = /*@__PURE__*/defineOverlayContainer('ion-toast', ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'position', 'translucent'], toastController); diff --git a/packages/vue/test-app/src/views/Overlays.vue b/packages/vue/test-app/src/views/Overlays.vue index 90502e966a0..723dc2327a6 100644 --- a/packages/vue/test-app/src/views/Overlays.vue +++ b/packages/vue/test-app/src/views/Overlays.vue @@ -110,11 +110,10 @@ - + { - const actionSheet = await actionSheetController.create({ buttons: actionSheetButtons }); + const actionSheet = await actionSheetController.create({ cssClass: "ion-action-sheet-controller", buttons: actionSheetButtons }); await actionSheet.present(); } const openAlert = async () => { - const alert = await alertController.create({ buttons: alertButtons, header: 'Alert!' }); + const alert = await alertController.create({ cssClass: "ion-alert-controller", buttons: alertButtons, header: 'Alert!' }); await alert.present(); } const openLoading = async () => { - const loading = await loadingController.create({ message: "Loading", duration: 2000, backdropDismiss: true }); + const loading = await loadingController.create({ cssClass: "ion-loading-controller", message: "Loading", duration: 2000, backdropDismiss: true }); await loading.present(); } const openToast = async () => { - const toast = await toastController.create({ header: "Toast!", buttons: toastButtons }); + const toast = await toastController.create({ cssClass: "ion-toast-controller", header: "Toast!", buttons: toastButtons }); await toast.present(); } const openModal = async () => { - const modal = await modalController.create({ component: ModalContent, componentProps: overlayProps }); + const modal = await modalController.create({ cssClass: "ion-modal-controller", component: ModalContent, componentProps: overlayProps }); await modal.present(); } const openPopover = async (event: Event) => { - const popover = await popoverController.create({ component: PopoverContent, event, componentProps: overlayProps }); + const popover = await popoverController.create({ cssClass: "ion-popover-controller", component: PopoverContent, event, componentProps: overlayProps }); await popover.present(); } diff --git a/packages/vue/test-app/tests/e2e/specs/overlays.js b/packages/vue/test-app/tests/e2e/specs/overlays.js index d6eb3dfcc7c..0cfadb5d03c 100644 --- a/packages/vue/test-app/tests/e2e/specs/overlays.js +++ b/packages/vue/test-app/tests/e2e/specs/overlays.js @@ -8,15 +8,16 @@ describe('Overlays', () => { for (let overlay of overlays) { it(`should open and close ${overlay} via controller`, () => { + const selector = `.${overlay}-controller`; cy.get(`ion-radio#${overlay}`).click(); cy.get('ion-radio#controller').click(); cy.get('ion-button#present-overlay').click(); - cy.get(overlay).should('exist').should('be.visible'); + cy.get(selector).should('exist').should('be.visible'); - cy.get(`${overlay} ion-backdrop`).click({ force: true }); + cy.get(`${selector} ion-backdrop`).click({ force: true }); - cy.get(overlay).should('not.exist'); + cy.get(selector).should('not.exist'); }); } From ceb05550d70c217ec33f1c2bd1e0013f5af2c86e Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Apr 2021 21:12:06 +0000 Subject: [PATCH 07/22] fix vue test --- packages/vue/test-app/src/views/Overlays.vue | 1 + packages/vue/test-app/tests/e2e/specs/overlays.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vue/test-app/src/views/Overlays.vue b/packages/vue/test-app/src/views/Overlays.vue index 723dc2327a6..fbdea4b5dcf 100644 --- a/packages/vue/test-app/src/views/Overlays.vue +++ b/packages/vue/test-app/src/views/Overlays.vue @@ -109,6 +109,7 @@ { cy.get('ion-radio#controller').click(); cy.get('ion-button#present-overlay').click(); - cy.get('ion-popover').should('exist'); + cy.get('ion-popover.ion-popover-controller').should('exist'); - cy.get('ion-popover ion-content').should('have.text', 'Custom Title'); + cy.get('ion-popover.ion-popover-controller ion-content').should('have.text', 'Custom Title'); }); it('should pass props to popover via component', () => { @@ -96,7 +96,7 @@ describe('Overlays', () => { cy.get('ion-button#present-overlay').click(); cy.get('ion-popover').should('exist'); - cy.get('ion-popover ion-content').should('have.text', 'Custom Title'); + cy.get('ion-popover.popover-inline ion-content').should('have.text', 'Custom Title'); }); it('should only open one instance at a time when props change quickly on component', () => { From e4f08bbdb7e934cffbb76153e34d7e3daf99866c Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Mon, 26 Apr 2021 17:35:28 +0000 Subject: [PATCH 08/22] add popover for ionic vue --- core/stencil.config.ts | 1 + packages/vue/src/components/IonPopover.ts | 22 ++++++++++++++++++ packages/vue/src/index.ts | 1 + packages/vue/src/proxies.ts | 27 ----------------------- 4 files changed, 24 insertions(+), 27 deletions(-) create mode 100644 packages/vue/src/components/IonPopover.ts diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 949ad216d62..17f2d568139 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -82,6 +82,7 @@ export const config: Config = { 'ion-loading', 'ion-modal', 'ion-picker', + 'ion-popover', 'ion-toast', 'ion-app', diff --git a/packages/vue/src/components/IonPopover.ts b/packages/vue/src/components/IonPopover.ts new file mode 100644 index 00000000000..b5ce6b59603 --- /dev/null +++ b/packages/vue/src/components/IonPopover.ts @@ -0,0 +1,22 @@ +import { defineComponent, h, ref, onMounted } from 'vue'; + +export const IonPopover = defineComponent({ + name: 'IonPopover', + setup(_, { attrs, slots }) { + const isOpen = ref(false); + const elementRef = ref(); + + onMounted(() => { + elementRef.value.addEventListener('will-present', () => isOpen.value = true); + elementRef.value.addEventListener('did-dismiss', () => isOpen.value = false); + }); + + return () => { + return h( + 'ion-popover', + { ...attrs, ref: elementRef }, + (isOpen.value) ? slots : undefined + ) + } + } +}); diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index f121e581ce3..dae0296500c 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -13,6 +13,7 @@ export { IonTabBar } from './components/IonTabBar'; export { IonNav } from './components/IonNav'; export { IonIcon } from './components/IonIcon'; export { IonApp } from './components/IonApp'; +export { IonPopover } from './components/IonPopover'; export * from './components/Overlays'; diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index e329301870d..427568d9849 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -456,33 +456,6 @@ export const IonNote = /*@__PURE__*/ defineContainer('ion-note', [ ]); -export const IonPopover = /*@__PURE__*/ defineContainer('ion-popover', [ - 'inline', - 'delegate', - 'overlayIndex', - 'enterAnimation', - 'leaveAnimation', - 'component', - 'componentProps', - 'keyboardClose', - 'cssClass', - 'backdropDismiss', - 'event', - 'showBackdrop', - 'translucent', - 'animated', - 'isOpen', - 'ionPopoverDidPresent', - 'ionPopoverWillPresent', - 'ionPopoverWillDismiss', - 'ionPopoverDidDismiss', - 'didPresent', - 'willPresent', - 'willDismiss', - 'didDismiss' -]); - - export const IonProgressBar = /*@__PURE__*/ defineContainer('ion-progress-bar', [ 'type', 'reversed', From cc6c620b3555bf1d91137d2e0769e41c7224ad3e Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 27 Apr 2021 16:48:16 +0000 Subject: [PATCH 09/22] add popover for ionic react --- packages/react/src/components/IonPopover.tsx | 7 + .../createInlineOverlayComponent.tsx | 126 ++++++++++++++++++ packages/react/src/components/index.ts | 1 + packages/react/src/components/proxies.ts | 4 - 4 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 packages/react/src/components/IonPopover.tsx create mode 100644 packages/react/src/components/createInlineOverlayComponent.tsx diff --git a/packages/react/src/components/IonPopover.tsx b/packages/react/src/components/IonPopover.tsx new file mode 100644 index 00000000000..cac5d9305b9 --- /dev/null +++ b/packages/react/src/components/IonPopover.tsx @@ -0,0 +1,7 @@ +import { JSX } from '@ionic/core'; +import { createInlineOverlayComponent } from './createInlineOverlayComponent' + +export const IonPopover = /*@__PURE__*/ createInlineOverlayComponent< + JSX.IonPopover, + HTMLIonPopoverElement +>('ion-popover'); diff --git a/packages/react/src/components/createInlineOverlayComponent.tsx b/packages/react/src/components/createInlineOverlayComponent.tsx new file mode 100644 index 00000000000..ef39a6c697f --- /dev/null +++ b/packages/react/src/components/createInlineOverlayComponent.tsx @@ -0,0 +1,126 @@ +import { OverlayEventDetail } from '@ionic/core' +import React from 'react'; + +import { + attachProps, + camelToDashCase, + createForwardRef, + dashToPascalCase, + isCoveredByReact, + mergeRefs, +} from './utils'; + +type InlineOverlayState = { + isOpen: boolean; +} + +interface IonicReactInternalProps extends React.HTMLAttributes { + forwardedRef?: React.ForwardedRef; + ref?: React.Ref; + onDidDismiss?: (event: CustomEvent) => void; + onDidPresent?: (event: CustomEvent) => void; + onWillDismiss?: (event: CustomEvent) => void; + onWillPresent?: (event: CustomEvent) => void; +} + +export const createInlineOverlayComponent = ( + tagName: string +) => { + const displayName = dashToPascalCase(tagName); + const ReactComponent = class extends React.Component, InlineOverlayState> { + ref: React.RefObject; + wrapperRef: React.RefObject; + stableMergedRefs: React.RefCallback + + constructor(props: IonicReactInternalProps) { + super(props); + // Create a local ref to to attach props to the wrapped element. + this.ref = React.createRef(); + // React refs must be stable (not created inline). + this.stableMergedRefs = mergeRefs(this.ref, this.props.forwardedRef) + // Component is hidden by default + this.state = { isOpen: false }; + // Create a local ref to the inner child element. + this.wrapperRef = React.createRef(); + } + + componentDidMount() { + this.componentDidUpdate(this.props); + + /** + * Mount the inner component + * when overlay is about to open. + * Also manually call the onWillPresent + * handler if present as setState will + * cause the event handlers to be + * destroyed and re-created. + */ + this.ref.current?.addEventListener('willPresent', (evt: any) => { + this.setState({ isOpen: true }); + + this.props.onWillPresent && this.props.onWillPresent(evt); + }); + + /** + * Unmount the inner component. + * React will call Node.removeChild + * which expects the child to be + * a direct descendent of the parent + * but due to the presence of + * Web Component slots, this is not + * always the case. To work around this + * we move the inner component to the root + * of the Web Component so React can + * cleanup properly. + */ + this.ref.current?.addEventListener('didDismiss', (evt: any) => { + const wrapper = this.wrapperRef.current!; + this.ref.current!.append(wrapper); + + this.setState({ isOpen: false }); + + this.props.onDidDismiss && this.props.onDidDismiss(evt); + }); + } + + componentDidUpdate(prevProps: IonicReactInternalProps) { + const node = this.ref.current! as HTMLElement; + attachProps(node, this.props, prevProps); + } + + render() { + const { children, forwardedRef, style, className, ref, ...cProps } = this.props; + + const propsToPass = Object.keys(cProps).reduce((acc, name) => { + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + const eventName = name.substring(2).toLowerCase(); + if (isCoveredByReact(eventName)) { + (acc as any)[name] = (cProps as any)[name]; + } + } else if (['string', 'boolean', 'number'].includes(typeof (cProps as any)[name])) { + (acc as any)[camelToDashCase(name)] = (cProps as any)[name]; + } + return acc; + }, {}); + + const newProps: IonicReactInternalProps = { + ...propsToPass, + ref: this.stableMergedRefs, + style, + }; + + /** + * We only want the inner component + * to be mounted if the overlay is open, + * so conditionally render the component + * based on the isOpen state. + */ + return React.createElement(tagName, newProps, (this.state.isOpen) ? React.createElement('div', { id: 'ion-react-wrapper', ref: this.wrapperRef }, children) : null); + } + + static get displayName() { + return displayName; + } + }; + return createForwardRef(ReactComponent, displayName); +}; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 771e7da3891..0395ab811a5 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -47,6 +47,7 @@ export { IonPicker } from './IonPicker'; // createOverlayComponent export * from './IonActionSheet'; export { IonModal } from './IonModal'; +export { IonPopover } from './IonPopover'; // Custom Components export { IonPage } from './IonPage'; diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index 2fe818592ba..21ea52aaa58 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -143,10 +143,6 @@ export const IonPickerColumn = /*@__PURE__*/ createReactComponent< JSX.IonPickerColumn, HTMLIonPickerColumnElement >('ion-picker-column'); -export const IonPopover = /*@__PURE__*/ createReactComponent< - JSX.IonPopover, - HTMLIonPopoverElement ->('ion-popover'); export const IonNav = /*@__PURE__*/ createReactComponent('ion-nav'); export const IonProgressBar = /*@__PURE__*/ createReactComponent< JSX.IonProgressBar, From f9bb970b8ab5ef8c27d765f2f79010853ba16ba7 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 27 Apr 2021 17:19:32 +0000 Subject: [PATCH 10/22] run linter --- packages/react/src/components/IonPopover.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/components/IonPopover.tsx b/packages/react/src/components/IonPopover.tsx index cac5d9305b9..91af321921f 100644 --- a/packages/react/src/components/IonPopover.tsx +++ b/packages/react/src/components/IonPopover.tsx @@ -1,4 +1,5 @@ import { JSX } from '@ionic/core'; + import { createInlineOverlayComponent } from './createInlineOverlayComponent' export const IonPopover = /*@__PURE__*/ createInlineOverlayComponent< From 80091832688c6b8194d52c5f30cd378330d350dd Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Apr 2021 11:52:41 -0400 Subject: [PATCH 11/22] convert popover to shadow dom --- BREAKING.md | 7 ++++++ core/api.txt | 5 +++- .../popover/animations/ios.enter.ts | 11 +++++---- .../popover/animations/ios.leave.ts | 7 +++--- .../components/popover/animations/md.enter.ts | 11 +++++---- .../components/popover/animations/md.leave.ts | 7 +++--- core/src/components/popover/popover.tsx | 23 ++++++++++--------- core/src/components/popover/readme.md | 16 +++++++++++++ core/src/utils/framework-delegate.ts | 1 + 9 files changed, 60 insertions(+), 28 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index f52b0a8b66f..ab70dd22e6a 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -14,6 +14,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver - [Components](#components) * [Header](#header) + * [Popover](#popover) * [Tab Bar](#tab-bar) * [Toast](#toast) * [Toolbar](#toolbar) @@ -42,6 +43,12 @@ ion-header.header-collapse-condense ion-toolbar:last-of-type { } ``` +#### Popover + +Converted `ion-popover` to use [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). + +If you were targeting the internals of `ion-popover` in your CSS, you will need to target the `backdrop`, `arrow`, or `content` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead. + #### Tab Bar The default iOS tab bar background color has been updated to better reflect the latest iOS styles. The new default value is: diff --git a/core/api.txt b/core/api.txt index f2f45d1806a..6548aaf5a11 100644 --- a/core/api.txt +++ b/core/api.txt @@ -823,7 +823,7 @@ ion-picker,css-prop,--min-height ion-picker,css-prop,--min-width ion-picker,css-prop,--width -ion-popover,scoped +ion-popover,shadow ion-popover,prop,animated,boolean,true,false,false ion-popover,prop,backdropDismiss,boolean,true,false,false ion-popover,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false @@ -858,6 +858,9 @@ ion-popover,css-prop,--max-width ion-popover,css-prop,--min-height ion-popover,css-prop,--min-width ion-popover,css-prop,--width +ion-popover,part,arrow +ion-popover,part,backdrop +ion-popover,part,content ion-progress-bar,shadow ion-progress-bar,prop,buffer,number,1,false,false diff --git a/core/src/components/popover/animations/ios.enter.ts b/core/src/components/popover/animations/ios.enter.ts index 0c3655ef32c..17dc922659c 100644 --- a/core/src/components/popover/animations/ios.enter.ts +++ b/core/src/components/popover/animations/ios.enter.ts @@ -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]); diff --git a/core/src/components/popover/animations/ios.leave.ts b/core/src/components/popover/animations/ios.leave.ts index a83bfddff1d..2d1553f3155 100644 --- a/core/src/components/popover/animations/ios.leave.ts +++ b/core/src/components/popover/animations/ios.leave.ts @@ -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]); diff --git a/core/src/components/popover/animations/md.enter.ts b/core/src/components/popover/animations/md.enter.ts index 30cc2dbdce8..63d8624b82a 100644 --- a/core/src/components/popover/animations/md.enter.ts +++ b/core/src/components/popover/animations/md.enter.ts @@ -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]); diff --git a/core/src/components/popover/animations/md.leave.ts b/core/src/components/popover/animations/md.leave.ts index 8200b68a302..350940081e6 100644 --- a/core/src/components/popover/animations/md.leave.ts +++ b/core/src/components/popover/animations/md.leave.ts @@ -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]); diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 1166df062dd..55a93d49f7e 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -16,9 +16,8 @@ import { mdLeaveAnimation } from './animations/md.leave'; const CoreDelegate = () => { let Cmp: any; const attachViewToDom = (parentElement: HTMLElement) => { - Cmp = parentElement.closest('ion-popover'); + Cmp = (parentElement.getRootNode() as ShadowRoot).host; const app = document.querySelector('ion-app') || document.body; - if (app && Cmp) { app.appendChild(Cmp); } @@ -38,6 +37,12 @@ const CoreDelegate = () => { /** * @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', @@ -45,7 +50,7 @@ const CoreDelegate = () => { ios: 'popover.ios.scss', md: 'popover.md.scss' }, - scoped: true + shadow: true }) export class Popover implements ComponentInterface, OverlayInterface { @@ -233,10 +238,6 @@ export class Popover implements ComponentInterface, OverlayInterface { await this.currentTransition; } - const container = this.el.querySelector('.popover-content'); - if (!container) { - throw new Error('container is undefined'); - } const data = { ...this.componentProps, popover: this.el @@ -249,7 +250,7 @@ export class Popover implements ComponentInterface, OverlayInterface { */ const delegate = (this.inline) ? this.delegate || this.coreDelegate : this.delegate; - this.usersElement = await attachComponent(delegate, container, this.component, ['popover-viewport', (this.el as any)['s-sc']], data, this.inline); + this.usersElement = await attachComponent(delegate, this.el, this.component, ['popover-viewport'], data, this.inline); await deepReady(this.usersElement); this.currentTransition = present(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, this.event); @@ -356,13 +357,13 @@ export class Popover implements ComponentInterface, OverlayInterface { onIonDismiss={this.onDismiss} onIonBackdropTap={this.onBackdropTap} > - +
-
-
+
+
diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index 2a71533c59c..2249d30ebe4 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -477,6 +477,22 @@ Type: `Promise` +## 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 | diff --git a/core/src/utils/framework-delegate.ts b/core/src/utils/framework-delegate.ts index 11f358e5baf..f93efab7234 100644 --- a/core/src/utils/framework-delegate.ts +++ b/core/src/utils/framework-delegate.ts @@ -29,6 +29,7 @@ export const attachComponent = async ( } container.appendChild(el); + await new Promise(resolve => componentOnReady(el, resolve)); return el; From 3677e742bcb2aeed5173add13c3cef34cff03618 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Apr 2021 16:14:38 +0000 Subject: [PATCH 12/22] grab correct element for delegate --- core/src/components/popover/popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 55a93d49f7e..a18267fd08b 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -16,7 +16,7 @@ import { mdLeaveAnimation } from './animations/md.leave'; const CoreDelegate = () => { let Cmp: any; const attachViewToDom = (parentElement: HTMLElement) => { - Cmp = (parentElement.getRootNode() as ShadowRoot).host; + Cmp = parentElement; const app = document.querySelector('ion-app') || document.body; if (app && Cmp) { app.appendChild(Cmp); From 6651c479a1e163830824cd5dd76dc0b8ad3ad591 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Apr 2021 17:28:53 +0000 Subject: [PATCH 13/22] add popover for ionic angular --- .../src/directives/overlays/ion-popover.ts | 37 +++++++++++++++++++ angular/src/directives/proxies-list.txt | 1 - angular/src/directives/proxies.ts | 20 ---------- angular/src/ionic-module.ts | 3 +- core/stencil.config.ts | 1 + 5 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 angular/src/directives/overlays/ion-popover.ts diff --git a/angular/src/directives/overlays/ion-popover.ts b/angular/src/directives/overlays/ion-popover.ts new file mode 100644 index 00000000000..9c250eaaf33 --- /dev/null +++ b/angular/src/directives/overlays/ion-popover.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, NgZone, TemplateRef } from "@angular/core"; +import { ProxyCmp, proxyOutputs } from "../proxies-utils"; +import { Components } from "@ionic/core"; +export declare interface IonPopover extends Components.IonPopover { +} +@ProxyCmp({ inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"], "methods": ["present", "dismiss", "onDidDismiss", "onWillDismiss"] }) +@Component({ selector: "ion-popover", changeDetection: ChangeDetectionStrategy.OnPush, template: ``, inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"] }) +export class IonPopover { + @ContentChild(TemplateRef, { static: false }) template: TemplateRef; + + ionPopoverDidPresent!: EventEmitter; + ionPopoverWillPresent!: EventEmitter; + ionPopoverWillDismiss!: EventEmitter; + ionPopoverDidDismiss!: EventEmitter; + didPresent!: EventEmitter; + willPresent!: EventEmitter; + willDismiss!: EventEmitter; + didDismiss!: EventEmitter; + isCmpOpen: boolean = false; + + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + + this.el.addEventListener('willPresent', () => { + this.isCmpOpen = true; + c.detectChanges(); + }); + this.el.addEventListener('didDismiss', () => { + this.isCmpOpen = false; + c.detectChanges(); + }); + + proxyOutputs(this, this.el, ["ionPopoverDidPresent", "ionPopoverWillPresent", "ionPopoverWillDismiss", "ionPopoverDidDismiss", "didPresent", "willPresent", "willDismiss", "didDismiss"]); + } +} diff --git a/angular/src/directives/proxies-list.txt b/angular/src/directives/proxies-list.txt index 12a6fc4497f..1d0a9e4bf2e 100644 --- a/angular/src/directives/proxies-list.txt +++ b/angular/src/directives/proxies-list.txt @@ -47,7 +47,6 @@ export const DIRECTIVES = [ d.IonNav, d.IonNavLink, d.IonNote, - d.IonPopover, d.IonProgressBar, d.IonRadio, d.IonRadioGroup, diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 05220934825..de592ce60bf 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -542,26 +542,6 @@ export class IonNote { this.el = r.nativeElement; } } -export declare interface IonPopover extends Components.IonPopover { -} -@ProxyCmp({ inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"], "methods": ["present", "dismiss", "onDidDismiss", "onWillDismiss"] }) -@Component({ selector: "ion-popover", changeDetection: ChangeDetectionStrategy.OnPush, template: "", inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"] }) -export class IonPopover { - ionPopoverDidPresent!: EventEmitter; - ionPopoverWillPresent!: EventEmitter; - ionPopoverWillDismiss!: EventEmitter; - ionPopoverDidDismiss!: EventEmitter; - didPresent!: EventEmitter; - willPresent!: EventEmitter; - willDismiss!: EventEmitter; - didDismiss!: EventEmitter; - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ["ionPopoverDidPresent", "ionPopoverWillPresent", "ionPopoverWillDismiss", "ionPopoverDidDismiss", "didPresent", "willPresent", "willDismiss", "didDismiss"]); - } -} export declare interface IonProgressBar extends Components.IonProgressBar { } @ProxyCmp({ inputs: ["buffer", "color", "mode", "reversed", "type", "value"] }) diff --git a/angular/src/ionic-module.ts b/angular/src/ionic-module.ts index 66a5f52342d..f60aadf2723 100644 --- a/angular/src/ionic-module.ts +++ b/angular/src/ionic-module.ts @@ -13,7 +13,8 @@ import { IonRouterOutlet } from './directives/navigation/ion-router-outlet'; import { IonTabs } from './directives/navigation/ion-tabs'; import { NavDelegate } from './directives/navigation/nav-delegate'; import { RouterLinkDelegate } from './directives/navigation/router-link-delegate'; -import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonPopover, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies'; +import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies'; +import { IonPopover } from './directives/overlays/ion-popover'; import { VirtualFooter } from './directives/virtual-scroll/virtual-footer'; import { VirtualHeader } from './directives/virtual-scroll/virtual-header'; import { VirtualItem } from './directives/virtual-scroll/virtual-item'; diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 17f2d568139..71d7601ffb3 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -155,6 +155,7 @@ export const config: Config = { 'ion-loading', 'ion-modal', 'ion-picker', + 'ion-popover', 'ion-toast', 'ion-toast', From dcf4fe48a1bf172cac64e6bf769d4c2fb08edcca Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Apr 2021 17:36:27 +0000 Subject: [PATCH 14/22] fix vue tests --- .../vue/test-app/tests/e2e/specs/overlays.js | 86 ++++++++++++++----- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/packages/vue/test-app/tests/e2e/specs/overlays.js b/packages/vue/test-app/tests/e2e/specs/overlays.js index 345381759b6..d60a4f10cc0 100644 --- a/packages/vue/test-app/tests/e2e/specs/overlays.js +++ b/packages/vue/test-app/tests/e2e/specs/overlays.js @@ -1,25 +1,61 @@ +const testController = (overlay, shadow = false) => { + const selector = `.${overlay}-controller`; + cy.get(`ion-radio#${overlay}`).click(); + cy.get('ion-radio#controller').click(); + + cy.get('ion-button#present-overlay').click(); + cy.get(selector).should('exist').should('be.visible'); + + if (shadow) { + cy.get(selector).shadow().find('ion-backdrop').click({ force: true }); + } else { + cy.get(`${selector} ion-backdrop`).click({ force: true }); + } + + cy.get(selector).should('not.exist'); +} + +const testComponent = (overlay, shadow = false) => { + cy.get(`ion-radio#${overlay}`).click(); + cy.get('ion-radio#component').click(); + + cy.get('ion-button#present-overlay').click(); + cy.get(overlay).should('exist').should('be.visible'); + + if (shadow) { + cy.get(overlay).shadow().find('ion-backdrop').click({ force: true }); + } else { + cy.get(`${overlay} ion-backdrop`).click({ force: true }); + } + + cy.get(overlay).should('not.exist'); +} + describe('Overlays', () => { beforeEach(() => { cy.viewport(1000, 900); cy.visit('http://localhost:8080/overlays') }) - const overlays = ['ion-alert', 'ion-action-sheet', 'ion-loading', 'ion-modal', 'ion-popover']; + it(`should open and close ion-alert via controller`, () => { + testController('ion-alert'); + }); - for (let overlay of overlays) { - it(`should open and close ${overlay} via controller`, () => { - const selector = `.${overlay}-controller`; - cy.get(`ion-radio#${overlay}`).click(); - cy.get('ion-radio#controller').click(); + it(`should open and close ion-action-sheet via controller`, () => { + testController('ion-action-sheet'); + }); - cy.get('ion-button#present-overlay').click(); - cy.get(selector).should('exist').should('be.visible'); + it(`should open and close ion-loading via controller`, () => { + testController('ion-loading'); + }); - cy.get(`${selector} ion-backdrop`).click({ force: true }); + it(`should open and close ion-modal via controller`, () => { + testController('ion-modal'); + }); - cy.get(selector).should('not.exist'); - }); - } + it(`should open and close ion-popover via controller`, () => { + testController('ion-popover', true); + }); it(`should open and close ion-toast via controller`, () => { cy.get(`ion-radio#ion-toast`).click(); @@ -33,19 +69,25 @@ describe('Overlays', () => { cy.get('ion-toast').should('not.exist'); }); - for (let overlay of overlays) { - it(`should open and close ${overlay} via component`, () => { - cy.get(`ion-radio#${overlay}`).click(); - cy.get('ion-radio#component').click(); + it(`should open and close ion-alert via component`, () => { + testComponent('ion-alert'); + }); + + it(`should open and close ion-action-sheet via component`, () => { + testComponent('ion-action-sheet'); + }); - cy.get('ion-button#present-overlay').click(); - cy.get(overlay).should('exist').should('be.visible'); + it(`should open and close ion-loading via component`, () => { + testComponent('ion-loading'); + }); - cy.get(`${overlay} ion-backdrop`).click({ force: true }); + it(`should open and close ion-modal via component`, () => { + testComponent('ion-modal'); + }); - cy.get(overlay).should('not.exist'); - }); - } + it(`should open and close ion-popover via component`, () => { + testComponent('ion-popover', true); + }); it(`should open and close ion-toast via component`, () => { cy.get(`ion-radio#ion-toast`).click(); From a6fdba750b5ee24c0bd8484a56c90e2669c21b76 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Apr 2021 21:46:52 +0000 Subject: [PATCH 15/22] fix focus trap with shadow dom --- core/src/components/popover/popover.tsx | 4 - core/src/utils/overlays.ts | 123 ++++++++++++++++++------ 2 files changed, 92 insertions(+), 35 deletions(-) diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index a18267fd08b..db9c9099329 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -359,16 +359,12 @@ export class Popover implements ComponentInterface, OverlayInterface { > -
-
- -
); } diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 08361954d72..f8c95843a62 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -117,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; + + /** + * 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 overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper'); + const overlayRoot = getElementRoot(lastOverlay); + if (!overlayRoot.contains(target)) { return; } - if (!overlayWrapper) { 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. */ @@ -172,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 @@ -184,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) => { @@ -331,7 +392,7 @@ export const dismiss = async ( const mode = getIonMode(overlay); const animationBuilder = (overlay.leaveAnimation) - ? overlay.leaveAnimation + ?overlay.leaveAnimation : config.get(name, mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation); // If dismissed via gesture, no need to play leaving animation again @@ -374,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(); } }); From 7667f4e57e64fe3ba9a948011bf3dddd7d75ab2d Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Apr 2021 21:47:00 +0000 Subject: [PATCH 16/22] lint --- core/src/utils/overlays.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index f8c95843a62..73f0ecdc323 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -392,7 +392,7 @@ export const dismiss = async ( const mode = getIonMode(overlay); const animationBuilder = (overlay.leaveAnimation) - ?overlay.leaveAnimation + ? overlay.leaveAnimation : config.get(name, mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation); // If dismissed via gesture, no need to play leaving animation again From ce71633a127c2e3dcf723b67a4035113c80e0bc8 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 29 Apr 2021 09:02:15 -0400 Subject: [PATCH 17/22] lint angular --- angular/src/directives/overlays/ion-popover.ts | 2 ++ angular/src/ionic-module.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/angular/src/directives/overlays/ion-popover.ts b/angular/src/directives/overlays/ion-popover.ts index 9c250eaaf33..801536573fb 100644 --- a/angular/src/directives/overlays/ion-popover.ts +++ b/angular/src/directives/overlays/ion-popover.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ +/* tslint:disable */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, NgZone, TemplateRef } from "@angular/core"; import { ProxyCmp, proxyOutputs } from "../proxies-utils"; import { Components } from "@ionic/core"; diff --git a/angular/src/ionic-module.ts b/angular/src/ionic-module.ts index f60aadf2723..1ac600a3bf8 100644 --- a/angular/src/ionic-module.ts +++ b/angular/src/ionic-module.ts @@ -13,8 +13,8 @@ import { IonRouterOutlet } from './directives/navigation/ion-router-outlet'; import { IonTabs } from './directives/navigation/ion-tabs'; import { NavDelegate } from './directives/navigation/nav-delegate'; import { RouterLinkDelegate } from './directives/navigation/router-link-delegate'; -import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies'; import { IonPopover } from './directives/overlays/ion-popover'; +import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies'; import { VirtualFooter } from './directives/virtual-scroll/virtual-footer'; import { VirtualHeader } from './directives/virtual-scroll/virtual-header'; import { VirtualItem } from './directives/virtual-scroll/virtual-item'; From ede758c1b51a08499ed2387d58cf4a03fd71391d Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 29 Apr 2021 09:40:56 -0400 Subject: [PATCH 18/22] update docs --- .../src/directives/overlays/ion-popover.ts | 2 +- core/api.txt | 3 -- core/src/components/popover/popover.tsx | 3 ++ core/src/components/popover/readme.md | 41 ++++++++++++------- .../components/popover/test/inline/index.html | 22 ++++++++-- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/angular/src/directives/overlays/ion-popover.ts b/angular/src/directives/overlays/ion-popover.ts index 801536573fb..c5add56ec73 100644 --- a/angular/src/directives/overlays/ion-popover.ts +++ b/angular/src/directives/overlays/ion-popover.ts @@ -5,7 +5,7 @@ import { ProxyCmp, proxyOutputs } from "../proxies-utils"; import { Components } from "@ionic/core"; export declare interface IonPopover extends Components.IonPopover { } -@ProxyCmp({ inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"], "methods": ["present", "dismiss", "onDidDismiss", "onWillDismiss"] }) +@ProxyCmp({ inputs: ["animated", "backdropDismiss", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"], "methods": ["present", "dismiss", "onDidDismiss", "onWillDismiss"] }) @Component({ selector: "ion-popover", changeDetection: ChangeDetectionStrategy.OnPush, template: ``, inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"] }) export class IonPopover { @ContentChild(TemplateRef, { static: false }) template: TemplateRef; diff --git a/core/api.txt b/core/api.txt index 6548aaf5a11..ea21259eea1 100644 --- a/core/api.txt +++ b/core/api.txt @@ -826,9 +826,6 @@ ion-picker,css-prop,--width ion-popover,shadow ion-popover,prop,animated,boolean,true,false,false ion-popover,prop,backdropDismiss,boolean,true,false,false -ion-popover,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false -ion-popover,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false -ion-popover,prop,cssClass,string | string[] | undefined,undefined,false,false ion-popover,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false ion-popover,prop,event,any,undefined,false,false ion-popover,prop,isOpen,boolean,false,false,false diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index db9c9099329..dac3cda8a74 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -87,11 +87,13 @@ export class Popover implements ComponentInterface, OverlayInterface { /** * The component to display inside of the popover. + * @internal */ @Prop() component?: ComponentRef; /** * The data to pass to the popover component. + * @internal */ @Prop() componentProps?: ComponentProps; @@ -103,6 +105,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[]; diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index 2249d30ebe4..04b3e463465 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -8,6 +8,20 @@ There are two ways to use `ion-popover`: inline or via the `popoverController`. `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. As this functionality is provided by the JavaScript framework, using `ion-popover` without a JavaScript framework will not destroy the component you passed in. If this is a needed functionality, we recommend using the `popoverController` instead. + +### 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 `` internally. Instead, we use `` which expects an `` to be passed in. As a result, when passing in your component you will need to wrap it in an ``: + +```html + + + + + +``` + Liam: Usage will be filled out via desktop popover PR. ### When to use @@ -403,21 +417,18 @@ 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` | `component` | The component to display inside of the popover. | `Function \| HTMLElement \| null \| string \| undefined` | `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` | -| `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` | +| 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` | +| `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 diff --git a/core/src/components/popover/test/inline/index.html b/core/src/components/popover/test/inline/index.html index 29b28290ce0..7ac0423ca8c 100644 --- a/core/src/components/popover/test/inline/index.html +++ b/core/src/components/popover/test/inline/index.html @@ -20,10 +20,8 @@ Open Popover - - - This is my inline popover content! - + + @@ -39,6 +37,22 @@ popover.isOpen = false; popover.event = undefined; }); + + class TranslucentPage extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.innerHTML = ` +
+

Translucent Popover

+
+ `; + } + } + + customElements.define('translucent-page', TranslucentPage); From f7a76eece8a28df9fbfe211c6877ffb003136feb Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 29 Apr 2021 13:02:34 -0400 Subject: [PATCH 19/22] fix test --- .../components/popover/test/inline/index.html | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/core/src/components/popover/test/inline/index.html b/core/src/components/popover/test/inline/index.html index 7ac0423ca8c..29b28290ce0 100644 --- a/core/src/components/popover/test/inline/index.html +++ b/core/src/components/popover/test/inline/index.html @@ -20,8 +20,10 @@ Open Popover - - + + + This is my inline popover content! + @@ -37,22 +39,6 @@ popover.isOpen = false; popover.event = undefined; }); - - class TranslucentPage extends HTMLElement { - constructor() { - super(); - } - - connectedCallback() { - this.innerHTML = ` -
-

Translucent Popover

-
- `; - } - } - - customElements.define('translucent-page', TranslucentPage); From ab262a9df1d2cbfb674c9ba9a1648f67a9d1af7a Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 29 Apr 2021 14:27:10 -0400 Subject: [PATCH 20/22] improve overlay animations with shadow dom --- core/src/components/toast/animations/ios.enter.ts | 10 +++++----- core/src/components/toast/animations/ios.leave.ts | 8 ++++---- core/src/components/toast/animations/md.enter.ts | 10 +++++----- core/src/components/toast/animations/md.leave.ts | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/core/src/components/toast/animations/ios.enter.ts b/core/src/components/toast/animations/ios.enter.ts index 5af7d850f9a..7ee45b65120 100644 --- a/core/src/components/toast/animations/ios.enter.ts +++ b/core/src/components/toast/animations/ios.enter.ts @@ -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); diff --git a/core/src/components/toast/animations/ios.leave.ts b/core/src/components/toast/animations/ios.leave.ts index 38a4d0a1992..8694bd83456 100644 --- a/core/src/components/toast/animations/ios.leave.ts +++ b/core/src/components/toast/animations/ios.leave.ts @@ -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); diff --git a/core/src/components/toast/animations/md.enter.ts b/core/src/components/toast/animations/md.enter.ts index d2b696045d4..8e7e00a3ea7 100644 --- a/core/src/components/toast/animations/md.enter.ts +++ b/core/src/components/toast/animations/md.enter.ts @@ -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); diff --git a/core/src/components/toast/animations/md.leave.ts b/core/src/components/toast/animations/md.leave.ts index c3a1558f652..8e261fc5eec 100644 --- a/core/src/components/toast/animations/md.leave.ts +++ b/core/src/components/toast/animations/md.leave.ts @@ -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); From 94645b79d9e630b5a8e0870e992a1b6c99d87e65 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 30 Apr 2021 10:05:29 -0400 Subject: [PATCH 21/22] do not pass in shadow root --- core/src/utils/overlays.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 73f0ecdc323..54c4ea0e47f 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -425,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)) { From 7fffca884b4897108a068f028bb5a9d87258735a Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 30 Apr 2021 16:06:00 -0400 Subject: [PATCH 22/22] update docs --- core/api.txt | 2 ++ core/src/components.d.ts | 8 +++---- core/src/components/popover/popover.tsx | 8 +++++-- core/src/components/popover/readme.md | 28 +++++++++++++------------ 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/core/api.txt b/core/api.txt index ea21259eea1..de0aab9d541 100644 --- a/core/api.txt +++ b/core/api.txt @@ -826,6 +826,8 @@ ion-picker,css-prop,--width ion-popover,shadow ion-popover,prop,animated,boolean,true,false,false ion-popover,prop,backdropDismiss,boolean,true,false,false +ion-popover,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false +ion-popover,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false ion-popover,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false ion-popover,prop,event,any,undefined,false,false ion-popover,prop,isOpen,boolean,false,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ef2e88fa1b5..5e3d35e3045 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -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; /** - * 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; /** @@ -4995,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; /** - * 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; /** diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index dac3cda8a74..c645a71d394 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -87,13 +87,17 @@ export class Popover implements ComponentInterface, OverlayInterface { /** * The component to display inside of the popover. - * @internal + * 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. - * @internal + * 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; diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index 04b3e463465..45acd59e5ed 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -8,7 +8,7 @@ There are two ways to use `ion-popover`: inline or via the `popoverController`. `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. As this functionality is provided by the JavaScript framework, using `ion-popover` without a JavaScript framework will not destroy the component you passed in. If this is a needed functionality, we recommend using the `popoverController` instead. +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 @@ -417,18 +417,20 @@ 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` | -| `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` | +| 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