# 底部面板 · BottomSheet

- 页面: https://jiguang132.com/zh/resources/astryx/docs/bottom-sheet
- 资源: Astryx (https://jiguang132.com/zh/resources/astryx.md)
- 全部指南: https://jiguang132.com/zh/resources/astryx/docs.md

在视口底部展示移动端友好的面板。

## 安装与前置条件

```bash
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0
```

需要 React 19+，按顺序导入基础 CSS 和主题，并设置 Theme Provider。参见[开始使用](/resources/astryx/docs/getting-started)。

## 示例与使用方式

- **底部面板 · 综合展示 1**：首先体验这个示例，再查看代码理解组件的组合方式。
- **底部面板 · 综合展示 2**：可交互查看这一用法的行为，复制代码时保留它实际使用的属性与导入。
- **底部面板 · 交互示例 3**：可交互查看这一用法的行为，复制代码时保留它实际使用的属性与导入。
- **底部面板 · 键盘操作 4**：可交互查看这一用法的行为，复制代码时保留它实际使用的属性与导入。
- **底部面板 · 交互示例 5**：可交互查看这一用法的行为，复制代码时保留它实际使用的属性与导入。
- **底部面板 · 交互示例 6**：可交互查看这一用法的行为，复制代码时保留它实际使用的属性与导入。
- **底部面板 · 交互示例 7**：可交互查看这一用法的行为，复制代码时保留它实际使用的属性与导入。

## API 与上游详细参考

优先使用 0.7.1 官方中文说明；上游尚未提供中文的条目保留英文。属性名、类型及默认值保持原生 API，不把示例状态名称当成组件属性。

## Bottom Sheet

A mobile touch surface for filters, actions, forms, and detail views that should rise from the bottom of the viewport; use BottomSheetSwitcher for multi-step flows.

| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| isOpen | boolean |  | Whether a standalone sheet is open. Fully controlled; pair with onOpenChange. Omit inside BottomSheetSwitcher. |
| onOpenChange | (isOpen: boolean) => void |  | For a standalone sheet, called when it requests an open-state change. Automatic calls follow purpose: info dismisses on Escape, scrim click, or swipe; form dismisses on Escape only; required never dismisses implicitly. Omit inside BottomSheetSwitcher. |
| finalFocusRef | RefObject<HTMLElement \| null> |  | Optional explicit focus-return target for a standalone sheet. Use when the opener can remount or the active element is not a reliable trigger, such as an adaptive presentation switch. Omit inside BottomSheetSwitcher. |
| purpose | 'required' \| 'form' \| 'info' | 'info' | Controls implicit dismissal behavior, matching Dialog. info allows Escape, scrim click, and swipe-to-dismiss. form protects entered data by blocking scrim click and swipe while allowing Escape. required blocks every implicit dismissal path and uses role='alertdialog'. Explicit controls may still update the controlled state. Works for standalone and BottomSheetSwitcher-managed sheets. |
| sheetId | string |  | Unique ID for this sheet inside BottomSheetSwitcher. The switcher opens it when activeSheet matches. Omit isOpen and onOpenChange when sheetId is used. |
| label * | string |  | Accessible label for the sheet. Required; the sheet has no built-in heading to derive a name from. |
| children * | ReactNode |  | Sheet content in a scrollable area. The named body is keyboard reachable while overflowing. Forward Tab entry may move directly to the first native link or button; input controls, composite widgets, and nested scroll areas retain the body stop. Shift+Tab from a delegated first child skips the body. Fitting content adds no body stop. The internal observed content box preserves block flow and percentage heights. If it includes a text-entry control that can bring up the mobile keyboard, use height='tall' and keep the sheet fully expanded while editing. |
| padding | 0 \| 0.5 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 5 \| 6 \| 8 \| 10 |  | Internal padding of the sheet content using the spacing scale, matching Dialog. When omitted, uses the theme default for bottom sheets (--spacing-4 unless the theme sets padding on bottom-sheet). A lone Section child escapes the padding, and bleed children such as Table and Divider compensate against it. Use 0 for edge-to-edge content that supplies its own inset. |
| height | 'hug' \| 'capped' \| 'tall' \| number \| string | 'capped' | How tall the sheet is. Named budgets: 'hug' fits its content up to 92% of the viewport, 'capped' is a scrolling mid-height panel (~62%), and 'tall' is a pinned near-full panel (~92%) for content that streams in. Or pass a number (px) / CSS length for a custom budget. Give snapPoints to let the user drag between heights. On shorter viewports the sheet fills the available height. Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation: it stays put and scrolls each focused control above the keyboard. Hug, Capped, numeric and CSS-length heights never do, and a Tall sheet stops doing it the moment the user drags it to a shorter detent, resuming when they drag it back. Outside that state the sheet neither moves nor adds keyboard scroll space, and the browser's own focus reveal is left in place; on iOS that reveal can shift the whole page. |
| snapPoints | ReadonlyArray<number \| string> |  | Extra heights the sheet can rest at when dragged; its own height is always the tallest stop, and omitting this gives a sheet that only opens and closes. Each stop is the sheet's visible height: a number is a viewport fraction (0.5 is half the screen), '50%' the same in CSS, '320px' an absolute length. A stop of a quarter of the sheet or less is a peek: it slides away instead of reflowing, and thins the scrim. |
| hasScrim | boolean | true | For a standalone BottomSheet, whether to render a scrim, the semi-transparent overlay that covers and blocks the background. true (default) uses showModal(): top layer, focus trap, ::backdrop scrim, body scroll lock, and tap-scrim-to-dismiss when purpose='info', with the background inert. false uses show() with no scrim, leaving the page behind interactive and scrollable. For a multi-step flow, configure hasScrim on BottomSheetSwitcher instead; it owns one shared dialog across every child. |

### 使用建议

- Use for mobile-first surfaces (filters, share sheets, quick actions) where the content should rise from the bottom edge.
- Pick the starting height that fits the content: 'hug' for short bounded content, 'capped' for lists, and 'tall' for forms or streaming/resizing content.
- Use purpose='form' to protect entered data from scrim clicks and swipes while keeping Escape available; reserve purpose='required' for flows that must end through an explicit action.
- 避免：Don't make the sheet content overly long. Consider breaking it into steps and using Bottom Sheet Switcher.

### Basic

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Filters">
  <FilterControls />
</BottomSheet>
```

### Tall sheet (a list)

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Nearby places"
  height="tall">
  <PlaceList />
</BottomSheet>
```

### Collapsible to half the screen

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Nearby places"
  height="tall"
  snapPoints={[0.5]}>
  <PlaceList />
</BottomSheet>
```

### A peek, a working height, and full

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Route"
  height="tall"
  snapPoints={['96px', '50%']}>
  <RouteDetails />
</BottomSheet>
```

### Hug height (fits content)

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Share"
  height="hug">
  <ShareActions />
</BottomSheet>
```

### Long scrolling content

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Release notes"
  height="hug">
  <ReleaseNotes />
</BottomSheet>
```

### Multi-step flow (one sheet at a time)

```tsx
const [activeSheet, setActiveSheet] = useState(null);
<>
  <Button label="Start" onClick={() => setActiveSheet('details')} />
  <BottomSheetSwitcher
    activeSheet={activeSheet}
    onActiveSheetChange={setActiveSheet}>
    <BottomSheet sheetId="details" label="Details">
      <Button label="Continue" onClick={() => setActiveSheet('confirm')} />
    </BottomSheet>
    <BottomSheet sheetId="confirm" label="Confirm">
      <Button label="Back" onClick={() => setActiveSheet('details')} />
    </BottomSheet>
  </BottomSheetSwitcher>
</>
```

### Mobile keyboard

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Add a comment"
  height="tall">
  <LongCommentForm />
</BottomSheet>
```

### No scrim

```tsx
const [isOpen, setIsOpen] = useState(true);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  label="Nearby places"
  hasScrim={false}>
  <PlaceList />
</BottomSheet>
```

### Protect form input

```tsx
const [isOpen, setIsOpen] = useState(false);
<BottomSheet
  isOpen={isOpen}
  onOpenChange={setIsOpen}
  purpose="form"
  label="Edit profile">
  <ProfileForm onSave={() => setIsOpen(false)} />
</BottomSheet>
```

### 主题 API

```json
{
  "container": true,
  "targets": [
    {
      "className": "astryx-bottom-sheet",
      "visualProps": []
    }
  ],
  "derived": [
    {
      "property": "padding",
      "expand": "container"
    }
  ]
}
```

### 组件结构

- **Sheet panel**: Painted surface that rises from the bottom edge and contains the sheet.
- **Content area**: Padded, scrollable container that presents the caller-provided sheet content.
- **Handle**: Decorative grab affordance and drag region at the top of the panel.
- **Scrim**: Backdrop that dims and blocks the page in a scrim-backed presentation.

## Bottom Sheet Switcher

Coordinates a multi-step bottom-sheet flow in one shared dialog; set activeSheet to a nested BottomSheet's sheetId to open or switch steps, and to null to close.

| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| ref | Ref<HTMLDialogElement> |  | Ref forwarded to the one shared native dialog. |
| onCancel | (event: SyntheticEvent<HTMLDialogElement>) => void |  | Called before the switcher handles a native dialog cancel request. Calling preventDefault() keeps the controlled flow open. |
| activeSheet * | string \| null |  | ID of the interactive BottomSheet, or null when the flow should close. Match a nested BottomSheet's unique sheetId; the previous sheet may remain visually present and inert while the new sheet enters, simultaneously align downward behind a shorter step, then fade away. |
| onActiveSheetChange * | (activeSheet: string \| null) => void |  | Called with null when the active sheet dismisses according to its purpose. Child BottomSheets may use purpose='form' or purpose='required' to limit implicit dismissal while flow controls can still use the same state setter to switch sheets or close the flow. |
| hasScrim | boolean | true | Whether the shared dialog is modal. true uses showModal() once for one native ::backdrop, focus trap, scroll lock, and click-to-dismiss when the active BottomSheet has purpose='info'. false uses show() with no backdrop and leaves the page interactive; avoid transformed, contained, or clipping ancestors because the non-modal dialog remains in its containing context. |
| children * | ReactNode |  | BottomSheets identified by unique sheetId values. |

### 使用建议

- Use when each step depends on the previous one and only one step needs attention at a time.
- Give every child a unique sheetId and non-empty label, choose its purpose to match dismissal requirements, and follow the WAI-ARIA Dialog (Modal) pattern for scrim-backed flows: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/.
- 避免：Don't split information across sheets when people need to compare it; use a full-page layout that keeps the relevant content visible together instead.
- 避免：Don't use the switcher when multiple panels must stay interactive or visible together; activeSheet intentionally selects one interactive step.

### Three-step flow

```tsx
const [activeSheet, setActiveSheet] = useState(null);

<>
  <Button label="Start" onClick={() => setActiveSheet('details')} />
  <BottomSheetSwitcher
    activeSheet={activeSheet}
    onActiveSheetChange={setActiveSheet}>
    <BottomSheet sheetId="details" label="Details" height="hug">
      <SetupDetails />
      <Button label="Continue" onClick={() => setActiveSheet('preferences')} />
    </BottomSheet>
    <BottomSheet sheetId="preferences" label="Preferences" height="hug">
      <Preferences />
      <Button label="Back" onClick={() => setActiveSheet('details')} />
      <Button label="Continue" onClick={() => setActiveSheet('confirm')} />
    </BottomSheet>
    <BottomSheet sheetId="confirm" label="Confirm" height="hug">
      <Confirmation />
      <Button label="Back" onClick={() => setActiveSheet('preferences')} />
      <Button label="Done" onClick={() => setActiveSheet(null)} />
    </BottomSheet>
  </BottomSheetSwitcher>
</>
```

### 组件结构

- **Shared dialog**: One native dialog that owns modality, focus, dismissal, and lifecycle for the complete flow.
- **Sheet panels**: Direct BottomSheet children; exactly one is interactive while a previous panel may remain visible and inert during a handoff.
- **Scrim**: Native dialog backdrop shown by the default scrim-backed modal presentation.


## 完整示例的额外依赖

部分示例使用独立图标包。示例清单中的每条安装命令包含该示例实际需要的依赖；若要运行本页全部示例，还需安装：

```bash
npm install @heroicons/react@2.2.0
```

---

JGuang 全部资源索引: https://jiguang132.com/llms.txt
