diff --git a/src/components/ContextMenu.svelte b/src/components/ContextMenu.svelte index 186c2c67d..8fca19189 100644 --- a/src/components/ContextMenu.svelte +++ b/src/components/ContextMenu.svelte @@ -4,6 +4,7 @@ import { autoUpdate, computePosition, flip, shift } from "@floating-ui/dom"; import { onMount } from "svelte"; + import { menuFocusIndex } from "@app/lib/menuNavigation"; import { portal } from "@app/lib/portal"; import { getScrollViewport } from "@app/components/ScrollArea.svelte"; @@ -46,35 +47,14 @@ ); } - function focusItem(index: number) { - const items = menuItems(); - if (items.length === 0) return; - const wrapped = ((index % items.length) + items.length) % items.length; - items[wrapped].focus(); - } - function onMenuKeydown(ev: KeyboardEvent) { const items = menuItems(); if (items.length === 0) return; const current = items.findIndex(el => el === document.activeElement); - switch (ev.key) { - case "ArrowDown": - ev.preventDefault(); - focusItem(current + 1); - break; - case "ArrowUp": - ev.preventDefault(); - focusItem(current - 1); - break; - case "Home": - ev.preventDefault(); - focusItem(0); - break; - case "End": - ev.preventDefault(); - focusItem(items.length - 1); - break; - } + const next = menuFocusIndex(ev.key, current, items.length); + if (next === undefined) return; + ev.preventDefault(); + items[next].focus(); } function onMenuClick(ev: MouseEvent) { diff --git a/src/lib/menuNavigation.ts b/src/lib/menuNavigation.ts new file mode 100644 index 000000000..0f6f0dc13 --- /dev/null +++ b/src/lib/menuNavigation.ts @@ -0,0 +1,19 @@ +export function menuFocusIndex( + key: string, + current: number, + count: number, +): number | undefined { + const wrap = (index: number) => ((index % count) + count) % count; + switch (key) { + case "ArrowDown": + return wrap(current + 1); + case "ArrowUp": + return wrap(current - 1); + case "Home": + return 0; + case "End": + return count - 1; + default: + return undefined; + } +} diff --git a/tests/unit/menuNavigation.test.ts b/tests/unit/menuNavigation.test.ts new file mode 100644 index 000000000..93ba96d12 --- /dev/null +++ b/tests/unit/menuNavigation.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "vitest"; + +import { menuFocusIndex } from "@app/lib/menuNavigation"; + +test.each([ + ["ArrowDown", -1, 0], + ["ArrowDown", 0, 1], + ["ArrowDown", 2, 0], + ["ArrowUp", 0, 2], + ["ArrowUp", 2, 1], + ["Home", 1, 0], + ["End", 0, 2], + ["Enter", 1, undefined], +])("%j from %j of three focuses %j", (key, current, expected) => { + expect(menuFocusIndex(key, current, 3)).toBe(expected); +});