fix(overlays): assign incremental id to overlay host (#27278)

Issue number: Internal

---------

<!-- Please refer to our contributing documentation for any questions on
submitting a pull request, or let us know here if you need any help:
https://ionicframework.com/docs/building/contributing -->

<!-- Some docs updates need to be made in the `ionic-docs` repo, in a
separate PR. See
https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#modifying-documentation
for details. -->

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

The counter for incrementing the `id` and `z-index` of an overlay is
incremented whenever the `connectedCallback` is fired for an overlay.

When an overlay is presented and/or conditionally rendered, the overlay
`id` can increment by `n+2` instead of `n+1`.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Increments all overlay ids consistently
- Removes legacy `ion-modal-{id}` and `ion-popover-{id}` logic
- Adds unit tests for the id behavior
- Tests are split up into separate files so that the counter is always
starting from `0`
- Adds an integration test with the Angular test app to verify
conditional rendering behavior

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
This commit is contained in:
Sean Perkins
2023-05-03 13:24:19 -04:00
committed by GitHub
parent 27a9aaaedc
commit 9313a914b7
19 changed files with 365 additions and 38 deletions

View File

@ -121,4 +121,4 @@ describe('when in a modal', () => {
cy.get('#set-to-null').click(); cy.get('#set-to-null').click();
cy.get('#inputWithFloatingLabel').should('not.have.class', 'item-has-value'); cy.get('#inputWithFloatingLabel').should('not.have.class', 'item-has-value');
}); });
}); });

View File

@ -17,4 +17,4 @@
</ion-list> </ion-list>
</ion-content> </ion-content>
</ng-template> </ng-template>
</ion-modal> </ion-modal>

View File

@ -24,4 +24,5 @@ export class ModalInlineComponent implements AfterViewInit {
onBreakpointDidChange() { onBreakpointDidChange() {
this.breakpointDidChangeCounter++; this.breakpointDidChangeCounter++;
} }
} }

View File

@ -15,6 +15,7 @@ import {
prepareOverlay, prepareOverlay,
present, present,
safeCall, safeCall,
setOverlayId,
} from '../../utils/overlays'; } from '../../utils/overlays';
import type { OverlayEventDetail } from '../../utils/overlays-interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface';
import { getClassMap } from '../../utils/theme'; import { getClassMap } from '../../utils/theme';
@ -311,6 +312,10 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
this.triggerController.removeClickListener(); this.triggerController.removeClickListener();
} }
componentWillLoad() {
setOverlayId(this.el);
}
componentDidLoad() { componentDidLoad() {
/** /**
* Do not create gesture if: * Do not create gesture if:

View File

@ -0,0 +1,41 @@
import { newSpecPage } from '@stencil/core/testing';
import { ActionSheet } from '../action-sheet';
it('action sheet should be assigned an incrementing id', async () => {
const page = await newSpecPage({
components: [ActionSheet],
html: `<ion-action-sheet is-open="true"></ion-action-sheet>`,
});
let actionSheet: HTMLIonActionSheetElement;
actionSheet = page.body.querySelector('ion-action-sheet')!;
expect(actionSheet).not.toBe(null);
expect(actionSheet.getAttribute('id')).toBe('ion-overlay-1');
// Remove the action sheet from the DOM
actionSheet.remove();
await page.waitForChanges();
// Create a new action sheet to verify the id is incremented
actionSheet = document.createElement('ion-action-sheet');
actionSheet.isOpen = true;
page.body.appendChild(actionSheet);
await page.waitForChanges();
actionSheet = page.body.querySelector('ion-action-sheet')!;
expect(actionSheet.getAttribute('id')).toBe('ion-overlay-2');
// Presenting the same action sheet again should reuse the existing id
actionSheet.isOpen = false;
await page.waitForChanges();
actionSheet.isOpen = true;
await page.waitForChanges();
actionSheet = page.body.querySelector('ion-action-sheet')!;
expect(actionSheet.getAttribute('id')).toBe('ion-overlay-2');
});

View File

@ -17,6 +17,7 @@ import {
prepareOverlay, prepareOverlay,
present, present,
safeCall, safeCall,
setOverlayId,
} from '../../utils/overlays'; } from '../../utils/overlays';
import type { OverlayEventDetail } from '../../utils/overlays-interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface';
import type { IonicSafeString } from '../../utils/sanitization'; import type { IonicSafeString } from '../../utils/sanitization';
@ -329,6 +330,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
} }
componentWillLoad() { componentWillLoad() {
setOverlayId(this.el);
this.inputsChanged(); this.inputsChanged();
this.buttonsChanged(); this.buttonsChanged();
} }

View File

@ -0,0 +1,41 @@
import { newSpecPage } from '@stencil/core/testing';
import { Alert } from '../alert';
it('alert should be assigned an incrementing id', async () => {
const page = await newSpecPage({
components: [Alert],
html: `<ion-alert is-open="true"></ion-alert>`,
});
let alert: HTMLIonAlertElement;
alert = page.body.querySelector('ion-alert')!;
expect(alert).not.toBe(null);
expect(alert.getAttribute('id')).toBe('ion-overlay-1');
// Remove the alert from the DOM
alert.remove();
await page.waitForChanges();
// Create a new alert to verify the id is incremented
alert = document.createElement('ion-alert');
alert.isOpen = true;
page.body.appendChild(alert);
await page.waitForChanges();
alert = page.body.querySelector('ion-alert')!;
expect(alert.getAttribute('id')).toBe('ion-overlay-2');
// Presenting the same alert again should reuse the existing id
alert.isOpen = false;
await page.waitForChanges();
alert.isOpen = true;
await page.waitForChanges();
alert = page.body.querySelector('ion-alert')!;
expect(alert.getAttribute('id')).toBe('ion-overlay-2');
});

View File

@ -14,6 +14,7 @@ import {
present, present,
createDelegateController, createDelegateController,
createTriggerController, createTriggerController,
setOverlayId,
} from '../../utils/overlays'; } from '../../utils/overlays';
import type { OverlayEventDetail } from '../../utils/overlays-interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface';
import type { IonicSafeString } from '../../utils/sanitization'; import type { IonicSafeString } from '../../utils/sanitization';
@ -212,6 +213,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
const mode = getIonMode(this); const mode = getIonMode(this);
this.spinner = config.get('loadingSpinner', config.get('spinner', mode === 'ios' ? 'lines' : 'crescent')); this.spinner = config.get('loadingSpinner', config.get('spinner', mode === 'ios' ? 'lines' : 'crescent'));
} }
setOverlayId(this.el);
} }
componentDidLoad() { componentDidLoad() {

View File

@ -0,0 +1,41 @@
import { newSpecPage } from '@stencil/core/testing';
import { Loading } from '../loading';
it('loading should be assigned an incrementing id', async () => {
const page = await newSpecPage({
components: [Loading],
html: `<ion-loading is-open="true"></ion-loading>`,
});
let loading: HTMLIonLoadingElement;
loading = page.body.querySelector('ion-loading')!;
expect(loading).not.toBe(null);
expect(loading.getAttribute('id')).toBe('ion-overlay-1');
// Remove the loading from the DOM
loading.remove();
await page.waitForChanges();
// Create a new loading to verify the id is incremented
loading = document.createElement('ion-loading');
loading.isOpen = true;
page.body.appendChild(loading);
await page.waitForChanges();
loading = page.body.querySelector('ion-loading')!;
expect(loading.getAttribute('id')).toBe('ion-overlay-2');
// Presenting the same loading again should reuse the existing id
loading.isOpen = false;
await page.waitForChanges();
loading.isOpen = true;
await page.waitForChanges();
loading = page.body.querySelector('ion-loading')!;
expect(loading.getAttribute('id')).toBe('ion-overlay-2');
});

View File

@ -1,15 +1,16 @@
import { newSpecPage } from '@stencil/core/testing'; import { newSpecPage } from '@stencil/core/testing';
import { Loading } from '../loading';
import { config } from '../../../global/config';
describe('alert: custom html', () => { import { config } from '../../../global/config';
import { Loading } from '../loading';
describe('loading: custom html', () => {
it('should not allow for custom html by default', async () => { it('should not allow for custom html by default', async () => {
const page = await newSpecPage({ const page = await newSpecPage({
components: [Loading], components: [Loading],
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`, html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
}); });
const content = page.body.querySelector('.loading-content'); const content = page.body.querySelector('.loading-content')!;
expect(content.textContent).toContain('Custom Text'); expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null); expect(content.querySelector('button.custom-html')).toBe(null);
}); });
@ -21,7 +22,7 @@ describe('alert: custom html', () => {
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`, html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
}); });
const content = page.body.querySelector('.loading-content'); const content = page.body.querySelector('.loading-content')!;
expect(content.textContent).toContain('Custom Text'); expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null); expect(content.querySelector('button.custom-html')).not.toBe(null);
}); });
@ -33,7 +34,7 @@ describe('alert: custom html', () => {
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`, html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
}); });
const content = page.body.querySelector('.loading-content'); const content = page.body.querySelector('.loading-content')!;
expect(content.textContent).toContain('Custom Text'); expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null); expect(content.querySelector('button.custom-html')).toBe(null);
}); });

View File

@ -28,6 +28,7 @@ import {
prepareOverlay, prepareOverlay,
present, present,
createTriggerController, createTriggerController,
setOverlayId,
} from '../../utils/overlays'; } from '../../utils/overlays';
import type { OverlayEventDetail } from '../../utils/overlays-interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface';
import { getClassMap } from '../../utils/theme'; import { getClassMap } from '../../utils/theme';
@ -65,8 +66,6 @@ import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
export class Modal implements ComponentInterface, OverlayInterface { export class Modal implements ComponentInterface, OverlayInterface {
private readonly triggerController = createTriggerController(); private readonly triggerController = createTriggerController();
private gesture?: Gesture; private gesture?: Gesture;
private modalIndex = modalIds++;
private modalId?: string;
private coreDelegate: FrameworkDelegate = CoreDelegate(); private coreDelegate: FrameworkDelegate = CoreDelegate();
private currentTransition?: Promise<any>; private currentTransition?: Promise<any>;
private sheetTransition?: Promise<any>; private sheetTransition?: Promise<any>;
@ -344,16 +343,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
componentWillLoad() { componentWillLoad() {
const { breakpoints, initialBreakpoint, el } = this; const { breakpoints, initialBreakpoint, el } = this;
const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
this.inheritedAttributes = inheritAttributes(el, ['aria-label', 'role']); this.inheritedAttributes = inheritAttributes(el, ['aria-label', 'role']);
/**
* If user has custom ID set then we should
* not assign the default incrementing ID.
*/
this.modalId = this.el.hasAttribute('id') ? this.el.getAttribute('id')! : `ion-modal-${this.modalIndex}`;
const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
if (isSheetModal) { if (isSheetModal) {
this.currentBreakpoint = this.initialBreakpoint; this.currentBreakpoint = this.initialBreakpoint;
} }
@ -361,6 +354,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) { if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) {
printIonWarning('Your breakpoints array must include the initialBreakpoint value.'); printIonWarning('Your breakpoints array must include the initialBreakpoint value.');
} }
setOverlayId(el);
} }
componentDidLoad() { componentDidLoad() {
@ -861,7 +856,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
const showHandle = handle !== false && isSheetModal; const showHandle = handle !== false && isSheetModal;
const mode = getIonMode(this); const mode = getIonMode(this);
const { modalId } = this;
const isCardModal = presentingElement !== undefined && mode === 'ios'; const isCardModal = presentingElement !== undefined && mode === 'ios';
const isHandleCycle = handleBehavior === 'cycle'; const isHandleCycle = handleBehavior === 'cycle';
@ -881,7 +875,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
'overlay-hidden': true, 'overlay-hidden': true,
...getClassMap(this.cssClass), ...getClassMap(this.cssClass),
}} }}
id={modalId}
onIonBackdropTap={this.onBackdropTap} onIonBackdropTap={this.onBackdropTap}
onIonModalDidPresent={this.onLifecycle} onIonModalDidPresent={this.onLifecycle}
onIonModalWillPresent={this.onLifecycle} onIonModalWillPresent={this.onLifecycle}
@ -935,8 +928,6 @@ const LIFECYCLE_MAP: any = {
ionModalDidDismiss: 'ionViewDidLeave', ionModalDidDismiss: 'ionViewDidLeave',
}; };
let modalIds = 0;
interface ModalOverlayOptions { interface ModalOverlayOptions {
/** /**
* The element that presented the modal. * The element that presented the modal.

View File

@ -0,0 +1,41 @@
import { newSpecPage } from '@stencil/core/testing';
import { Modal } from '../modal';
it('modal should be assigned an incrementing id', async () => {
const page = await newSpecPage({
components: [Modal],
html: `<ion-modal is-open="true"></ion-modal>`,
});
let modal: HTMLIonModalElement;
modal = page.body.querySelector('ion-modal')!;
expect(modal).not.toBe(null);
expect(modal.getAttribute('id')).toBe('ion-overlay-1');
// Remove the modal from the DOM
modal.remove();
await page.waitForChanges();
// Create a new modal to verify the id is incremented
modal = document.createElement('ion-modal');
modal.isOpen = true;
page.body.appendChild(modal);
await page.waitForChanges();
modal = page.body.querySelector('ion-modal')!;
expect(modal.getAttribute('id')).toBe('ion-overlay-2');
// Presenting the same modal again should reuse the existing id
modal.isOpen = false;
await page.waitForChanges();
modal.isOpen = true;
await page.waitForChanges();
modal = page.body.querySelector('ion-modal')!;
expect(modal.getAttribute('id')).toBe('ion-overlay-2');
});

View File

@ -13,6 +13,7 @@ import {
prepareOverlay, prepareOverlay,
present, present,
safeCall, safeCall,
setOverlayId,
} from '../../utils/overlays'; } from '../../utils/overlays';
import type { OverlayEventDetail } from '../../utils/overlays-interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface';
import { getClassMap } from '../../utils/theme'; import { getClassMap } from '../../utils/theme';
@ -194,6 +195,10 @@ export class Picker implements ComponentInterface, OverlayInterface {
this.triggerController.removeClickListener(); this.triggerController.removeClickListener();
} }
componentWillLoad() {
setOverlayId(this.el);
}
/** /**
* Present the picker overlay after it has been created. * Present the picker overlay after it has been created.
*/ */

View File

@ -0,0 +1,41 @@
import { newSpecPage } from '@stencil/core/testing';
import { Picker } from '../picker';
it('picker should be assigned an incrementing id', async () => {
const page = await newSpecPage({
components: [Picker],
html: `<ion-picker is-open="true"></ion-picker>`,
});
let picker: HTMLIonPickerElement;
picker = page.body.querySelector('ion-picker')!;
expect(picker).not.toBe(null);
expect(picker.getAttribute('id')).toBe('ion-overlay-1');
// Remove the picker from the DOM
picker.remove();
await page.waitForChanges();
// Create a new picker to verify the id is incremented
picker = document.createElement('ion-picker');
picker.isOpen = true;
page.body.appendChild(picker);
await page.waitForChanges();
picker = page.body.querySelector('ion-picker')!;
expect(picker.getAttribute('id')).toBe('ion-overlay-2');
// Presenting the same picker again should reuse the existing id
picker.isOpen = false;
await page.waitForChanges();
picker.isOpen = true;
await page.waitForChanges();
picker = page.body.querySelector('ion-picker')!;
expect(picker.getAttribute('id')).toBe('ion-overlay-2');
});

View File

@ -6,7 +6,15 @@ import type { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate'; import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
import { addEventListener, raf, hasLazyBuild } from '../../utils/helpers'; import { addEventListener, raf, hasLazyBuild } from '../../utils/helpers';
import { printIonWarning } from '../../utils/logging'; import { printIonWarning } from '../../utils/logging';
import { BACKDROP, dismiss, eventMethod, focusFirstDescendant, prepareOverlay, present } from '../../utils/overlays'; import {
BACKDROP,
dismiss,
eventMethod,
focusFirstDescendant,
prepareOverlay,
present,
setOverlayId,
} from '../../utils/overlays';
import type { OverlayEventDetail } from '../../utils/overlays-interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface';
import { isPlatform } from '../../utils/platform'; import { isPlatform } from '../../utils/platform';
import { getClassMap } from '../../utils/theme'; import { getClassMap } from '../../utils/theme';
@ -49,8 +57,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
private usersElement?: HTMLElement; private usersElement?: HTMLElement;
private triggerEl?: HTMLElement | null; private triggerEl?: HTMLElement | null;
private parentPopover: HTMLIonPopoverElement | null = null; private parentPopover: HTMLIonPopoverElement | null = null;
private popoverIndex = popoverIds++;
private popoverId?: string;
private coreDelegate: FrameworkDelegate = CoreDelegate(); private coreDelegate: FrameworkDelegate = CoreDelegate();
private currentTransition?: Promise<any>; private currentTransition?: Promise<any>;
private destroyTriggerInteraction?: () => void; private destroyTriggerInteraction?: () => void;
@ -338,13 +344,10 @@ export class Popover implements ComponentInterface, PopoverInterface {
} }
componentWillLoad() { componentWillLoad() {
/** const { el } = this;
* If user has custom ID set then we should const popoverId = setOverlayId(el);
* not assign the default incrementing ID.
*/
this.popoverId = this.el.hasAttribute('id') ? this.el.getAttribute('id')! : `ion-popover-${this.popoverIndex}`;
this.parentPopover = this.el.closest(`ion-popover:not(#${this.popoverId})`) as HTMLIonPopoverElement | null; this.parentPopover = el.closest(`ion-popover:not(#${popoverId})`) as HTMLIonPopoverElement | null;
if (this.alignment === undefined) { if (this.alignment === undefined) {
this.alignment = getIonMode(this) === 'ios' ? 'center' : 'start'; this.alignment = getIonMode(this) === 'ios' ? 'center' : 'start';
@ -660,7 +663,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
render() { render() {
const mode = getIonMode(this); const mode = getIonMode(this);
const { onLifecycle, popoverId, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this; const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this;
const desktop = isPlatform('desktop'); const desktop = isPlatform('desktop');
const enableArrow = arrow && !parentPopover; const enableArrow = arrow && !parentPopover;
@ -673,7 +676,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
style={{ style={{
zIndex: `${20000 + this.overlayIndex}`, zIndex: `${20000 + this.overlayIndex}`,
}} }}
id={popoverId}
class={{ class={{
...getClassMap(this.cssClass), ...getClassMap(this.cssClass),
[mode]: true, [mode]: true,
@ -709,8 +711,6 @@ const LIFECYCLE_MAP: any = {
ionPopoverDidDismiss: 'ionViewDidLeave', ionPopoverDidDismiss: 'ionViewDidLeave',
}; };
let popoverIds = 0;
interface PopoverPresentOptions { interface PopoverPresentOptions {
/** /**
* The original target event that presented the popover. * The original target event that presented the popover.

View File

@ -0,0 +1,41 @@
import { newSpecPage } from '@stencil/core/testing';
import { Popover } from '../popover';
it('popover should be assigned an incrementing id', async () => {
const page = await newSpecPage({
components: [Popover],
html: `<ion-popover is-open="true"></ion-popover>`,
});
let popover: HTMLIonPopoverElement;
popover = page.body.querySelector('ion-popover')!;
expect(popover).not.toBe(null);
expect(popover.getAttribute('id')).toBe('ion-overlay-1');
// Remove the popover from the DOM
popover.remove();
await page.waitForChanges();
// Create a new popover to verify the id is incremented
popover = document.createElement('ion-popover');
popover.isOpen = true;
page.body.appendChild(popover);
await page.waitForChanges();
popover = page.body.querySelector('ion-popover')!;
expect(popover.getAttribute('id')).toBe('ion-overlay-2');
// Presenting the same popover again should reuse the existing id
popover.isOpen = false;
await page.waitForChanges();
popover.isOpen = true;
await page.waitForChanges();
popover = page.body.querySelector('ion-popover')!;
expect(popover.getAttribute('id')).toBe('ion-overlay-2');
});

View File

@ -0,0 +1,41 @@
import { newSpecPage } from '@stencil/core/testing';
import { Toast } from '../toast';
it('toast should be assigned an incrementing id', async () => {
const page = await newSpecPage({
components: [Toast],
html: `<ion-toast is-open="true"></ion-toast>`,
});
let toast: HTMLIonToastElement;
toast = page.body.querySelector('ion-toast')!;
expect(toast).not.toBe(null);
expect(toast.getAttribute('id')).toBe('ion-overlay-1');
// Remove the toast from the DOM
toast.remove();
await page.waitForChanges();
// Create a new toast to verify the id is incremented
toast = document.createElement('ion-toast');
toast.isOpen = true;
page.body.appendChild(toast);
await page.waitForChanges();
toast = page.body.querySelector('ion-toast')!;
expect(toast.getAttribute('id')).toBe('ion-overlay-2');
// Presenting the same toast again should reuse the existing id
toast.isOpen = false;
await page.waitForChanges();
toast.isOpen = true;
await page.waitForChanges();
toast = page.body.querySelector('ion-toast')!;
expect(toast.getAttribute('id')).toBe('ion-overlay-2');
});

View File

@ -15,6 +15,7 @@ import {
prepareOverlay, prepareOverlay,
present, present,
safeCall, safeCall,
setOverlayId,
} from '../../utils/overlays'; } from '../../utils/overlays';
import type { OverlayEventDetail } from '../../utils/overlays-interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface';
import type { IonicSafeString } from '../../utils/sanitization'; import type { IonicSafeString } from '../../utils/sanitization';
@ -248,6 +249,10 @@ export class Toast implements ComponentInterface, OverlayInterface {
this.triggerController.removeClickListener(); this.triggerController.removeClickListener();
} }
componentWillLoad() {
setOverlayId(this.el);
}
/** /**
* Present the toast overlay after it has been created. * Present the toast overlay after it has been created.
*/ */

View File

@ -22,6 +22,7 @@ import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button';
import { addEventListener, componentOnReady, focusElement, getElementRoot, removeEventListener } from './helpers'; import { addEventListener, componentOnReady, focusElement, getElementRoot, removeEventListener } from './helpers';
import { printIonWarning } from './logging'; import { printIonWarning } from './logging';
let lastOverlayIndex = 0;
let lastId = 0; let lastId = 0;
export const activeAnimations = new WeakMap<OverlayInterface, Animation[]>(); export const activeAnimations = new WeakMap<OverlayInterface, Animation[]>();
@ -50,15 +51,42 @@ export const pickerController = /*@__PURE__*/ createController<PickerOptions, HT
export const popoverController = /*@__PURE__*/ createController<PopoverOptions, HTMLIonPopoverElement>('ion-popover'); export const popoverController = /*@__PURE__*/ createController<PopoverOptions, HTMLIonPopoverElement>('ion-popover');
export const toastController = /*@__PURE__*/ createController<ToastOptions, HTMLIonToastElement>('ion-toast'); export const toastController = /*@__PURE__*/ createController<ToastOptions, HTMLIonToastElement>('ion-toast');
/**
* Prepares the overlay element to be presented.
*/
export const prepareOverlay = <T extends HTMLIonOverlayElement>(el: T) => { export const prepareOverlay = <T extends HTMLIonOverlayElement>(el: T) => {
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
/**
* Adds a single instance of event listeners for application behaviors:
*
* - Escape Key behavior to dismiss an overlay
* - Trapping focus within an overlay
* - Back button behavior to dismiss an overlay
*
* This only occurs when the first overlay is created.
*/
connectListeners(document); connectListeners(document);
} }
const overlayIndex = lastId++; const overlayIndex = lastOverlayIndex++;
/**
* overlayIndex is used in the overlay components to set a zIndex.
* This ensures that the most recently presented overlay will be
* on top.
*/
el.overlayIndex = overlayIndex; el.overlayIndex = overlayIndex;
};
/**
* Assigns an incrementing id to an overlay element, that does not
* already have an id assigned to it.
*
* Used to track unique instances of an overlay element.
*/
export const setOverlayId = <T extends HTMLIonOverlayElement>(el: T) => {
if (!el.hasAttribute('id')) { if (!el.hasAttribute('id')) {
el.id = `ion-overlay-${overlayIndex}`; el.id = `ion-overlay-${++lastId}`;
} }
return el.id;
}; };
export const createOverlay = <T extends HTMLIonOverlayElement>( export const createOverlay = <T extends HTMLIonOverlayElement>(
@ -301,8 +329,8 @@ const trapKeyboardFocus = (ev: Event, doc: Document) => {
}; };
const connectListeners = (doc: Document) => { const connectListeners = (doc: Document) => {
if (lastId === 0) { if (lastOverlayIndex === 0) {
lastId = 1; lastOverlayIndex = 1;
doc.addEventListener( doc.addEventListener(
'focus', 'focus',
(ev: FocusEvent) => { (ev: FocusEvent) => {