From 5d22fa1e27cface8e7c86b68433fd26c4520db60 Mon Sep 17 00:00:00 2001 From: VladBrok Date: Tue, 20 Feb 2024 16:06:47 +0100 Subject: [PATCH 1/8] add search by first character for menuKeyUX --- menu.js | 8 ++++++++ package.json | 2 +- test/demo/index.tsx | 1 + test/menu.test.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/menu.js b/menu.js index 7249633..19a1cfb 100644 --- a/menu.js +++ b/menu.js @@ -44,6 +44,14 @@ export function menuKeyUX() { } else if (event.key === 'End') { event.preventDefault() focus(event.target, items[items.length - 1]) + } else { + let nextItem = Array.from(items).find( + item => item.textContent[0]?.toLowerCase() === event.key.toLowerCase() + ) + if (nextItem) { + event.preventDefault() + focus(event.target, nextItem) + } } } diff --git a/package.json b/package.json index 3e63613..ac00921 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "import": { "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, menuKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }" }, - "limit": "1446 B" + "limit": "1495 B" } ], "clean-publish": { diff --git a/test/demo/index.tsx b/test/demo/index.tsx index 4943aec..89723c3 100644 --- a/test/demo/index.tsx +++ b/test/demo/index.tsx @@ -205,6 +205,7 @@ const App: FC = () => { return ( <> + { equal(window.document.activeElement, items[0]) }) +test('moves focus by typing first letter of an item', () => { + let window = new JSDOM().window + startKeyUX(window, [menuKeyUX()]) + + window.document.body.innerHTML = + '' + let items = window.document.querySelectorAll('a') + items[0].focus() + + press(window, 'a') + equal(window.document.activeElement, items[1]) + + press(window, 'h') + equal(window.document.activeElement, items[0]) + + press(window, 'h') + equal(window.document.activeElement, items[0]) + + press(window, 'C') + equal(window.document.activeElement, items[2]) + + press(window, 'q') + equal(window.document.activeElement, items[2]) +}) + test('supports RTL locales', () => { let window = new JSDOM().window startKeyUX(window, [menuKeyUX()]) From e5c75ac0d19bcf97338536d1f2d918af6266a31c Mon Sep 17 00:00:00 2001 From: VladBrok Date: Tue, 20 Feb 2024 16:11:53 +0100 Subject: [PATCH 2/8] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46cd793..df88f24 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,8 @@ with arrows navigation. ``` -Users will use Tab to go inside menu and will use arrows and Home, -End to navigate inside. +Users will use Tab to go inside menu and will use either arrows and Home, +End or the first letter of each item to navigate inside. To enable this feature call `menuKeyUX`. From 345946e686fd31acde61ef042115561c96172afb Mon Sep 17 00:00:00 2001 From: VladBrok Date: Tue, 20 Feb 2024 16:25:08 +0100 Subject: [PATCH 3/8] cleanup --- menu.js | 2 +- package.json | 2 +- test/demo/index.tsx | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/menu.js b/menu.js index 19a1cfb..d59fb61 100644 --- a/menu.js +++ b/menu.js @@ -46,7 +46,7 @@ export function menuKeyUX() { focus(event.target, items[items.length - 1]) } else { let nextItem = Array.from(items).find( - item => item.textContent[0]?.toLowerCase() === event.key.toLowerCase() + item => item.textContent?.[0]?.toLowerCase() === event.key.toLowerCase() ) if (nextItem) { event.preventDefault() diff --git a/package.json b/package.json index ac00921..a7bae1d 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "import": { "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, menuKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }" }, - "limit": "1495 B" + "limit": "1497 B" } ], "clean-publish": { diff --git a/test/demo/index.tsx b/test/demo/index.tsx index 89723c3..4943aec 100644 --- a/test/demo/index.tsx +++ b/test/demo/index.tsx @@ -205,7 +205,6 @@ const App: FC = () => { return ( <> - Date: Tue, 20 Feb 2024 16:44:58 +0100 Subject: [PATCH 4/8] add .trim() --- menu.js | 2 +- package.json | 2 +- test/menu.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/menu.js b/menu.js index d59fb61..5ea52ee 100644 --- a/menu.js +++ b/menu.js @@ -46,7 +46,7 @@ export function menuKeyUX() { focus(event.target, items[items.length - 1]) } else { let nextItem = Array.from(items).find( - item => item.textContent?.[0]?.toLowerCase() === event.key.toLowerCase() + item => item.textContent?.trim()?.[0]?.toLowerCase() === event.key.toLowerCase() ) if (nextItem) { event.preventDefault() diff --git a/package.json b/package.json index a7bae1d..b0c0196 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "import": { "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, menuKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }" }, - "limit": "1497 B" + "limit": "1500 B" } ], "clean-publish": { diff --git a/test/menu.test.ts b/test/menu.test.ts index c4134b2..58cb5cb 100644 --- a/test/menu.test.ts +++ b/test/menu.test.ts @@ -121,7 +121,7 @@ test('moves focus by typing first letter of an item', () => { '' let items = window.document.querySelectorAll('a') items[0].focus() From 76bc213dda186ed38bcffbd6a9bc5d89585879a9 Mon Sep 17 00:00:00 2001 From: VladBrok Date: Tue, 20 Feb 2024 17:11:54 +0100 Subject: [PATCH 5/8] search menu items by pattern --- index.d.ts | 17 ++++++++++++++++- menu.js | 17 ++++++++++++++--- package.json | 2 +- test/demo/index.tsx | 18 +++++++++++++++--- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/index.d.ts b/index.d.ts index c0e3bd0..d963bab 100644 --- a/index.d.ts +++ b/index.d.ts @@ -28,6 +28,21 @@ export interface KeyUXModule { (window: MinimalWindow): () => void } +export interface MenuKeyUXOptions { + /** + * Maximum allowed pause between key presses when searching for a list item by name. + * Default is 300. + * + * @example + * let's say `typingDelayMs` is equal to 100: + * a — looking for a + * b after 100ms — looking for ab + * c after 50ms — looking for abc + * d after 5000ms — looking for d + */ + typingDelayMs?: number +} + export type HotkeyOverride = Record /** @@ -54,7 +69,7 @@ export function hotkeyKeyUX(overrides?: HotkeyOverride): KeyUXModule * ]) * ``` */ -export function menuKeyUX(): KeyUXModule +export function menuKeyUX(options?: MenuKeyUXOptions): KeyUXModule /** * Add pressed style on button activation from keyboard. diff --git a/menu.js b/menu.js index 5ea52ee..0fe3dd1 100644 --- a/menu.js +++ b/menu.js @@ -1,6 +1,9 @@ -export function menuKeyUX() { +export function menuKeyUX(options) { return window => { let inMenu = false + let typingDelayMs = options?.typingDelayMs || 300 + let lastTimeTyped = 0 + let searchPrefix = '' function focus(current, next) { next.tabIndex = 0 @@ -44,9 +47,17 @@ export function menuKeyUX() { } else if (event.key === 'End') { event.preventDefault() focus(event.target, items[items.length - 1]) - } else { + } else if (event.key.length === 1) { + let curTime = new Date().getTime() + if (curTime - lastTimeTyped <= typingDelayMs) { + searchPrefix += event.key.toLowerCase() + } else { + searchPrefix = event.key.toLowerCase() + } + lastTimeTyped = curTime + let nextItem = Array.from(items).find( - item => item.textContent?.trim()?.[0]?.toLowerCase() === event.key.toLowerCase() + item => item.textContent?.trim()?.toLowerCase()?.startsWith(searchPrefix) ) if (nextItem) { event.preventDefault() diff --git a/package.json b/package.json index b0c0196..755a61c 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "import": { "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, menuKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }" }, - "limit": "1500 B" + "limit": "1592 B" } ], "clean-publish": { diff --git a/test/demo/index.tsx b/test/demo/index.tsx index 4943aec..cebb44f 100644 --- a/test/demo/index.tsx +++ b/test/demo/index.tsx @@ -92,7 +92,7 @@ const Menu: FC<{ router: string; setRouter: (value: string) => void }> = ({ hotkey="a" route="about" router={router} - routes={['about', 'history', 'team']} + routes={['about', 'history', 'team', 'treasury']} setRouter={setRouter} /> ) - } else if (router === 'about' || router === 'history' || router === 'team') { + } else if ( + router === 'about' || + router === 'history' || + router === 'team' || + router === 'treasury' + ) { content = ( <>
+
- {router === 'history' || router === 'team' ? ( + {router === 'history' || router === 'team' || router === 'treasury' ? (

The {router} page{' '} Date: Tue, 20 Feb 2024 17:27:03 +0100 Subject: [PATCH 6/8] add tests for searching list items by pattern --- test/menu.test.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/test/menu.test.ts b/test/menu.test.ts index 58cb5cb..035ec2a 100644 --- a/test/menu.test.ts +++ b/test/menu.test.ts @@ -1,6 +1,7 @@ import { type DOMWindow, JSDOM } from 'jsdom' import { equal } from 'node:assert' import { test } from 'node:test' +import { setTimeout } from 'node:timers/promises' import { menuKeyUX, startKeyUX } from '../index.js' @@ -113,15 +114,18 @@ test('supports horizontal menus', () => { equal(window.document.activeElement, items[0]) }) -test('moves focus by typing first letter of an item', () => { +test('moves focus by typing item name', async () => { let window = new JSDOM().window - startKeyUX(window, [menuKeyUX()]) + startKeyUX(window, [menuKeyUX({ + typingDelayMs: 100 + })]) window.document.body.innerHTML = '

' let items = window.document.querySelectorAll('a') items[0].focus() @@ -129,17 +133,27 @@ test('moves focus by typing first letter of an item', () => { press(window, 'a') equal(window.document.activeElement, items[1]) - press(window, 'h') - equal(window.document.activeElement, items[0]) + await setTimeout(50) + press(window, 'G') + equal(window.document.activeElement, items[2]) + await setTimeout(100) press(window, 'h') equal(window.document.activeElement, items[0]) - - press(window, 'C') - equal(window.document.activeElement, items[2]) - - press(window, 'q') - equal(window.document.activeElement, items[2]) + + press(window, 'a') + equal(window.document.activeElement, items[0]) + + await setTimeout(100) + press(window, 'a') + equal(window.document.activeElement, items[1]) + + await setTimeout(100) + press(window, 'Backspace') + equal(window.document.activeElement, items[1]) + + press(window, 'b') + equal(window.document.activeElement, items[3]) }) test('supports RTL locales', () => { From 7691dec8227da62f3962bbc6f5b9be1d7c9022ef Mon Sep 17 00:00:00 2001 From: VladBrok Date: Tue, 20 Feb 2024 17:28:23 +0100 Subject: [PATCH 7/8] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df88f24..23fdcda 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ with arrows navigation. ``` Users will use Tab to go inside menu and will use either arrows and Home, -End or the first letter of each item to navigate inside. +End or a name of some an item to navigate inside. To enable this feature call `menuKeyUX`. From 2edf2486cbfe8997b8866386ff275a91ba713e8b Mon Sep 17 00:00:00 2001 From: VladBrok Date: Tue, 20 Feb 2024 17:30:38 +0100 Subject: [PATCH 8/8] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23fdcda..447b5d9 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ with arrows navigation. ``` Users will use Tab to go inside menu and will use either arrows and Home, -End or a name of some an item to navigate inside. +End or a name of some item to navigate inside. To enable this feature call `menuKeyUX`.