# 底部弹窗切换器

## 使用方法

把一串分步的底部弹窗放进同一个对话框里。用 `v-model:active-sheet` 指向某张 `BottomSheet` 的 `sheet-id` 就打开或切到那一步，指向 `null` 就整段关掉。

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

const activeSheet = ref<string | null>(null)
</script>

<template>
  <button type="button" @click="activeSheet = 'details'">
    开始结账
  </button>
  <BottomSheetSwitcher v-model:active-sheet="activeSheet">
    <BottomSheet sheet-id="details" label="订单信息">
      <p>确认一下订单。</p>
    </BottomSheet>
    <BottomSheet sheet-id="review" label="核对">
      <p>付款前再核对一遍。</p>
    </BottomSheet>
  </BottomSheetSwitcher>
</template>
```

每一张子面板都要有自己的 `sheet-id` 和一个非空的 `label`。对话框的名字优先用切换器自己的 `label`，没写就取当前那张的 `label`，所以整段流程从头到尾只有一个无障碍名称。只有 `sheet-id` 和 `active-sheet` 对上的那一张可以交互；换页时上一张还留在底下，但已经不可交互，等新的一张落定之后才撤掉，所以切步骤的时候遮罩不会闪一下。

`purpose` 决定整段流程怎么被关掉，和 `Dialog` 一致。不传就跟着当前那张走：`info` 允许 Escape、点遮罩和下滑，`form` 只允许 Escape，免得把填到一半的内容丢掉，`required` 堵住所有隐式关闭。`hasScrim` 默认是 `true`，整段流程共用一层遮罩：换页时它一直留着，只有整段关掉才消失，这样多步流程看起来才是一块完整的表面，而不是好几个对话框连着弹。

按 Escape 会请求关闭。在此之前先发 `cancel` 事件，参数就是原生的 cancel 事件；在它上面调 `event.preventDefault()` 就能留住这段流程。

```ts
import { BottomSheet, BottomSheetSwitcher } from '@astryx-vue/core/BottomSheet'
```

### 结构

- **共享对话框**: 一个原生对话框，整段流程的模态、焦点、关闭方式和生命周期都由它负责。
- **面板**: 直接放在里面的 `BottomSheet` 子节点；同一时刻只有一张可以交互，交接过程中上一张可能还看得见，但已经不可交互。
- **遮罩**: 默认带遮罩的呈现方式里，原生对话框背后那层背景。

## 最佳实践

| 指引 | 实践 |
| --- | --- |
| 推荐 | 只在后一步依赖前一步、而且同一时间只需要看一步时使用。 |
| 推荐 | 给每张子面板一个不重复的 `sheet-id` 和非空的 `label`，按关闭要求选好各自的 `purpose`；带遮罩的流程请按 [WAI-ARIA 对话框（模态）模式](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)来做。 |
| 避免 | 把需要互相对照的信息拆到几张面板里。这种情况用整页布局，让相关内容能同时看到。 |
| 避免 | 需要多张面板同时可交互或同时可见时使用切换器。`active-sheet` 本来就是只让一步可交互。 |

## 示例

常见的用法和不同的状态。

### 核对流程

两步的表单流程：先核对设置，再确认保存，也可以退回上一步，全程不换对话框。

```vue
<script setup lang="ts">
import { BottomSheet, BottomSheetSwitcher } from '@astryx-vue/core/BottomSheet'
import { Button } from '@astryx-vue/core/Button'
import { Heading } from '@astryx-vue/core/Heading'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { shallowRef } from 'vue'

const activeSheet = shallowRef<string | null>(null)
</script>

<template>
  <Button label="Review settings" variant="secondary" @click="activeSheet = 'review'" />
  <BottomSheetSwitcher v-model:active-sheet="activeSheet" label="Review settings">
    <BottomSheet sheet-id="review" label="Review settings" height="hug">
      <VStack :gap="4">
        <VStack :gap="1">
          <Heading :level="3">
            Review settings
          </Heading>
          <Text color="secondary" type="supporting">
            Daily summaries will be sent by email.
          </Text>
        </VStack>
        <HStack :gap="2">
          <Button label="Cancel" variant="secondary" @click="activeSheet = null" />
          <Button label="Continue" variant="secondary" @click="activeSheet = 'confirm'" />
        </HStack>
      </VStack>
    </BottomSheet>

    <BottomSheet sheet-id="confirm" label="Confirm settings" height="hug">
      <VStack :gap="4">
        <VStack :gap="1">
          <Heading :level="3">
            Confirm settings
          </Heading>
          <Text color="secondary" type="supporting">
            Your notification settings are ready to save.
          </Text>
        </VStack>
        <HStack :gap="2">
          <Button label="Back" variant="secondary" @click="activeSheet = 'review'" />
          <Button label="Save" variant="secondary" @click="activeSheet = null" />
        </HStack>
      </VStack>
    </BottomSheet>
  </BottomSheetSwitcher>
</template>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `activeSheet` | `string \| null` | `null` | 用 `v-model:active-sheet` 绑定。值是当前要显示那一步的 `sheet-id`；绑成 `null` 就整段关掉。 |
| `label` | `string` | — | 整段流程的读屏名称。不传就用当前那张面板自己的 `label`，都没有时用 `Bottom sheet`。 |
| `purpose` | `'info' \| 'form' \| 'required'` | 跟当前面板的 `purpose` | 整段流程怎么被关掉。`info` 允许 Escape、点遮罩和下滑；`form` 只允许 Escape；`required` 堵住所有隐式关闭。 |
| `hasScrim` | `boolean` | `true` | 整段流程共用一层遮罩。换页时它一直留着，只有整段关掉才消失。关掉它，背后页面仍然可以滚动和点击。 |

## 事件

- `update:activeSheet(value: string | null)`：切换器想切到哪一步就发哪个 `sheet-id`，想关掉时发 `null`。这就是 `v-model:active-sheet` 的绑定。
- `cancel(event: Event)`：对话框收到原生 cancel 请求时先发这个事件。在事件上调用 `event.preventDefault()` 可以留住这段流程，不让它关掉。

## 插槽

- `default`：放各个 `BottomSheet` 步骤。每一张都要有自己不重复的 `sheet-id` 和自己的 `label`。
