Skip to content

Commit

Permalink
Merge pull request #2 from VladBrok/add-menu-search
Browse files Browse the repository at this point in the history
Adds search by list item name to `menuKeyUX`
  • Loading branch information
ai authored Feb 20, 2024
2 parents 6d3cadd + 2edf248 commit 9e1132d
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 8 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ with arrows navigation.
</nav>
```

Users will use Tab to go inside menu and will use arrows and <kbd>Home</kbd>,
<kbd>End</kbd> to navigate inside.
Users will use Tab to go inside menu and will use either arrows and <kbd>Home</kbd>,
<kbd>End</kbd> or a name of some item to navigate inside.

To enable this feature call `menuKeyUX`.

Expand Down
17 changes: 16 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>

/**
Expand All @@ -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.
Expand Down
21 changes: 20 additions & 1 deletion menu.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -44,6 +47,22 @@ export function menuKeyUX() {
} else if (event.key === 'End') {
event.preventDefault()
focus(event.target, items[items.length - 1])
} 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()?.toLowerCase()?.startsWith(searchPrefix)
)
if (nextItem) {
event.preventDefault()
focus(event.target, nextItem)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"import": {
"./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, menuKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }"
},
"limit": "1446 B"
"limit": "1592 B"
}
],
"clean-publish": {
Expand Down
18 changes: 15 additions & 3 deletions test/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
<MenuItem
Expand Down Expand Up @@ -130,7 +130,12 @@ const Page: FC<{
</ul>
</>
)
} else if (router === 'about' || router === 'history' || router === 'team') {
} else if (
router === 'about' ||
router === 'history' ||
router === 'team' ||
router === 'treasury'
) {
content = (
<>
<div
Expand All @@ -156,8 +161,15 @@ const Page: FC<{
setRouter={setRouter}
tabIndex={-1}
/>
<MenuItem
controls="about_subpage"
route="treasury"
router={router}
setRouter={setRouter}
tabIndex={-1}
/>
</div>
{router === 'history' || router === 'team' ? (
{router === 'history' || router === 'team' || router === 'treasury' ? (
<p id="about_subpage">
The {router} page{' '}
<a
Expand Down
43 changes: 43 additions & 0 deletions test/menu.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -113,6 +114,48 @@ test('supports horizontal menus', () => {
equal(window.document.activeElement, items[0])
})

test('moves focus by typing item name', async () => {
let window = new JSDOM().window
startKeyUX(window, [menuKeyUX({
typingDelayMs: 100
})])

window.document.body.innerHTML =
'<nav role="menu">' +
'<a href="#" role="menuitem">HOME</a>' +
'<a href="#" role="menuitem">About</a>' +
'<a href="#" role="menuitem"> agh </a>' +
'<a href="#" role="menuitem">Backspace</a>' +
'</nav>'
let items = window.document.querySelectorAll('a')
items[0].focus()

press(window, 'a')
equal(window.document.activeElement, items[1])

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, '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', () => {
let window = new JSDOM().window
startKeyUX(window, [menuKeyUX()])
Expand Down

0 comments on commit 9e1132d

Please sign in to comment.