Skip to content

Commit

Permalink
feat(sbb-menu): improvements on arrow navigation (#3341)
Browse files Browse the repository at this point in the history
  • Loading branch information
DavideMininni-Fincons authored Jan 14, 2025
1 parent 174395c commit 795d94d
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 4 deletions.
57 changes: 57 additions & 0 deletions src/elements/menu/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,63 @@ describe(`sbb-menu`, () => {
expect(element).to.have.attribute('data-state', 'closed');
});

it('keyboard navigation', async () => {
const didOpenEventSpy = new EventSpy(SbbMenuElement.events.didOpen, element);
trigger.click();
await waitForLitRender(element);
await didOpenEventSpy.calledOnce();
expect(didOpenEventSpy.count).to.be.equal(1);
expect(element).to.have.attribute('data-state', 'opened');

// First element focused by default.
expect(document.activeElement!.id).to.be.equal('menu-link');

// Pressing an invalid key would not change the focus
await sendKeys({ press: 'A' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-link');

// Move down with down arrow
await sendKeys({ press: 'ArrowDown' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-1');

// Move down with right arrow; menu-action-2 is disabled, so the next focusable is menu-action-3
await sendKeys({ press: 'ArrowRight' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-3');

// Move up with left arrow
await sendKeys({ press: 'ArrowLeft' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-1');

// Move up with left arrow will go to the last element due wrap
await sendKeys({ press: 'ArrowUp' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-4');

// Move to first
await sendKeys({ press: 'PageUp' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-1');

// Move to last
await sendKeys({ press: 'PageDown' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-4');

// Move to first
await sendKeys({ press: 'Home' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-1');

// Move to last
await sendKeys({ press: 'End' });
await waitForLitRender(element);
expect(document.activeElement!.id).to.be.equal('menu-action-4');
});

it('closes on menu action click', async () => {
const willOpenEventSpy = new EventSpy(SbbMenuElement.events.willOpen, element);
const didOpenEventSpy = new EventSpy(SbbMenuElement.events.didOpen, element);
Expand Down
30 changes: 26 additions & 4 deletions src/elements/menu/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getNextElementIndex,
interactivityChecker,
IS_FOCUSABLE_QUERY,
isArrowKeyPressed,
isArrowKeyOrPageKeysPressed,
SbbFocusHandler,
setModalityOnNextFocus,
} from '../../core/a11y.js';
Expand Down Expand Up @@ -211,7 +211,7 @@ class SbbMenuElement extends SbbNamedSlotListMixin<
}

private _handleKeyDown(evt: KeyboardEvent): void {
if (!isArrowKeyPressed(evt)) {
if (!isArrowKeyOrPageKeysPressed(evt)) {
return;
}
evt.preventDefault();
Expand All @@ -223,9 +223,31 @@ class SbbMenuElement extends SbbNamedSlotListMixin<
).filter(
(el) => (!el.disabled || el.disabledInteractive) && interactivityChecker.isVisible(el),
);

const current = enabledActions.findIndex((e: Element) => e === evt.target);
const nextIndex = getNextElementIndex(evt, current, enabledActions.length);

let nextIndex;
switch (evt.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
nextIndex = getNextElementIndex(evt, current, enabledActions.length);
break;

case 'PageUp':
case 'Home':
nextIndex = 0;
break;

case 'End':
case 'PageDown':
nextIndex = enabledActions.length - 1;
break;

// this should never happen since all the case allowed by `isArrowKeyOrPageKeysPressed` should be covered
default:
nextIndex = 0;
}

(enabledActions[nextIndex] as HTMLElement).focus();
}
Expand Down
3 changes: 3 additions & 0 deletions src/elements/menu/menu/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ As the menu opens, the focus will automatically be set to the first focusable it
When using the `sbb-menu` as a select (e.g. language selection) it's recommended to use the `aria-pressed` attribute
to identify which actions are active and which are not.

It is possible to navigate the slotted `sbb-menu-button`/`sbb-menu-link` via keyboard using arrow keys or page keys
(<kbd>Home</kbd>, <kbd>PageUp</kbd>, <kbd>End</kbd> and <kbd>PageDown</kbd>).

<!-- Auto Generated Below -->

## Properties
Expand Down

0 comments on commit 795d94d

Please sign in to comment.