# FilterChip

> 生效中的筛选，显示为一个 info-soft 标签片，放在始终写着「筛选」的 Filter 按钮旁边，带一个把筛选恢复为默认值的 ×。

Source: https://raft-design.botiverse.build/zh/components/filter-chip. Translated from https://raft-design.botiverse.build/components/filter-chip.md; where the two differ, the English page is right.

FilterChip 放在始终写着「筛选」的 Filter 按钮旁边，写出生效中的筛选。它是已应用筛选的样子：和按钮一样高的 `info-soft` 填充，以及一个把筛选恢复为默认值的 ×。只有筛选不同于默认值时它才显示。人们很少改动、凭习惯设置的筛选，比如动态里的「未读」或「提及」，就把它和 Filter 按钮的模式（[Select › 用哪种筛选按钮](https://raft-design.botiverse.build/zh/components/select.md#which-filter-button)）一起使用。

```tsx
import { AtSign, ChevronDown, CircleDot, Inbox, ListFilter, Pin } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItemLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  PickerTriggerButton,
} from "@botiverse/raft-ui";
import { FilterChip, FilterChipLabel, FilterChipRemove } from "@botiverse/raft-ui/wip";

const FILTERS = {
  all: { label: "All", icon: Inbox },
  unread: { label: "Unread", icon: CircleDot },
  mentions: { label: "Mentions", icon: AtSign },
  pinned: { label: "Pinned", icon: Pin },
} as const;

type Filter = keyof typeof FILTERS;

function isFilter(value: unknown): value is Filter {
  return typeof value === "string" && value in FILTERS;
}

export function FilterChipFilterButtonContent() {
  const [filter, setFilter] = useState<Filter>("unread");
  const triggerRef = useRef<HTMLButtonElement>(null);
  const active = FILTERS[filter];

  const handleValueChange = useCallback((value: unknown) => {
    if (isFilter(value)) setFilter(value);
  }, []);

  const handleClear = useCallback(() => {
    setFilter("all");
    // The chip and its × go away: focus moves to the Filter button.
    triggerRef.current?.focus();
  }, []);

  return (
    <div className="flex min-w-0 items-center gap-2">
      <DropdownMenu>
        <DropdownMenuTrigger ref={triggerRef} render={<PickerTriggerButton />}>
          <ListFilter aria-hidden data-icon="inline-start" />
          Filter
          <span data-picker-trigger-indicator="" aria-hidden="true">
            <ChevronDown />
          </span>
        </DropdownMenuTrigger>
        <DropdownMenuContent aria-label="Filter activity">
          <DropdownMenuRadioGroup value={filter} onValueChange={handleValueChange}>
            <DropdownMenuRadioItem value="all">
              <Inbox aria-hidden />
              <DropdownMenuItemLabel>All</DropdownMenuItemLabel>
            </DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="unread">
              <CircleDot aria-hidden />
              <DropdownMenuItemLabel>Unread</DropdownMenuItemLabel>
            </DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="mentions">
              <AtSign aria-hidden />
              <DropdownMenuItemLabel>Mentions</DropdownMenuItemLabel>
            </DropdownMenuRadioItem>
            <DropdownMenuSeparator />
            <DropdownMenuRadioItem value="pinned">
              <Pin aria-hidden />
              <DropdownMenuItemLabel>Pinned</DropdownMenuItemLabel>
            </DropdownMenuRadioItem>
          </DropdownMenuRadioGroup>
        </DropdownMenuContent>
      </DropdownMenu>
      {filter !== "all" && (
        <FilterChip>
          <active.icon aria-hidden />
          <FilterChipLabel>{active.label}</FilterChipLabel>
          <FilterChipRemove aria-label="Clear filter" onClick={handleClear} />
        </FilterChip>
      )}
    </div>
  );
}
```

## 何时使用
| 情况 | 使用 |
| --- | --- |
| 人们很少改动、凭个人习惯设置的筛选，按地方为他们记住 | FilterChip，放在始终写着「筛选」的 Filter 按钮旁边 |
| 人们工作时经常改动的筛选，或者它的值就是这个视图要点的筛选（排序、频道、负责人） | 显示当前值的筛选按钮（[PickerTriggerButton](https://raft-design.botiverse.build/zh/components/picker-trigger-button.md)） |
| 能放多个值的输入框里选中的一个值（人、标签） | 组合框自己的标签片（[Combobox](https://raft-design.botiverse.build/zh/components/combobox.md)） |
| 不能移除的标签 | [Badge](https://raft-design.botiverse.build/zh/components/badge.md) |

## 结构
- `FilterChip`：标签片本身。必需。
- 开头的图标：可选，14px，和菜单里这个筛选的选项用同一个图标。
- `FilterChipLabel`：筛选的名称，和菜单里写的一样（「未读」）。必需。
- `FilterChipRemove`：把筛选恢复为默认值的 ×。必需。

把标签片紧接着放在 Filter 按钮后面，相距 8px，在同一行里。

## 状态
- 标签片本身没有悬停状态：只有 × 有悬停和按下，用中档填充（[悬停与按下](https://raft-design.botiverse.build/zh/foundations/states.md#hover-and-press)）。
- × 的键盘焦点环画在它自己的框里面，因为标签片太紧，环画不到外面。

## 行为
- 只在筛选不同于默认值时渲染标签片。是默认值时，Filter 按钮单独放着。
- 按 × 一步把筛选恢复为默认值，不打开菜单。然后标签片消失。
- 标签片和 Filter 按钮一样高，32px，在手机上也是。× 是 24px，离标签片末端 4px；在触屏上，它的点击区域扩大到 44px，是一个透明的 `::after`，上下各伸出 10px，朝标签伸出 6px，朝标签片末端伸出 14px（[无障碍 › 点击区域](https://raft-design.botiverse.build/zh/foundations/accessibility.md#hit-targets)）。朝标签那一侧，它越过 6px 的间隔，止于标签的边缘，不再往前，这样点在标签末尾不会被当成点了 ×。朝末端那一侧，它伸出标签片末端 10px，所以在触屏上，下一个控件离标签片至少 16px（那个控件自己 32px 的按钮会朝标签片伸出 6px），或者让标签片排在这一行的最后。它的墨色比开头图标的墨色离边缘更远：和标签上的 × 一样（[对齐 › 标签片](https://raft-design.botiverse.build/zh/foundations/alignment.md#chips)），它保持自己的内缩距离，而不是去对齐墨色到边缘的距离。
- 一行太窄时，标签截断，图标和 × 保留。

## 内容
- 筛选的名称和它在菜单里的选项完全一样：「未读」「提及」「已置顶」。不要加上「筛选」这个词，也不要加动词。
- 在 × 的 `aria-label` 里，按它做的事给它命名：「清除筛选」；默认值是「全部」时写「显示全部」。

## 无障碍
- FilterChip 和 FilterChipLabel 是纯文字。标签片没有角色：读屏软件在这一行的文字中读出它的标签。
- FilterChipRemove 封装了 Base UI 的 Button，并且必须有 `aria-label`，因为它只显示一个符号。
- × 会把自己移除。在它的按下处理函数里，把焦点移到 Filter 按钮上，这样键盘焦点留在这一行里，而不是掉回页面。
- 开头的图标对辅助技术隐藏（`aria-hidden`）。

## 规则
- 只在始终写着「筛选」的 Filter 按钮旁边用 FilterChip：显示值的筛选按钮已经写出了筛选，再加一个标签片就说了两遍。
- 每个 Filter 按钮只显示一个标签片，对应它应用的那一个筛选。
- 不要把标签片本身做成按钮。Filter 按钮改变筛选；× 只是把它恢复为默认值。

## API

### FilterChip

The filter in effect, beside the filter button that chose it: an `info-soft` chip as high as the button, with an optional 14px icon, a `FilterChipLabel` and a `FilterChipRemove` × that resets the filter.

Also takes the attributes of its element.

### FilterChipLabel

The name of the active filter. It truncates when the row has no room.

Also takes the attributes of its element.

### FilterChipRemove

The × that resets the filter to its default. Shows an X unless given children.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `aria-label` (required) | `string` |  | What the × does, said in words: the chip shows only a glyph. Name the result, such as "Clear filter" or "Show all". |
| `focusableWhenDisabled` | `boolean` | `false` | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.
