# v-ripple 涟漪

`v-ripple` 是一条**全局指令**，不是组件。把它挂在任意元素（原生 `div` / `button` 或别的组件）上，按下时就会出现 Material 风格的扩散波纹。它复刻了 MUI TouchRipple 的动效细节：按住常显、松手淡出、触摸延迟出现、键盘聚焦时从中心脉冲。库里的 `MButton`、`MFab`、`MCheckbox` 等组件已经在内部挂好了这条指令。

## 代码演示

### 基础用法

不带值即为默认参数，指令会自动给静态定位的宿主补上 `position: relative`。

**演示说明：** 按住不放可以保持波纹；松开或把指针移出元素后波纹淡出。键盘 Tab 聚焦后按 Enter / 空格也会出现居中的脉冲波纹。

```vue
<script setup lang="ts">
import type { CSSProperties } from 'vue'

const filled: CSSProperties = {
  padding: '12px 18px',
  border: 0,
  borderRadius: '8px',
  background: 'var(--m-primary)',
  color: 'var(--m-primary-contrast)',
  font: 'inherit',
  cursor: 'pointer',
  userSelect: 'none',
}

const outlined: CSSProperties = {
  padding: '11px 16px',
  border: '1px solid var(--m-border)',
  borderRadius: '8px',
  background: 'var(--m-card)',
  color: 'var(--m-text-default)',
  font: 'inherit',
  cursor: 'pointer',
  userSelect: 'none',
}
</script>

<template>
  <div class="m-row-demo">
    <!-- 普通 div：指令会自动补上 position: relative -->
    <div v-ripple :style="filled">按住看我</div>
    <!-- 原生 button -->
    <button v-ripple type="button" :style="outlined">button</button>
  </div>
</template>
```

### 禁用涟漪

三种关闭方式：绑定 `false`、传 `disabled: true`，或者宿主本身是原生 `disabled` / `aria-disabled="true"`。

**演示说明：** 第三颗按钮没有做任何配置，仅因为它是原生 `disabled` 按钮，波纹就不会出现。

```vue
<script setup lang="ts">
import type { CSSProperties } from 'vue'

const plain: CSSProperties = {
  padding: '12px 18px',
  border: '1px solid var(--m-border)',
  borderRadius: '8px',
  background: 'var(--m-card)',
  color: 'var(--m-text-default)',
  font: 'inherit',
  cursor: 'pointer',
}

const nativeDisabled: CSSProperties = {
  ...plain,
  background: 'var(--m-action-disabled-bg)',
  color: 'var(--m-action-disabled)',
  cursor: 'not-allowed',
}
</script>

<template>
  <div class="m-row-demo">
    <button v-ripple="false" type="button" :style="plain">v-ripple="false"</button>

    <button v-ripple="{ disabled: true }" type="button" :style="plain">disabled: true</button>

    <!-- 原生 disabled 由指令自身识别，无需额外配置 -->
    <button v-ripple type="button" disabled :style="nativeDisabled">原生 disabled</button>
  </div>
</template>
```

### 自定义选项

`color`、`duration`、`initialScale`、`center` 四个选项可以组合使用。

**演示说明：** `duration` 同时作用于进入与退出；`initialScale: 0.5` 让波纹从一半大小开始扩散，起点更明显、观感更快。

```vue
<script setup lang="ts">
import type { CSSProperties } from 'vue'

interface Host {
  label: string
  color?: string
  duration?: number
  initialScale?: number
  center?: boolean
  filled?: boolean
}

const base: CSSProperties = {
  padding: '14px 20px',
  borderRadius: '8px',
  cursor: 'pointer',
  userSelect: 'none',
}

const hosts: Host[] = [
  { label: 'color', color: '#ffffff', filled: true },
  { label: 'duration 200', duration: 200 },
  { label: 'initialScale 0.5', duration: 300, initialScale: 0.5 },
  { label: 'center', center: true },
]

function hostStyle(host: Host): CSSProperties {
  return host.filled
    ? { ...base, background: 'var(--m-primary)', color: 'var(--m-primary-contrast)' }
    : {
        ...base,
        border: '1px solid var(--m-border)',
        background: 'var(--m-card)',
        color: 'var(--m-text-default)',
      }
}
</script>

<template>
  <div class="m-row-demo">
    <div
      v-for="host in hosts"
      :key="host.label"
      v-ripple="{
        color: host.color,
        duration: host.duration,
        initialScale: host.initialScale,
        center: host.center,
      }"
      :style="hostStyle(host)"
    >
      {{ host.label }}
    </div>
  </div>
</template>
```

### 用在自定义元素与组件上

指令不挑宿主，普通 `div` 也能用；而已经自带涟漪的组件不需要再挂一次。

**演示说明：** `MButton` 内部已经有 `v-ripple`，再挂一次不会出现第二层波纹（容器记录在同一个宿主元素上），但会多出一套事件监听，没有必要。

```vue
<script setup lang="ts">
import type { CSSProperties } from 'vue'

const custom: CSSProperties = {
  padding: '10px 16px',
  borderRadius: '8px',
  background: 'var(--m-action-hover)',
  color: 'var(--m-text-default)',
  cursor: 'pointer',
  userSelect: 'none',
}
</script>

<template>
  <div class="m-row-demo">
    <!-- MButton 内部已经挂了 v-ripple -->
    <MButton variant="contained" color="primary">组件自带涟漪</MButton>

    <!-- 再挂一次属于重复挂载，看不出额外效果，也会多出一套监听 -->
    <MButton v-ripple variant="outlined" color="primary">重复挂载 v-ripple</MButton>

    <!-- 指令也可以用在任意自定义元素上 -->
    <div v-ripple :style="custom">自定义元素</div>
  </div>
</template>
```

## API

### 绑定值 RippleOptions

| 选项 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `undefined` | 禁用涟漪 |
| `color` | `string` | `undefined` | 涟漪颜色；缺省跟随宿主的 `currentColor` |
| `duration` | `number` | `550` | 动画时长（ms），进入与退出共用 |
| `initialScale` | `number` | `0` | 起始缩放，即从 `initialScale` 扩散到 `1` |
| `center` | `boolean` | `false` | 是否从元素中心扩散；`false` 时从点击点扩散 |

```ts
export type RippleOptions =
  | boolean
  | {
      disabled?: boolean // 禁用涟漪
      color?: string // 涟漪颜色，默认跟随 currentColor
      duration?: number // 动画时长（ms），默认 550（进入与退出共用）
      initialScale?: number // 起始缩放，默认 0（即从 0 扩散到 1）
      center?: boolean // 是否从元素中心扩散，默认 false
    }
```

绑定值的几种写法：

| 写法 | 含义 |
| --- | --- |
| `v-ripple` | 使用全部默认参数 |
| `v-ripple="false"` | 关闭涟漪 |
| `v-ripple="{ duration: 300 }"` | 传入配置对象；`updated` 钩子会同步最新值，不需要重新绑定监听器 |

### 修饰符

无。所有配置都通过绑定值对象传入（`disabled`、`color`、`duration`、`initialScale`、`center`），没有 `.center` 这类修饰符写法。

### 行为细节

- **定位**：宿主元素不是 `position: static` 时，指令会自动把它设为 `position: relative`，保证涟漪容器以宿主为定位上下文。
- **DOM 结构**：指令会向宿主 `appendChild` 一个 `<span class="m-ripple">` 容器，它成为宿主的最后一个子节点，波纹是这个容器的子节点。容器本身是绝对定位（`inset: 0`）且 `pointer-events: none`，不占布局空间，但依赖 `:last-child` 之类的选择器时要留意这个多出来的子节点。
- **触发条件**：宿主为原生 `disabled` 或 `aria-disabled="true"` 时不触发；鼠标只响应主键（`event.button === 0`）。
- **指针行为**：`pointerdown` 按下即出现，波纹直径恰好覆盖元素最远角；按住保持常显，`pointerup` / `pointercancel` / 鼠标 `pointerleave` 后淡出。
- **触摸与笔**：延迟 80ms 出现，用来区分「滚动」与「按下」；期间发生滚动或移动会取消这次波纹。
- **键盘**：`:focus-visible` 或按 Enter / 空格激活时产生居中脉冲波纹，失焦后停止。
- **颜色**：默认取宿主的 `currentColor` 并以约 30% 不透明度呈现，所以深底浅字的按钮会自动得到浅色波纹。

## 使用建议

::: tip 深色底上的波纹不用特意调色
波纹默认跟随 `currentColor`，而 `currentColor` 就是宿主文字的颜色。`MButton` 的 `contained` 变体把文字色设成了对比色，所以涟漪天然可见；只有当你想要一个和文字色不同的波纹时才需要传 `color`。

```vue
<div v-ripple="{ color: '#ffffff' }">深色底上的白色涟漪</div>
```
:::

::: tip 给自定义元素补上交互语义
指令只负责视觉反馈，不会给 `div` 加 `tabindex` 或 `role`。如果宿主是可以点击的自定义元素，记得自己补 `role="button"` 与 `tabindex="0"`，否则键盘用户既聚焦不到、也不会有键盘涟漪。
:::

::: warning 自带涟漪的组件不要再挂一次
`MButton`、`MFab`、`MCheckbox`、`MRadio`、`MSwitch`、`MDropdownItem`、`MSelect` 选项、`MTabs` 的关闭 / 新增按钮等都已内置这条指令，并且各自传了合适的配置（例如 `MCheckbox` 用 `{ center: true }`）。在它们身上再写一次 `v-ripple` 属于重复挂载：容器与状态都记录在同一个宿主元素上，后者会覆盖前者的记录，只是多出一个空的涟漪容器和一套多余的监听，不会有更好的效果。
:::

::: warning 别把涟漪当点击反馈的全部
涟漪只在指针 / 键盘交互时出现，并且会被 `prefers-reduced-motion` 关掉。真正的状态变化（加载、成功、失败）仍然要用 `loading`、`toast` 这类明确的反馈来表达。
:::

## 相关组件

- [Button 按钮](/components/button)：内置 `v-ripple`，是最常见的涟漪宿主。
- [Fab 悬浮按钮](/components/fab)：同样内置涟漪，扩展模式下图标与文字共用一层波纹。
