Merge remote-tracking branch 'origin/main' into chore/sync-with-main-6
@ -29,7 +29,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
working-directory: ./angular/test
|
working-directory: ./angular/test
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install --legacy-peer-deps
|
run: npm install
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: ./angular/test/build/${{ inputs.app }}
|
working-directory: ./angular/test/build/${{ inputs.app }}
|
||||||
- name: Sync Built Changes
|
- 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"
|
"test.watch": "concurrently \"npm run start\" \"wait-on http-get://localhost:4200 && npm run cy.open\" --kill-others --success first"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^16.0.0-rc.0",
|
"@angular/animations": "^16.0.0",
|
||||||
"@angular/common": "^16.0.0-rc.0",
|
"@angular/common": "^16.0.0",
|
||||||
"@angular/compiler": "^16.0.0-rc.0",
|
"@angular/compiler": "^16.0.0",
|
||||||
"@angular/core": "^16.0.0-rc.0",
|
"@angular/core": "^16.0.0",
|
||||||
"@angular/forms": "^16.0.0-rc.0",
|
"@angular/forms": "^16.0.0",
|
||||||
"@angular/platform-browser": "^16.0.0-rc.0",
|
"@angular/platform-browser": "^16.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^16.0.0-rc.0",
|
"@angular/platform-browser-dynamic": "^16.0.0",
|
||||||
"@angular/platform-server": "^16.0.0-rc.0",
|
"@angular/platform-server": "^16.0.0",
|
||||||
"@angular/router": "^16.0.0-rc.0",
|
"@angular/router": "^16.0.0",
|
||||||
"@ionic/angular": "^7.0.0",
|
"@ionic/angular": "^7.0.0",
|
||||||
"@ionic/angular-server": "^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",
|
"core-js": "^2.6.11",
|
||||||
"express": "^4.15.2",
|
"express": "^4.15.2",
|
||||||
"ionicons": "^6.0.4",
|
"ionicons": "^7.0.4",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"typescript-eslint-language-service": "^4.1.5",
|
"typescript-eslint-language-service": "^4.1.5",
|
||||||
"zone.js": "~0.13.0"
|
"zone.js": "~0.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^16.0.0-rc.0",
|
"@angular-devkit/build-angular": "^16.0.0",
|
||||||
"@angular-eslint/builder": "^15.0.0",
|
"@angular-eslint/builder": "^16.0.0",
|
||||||
"@angular-eslint/eslint-plugin": "^15.0.0",
|
"@angular-eslint/eslint-plugin": "^16.0.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "^15.0.0",
|
"@angular-eslint/eslint-plugin-template": "^16.0.0",
|
||||||
"@angular-eslint/schematics": "^15.0.0",
|
"@angular-eslint/schematics": "^16.0.0",
|
||||||
"@angular-eslint/template-parser": "^15.0.0",
|
"@angular-eslint/template-parser": "^16.0.0",
|
||||||
"@angular/cli": "^16.0.0-rc.0",
|
"@angular/cli": "^16.0.0",
|
||||||
"@angular/compiler-cli": "^16.0.0-rc.0",
|
"@angular/compiler-cli": "^16.0.0",
|
||||||
"@angular/language-service": "^16.0.0-rc.0",
|
"@angular/language-service": "^16.0.0",
|
||||||
"@nguniversal/builders": "^15.0.0",
|
"@nguniversal/builders": "^16.0.0",
|
||||||
"@types/express": "^4.17.7",
|
"@types/express": "^4.17.7",
|
||||||
"@types/node": "^12.12.54",
|
"@types/node": "^12.12.54",
|
||||||
"@typescript-eslint/eslint-plugin": "4.28.2",
|
"@typescript-eslint/eslint-plugin": "4.28.2",
|
||||||
|
@ -15,7 +15,7 @@ npm pack ../../../dist
|
|||||||
npm pack ../../../../packages/angular-server/dist
|
npm pack ../../../../packages/angular-server/dist
|
||||||
|
|
||||||
# Install Dependencies
|
# Install Dependencies
|
||||||
npm install *.tgz --no-save --legacy-peer-deps
|
npm install *.tgz --no-save
|
||||||
|
|
||||||
# Delete Angular cache directory
|
# Delete Angular cache directory
|
||||||
rm -rf .angular/
|
rm -rf .angular/
|
||||||
|
@ -24,4 +24,5 @@ export class ModalInlineComponent implements AfterViewInit {
|
|||||||
onBreakpointDidChange() {
|
onBreakpointDidChange() {
|
||||||
this.breakpointDidChangeCounter++;
|
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)
|
* [@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.
|
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,
|
prepareOverlay,
|
||||||
present,
|
present,
|
||||||
safeCall,
|
safeCall,
|
||||||
|
setOverlayId
|
||||||
} from '@utils/overlays';
|
} from '@utils/overlays';
|
||||||
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');
|
||||||
|
});
|
@ -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 { expect } from '@playwright/test';
|
||||||
import type { Locator } from '@playwright/test';
|
|
||||||
import { configs, test } from '@utils/test/playwright';
|
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'), () => {
|
test.describe(title('action sheet: data'), () => {
|
||||||
let actionSheetFixture!: ActionSheetFixture;
|
let actionSheetFixture!: ActionSheetFixture;
|
||||||
test.beforeEach(async ({ page }) => {
|
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
|
* This behavior does not vary across modes/directions
|
||||||
* 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();
|
configs({ mode: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) => {
|
||||||
});
|
|
||||||
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 }) => {
|
|
||||||
test.describe(title('action sheet: variant functionality'), () => {
|
test.describe(title('action sheet: variant functionality'), () => {
|
||||||
let actionSheetFixture!: ActionSheetFixture;
|
let actionSheetFixture!: ActionSheetFixture;
|
||||||
test.beforeEach(async ({ page }) => {
|
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.describe(title('action sheet: focus trap'), () => {
|
||||||
test('it should trap focus in action sheet', async ({ page, browserName }) => {
|
test('it should trap focus in action sheet', async ({ page, browserName }) => {
|
||||||
await page.goto(`/src/components/action-sheet/test/basic`, config);
|
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,
|
prepareOverlay,
|
||||||
present,
|
present,
|
||||||
safeCall,
|
safeCall,
|
||||||
|
setOverlayId
|
||||||
} from '@utils/overlays';
|
} from '@utils/overlays';
|
||||||
import { sanitizeDOMString } from '@utils/sanitization';
|
import { sanitizeDOMString } from '@utils/sanitization';
|
||||||
import { getClassMap } from '@utils/theme';
|
import { getClassMap } from '@utils/theme';
|
||||||
@ -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
@ -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,
|
present,
|
||||||
createDelegateController,
|
createDelegateController,
|
||||||
createTriggerController,
|
createTriggerController,
|
||||||
|
setOverlayId
|
||||||
} from '@utils/overlays';
|
} from '@utils/overlays';
|
||||||
import { sanitizeDOMString } from '@utils/sanitization';
|
import { sanitizeDOMString } from '@utils/sanitization';
|
||||||
import { getClassMap } from '@utils/theme';
|
import { getClassMap } from '@utils/theme';
|
||||||
@ -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
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
prepareOverlay,
|
prepareOverlay,
|
||||||
present,
|
present,
|
||||||
createTriggerController,
|
createTriggerController,
|
||||||
|
setOverlayId
|
||||||
} from '@utils/overlays';
|
} from '@utils/overlays';
|
||||||
import { getClassMap } from '@utils/theme';
|
import { getClassMap } from '@utils/theme';
|
||||||
import { deepReady, waitForMount } from '@utils/transition';
|
import { deepReady, waitForMount } from '@utils/transition';
|
||||||
@ -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
@ -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,
|
prepareOverlay,
|
||||||
present,
|
present,
|
||||||
safeCall,
|
safeCall,
|
||||||
|
setOverlayId
|
||||||
} from '@utils/overlays';
|
} from '@utils/overlays';
|
||||||
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
@ -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 { 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 { isPlatform } from '@utils/platform';
|
import { isPlatform } from '@utils/platform';
|
||||||
import { getClassMap } from '@utils/theme';
|
import { getClassMap } from '@utils/theme';
|
||||||
import { deepReady, waitForMount } from '@utils/transition';
|
import { deepReady, waitForMount } from '@utils/transition';
|
||||||
@ -49,8 +49,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 +336,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 +655,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 +668,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 +703,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.
|
||||||
|
@ -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 |