Options in JavaScript
Add keyboard navigation to markup you cannot change, such as a menu from a component library or CMS, with JavaScript options that select items by role.
Use options to navigate markup from a component library, CMS or other code
you cannot change. This menu has no data-keyrove-* attributes; its options
select items by their existing role.
<div id="share" role="menu" aria-label="Share">
<button type="button" role="menuitem" tabindex="0">Copy link</button>
<button type="button" role="menuitem" tabindex="-1">Email a copy</button>
<button type="button" role="menuitem" tabindex="-1" disabled>
Export as PDF
</button>
<button type="button" role="menuitem" tabindex="-1">Post to Slack</button>
<button type="button" role="menuitem" tabindex="-1">Open in Figma</button>
</div>
import { createTypeahead, keyRove } from '@mixedrays/keyrove';
const config = {
items: '[role="menuitem"]',
loop: true,
rovingTabindex: true,
};
const typeahead = createTypeahead(config);
document
.querySelector('#share')
.addEventListener('keydown', (e) => keyRove(e, config) || typeahead(e));
↑/↓ move between items and wrap at the ends. Typing finds an entry, and
Tab leaves the menu. The options configure navigation; the markup supplies
roles, an initial tab stop and the disabled attribute on Export as PDF.
What is coming from where
Options override attributes one setting at a time. For example,
keyRove(e, { loop: true }) enables looping while still finding items from
markup. See attributes and options for
replacement rules and the API reference for every field.
One object, both handlers
Pass the same items, root, skip and rovingTabindex settings to both
handlers. In the demo, typing P reaches Post to Slack through the same
selector the arrows use.
Typeahead ignores movement settings such as keys, columns and looping. Use its
label option when an item's text is not
its search label. Create the handler once per listener.
Items, by selector or by hand
items takes a selector, run inside the group's root on each keypress, or a
function returning the elements:
keyRove(e, { items: '[role="menuitem"]' });
keyRove(e, { items: (root) => [...root.querySelectorAll('.row')].reverse() });
Either way the elements are the group's sequence in the order given, and
disabled ones are left out — the same rule the attribute reading follows,
because it is applied to the items rather than to the reading that found them.
A skip test passes items over while keeping them in the sequence, which is
what a grid needs: a skipped cell holds its column.
keyRove(e, { items: '.cell', skip: '.cell-empty', cols: 4 });
Where this belongs
Use options when you cannot edit the markup or need a computed setting:
keyRove(e, { items: '.cell', cols: columnsNow() }) reads the column count on
each call. For CSS grids, cols: 'auto' or data-keyrove-cols="auto" reads it
from the layout; see responsive grid.