mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
@@ -176,6 +176,8 @@ type PositionAlign = 'start' | 'center' | 'end';
|
||||
| ------------------ | -------------------------------------------------------------- |
|
||||
| `ArrowUp` | Moves focus to the previous focusable element. |
|
||||
| `ArrowDown` | Moves focus to the next focusable element. |
|
||||
| `Home` | Moves focus to the first focusable element. |
|
||||
| `End` | Moves focus to the last focusable element. |
|
||||
| `ArrowLeft` | When used in a child popover, closes the popover and returns focus to the parent popover. |
|
||||
| `Space`, `Enter`, and `ArrowRight` | When focusing a trigger element, opens the associated popover. |
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
/**
|
||||
* Focusing happens async inside of popover so we need
|
||||
* to wait for the requestAnimationFrame to fire.
|
||||
*/
|
||||
const expectActiveElementTextToEqual = async (page, textValue) => {
|
||||
await page.waitFor((text) => document.activeElement.textContent === text, {}, textValue)
|
||||
}
|
||||
|
||||
test('popover: focus trap', async () => {
|
||||
@@ -21,20 +24,33 @@ test('popover: focus trap', async () => {
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Item 0');
|
||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Item 3');
|
||||
await expectActiveElementTextToEqual(page, 'Item 3');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Item 0');
|
||||
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 () => {
|
||||
|
||||
@@ -314,6 +314,16 @@ export const getPrevItem = (items: HTMLIonItemElement[], currentItem: HTMLElemen
|
||||
return items[currentItemIndex - 1];
|
||||
}
|
||||
|
||||
/** Focus the internal button of the ion-item */
|
||||
const focusItem = (item: HTMLIonItemElement) => {
|
||||
const root = getElementRoot(item);
|
||||
const button = root.querySelector('button');
|
||||
|
||||
if (button) {
|
||||
raf(() => button.focus());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if `el` has been designated
|
||||
* as a trigger element for an ion-popover.
|
||||
@@ -326,7 +336,7 @@ export const configureKeyboardInteraction = (
|
||||
|
||||
const callback = async (ev: KeyboardEvent) => {
|
||||
const activeElement = document.activeElement as HTMLElement | null;
|
||||
let items = [] as any;
|
||||
let items: HTMLIonItemElement[] = [];
|
||||
|
||||
/**
|
||||
* Complex selectors with :not() are :not supported
|
||||
@@ -339,7 +349,7 @@ export const configureKeyboardInteraction = (
|
||||
* Select all ion-items that are not children of child popovers.
|
||||
* i.e. only select ion-item elements that are part of this popover
|
||||
*/
|
||||
items = Array.from(popoverEl.querySelectorAll('ion-item:not(ion-popover ion-popover *)'));
|
||||
items = Array.from(popoverEl.querySelectorAll('ion-item:not(ion-popover ion-popover *):not([disabled])') as NodeListOf<HTMLIonItemElement>);
|
||||
/* tslint:disable-next-line */
|
||||
} catch {}
|
||||
|
||||
@@ -362,20 +372,44 @@ export const configureKeyboardInteraction = (
|
||||
* ArrowDown should move focus to the next focusable ion-item.
|
||||
*/
|
||||
case 'ArrowDown':
|
||||
ev.preventDefault();
|
||||
const nextItem = getNextItem(items, activeElement);
|
||||
// tslint:disable-next-line:strict-type-predicates
|
||||
if (nextItem !== undefined) {
|
||||
nextItem.focus();
|
||||
focusItem(nextItem);
|
||||
}
|
||||
break;
|
||||
/**
|
||||
* ArrowUp should move focus to the previous focusable ion-item.
|
||||
*/
|
||||
case 'ArrowUp':
|
||||
ev.preventDefault();
|
||||
const prevItem = getPrevItem(items, activeElement);
|
||||
// tslint:disable-next-line:strict-type-predicates
|
||||
if (prevItem !== undefined) {
|
||||
prevItem.focus();
|
||||
focusItem(prevItem);
|
||||
}
|
||||
break;
|
||||
/**
|
||||
* Home should move focus to the first focusable ion-item.
|
||||
*/
|
||||
case 'Home':
|
||||
ev.preventDefault();
|
||||
const firstItem = items[0];
|
||||
// tslint:disable-next-line:strict-type-predicates
|
||||
if (firstItem !== undefined) {
|
||||
focusItem(firstItem);
|
||||
}
|
||||
break;
|
||||
/**
|
||||
* End should move focus to the last focusable ion-item.
|
||||
*/
|
||||
case 'End':
|
||||
ev.preventDefault();
|
||||
const lastItem = items[items.length - 1];
|
||||
// tslint:disable-next-line:strict-type-predicates
|
||||
if (lastItem !== undefined) {
|
||||
focusItem(lastItem);
|
||||
}
|
||||
break;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user