mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
chore(): sync with main
This commit is contained in:
@@ -69,6 +69,7 @@ const transitionEnd = (el: HTMLElement | null, expectedDuration = 0, callback: (
|
||||
*/
|
||||
export const componentOnReady = (el: any, callback: any) => {
|
||||
if (el.componentOnReady) {
|
||||
// eslint-disable-next-line custom-rules/no-component-on-ready-method
|
||||
el.componentOnReady().then((resolvedEl: any) => callback(resolvedEl));
|
||||
} else {
|
||||
raf(() => callback(el));
|
||||
|
||||
@@ -82,8 +82,20 @@ export const createOverlay = <T extends HTMLIonOverlayElement>(
|
||||
return Promise.resolve() as any;
|
||||
};
|
||||
|
||||
/**
|
||||
* This query string selects elements that
|
||||
* are eligible to receive focus. We select
|
||||
* interactive elements that meet the following
|
||||
* criteria:
|
||||
* 1. Element does not have a negative tabindex
|
||||
* 2. Element does not have `hidden`
|
||||
* 3. Element does not have `disabled` for non-Ionic components.
|
||||
* 4. Element does not have `disabled` or `disabled="true"` for Ionic components.
|
||||
* Note: We need this distinction because `disabled="false"` is
|
||||
* valid usage for the disabled property on ion-button.
|
||||
*/
|
||||
const focusableQueryString =
|
||||
'[tabindex]:not([tabindex^="-"]), input:not([type=hidden]):not([tabindex^="-"]), textarea:not([tabindex^="-"]), button:not([tabindex^="-"]), select:not([tabindex^="-"]), .ion-focusable:not([tabindex^="-"])';
|
||||
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
|
||||
|
||||
export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
|
||||
let firstInput = ref.querySelector(focusableQueryString) as HTMLElement | null;
|
||||
|
||||
@@ -2,9 +2,10 @@ import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('overlays: focus', () => {
|
||||
test('should not focus the overlay container if element inside of overlay is focused', async ({ page, skip }) => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
|
||||
});
|
||||
test('should not focus the overlay container if element inside of overlay is focused', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-button id="open-modal">Show Modal</ion-button>
|
||||
<ion-modal trigger="open-modal">
|
||||
@@ -26,4 +27,93 @@ test.describe('overlays: focus', () => {
|
||||
|
||||
await expect(page.locator('ion-input input')).toBeFocused();
|
||||
});
|
||||
|
||||
test('should not select a hidden focusable element', async ({ page, browserName }) => {
|
||||
await page.setContent(`
|
||||
<style>
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-button id="open-modal">Show Modal</ion-button>
|
||||
<ion-modal trigger="open-modal">
|
||||
<ion-content>
|
||||
<ion-button hidden id="hidden">Hidden Button</ion-button>
|
||||
<ion-button id="visible">Visible Button</ion-button>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
`);
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const presentButton = page.locator('ion-button#open-modal');
|
||||
const visibleButton = page.locator('ion-button#visible');
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
await presentButton.click();
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(visibleButton).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(visibleButton).toBeFocused();
|
||||
});
|
||||
|
||||
test('should not select a disabled focusable element', async ({ page, browserName }) => {
|
||||
await page.setContent(`
|
||||
<ion-button id="open-modal">Show Modal</ion-button>
|
||||
<ion-modal trigger="open-modal">
|
||||
<ion-content>
|
||||
<ion-button disabled="true" id="disabled">Button</ion-button>
|
||||
<ion-button id="active">Active Button</ion-button>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
`);
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const presentButton = page.locator('ion-button#open-modal');
|
||||
const activeButton = page.locator('ion-button#active');
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
await presentButton.click();
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(activeButton).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(activeButton).toBeFocused();
|
||||
});
|
||||
|
||||
test('should select a focusable element with disabled="false"', async ({ page, browserName }) => {
|
||||
await page.setContent(`
|
||||
<ion-button id="open-modal">Show Modal</ion-button>
|
||||
<ion-modal trigger="open-modal">
|
||||
<ion-content>
|
||||
<ion-button disabled="false" id="disabled-false">Button</ion-button>
|
||||
<ion-button id="active">Active Button</ion-button>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
`);
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const presentButton = page.locator('ion-button#open-modal');
|
||||
const disabledFalseButton = page.locator('ion-button#disabled-false');
|
||||
const activeButton = page.locator('ion-button#active');
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
await presentButton.click();
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(disabledFalseButton).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(activeButton).toBeFocused();
|
||||
|
||||
// Loop back to beginning of overlay
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(disabledFalseButton).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { toHaveReceivedEvent } from './toHaveReceivedEvent';
|
||||
import { toHaveReceivedEventDetail } from './toHaveReceivedEventDetail';
|
||||
import { toHaveReceivedEventTimes } from './toHaveReceivedEventTimes';
|
||||
|
||||
export const matchers = {
|
||||
toHaveReceivedEvent,
|
||||
toHaveReceivedEventDetail,
|
||||
toHaveReceivedEventTimes,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { EventSpy } from '../page/event-spy';
|
||||
|
||||
export function toHaveReceivedEventTimes(eventSpy: EventSpy, count: number) {
|
||||
if (!eventSpy) {
|
||||
return {
|
||||
message: () => `toHaveReceivedEventTimes event spy is null`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof (eventSpy as any).then === 'function') {
|
||||
return {
|
||||
message: () =>
|
||||
`expected spy to have received event, but it was not resolved (did you forget an await operator?).`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!eventSpy.eventName) {
|
||||
return {
|
||||
message: () => `toHaveReceivedEventTimes did not receive an event spy`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
|
||||
const pass = eventSpy.length === count;
|
||||
|
||||
return {
|
||||
message: () =>
|
||||
`expected event "${eventSpy.eventName}" to have been called ${count} times, but it was called ${eventSpy.events.length} times`,
|
||||
pass: pass,
|
||||
};
|
||||
}
|
||||
@@ -33,6 +33,12 @@ export const waitForChanges = async (page: Page, timeoutMs = 100) => {
|
||||
const childElm = children[i];
|
||||
const childStencilElm = childElm as HostElement;
|
||||
if (childElm.tagName.includes('-') && typeof childStencilElm.componentOnReady === 'function') {
|
||||
/**
|
||||
* We are only using the lazy loaded bundle
|
||||
* here so we can safely use the
|
||||
* componentOnReady method.
|
||||
*/
|
||||
// eslint-disable-next-line custom-rules/no-component-on-ready-method
|
||||
promises.push(childStencilElm.componentOnReady());
|
||||
}
|
||||
waitComponentOnReady(childElm, promises);
|
||||
|
||||
@@ -8,6 +8,11 @@ interface CustomMatchers<R = unknown> {
|
||||
* @param eventDetail The expected detail of the event.
|
||||
*/
|
||||
toHaveReceivedEventDetail(eventDetail: any): R;
|
||||
|
||||
/**
|
||||
* Will check how many times the event has been received.
|
||||
*/
|
||||
toHaveReceivedEventTimes(count: number): R;
|
||||
}
|
||||
|
||||
declare namespace PlaywrightTest {
|
||||
|
||||
@@ -206,6 +206,7 @@ export const deepReady = async (el: any | undefined): Promise<void> => {
|
||||
const element = el as any;
|
||||
if (element) {
|
||||
if (element.componentOnReady != null) {
|
||||
// eslint-disable-next-line custom-rules/no-component-on-ready-method
|
||||
const stencilEl = await element.componentOnReady();
|
||||
if (stencilEl != null) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user