Guides
Listbox Rules
The filtering, keyboard and panel rules behind Select and Combobox, published as a module you can build your own view on.
Select and Combobox agree about a great deal. An arrow key moves the highlight and wraps at the ends. Enter takes the highlighted option. Typing opens the panel and starts the search again. A panel with nothing in it says whether it filtered everything out or never had anything to begin with.
Those rules do not live in either component. They live in one module in the headless layer that both of them — and the React and Svelte builds, which share no code with either — read from. It is exported, so your own views can read it too.
npm install @nana-tec/ui-headlessimport { project, reduce, keyToAction } from "@nana-tec/ui-headless/listbox";Nothing in the module touches the DOM or a framework. It takes plain data and returns plain data; painting, focus and events stay with whoever is drawing.
What it decides
Section titled “What it decides”The panel
Section titled “The panel”project works out the whole panel from the state and the options: what is
hidden, what reads as selected, what can be moved to, and what an empty panel
should say.
const view = project( { open: true, query: "ug", filtering: true, selectedValues: ["ke"] }, [ { value: "ke", label: "Kenya" }, { value: "ug", label: "Uganda" }, { value: "rw", label: "Rwanda", disabled: true }, ],);
view.hidden;view.navigable;view.shown;view.emptyText;shown and navigable are counted separately on purpose. A disabled option is
still on screen, so a query that only matches one is not an empty panel —
it is a panel you cannot pick from.
filtering is yours to decide, because the two components disagree. A combobox
types into the field that also displays its value, so it ignores a query that
still reads as the selected label — isFiltering(open, query, selectedLabel)
answers that. A select searches in a box of its own and filters on anything
typed there.
The keyboard
Section titled “The keyboard”keyToAction says what a key means, or null when the key is none of the
listbox’s business and should be left to the page.
keyToAction("ArrowDown", { open: false, activeIndex: -1 }, 3);
keyToAction("Tab", { open: true, activeIndex: 0 }, 3);consumesKey is the listbox saying whether it takes the key for itself. Tab is
the one that does not: it closes the panel and still moves focus onwards.
The transitions
Section titled “The transitions”reduce moves the state on. It is pure, and it never touches the state you hand
it.
reduce({ open: false, activeIndex: -1, query: "" }, { type: "query", value: "ug" }, 3);Events are open, close, move, moveTo, query and select. Closing and
selecting drop the highlight but leave the query alone — whether the field snaps
back to the selected label is a decision about value, and that stays with your
view.
Pointing at the active option
Section titled “Pointing at the active option”optionId(baseId, index) and activeDescendantId(baseId, activeItem) give every
view one id scheme, so aria-activedescendant names an element that exists.
Building a view on it
Section titled “Building a view on it”const view = project({ open, query, filtering, selectedValues: [value] }, options);
function onKeyDown(event) { const result = keyToAction(event.key, { open, activeIndex }, view.navigable.length); if (!result) return; if (result.consumesKey) event.preventDefault();
if (result.action.type === "select") choose(view.navigable[result.action.index]); else setState((state) => reduce(state, result.action, view.navigable.length));}That is the whole contract. The library’s own components are the same handful of
calls plus their markup — the web components add scrollIntoView and the shadow
DOM, React adds its own JSX, and neither adds a rule.
What is not in it
Section titled “What is not in it”Focus management, click-outside, panel positioning and form participation stay
in the components. They cannot be expressed without an element, and a rules
module that takes HTMLElement arguments is no longer a rules module.
Owning the source
Section titled “Owning the source”If you install with the CLI rather than the package, the rules come with the component:
npx @nana-tec/ui add comboboxlands combobox and utils/listbox.ts in your project, on Web Components
and React alike. Edit the view all you like; the behaviour underneath is the
same file this page documents.