mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
chore(): sync with main
chore(): sync with main
This commit is contained in:
@ -46,14 +46,16 @@ const generateProjects = () => {
|
|||||||
...project,
|
...project,
|
||||||
metadata: {
|
metadata: {
|
||||||
mode,
|
mode,
|
||||||
rtl: false
|
rtl: false,
|
||||||
|
_testing: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
projectsWithMetadata.push({
|
projectsWithMetadata.push({
|
||||||
...project,
|
...project,
|
||||||
metadata: {
|
metadata: {
|
||||||
mode,
|
mode,
|
||||||
rtl: true
|
rtl: true,
|
||||||
|
_testing: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -72,7 +74,14 @@ const config: PlaywrightTestConfig = {
|
|||||||
* Maximum time expect() should wait for the condition to be met.
|
* Maximum time expect() should wait for the condition to be met.
|
||||||
* For example in `await expect(locator).toHaveText();`
|
* For example in `await expect(locator).toHaveText();`
|
||||||
*/
|
*/
|
||||||
timeout: 5000
|
timeout: 5000,
|
||||||
|
toMatchSnapshot: {
|
||||||
|
/**
|
||||||
|
* Increases the maximum allowed pixel difference to account
|
||||||
|
* for slight browser rendering inconsistencies.
|
||||||
|
*/
|
||||||
|
maxDiffPixelRatio: 0.05
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { E2EPage } from '@stencil/core/testing';
|
||||||
import { newE2EPage } from '@stencil/core/testing';
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
import { testPopover } from '../test.utils';
|
import { testPopover } from '../test.utils';
|
||||||
@ -8,51 +9,23 @@ const DIRECTORY = 'basic';
|
|||||||
* Focusing happens async inside of popover so we need
|
* Focusing happens async inside of popover so we need
|
||||||
* to wait for the requestAnimationFrame to fire.
|
* to wait for the requestAnimationFrame to fire.
|
||||||
*/
|
*/
|
||||||
const expectActiveElementTextToEqual = async (page, textValue) => {
|
const expectActiveElementTextToEqual = async (page: E2EPage, textValue: string) => {
|
||||||
await page.waitFor((text) => document.activeElement.textContent === text, {}, textValue);
|
await page.evaluate((text) => document.activeElement!.textContent === text, textValue)
|
||||||
};
|
};
|
||||||
|
|
||||||
test('popover: focus trap', async () => {
|
const getActiveElementSelectionStart = (page: E2EPage) => {
|
||||||
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
return page.evaluate(() =>
|
||||||
|
document.activeElement instanceof HTMLTextAreaElement ? document.activeElement.selectionStart : null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
await page.click('#basic-popover');
|
const getActiveElementScrollTop = (page: E2EPage) => {
|
||||||
await page.waitForSelector('#basic-popover');
|
return page.evaluate(() => {
|
||||||
|
// Returns the closest ion-textarea or active element
|
||||||
const popover = await page.find('ion-popover');
|
const target = document.activeElement!.closest('ion-textarea') ?? document.activeElement;
|
||||||
|
return target!.scrollTop;
|
||||||
expect(popover).not.toBe(null);
|
});
|
||||||
await popover.waitForVisible();
|
};
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
|
||||||
|
|
||||||
await page.keyboard.down('Shift');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.up('Shift');
|
|
||||||
|
|
||||||
await expectActiveElementTextToEqual(page, 'Item 3');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
|
||||||
|
|
||||||
await page.keyboard.press('ArrowDown');
|
|
||||||
|
|
||||||
await expectActiveElementTextToEqual(page, 'Item 1');
|
|
||||||
|
|
||||||
await page.keyboard.press('ArrowDown');
|
|
||||||
|
|
||||||
await expectActiveElementTextToEqual(page, 'Item 2');
|
|
||||||
|
|
||||||
await page.keyboard.press('Home');
|
|
||||||
|
|
||||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
|
||||||
|
|
||||||
await page.keyboard.press('End');
|
|
||||||
|
|
||||||
await expectActiveElementTextToEqual(page, 'Item 3');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('popover: basic', async () => {
|
test('popover: basic', async () => {
|
||||||
await testPopover(DIRECTORY, '#basic-popover');
|
await testPopover(DIRECTORY, '#basic-popover');
|
||||||
@ -125,7 +98,141 @@ test('popover: htmlAttributes', async () => {
|
|||||||
expect(alert).not.toBe(null);
|
expect(alert).not.toBe(null);
|
||||||
await alert.waitForVisible();
|
await alert.waitForVisible();
|
||||||
|
|
||||||
const attribute = await page.evaluate(() => document.querySelector('ion-popover').getAttribute('data-testid'));
|
const attribute = await page.evaluate(() => document.querySelector('ion-popover')!.getAttribute('data-testid'));
|
||||||
|
|
||||||
expect(attribute).toEqual('basic-popover');
|
expect(attribute).toEqual('basic-popover');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('popover: focus trap', () => {
|
||||||
|
|
||||||
|
it('should focus the first ion-item on ArrowDown', async () => {
|
||||||
|
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||||
|
|
||||||
|
await page.click('#basic-popover');
|
||||||
|
|
||||||
|
const popover = await page.find('ion-popover');
|
||||||
|
|
||||||
|
expect(popover).not.toBe(null);
|
||||||
|
await popover.waitForVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ion-item children', async () => {
|
||||||
|
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||||
|
|
||||||
|
await page.click('#basic-popover');
|
||||||
|
await page.waitForSelector('#basic-popover');
|
||||||
|
|
||||||
|
const popover = await page.find('ion-popover');
|
||||||
|
|
||||||
|
expect(popover).not.toBe(null);
|
||||||
|
await popover.waitForVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||||
|
|
||||||
|
await page.keyboard.down('Shift');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.keyboard.up('Shift');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 3');
|
||||||
|
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 1');
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 2');
|
||||||
|
|
||||||
|
await page.keyboard.press('Home');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||||
|
|
||||||
|
await page.keyboard.press('End');
|
||||||
|
|
||||||
|
await expectActiveElementTextToEqual(page, 'Item 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override keyboard interactions for textarea elements', async () => {
|
||||||
|
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||||
|
|
||||||
|
await page.waitForSelector('#popover-with-textarea');
|
||||||
|
await page.click('#popover-with-textarea');
|
||||||
|
|
||||||
|
const popover = await page.find('ion-popover');
|
||||||
|
await popover.waitForVisible();
|
||||||
|
|
||||||
|
await page.waitForFunction('document.activeElement.tagName === "ION-POPOVER"');
|
||||||
|
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
// Checking within ion-textarea
|
||||||
|
|
||||||
|
let activeElementTagName = await page.evaluate(() => document.activeElement!.tagName);
|
||||||
|
let scrollTop = null;
|
||||||
|
let selectionStart = null;
|
||||||
|
let previousSelectionStart = null;
|
||||||
|
|
||||||
|
// This is the native textarea within ion-textarea
|
||||||
|
expect(activeElementTagName).toBe('TEXTAREA');
|
||||||
|
|
||||||
|
selectionStart = await getActiveElementSelectionStart(page);
|
||||||
|
expect(selectionStart).toBe(0);
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
selectionStart = await getActiveElementSelectionStart(page);
|
||||||
|
expect(selectionStart).toBeGreaterThan(0);
|
||||||
|
previousSelectionStart = selectionStart;
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
selectionStart = await getActiveElementSelectionStart(page);
|
||||||
|
expect(selectionStart).toBeGreaterThan(previousSelectionStart!);
|
||||||
|
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
// Checking within HTML textarea
|
||||||
|
|
||||||
|
// Reset tracking variables as the focus element has changed
|
||||||
|
scrollTop = null;
|
||||||
|
selectionStart = null;
|
||||||
|
previousSelectionStart = null;
|
||||||
|
|
||||||
|
activeElementTagName = await page.evaluate(() => document.activeElement!.tagName);
|
||||||
|
expect(activeElementTagName).toBe('TEXTAREA');
|
||||||
|
|
||||||
|
selectionStart = await getActiveElementSelectionStart(page);
|
||||||
|
expect(selectionStart).toBe(0);
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
selectionStart = await getActiveElementSelectionStart(page);
|
||||||
|
expect(selectionStart).toBeGreaterThan(0);
|
||||||
|
previousSelectionStart = selectionStart;
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
selectionStart = await getActiveElementSelectionStart(page);
|
||||||
|
expect(selectionStart).toBeGreaterThan(previousSelectionStart!);
|
||||||
|
|
||||||
|
await page.keyboard.press('Home');
|
||||||
|
|
||||||
|
scrollTop = await getActiveElementScrollTop(page);
|
||||||
|
expect(scrollTop).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const previousScrollTop = scrollTop;
|
||||||
|
|
||||||
|
await page.keyboard.press('End');
|
||||||
|
|
||||||
|
scrollTop = await getActiveElementScrollTop(page);
|
||||||
|
expect(scrollTop).toBeGreaterThanOrEqual(previousScrollTop);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,91 +1,67 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" dir="ltr">
|
<html lang="en" dir="ltr">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
<title>Popover - Basic</title>
|
<title>Popover - Basic</title>
|
||||||
<meta
|
<meta name="viewport"
|
||||||
name="viewport"
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||||
/>
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
|
||||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
|
||||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { popoverController } from '../../../../dist/ionic/index.esm.js';
|
import { popoverController } from '../../../../dist/ionic/index.esm.js';
|
||||||
window.popoverController = popoverController;
|
window.popoverController = popoverController;
|
||||||
</script>
|
</script>
|
||||||
<body>
|
|
||||||
|
<body>
|
||||||
<ion-app>
|
<ion-app>
|
||||||
<ion-header>
|
|
||||||
<ion-toolbar color="primary">
|
|
||||||
<ion-buttons slot="secondary">
|
|
||||||
<ion-button onclick="presentPopover({component: 'profile-page', event: event })">
|
|
||||||
<ion-icon slot="icon-only" name="person"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>Popover - Basic</ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding" id="content">
|
<ion-content class="ion-padding" id="content">
|
||||||
<ion-button
|
<ion-button id="basic-popover" expand="block"
|
||||||
id="basic-popover"
|
onclick="presentPopover({ component: 'profile-page', event: event, htmlAttributes: { 'data-testid': 'basic-popover' } })">
|
||||||
expand="block"
|
Show Popover</ion-button>
|
||||||
onclick="presentPopover({ component: 'profile-page', event: event, htmlAttributes: { 'data-testid': 'basic-popover' } })"
|
<ion-button id="translucent-popover" expand="block"
|
||||||
>Show Popover</ion-button
|
onclick="presentPopover({ component: 'translucent-page', event: event, translucent: true })">Show Translucent
|
||||||
>
|
Popover</ion-button>
|
||||||
<ion-button
|
<ion-button id="long-list-popover" expand="block" color="secondary"
|
||||||
id="translucent-popover"
|
onclick="presentPopover({ component: 'list-page', event: event })">Show Long List Popover</ion-button>
|
||||||
expand="block"
|
<ion-button id="no-event-popover" expand="block" color="danger"
|
||||||
onclick="presentPopover({ component: 'translucent-page', event: event, translucent: true })"
|
onclick="presentPopover({ component: 'profile-page' })">No Event Popover</ion-button>
|
||||||
>Show Translucent Popover</ion-button
|
<ion-button id="custom-class-popover" expand="block" color="tertiary"
|
||||||
>
|
onclick="presentPopover({ component: 'translucent-page', event: event, cssClass: 'my-custom-class' })">Custom
|
||||||
<ion-button
|
Class Popover</ion-button>
|
||||||
id="long-list-popover"
|
<ion-button id="header-popover" expand="block" onclick="presentPopover({ component: 'header-page' })">Popover With
|
||||||
expand="block"
|
Header</ion-button>
|
||||||
color="secondary"
|
<ion-button id="translucent-header-popover" expand="block"
|
||||||
onclick="presentPopover({ component: 'list-page', event: event })"
|
onclick="presentPopover({ component: 'translucent-header-page' })">Popover With Translucent Header</ion-button>
|
||||||
>Show Long List Popover</ion-button
|
|
||||||
>
|
|
||||||
<ion-button
|
|
||||||
id="no-event-popover"
|
|
||||||
expand="block"
|
|
||||||
color="danger"
|
|
||||||
onclick="presentPopover({ component: 'profile-page' })"
|
|
||||||
>No Event Popover</ion-button
|
|
||||||
>
|
|
||||||
<ion-button
|
|
||||||
id="custom-class-popover"
|
|
||||||
expand="block"
|
|
||||||
color="tertiary"
|
|
||||||
onclick="presentPopover({ component: 'translucent-page', event: event, cssClass: 'my-custom-class' })"
|
|
||||||
>Custom Class Popover</ion-button
|
|
||||||
>
|
|
||||||
<ion-button id="header-popover" expand="block" onclick="presentPopover({ component: 'header-page' })"
|
|
||||||
>Popover With Header</ion-button
|
|
||||||
>
|
|
||||||
<ion-button
|
|
||||||
id="translucent-header-popover"
|
|
||||||
expand="block"
|
|
||||||
onclick="presentPopover({ component: 'translucent-header-page' })"
|
|
||||||
>Popover With Translucent Header</ion-button
|
|
||||||
>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
||||||
<ion-footer>
|
<ion-content class="ion-padding" id="content">
|
||||||
<ion-toolbar color="primary">
|
<ion-button id="basic-popover" expand="block"
|
||||||
<ion-buttons slot="primary">
|
onclick="presentPopover({ component: 'profile-page', event: event, htmlAttributes: { 'data-testid': 'basic-popover' } })">
|
||||||
<ion-button onclick="presentPopover({component: 'profile-page', event: event })">
|
Show Popover</ion-button>
|
||||||
<ion-icon slot="icon-only" name="person"></ion-icon>
|
<ion-button id="translucent-popover" expand="block"
|
||||||
</ion-button>
|
onclick="presentPopover({ component: 'translucent-page', event: event, translucent: true })">Show Translucent
|
||||||
</ion-buttons>
|
Popover</ion-button>
|
||||||
<ion-title>Popover</ion-title>
|
<ion-button id="long-list-popover" expand="block" color="secondary"
|
||||||
</ion-toolbar>
|
onclick="presentPopover({ component: 'list-page', event: event })">Show Long List Popover</ion-button>
|
||||||
</ion-footer>
|
<ion-button id="no-event-popover" expand="block" color="danger"
|
||||||
</ion-app>
|
onclick="presentPopover({ component: 'profile-page' })">No Event Popover</ion-button>
|
||||||
|
<ion-button id="custom-class-popover" expand="block" color="tertiary"
|
||||||
|
onclick="presentPopover({ component: 'translucent-page', event: event, cssClass: 'my-custom-class' })">Custom
|
||||||
|
Class Popover</ion-button>
|
||||||
|
<ion-button id="header-popover" expand="block" onclick="presentPopover({ component: 'header-page' })">Popover With
|
||||||
|
Header</ion-button>
|
||||||
|
<ion-button id="translucent-header-popover" expand="block"
|
||||||
|
onclick="presentPopover({ component: 'translucent-header-page' })">Popover With Translucent Header</ion-button>
|
||||||
|
<ion-button id="popover-with-textarea" expand="block"
|
||||||
|
onclick="presentPopover({ component: 'textarea-page', event: event, htmlAttributes: { 'data-testid': 'popover-with-textarea'} })">
|
||||||
|
Popover With Textarea</ion-button>
|
||||||
|
</ion-content>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.my-custom-class {
|
.my-custom-class {
|
||||||
@ -217,6 +193,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('translucent-header-page', TranslucentHeaderPage);
|
customElements.define('translucent-header-page', TranslucentHeaderPage);
|
||||||
|
|
||||||
|
class TextAreaPage extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<ion-content>
|
||||||
|
<ion-textarea rows="4" style="max-height: 44px; overflow-y: auto;" value="the cursor in this <ion-textarea>\nmust be able to be moved\nwith the arrow keys and\nhome and end keys"></ion-textarea>
|
||||||
|
<textarea rows="4" style="display:block;width: 100%;border: 0;padding: 8px; max-height: 44px; overflow-y: auto">the cursor in this <textarea>\nmust be able to be moved\nwith the arrow keys and\nhome and end keys</textarea>
|
||||||
|
</ion-content>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('textarea-page', TextAreaPage);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -337,6 +337,14 @@ export const configureKeyboardInteraction = (popoverEl: HTMLIonPopoverElement) =
|
|||||||
const activeElement = document.activeElement as HTMLElement | null;
|
const activeElement = document.activeElement as HTMLElement | null;
|
||||||
let items: HTMLIonItemElement[] = [];
|
let items: HTMLIonItemElement[] = [];
|
||||||
|
|
||||||
|
const targetTagName = (ev.target as HTMLElement)?.tagName;
|
||||||
|
/**
|
||||||
|
* Only handle custom keyboard interactions for the host popover element
|
||||||
|
* and children ion-item elements.
|
||||||
|
*/
|
||||||
|
if (targetTagName !== 'ION-POPOVER' && targetTagName !== 'ION-ITEM') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Complex selectors with :not() are :not supported
|
* Complex selectors with :not() are :not supported
|
||||||
* in older versions of Chromium so we need to do a
|
* in older versions of Chromium so we need to do a
|
||||||
@ -353,7 +361,7 @@ export const configureKeyboardInteraction = (popoverEl: HTMLIonPopoverElement) =
|
|||||||
) as NodeListOf<HTMLIonItemElement>
|
) as NodeListOf<HTMLIonItemElement>
|
||||||
);
|
);
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
/**
|
/**
|
||||||
|
@ -46,18 +46,18 @@
|
|||||||
|
|
||||||
<ion-content class="ion-padding" no-bounce>
|
<ion-content class="ion-padding" no-bounce>
|
||||||
<p>
|
<p>
|
||||||
<ion-button size="small">Small</ion-button>
|
<ion-button id="small-btn" size="small">Small</ion-button>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<ion-button size="large">Large</ion-button>
|
<ion-button id="large-btn" size="large">Large</ion-button>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<ion-button size="large" fill="outline">Large</ion-button>
|
<ion-button id="large-btn-outline" size="large" fill="outline">Large</ion-button>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<ion-button size="large" fill="clear">Large</ion-button>
|
<ion-button id="large-btn-clear" size="large" fill="clear">Large</ion-button>
|
||||||
</p>
|
</p>
|
||||||
<div class="my-block ion-activatable">
|
<div class="my-block ion-activatable" id="ripple-with-button">
|
||||||
<ion-ripple-effect></ion-ripple-effect>
|
<ion-ripple-effect></ion-ripple-effect>
|
||||||
This is just a div + effect behind
|
This is just a div + effect behind
|
||||||
<ion-button onclick="buttonClicked()">Nested button</ion-button>
|
<ion-button onclick="buttonClicked()">Nested button</ion-button>
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import type { IonicPage } from '@utils/test/playwright';
|
||||||
|
import { test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
test.describe('ripple-effect: basic', () => {
|
||||||
|
test('should add .ion-activated when pressed', async ({ page }) => {
|
||||||
|
await verifyRippleEffect(page, '#small-btn');
|
||||||
|
await verifyRippleEffect(page, '#large-btn');
|
||||||
|
await verifyRippleEffect(page, '#large-btn-outline');
|
||||||
|
await verifyRippleEffect(page, '#large-btn-clear');
|
||||||
|
await verifyRippleEffect(page, '.block');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('ripple effect with nested ion-button', () => {
|
||||||
|
test('should add .ion-activated when the block is pressed', async ({ page }) => {
|
||||||
|
await page.goto(`/src/components/ripple-effect/test/basic?ionic:_testing=false&ionic:mode=md`);
|
||||||
|
|
||||||
|
const el = page.locator('#ripple-with-button');
|
||||||
|
|
||||||
|
await el.scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
|
const boundingBox = await el.boundingBox();
|
||||||
|
|
||||||
|
if (boundingBox) {
|
||||||
|
await page.mouse.move(boundingBox.x + 5, boundingBox.y + 5);
|
||||||
|
await page.mouse.down();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waits for the ripple effect to be added
|
||||||
|
await page.waitForSelector('.ion-activated');
|
||||||
|
|
||||||
|
const elHandle = await el.elementHandle();
|
||||||
|
const classes = await elHandle?.evaluate((el) => el.classList.value);
|
||||||
|
expect(classes).toMatch('ion-activated');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add .ion-activated when the button is pressed', async ({ page }) => {
|
||||||
|
await verifyRippleEffect(page, '#ripple-with-button ion-button');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifyRippleEffect = async (page: IonicPage, selector: string) => {
|
||||||
|
await page.goto(`/src/components/ripple-effect/test/basic?ionic:_testing=false&ionic:mode=md`);
|
||||||
|
|
||||||
|
const el = page.locator(selector);
|
||||||
|
|
||||||
|
await el.scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
|
const boundingBox = await el.boundingBox();
|
||||||
|
|
||||||
|
if (boundingBox) {
|
||||||
|
await page.mouse.move(boundingBox.x + boundingBox.width / 2, boundingBox.y + boundingBox.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waits for the ripple effect to be added
|
||||||
|
await page.waitForSelector(`${selector}.ion-activated`);
|
||||||
|
|
||||||
|
const elHandle = await el.elementHandle();
|
||||||
|
const classes = await elHandle?.evaluate((el) => el.classList.value);
|
||||||
|
expect(classes).toMatch('ion-activated');
|
||||||
|
};
|
@ -15,7 +15,8 @@ export const startTapClick = (config: Config) => {
|
|||||||
const clearDefers = new WeakMap<HTMLElement, any>();
|
const clearDefers = new WeakMap<HTMLElement, any>();
|
||||||
|
|
||||||
const isScrolling = () => {
|
const isScrolling = () => {
|
||||||
return scrollingEl?.parentElement !== null;
|
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||||
|
return scrollingEl !== undefined && scrollingEl.parentElement !== null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Touch Events
|
// Touch Events
|
||||||
@ -169,7 +170,7 @@ const getActivatableTarget = (ev: any): any => {
|
|||||||
const path = ev.composedPath() as HTMLElement[];
|
const path = ev.composedPath() as HTMLElement[];
|
||||||
for (let i = 0; i < path.length - 2; i++) {
|
for (let i = 0; i < path.length - 2; i++) {
|
||||||
const el = path[i];
|
const el = path[i];
|
||||||
if (el?.classList.contains('ion-activatable')) {
|
if (!(el instanceof ShadowRoot) && el.classList.contains('ion-activatable')) {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export const test = base.extend<CustomFixtures>({
|
|||||||
* to be hydrated before proceeding with the test.
|
* to be hydrated before proceeding with the test.
|
||||||
*/
|
*/
|
||||||
page.goto = async (url: string) => {
|
page.goto = async (url: string) => {
|
||||||
const { mode, rtl } = testInfo.project.metadata;
|
const { mode, rtl, _testing } = testInfo.project.metadata;
|
||||||
|
|
||||||
const splitUrl = url.split('?');
|
const splitUrl = url.split('?');
|
||||||
const paramsString = splitUrl[1];
|
const paramsString = splitUrl[1];
|
||||||
@ -55,8 +55,9 @@ export const test = base.extend<CustomFixtures>({
|
|||||||
const urlToParams = new URLSearchParams(paramsString);
|
const urlToParams = new URLSearchParams(paramsString);
|
||||||
const formattedMode = urlToParams.get('ionic:mode') ?? mode;
|
const formattedMode = urlToParams.get('ionic:mode') ?? mode;
|
||||||
const formattedRtl = urlToParams.get('rtl') ?? rtl;
|
const formattedRtl = urlToParams.get('rtl') ?? rtl;
|
||||||
|
const ionicTesting = urlToParams.get('ionic:_testing') ?? _testing;
|
||||||
|
|
||||||
const formattedUrl = `${splitUrl[0]}?ionic:_testing=true&ionic:mode=${formattedMode}&rtl=${formattedRtl}`;
|
const formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}`;
|
||||||
|
|
||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
page.waitForFunction(() => (window as any).testAppLoaded === true),
|
page.waitForFunction(() => (window as any).testAppLoaded === true),
|
||||||
|
Reference in New Issue
Block a user