Merge remote-tracking branch 'origin/main' into chore/sync-with-main-6
@ -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
|
||||
|
5360
angular/test/apps/ng16/package-lock.json
generated
@ -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",
|
||||
|
@ -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/
|
||||
|
@ -24,4 +24,5 @@ export class ModalInlineComponent implements AfterViewInit {
|
||||
onBreakpointDidChange() {
|
||||
this.breakpointDidChangeCounter++;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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');
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@ -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`));
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
42
core/src/components/action-sheet/test/basic/fixture.ts
Normal 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`));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
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');
|
||||
});
|
@ -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() {
|
||||
|
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 { 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);
|
||||
});
|
||||
|
@ -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.
|
||||
|
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');
|
||||
});
|
@ -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.
|
||||
*/
|
||||
|
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');
|
||||
});
|
@ -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.
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
37
core/src/components/popover/test/adjustment/popover.e2e.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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`);
|
||||
});
|
||||
});
|
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 28 KiB |
28
core/src/components/popover/test/arrow/popover.e2e.ts
Normal 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`));
|
||||
});
|
||||
});
|
||||
});
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@ -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();
|
||||
});
|
||||
});
|
Before Width: | Height: | Size: 31 KiB |