From 3b2f814f3de63556a609c1892e6bf8b3caf6cf45 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 27 Nov 2024 23:19:12 +0100 Subject: [PATCH] Support exclusion of elements from focusgroups (#50) * Add test cases and examples * Update docs * Implement opt-out behavior for focusgroup * Remove unnecessary handling --- README.md | 3 +- focus-group.js | 23 +++++++--- package.json | 2 +- test/demo/index.html | 5 ++ test/demo/index.tsx | 32 +++++++++++-- test/focus-group.test.ts | 98 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a58b607..b18a474 100644 --- a/README.md +++ b/README.md @@ -307,8 +307,9 @@ Key UX supports (you can combine these features): - `focusgroup="block"` for vertical arrows. - `focusgroup="no-memory"` to not restore last focus position. - `focusgroup="wrap"` enables cyclic focus movement within a group. +- `focusgroup="none"` excludes element from arrow key navigation. -Key UX doesn’t support `none` and `grid` features. +Key UX doesn’t support `grid` feature. ### Menu diff --git a/focus-group.js b/focus-group.js index 622218c..79621ec 100644 --- a/focus-group.js +++ b/focus-group.js @@ -13,7 +13,7 @@ function focus(current, next) { } function findGroupNodeByEventTarget(target) { - let fg = target.closest('[focusgroup]') + let fg = target.closest('[focusgroup]:not([focusgroup="none"])') if (fg) return fg let itemRole = target.role || target.type || target.tagName @@ -36,7 +36,7 @@ function getItems(target, group) { } function getToolbarItems(group) { - let items = [...group.querySelectorAll('*')] + let items = [...group.querySelectorAll('*:not([focusgroup="none"])')] return items.filter(item => { return ( item.role === 'button' || @@ -150,11 +150,20 @@ export function focusGroupKeyUX(options) { inGroup = true window.addEventListener('keydown', keyDown) } - let items = getItems(event.target, group) - for (let item of items) { - if (item !== event.target) { - item.setAttribute('tabindex', -1) - } + + let items = Array.from(getItems(event.target, group)) + if ( + !items.some(item => item.getAttribute('tabindex') === '0') && + group.hasAttribute('focusgroup') + ) { + items.forEach((item, idx) => + item.setAttribute('tabindex', idx === 0 ? 0 : -1) + ) + items[0]?.focus() + } else { + items.forEach(item => { + if (item !== event.target) item.setAttribute('tabindex', -1) + }) } } else if (inGroup) { stop() diff --git a/package.json b/package.json index 73becc3..9a6794c 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "import": { "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, focusGroupKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint, hotkeyOverrides, hotkeyMacCompat }" }, - "limit": "2309 B" + "limit": "2366 B" } ], "clean-publish": { diff --git a/test/demo/index.html b/test/demo/index.html index ab76455..b341e27 100644 --- a/test/demo/index.html +++ b/test/demo/index.html @@ -137,6 +137,11 @@ margin-bottom: 1em; } + [focusgroup='none'] { + background-color: #c1c1c1; + color: #4c4c4c; + } + button { background-color: white; } diff --git a/test/demo/index.tsx b/test/demo/index.tsx index 38334b9..75b40ce 100644 --- a/test/demo/index.tsx +++ b/test/demo/index.tsx @@ -407,9 +407,29 @@ const FocusGroupInline: FC = () => { focusgroup="inline no-memory" tabIndex={0} > - + - + + + ) @@ -424,7 +444,13 @@ const FocusGroupBlock: FC = () => { focusgroup="block wrap" tabIndex={0} > - + diff --git a/test/focus-group.test.ts b/test/focus-group.test.ts index 9071d00..5bae2e0 100644 --- a/test/focus-group.test.ts +++ b/test/focus-group.test.ts @@ -648,3 +648,101 @@ test('enabling wrap behaviors in focusgroup', () => { press(window, 'ArrowLeft') equal(window.document.activeElement, buttons[2]) }) + +test('disabling focusgroup=none elements', () => { + let window = new JSDOM().window + startKeyUX(window, [focusGroupKeyUX()]) + window.document.body.innerHTML = + '
' + + '' + + '' + + '' + + '' + + '
' + let buttons = window.document.querySelectorAll('button') + buttons[1].focus() + + equal(window.document.activeElement, buttons[1]) + + press(window, 'ArrowDown') + equal(window.document.activeElement, buttons[3]) + + press(window, 'ArrowUp') + equal(window.document.activeElement, buttons[1]) + + press(window, 'End') + equal(window.document.activeElement, buttons[3]) + + press(window, 'Home') + equal(window.document.activeElement, buttons[1]) + + press(window, 'ArrowLeft') + equal(window.document.activeElement, buttons[1]) + + press(window, 'ArrowRight') + equal(window.document.activeElement, buttons[1]) +}) + +test('focusgroup=none with wrap behavior', () => { + let window = new JSDOM().window + startKeyUX(window, [focusGroupKeyUX()]) + window.document.body.innerHTML = + '
' + + '' + + '' + + '' + + '' + + '
' + let buttons = window.document.querySelectorAll('button') + buttons[1].focus() + + equal(window.document.activeElement, buttons[1]) + + press(window, 'ArrowLeft') + equal(window.document.activeElement, buttons[3]) + + press(window, 'End') + equal(window.document.activeElement, buttons[3]) + + press(window, 'ArrowRight') + equal(window.document.activeElement, buttons[1]) + + press(window, 'Home') + equal(window.document.activeElement, buttons[1]) + + press(window, 'ArrowLeft') + equal(window.document.activeElement, buttons[3]) + + press(window, 'ArrowDown') + equal(window.document.activeElement, buttons[3]) + + press(window, 'ArrowUp') + equal(window.document.activeElement, buttons[3]) +}) + +test('skips all lements with focusgroup=none', () => { + let window = new JSDOM().window + startKeyUX(window, [focusGroupKeyUX()]) + window.document.body.innerHTML = + '
' + + '' + + '' + + '' + + '
' + let buttons = window.document.querySelectorAll('button') + buttons[0].focus() + + equal(window.document.activeElement, buttons[0]) + + press(window, 'ArrowRight') + equal(window.document.activeElement, buttons[0]) + + press(window, 'ArrowLeft') + equal(window.document.activeElement, buttons[0]) + + press(window, 'Home') + equal(window.document.activeElement, buttons[0]) + + press(window, 'End') + equal(window.document.activeElement, buttons[0]) +})