Skip to content

fix: treat items disabled by a fieldset as disabled - #43

Merged
mixedrays merged 1 commit into
developfrom
fix/fieldset-disabled-item
Nov 6, 2026
Merged

mixedrays merged 1 commit into
developfrom
fix/fieldset-disabled-item

Conversation

@mixedrays

Copy link
Copy Markdown
Owner

Summary

This brings the task 036 fix (a47c158) into develop. #42 merged it into main only. The commit is unchanged, and it is the branch's only commit ahead of develop.

A control inside <fieldset disabled> matches :disabled but doesn't have its own disabled attribute. keyrove checked only the attribute, so the control stayed in the item sequence. Each arrow move onto it was refused by focus() and ended as a consumed no-op, so arrows couldn't reach the items after it.

<button id="a" data-keyrove-item>A</button>
<fieldset disabled>
  <button id="b" data-keyrove-item>B</button>   <!-- b.matches(':disabled') === true -->
</fieldset>
<button id="c" data-keyrove-item>C</button>
 // packages/keyrove/src/group.ts
-const isDisabled = (item: Element) => item.hasAttribute('disabled');
+export const isDisabled = (element: Element) =>
+  element.hasAttribute('disabled') || element.matches(':disabled');
isDisabled
├── group.items           → item sequence, typeahead candidates
├── group.isNavigable
├── stop holder           → initRovingTabindex, land
└── readFocusKeys filter  (config.ts, was its own hasAttribute check)
  • The disabled attribute still counts on any element, such as <li disabled>.
  • A focusable non-control inside a disabled fieldset, such as <div tabindex="-1">, is still an item.
  • aria-disabled still doesn't count.
  • Docs updated: GLOSSARY.md, docs/glossary.md, docs/api.md (Prettier re-padded its attribute table), examples/skipped-items.md, examples/typeahead.md, and the types.ts doc comments.

Evidence

  • Before: the new jsdom tests against the old group.ts and config.ts: 6 of 7 fail. The seventh is a guard for the <div tabindex="-1"> case and passes either way.

    navigation   A → ArrowDown            expect focus on C     ✗ got A
    navigation   disable, then re-enable  expect B back         ✗
    focusKey     attribute on B           expect key untouched  ✗
    focusKey     focusKeys map names B    expect key untouched  ✗
    typeahead    type "b"                 expect null           ✗
    initRoving   B never gets the stop                          ✗
    

    After: library 561/561, bench 19, docs 19, browser suite 21, typecheck and Prettier all pass.

  • Benchmark (pnpm bench:headless, M1 Pro, Chrome 156 headless): +4.7% geometric mean across keyRove cases, unbound keys unchanged, at most about 1 µs more per bound keypress. Full numbers are in fix: treat items disabled by a fieldset as disabled #42.

Merge Danger

Door: two-way

The change is one predicate with no stored state, and merging into develop cuts no release. main already has a47c158, so the next develop → main merge sees the same commit and doesn't conflict over it.

Blast Radius: behavioural

  • Items inside a <fieldset disabled> leave the item sequence. Arrows, Home/End, typeahead and focus keys skip them, as the README already promised.
  • In a grid, the cells after a removed item shift, as they already do for the disabled attribute.
  • Branches cut from develop before this merges don't have the fix.

🤖 Generated with Claude Code

keyrove read "disabled" only from an element's own `disabled` attribute,
so a button inside `<fieldset disabled>` stayed in the item sequence.
Every arrow move onto it was refused by focus() and became a consumed
no-op, so the items after it could not be reached, typeahead matched it,
a focus key declared on it swallowed the key, and initRovingTabindex
could give it the stop.

An element is now disabled when it carries `disabled` or matches
`:disabled`. The item sequence, the navigable test, the stop holder and
the focus-key filter all use the same check. `aria-disabled` still does
not count. A focusable non-control inside a disabled fieldset stays an
item, since `:disabled` matches only form controls.
@mixedrays
mixedrays marked this pull request as ready for review November 6, 2026 14:53
@mixedrays
mixedrays merged commit d71bcb9 into develop Nov 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant