工具栏 toolbar
导航组件。三层同源:无头内核给出解剖与状态机,Vue 组件与自定义元素只是它的两层外壳,行为完全一致。
示例
基础用法
整条在 Tab 序列里只占一个位子,条内改用方向键走;条目是作者自己的按钮,工具条不接管它的点击
最近点击:(无)
<script setup lang="ts">
import { ref } from "vue";
import {
XhToolbarItem,
XhToolbarRoot,
XhToolbarSeparator,
} from "@xihan-ui/vue";
// 条目的观感归条目自己,工具条只补焦点环与禁用光标
const itemStyle = {
padding: "4px 10px",
borderRadius: "6px",
border: "1px solid var(--xh-border-default)",
background: "var(--xh-bg-surface)",
};
const command = ref("(无)");
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; inline-size: 100%">
<XhToolbarRoot>
<XhToolbarItem value="bold" :style="itemStyle" @click="command = '粗体'">
粗体
</XhToolbarItem>
<XhToolbarItem value="italic" :style="itemStyle" @click="command = '斜体'">
斜体
</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarItem value="link" :style="itemStyle" @click="command = '插入链接'">
插入链接
</XhToolbarItem>
</XhToolbarRoot>
<span>最近点击:{{ command }}</span>
</div>
</template>分组
分组只是把一伙控件在视觉上收紧,不是导航里多出来的一层:方向键照样一路走过去
<script setup lang="ts">
import {
XhToolbarGroup,
XhToolbarItem,
XhToolbarRoot,
XhToolbarSeparator,
} from "@xihan-ui/vue";
const itemStyle = {
padding: "4px 10px",
borderRadius: "6px",
border: "1px solid var(--xh-border-default)",
background: "var(--xh-bg-surface)",
};
</script>
<template>
<XhToolbarRoot style="inline-size: 100%">
<XhToolbarItem value="undo" :style="itemStyle">撤销</XhToolbarItem>
<XhToolbarItem value="redo" :style="itemStyle">重做</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarGroup>
<XhToolbarItem value="align-left" :style="itemStyle">左对齐</XhToolbarItem>
<XhToolbarItem value="align-center" :style="itemStyle">居中</XhToolbarItem>
<XhToolbarItem value="align-right" :style="itemStyle">右对齐</XhToolbarItem>
</XhToolbarGroup>
</XhToolbarRoot>
</template>竖排
orientation 决定方向键收哪一对键(另一轴原样放行给页面),分隔线的朝向恒与主轴垂直
<script setup lang="ts">
import {
XhToolbarItem,
XhToolbarRoot,
XhToolbarSeparator,
} from "@xihan-ui/vue";
const itemStyle = {
padding: "4px 10px",
borderRadius: "6px",
border: "1px solid var(--xh-border-default)",
background: "var(--xh-bg-surface)",
};
</script>
<template>
<XhToolbarRoot orientation="vertical" style="inline-size: 140px">
<XhToolbarItem value="zoom-in" :style="itemStyle">放大</XhToolbarItem>
<XhToolbarItem value="zoom-out" :style="itemStyle">缩小</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarItem value="fit" :style="itemStyle">适应画布</XhToolbarItem>
</XhToolbarRoot>
</template>禁用
禁用走 aria-disabled 而非原生 disabled:禁用项仍聚焦得上、仍能当方向键的起点,只是方向键路过时跳过它
<script setup lang="ts">
import { ref } from "vue";
import {
XhSwitch,
XhToolbarItem,
XhToolbarRoot,
XhToolbarSeparator,
} from "@xihan-ui/vue";
const itemStyle = {
padding: "4px 10px",
borderRadius: "6px",
border: "1px solid var(--xh-border-default)",
background: "var(--xh-bg-surface)",
};
const locked = ref(false);
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; inline-size: 100%">
<XhToolbarRoot :disabled="locked">
<XhToolbarItem value="cut" :style="itemStyle">剪切</XhToolbarItem>
<!-- 单项禁用:整条没锁时,方向键也只跳过这一项 -->
<XhToolbarItem value="paste" :style="itemStyle" disabled>
粘贴(禁用)
</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarItem value="delete" :style="itemStyle">删除</XhToolbarItem>
</XhToolbarRoot>
<label style="display: flex; align-items: center; gap: 8px">
<XhSwitch v-model:checked="locked" />
整条禁用(方向键当场不再接管,焦点进来就停在容器上)
</label>
</div>
</template>尺寸
size 只换整条的内边距与条目间的间距,条目自身的高度与字号归条目的皮肤管
sm
缺省
lg
<script setup lang="ts">
import {
XhToolbarItem,
XhToolbarRoot,
XhToolbarSeparator,
} from "@xihan-ui/vue";
const itemStyle = {
padding: "4px 10px",
borderRadius: "6px",
border: "1px solid var(--xh-border-default)",
background: "var(--xh-bg-surface)",
};
</script>
<template>
<div style="display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px">
<div style="display: flex; flex-direction: column; gap: 6px">
<span>sm</span>
<XhToolbarRoot size="sm">
<XhToolbarItem value="sm-bold" :style="itemStyle">粗体</XhToolbarItem>
<XhToolbarItem value="sm-italic" :style="itemStyle">斜体</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarItem value="sm-link" :style="itemStyle">链接</XhToolbarItem>
</XhToolbarRoot>
</div>
<div style="display: flex; flex-direction: column; gap: 6px">
<span>缺省</span>
<XhToolbarRoot>
<XhToolbarItem value="md-bold" :style="itemStyle">粗体</XhToolbarItem>
<XhToolbarItem value="md-italic" :style="itemStyle">斜体</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarItem value="md-link" :style="itemStyle">链接</XhToolbarItem>
</XhToolbarRoot>
</div>
<div style="display: flex; flex-direction: column; gap: 6px">
<span>lg</span>
<XhToolbarRoot size="lg">
<XhToolbarItem value="lg-bold" :style="itemStyle">粗体</XhToolbarItem>
<XhToolbarItem value="lg-italic" :style="itemStyle">斜体</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarItem value="lg-link" :style="itemStyle">链接</XhToolbarItem>
</XhToolbarRoot>
</div>
</div>
</template>图标条目
只画图标的条目必须自带无障碍名:aria-label 直接写在条目上,透传到那一层 DOM
最近点击:(无)
<script setup lang="ts">
import { ref } from "vue";
import {
XhIcon,
XhToolbarGroup,
XhToolbarItem,
XhToolbarRoot,
XhToolbarSeparator,
} from "@xihan-ui/vue";
// 描边取 currentColor,图标颜色随条目文字色走
const strokeAttrs = {
"fill": "none",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
} as const;
const UndoIcon = {
name: "undo",
viewBox: "0 0 24 24",
attrs: strokeAttrs,
nodes: [
{ tag: "path", attrs: { d: "M4 10H15A5 5 0 0 1 15 20H10" } },
{ tag: "path", attrs: { d: "M8 6L4 10L8 14" } },
],
} as const;
const RedoIcon = {
name: "redo",
viewBox: "0 0 24 24",
attrs: strokeAttrs,
nodes: [
{ tag: "path", attrs: { d: "M20 10H9A5 5 0 0 0 9 20H14" } },
{ tag: "path", attrs: { d: "M16 6L20 10L16 14" } },
],
} as const;
const AlignLeftIcon = {
name: "align-left",
viewBox: "0 0 24 24",
attrs: strokeAttrs,
nodes: [{ tag: "path", attrs: { d: "M4 6H20M4 12H14M4 18H18" } }],
} as const;
const AlignCenterIcon = {
name: "align-center",
viewBox: "0 0 24 24",
attrs: strokeAttrs,
nodes: [{ tag: "path", attrs: { d: "M4 6H20M7 12H17M5 18H19" } }],
} as const;
const AlignRightIcon = {
name: "align-right",
viewBox: "0 0 24 24",
attrs: strokeAttrs,
nodes: [{ tag: "path", attrs: { d: "M4 6H20M10 12H20M6 18H20" } }],
} as const;
// 条目的观感归条目自己,工具条只补焦点环与禁用光标
const itemStyle = {
display: "inline-flex",
alignItems: "center",
padding: "6px",
borderRadius: "6px",
border: "1px solid var(--xh-border-default)",
background: "var(--xh-bg-surface)",
color: "inherit",
};
const command = ref("(无)");
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; inline-size: 100%">
<XhToolbarRoot>
<XhToolbarItem
value="undo"
:style="itemStyle"
aria-label="撤销"
@click="command = '撤销'"
>
<XhIcon :icon="UndoIcon" size="sm" />
</XhToolbarItem>
<XhToolbarItem
value="redo"
:style="itemStyle"
aria-label="重做"
@click="command = '重做'"
>
<XhIcon :icon="RedoIcon" size="sm" />
</XhToolbarItem>
<XhToolbarSeparator />
<XhToolbarGroup>
<XhToolbarItem
value="align-left"
:style="itemStyle"
aria-label="左对齐"
@click="command = '左对齐'"
>
<XhIcon :icon="AlignLeftIcon" size="sm" />
</XhToolbarItem>
<XhToolbarItem
value="align-center"
:style="itemStyle"
aria-label="居中"
@click="command = '居中'"
>
<XhIcon :icon="AlignCenterIcon" size="sm" />
</XhToolbarItem>
<XhToolbarItem
value="align-right"
:style="itemStyle"
aria-label="右对齐"
@click="command = '右对齐'"
>
<XhIcon :icon="AlignRightIcon" size="sm" />
</XhToolbarItem>
</XhToolbarGroup>
</XhToolbarRoot>
<span>最近点击:{{ command }}</span>
</div>
</template>对齐与分布
工具条只定主轴与条目间距,怎么分布交给 CSS:justify-content 一改,同一条就贴尾、居中或两端摊开
贴尾
居中
两端摊开
<script setup lang="ts">
import {
XhToolbarGroup,
XhToolbarItem,
XhToolbarRoot,
XhToolbarSeparator,
} from "@xihan-ui/vue";
const itemStyle = {
padding: "4px 10px",
borderRadius: "6px",
border: "1px solid var(--xh-border-default)",
background: "var(--xh-bg-surface)",
};
// 分布方式写在 root 的内联样式上,条目的 DOM 顺序不动,方向键行程也就不受影响
const layouts = [
{ value: "flex-end", label: "贴尾" },
{ value: "center", label: "居中" },
{ value: "space-between", label: "两端摊开" },
];
</script>
<template>
<div style="display: grid; gap: 12px; inline-size: 100%">
<div v-for="l in layouts" :key="l.value" style="display: grid; gap: 6px">
<span>{{ l.label }}</span>
<XhToolbarRoot :style="{ justifyContent: l.value }">
<XhToolbarGroup>
<XhToolbarItem :value="`${l.value}-undo`" :style="itemStyle">
撤销
</XhToolbarItem>
<XhToolbarItem :value="`${l.value}-redo`" :style="itemStyle">
重做
</XhToolbarItem>
</XhToolbarGroup>
<XhToolbarSeparator />
<XhToolbarItem :value="`${l.value}-publish`" :style="itemStyle">
发布
</XhToolbarItem>
</XhToolbarRoot>
</div>
</div>
</template>产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-toolbar> |
| Vue 组件 | XhToolbarGroup XhToolbarItem XhToolbarRoot XhToolbarSeparator |
| 组合式函数 | useToolbar |
| 状态机 | toolbarMachine |
| 皮肤 | @xihan-ui/styles/toolbar.css |
解剖
部件名即 data-part 属性值,也是皮肤的选择器。加粗的是必备部件,不渲染它组件不工作(Web Components 适配器会在诊断通道上报 wc.missing-part)。
data-scope="toolbar":root · group · item · separator
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
orientation | Orientation | 主轴,默认 horizontal。它决定 root 的 aria-orientation、方向键收哪一对键 (另一轴原样放行给页面),以及分隔线的朝向(恒与主轴垂直)。 | |
dir | Direction | 文字方向,默认 ltr;只改写水平主轴上左右方向键的语义。 | |
loop | boolean | 方向键走到尽头是否回绕,默认 true。 | |
disabled | boolean | 整条禁用:条目全部转 aria-disabled,方向键不再接管。 | |
size | Size | 尺寸:sm / md / lg。工具条是布局容器,只换排布尺寸,不带语气。 |
状态机
状态:idle
事件:ITEM.FOCUS · TOOLBAR.BLUR
connect API
useToolbar 产出的对象。getXxxProps() 铺到对应部件的宿主元素上,其余是可读状态与操作入口。
| 成员 | 类型 | 说明 |
|---|---|---|
focusedValue | string | null | 焦点锚点;焦点不在工具条内时为 null。 |
orientation | Orientation | 生效的主轴。 |
separatorOrientation | Orientation | 分隔线的朝向:恒与主轴垂直(横排工具条里的分隔线是竖线)。 |
disabled | boolean | |
getRootProps | () => T['element'] | |
getGroupProps | () => T['element'] | |
getItemProps | (props: ToolbarItemProps) => T['element'] | |
getSeparatorProps | () => T['element'] |
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Tab / Shift+Tab | roving tabindex(恒开) | 整条只占一个 Tab 位:焦点落到锚点条目,无锚点时先落容器再由它转投给第一个可停留条目 |
ArrowRight / ArrowDown | 焦点在条内且未整条禁用;横排收 ArrowRight、竖排收 ArrowDown | 焦点移到下一个可停留条目(禁用项跳过、尽头按 loop 回绕);dir=rtl 时水平主轴改由 ArrowLeft 承担 |
ArrowLeft / ArrowUp | 焦点在条内且未整条禁用;横排收 ArrowLeft、竖排收 ArrowUp | 焦点移到上一个可停留条目(禁用项跳过、尽头按 loop 回绕);dir=rtl 时水平主轴改由 ArrowRight 承担 |
Home | 焦点在条内且未整条禁用 | 焦点移到首个可停留条目 |
End | 焦点在条内且未整条禁用 | 焦点移到末个可停留条目 |
交叉轴的两个方向键 | 焦点在条内(横排按上下、竖排按左右) | 不归工具条管:原样放行给页面滚动与读屏,绝不 preventDefault |
