fix(popover): fix keyboard arrow navigation (#23709)

resolves #23512
This commit is contained in:
William Martin
2021-08-03 13:43:41 -04:00
committed by GitHub
parent fe2810b227
commit f2e7a26797
3 changed files with 65 additions and 13 deletions

View File

@@ -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. |

View File

@@ -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 () => {

View File

@@ -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;
/**