# Listbox Rules

> Source: https://design.nanatec.co.ke/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](/guides/headless/) 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.

```bash
npm install @nana-tec/ui-headless
```

```js
import { 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

### 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.

```js
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

`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.

```js
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

`reduce` moves the state on. It is pure, and it never touches the state you hand
it.

```js
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

`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

```jsx
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

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

If you install with the CLI rather than the package, the rules come with the
component:

```bash
npx @nana-tec/ui add combobox
```

lands `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.
