Skip to content

Release - #51

Open
mixedrays wants to merge 22 commits into
mainfrom
develop
Open

mixedrays wants to merge 22 commits into
mainfrom
develop

Conversation

@mixedrays

Copy link
Copy Markdown
Owner

No description provided.

mixedrays and others added 22 commits November 6, 2026 14:26
A listener outside a shadow root sees a press from a web component's input
retargeted to the shadow host, so the editable-target guard in keyRove and
createTypeahead saw a plain element: arrows, Home/End and typed characters
were consumed as moves and never reached the field.

The guard now also checks the element the press began on, the first entry
of the composed path, read off nativeEvent for React's synthetic events.
Group resolution still uses the retargeted target. KeyRoveEvent gains two
optional members, composedPath and nativeEvent.
fix: treat items disabled by a fieldset as disabled
fix: leave keys typed into a field inside a shadow root to the field
A move whose target refused focus (hidden, inert, display: none,
visibility: hidden, inside a closed <details>) was a consumed no-op, so
one such item in the middle of a group stopped the arrows from ever
reaching the items past it.

The move is now resolved again with the refused item passed over, until
focus lands or nothing is left. keyRove, rove and typeahead walk on;
focus keys and exit/enter, which name one destination, do not. The
roving stop is put back on every item passed over, onMove fires once,
and a move where nothing takes focus is the consumed no-op it was. A
move whose first target takes focus costs the same as before.

skip stays the recommended way to hide items: a run of 1,000 refusing
items costs about 7 ms per press, against 0.3 ms when they are skipped.
pnpm bench:headless removed Chrome's profile while Chrome was still
writing it after Browser.close. The removal threw ENOTEMPTY from
runHarness's finally block and replaced the finished results, so the
runner printed no report, wrote no --json file and exited 1.

- Move Chrome handling into scripts/chrome.ts. withChrome runs a body
  in a fresh profile, waits up to 5 s for Chrome to exit, kills it with
  SIGKILL if it hasn't, then removes the profile.
- Removing the profile or the scratch directory retries, and if it
  still fails it warns with the leftover path instead of throwing. A
  finished run keeps its report, and a cleanup error never hides a
  harness error.
- Start cleanup as soon as the preview server is up, so a failed launch
  or DevTools connection also stops Chrome and the server. Pending
  DevTools calls now fail when the socket closes.
- Test the cleanup with fake Chrome executables.
… ES modules

The exports map offered only the types and import conditions, so
require('@mixedrays/keyrove') failed with ERR_PACKAGE_PATH_NOT_EXPORTED
before Node could load the file, even on Node versions that load ES
modules through require(). A default condition points require, and any
resolver with its own conditions, at the same ESM build. No CommonJS
build is added.

On Node below 20.19 / 22.12, require now fails with ERR_REQUIRE_ESM,
which names the real cause.

The installation page notes that the package can be required on
Node >= 20.19 or >= 22.12.
feat: walk a move on past an item that refuses focus
A move whose destination refused focus resolved the whole move again
from the focused item, so a run of N items that refuse focus cost
O(N²) navigable tests: about 7.3–7.7 ms per press for 1,000 hidden
rows on an M1 Pro.

The move now takes each next destination from the same walk.
walkTargets in position.ts hands out every destination in the order a
stride tries them, its own scan and then its clamp or wrap, testing
each item only as it reaches it. keyRove, rove and typeahead take the
first destination from it and carry it on past each refusal by the
stricter test land asks with. That test only grows stricter, so the
walk lands where resolving again would, for every stride. 1,000 hidden
rows now cost about 0.65 ms, and typeahead reads each label once
however many matches refuse.

The benchmark gains a "refused focus" section, and its recorded run
and README are re-recorded with it.
Windows browsers report AltGr as Ctrl+Alt, so a press that types "@" or
"€" read as a command: a ctrl+alt+ focus key fired from inside a field,
and typeahead dropped characters typed with AltGr. hasCommandModifier now
treats a press with getModifierState('AltGraph') held as typing, in both
the editable-target guard and typeahead. Combo matching is unchanged, so
outside a field a ctrl+alt+ focus key still fires on the AltGr press.

KeyRoveEvent gains an optional getModifierState, asked only about
'AltGraph' so React's narrower ModifierKey parameter fits too. An event
without it behaves as before. The docs drop the AltGr caveat.
chore(bench): wait for Chrome to exit before removing its profile
fix: let CommonJS code require the package on Node versions that load ES modules
perf: walk on past refusing items in one pass
fix: treat AltGr presses as typing, not commands
`cols: 'auto'` counted only CSS grid tracks, so a flex-wrap or
inline-block gallery fell back to a one-column list and needed
hand-written glue to copy its column count into the attribute.

Where the root has no more than one grid track, `auto` now counts the
items on the first line: from the first item of the sequence, every
item that starts above the first item's bottom edge, on each keypress.
Items of different heights centred on one line count as that line. Only
the first line is read, plus one item. An item with no box ends the
count, so without layout (jsdom) the group stays a list. A grid with
more than one track returns before any item is read, and a numeric
count never reads layout.

readConfig takes the group (its root and its lazily read items) in
place of the root.

This widens what `auto` means on a non-grid root: a single-line flex
row becomes a one-row grid, so its next/prev keys move to
ArrowLeft/Right. A toolbar should use data-keyrove-orientation instead.
With cols auto, readConfig read the layout on every keydown, before the
key was matched. On a flex root an unbound key paid for reading the
items and two rect calls per first-line item, and laid the page out
when it had changed since the last read.

readConfig now reads the layout on first ask. findBinding asks for it
only when the press's code is a default key of some layout, or the
press matches a combo some move is bound to. Any other press can match
nothing but a focus key, and focus keys head every table, so it is
resolved among them without the layout. Default keys are tested by
code alone, a bound combo is parsed only where its text holds the
press's code, and each move's attribute name is spelled once.

countFirstLine reads one getClientRects() per item, its first box,
in place of a getClientRects() and a getBoundingClientRect().

An unbound key now costs about 1.2 µs on every root, down from 2.5 µs
on a list, 5.6 µs on a grid with auto and 10-25 µs on a flex root with
auto, and none lays out a changed page.
…e benchmark

The columns section timed only a four-column CSS grid, whose tracks
auto counts without reading any item, so the first-line count went
unmeasured.

It now also lays groups out as a flex-wrap row of four, a flex row of
all 20 and a flex column, with a fixed cols and with auto. Each page
times the unbound key and a move with the layout laid out and with it
changed just before each press, and one layout of the page with no
handler. A probe skips the flex moves on versions that read a layout
with no grid tracks as a list.

Re-recorded, with the README's results and findings, and the bench
tests' copied rows, updated to the new run.
feat: count the first line's items for cols auto on wrapping layouts
- Build TypeaheadOptions on RovingTabindexOptions, and read groups by it,
  in place of three copies of the same Pick of GroupOptions.
  GroupReadingOptions, identical to RovingTabindexOptions, is gone.
- Type MoveAction and the keys option with BoundaryAction instead of
  spelling out 'exit' | 'enter'.
- Export the KEYROVE_ATTR_* constants from index.ts directly rather than
  through keyRove.ts, and name the keyRove and createTypeahead exports, so
  the public surface reads at a glance in one file.
- Move resolveTarget, used only by the position tests, into them.
- Correct the ExplicitBinding comment: since 2.4.0 the keys option is read
  before the root's *-key attribute.

The public surface is unchanged.
AnyMove, Move | TypeaheadMove, types an onMove in one config object that
is passed to both keyRove and createTypeahead. Typed with Move alone, such
an object fails to type-check against createTypeahead, whose moves have
the action 'typeahead'.

BoundaryAction, 'exit' | 'enter', lets a keys map for the boundary moves
be typed, as StrideAction already does for the strides.

The README's and the API reference's shared-config examples type onMove
with AnyMove, and the docs site's readout takes it.
toggleTabIndex is a single setAttribute call. Its parameter is named root
though callers pass an item, and its spelling differs from
initRovingTabindex and RovingTabindexOptions. For a roving group, use
initRovingTabindex to keep its one tab stop and followFocus to move it
with focus; for a stop managed by hand, set element.tabIndex. The library
and the tree demo no longer call it.

Options, the earlier name of KeyRoveOptions, now carries @deprecated, so
editors point to KeyRoveOptions.

Both keep working and will be removed in the next major version.
data-keyrove-orientation="Horizontal" or " horizontal " silently gave a
vertical list, while cols="auto", none and the boolean attributes are
trimmed and read in any case. The attribute now is too. The orientation
option is typed and is still compared as given.
feat: export AnyMove and BoundaryAction, deprecate toggleTabIndex and Options, fix orientation case

This branch has not been deployed

No deployments
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