Merge remote-tracking branch 'origin/main' into chore/sync-with-main-6

This commit is contained in:
amandaesmith3
2023-05-05 13:30:21 -05:00
1333 changed files with 8602 additions and 6610 deletions

View File

@ -29,7 +29,7 @@ runs:
shell: bash
working-directory: ./angular/test
- name: Install Dependencies
run: npm install --legacy-peer-deps
run: npm install
shell: bash
working-directory: ./angular/test/build/${{ inputs.app }}
- name: Sync Built Changes

File diff suppressed because it is too large Load Diff

View File

@ -19,37 +19,37 @@
"test.watch": "concurrently \"npm run start\" \"wait-on http-get://localhost:4200 && npm run cy.open\" --kill-others --success first"
},
"dependencies": {
"@angular/animations": "^16.0.0-rc.0",
"@angular/common": "^16.0.0-rc.0",
"@angular/compiler": "^16.0.0-rc.0",
"@angular/core": "^16.0.0-rc.0",
"@angular/forms": "^16.0.0-rc.0",
"@angular/platform-browser": "^16.0.0-rc.0",
"@angular/platform-browser-dynamic": "^16.0.0-rc.0",
"@angular/platform-server": "^16.0.0-rc.0",
"@angular/router": "^16.0.0-rc.0",
"@angular/animations": "^16.0.0",
"@angular/common": "^16.0.0",
"@angular/compiler": "^16.0.0",
"@angular/core": "^16.0.0",
"@angular/forms": "^16.0.0",
"@angular/platform-browser": "^16.0.0",
"@angular/platform-browser-dynamic": "^16.0.0",
"@angular/platform-server": "^16.0.0",
"@angular/router": "^16.0.0",
"@ionic/angular": "^7.0.0",
"@ionic/angular-server": "^7.0.0",
"@nguniversal/express-engine": "^15.0.0",
"@nguniversal/express-engine": "^16.0.0",
"core-js": "^2.6.11",
"express": "^4.15.2",
"ionicons": "^6.0.4",
"ionicons": "^7.0.4",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"typescript-eslint-language-service": "^4.1.5",
"zone.js": "~0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.0.0-rc.0",
"@angular-eslint/builder": "^15.0.0",
"@angular-eslint/eslint-plugin": "^15.0.0",
"@angular-eslint/eslint-plugin-template": "^15.0.0",
"@angular-eslint/schematics": "^15.0.0",
"@angular-eslint/template-parser": "^15.0.0",
"@angular/cli": "^16.0.0-rc.0",
"@angular/compiler-cli": "^16.0.0-rc.0",
"@angular/language-service": "^16.0.0-rc.0",
"@nguniversal/builders": "^15.0.0",
"@angular-devkit/build-angular": "^16.0.0",
"@angular-eslint/builder": "^16.0.0",
"@angular-eslint/eslint-plugin": "^16.0.0",
"@angular-eslint/eslint-plugin-template": "^16.0.0",
"@angular-eslint/schematics": "^16.0.0",
"@angular-eslint/template-parser": "^16.0.0",
"@angular/cli": "^16.0.0",
"@angular/compiler-cli": "^16.0.0",
"@angular/language-service": "^16.0.0",
"@nguniversal/builders": "^16.0.0",
"@types/express": "^4.17.7",
"@types/node": "^12.12.54",
"@typescript-eslint/eslint-plugin": "4.28.2",

View File

@ -15,7 +15,7 @@ npm pack ../../../dist
npm pack ../../../../packages/angular-server/dist
# Install Dependencies
npm install *.tgz --no-save --legacy-peer-deps
npm install *.tgz --no-save
# Delete Angular cache directory
rm -rf .angular/

View File

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

View File

@ -40,7 +40,7 @@ The `@ionic/core` package can be used in simple HTML, or by vanilla JavaScript w
* [@ionic/angular](https://www.npmjs.com/package/@ionic/angular)
## Custom Elements Build (Experimental)
## Custom Elements Build
In addition to the default, self lazy-loading components built by Stencil, this package also comes with each component exported as a stand-alone custom element within `@ionic/core/components`. Each component extends `HTMLElement`, and does not lazy-load itself. Instead, this package is useful for projects already using a bundler such as Webpack or Rollup. While all components are available to be imported, the custom elements build also ensures bundlers only import what's used, and tree-shakes any unused components.

View File

@ -12,6 +12,7 @@ import {
prepareOverlay,
present,
safeCall,
setOverlayId
} from '@utils/overlays';
import { getClassMap } from '@utils/theme';
@ -311,6 +312,10 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
this.triggerController.removeClickListener();
}
componentWillLoad() {
setOverlayId(this.el);
}
componentDidLoad() {
/**
* 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

@ -0,0 +1,42 @@
import { configs, test } from '@utils/test/playwright';
import { ActionSheetFixture } from './fixture';
configs().forEach(({ config, screenshot, title }) => {
test.describe(title('action sheet: variant rendering'), () => {
let actionSheetFixture!: ActionSheetFixture;
test.beforeEach(async ({ page }) => {
actionSheetFixture = new ActionSheetFixture(page, screenshot);
await page.goto(`/src/components/action-sheet/test/basic`, config);
});
test('should open basic action sheet', async () => {
await actionSheetFixture.open('#basic');
await actionSheetFixture.screenshot('basic');
/**
* We want to test that the dismiss method
* actually works, but we do not need to test
* it every time. As a result, we only
* call dismiss in this test.
*/
await actionSheetFixture.dismiss();
});
test('should open cancel only action sheet', async () => {
await actionSheetFixture.open('#cancelOnly');
await actionSheetFixture.screenshot('cancel-only');
});
test('should open custom action sheet', async () => {
await actionSheetFixture.open('#custom');
await actionSheetFixture.screenshot('custom');
});
test('should open scrollable action sheet', async () => {
await actionSheetFixture.open('#scrollableOptions');
await actionSheetFixture.screenshot('scrollable-options');
});
test('should open scrollable action sheet without cancel', async () => {
await actionSheetFixture.open('#scrollWithoutCancel');
await actionSheetFixture.screenshot('scroll-without-cancel');
});
});
});

View File

@ -1,9 +1,12 @@
import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import type { E2EPage } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
import { ActionSheetFixture } from './fixture';
/**
* This behavior does not vary across modes/directions
*/
configs({ mode: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('action sheet: data'), () => {
let actionSheetFixture!: ActionSheetFixture;
test.beforeEach(async ({ page }) => {
@ -35,58 +38,11 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
});
});
});
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('action sheet: attributes'), () => {
test('should set htmlAttributes', async ({ page }) => {
await page.goto(`/src/components/action-sheet/test/basic`, config);
const actionSheetFixture = new ActionSheetFixture(page);
await actionSheetFixture.open('#basic');
const actionSheet = page.locator('ion-action-sheet');
await expect(actionSheet).toHaveAttribute('data-testid', 'basic-action-sheet');
});
});
});
configs().forEach(({ config, screenshot, title }) => {
test.describe(title('action sheet: variant rendering'), () => {
let actionSheetFixture!: ActionSheetFixture;
test.beforeEach(async ({ page }) => {
actionSheetFixture = new ActionSheetFixture(page, screenshot);
await page.goto(`/src/components/action-sheet/test/basic`, config);
});
test('should open basic action sheet', async () => {
await actionSheetFixture.open('#basic');
await actionSheetFixture.screenshot('basic');
/**
* We want to test that the dismiss method
* actually works, but we do not need to test
* it every time. As a result, we only
* call dismiss in this test.
* This behavior does not vary across modes/directions
*/
await actionSheetFixture.dismiss();
});
test('should open cancel only action sheet', async () => {
await actionSheetFixture.open('#cancelOnly');
await actionSheetFixture.screenshot('cancel-only');
});
test('should open custom action sheet', async () => {
await actionSheetFixture.open('#custom');
await actionSheetFixture.screenshot('custom');
});
test('should open scrollable action sheet', async () => {
await actionSheetFixture.open('#scrollableOptions');
await actionSheetFixture.screenshot('scrollable-options');
});
test('should open scrollable action sheet without cancel', async () => {
await actionSheetFixture.open('#scrollWithoutCancel');
await actionSheetFixture.screenshot('scroll-without-cancel');
});
});
});
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
configs({ mode: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('action sheet: variant functionality'), () => {
let actionSheetFixture!: ActionSheetFixture;
test.beforeEach(async ({ page }) => {
@ -118,7 +74,11 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
});
});
});
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
/**
* This behavior does not vary across modes/directions
*/
configs({ mode: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('action sheet: focus trap'), () => {
test('it should trap focus in action sheet', async ({ page, browserName }) => {
await page.goto(`/src/components/action-sheet/test/basic`, config);
@ -140,42 +100,3 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
});
});
});
class ActionSheetFixture {
readonly page: E2EPage;
readonly screenshotFn?: (file: string) => string;
private actionSheet!: Locator;
constructor(page: E2EPage, screenshot?: (file: string) => string) {
this.page = page;
this.screenshotFn = screenshot;
}
async open(selector: string) {
const ionActionSheetDidPresent = await this.page.spyOnEvent('ionActionSheetDidPresent');
await this.page.locator(selector).click();
await ionActionSheetDidPresent.next();
this.actionSheet = this.page.locator('ion-action-sheet');
await expect(this.actionSheet).toBeVisible();
}
async dismiss() {
const ionActionSheetDidDismiss = await this.page.spyOnEvent('ionActionSheetDidDismiss');
await this.actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.dismiss());
await ionActionSheetDidDismiss.next();
await expect(this.actionSheet).not.toBeVisible();
}
async screenshot(modifier: string) {
const { screenshotFn } = this;
if (!screenshotFn) {
throw new Error(
'A screenshot function is required to take a screenshot. Pass one in when creating ActionSheetFixture.'
);
}
await expect(this.actionSheet).toHaveScreenshot(screenshotFn(`action-sheet-${modifier}-diff`));
}
}

View File

@ -0,0 +1,17 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { ActionSheet } from '../../action-sheet';
describe('action sheet: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [ActionSheet],
template: () => <ion-action-sheet htmlAttributes={{ 'data-testid': 'basic-action-sheet' }}></ion-action-sheet>,
});
const actionSheet = page.body.querySelector('ion-action-sheet');
await expect(actionSheet.getAttribute('data-testid')).toBe('basic-action-sheet');
});
});

View File

@ -0,0 +1,42 @@
import type { Locator } from '@playwright/test';
import { expect } from '@playwright/test';
import type { E2EPage } from '@utils/test/playwright';
export class ActionSheetFixture {
readonly page: E2EPage;
readonly screenshotFn?: (file: string) => string;
private actionSheet!: Locator;
constructor(page: E2EPage, screenshot?: (file: string) => string) {
this.page = page;
this.screenshotFn = screenshot;
}
async open(selector: string) {
const ionActionSheetDidPresent = await this.page.spyOnEvent('ionActionSheetDidPresent');
await this.page.locator(selector).click();
await ionActionSheetDidPresent.next();
this.actionSheet = this.page.locator('ion-action-sheet');
await expect(this.actionSheet).toBeVisible();
}
async dismiss() {
const ionActionSheetDidDismiss = await this.page.spyOnEvent('ionActionSheetDidDismiss');
await this.actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.dismiss());
await ionActionSheetDidDismiss.next();
await expect(this.actionSheet).not.toBeVisible();
}
async screenshot(modifier: string) {
const { screenshotFn } = this;
if (!screenshotFn) {
throw new Error(
'A screenshot function is required to take a screenshot. Pass one in when creating ActionSheetFixture.'
);
}
await expect(this.actionSheet).toHaveScreenshot(screenshotFn(`action-sheet-${modifier}-diff`));
}
}

View File

@ -13,6 +13,7 @@ import {
prepareOverlay,
present,
safeCall,
setOverlayId
} from '@utils/overlays';
import { sanitizeDOMString } from '@utils/sanitization';
import { getClassMap } from '@utils/theme';
@ -329,6 +330,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
}
componentWillLoad() {
setOverlayId(this.el);
this.inputsChanged();
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

@ -10,6 +10,7 @@ import {
present,
createDelegateController,
createTriggerController,
setOverlayId
} from '@utils/overlays';
import { sanitizeDOMString } from '@utils/sanitization';
import { getClassMap } from '@utils/theme';
@ -212,6 +213,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
const mode = getIonMode(this);
this.spinner = config.get('loadingSpinner', config.get('spinner', mode === 'ios' ? 'lines' : 'crescent'));
}
setOverlayId(this.el);
}
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 { 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 () => {
const page = await newSpecPage({
components: [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.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>`,
});
const content = page.body.querySelector('.loading-content');
const content = page.body.querySelector('.loading-content')!;
expect(content.textContent).toContain('Custom Text');
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>`,
});
const content = page.body.querySelector('.loading-content');
const content = page.body.querySelector('.loading-content')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});

View File

@ -15,6 +15,7 @@ import {
prepareOverlay,
present,
createTriggerController,
setOverlayId
} from '@utils/overlays';
import { getClassMap } from '@utils/theme';
import { deepReady, waitForMount } from '@utils/transition';
@ -65,8 +66,6 @@ import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
export class Modal implements ComponentInterface, OverlayInterface {
private readonly triggerController = createTriggerController();
private gesture?: Gesture;
private modalIndex = modalIds++;
private modalId?: string;
private coreDelegate: FrameworkDelegate = CoreDelegate();
private currentTransition?: Promise<any>;
private sheetTransition?: Promise<any>;
@ -344,16 +343,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
componentWillLoad() {
const { breakpoints, initialBreakpoint, el } = this;
const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
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) {
this.currentBreakpoint = this.initialBreakpoint;
}
@ -361,6 +354,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) {
printIonWarning('Your breakpoints array must include the initialBreakpoint value.');
}
setOverlayId(el);
}
componentDidLoad() {
@ -861,7 +856,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
const showHandle = handle !== false && isSheetModal;
const mode = getIonMode(this);
const { modalId } = this;
const isCardModal = presentingElement !== undefined && mode === 'ios';
const isHandleCycle = handleBehavior === 'cycle';
@ -881,7 +875,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
'overlay-hidden': true,
...getClassMap(this.cssClass),
}}
id={modalId}
onIonBackdropTap={this.onBackdropTap}
onIonModalDidPresent={this.onLifecycle}
onIonModalWillPresent={this.onLifecycle}
@ -935,8 +928,6 @@ const LIFECYCLE_MAP: any = {
ionModalDidDismiss: 'ionViewDidLeave',
};
let modalIds = 0;
interface ModalOverlayOptions {
/**
* 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

@ -10,6 +10,7 @@ import {
prepareOverlay,
present,
safeCall,
setOverlayId
} from '@utils/overlays';
import { getClassMap } from '@utils/theme';
@ -194,6 +195,10 @@ export class Picker implements ComponentInterface, OverlayInterface {
this.triggerController.removeClickListener();
}
componentWillLoad() {
setOverlayId(this.el);
}
/**
* 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

@ -3,7 +3,7 @@ import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '
import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate';
import { addEventListener, raf, hasLazyBuild } from '@utils/helpers';
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 { isPlatform } from '@utils/platform';
import { getClassMap } from '@utils/theme';
import { deepReady, waitForMount } from '@utils/transition';
@ -49,8 +49,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
private usersElement?: HTMLElement;
private triggerEl?: HTMLElement | null;
private parentPopover: HTMLIonPopoverElement | null = null;
private popoverIndex = popoverIds++;
private popoverId?: string;
private coreDelegate: FrameworkDelegate = CoreDelegate();
private currentTransition?: Promise<any>;
private destroyTriggerInteraction?: () => void;
@ -338,13 +336,10 @@ export class Popover implements ComponentInterface, PopoverInterface {
}
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}`;
const { el } = this;
const popoverId = setOverlayId(el);
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) {
this.alignment = getIonMode(this) === 'ios' ? 'center' : 'start';
@ -660,7 +655,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
render() {
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 enableArrow = arrow && !parentPopover;
@ -673,7 +668,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
style={{
zIndex: `${20000 + this.overlayIndex}`,
}}
id={popoverId}
class={{
...getClassMap(this.cssClass),
[mode]: true,
@ -709,8 +703,6 @@ const LIFECYCLE_MAP: any = {
ionPopoverDidDismiss: 'ionViewDidLeave',
};
let popoverIds = 0;
interface PopoverPresentOptions {
/**
* The original target event that presented the popover.

View File

@ -1,32 +0,0 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('popover: adjustment', async () => {
test('should not render the popover offscreen', async ({ page }) => {
await page.goto('/src/components/popover/test/adjustment');
/**
* We need to click in an area where
* there is not enough room to show the popover
* below the click coordinates but not enough
* room above the click coordinates that we
* can just move the popover to without it going
* offscreen.
*/
await page.setViewportSize({
width: 500,
height: 400,
});
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
await page.mouse.click(300, 300);
await ionPopoverDidPresent.next();
const popoverContent = page.locator('ion-popover .popover-content');
const box = (await popoverContent.boundingBox())!;
expect(box.y > 0).toBe(true);
});
});

View File

@ -0,0 +1,37 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('popover: adjustment'), async () => {
test('should not render the popover offscreen', async ({ page }) => {
await page.goto('/src/components/popover/test/adjustment', config);
/**
* We need to click in an area where
* there is not enough room to show the popover
* below the click coordinates but not enough
* room above the click coordinates that we
* can just move the popover to without it going
* offscreen.
*/
await page.setViewportSize({
width: 500,
height: 400,
});
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
await page.mouse.click(300, 300);
await ionPopoverDidPresent.next();
const popoverContent = page.locator('ion-popover .popover-content');
const box = (await popoverContent.boundingBox())!;
expect(box.y > 0).toBe(true);
});
});
});

View File

@ -1,23 +0,0 @@
import { expect } from '@playwright/test';
import { test, Viewports } from '@utils/test/playwright';
import { openPopover } from '../test.utils';
test.describe('popover: arrow rendering', async () => {
/**
* The popovers have showBackdrop=false so we can open all of them at once
* and massively cut down on screenshots taken. The content has its own
* backdrop so you can still see the popovers.
*/
test('should not have visual regressions', async ({ page }) => {
await page.goto('/src/components/popover/test/arrow');
await page.setViewportSize(Viewports.tablet.portrait); // avoid extra-long viewport screenshots
const sides = ['top', 'right', 'bottom', 'left', 'start', 'end'];
for (const side of sides) {
await openPopover(page, `${side}-trigger`, true);
}
await expect(page).toHaveScreenshot(`popover-arrow-${page.getSnapshotSettings()}.png`);
});
});

View File

@ -0,0 +1,28 @@
import { expect } from '@playwright/test';
import { configs, test, Viewports } from '@utils/test/playwright';
import { openPopover } from '../test.utils';
/**
* This feature only exists on iOS.
*/
configs({ modes: ['ios'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('popover: arrow rendering'), async () => {
/**
* The popovers have showBackdrop=false so we can open all of them at once
* and massively cut down on screenshots taken. The content has its own
* backdrop so you can still see the popovers.
*/
test('should not have visual regressions', async ({ page }) => {
await page.goto('/src/components/popover/test/arrow', config);
await page.setViewportSize(Viewports.tablet.portrait); // avoid extra-long viewport screenshots
const sides = ['top', 'right', 'bottom', 'left', 'start', 'end'];
for (const side of sides) {
await openPopover(page, `${side}-trigger`, true);
}
await expect(page).toHaveScreenshot(screenshot(`popover-arrow`));
});
});
});

View File

@ -1,129 +0,0 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
import { openPopover, screenshotPopover } from '../test.utils';
test.describe('popover: rendering', async () => {
test('should not have visual regressions', async ({ page }) => {
const buttonIDs = [
'basic-popover',
'translucent-popover',
'long-list-popover',
'no-event-popover',
'custom-class-popover',
'header-popover',
'translucent-header-popover',
];
for (const id of buttonIDs) {
await screenshotPopover(page, id, 'basic');
}
});
});
test.describe('popover: htmlAttributes', async () => {
test('should inherit attributes on host', async ({ page }) => {
await page.goto('/src/components/popover/test/basic');
await openPopover(page, 'basic-popover');
const alert = page.locator('ion-popover');
await expect(alert).toHaveAttribute('data-testid', 'basic-popover');
});
});
test.describe('popover: focus trap', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/popover/test/basic');
});
test('should focus the first ion-item on ArrowDown', async ({ page }) => {
const item0 = page.locator('ion-popover ion-item:nth-of-type(1)');
await openPopover(page, 'basic-popover');
await page.keyboard.press('ArrowDown');
await expect(item0).toBeFocused();
});
test('should trap focus', async ({ page, browserName }) => {
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
const items = page.locator('ion-popover ion-item');
await openPopover(page, 'basic-popover');
await page.keyboard.press(tabKey);
await expect(items.nth(0)).toBeFocused();
await page.keyboard.press(`Shift+${tabKey}`);
await expect(items.nth(3)).toBeFocused();
await page.keyboard.press(tabKey);
await expect(items.nth(0)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(items.nth(1)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(items.nth(2)).toBeFocused();
await page.keyboard.press('Home');
await expect(items.nth(0)).toBeFocused();
await page.keyboard.press('End');
await expect(items.nth(3)).toBeFocused();
});
test('should not override keyboard interactions for textarea elements', async ({ page, browserName }) => {
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
const popover = page.locator('ion-popover');
const innerNativeTextarea = page.locator('ion-textarea textarea').nth(0);
const vanillaTextarea = page.locator('ion-textarea + textarea');
await openPopover(page, 'popover-with-textarea');
/**
* Focusing happens async inside of popover so we need
* to wait for the requestAnimationFrame to fire.
*/
await expect(popover).toBeFocused();
await page.keyboard.press(tabKey);
// for Firefox, ion-textarea is focused first
// need to tab again to get to native input
if (browserName === 'firefox') {
await page.keyboard.press(tabKey);
}
await expect(innerNativeTextarea).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(innerNativeTextarea).toBeFocused();
await page.keyboard.press('ArrowUp');
await expect(innerNativeTextarea).toBeFocused();
await page.keyboard.press(tabKey);
// Checking within HTML textarea
await expect(vanillaTextarea).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(vanillaTextarea).toBeFocused();
await page.keyboard.press('ArrowUp');
await expect(vanillaTextarea).toBeFocused();
await page.keyboard.press('Home');
await expect(vanillaTextarea).toBeFocused();
await page.keyboard.press('End');
await expect(vanillaTextarea).toBeFocused();
});
});

Some files were not shown because too many files have changed in this diff Show More