# 溢出列表

## 使用方法

`OverflowList` 按容器宽度决定能放下几项，剩下的收进「更多」菜单。默认从末尾收起，`collapseFrom="start"` 时从开头收起。`visibleCount` 可以直接指定可见项数，跳过测量，测试时用得上。

每一项是 `{ label, href?, iconClass? }`。自定义外观用 `item` 插槽，自定义溢出菜单用 `overflow` 插槽。

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

const items = [
  { href: '/home', label: '首页' },
  { href: '/files', label: '文件' },
  { href: '/settings', label: '设置' },
]
</script>

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

### 结构

- **列表**: 装着当前可见内容的横向列表容器。
- **项**: 使用方提供的项里，按当前宽度和数量限制选出来显示的那些。
- **溢出指示**: 可选的、由使用方渲染的指示，表示被宽度或数量限制收起来的项。

## 最佳实践

| 指引 | 实践 |
| --- | --- |
| 推荐 | 让列表宽度跟着父级走。项一多，组件会自己把放不下的收进菜单。 |
| 推荐 | 需要至少留下几项时设置 <code>minVisibleItems</code>。 |
| 避免 | 不要同时用插槽把同一项再画一遍到菜单外面。可见项和溢出项是切开的两段。 |

## 示例

### 限量工具栏

maxVisibleItems 把一行限制成三个操作，即使还能放下更多；其余进入下拉菜单。

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

### 从开头折叠

从开头隐藏放不下的项，让最新的项保持可见。

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

### 多行标签

标签最多换到两行，其余折叠成数量徽章。

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

### 徽章标签

可调整宽度的徽章行，溢出时收成一个数量徽章。

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

### 下拉操作

操作工具栏把放不下的按钮收进下拉菜单。

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

### 表格筛选

表格上方的筛选条：搜索框在最前，旁边每个字段既是关闭的触发器，也是自己的筛选条；行变窄时，条件从末尾收成一个数量。

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

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `items` | `OverflowListItem[]` | - | 要排布的项。放不下的会进「更多」菜单。 |
| `gap` | `SpacingStep` | `2` | 项间距，使用间距刻度。 |
| `minVisibleItems` | `number` | `0` | 至少留下几项。 |
| `maxVisibleItems` | `number` | - | 最多留下几项。 |
| `maxRows` | `number` | - | 最多排几行，剩下的再收起。 |
| `collapseFrom` | `'start' \| 'end'` | `'end'` | 从哪一头开始收。 |
| `behavior` | `'observeSelf' \| 'observeParent'` | `'observeSelf'` | 用自己的宽度还是父元素的内容宽度。 |
| `visibleCount` | `number` | - | 直接指定可见项数，跳过测量。 |
| `as` | `NavLinkComponent` | `'a'` | 可见项和菜单项的链接根组件。 |

## OverflowList 事件

| 名称 | 说明 |
| --- | --- |
| `select` | 选中一项时触发，参数是该项和点击事件。 |
| `overflowChange` | 被收起的那一组变了才触发，参数是 `{ item, index }[]`。`index` 是该项在 `items` 里的下标。一开始就放得下时不触发。重新放得下时再触发一次空数组。 |

## OverflowList Slots

| 名称 | 说明 |
| --- | --- |
| `item` | 自定义一项。参数是 `{ item, index }`。 |
| `overflow` | 自定义溢出区域。参数是 `{ items }`，默认是「更多」菜单。 |
