滑块 slider
数据录入组件。三层同源:无头内核给出解剖与状态机,Vue 组件与自定义元素只是它的两层外壳,行为完全一致。
示例
基础用法
值恒是数组,单滑块即长度 1;方向键走一格 step,PageUp 与 PageDown 走 largeStep,Home 与 End 贴到端点
<script setup lang="ts">
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
</script>
<template>
<XhSliderRoot
v-slot="{ value }"
:default-value="[40]"
:min="0"
:max="100"
:step="1"
:large-step="10"
name="volume"
style="inline-size: 320px"
>
<XhSliderLabel>音量:{{ value[0] }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</template>区间选择
两个拇指互为对方的边界、永不交叉,minStepsBetweenThumbs 再给它们之间留出格数;getValueText 把值翻成读屏念得出的话
<script setup lang="ts">
import { ref } from "vue";
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
const price = ref([200, 600]);
function valueText({ value, index }: { value: number; index: number }) {
return `${index === 0 ? "起价" : "止价"} ${value} 元`;
}
</script>
<template>
<XhSliderRoot
v-model:value="price"
:min="0"
:max="1000"
:step="10"
:min-steps-between-thumbs="2"
:get-value-text="valueText"
name="price"
style="inline-size: 320px"
>
<XhSliderLabel>价格:¥{{ price[0] }} – ¥{{ price[1] }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb :index="0">
<XhSliderHiddenInput />
</XhSliderThumb>
<XhSliderThumb :index="1">
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</template>竖向
orientation 换成 vertical 后整条控件收成一块,键盘与拖动的方向跟着一起翻
<script setup lang="ts">
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
</script>
<template>
<XhSliderRoot v-slot="{ value }" :default-value="[30]" orientation="vertical">
<XhSliderLabel>亮度:{{ value[0] }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</template>禁用与只读
禁用的拇指退出 Tab 序列、值也不再随表单提交;只读仍可聚焦与朗读,只是推不动
<script setup lang="ts">
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
</script>
<template>
<XhSliderRoot :default-value="[60]" disabled name="brightness" style="inline-size: 280px">
<XhSliderLabel>禁用</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
<XhSliderRoot :default-value="[60]" read-only style="inline-size: 280px">
<XhSliderLabel>只读</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</template>语气
tone 决定已填轨道与滑块用哪族颜色,不写时沿用品牌色
<script setup lang="ts">
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
const tones = ["brand", "neutral", "success", "warning", "danger", "info"] as const;
</script>
<template>
<div style="display: grid; gap: 16px">
<XhSliderRoot
v-for="t in tones"
:key="t"
:tone="t"
:default-value="[60]"
style="inline-size: 280px"
>
<XhSliderLabel>{{ t }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</div>
</template>尺寸
size 改轨道厚度与滑块直径,不写即缺省中档
<script setup lang="ts">
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
</script>
<template>
<div style="display: grid; gap: 20px">
<XhSliderRoot :default-value="[50]" size="sm" style="inline-size: 280px">
<XhSliderLabel>sm</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
<XhSliderRoot :default-value="[50]" style="inline-size: 280px">
<XhSliderLabel>缺省</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
<XhSliderRoot :default-value="[50]" size="lg" style="inline-size: 280px">
<XhSliderLabel>lg</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</div>
</template>文字方向
dir 换成 rtl 后轨道从右往左填,左右两键的语义跟着对调;上下键与 Home、End 不受影响
<script setup lang="ts">
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
</script>
<template>
<div style="display: grid; gap: 20px">
<XhSliderRoot v-slot="{ value }" :default-value="[35]" style="inline-size: 280px">
<XhSliderLabel>从左往右:{{ value[0] }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
<!-- 外层节点声明文字方向,轨道与滑块用的逻辑属性据此换向 -->
<div dir="rtl">
<XhSliderRoot
v-slot="{ value }"
dir="rtl"
:default-value="[35]"
style="inline-size: 280px"
>
<XhSliderLabel>从右往左:{{ value[0] }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</div>
</div>
</template>滑块里的内容
thumb 是个普通容器,往里放什么都由作者说了算;放得下靠 --xh-slider-thumb-size 把直径撑开
<script setup lang="ts">
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
const badge = {
display: "flex",
alignItems: "center",
justifyContent: "center",
blockSize: "100%",
fontSize: "11px",
color: "var(--xh-fg-on-brand)",
};
</script>
<template>
<XhSliderRoot
v-slot="{ value }"
:default-value="[45]"
:step="5"
style="inline-size: 320px; --xh-slider-thumb-size: 34px"
>
<XhSliderLabel>完成度</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<span :style="badge">{{ value[0] }}%</span>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</template>轨道刻度
刻度是作者写进 track 里的普通节点:按值算出百分比绝对定位;轨道不裁剪,刻度线与文字都露得出来,内容随便写
<script setup lang="ts">
import type { CSSProperties } from "vue";
import { ref } from "vue";
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
const min = 0;
const max = 200;
const volume = ref([120]);
const marks = [
{ value: 0, text: "静音" },
{ value: 60, text: "60" },
{ value: 120, text: "★ 推荐" },
{ value: 200, text: "200" },
];
// 值在轨道上的位置
function offset(value: number) {
return `${((value - min) / (max - min)) * 100}%`;
}
const mark: CSSProperties = {
position: "absolute",
insetBlockStart: "-2px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "8px",
transform: "translateX(-50%)",
whiteSpace: "nowrap",
// 刻度只是装饰,指针照旧交给轨道
pointerEvents: "none",
};
const tick: CSSProperties = {
inlineSize: "2px",
blockSize: "10px",
borderRadius: "1px",
background: "var(--xh-border-strong)",
};
const text: CSSProperties = {
fontSize: "11px",
color: "var(--xh-fg-muted)",
};
</script>
<template>
<div style="inline-size: 320px; padding-block-end: 24px">
<XhSliderRoot
v-model:value="volume"
:min="min"
:max="max"
:step="10"
name="volume"
>
<XhSliderLabel>音量:{{ volume[0] }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
<span
v-for="item in marks"
:key="item.value"
:style="[mark, { insetInlineStart: offset(item.value) }]"
>
<span :style="tick"></span>
<span :style="text">{{ item.text }}</span>
</span>
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</div>
</template>拖动时的值气泡
thumb 自己是定位上下文,气泡挂在它上方就跟着走位;dragging 决定露不露面,气泡里的文字由作者的格式化函数产出
<script setup lang="ts">
import type { CSSProperties } from "vue";
import { ref } from "vue";
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
const budget = ref([1800]);
function money(value: number) {
return `¥${value.toLocaleString("zh-CN")}`;
}
// 读屏走 aria-valuetext,与可见气泡各念各的同一个值
function valueText({ value }: { value: number }) {
return money(value);
}
const bubble: CSSProperties = {
position: "absolute",
insetBlockEnd: "100%",
insetInlineStart: "50%",
transform: "translateX(-50%)",
marginBlockEnd: "8px",
padding: "2px 8px",
borderRadius: "var(--xh-shape-control)",
background: "var(--xh-bg-brand)",
color: "var(--xh-fg-on-brand)",
fontSize: "11px",
lineHeight: "18px",
whiteSpace: "nowrap",
pointerEvents: "none",
};
</script>
<template>
<XhSliderRoot
v-slot="{ value, dragging }"
v-model:value="budget"
:min="0"
:max="5000"
:step="50"
:get-value-text="valueText"
name="budget"
style="inline-size: 320px; --xh-slider-gap: 32px"
>
<XhSliderLabel>预算上限:{{ money(budget[0]) }}</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<span v-if="dragging" :style="bubble">{{ money(value[0]) }}</span>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
</template>离散档位
可选值不必是等距数值:让滑块在档位下标上走,宿主再把下标映射回自己的取值表,键盘与拖动都只落在档位上
<script setup lang="ts">
import { computed, ref } from "vue";
import {
XhSliderControl,
XhSliderHiddenInput,
XhSliderLabel,
XhSliderRange,
XhSliderRoot,
XhSliderThumb,
XhSliderTrack,
} from "@xihan-ui/vue";
const levels = [1, 5, 10, 50, 100, 500];
const index = ref([2]);
const current = computed(() => levels[index.value[0]]);
function valueText({ value }: { value: number }) {
return `每页 ${levels[value]} 条`;
}
</script>
<template>
<div style="inline-size: 320px; display: grid; gap: 12px">
<XhSliderRoot
v-model:value="index"
:min="0"
:max="levels.length - 1"
:step="1"
:large-step="1"
:get-value-text="valueText"
>
<XhSliderLabel>每页 {{ current }} 条</XhSliderLabel>
<XhSliderControl>
<XhSliderTrack>
<XhSliderRange />
</XhSliderTrack>
<XhSliderThumb>
<XhSliderHiddenInput />
</XhSliderThumb>
</XhSliderControl>
</XhSliderRoot>
<span style="font-size: 12px; color: var(--xh-fg-muted)">
可选:{{ levels.join(" / ") }}
</span>
</div>
</template>产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-slider> |
| Vue 组件 | XhSliderControl XhSliderHiddenInput XhSliderLabel XhSliderRange XhSliderRoot XhSliderThumb XhSliderTrack |
| 组合式函数 | useSlider |
| 状态机 | sliderMachine |
| 皮肤 | @xihan-ui/styles/slider.css |
解剖
部件名即 data-part 属性值,也是皮肤的选择器。加粗的是必备部件,不渲染它组件不工作(Web Components 适配器会在诊断通道上报 wc.missing-part)。
data-scope="slider":root · label · control · track · range · thumb · hidden-input
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
value | number[] | ||
defaultValue | number[] | ||
min | number | ||
max | number | ||
step | number | ||
largeStep | number | PageUp / PageDown 的步长,默认 10 倍 step。 | |
orientation | Orientation | ||
dir | Direction | ||
disabled | boolean | ||
readOnly | boolean | ||
invalid | boolean | ||
tone | Tone | 语气:brand / neutral / success / warning / danger / info,决定用哪族颜色 | |
size | Size | 尺寸:sm / md / lg,决定拇指直径与轨道厚度 | |
name | string | 表单字段名;多滑块时逐个 append。 | |
minStepsBetweenThumbs | number | 相邻滑块至少隔几格,默认 0(可以贴在一起但不能交换顺序)。 | |
getValueText | (details: SliderValueTextDetails) => string | 把值翻成人话,产出写进拇指的 aria-valuetext。 不给就不写这个属性,读屏退回念 aria-valuenow。 | |
onValueChange | (details: SliderValueChangeDetails) => void | 每次推动都发;拖动过程中会连续发很多次。 | |
onValueChangeEnd | (details: SliderValueChangeEndDetails) => void | 只在一次操作结束时发一次,适合拿来发请求。 |
状态机
状态:idle · dragging
事件:VALUE.SET · THUMB.STEP · THUMB.TO_MIN · THUMB.TO_MAX · THUMB.SET · THUMB.FOCUS · DRAG.START · DRAG.MOVE · DRAG.END · FORM.RESET
判据:canDrag
connect API
useSlider 产出的对象。getXxxProps() 铺到对应部件的宿主元素上,其余是可读状态与操作入口。
| 成员 | 类型 | 说明 |
|---|---|---|
value | number[] | |
range | { start: number, end: number } | 已选区间在轨道上的起止,0-1。 |
thumbs | SliderThumbState[] | |
dragging | boolean | |
disabled | boolean | |
readOnly | boolean | |
setValue | (next: number[]) => void | |
setThumbValue | (index: number, next: number) => void | |
getRootProps | () => T['element'] | |
getLabelProps | () => T['label'] | |
getControlProps | () => T['element'] | |
getTrackProps | () => T['element'] | |
getRangeProps | () => T['element'] | |
getThumbProps | (index: number) => T['element'] | |
getHiddenInputProps | (index: number) => T['input'] |
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
ArrowRight / ArrowUp | focus in thumb, not disabled/readOnly | 按 step 增大;RTL 与竖直排布下按屏幕方向对调,语义恒是"朝 max 走一格" |
ArrowLeft / ArrowDown | focus in thumb, not disabled/readOnly | 按 step 减小,同上对调规则 |
PageUp | focus in thumb, not disabled/readOnly | 按 largeStep 增大(默认 10 倍 step) |
PageDown | focus in thumb, not disabled/readOnly | 按 largeStep 减小 |
Home | focus in thumb, not disabled/readOnly | 取 min;多滑块时取自己被邻居允许的下界 |
End | focus in thumb, not disabled/readOnly | 取 max;多滑块时取自己被邻居允许的上界 |
