Skip to content

Commit

Permalink
Support exclusion of elements from focusgroups (#50)
Browse files Browse the repository at this point in the history
* Add test cases and examples

* Update docs

* Implement opt-out behavior for focusgroup

* Remove unnecessary handling
  • Loading branch information
echo-vladimir authored Nov 27, 2024
1 parent e13c33f commit 3b2f814
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 12 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 16 additions & 7 deletions focus-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' ||
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions test/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@
margin-bottom: 1em;
}

[focusgroup='none'] {
background-color: #c1c1c1;
color: #4c4c4c;
}

button {
background-color: white;
}
Expand Down
32 changes: 29 additions & 3 deletions test/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,29 @@ const FocusGroupInline: FC = () => {
focusgroup="inline no-memory"
tabIndex={0}
>
<button type="button">Mac</button>
<button
// @ts-expect-error
focusgroup="none"
type="button"
>
Mac
</button>
<button type="button">Windows</button>
<button type="button">Linux</button>
<button
// @ts-expect-error
focusgroup="none"
type="button"
>
Linux
</button>
<button type="button">Android</button>
<button
// @ts-expect-error
focusgroup="none"
type="button"
>
IOS
</button>
</div>
</>
)
Expand All @@ -424,7 +444,13 @@ const FocusGroupBlock: FC = () => {
focusgroup="block wrap"
tabIndex={0}
>
<button type="button">Dog</button>
<button
// @ts-expect-error
focusgroup="none"
type="button"
>
Dog
</button>
<button type="button">Cat</button>
<button type="button">Turtle</button>
</div>
Expand Down
98 changes: 98 additions & 0 deletions test/focus-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
'<div focusgroup="block">' +
'<button role="button" focusgroup="none">Button 1</button>' +
'<button role="button">Button 2</button>' +
'<button role="button" focusgroup="none">Button 3</button>' +
'<button role="button">Button 4</button>' +
'</div>'
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 =
'<div focusgroup="inline wrap">' +
'<button role="button" focusgroup="none">Button 1</button>' +
'<button role="button">Button 2</button>' +
'<button role="button" focusgroup="none">Button 3</button>' +
'<button role="button">Button 4</button>' +
'</div>'
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 =
'<div focusgroup="wrap">' +
'<button role="button" focusgroup="none">Button 1</button>' +
'<button role="button" focusgroup="none">Button 2</button>' +
'<button role="button" focusgroup="none">Button 3</button>' +
'</div>'
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])
})

0 comments on commit 3b2f814

Please # to comment.