# Overflow List

## Usage

`OverflowList` decides how many items fit in the container and moves the rest into a More menu. It collapses from the end by default. `collapseFrom="start"` collapses from the start. `visibleCount` skips measurement and is what tests use.

Each item is `{ label, href?, iconClass? }`. Customize a row with the `item` slot and the overflow menu with the `overflow` slot.

```vue
<script setup lang="ts">
import { OverflowList } from '@astryx-vue/core/OverflowList'

const items = [
  { href: '/home', label: 'Home' },
  { href: '/files', label: 'Files' },
  { href: '/settings', label: 'Settings' },
]
</script>

<template>
  <OverflowList :items="items" />
</template>
```

### Anatomy

- **List**: Visible horizontal list container for the currently shown content.
- **Items**: Caller-supplied items selected for visible display by the current width and count limits.
- **Overflow indicator**: Optional caller-rendered indicator for items collapsed by width or count limits.

## Best practices

| Guidance | Practice |
| --- | --- |
| Do | Let the list take its width from the parent. Extra items move into the menu on their own. |
| Do | Set <code>minVisibleItems</code> when a few items must stay visible. |
| Avoid | Do not render the same item again outside the menu. Visible items and overflow items are two slices. |

## Examples

### Capped toolbar

maxVisibleItems caps the row at three actions even when more would fit; the rest move to a dropdown.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { NavDisclosure, NavMenu, NavMenuItem } from '@astryx-vue/core/NavMenu'
import { OverflowList } from '@astryx-vue/core/OverflowList'

const items = [
  { id: 'save', label: 'Save' },
  { id: 'edit', label: 'Edit' },
  { id: 'duplicate', label: 'Duplicate' },
  { id: 'share', label: 'Share' },
  { id: 'archive', label: 'Archive' },
  { id: 'delete', label: 'Delete' },
]
</script>

<template>
  <Card :padding="2" :width="260">
    <OverflowList :items="items" :max-visible-items="3">
      <template #item="{ item }">
        <Button :label="item.label" size="sm" />
      </template>
      <template #overflow="{ items: hidden }">
        <NavDisclosure label="More actions">
          <template #trigger="{ isOpen, panelId }">
            <Button
              :label="`+${hidden.length}`"
              size="sm"
              variant="ghost"
              :aria-controls="panelId"
              :aria-expanded="isOpen ? 'true' : 'false'"
              aria-haspopup="menu"
            />
          </template>
          <template #default="{ close, focusRequest }">
            <NavMenu label="More actions" :focus-request="focusRequest" @close="close">
              <NavMenuItem v-for="item in hidden" :key="item.id" :label="item.label" />
            </NavMenu>
          </template>
        </NavDisclosure>
      </template>
    </OverflowList>
  </Card>
</template>
```

### Collapse from start

Overflow list that hides items from the start, keeping the latest visible.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { NavDisclosure, NavMenu, NavMenuItem } from '@astryx-vue/core/NavMenu'
import { OverflowList } from '@astryx-vue/core/OverflowList'

const items = [
  { id: '1', label: 'Step 1' },
  { id: '2', label: 'Step 2' },
  { id: '3', label: 'Step 3' },
  { id: '4', label: 'Step 4' },
  { id: '5', label: 'Step 5' },
]
</script>

<template>
  <Card :padding="2" :width="300">
    <OverflowList collapse-from="start" :items="items" :visible-count="2">
      <template #item="{ item }">
        <Button :label="item.label" size="sm" />
      </template>
      <template #overflow="{ items: hidden }">
        <NavDisclosure label="More steps">
          <template #trigger="{ isOpen, panelId }">
            <Button
              :label="`+${hidden.length} more`"
              size="sm"
              variant="ghost"
              :aria-controls="panelId"
              :aria-expanded="isOpen ? 'true' : 'false'"
              aria-haspopup="menu"
            />
          </template>
          <template #default="{ close, focusRequest }">
            <NavMenu label="More steps" :focus-request="focusRequest" @close="close">
              <NavMenuItem v-for="item in hidden" :key="item.id" :label="item.label" />
            </NavMenu>
          </template>
        </NavDisclosure>
      </template>
    </OverflowList>
  </Card>
</template>
```

### Multi-row tags

Tags wrap onto up to two rows with maxRows, then collapse the rest into a count badge.

```vue
<script setup lang="ts">
import { Badge } from '@astryx-vue/core/Badge'
import { Card } from '@astryx-vue/core/Card'
import { OverflowList } from '@astryx-vue/core/OverflowList'

const items = [
  { id: 'react', label: 'React' },
  { id: 'typescript', label: 'TypeScript' },
  { id: 'stylex', label: 'StyleX' },
  { id: 'storybook', label: 'Storybook' },
  { id: 'vitest', label: 'Vitest' },
  { id: 'playwright', label: 'Playwright' },
  { id: 'eslint', label: 'ESLint' },
  { id: 'prettier', label: 'Prettier' },
  { id: 'vite', label: 'Vite' },
  { id: 'pnpm', label: 'pnpm' },
]
</script>

<template>
  <Card :padding="2" :width="340">
    <OverflowList :items="items" :max-rows="2" :max-visible-items="6">
      <template #item="{ item }">
        <Badge variant="info">
          {{ item.label }}
        </Badge>
      </template>
      <template #overflow="{ items: hidden }">
        <Badge>+{{ hidden.length }}</Badge>
      </template>
    </OverflowList>
  </Card>
</template>
```

### Badge tags

Resizable row of badges that collapses into a count badge on overflow.

```vue
<script setup lang="ts">
import { Badge } from '@astryx-vue/core/Badge'
import { Card } from '@astryx-vue/core/Card'
import { OverflowList } from '@astryx-vue/core/OverflowList'

const items = [
  { id: 'react', label: 'React' },
  { id: 'typescript', label: 'TypeScript' },
  { id: 'stylex', label: 'StyleX' },
  { id: 'storybook', label: 'Storybook' },
  { id: 'vitest', label: 'Vitest' },
]
const variants = {
  react: 'info',
  typescript: 'success',
  stylex: 'warning',
  storybook: 'neutral',
  vitest: 'error',
} as const
</script>

<template>
  <Card :padding="2" :width="300">
    <OverflowList :items="items" :max-visible-items="3">
      <template #item="{ item }">
        <Badge :variant="variants[item.id as keyof typeof variants]">
          {{ item.label }}
        </Badge>
      </template>
      <template #overflow="{ items: hidden }">
        <Badge>+{{ hidden.length }}</Badge>
      </template>
    </OverflowList>
  </Card>
</template>
```

### Dropdown actions

Action toolbar that collapses overflow buttons into a dropdown menu.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { NavDisclosure, NavMenu, NavMenuItem } from '@astryx-vue/core/NavMenu'
import { OverflowList } from '@astryx-vue/core/OverflowList'

const items = [
  { id: 'save', label: 'Save' },
  { id: 'edit', label: 'Edit' },
  { id: 'duplicate', label: 'Duplicate' },
  { id: 'share', label: 'Share' },
  { id: 'archive', label: 'Archive' },
  { id: 'delete', label: 'Delete' },
]
</script>

<template>
  <Card :padding="2" :width="360">
    <OverflowList :items="items" :max-visible-items="3">
      <template #item="{ item }">
        <Button :label="item.label" size="sm" :variant="item.id === 'save' ? 'primary' : undefined" />
      </template>
      <template #overflow="{ items: hidden }">
        <NavDisclosure label="More actions">
          <template #trigger="{ isOpen, panelId }">
            <Button
              :label="`+${hidden.length}`"
              size="sm"
              variant="ghost"
              :aria-controls="panelId"
              :aria-expanded="isOpen ? 'true' : 'false'"
              aria-haspopup="menu"
            >
              <template #end>
                <span class="nav-overflow-chevron i-carbon-chevron-down" />
              </template>
            </Button>
          </template>
          <template #default="{ close, focusRequest }">
            <NavMenu label="More actions" :focus-request="focusRequest" @close="close">
              <NavMenuItem v-for="item in hidden" :key="item.id" :label="item.label" />
            </NavMenu>
          </template>
        </NavDisclosure>
      </template>
    </OverflowList>
  </Card>
</template>
```

### Table filter

Filter bar above a table: a search box leads the row, each field beside it is a closed trigger that doubles as its own filter chip, and the clauses fold from the end into a count as the row narrows.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Selector } from '@astryx-vue/core/Selector'
import { Text } from '@astryx-vue/core/Text'
import { TextInput } from '@astryx-vue/core/TextInput'
import { Toolbar } from '@astryx-vue/core/Toolbar'
import { computed, ref } from 'vue'

const jobs = [
  { customer: 'Northgate Dental', id: 'SJ-2136', job: 'VAV zones not cooling', priority: 'High', status: 'In progress' },
  { customer: 'Riverside Club', id: 'SJ-2135', job: 'Exhaust fan noise', priority: 'Normal', status: 'On hold' },
  { customer: 'Summit Medical', id: 'SJ-2134', job: 'Humidifier board fault', priority: 'High', status: 'Overdue' },
  { customer: 'Northgate Dental', id: 'SJ-2133', job: 'Economizer stuck shut', priority: 'Normal', status: 'Scheduled' },
  { customer: 'Harborview Hotel', id: 'SJ-2101', job: 'Contactor replacement', priority: 'Low', status: 'Completed' },
]
const query = ref('')
const status = ref<string | null>(null)
const priority = ref<string | null>(null)
const customer = ref<string | null>(null)
const statusOptions = ['Scheduled', 'In progress', 'On hold', 'Overdue', 'Completed'].map(value => ({ label: value, value }))
const priorityOptions = ['High', 'Normal', 'Low'].map(value => ({ label: value, value }))
const customerOptions = ['Northgate Dental', 'Riverside Club', 'Summit Medical', 'Harborview Hotel'].map(value => ({ label: value, value }))
const visible = computed(() => jobs.filter((job) => {
  if (query.value && !job.job.toLowerCase().includes(query.value.toLowerCase()))
    return false
  if (status.value && job.status !== status.value)
    return false
  if (priority.value && job.priority !== priority.value)
    return false
  if (customer.value && job.customer !== customer.value)
    return false
  return true
}))

function clearFilters(): void {
  query.value = ''
  status.value = null
  priority.value = null
  customer.value = null
}
</script>

<template>
  <div class="nav-frame nav-frame--fill nav-filter">
    <Toolbar label="Table filter" size="sm">
      <template #start>
        <TextInput v-model="query" is-label-hidden label="Search jobs" placeholder="Search jobs" size="sm" />
        <Selector v-model="status" label="Status" :options="statusOptions" placeholder="Status" size="sm" />
        <Selector v-model="priority" label="Priority" :options="priorityOptions" placeholder="Priority" size="sm" />
        <Selector v-model="customer" label="Customer" :options="customerOptions" placeholder="Customer" size="sm" />
        <Button v-if="query || status || priority || customer" label="Clear all" size="sm" variant="ghost" @click="clearFilters" />
      </template>
      <template #end>
        <Text color="secondary" type="supporting">
          {{ visible.length }} of {{ jobs.length }}
        </Text>
      </template>
    </Toolbar>
    <table class="nav-bulk-table">
      <thead>
        <tr>
          <th>Job</th>
          <th>Customer</th>
          <th>Status</th>
          <th>Priority</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="job in visible" :key="job.id">
          <td>{{ job.job }}</td>
          <td>{{ job.customer }}</td>
          <td>{{ job.status }}</td>
          <td>{{ job.priority }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>
```

## OverflowList Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `OverflowListItem[]` | - | Items to lay out. The ones that do not fit go into More. |
| `gap` | `SpacingStep` | `2` | Gap on the spacing scale. |
| `minVisibleItems` | `number` | `0` | Minimum number of items to keep visible. |
| `maxVisibleItems` | `number` | - | Maximum number of items to keep visible. |
| `maxRows` | `number` | - | Wrap up to this many rows before overflowing. |
| `collapseFrom` | `'start' \| 'end'` | `'end'` | Which end collapses first. |
| `behavior` | `'observeSelf' \| 'observeParent'` | `'observeSelf'` | Measure the list or its parent. |
| `visibleCount` | `number` | - | Forces the visible count and skips measurement. |
| `as` | `NavLinkComponent` | `'a'` | Link root for visible items and menu items. |

## OverflowList events

| Name | Description |
| --- | --- |
| `select` | Fired when an item is chosen, with the item and the click event. |
| `overflowChange` | Fired when the collapsed set changes. The payload is `{ item, index }[]`, where `index` is the item's original place in `items`. A list that fits from the start never fires. When everything fits again, it fires once with an empty array. |

## OverflowList slots

| Name | Description |
| --- | --- |
| `item` | Custom item. Props are `{ item, index }`. |
| `overflow` | Custom overflow. Props are `{ items }`. Defaults to the More menu. |
