# 勾选指示器

## 使用方法

指示器是复选框、单选按钮、选择器和 Markdown 任务列表共用的选择图形。它们只负责把状态画出来。真正的控件保留输入、角色、名称、焦点和键盘。正因为拆开了，图形才可以换：先改它的类，形状不对时再按名字替换组件。

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

<template>
  <CheckIndicator state="checked" />
</template>
```

这是选中项上的标记。未选中、插槽里又没有内容时，它什么都不渲染，列表不会在每一行旁边留一个空盒子。它没有自己的外框，本身就是那枚图形，所以宿主传入的类会落在图标同一个元素上。

想让所有单选标记都变成圆点时，把这个名字映射到 [单选指示器](/components/radio-indicator)。没选中的行也会画出空心圆。方框是 [复选指示器](/components/checkbox-indicator)。

## 结构

| 元素 | 说明 |
| --- | --- |
| 外框（方框、圆点） | 方框和圆点必需，它们每种状态都画。勾选指示器自己就是状态标记，没有外框。 |
| 状态标记 | 选中时画出的勾。未选中时，除非插槽里有可渲染的内容，否则什么都不画。 |

## 最佳实践

| 指引 | 实践 |
| --- | --- |
| 推荐 | 先改稳定的类名。形状本身不对时，再替换组件。 |
| 推荐 | 用 isRenderable 判断插槽是否替换标记，不要用空值判断。它只排除空值、布尔值和空字符串。元素算数，哪怕里面后来是空的。宿主用 v-if="busy" 放转圈。条件为假时，空值判断会把已选中的勾删掉。 |
| 推荐 | 替换图形必须隐藏起来。角色和名称由外面的控件提供，再宣布一次就会读两遍。 |
| 推荐 | 替换图形里的颜色、圆角和边框宽度都用主题变量。 |
| 推荐 | 只渲染一个根元素，并保留焦点环要跟着走的圆角。真正的输入被藏起来时，控件把焦点环画在指示器上。不要再自己画一圈。 |
| 避免 | 把悬停或按下当成属性传进去。悬停通过祖先上的 astryx-indicator-scope 到达图形。 |
| 避免 | 以为只有选中时才会挂上图形。宿主每种状态都会渲染，并传入状态。替换图形才能在未选中的行上画出空心圆。 |

## 示例

### 状态

未选中不留盒子。选中时两种尺寸都画这枚图形。禁用时用禁用色。

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

<template>
  <div class="indicator-board">
    <div v-for="size in (['sm', 'md'] as const)" :key="size" class="indicator-board-row">
      <span class="checkbox-indicator-preview-size">{{ size }}</span>
      <span class="indicator-board-item">
        <CheckIndicator state="unchecked" :size="size" />
        <span>Unchecked</span>
      </span>
      <span class="indicator-board-item">
        <CheckIndicator state="checked" :size="size" />
        <span>Checked</span>
      </span>
      <span class="indicator-board-item">
        <CheckIndicator state="checked" :size="size" is-disabled />
        <span>Disabled</span>
      </span>
    </div>
  </div>
</template>
```

### 按名字替换

同一行标记。左边是默认的勾。右边包在主题里之后变成圆点，没选中也会画出空心圆。

```vue
<script setup lang="ts">
import type { IndicatorState } from '@astryx-vue/core/Indicator'
import { RadioIndicator, useIndicator } from '@astryx-vue/core/Indicator'
import { defineTheme, Theme } from '@astryx-vue/core/Theme'
import { defineComponent, h } from 'vue'

const radioMarks = defineTheme({
  name: 'radio-marks',
  indicators: { check: RadioIndicator },
})

const states = ['unchecked', 'checked'] as const satisfies readonly IndicatorState<'singleSelection'>[]

const Marks = defineComponent({
  name: 'IndicatorMarkRow',
  setup() {
    const Mark = useIndicator('check')
    return () => h('div', { class: 'indicator-board-row' }, states.map(state =>
      h(Mark.value, { state, size: 'md' }),
    ))
  },
})
</script>

<template>
  <div class="indicator-replace">
    <div class="indicator-replace-card">
      <Marks />
      <span>Default check</span>
    </div>
    <Theme :theme="radioMarks">
      <div class="indicator-replace-card">
        <Marks />
        <span>Radio instead</span>
      </div>
    </Theme>
  </div>
</template>
```

### 加载中替换标记

插槽里的转圈会占住勾的位置，选中与否都一样。关掉之后，未选中的行不再占位。

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { CheckboxIndicator } from '@astryx-vue/core/CheckboxIndicator'
import { CheckIndicator } from '@astryx-vue/core/Indicator'
import { ref } from 'vue'

const busy = ref(true)
</script>

<template>
  <div class="indicator-board">
    <div class="indicator-board-row">
      <span class="indicator-board-item">
        <CheckboxIndicator state="checked">
          <span v-if="busy" class="indicator-spinner" />
        </CheckboxIndicator>
        <span>Checkbox</span>
      </span>
      <span class="indicator-board-item">
        <CheckIndicator state="unchecked">
          <span v-if="busy" class="indicator-spinner" />
        </CheckIndicator>
        <span>Check</span>
      </span>
      <Button size="sm" variant="secondary" :label="busy ? 'Show marks' : 'Show spinner'" @click="busy = !busy" />
    </div>
  </div>
</template>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `state` | `'unchecked' \| 'checked'` | — | 必需。默认在未选中时什么都不画。替换图形可以两种状态都画。 |
| `size` | `'sm' \| 'md'` | `'md'` | 写到元素上。图形本身两种尺寸都是 1rem，和小图标一致。 |
| `isDisabled` | `boolean` | false | 只改变外观。选中的图形用禁用色。禁用的语义留在外面的行上。 |

## 插槽

- 默认插槽在每种状态都替换标记，选中与否都一样。宿主用它传入等待中的转圈。
- 插槽里有可渲染内容时，用 1rem 的盒子占住位置。
- 没有可渲染内容时，未选中的勾不渲染。用 isRenderable 判断。条件为假时不能把已选中的勾删掉。
- 插槽内容只作装饰。不要放可聚焦控件。

## 无障碍

- 勾被画出来，或插槽占住位置时，根元素是 `aria-hidden="true"`。
- 组件不设置交互角色，也不提供点击或键盘行为。
- 传入的角色、Tab 顺序、可编辑属性，以及以 aria 开头的属性会被丢掉。
- 未选中且插槽为空时，暴露的元素为空。

## 主题

没有外框类。选中的勾就是图标本身。插槽占位是 `.astryx-check-indicator`，宽高 1rem。

- 选中用强调色图标，禁用用禁用色。
- `data-size` 是 sm 或 md。
- 行禁用时设置 `data-disabled`。
