import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";

import * as GroupedListStories from "./GroupedList.stories";

<Meta of={GroupedListStories} />

# GroupedList

An always-open list whose rows sit under group headings. Each row is a label, an optional
muted description, optional badges, optional richer content, an optional figure beside the
text, and optional trailing actions. A row can also be the target itself, muted, or
highlighted.

Three apps drew this shape by hand: castkit's page picker (pages under the screen that shows
them), tally-marks' cards (cards under the team that earns them) and whereabouts' nested
holdings (things under the place that holds them). The library owns the **shape**. The app
owns the **data**: the grouping, the search and the filters stay in the app, and a
`SearchInput` above the list is still the way to narrow it.

<Canvas of={GroupedListStories.Playground} />

## Props

<Controls />

## A group is a named list, not a landmark

The group heading labels the `<ul>` through `aria-labelledby`. A screen reader announces
"Screens, list, 3 items", and `getByRole("list", { name: "Screens" })` finds the group.

There is no `<section>`. A named `<section>` is a `region` landmark, and one screen of pages
is a dozen groups — the "landmark region proliferation" the APG warns about. `Accordion`'s
panels are a `group` for the same reason.

The heading level is `headingLevel`, `3` by default. A list under a page's `<h2>` needs `3`.

## An empty group is not drawn

A heading over nothing reads as a bug once a search narrows the list, so the component skips
a group with no items. When **every** group is empty it draws `emptyState` — usually an
`EmptyState` whose wording says why.

<Canvas of={GroupedListStories.AllStates} />

## Long text

A long label or description clips to one line. The full text becomes a `title` tooltip only
while it is clipped, measured the same way `Badge` measures. Pass `isTextTruncated={false}`
to wrap it instead.

<Canvas of={GroupedListStories.LongText} />

## Narrow View

The row is a wrapping flex line. The text column asks for 12rem and grows into the rest, so
the actions sit at the inline end in a wide list and drop under the text in a narrow one.
There is no container query and no media query: the content decides.

<Canvas of={GroupedListStories.Responsive} />

## Sticky headings

`isHeadingSticky` pins each group's heading to the top of the nearest scrolling ancestor while
that group's rows scroll under it, and the next group's heading pushes it away. Use it for a
list inside a scrolling pane, where the heading is the only thing that says which screen or
team a row belongs to once the row scrolls out of sight. It is off by default.

A stuck heading needs an opaque background, or the rows show through it. The component cannot
know which surface it sits on — a page is `surface-base`, a settings pane is
`surface-raised` — so the host says so with a custom property:

| Property | Default | Set it to |
| --- | --- | --- |
| `--charcuterie-grouped-list-heading-surface` | `var(--color-surface-base)` | The background of the pane the list sits on. |
| `--charcuterie-grouped-list-heading-offset` | `0px` | The height of anything already stuck to the top of the same scroll container, such as a sticky `Header`. |

Set either one on any ancestor of the list. The names are exported as
`GROUPED_LIST_HEADING_SURFACE_PROPERTY` and `GROUPED_LIST_HEADING_OFFSET_PROPERTY` for a host
that sets them through a `style` object.

```css
.settings-panel {
  --charcuterie-grouped-list-heading-surface: var(--color-surface-raised);
}
```

<Canvas of={GroupedListStories.StickyHeadings} />

## A row can be the target

Give an item `onSelect` and the whole row becomes one native `<button>` — "open this card"
for a list whose primary action is the row itself. `selectLabel` names it (it defaults to
`label`); name the verb, as with any repeated action: `selectLabel="Edit Water the ferns"`.

- **Every state is drawn.** Hover tints the row and rings its edge (only on devices that
  hover, so a tap does not leave a row lit). A press sinks the row slightly, with motion only
  when the user has not asked the OS for less. Keyboard focus draws the focus ring inside the
  row's edge, where the list's rounded corners cannot clip it, and one ring-width in from it,
  so a highlighted row that has focus shows both rings.
- **The actions stay their own targets.** The button is drawn under the row's text and the
  text lets the pointer through to it; `actions` sit above it. Pressing "Delete" never opens
  the row, and Tab reaches the row, then its actions, then the next row.
- **Nothing is nested in the button.** It is empty and named by `aria-label`, so `badges`,
  `content` and `aside` must not hold controls — a control belongs in `actions`.

`aside` is a figure that belongs **beside** the text — a payout, a total. It asks for at
least 11rem next to the text column's 12rem, so it sits between the text and the actions in a
wide list and drops under the text in a narrow one.

<Canvas of={GroupedListStories.SelectableRows} />

## Muted and highlighted rows

- `isMuted` recedes a row that exists but is switched off: a sunken surface and a quieter
  label. It stays fully operable, and it is a surface and a text role rather than an opacity,
  because a faded row would drop below the AA contrast floor and only a disabled control may.
  Say why in a badge as well ("off").
- `isHighlighted` rings the row and pulses the ring twice — "this one just changed", after a
  scan or a save. `highlightIntent` picks the colour (`accent` by default). The ring stays
  while the flag does, so the app decides how long it lasts, usually with a timer. **To pulse
  an already-highlighted row again, change its `key`**: a new key is a new row, and a new row
  starts the pulse from the beginning. Under `prefers-reduced-motion` and on ePaper the ring
  does not pulse; it is simply there. The pulse lives in `@charcuterie/ui/styles.css`, which
  every app already imports.

<Canvas of={GroupedListStories.RowStates} />

## Name a repeated action after its row

Ten rows of "Open" are ten buttons an agent cannot tell apart. Give each one a name that
includes its row — `aria-label="Open Lobby"`.

## Not collapsible

A group you can close is `Accordion`. `GroupedList` is for a list short enough to read whole
and long enough to want headings.
