diff --git a/README.md b/README.md index a4f0a49..d702982 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ import { hiddenKeyUX, hotkeyKeyUX, jumpKeyUX, - menuKeyUX, + focusGroupKeyUX, pressKeyUX, startKeyUX } from 'keyux' @@ -55,7 +55,7 @@ const overrides = {} startKeyUX(window, [ hotkeyKeyUX(overrides), - menuKeyUX(), + focusGroupKeyUX(), pressKeyUX('is-pressed'), jumpKeyUX(), hiddenKeyUX() @@ -217,13 +217,69 @@ with arrow navigation. Users will use Tab to get inside the menu, and will use either arrows or Home, End or an item name to navigate inside. -To enable this feature, call `menuKeyUX`. +To enable this feature, call `focusGroupKeyUX`. ```js -import { menuKeyUX } from 'keyux' +import { focusGroupKeyUX } from 'keyux' startKeyUX(window, [ - menuKeyUX() + focusGroupKeyUX() +]) +``` + + +### Listbox + +The [`role="listbox"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role) +is used for lists from which a user may select one or +more items which are static and, unlike HTML ` to make the focus jump on Enter { - let inMenu = false + let inGroup = false let typingDelayMs = options?.searchDelayMs || 300 let lastTyped = 0 let searchPrefix = '' @@ -11,21 +17,41 @@ export function menuKeyUX(options) { current.tabIndex = -1 } + function findGroupNodeByEventTarget(eventTarget) { + let itemRole = eventTarget.role + let groupRoles = ROLES[itemRole] + if (!groupRoles) return null + + for (let role of groupRoles) { + let node = eventTarget.closest(`[role=${role}]`) + if (node) return node + } + } + + function isHorizontalOrientation(group) { + let ariaOrientation = group.getAttribute('aria-orientation') + if (ariaOrientation === "vertical") return false + if (ariaOrientation === "horizontal") return true + + let role = group.role + return role === "menubar" || role === "tablist"; + } + + function keyDown(event) { - if (event.target.role !== 'menuitem') { + let group = findGroupNodeByEventTarget(event.target); + + if (!group) { stop() return } - let menu = event.target.closest('[role="menu"]') - if (!menu) return - - let items = menu.querySelectorAll('[role="menuitem"]') + let items = group.querySelectorAll(`[role=${event.target.role}]`) let index = Array.from(items).indexOf(event.target) let nextKey = 'ArrowDown' let prevKey = 'ArrowUp' - if (menu.getAttribute('aria-orientation') === 'horizontal') { + if (isHorizontalOrientation(group)) { if (window.document.dir === 'rtl') { nextKey = 'ArrowLeft' prevKey = 'ArrowRight' @@ -47,7 +73,7 @@ export function menuKeyUX(options) { } else if (event.key === 'End') { event.preventDefault() focus(event.target, items[items.length - 1]) - } else if (event.key.length === 1) { + } else if (event.key.length === 1 && group.role !== "tablist") { let now = Date.now() if (now - lastTyped <= typingDelayMs) { searchPrefix += event.key.toLowerCase() @@ -70,26 +96,25 @@ export function menuKeyUX(options) { } function stop() { - inMenu = false + inGroup = false window.removeEventListener('keydown', keyDown) } function focusIn(event) { - if (event.target.role === 'menuitem') { - let menu = event.target.closest('[role="menu"]') - if (!menu) return + let group = findGroupNodeByEventTarget(event.target); + if (group) { - if (!inMenu) { - inMenu = true + if (!inGroup) { + inGroup = true window.addEventListener('keydown', keyDown) } - let items = menu.querySelectorAll('[role="menuitem"]') + let items = group.querySelectorAll(`[role=${event.target.role}]`) for (let item of items) { if (item !== event.target) { item.setAttribute('tabindex', -1) } } - } else if (inMenu) { + } else if (inGroup) { stop() } } diff --git a/index.d.ts b/index.d.ts index d3604b3..049c131 100644 --- a/index.d.ts +++ b/index.d.ts @@ -28,7 +28,7 @@ export interface KeyUXModule { (window: MinimalWindow): () => void } -export interface MenuKeyUXOptions { +export interface FocusGroupKeyUXOptions { /** * Maximum allowed pause between key presses when searching * for a list item by name. Default is 300. @@ -55,14 +55,14 @@ export function hotkeyKeyUX(overrides?: HotkeyOverride): KeyUXModule * Add arrow-navigation on `role="menu"`. * * ```js - * import { startKeyUX, menuKeyUX } from 'keyux' + * import { startKeyUX, focusGroupKeyUX } from 'keyux' * * startKeyUX(window, [ - * menuKeyUX() + * focusGroupKeyUX() * ]) * ``` */ -export function menuKeyUX(options?: MenuKeyUXOptions): KeyUXModule +export function focusGroupKeyUX(options?: FocusGroupKeyUXOptions): KeyUXModule /** * Add pressed style on button activation from keyboard. @@ -111,7 +111,7 @@ export function hiddenKeyUX(): KeyUXModule * import { * startKeyUX, * hotkeyKeyUX, - * menuKeyUX, + * focusGroupKeyUX, * pressKeyUX, * jumpKeyUX, * hiddenKeyUX @@ -119,7 +119,7 @@ export function hiddenKeyUX(): KeyUXModule * * startKeyUX(window, [ * hotkeyKeyUX(), - * menuKeyUX(), + * focusGroupKeyUX(), * pressKeyUX('is-pressed'), * jumpKeyUX(), * hiddenKeyUX() diff --git a/index.js b/index.js index 875b9a3..f97b57f 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ +export * from './focus-group.js' export * from './hotkey.js' export * from './hidden.js' export * from './press.js' -export * from './menu.js' export * from './jump.js' export function startKeyUX(window, plugins) { diff --git a/package.json b/package.json index e721ae3..d918cdd 100644 --- a/package.json +++ b/package.json @@ -83,9 +83,9 @@ { "name": "All modules", "import": { - "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, menuKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }" + "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, focusGroupKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }" }, - "limit": "1737 B" + "limit": "1853 B" } ], "clean-publish": { diff --git a/test/demo/index.html b/test/demo/index.html index 9621880..48b2446 100644 --- a/test/demo/index.html +++ b/test/demo/index.html @@ -98,6 +98,19 @@ border-radius: 2px; margin: 0.5em 0; } + .tablist_container { + margin-top: 2em; + margin-bottom: 2em; + } + .tablist_tab { + margin-right: 0.5em; + } + .tabcontent { + display: none; + } + .tabcontent--current { + display: block; + } diff --git a/test/demo/index.tsx b/test/demo/index.tsx index 3defb7b..f341900 100644 --- a/test/demo/index.tsx +++ b/test/demo/index.tsx @@ -3,12 +3,12 @@ import { createRoot } from 'react-dom/client' import type { HotkeyOverride } from '../../index.js' import { + focusGroupKeyUX, getHotKeyHint, hiddenKeyUX, hotkeyKeyUX, jumpKeyUX, likelyWithKeyboard, - menuKeyUX, pressKeyUX, startKeyUX } from '../../index.js' @@ -17,7 +17,7 @@ let overrides: HotkeyOverride = {} startKeyUX(window, [ hotkeyKeyUX(overrides), - menuKeyUX(), + focusGroupKeyUX(), pressKeyUX('is-pressed'), jumpKeyUX(), hiddenKeyUX() @@ -74,6 +74,36 @@ const Counter: FC = () => { ) } +const Tablist: FC = () => { + let [tab, setTab] = useState("Home") + return ( +
+ Tablist: + +
+ + + +
+ +
+

Home Content

+ +
+ +
+

About Content

+ +
+ +
+

Contact Content

+ +
+
+ ) +} + const Menu: FC<{ router: string; setRouter: (value: string) => void }> = ({ router, setRouter @@ -130,14 +160,14 @@ const Page: FC<{ content = ( <> -