# xpl-list

## Description

**`xpl-list`** is the list **container** (`role="list"`). It coordinates **single selection** (shared radio group), **reordering** (drag-and-drop and keyboard), and assigns default **`item-id`** values when missing.

**`xpl-list-item`** is one **row**. In **slot-based** mode, `role="listitem"` is on the row surface inside the host (not on `<xpl-list-item>`), so list semantics work with the host’s `display: contents` layout. Legacy **`item`** rows keep `role="listitem"` on the host. Place items **inside** **`xpl-list`** (default slot). Compose each row with **named slots** (recommended) or use the **legacy** `item` prop / list-level `items` array. Row-level props, events, and dependencies: [xpl-list-item readme](./xpl-list-item/readme.md).

When the list is **`selectable`**, pressing **Escape** while focus is on the **selected** row clears the radio selection (same as **`clearSelection()`**). Clicking elsewhere does not clear selection. Row radios use the **title** slot for their accessible name when present; with no title slot, the label is **`Select <item-id>`** when **`item-id`** or **`id`** is set, otherwise a generic default.

**Single-select vs multi-select:** **`selectable`** on **`xpl-list`** is for **exactly one chosen row** (implemented with **`xpl-radio`** and a shared group **`name`**). For **multi-select** or **independent** toggles per row, do **not** use this flag; compose **`xpl-checkbox`** (or your own controls) in row slots and manage state in the app.

---

## Slot-based vs legacy integration


|                         | **Slot-based (recommended)**                                  | **Legacy prop-based (deprecated)**                                                                                          |
| ----------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Where content lives** | Light DOM: named slots on each `<xpl-list-item>`              | Data: `items` on `<xpl-list>` and/or `item` on `<xpl-list-item>`                                                            |
| **Row markup**          | You author `<xpl-list-item>` children with `slot="…"` content | List renders rows from `items`, or item renders from `item` object                                                          |
| **Flexibility**         | Full UI composition (avatar, actions, errors, extras)         | Fixed layout from `ListItem` fields                                                                                         |
| **Identity**            | Set `item-id` (or `id`) on each row                           | Legacy rows from `items` use `ListItem.id` when set, else `item-0`, `item-1`, …                                         |
| **Reordering**          | `reorderable` on `xpl-list`: drag handle + keyboard on each slot row | **No reorder UI:** legacy `items` / `item` rows do not render a handle; use slots if you need reorder.                  |
| **Notes**               | Do not set `item` on rows in this mode                        | `items` logs a console warning when non-empty; mixing `items` **and** slotted children renders **both** (legacy rows first) |


---

## Slot-based integration

Put one or more `<xpl-list-item>` elements **inside** `<xpl-list>`. Do **not** set the **`item`** prop on a row when using slots—if `item` is set, that row uses **legacy** rendering and ignores slot composition.

On **`xpl-list`**, set `selectable` and/or `reorderable` as needed; the list mirrors `radio-name`, `reorderable`, and `keyboard-active` onto **slot-based** direct child rows only (nested lists in slots are unchanged). Rows using the deprecated **`item`** prop or list-level **`items`** array are **legacy** markup: the list does **not** set `selectable`, `radio-name`, or `reorderable` on them, because that UI exists only in slot-based mode—migrate to slotted `<xpl-list-item>` children if you need selection or reorder.

### Slots (on `xpl-list-item`)

All slots are **named**. Use `slot="<name>"` on light-DOM children.


| Slot              | Role                                                      | HTML example                                                      |
| ----------------- | --------------------------------------------------------- | ----------------------------------------------------------------- |
| `title`           | Primary title line                                        | `<span slot="title">Account name</span>`                          |
| `metadata`        | Secondary lines under the title                           | `<ul slot="metadata"><li>Line one</li></ul>`                      |
| `avatar`          | Leading avatar or custom media                            | `<xpl-avatar slot="avatar" name="JD"></xpl-avatar>`               |
| `details`         | Right column: supporting text                             | `<span slot="details">Last updated</span>`                        |
| `badges`          | Right column: badges / tags                               | `<span slot="badges"><xpl-badge>New</xpl-badge></span>`           |
| `actions`         | Trailing actions; clicks do **not** follow `href`         | `<span slot="actions"><xpl-button type="button" variant="tertiary" size="sm">Edit</xpl-button></span>` |
| `error-text`      | Message below the row; affects error styling when present | `<span slot="error-text">This field is required.</span>`          |
| `stacked-extra-1` | Optional stacked block                                    | `<div slot="stacked-extra-1">Extra block 1</div>`                 |
| `stacked-extra-2` | Second optional stacked block                             | `<div slot="stacked-extra-2">Extra block 2</div>`                 |
| `stacked-extra-3` | Third optional stacked block                              | `<div slot="stacked-extra-3">Extra block 3</div>`                 |


**Dot badge next to the title:** set **`title-badge-variant`** on **`xpl-list-item`** (same variants as `xpl-badge`), not inside the title slot.

**Divider under the title row:** set **`show-divider`** on **`xpl-list-item`**.

**Minimal example:**

```html
<xpl-list selectable reorderable>
  <xpl-list-item item-id="row-1" href="/settings">
    <xpl-avatar slot="avatar" name="AC"></xpl-avatar>
    <span slot="title">Settings</span>
    <span slot="metadata">Workspace</span>
    <span slot="details">Updated today</span>
    <span slot="actions"><xpl-button type="button" variant="tertiary" size="sm">Open</xpl-button></span>
  </xpl-list-item>
</xpl-list>
```

---

### Attributes: where to set them

#### `xpl-list` (container)


| Attribute      | Purpose                                                                                                                              |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `label`        | Accessible name: sets `aria-label` on the list host when there is no visible heading. **Not used** if `labelled-by` is also set.    |
| `labelled-by`  | Space-separated element ids: sets `aria-labelledby` on the host. Takes precedence over `label` so both are never applied together.   |
| `selectable`   | Enables radio selection; list assigns shared `radio-name`. **Escape** on the **selected** row clears selection (no extra attribute). |
| `reorderable`  | Enables drag handles and reorder behavior on **slot-based** rows only (see legacy section).                                       |

Use **`label`** *or* **`labelled-by`**, not both. If both attributes are present, only **`labelled-by`** affects the accessible name.


#### `xpl-list-item` (row, slot mode — omit `item`)


| Attribute                     | Purpose                                                                                                              |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `item-id`                     | Stable id for selection and reorder events (or use host `id`). If omitted, the list may assign `item-0`, `item-1`, … |
| `href`                        | Optional: row acts as a link; `actions` / `avatar` clicks do not navigate.                                           |
| `disabled`, `error`           | Row state: non-interactive row and/or error styling (border, `error-text` slot).                                   |
| `hidden`                      | Hides the row from layout and from visible-only list behavior; see **Hidden rows** below.                          |
| `selected`                    | Controlled selected state when the list is `selectable`.                                                             |
| `show-divider`                | Divider under the title area.                                                                                        |
| `title-badge-variant`         | Dot badge beside the title.                                                                                          |


#### Usually set by `xpl-list` on items (avoid overriding unless needed)


| Attribute                   | Meaning                                             |
| --------------------------- | --------------------------------------------------- |
| `selectable`, `reorderable` | Mirrored from the list (**slot-based** rows only; not legacy `item` / `items` rows). |
| `radio-name`                | Shared radio group when the list is `selectable` (slot-based rows only).   |
| `keyboard-active`           | Set on the row in keyboard reorder mode (slot-based rows only).            |


### Hidden rows

Set the **`hidden`** attribute (or **`hidden`** prop) on **`xpl-list-item`** when a row should not appear in the list or take up space, but you still want it in the DOM. Typical use cases:

- **Filtering or search** — hide rows that do not match without tearing down markup or losing local state.
- **Progressive disclosure or permissions** — keep a stable **`item-id`** and show the row only when a condition becomes true.
- **Temporary removal** — same as filtering when the row may return without re-creating the element.

**List behavior:** Hidden rows are omitted from **`orderChange`**’s **`orderedItemIds`** and from drag-and-drop / keyboard reorder among **visible** rows only (see tests and list implementation). They do not receive focus while hidden.

**Accessibility:** Native **`hidden`** removes the row from the accessibility tree while it is hidden. If users need an equivalent announcement elsewhere (for example, “3 results hidden by filter”), provide that in your application UI.

---

### Events and programmatic API

#### `xpl-list` — listen on the list host


| Event         | When                                                                                                                                                              | `detail`                                                                  |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `itemSelect`  | Radio selection changes; when clearing via **`clearSelection()`** or **Escape** on the selected row, emitted **once per** previously selected row (typically one) | `{ itemId?: string }`                                                     |
| `orderChange` | After a successful reorder (drag/drop or keyboard); DOM is already updated                                                                                        | `{ itemId?: string; direction: 'up' \| 'down'; orderedItemIds: string[] }` |


**`orderedItemIds`:** visible rows only (DOM order); hidden `xpl-list-item` rows are omitted.

**TypeScript:** `orderChange` detail is typed as **`XplListOrderChangeDetail`** (exported from `@xplortech/apollo-core` alongside **`ListItem`**).

**Legacy:** `xplListReorder` may also fire (bubbles, composed). Prefer **`orderChange`** for new code. Legacy `detail` includes `orderedItemIds` as well.


| Method             | Notes                                                                                               |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| `clearSelection()` | Promise. Clears radios when `selectable`. Same effect as **Escape** with focus on the selected row. |


```html
<xpl-list id="my-list" selectable reorderable>...</xpl-list>
<script type="module">
  const list = document.getElementById('my-list');
  list.addEventListener('itemSelect', (e) => {
    /* handle selection: */ void e.detail.itemId;
  });
  list.addEventListener('orderChange', (e) => {
    /* handle reorder: */ void e.detail.direction;
    void e.detail.orderedItemIds;
  });
</script>
```

#### `xpl-list-item` — optional row-level listeners


| Event           | When                                           | `detail`                                 |
| --------------- | ---------------------------------------------- | ---------------------------------------- |
| `itemSelect`    | Radio change on this row                       | `{ itemId?: string }`                    |
| `itemDragStart` | Drag started from reorder handle               | `{ itemId?: string; item: HTMLElement }` |
| `itemDragEnd`   | Drag ended on this row (after drop or cancel)  | `{ itemId?: string; item: HTMLElement }` |
| `itemDrop`      | Drop target during reorder                     | `{ itemId?: string; item: HTMLElement }` |


---

## Legacy prop-based integration

Use only for migration or data-driven rows. **Prefer slots for new work.**

### At-a-glance migration

| | **Before (deprecated)** | **After (recommended)** |
| --- | --- | --- |
| **Data** | `items` array on `xpl-list`, or `item` object on each row | Light DOM: `<xpl-list-item>` children with named slots |
| **Identity** | Optional `ListItem.id`; else `item-0`, `item-1`, … | Explicit `item-id` (or host `id`) per row |

```html
<!-- Before: data-driven rows -->
<xpl-list id="legacy-list"></xpl-list>
<script type="module">
  document.getElementById('legacy-list').items = [{ title: 'Acme Co.', href: '/acme' }];
</script>

<!-- After: slot-based rows -->
<xpl-list>
  <xpl-list-item item-id="acme" href="/acme">
    <span slot="title">Acme Co.</span>
  </xpl-list-item>
</xpl-list>
```

### `xpl-list` — `items` (deprecated)


| Prop    | Type         | Description                                                                                             |
| ------- | ------------ | ------------------------------------------------------------------------------------------------------- |
| `items` | `ListItem[]` | **Deprecated.** Renders internal `<xpl-list-item>` rows with `item={...}` and ids `item-0`, `item-1`, … |


A **console warning** runs when `items` is non-empty. Using **`items`** and slotted children renders **both**.

### `xpl-list-item` — `item` (deprecated)


| Prop   | Type       | Description                                                                  |
| ------ | ---------- | ---------------------------------------------------------------------------- |
| `item` | `ListItem` | **Deprecated.** Prop-driven row; **no named slots**. Prefer slot-based rows. |


### `ListItem` (`listitem.ts`, deprecated)


| Field      | Type                             | Notes                  |
| ---------- | -------------------------------- | ---------------------- |
| `id`       | `string`                         | Optional stable row id (`item-id` when rendered from `items`) |
| `title`    | `string`                         | Required               |
| `href`     | `string`                         | Optional               |
| `avatar`   | `string`                         | Image URL or icon name |
| `metadata` | `string[]` or `{ icon, text }[]` |                        |
| `subtext`  | `string`                         |                        |
| `avatars`  | `string[]`                       |                        |
| `badges`   | `string[]` or badge objects      |                        |


**Legacy layout:** Rows rendered from the deprecated `item` / inner `.xpl-list-item` flex markup use the **same layout at all viewport widths** (there is no dedicated narrow-viewport stacking breakpoint in `list-item.css`). If you need stacked mobile behavior for legacy lists, handle it in the app (e.g. layout wrapper or scoped CSS).

**Legacy reordering:** The list does **not** set `reorderable` (or `selectable` / `radio-name`) on legacy **`items`** / **`item`** rows—there is **no** drag handle or keyboard reorder entry point in prop-driven mode. Only **slot-composed** rows participate in reorder and list-level selection UI; migrate from `items` / `item` to slotted `<xpl-list-item>` children if you need those features.

---

## Dependencies

- **Rows:** `xpl-list-item` uses [xpl-avatar](../xpl-avatar), [xpl-icon](../xpl-icon), [xpl-badge](../xpl-badge), [xpl-divider](../xpl-divider).

```mermaid
graph TD;
  xpl-list-item --> xpl-avatar
  xpl-list-item --> xpl-icon
  xpl-list-item --> xpl-badge
  xpl-list-item --> xpl-divider
  xpl-list --> xpl-list-item
```



## Design tokens

List styling is split across [`list.css`](https://github.com/xplor/apollo/blob/main/packages/apollo-core/src/css/list.css) (the `xpl-list` host), [`list-item.css`](https://github.com/xplor/apollo/blob/main/packages/apollo-core/src/css/list-item.css) (legacy vs slot-based row layout, error/disabled/hidden, title/metadata columns), and [`list-action.css`](https://github.com/xplor/apollo/blob/main/packages/apollo-core/src/css/list-action.css) (reorder handle, selectable radio chrome, legacy action-button modifiers). All are imported from the package `style` entry. Visual values use **Apollo Foundation** CSS variables (`--xpl-*`); no raw hex colors in those stylesheets.


| Category             | Tokens (representative)                                                                                                                                                                      |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Layout / spacing** | `--xpl-space-4`, `--xpl-space-8`, `--xpl-space-12`, `--xpl-space-16`, `--xpl-space-40`                                                                                                       |
| **Sizing**           | `--xpl-size-050`, `--xpl-size-075`, `--xpl-size-350`                                                                                                                                         |
| **Borders**          | `--xpl-border-small`, `--xpl-border-large`, `--xpl-border-default`, `--xpl-border-hover`, `--xpl-border-focus`, `--xpl-border-negative`, `--xpl-border-subtle`, `--xpl-border-strong`        |
| **Radius**           | `--xpl-border-radius-default`, `--xpl-border-radius-small`, `--xpl-border-radius-full`                                                                                                       |
| **Text**             | `--xpl-text-default`, `--xpl-text-subdued`, `--xpl-text-disabled`, `--xpl-text-negative`                                                                                                     |
| **Icons / surfaces** | `--xpl-icon-default`, `--xpl-icon-subtle`, `--xpl-background-surface-default`, `--xpl-background-action-disabled`, `--xpl-background-surface-subtle`                  |
| **Typography**       | `--xpl-font-family-default`, `--xpl-font-size-body`, `--xpl-font-size-title-5`, `--xpl-font-size-callout`, `--xpl-font-size-caption`, `--xpl-font-weight-normal`, `--xpl-font-weight-medium` |


## Accessibility

- **`xpl-list`** sets `role="list"` on the host. Use **`label`** / **`labelled-by`** when the list has no visible caption.
- **`xpl-list-item`**: **`role="listitem"`** is on the **inner row surface** in **slot-based** mode (see intro above); **legacy** `item` rows keep it on the **host**. When the list is **`selectable`**, the row renders **`xpl-radio`** with a shared group **`name`** and visually hidden label text (from the title slot when present, else a default).
- With **`selectable`**, **Escape** while focus is on the **selected** row **clears selection** (same behavior as **`clearSelection()`**).
- **Reorder**: the drag handle is a native **`<button>`** inside **`xpl-button`** (focusable, with **`aria-pressed`** in keyboard reorder mode and **`aria-label`** instructions for keyboard vs drag). Row links use `:focus-visible` for focus indication.
- **`hidden`** on a row removes it from layout and visible reorder semantics; see **[Hidden rows](#hidden-rows)** for use cases and accessibility notes.

---

<!-- Auto Generated Below -->


## Properties

| Property      | Attribute     | Description                                                                                                                                                                                                                      | Type         | Default     |
| ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- |
| `items`       | --            | Array of list items to render. Each item should follow the ListItem interface. **Deprecated.** Use slot-based xpl-list-item children instead.                                                                                    | `ListItem[]` | `[]`        |
| `label`       | `label`       | Accessible name for the list, exposed as `aria-label` on the host when set. Use when the list has no visible heading. Ignored when `labelledBy` is set (see `labelledBy` for precedence).                                        | `string`     | `undefined` |
| `labelledBy`  | `labelled-by` | Element id(s) that label this list (space-separated), exposed as `aria-labelledby` when set. When set, `label` is not applied as `aria-label` so the list has a single naming source.                                            | `string`     | `undefined` |
| `reorderable` | `reorderable` | When true, enables reorder UI and behavior for **slot-based** child rows (drag handle + keyboard). Legacy rows rendered from the deprecated `items` prop do **not** show a drag handle; migrate to slots if reorder is required. | `boolean`    | `false`     |
| `selectable`  | `selectable`  | Determines if this list is selectable (shows `xpl-radio` per row in a shared group). Press Escape while focus is on the **selected** row to clear selection (same as `clearSelection()`).                                        | `boolean`    | `false`     |


## Events

| Event         | Description                                                                                                                                       | Type                                    |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `itemSelect`  | Emitted when an item is selected.                                                                                                                 | `CustomEvent<{ itemId?: string; }>`     |
| `orderChange` | Emitted when the order of items changes (after the DOM has been updated). `orderedItemIds` lists **visible** row ids only (hidden rows excluded). | `CustomEvent<XplListOrderChangeDetail>` |


## Methods

### `clearSelection() => Promise<void>`

Clears all radio selections in the list.
Also runs when `selectable` is true, the user presses Escape, and focus is on the selected row.
Emits `itemSelect` once per row that was selected (checked radio or `selected` prop), typically one.

#### Returns

Type: `Promise<void>`




## Dependencies

### Depends on

- [xpl-list-item](xpl-list-item)

### Graph
```mermaid
graph TD;
  xpl-list --> xpl-list-item
  xpl-list-item --> xpl-avatar
  xpl-list-item --> xpl-icon
  xpl-list-item --> xpl-badge
  xpl-list-item --> xpl-button
  xpl-list-item --> xpl-radio
  xpl-list-item --> xpl-divider
  xpl-avatar --> xpl-badge
  xpl-button --> xpl-icon
  style xpl-list fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
