mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
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:
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -17,4 +17,4 @@
|
|||||||
</ion-list>
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ion-modal>
|
</ion-modal>
|
@ -24,4 +24,5 @@ export class ModalInlineComponent implements AfterViewInit {
|
|||||||
onBreakpointDidChange() {
|
onBreakpointDidChange() {
|
||||||
this.breakpointDidChangeCounter++;
|
this.breakpointDidChangeCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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');
|
||||||
|
});
|
@ -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();
|
||||||
}
|
}
|
||||||
|
41
core/src/components/alert/test/alert-id.spec.ts
Normal file
41
core/src/components/alert/test/alert-id.spec.ts
Normal 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');
|
||||||
|
});
|
@ -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() {
|
||||||
|
41
core/src/components/loading/test/loading-id.spec.ts
Normal file
41
core/src/components/loading/test/loading-id.spec.ts
Normal 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');
|
||||||
|
});
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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.
|
||||||
|
41
core/src/components/modal/test/modal-id.spec.ts
Normal file
41
core/src/components/modal/test/modal-id.spec.ts
Normal 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');
|
||||||
|
});
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
41
core/src/components/picker/test/picker-id.spec.ts
Normal file
41
core/src/components/picker/test/picker-id.spec.ts
Normal 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');
|
||||||
|
});
|
@ -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.
|
||||||
|
41
core/src/components/popover/test/popover-id.spec.ts
Normal file
41
core/src/components/popover/test/popover-id.spec.ts
Normal 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');
|
||||||
|
});
|
41
core/src/components/toast/test/toast-id.spec.ts
Normal file
41
core/src/components/toast/test/toast-id.spec.ts
Normal 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');
|
||||||
|
});
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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) => {
|
||||||
|
Reference in New Issue
Block a user