按钮 button
通用组件。三层同源:无头内核给出解剖与状态机,Vue 组件与自定义元素只是它的两层外壳,行为完全一致。
示例
基础用法
内容直接写在默认插槽里
<script setup lang="ts">
import { XhButton } from "@xihan-ui/vue";
</script>
<template>
<XhButton>按钮</XhButton>
</template>变体
variant 只改皮肤的几个颜色槽位,行为完全一致
<script setup lang="ts">
import { XhButton } from "@xihan-ui/vue";
</script>
<template>
<XhButton variant="solid">主要</XhButton>
<XhButton variant="outline">描边</XhButton>
<XhButton variant="ghost">幽灵</XhButton>
</template>尺寸
不传 size 即默认档
<script setup lang="ts">
import { XhButton } from "@xihan-ui/vue";
</script>
<template>
<XhButton size="sm">小</XhButton>
<XhButton>默认</XhButton>
<XhButton size="lg">大</XhButton>
</template>禁用与载入
loading 会挡住点击,并给 indicator 部件挂上旋转动画
<script setup lang="ts">
import { XhButton, XhButtonIndicator, XhButtonLabel } from "@xihan-ui/vue";
</script>
<template>
<XhButton disabled>禁用</XhButton>
<XhButton loading>
<XhButtonIndicator />
<XhButtonLabel>提交中</XhButtonLabel>
</XhButton>
</template>语气
tone 决定用哪族颜色,与 variant 正交:四种形态 × 六种语气都成立
solid
subtle
outline
ghost
<script setup lang="ts">
import { XhButton } from "@xihan-ui/vue";
const tones = ["brand", "neutral", "success", "warning", "danger", "info"] as const;
</script>
<template>
<div style="display: grid; gap: 10px">
<div v-for="variant in ['solid', 'subtle', 'outline', 'ghost']" :key="variant" style="display: flex; gap: 8px; align-items: center">
<span style="min-width: 56px; font-size: 13px; opacity: 0.7">{{ variant }}</span>
<XhButton v-for="tone in tones" :key="tone" :variant="variant" :tone="tone" size="sm">
{{ tone }}
</XhButton>
</div>
</div>
</template>图标与文字
图元放进 prefix 或 suffix 部件,文字放进 label;两个图元部件自带 aria-hidden,读屏念到的只有 label
<script setup lang="ts">
import { XhButton, XhButtonLabel, XhButtonPrefix, XhButtonSuffix, XhIcon } from "@xihan-ui/vue";
const PlusIcon = {
name: "plus",
viewBox: "0 0 24 24",
attrs: {
"fill": "none",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
nodes: [
{ tag: "path", attrs: { d: "M12 5V19" } },
{ tag: "path", attrs: { d: "M5 12H19" } },
],
} as const;
const ArrowRightIcon = {
name: "arrow-right",
viewBox: "0 0 24 24",
attrs: {
"fill": "none",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
nodes: [
{ tag: "path", attrs: { d: "M4 12H20" } },
{ tag: "path", attrs: { d: "M13 5L20 12L13 19" } },
],
} as const;
</script>
<template>
<!-- 图元在前 -->
<XhButton variant="solid">
<XhButtonPrefix>
<XhIcon :icon="PlusIcon" size="sm" />
</XhButtonPrefix>
<XhButtonLabel>新建</XhButtonLabel>
</XhButton>
<!-- 图元在后:换个部件就行,root 的 gap 两边通用 -->
<XhButton variant="outline">
<XhButtonLabel>下一步</XhButtonLabel>
<XhButtonSuffix>
<XhIcon :icon="ArrowRightIcon" size="sm" />
</XhButtonSuffix>
</XhButton>
<!-- 前后各一枚 -->
<XhButton variant="subtle" tone="success">
<XhButtonPrefix>
<XhIcon :icon="PlusIcon" size="sm" />
</XhButtonPrefix>
<XhButtonLabel>再来一件</XhButtonLabel>
<XhButtonSuffix>
<XhIcon :icon="ArrowRightIcon" size="sm" />
</XhButtonSuffix>
</XhButton>
</template>点击事件
处理器照常挂在组件上;载入态与禁用态的点击在根上就被拦下,作者挂的处理器也收不到
已计数 0 次
<script setup lang="ts">
import { ref } from "vue";
import { XhButton } from "@xihan-ui/vue";
const count = ref(0);
</script>
<template>
<XhButton variant="solid" @click="count++">点一下</XhButton>
<XhButton loading @click="count++">载入中</XhButton>
<XhButton disabled @click="count++">禁用</XhButton>
<span style="font-size: 13px">已计数 {{ count }} 次</span>
</template>形状与图标按钮
圆角是一个组件令牌;只放一枚图元时把左右内边距收成 0、宽度取控件档位,名字这时只能由 aria-label 给
<script setup lang="ts">
import { XhButton, XhIcon } from "@xihan-ui/vue";
const SearchIcon = {
name: "search",
viewBox: "0 0 24 24",
attrs: {
"fill": "none",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
nodes: [
{ tag: "circle", attrs: { cx: "10.5", cy: "10.5", r: "6.5" } },
{ tag: "path", attrs: { d: "M15.5 15.5L20.5 20.5" } },
],
} as const;
</script>
<template>
<XhButton variant="solid">直角</XhButton>
<!-- 胶囊:只改圆角这一个槽位 -->
<XhButton variant="solid" style="--xh-button-radius: var(--xh-shape-pill)">胶囊</XhButton>
<!-- 方形图标按钮 -->
<XhButton
variant="outline"
aria-label="搜索"
style="--xh-button-px: 0; inline-size: var(--xh-control-h-md)"
>
<XhIcon :icon="SearchIcon" size="sm" />
</XhButton>
<!-- 圆形图标按钮:方形再叠上胶囊圆角 -->
<XhButton
variant="solid"
aria-label="搜索"
style="--xh-button-px: 0; --xh-button-radius: var(--xh-shape-pill); inline-size: var(--xh-control-h-md)"
>
<XhIcon :icon="SearchIcon" size="sm" />
</XhButton>
</template>自定义配色
不写 variant 时底色与文字色取自组件令牌,逐个实例覆盖就能用上语气表以外的颜色
<script setup lang="ts">
import { XhButton } from "@xihan-ui/vue";
// 静止、悬停、按下三个底色各是一个槽位,缺哪个就落回缺省值
const grape = {
"--xh-button-bg": "#8a2be2",
"--xh-button-bg-hover": "#7a24ca",
"--xh-button-bg-active": "#691fac",
"--xh-button-fg": "#ffffff",
};
const flamingo = {
"--xh-button-bg": "#ff69b4",
"--xh-button-bg-hover": "#f2559f",
"--xh-button-bg-active": "#d94489",
"--xh-button-fg": "#ffffff",
};
</script>
<template>
<XhButton :style="grape">葡萄</XhButton>
<XhButton :style="flamingo">火烈鸟</XhButton>
<!-- 颜色以外的槽位可以一起换,这里再换掉圆角 -->
<XhButton :style="{ ...grape, '--xh-button-radius': 'var(--xh-shape-pill)' }">胶囊葡萄</XhButton>
</template>按钮组
相邻两段共用一条边,圆角只留在两端;档位与形状写在容器上,靠自定义属性流给组内每一段
<script setup lang="ts">
import { XhButton } from "@xihan-ui/vue";
const views = ["日", "周", "月"];
// 首段留起始两角、末段留结尾两角,中间保持直角;
// 后一段往回挪一个描边宽度,相邻的两条边重合成一条
function segment(index: number, total: number, radius = "var(--xh-shape-control)") {
return {
marginInlineStart: index ? "calc(-1 * var(--xh-stroke-thin))" : undefined,
borderStartStartRadius: index === 0 ? radius : undefined,
borderEndStartRadius: index === 0 ? radius : undefined,
borderStartEndRadius: index === total - 1 ? radius : undefined,
borderEndEndRadius: index === total - 1 ? radius : undefined,
};
}
</script>
<template>
<!-- 圆角槽位在容器上收成 0,组内每段都取得到,两端的圆角再逐段补回来 -->
<div style="display: inline-flex; --xh-button-radius: 0">
<XhButton
v-for="(v, i) in views"
:key="v"
variant="outline"
:style="segment(i, views.length)"
>
{{ v }}
</XhButton>
</div>
<!-- 同一份配方换一档:高度、内边距、字号在容器上写一次,两端收成胶囊 -->
<div
style="
display: inline-flex;
--xh-button-radius: 0;
--xh-button-h: var(--xh-control-h-sm);
--xh-button-px: var(--xh-control-px-sm);
--xh-button-font-size: var(--xh-font-size-sm);
"
>
<XhButton
v-for="(v, i) in views"
:key="v"
variant="outline"
:style="segment(i, views.length, 'var(--xh-shape-pill)')"
>
{{ v }}
</XhButton>
</div>
</template>渲染成链接
皮肤认的是 data-scope 与 data-part 这组契约,不是标签名:把契约铺到链接元素上就得到导航型按钮,跳转仍由浏览器原生完成
<script setup lang="ts">
import { XhButton } from "@xihan-ui/vue";
</script>
<template>
<XhButton variant="solid">留在本页</XhButton>
<!-- 根部件的两个契约属性铺上去就够;形态与档位照常由 data-variant、data-size 给 -->
<a
href="/introduction"
data-scope="button"
data-part="root"
data-variant="solid"
style="text-decoration: none"
>
去简介
</a>
<a
href="/guide/anatomy"
data-scope="button"
data-part="root"
data-variant="outline"
data-size="sm"
style="text-decoration: none"
>
看解剖
</a>
</template>产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-button> |
| Vue 组件 | XhButton XhButtonIndicator XhButtonLabel XhButtonPrefix XhButtonSuffix |
| 状态机 | 无,connect 直接由 props 算属性 |
| 皮肤 | @xihan-ui/styles/button.css |
解剖
部件名即 data-part 属性值,也是皮肤的选择器。加粗的是必备部件,不渲染它组件不工作(Web Components 适配器会在诊断通道上报 wc.missing-part)。
data-scope="button":root · label · indicator · prefix · suffix
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
disabled | boolean | ||
loading | boolean | 加载态:用 aria-disabled + 拦截事件表达,保留焦点。 | |
size | Size | ||
tone | Tone | 语气:brand / neutral / success / warning / danger / info,决定用哪族颜色 | |
type | 'button' | 'submit' | 'reset' | ||
variant | ActionVariant | 形态:solid / subtle / outline / ghost,决定颜色怎么用 |
connect API
connect 产出的对象。getXxxProps() 铺到对应部件的宿主元素上,其余是可读状态与操作入口。
| 成员 | 类型 | 说明 |
|---|---|---|
disabled | boolean | |
loading | boolean | |
getRootProps | () => T['button'] | |
getLabelProps | () => T['element'] | |
getIndicatorProps | () => T['element'] | |
getPrefixProps | () => T['element'] | |
getSuffixProps | () => T['element'] |
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Enter / Space | focus in root, interactive | 激活按钮(原生行为) |
