# 滚动条 Provider

## 使用方法

把应用包一层，所有会滚动的 Astryx 组件就从浏览器自带的滚动条，换成一套自绘的覆盖式滚动条：细、不占宽度、不滚的时候自己收起来，明暗主题自动跟着走。

这是 astryx-vue 额外加的能力，原版 Astryx 没有。底层用的是 [OverlayScrollbars](https://kingsora.github.io/OverlayScrollbars/)（MIT 协议），已经内置在 `@astryx-vue/core` 里，不用自己装。

**默认是关的。** 不套 `ScrollbarsProvider`，组件和以前完全一样。而且没套的时候，OverlayScrollbars 的 JS 和 CSS 都不会进你的包，因为只有 `ScrollbarsProvider` 会引它们。

```ts
import { ScrollbarsProvider } from '@astryx-vue/core/Scrollbars'
```

30 秒接入，在 `App.vue` 里套一层就行：

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

<template>
  <ScrollbarsProvider>
    <RouterView />
  </ScrollbarsProvider>
</template>
```

常用的几个属性：

- `enabled`：开关，默认 `true`。设成 `false` 就整棵子树回到原生滚动条。
- `autoHide`：什么时候把滚动条收起来。`'scroll'`（默认）是滚的时候才出现，`'leave'` 是鼠标离开后收起，`'move'` 是鼠标动的时候出现，`'never'` 是一直显示。
- `autoHideDelay`：收起前等多少毫秒，默认 800。
- `options`：直接传给 OverlayScrollbars 的选项，优先级最高，后面有例子。

## 示例

### Scrollbars Provider — 开关对比

一块带长列表的 ScrollableArea，一块有横向滚动的 CodeBlock，一起包在 ScrollbarsProvider 里。打开和关掉开关，对比原生滚动条和自绘滚动条。

```vue
<script setup lang="ts">
import { CodeBlock } from '@astryx-vue/core/Code'
import { ScrollableArea } from '@astryx-vue/core/ScrollableArea'
import { ScrollbarsProvider } from '@astryx-vue/core/Scrollbars'
import { Switch } from '@astryx-vue/core/Switch'
import { Text } from '@astryx-vue/core/Text'
import { shallowRef } from 'vue'

const on = shallowRef(true)

const notes = Array.from({ length: 24 }, (_, index) => `Release note ${index + 1}`)

const code = [
  'const message = "This line is long enough that it has to scroll sideways, so the horizontal scrollbar shows up under the code.";',
  'const another = "Hover the code block, or scroll it, and the thin bar appears without taking any width from the content.";',
  'export { message, another };',
].join('\n')
</script>

<template>
  <div class="scrollbars-provider-preview">
    <Switch v-model="on" label="Custom scrollbars" />
    <ScrollbarsProvider :enabled="on">
      <div class="scrollbars-provider-preview__panes">
        <ScrollableArea label="Release notes" :height="200" :padding="3">
          <div v-for="note in notes" :key="note" class="scrollbars-provider-preview__row">
            <Text type="body">
              {{ note }}
            </Text>
          </div>
        </ScrollableArea>
        <CodeBlock :code="code" language="typescript" title="long-lines.ts" />
      </div>
    </ScrollbarsProvider>
  </div>
</template>
```

### Scrollbars Provider — 自己的滚动容器

一个自己写的 overflow: auto 的 div，用 useScrollbars 接上自绘滚动条。外观用 CSS 变量改：这里调粗了滚动条，换成了强调色。

```vue
<script setup lang="ts">
import { ScrollbarsProvider, useScrollbars } from '@astryx-vue/core/Scrollbars'
import { Text } from '@astryx-vue/core/Text'
import { defineComponent, h, shallowRef } from 'vue'

/**
 * `useScrollbars` reads the nearest provider, so the hook lives in a child
 * component that sits inside `ScrollbarsProvider`.
 */
const Feed = defineComponent({
  name: 'DemoFeed',
  setup() {
    const el = shallowRef<HTMLElement | null>(null)
    const { active } = useScrollbars(el)
    const rows = Array.from({ length: 20 }, (_, index) => `Event ${index + 1}`)
    return () => h('div', {
      'ref': el,
      'class': 'scrollbars-provider-preview__feed',
      'data-active': String(active.value),
      'tabindex': 0,
      'role': 'region',
      'aria-label': 'Event feed',
    }, rows.map(row => h('div', { key: row, class: 'scrollbars-provider-preview__row' }, h(Text, { type: 'body' }, () => row))))
  },
})
</script>

<template>
  <ScrollbarsProvider>
    <Feed />
  </ScrollbarsProvider>
</template>
```

## 会自动换上的组件

套了 Provider 之后，下面这些组件里的滚动区域会自动换成自绘滚动条，不用你改代码：

- 滚动容器：`ScrollableArea`、`Layout`（`LayoutContent` 和 `LayoutPanel` 可滚动时）、`Card`、`Stack` 和 `StackItem`（可滚动时）
- 菜单和选择：`DropdownMenu`、`ContextMenu`、`MoreMenu`、`Selector`、`MultiSelector`、`ComplexSelector`、`CommandPalette`
- 输入候选：`Typeahead`、`Tokenizer`、`PowerSearch` 的候选列表，`TimeInput`、`DateRangeInput`
- 浮层：`Popover`、`AlertDialog`、`BottomSheet`
- 导航：`SideNav`、`MobileNav`
- 数据展示：`Table`、`CodeBlock`
- 对话：`Chat` 的消息区和触发菜单

## 不会换的地方

- `Chat` 的输入框。它是可编辑区，换了会影响输入。
- `<textarea>`。
- `Markdown` 里表格的横向滚动。
- 页面本身（`body`）的滚动条。

这些地方还是原生滚动条。

## 自己的滚动容器

首选 [`ScrollableArea`](/components/scrollable-area)。它自带无障碍名称、键盘聚焦和溢出判断，套了 Provider 就自动换上自绘滚动条，不用再想别的。

只有一种情况需要 `useScrollbars`：你已经有一个自己写的滚动容器，不想换成 `ScrollableArea`。把这个元素的 ref 交给它：

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

const el = shallowRef<HTMLElement | null>(null)
const { active } = useScrollbars(el)
</script>

<template>
  <div ref="el" class="feed">
    <!-- 很长的内容 -->
  </div>
</template>

<style scoped>
.feed {
  height: 240px;
  overflow: auto;
}
</style>
```

几个要点：

- 容器自己就是滚动的视口，里面的子节点不会被搬动。它只会在容器末尾多追加两个滚动条用的 `div`。
- 外层没有 `ScrollbarsProvider` 时，`useScrollbars` 什么都不做，`active` 一直是 `false`。
- 容器的 `overflow` 是什么，滚动条就跟着是什么。你自己要保证它能滚。
- `useScrollbars` 要在 `ScrollbarsProvider` 的**后代组件**里调用。在写 Provider 的那个组件里直接调，是读不到的。

## 局部关掉

只想让某一块保留原生滚动条，有三种办法，看你想关多大的范围。

**关掉一块 `ScrollableArea`：**

```vue
<ScrollableArea label="日志" :height="240" :scrollbars="false">
  ...
</ScrollableArea>
```

**关掉一棵子树。** 嵌套一层 Provider，里面的覆盖外面的：

```vue
<ScrollbarsProvider>
  <Main />
  <ScrollbarsProvider :enabled="false">
    <Sidebar />
  </ScrollbarsProvider>
</ScrollbarsProvider>
```

**关掉自己的某个容器。** 给 `useScrollbars` 传 `enabled`，可以是布尔值、ref 或者 getter：

```ts
const { active } = useScrollbars(el, { enabled: () => props.fancy })
```

外层 Provider 是关的时候，这里写 `true` 也不会生效。

## 改外观

外观走 CSS 变量，都有默认值，并且跟着主题 token 走，不用额外配明暗。

| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `--astryx-scrollbar-size` | `10px` | 滚动条的粗细，含两侧各 2px 的留白。 |
| `--astryx-scrollbar-thumb` | 次要文字色的 45% 透明度 | 滑块的颜色。 |
| `--astryx-scrollbar-thumb-hover` | 次要文字色的 70% 透明度 | 鼠标悬停和拖动时滑块的颜色。 |
| `--astryx-scrollbar-track` | `transparent` | 轨道的背景色。 |
| `--astryx-scrollbar-radius` | `9999px` | 轨道和滑块的圆角。 |

想全站统一改，写在 `astryx.overrides` 层里：

```css
@layer astryx.overrides {
  :root {
    --astryx-scrollbar-size: 12px;
    --astryx-scrollbar-thumb: color-mix(in srgb, var(--astryx-color-text-primary) 35%, transparent);
    --astryx-scrollbar-radius: 4px;
  }
}
```

变量可以写在任何祖先元素上，所以只想改某一块，就写在那一块的选择器里。也可以放进 [`Theme`](/components/theme) 的主题变量里，跟着主题一起切。更多变量的约定见 [CSS 变量](/themes/css-variables)。

### 用 options 改行为

`options` 会原样传给 OverlayScrollbars，深合并，并且优先级最高，连 `autoHide` 都会被它盖掉。

```vue
<ScrollbarsProvider
  :options="{
    scrollbars: {
      clickScroll: 'instant',
      visibility: 'visible',
    },
  }"
>
  <App />
</ScrollbarsProvider>
```

- `clickScroll: 'instant'`：点一下轨道，视口直接跳到那里。默认是关的。想要平滑滚过去（`true`），得另外注册 OverlayScrollbars 的 `ClickScrollPlugin`。
- `visibility: 'visible'`：滚动条一直显示。默认 `'auto'`，只在内容溢出时显示。

完整的选项见 [OverlayScrollbars 文档](https://kingsora.github.io/OverlayScrollbars/)。

## 注意事项

- **服务端渲染**：首屏先显示原生滚动条，页面挂载后才换成自绘的。所以刚打开页面时可能有一瞬间的样式变化。
- **macOS**：系统默认「滚动时显示滚动条」，原生滚动条本来就是覆盖式的。在 macOS 上换完以后，主要差别是各个平台的样式统一了。
- **减少动效**：用户系统开了「减少动效」时，滚动条的过渡会关掉。
- **键盘、触屏、读屏**：行为不变。因为真正在滚动的，仍然是原来的那个元素，滚动条只是画在它上面。
- **没套 Provider**：什么都不会发生，也不会多加载任何代码。

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | 子树里要不要用自绘滚动条。设为 `false` 时子树不初始化，已经挂上的会被销毁。嵌套时内层覆盖外层。 |
| `autoHide` | `'never' \| 'scroll' \| 'leave' \| 'move'` | `'scroll'` | 滚动条自动收起的时机。`never` 一直显示，`scroll` 滚动时显示，`leave` 指针离开后收起，`move` 指针移动时显示。 |
| `autoHideDelay` | `number` | `800` | 自动收起前等待的毫秒数。 |
| `options` | `ScrollbarsOptions` | — | 透传给 OverlayScrollbars 的选项（`PartialOptions`）。最后深合并，优先级最高。 |

## 插槽

| 插槽 | 说明 |
| --- | --- |
| `default` | 使用自绘滚动条的子树。 |

## 其它导出

| 导出 | 类型 | 说明 |
| --- | --- | --- |
| `useScrollbars` | `(target: MaybeRefOrGetter, options?: UseScrollbarsOptions) => UseScrollbarsResult` | 给自己写的滚动容器挂上自绘滚动条。外层没有 `ScrollbarsProvider` 时什么都不做。容器自己就是视口，子节点不动。要在 Provider 的后代组件里调用。 |
| `UseScrollbarsOptions` | `{ enabled?: MaybeRefOrGetter<boolean> }` | `useScrollbars` 的第二个参数。`enabled` 默认 `true`，控制这个容器自己要不要用。外层 Provider 关闭时，写 `true` 也不生效。 |
| `UseScrollbarsResult` | `{ active: Readonly<Ref<boolean>> }` | `active` 表示当前元素上是否已经挂上自绘滚动条。 |
| `ScrollbarsProviderProps` | 类型 | 上面属性表对应的类型。 |
| `ScrollbarsProviderSlots` | 类型 | 上面插槽表对应的类型。 |
| `ScrollbarsOptions` | `PartialOptions` | OverlayScrollbars 的选项类型，`options` 属性用它。 |
| `ScrollbarsAutoHide` | `'never' \| 'scroll' \| 'leave' \| 'move'` | `autoHide` 的取值类型。 |
| `ScrollbarsContext` | 类型 | Provider 向子树提供的内容：`enabled`、合并后的 `options`，以及 `mount`。 |
| `ScrollbarsHandle` | `{ update, destroy }` | `mount` 返回的句柄，用来更新选项或销毁滚动条。 |
| `scrollbarsKey` | `InjectionKey` | Provider 用的注入 key。 |

## 外观变量

| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `--astryx-scrollbar-size` | `10px` | 滚动条粗细。 |
| `--astryx-scrollbar-thumb` | 次要文字色 45% | 滑块颜色。 |
| `--astryx-scrollbar-thumb-hover` | 次要文字色 70% | 悬停和拖动时的滑块颜色。 |
| `--astryx-scrollbar-track` | `transparent` | 轨道背景。 |
| `--astryx-scrollbar-radius` | `9999px` | 轨道和滑块圆角。 |

## 原生属性

组件本身不渲染元素，<code>class</code> 和 <code>style</code> 不会传下去。
