# 曦寒视图组件 · 组件参考 共 137 页。每页含解剖部件、Props、事件、状态、键盘、数据属性、CSS 变量与两个适配器的示例源码。 索引见 https://ui.docs.xihanfun.com/llms.txt --- 来源:https://ui.docs.xihanfun.com/components/accordion # Accordion 手风琴 一列可展开的区块,标题常驻,内容按需展开。 ## 用法 默认单开:展开一项即收起其余,defaultValue 只提供初始值,之后由组件自行维护 ```vue ``` ```html

装 @xihan-ui/web-components 与 @xihan-ui/styles 两个包,皮肤单独引一次。

皮肤只认 data-part 与 data-state,覆写同名令牌即可。

方向键只在标题之间搬焦点,永不进内容区,首尾不回绕。
``` ## 组件结构 加粗的是必需部件。 `data-scope="accordion"`:`root` · `item` · `item-separator` · `header` · **`trigger`** · **`content`** · `indicator` ## 示例 ### 多项展开 multiple 允许多项并存,展开集合恒为 string[],受控绑定即可获取它 ```vue ``` ```html

value、defaultValue、multiple。

orientation 决定方向键走哪条轴,默认 vertical。

value-change 携带 { value }。
展开:basic、size
``` ### 允许全部收起 单开模式下最后一项默认无法收起,加 collapsible 后才能收起 ```vue ``` ```html

点当前展开项的标题,它会收起,展开集合变成空数组。

展开它会把上一项挤掉,单开模式一次只留一项。
``` ### 指示器与禁用 indicator 的朝向由 data-state 驱动,禁用项不可点击、方向键也跳过它 ```vue ``` ```html

标题右侧那个箭头就是 indicator,展开时自动翻转。

这一项展不开。

从第一项按方向键,会直接跳到这里。
``` ### 颜色 tone 落在展开态的标题上,六种颜色各预置一项展开做对照 ```vue ``` ```html

tone="brand"

收起态标题保持默认颜色。

tone="neutral"

收起态标题保持默认颜色。

tone="success"

收起态标题保持默认颜色。

tone="warning"

收起态标题保持默认颜色。

tone="danger"

收起态标题保持默认颜色。

tone="info"

收起态标题保持默认颜色。
``` ### 尺寸 size 改变标题栏的高度、内边距与字号,三档并排对照 ```vue ``` ```html

标题栏最矮,字号也最小。

同一档内所有标题一致。

不写 size 就是这一档。

同一档内所有标题一致。

标题栏最高,字号也最大。

同一档内所有标题一致。
``` ### 嵌套 content 中再放一组手风琴,内外两组各自维护展开集合,方向键也各自独立 ```vue ``` ```html

次日达,节假日照常发货。

下单后到门店凭码取货。

签收七日内可退,运费到付。
``` ### 标题栏附加信息 标题栏中的节点全部归作者,把计数与指示器包为一组排在末尾 ```vue ``` ```html

还没有人认领。

预计今天完成。

本周已归档。
``` ### 指示器在前 指示器写在标题之前即落到起始缘,标题用 auto 外边距占据余量 ```vue ``` ```html

指示器在标题左边,展开时照样翻转。

部件的先后顺序就是它们在标题栏里的顺序。

标题吃掉余量,右侧留白。
``` ### 缩小触发区域 trigger 只包住指示器,标题文字留在 header 里,点标题不再展开 ```vue ``` ```html

账户资料

只有右边那个按钮能展开这一段。

账单信息

标题文字不在按钮里,点它没有反应。
``` ### 自定义展开图标 indicator 是可选部件,不渲染它就没有默认字形;标记由作者按展开集合自行绘制 ```vue ``` ```html

同城次日达,跨省三日达。

支持电子普票与专票。

签收七日内无理由退换。
``` ### 变体 ghost 不绘制外壳,outline 连成单一表面,subtle 用淡底;三档只改变与页面分开的方式 ```vue ``` ```html

下单后 48 小时内发出。

签收 7 天内可申请退换。

下单后 48 小时内发出。

签收 7 天内可申请退换。

下单后 48 小时内发出。

签收 7 天内可申请退换。
``` ## 设计指引 ### 何时使用 - 常见问题、设置分组等由标题即可判断是否需要展开的内容。 - 内容较长,一次全部铺开会使页面失去结构。 ### 何时不用 - 只有一块内容时,使用[折叠区域](./collapsible)。 - 各块内容需要对照阅读时,直接铺开。 - 各块是并列视图且同一时间只看一个时,使用[标签页](./tabs)。 ### 特性 - `multiple` 决定能否同时展开多项,`collapsible` 决定能否全部收起。 - 指示器可置于标题前或标题后,图形可自定义。 - 支持嵌套;触发区大小由作者决定。 ### 组合 - 标题栏可以放置附加信息,如计数或状态[徽标](./badge)。 ### 最佳实践 - 标题应说明区块内容,不依赖展开来发现。 - 默认展开第一项,让用户看到内容的形态。 ### 反模式 - 将关键信息放进折叠区块,用户不会逐个展开。 - 展开时页面下方内容大幅跳动而没有滚动补偿。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhAccordionContent` `XhAccordionHeader` `XhAccordionIndicator` `XhAccordionItem` `XhAccordionItemSeparator` `XhAccordionRoot` `XhAccordionTrigger` | | 组合式函数 | `useAccordion` | | 状态机 | `accordionMachine` | | 皮肤 | `@xihan-ui/styles/accordion.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `collection` | `AccordionNode[]` | | 条目数据,标题文本、正文与禁用的事实源。提供后条目部件只需声明 value。 未提供时回到文本写在部件中、禁用写在条目上的方式。 | | `value` | `string[]` | | 展开集合,提供即受控。 | | `defaultValue` | `string[]` | | | | `multiple` | `boolean` | | 允许多项同时展开;false 时展开一项即收起其余。 | | `collapsible` | `boolean` | | 允许收起最后一个展开项,默认 false。 | | `loop` | `boolean` | | 方向键到达末尾是否回绕,默认 false。 | | `disabled` | `boolean` | | 整组禁用:所有条目都不可切换,条目上的 disabled 只能收紧不能放宽。 | | `variant` | `ControlVariant` | | 形态:ghost 条目直接相邻不画容器(默认),outline 为单一连续表面,subtle 为淡底。默认 ghost。 | | `orientation` | `Orientation` | | 方向键轴向,默认 vertical。 | | `dir` | `Direction` | | 文字方向,默认 ltr;影响水平轴上 ArrowLeft / ArrowRight 的语义。 | | `tone` | `Tone` | | 颜色:brand / neutral / success / warning / danger / info,决定使用哪组状态色。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `onValueChange` | `(details: AccordionValueChangeDetails) => void` | | 展开集合变化回调。 | ### AccordionNode `collection` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | 标题文本;默认回退为 value。 | | `content` | `string` | | 正文;需要放置纯文本以外的内容时改用 content 插槽。 | | `disabled` | `boolean` | | 条目禁用:方向键跳过该条目,但它仍可聚焦、仍是导航起点。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `AccordionValueChangeDetails` | 展开集合变化;detail 为 `{ value: string[] }` | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhAccordionItem` | `value` | `string` | 是 | | | `XhAccordionItem` | `disabled` | `boolean` | | 默认交给 connect 查询 collection,写死 false 会覆盖数据中的禁用。 | | `XhAccordionRoot` | `renderContent` | `(node: AccordionNodeMeta) => ReactNode` | | 每个条目正文的自定义内容;未提供时使用 collection 中的 content。 | | `XhAccordionRoot` | `children` | `ReactNode` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `item` | 'open' \| 'closed' | | `header` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `indicator` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`ITEM.TOGGLE` · `VALUE.SET` · `PRESS.START` · `PRESS.END` **判据**:`canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string[]` | 当前展开集合,单开模式下长度 ≤ 1。 | | `collection` | `readonly AccordionNodeMeta[]` | 由 collection 推导的条目元信息,按数据顺序排列;未提供 collection 时为空数组。 | | `setValue` | `(next: string[]) => void` | | | `isOpen` | `(value: string) => boolean` | | | `getRootProps` | `() => T['element']` | | | `getItemProps` | `(props: AccordionItemProps) => T['element']` | | | `getItemSeparatorProps` | `() => T['element']` | | | `getHeaderProps` | `(props: AccordionItemProps) => T['element']` | | | `getTriggerProps` | `(props: AccordionItemProps) => T['button']` | | | `getContentProps` | `(props: AccordionItemProps) => T['element']` | | | `getIndicatorProps` | `(props: AccordionItemProps) => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Space` / `Enter` | focus in trigger, not disabled | 展开/收起该条目的 content | | `ArrowDown` / `ArrowRight` | focus in trigger, 按键与 orientation 同轴(dir=rtl 时左右键语义互换) | 焦点移到下一个 trigger,末条不回绕 | | `ArrowUp` / `ArrowLeft` | focus in trigger, 按键与 orientation 同轴(dir=rtl 时左右键语义互换) | 焦点移到上一个 trigger,首条不回绕 | | `Home` | focus in trigger | 焦点移到首个 trigger | | `End` | focus in trigger | 焦点移到末个 trigger | | `Tab` / `Shift+Tab` | focus in trigger | 按文档序进出:每个 trigger 都是独立 Tab 停靠点,无 roving tabindex | | `Enter` / `Space` | held in trigger, not disabled | 按住期间该 trigger 投影 data-pressed,与指针 :active 同一副按压面(disclosure trigger 只换面不缩放);抬起、失焦或整组转禁用撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `item-separator` | `aria-hidden` | 'true' | | `header` | `aria-level` | 3 | | `header` | `role` | 'heading' | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-disabled` | 'true' \| 'false' | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `content` | `aria-labelledby` | `trigger` 部件的 id | | `content` | `role` | 'region' | | `indicator` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/accordion.css` 使用 `[data-scope="accordion"][data-part="root"]` 部件选择器,位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-orientation` | props.orientation | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-state` | 'open' \| 'closed' | | `header` | `data-disabled` | ''(条件成立时才出现) | | `header` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-disabled` | ''(条件成立时才出现) | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'disclosure-trigger' | | `trigger` | `data-xh-action-size` | props.size | | `trigger` | `data-xh-action-variant` | 'ghost' | | `content` | `data-instant` | '' | | `content` | `data-state` | 'open' \| 'closed' | | `indicator` | `data-disabled` | ''(条件成立时才出现) | | `indicator` | `data-instant` | '' | | `indicator` | `data-state` | 'open' \| 'closed' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-accordion-border` | `root` | `border` | `variant=outline` | `--xh-border-default` | accordion 的 root 部件 border 覆盖槽。 | | `--xh-accordion-content-fg` | `content` | `color` | `default` | `--xh-fg-muted` | accordion 的 content 部件 color 覆盖槽。 | | `--xh-accordion-content-font-size` | `content` | `font-size` | `default` | `--xh-text-secondary-size` | accordion 的 content 部件 font-size 覆盖槽。 | | `--xh-accordion-content-pb` | `content` | `padding-block-end` | `@keyframes xh-disclosure-collapse`
`@keyframes xh-disclosure-expand`
`default` | `--xh-_accordion-content-pb` | accordion 的 content 部件 padding-block-end 覆盖槽。 | | `--xh-accordion-content-px` | `content` | `padding-inline` | `default` | `--xh-_accordion-content-px` | accordion 的 content 部件 padding-inline 覆盖槽。 | | `--xh-accordion-icon-size` | `root`
`trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size`
`--xh-glyph-size-md` | accordion 的 root、trigger 部件 --xh-icon-size 覆盖槽。 | | `--xh-accordion-indicator-fg` | `indicator` | `color` | `default` | `--xh-fg-muted` | accordion 的 indicator 部件 color 覆盖槽。 | | `--xh-accordion-item-bg` | `root` | `background` | `variant=outline`
`variant=subtle` | `--xh-bg-subtle`
`--xh-bg-surface` | accordion 的 root 部件 background 覆盖槽。 | | `--xh-accordion-item-border` | `item`
`item-separator`
`root` | `background`
`border-block-start`
`border-inline-start` | `default`
`is([data-variant='outline'], [data-variant='subtle'])`
`not(:last-child)`
`orientation=horizontal`
`variant=outline`
`variant=subtle` | `--xh-border-subtle` | accordion 的 item、item-separator、root 部件 background、border-block-start、border-inline-start 覆盖槽。 | | `--xh-accordion-item-radius` | `root` | `border-radius` | `variant=outline`
`variant=subtle` | `--xh-shape-surface` | accordion 的 root 部件 border-radius 覆盖槽。 | | `--xh-accordion-item-shadow` | `root` | `box-shadow` | `variant=outline`
`variant=subtle` | `none` | accordion 的 root 部件 box-shadow 覆盖槽。 | | `--xh-accordion-trigger-bg` | `trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | accordion 的 trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-accordion-trigger-bg-hover` | `trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | accordion 的 trigger 部件 background-color 覆盖槽。 | | `--xh-accordion-trigger-fg` | `trigger` | `color` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | accordion 的 trigger 部件 color 覆盖槽。 | | `--xh-accordion-trigger-fg-disabled` | `trigger` | `color` | `disabled` | `--xh-_action-variant-fg-disabled` | accordion 的 trigger 部件 color 覆盖槽。 | | `--xh-accordion-trigger-fg-open` | `trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`state=open` | `--xh-_accordion-open-fg` | accordion 的 trigger 部件 color 覆盖槽。 | | `--xh-accordion-trigger-font-size` | `trigger` | `font-size` | `default` | `--xh-_action-profile-font-size` | accordion 的 trigger 部件 font-size 覆盖槽。 | | `--xh-accordion-trigger-font-weight` | `trigger` | `font-weight` | `default` | `--xh-text-label-weight` | accordion 的 trigger 部件 font-weight 覆盖槽。 | | `--xh-accordion-trigger-gap` | `trigger` | `gap` | `default` | `--xh-_action-profile-gap` | accordion 的 trigger 部件 gap 覆盖槽。 | | `--xh-accordion-trigger-h` | `trigger` | `block-size`
`min-block-size` | `default`
`xh-action-profile=disclosure-trigger` | `--xh-_action-profile-visual-size` | accordion 的 trigger 部件 block-size、min-block-size 覆盖槽。 | | `--xh-accordion-trigger-px` | `trigger` | `padding-inline` | `default` | `--xh-_action-profile-padding-inline` | accordion 的 trigger 部件 padding-inline 覆盖槽。 | | `--xh-accordion-trigger-py` | `trigger` | `padding-block` | `xh-action-profile=disclosure-trigger` | `--xh-_action-profile-padding-block` | accordion 的 trigger 部件 padding-block 覆盖槽。 | | `--xh-accordion-trigger-radius` | `trigger` | `border-radius` | `default` | `--xh-_action-profile-radius` | accordion 的 trigger 部件 border-radius 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 披露(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-disclosure-collapse` · `xh-disclosure-expand` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`rotate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/affix # Affix 固钉 在滚动超过指定位置后固定内容。 ## 用法 滚动后固定工具栏 ```vue ``` ```html
项目概览
项目动态

最近访问

团队成员
``` ## 组件结构 加粗的是必需部件。 `data-scope="affix"`:**`root`** · **`content`** ## 示例 ### 顶部偏移 避让固定页头 ```vue ``` ```html
吸顶栏
``` ### 底部固定 将操作栏固定在底部 ```vue ``` ```html
订单列表
``` ### 吸附状态 根据当前状态更新内容 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 固定表格操作栏、表单提交栏或文章目录。 ### 何时不用 - 始终固定的元素直接使用 `position: sticky`。 - 页面骨架使用[布局](./layout)的固定能力。 - 返回顶部操作使用[回到顶部](./back-top)。 ### 特性 - 固定时保留原始占位,避免页面跳动。 - 支持顶部、底部和偏移位置。 - 提供吸附状态和变化事件。 ### 组合 - 可与[锚点](./anchor)或[工具栏](./toolbar)组合使用。 ### 最佳实践 - 页面已有固定页头时设置对应的顶部偏移。 - 固定后使用轻微阴影或背景变化提示状态。 ### 反模式 - 不要在同一视口固定过多内容。 - 移动端避免固定过高的区域。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhAffixContent` `XhAffixRoot` | | 组合式函数 | `useAffix` | | 状态机 | `affixMachine` | | 皮肤 | `@xihan-ui/styles/affix.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `offsetTop` | `number` | | 吸附后距滚动容器可视区上边的距离(px)。 | | `offsetBottom` | `number` | | 吸附后距滚动容器可视区下边的距离(px);提供后改为贴靠下边。 | | `onAffixChange` | `(details: AffixChangeDetails) => void` | | 吸附状态变化回调。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `affix-change` | `AffixChangeDetails` | 吸附状态变化;detail 为 `{ affixed: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhAffixRoot` | `default` | `AffixRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhAffixRoot` | `target` | `() => HTMLElement \| null` | | 滚动容器取值器,默认即整页滚动;挂载效应执行时求值。 | | `XhAffixRoot` | `children` | `SlotChildren` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`released` · `affixed` **事件**:`SCROLL.RESOLVE` **判据**:`shouldAffix` · `shouldRelease` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `affixed` | `boolean` | 当前是否处于吸附状态。 | | `getRootProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/affix.css` 使用 `[data-scope="affix"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `content` | `data-fixed` | ''(条件成立时才出现) | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-affix-layer` | `content` | `z-index` | `fixed` | `--xh-layer-sticky` | affix 的 content 部件 z-index 覆盖槽。 | ### 动效 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 --- 来源:https://ui.docs.xihanfun.com/components/alert # Alert 警告提示 页面内常驻的一条提示,说明一件与当前上下文有关的事。 ## 用法 在中性抬升表面中说明当前状态与影响 ```vue ``` ```html
部署已排队
构建完成后会自动发布。
``` ## 组件结构 加粗的是必需部件。 `data-scope="alert"`:**`root`** · `indicator` · **`content`** · `title` · `description` · `action` · `close-trigger` ## 示例 ### 颜色 tone 只改配色,语义仍由内容与 role 决定 ```vue ``` ```html
保存成功
配额即将用尽
发布失败
``` ### 可关闭 closable 开启后才渲染关闭按钮;open 受控时由宿主决定去留 ```vue ``` ```html
点右侧关闭
``` ### 图标 icon 部件排在标题前面,颜色取当前语气的强调色;内容由作者放置,字形与内联 svg 均可 ```vue ``` ```html
发布完成
三个节点都已切到新版本。
发布失败
第 2 个节点健康检查未通过。
``` ### 操作 将与提示直接相关的短操作放在尾端 ```vue ``` ```html
配额即将用尽
本月还可处理 120 次请求。
``` ## 设计指引 ### 何时使用 - 表单顶部的整体错误、页面级的状态说明、功能公告。 - 信息需要持续存在,直到用户处理或关闭。 ### 何时不用 - 一次操作的结果反馈使用[轻提示](./toast),它会自动消失。 - 需要用户当场决定并阻断流程时使用[对话框](./dialog)。 - 单个字段的错误使用[表单字段](./field)的错误文本。 ### 特性 - 默认使用中性描边表面,语气只强调标题与图标,说明保持次级前景。 - `content` 是标题与说明共用的必需文本列,操作和关闭入口排在尾端。 - `closable` 显示关闭按钮,关闭状态可受控。 ### 组合 - 图标使用[图标](./icon),行动入口使用[按钮](./button)。 ### 最佳实践 - 说明发生了什么、影响是什么、用户可以做什么,三项缺一不可。 - 严重程度不能只靠颜色表达,标题文字本身应说明。 ### 反模式 - 同一屏堆叠多条提示,用户会全部略过。 - 将提示用作营销位。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhAlertAction` `XhAlertCloseTrigger` `XhAlertContent` `XhAlertDescription` `XhAlertIndicator` `XhAlertRoot` `XhAlertTitle` | | 状态机 | `alertMachine` | | 皮肤 | `@xihan-ui/styles/alert.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定使用哪族颜色,默认 info。 danger / warning 使用 role="alert",其余使用 role="status"。 | | `closable` | `boolean` | | 关闭按钮是否可用,默认 true。false 时该按钮同时被禁用与收起。 | | `open` | `boolean` | | 受控显隐;未提供该 prop 即非受控。 | | `defaultOpen` | `boolean` | | 非受控初始显隐,默认显示。 | | `onOpenChange` | `(details: AlertOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `translations` | `Partial` | | | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `open-change` | `AlertOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhAlertRoot` | `children` | `ReactNode` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `tone` | `string` | | | `closable` | `boolean` | | | `setOpen` | `(next: boolean) => void` | | | `getRootProps` | `() => T['element']` | | | `getIndicatorProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | 文本列容器:标题与说明纵向排列。 | | `getTitleProps` | `() => T['element']` | | | `getDescriptionProps` | `() => T['element']` | | | `getActionProps` | `() => T['element']` | 操作槽:划定按钮区,按钮本身由作者提供。 | | `getCloseTriggerProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus 在 close-trigger 上且 closable | 收起提示并通知 open=false | | `Enter` / `Space` | held on close-trigger, closable | 按住期间关闭按钮投影 data-pressed,与指针 :active 同一副按压面;抬起、失焦或提示收起撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-atomic` | 'true' | | `root` | `aria-describedby` | `description` 部件的 id | | `root` | `aria-labelledby` | `title` 部件的 id | | `root` | `aria-live` | live | | `root` | `role` | role | | `indicator` | `aria-hidden` | 'true' | | `close-trigger` | `aria-label` | props.translations.close | ## 样式参考 ### 皮肤 `@xihan-ui/styles/alert.css` 使用 `[data-scope="alert"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-state` | 'open' \| 'closed' | | `root` | `data-tone` | props.tone | | `close-trigger` | `data-disabled` | ''(条件成立时才出现) | | `close-trigger` | `data-pressed` | ''(条件成立时才出现) | | `close-trigger` | `data-xh-action-control` | '' | | `close-trigger` | `data-xh-action-display` | 'always' | | `close-trigger` | `data-xh-action-profile` | 'icon' | | `close-trigger` | `data-xh-action-size` | 'sm' | | `close-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-alert-action-gap` | `action` | `gap` | `default` | `--xh-space-2` | alert 的 action 部件 gap 覆盖槽。 | | `--xh-alert-bg` | `root` | `background` | `default` | `--xh-bg-surface` | alert 的 root 部件 background 覆盖槽。 | | `--xh-alert-border` | `root` | `border` | `default` | `--xh-border-default` | alert 的 root 部件 border 覆盖槽。 | | `--xh-alert-close-bg-active` | `close-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | alert 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-alert-close-bg-hover` | `close-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | alert 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-alert-close-fg` | `close-trigger` | `color` | `default` | `--xh-fg-muted` | alert 的 close-trigger 部件 color 覆盖槽。 | | `--xh-alert-close-fg-hover` | `close-trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-default` | alert 的 close-trigger 部件 color 覆盖槽。 | | `--xh-alert-close-radius` | `close-trigger` | `border-radius` | `default` | `--xh-shape-control` | alert 的 close-trigger 部件 border-radius 覆盖槽。 | | `--xh-alert-close-size` | `close-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size` | alert 的 close-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-alert-content-gap` | `content` | `gap` | `default` | `--xh-space-1` | alert 的 content 部件 gap 覆盖槽。 | | `--xh-alert-description-fg` | `description` | `color` | `default` | `--xh-fg-muted` | alert 的 description 部件 color 覆盖槽。 | | `--xh-alert-description-font-size` | `description` | `font-size` | `default` | `--xh-text-secondary-size` | alert 的 description 部件 font-size 覆盖槽。 | | `--xh-alert-fg` | `root` | `color` | `default` | `--xh-fg-default` | alert 的 root 部件 color 覆盖槽。 | | `--xh-alert-font-size` | `root` | `font-size` | `default` | `--xh-text-body-size` | alert 的 root 部件 font-size 覆盖槽。 | | `--xh-alert-gap` | `root` | `gap` | `default` | `--xh-space-4` | alert 的 root 部件 gap 覆盖槽。 | | `--xh-alert-icon-size` | `close-trigger`
`root` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size`
`--xh-glyph-size-md` | alert 的 close-trigger、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-alert-indicator-fg` | `indicator` | `color` | `default` | `--xh-_tone-fg` | alert 的 indicator 部件 color 覆盖槽。 | | `--xh-alert-indicator-p` | `indicator` | `padding` | `default` | `--xh-space-1` | alert 的 indicator 部件 padding 覆盖槽。 | | `--xh-alert-leading` | `root` | `line-height` | `default` | `--xh-leading-normal` | alert 的 root 部件 line-height 覆盖槽。 | | `--xh-alert-px` | `root` | `padding-inline` | `default` | `--xh-surface-px-sm` | alert 的 root 部件 padding-inline 覆盖槽。 | | `--xh-alert-py` | `root` | `padding-block` | `default` | `--xh-surface-py-sm` | alert 的 root 部件 padding-block 覆盖槽。 | | `--xh-alert-radius` | `root` | `border-radius` | `default` | `--xh-shape-surface` | alert 的 root 部件 border-radius 覆盖槽。 | | `--xh-alert-shadow` | `root` | `box-shadow` | `default` | `none` | alert 的 root 部件 box-shadow 覆盖槽。 | | `--xh-alert-title-fg` | `title` | `color` | `default` | `--xh-_tone-fg` | alert 的 title 部件 color 覆盖槽。 | | `--xh-alert-title-font-size` | `title` | `font-size` | `default` | `--xh-text-label-size` | alert 的 title 部件 font-size 覆盖槽。 | | `--xh-alert-title-font-weight` | `title` | `font-weight` | `default` | `--xh-font-weight-semibold` | alert 的 title 部件 font-weight 覆盖槽。 | | `--xh-alert-title-leading` | `title` | `line-height` | `default` | `--xh-leading-tight` | alert 的 title 部件 line-height 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/anchor # Anchor 锚点 根据滚动位置高亮当前章节的目录。 ## 用法 跟随滚动高亮当前章节 ```vue ``` ```html
概览

概览相关内容

安装

安装相关内容

主题

主题相关内容

发布

发布相关内容

``` ## 组件结构 加粗的是必需部件。 `data-scope="anchor"`:**`root`** · **`list`** · **`item`** · **`link`** · `link-text` · `indicator` ## 示例 ### 判定线偏移 为吸顶内容预留空间 ```vue ``` ```html
章节导航
第一节

第一节相关内容

第二节

第二节相关内容

第三节

第三节相关内容

``` ### 横向排列 在内容上方显示章节导航 ```vue ``` ```html
概览

概览相关内容

属性

属性相关内容

事件

事件相关内容

插槽

插槽相关内容

``` ### 嵌套目录 展示父级与子级章节 ```vue ``` ```html
指南

指南相关内容

安装

安装相关内容

快速开始

快速开始相关内容

接口

接口相关内容

属性

属性相关内容

事件

事件相关内容

``` ## 设计指引 ### 何时使用 - 为长文档、设置页或详情页提供章节导航。 ### 何时不用 - 切换独立内容使用[标签页](./tabs)。 - 不需要当前位置反馈时使用普通链接。 ### 特性 - 支持页面或指定容器滚动。 - 支持滚动偏移、平滑滚动和当前项指示线:不放 `indicator` 部件时当前链接自带一条静态线(竖排贴起始缘、横排贴底边),放了部件则由部件滑动。 - 支持水平、垂直和嵌套目录。 ### 组合 - 可与[固钉](./affix)和[排印](./typography)组合使用。 ### 最佳实践 - 有固定页头时设置对应的滚动偏移。 - 目录项文字应与正文标题一致。 ### 反模式 - 目录层级不宜超过两级。 - 不要用锚点切换独立视图。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhAnchorIndicator` `XhAnchorItem` `XhAnchorLink` `XhAnchorLinkText` `XhAnchorList` `XhAnchorRoot` | | 组合式函数 | `useAnchor` | | 状态机 | `anchorMachine` | | 皮肤 | `@xihan-ui/styles/anchor.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string \| null` | | 当前激活的锚点 id,给定即受控。 | | `defaultValue` | `string \| null` | | | | `collection` | `readonly string[]` | | 目标区块的 id 清单,按文档序提供;未提供时按渲染出的 link 查询。 | | `offset` | `number` | | 判定线距滚动容器视口顶边的距离(px),默认 0。 | | `bounds` | `number` | | 压线判定的容差(px),默认 1;区块顶边落在判定线下方该距离内仍视为越过。 | | `smooth` | `boolean` | | 点击链接时平滑滚动到目标,默认 false。 | | `dir` | `Direction` | | 文字方向,作用于排版与指示条的起始缘。 | | `orientation` | `Orientation` | | 列表轴向,默认 vertical,只影响样式。 | | `translations` | `Partial` | | | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `onValueChange` | `(details: AnchorValueChangeDetails) => void` | | value 变化意图回调。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `AnchorValueChangeDetails` | 激活项变化;detail 为 `{ value: string \| null }` | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhAnchorLink` | `value` | `string` | 是 | | | `XhAnchorRoot` | `scrollElement` | `() => HTMLElement \| null` | | 判定线所依附的滚动容器取值器,默认挂在窗口上;挂载效应执行时求值。 | | `XhAnchorRoot` | `children` | `ReactNode` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` · `scrolling` **事件**:`SPY.RESOLVE` · `LINK.CLICK` · `VALUE.SET` · `SCROLL.SETTLE` · `PRESS.START` · `PRESS.END` **判据**:`isSmooth` · `isTargetReached` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string \| null` | 当前激活的锚点 id;没有区块越过判定线时为 null。 | | `isActive` | `(value: string) => boolean` | | | `setValue` | `(next: string \| null) => void` | | | `getRootProps` | `() => T['element']` | | | `getListProps` | `() => T['element']` | | | `getItemProps` | `() => T['element']` | | | `getLinkProps` | `(props: AnchorLinkProps) => T['element']` | | | `getLinkTextProps` | `() => T['element']` | | | `getIndicatorProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/navigation.html) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` | focus in link | 跳到目标区块:smooth 关时由原生 <a href="#id"> 跳转,开时组件拦下并平滑滚动(两种情况都当场把激活项切过去,不等观察器) | | `Enter` / `Space` | held in link | 按住期间该链接投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下。跳到目标区块照旧由这一次按键承担,激活项与按压互相独立 | | `Tab` / `Shift+Tab` | focus in root | 逐条走过目录里的链接;锚点导航不做 roving tabindex,每一条都是独立的 Tab 停靠点 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-label` | props.translations.root | | `link` | `aria-current` | 'location' \| undefined | | `indicator` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/anchor.css` 使用 `[data-scope="anchor"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-orientation` | props.orientation | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `list` | `data-orientation` | props.orientation | | `link` | `data-current` | ''(条件成立时才出现) | | `link` | `data-pressed` | ''(条件成立时才出现) | | `link` | `data-xh-collection-context` | 'nav' | | `link` | `data-xh-collection-item` | '' | | `link` | `data-xh-collection-size` | props.size | | `link-text` | `data-xh-collection-slot` | 'text' | | `indicator` | `data-orientation` | props.orientation | | `indicator` | `data-value` | context.get('value') | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-anchor-fg` | `link`
`root` | `color` | `default`
`xh-collection-context=nav` | `--xh-fg-muted` | anchor 的 link、root 部件 color 覆盖槽。 | | `--xh-anchor-font-size` | `link`
`root` | `font-size` | `default` | `--xh-_anchor-font-size` | anchor 的 link、root 部件 font-size 覆盖槽。 | | `--xh-anchor-gap` | `list` | `gap` | `default` | `--xh-space-1` | anchor 的 list 部件 gap 覆盖槽。 | | `--xh-anchor-gap-horizontal` | `list` | `gap` | `orientation=horizontal` | `--xh-space-2` | anchor 的 list 部件 gap 覆盖槽。 | | `--xh-anchor-indicator-color` | `indicator`
`link` | `background` | `current`
`default` | `--xh-_anchor-accent` | anchor 的 indicator、link 部件 background 覆盖槽。 | | `--xh-anchor-indicator-radius` | `indicator`
`link` | `border-radius` | `current`
`default` | `--xh-shape-pill` | anchor 的 indicator、link 部件 border-radius 覆盖槽。 | | `--xh-anchor-indicator-thickness` | `indicator`
`link`
`list` | `block-size`
`inline-size`
`inset-block-end`
`inset-inline-start` | `current`
`default`
`orientation=horizontal`
`orientation=vertical` | `--xh-stroke-thick` | anchor 的 indicator、link、list 部件 block-size、inline-size、inset-block-end、inset-inline-start 覆盖槽。 | | `--xh-anchor-leading` | `link`
`root` | `line-height` | `default` | `--xh-leading-normal` | anchor 的 link、root 部件 line-height 覆盖槽。 | | `--xh-anchor-link-bg-hover` | `link` | `background-color` | `disabled`
`error`
`hover`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`xh-collection-context=nav` | `--xh-bg-subtle` | anchor 的 link 部件 background-color 覆盖槽。 | | `--xh-anchor-link-bg-pressed` | `link` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`xh-collection-context=nav` | `--xh-bg-subtle-hover` | anchor 的 link 部件 background-color 覆盖槽。 | | `--xh-anchor-link-fg-current` | `link` | `color` | `current`
`disabled`
`error`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`xh-collection-context=nav` | `--xh-_anchor-accent-text` | anchor 的 link 部件 color 覆盖槽。 | | `--xh-anchor-link-fg-hover` | `link` | `color` | `disabled`
`error`
`hover`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`xh-collection-context=nav` | `--xh-fg-default` | anchor 的 link 部件 color 覆盖槽。 | | `--xh-anchor-link-font-weight-current` | `link` | `font-weight` | `current`
`disabled`
`error`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`xh-collection-context=nav` | `--xh-font-weight-medium` | anchor 的 link 部件 font-weight 覆盖槽。 | | `--xh-anchor-link-max-w` | `link` | `max-inline-size` | `default` | `--xh-nav-link-max-w` | anchor 的 link 部件 max-inline-size 覆盖槽。 | | `--xh-anchor-link-px` | `link` | `padding-inline` | `default` | `--xh-_anchor-link-px` | anchor 的 link 部件 padding-inline 覆盖槽。 | | `--xh-anchor-link-py` | `link` | `padding-block` | `default` | `--xh-space-1` | anchor 的 link 部件 padding-block 覆盖槽。 | | `--xh-anchor-link-radius` | `link` | `border-radius` | `default` | `--xh-shape-control` | anchor 的 link 部件 border-radius 覆盖槽。 | | `--xh-anchor-track` | `list` | `border-block-end`
`border-inline-start` | `default`
`orientation=horizontal` | `--xh-border-default` | anchor 的 list 部件 border-block-end、border-inline-start 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 指示与换位(见[动效规范](../design/motion#角色))。 `block-size` · `inline-size` · `translate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:值由内核逐帧算出(`frameLoop`),皮肤里看不到这段。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/approval # Approval 审批 `alpha` 危险操作执行前的人工闸门:批准或拒绝,超时按拒绝处理,可附带勾选式的授权范围。 ## 用法 勾选与判定是原子的:批准的载荷带着批准的项,不存在已批准但范围尚未同步的窗口 ```vue ``` ```html

要动你的工作区

它想读一遍 src/ 并写回改动。

读取 src/ 下的文件
写回改动

``` ## 组件结构 加粗的是必需部件。 `data-scope="approval"`:**`root`** · `pending-indicator` · `title` · `description` · `live-region` · `group` · `item` · `item-indicator` · `item-text` · `note` · `timer` · `result` · `footer` · **`approve-trigger`** · **`deny-trigger`** ## 示例 ### 超时按拒绝收口 默认不提供超时值:替宿主制定安全策略比不制定更危险。到期落为拒绝,expired 只是显示态 ```vue ``` ```html

要执行一条删除命令

没人答的话,到点按拒绝处理。

还剩 10 秒

``` ### 附加备注 备注与勾选同批取快照,随判定载荷一起发出;留空时不带该字段,它不参与必选项是否勾满的判断 ```vue ``` ```html

要把这批改动推上去

推之前可以留一句话,随判定一起交给宿主。

``` ### 形态与尺寸 variant 改变该闸门与正文分开的方式,size 改变标题、条目与按钮的几何档;判定链不变 ```vue ``` ```html

描边(缺省)

写回改动

弱底分区

写回改动

无壳内联

写回改动

小档

写回改动

大档

写回改动
``` ## 设计指引 ### 何时使用 - Agent 执行写文件、发请求、付费等实际操作前需要用户确认。 - 授权带范围:批准的同时需要说明批准了哪些项。 ### 何时不用 - 只需要一句确认时,使用[弹出确认](./popconfirm)。 - 判定结果不影响任何执行时,它不是闸门,应使用提示。 ### 特性 - 超时一律按拒绝处理,由状态机结构保证,不依赖调用方遵守约定:判定的取值只有批准与拒绝,`expired` 只是显示状态;通往批准的转移只有一条;到期事件只在待决状态有转移,迟到的定时事件静默丢弃。 - 不提供默认超时值,由宿主决定安全策略。时长非有限数或非正数时不启动计时器,停留在待决状态;既不按 0ms 立即到期,也不视为无限期放行。 - 拒绝路径始终可达:状态机层的拒绝不受必选项和任何闸门限制,超时、卸载兜底与宿主的 `deny()` 入口都能落地。拒绝按钮与 Escape 另有一道挂起闸门:判定在途时与批准按钮一起锁定,避免等待宿主响应期间产生第二条判定。 - 勾选与判定是原子的:批准的载荷携带已勾选的授权项,不存在已批准但范围未同步的窗口。 - 备注(`note`)与勾选同批快照,随判定载荷一起发出;为空时不携带该字段。备注不参与必选项是否勾满的判断。 - `requestId` 变化即重新进入待决并按新时长重启计时;不为上一轮补发拒绝,旧结果由宿主自行作废。重入时勾选与备注回到各自默认值。 - 判定落定后 `result` 部件才显示,语气随判定变化:批准取成功档,拒绝与超时取危险档。它对读屏隐藏,同一句话由播报区读出一次。 - 两个按钮位于 `actions` 行内,间距与对齐由库统一处理,使用者不需要另写容器。 - 待决时 `pending-indicator` 在右上角显示一颗呼吸的圆点,颜色随语气、缺省取警示色,判定落定即收起;它不占作者排的版面。减弱动效下圆点静止。 ### 组合 - 放入[工具调用](./tool-call)的 `approval` 部件位:该位置常驻于开关与详情之间,不会被折叠隐藏。 - 需要弹窗时每个闸门一个[对话框](./dialog):`role="alertdialog"`、关闭 `closeOnEscape`,并把 `initialFocus` 设为本组件导出的 `APPROVAL_DENY_SELECTOR`。浮层只保留批准与拒绝两个出口,Escape 仍冒泡到闸门并判为拒绝。 - 剩余时间的显示交给[计时器](./timer),判定权仍由本组件持有。不要把倒计时直接渲染为 `timer` 节点:两套解剖打在同一节点上会互相覆盖,应让 `timer` 作为外层容器。 - 需要连续询问多件事时,使用[步骤条](./steps)或[走马灯](./carousel)串联多个闸门。本组件是单发闸门,`data-state` 的四个值互斥,不表达序号。 - 需要提供“稍后再说”入口时,该入口由宿主实现,不属于闸门。常见做法是在 `onDecision` 之外另留延后路径,或按上一条把闸门放进对话框;浮层内仍只有批准与拒绝两个出口。 ### 最佳实践 - 判定落定后两个按钮都会禁用,浮层不再有出口,宿主必须在判定回调中关闭浮层。 - 卸载即拒绝(`denyOnUnmount`)默认关闭。启用前确认列表换 key、路由切换、热更新等任何一次重新挂载都会替用户发出判定。 ### 反模式 - 把超时做成到点自动放行。 - 用可关闭的浮层承载闸门:关闭窗口既不是批准也不是拒绝,闸门会悬空。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhApprovalApproveTrigger` `XhApprovalDenyTrigger` `XhApprovalDescription` `XhApprovalFooter` `XhApprovalGroup` `XhApprovalItem` `XhApprovalItemIndicator` `XhApprovalItemText` `XhApprovalLiveRegion` `XhApprovalNote` `XhApprovalPendingIndicator` `XhApprovalResult` `XhApprovalRoot` `XhApprovalTimer` `XhApprovalTitle` | | 组合式函数 | `useApproval` | | 状态机 | `approvalMachine` | | 皮肤 | `@xihan-ui/styles/approval.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `requestId` | `string` | | 本轮请求的身份。变化即重新进入待决,并按新时长重新计时。 | | `status` | `ApprovalStatus` | | 提供即受控。 | | `defaultStatus` | `ApprovalStatus` | | | | `timeoutMs` | `number` | | 超时无人应答时按拒绝收口。默认不提供默认值:替宿主决定安全策略比不决定更危险。 非有限值或非正数同样不启动计时器,既不按 0ms 立即到期,也不视为无限期放行。 | | `scopes` | `readonly ApprovalScope[]` | | | | `grantedScopes` | `readonly string[]` | | | | `defaultGrantedScopes` | `readonly string[]` | | | | `note` | `string` | | 附在判定上的一段自由文本。提供即受控。 它只随判定载荷发出,不参与必选项是否全部勾选的判断。 | | `defaultNote` | `string` | | | | `loading` | `boolean` | | 判定在途:只阻止重复批准,不阻止拒绝。 | | `denyOnEscape` | `boolean` | | Escape 判为拒绝,默认开启。 | | `denyOnUnmount` | `boolean` | | 卸载时若仍待决则按拒绝派发一次,默认关闭。 机制成立不等于默认值成立:列表更换 key、路由切换、热更新的任何一次重挂, 都会替用户发出未做过的判定。 | | `live` | `'polite' \| 'assertive'` | | 播报档位,默认 polite。 | | `variant` | `ControlVariant` | | 形态:outline 描边、subtle 底色分区、ghost 无壳内联。默认 outline。 | | `tone` | `Tone` | | | | `size` | `Size` | | | | `translations` | `Partial` | | | | `onDecision` | `(details: ApprovalDecisionDetails) => void` | | | | `onGrantedScopesChange` | `(details: ApprovalScopesChangeDetails) => void` | | | | `onNoteChange` | `(details: ApprovalNoteChangeDetails) => void` | | | ### ApprovalScope `scopes` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | | | `required` | `boolean` | | 必选项:未全部勾选时不能批准。 | | `disabled` | `boolean` | | | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `decision` | `ApprovalDecisionDetails` | 判定落定;detail 为 `{ requestId, decision, source, scopes }` | | `granted-scopes-change` | `ApprovalScopesChangeDetails` | 勾选的授权项变化;detail 为 `{ value: string[] }` | | `note-change` | `ApprovalNoteChangeDetails` | 备注变化;detail 为 `{ value: string }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhApprovalItem` | `default` | `ApprovalScopeSlotProps` | | | `XhApprovalRoot` | `default` | `ApprovalRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhApprovalItem` | `scopeValue` | `string` | 是 | 该项授权范围的取值。 | | `XhApprovalItem` | `scopeLabel` | `string` | | | | `XhApprovalItem` | `scopeRequired` | `boolean` | | 必选项:未全部勾选时无法批准。 | | `XhApprovalItem` | `scopeDisabled` | `boolean` | | | | `XhApprovalItem` | `children` | `SlotChildren` | | | | `XhApprovalItemIndicator` | `scopeValue` | `string` | 是 | | | `XhApprovalItemText` | `scopeValue` | `string` | 是 | | | `XhApprovalRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `pending-indicator` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `item` | 'checked' \| 'unchecked' | | `item-indicator` | 'checked' \| 'unchecked' | | `note` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `timer` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `result` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `approve-trigger` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `deny-trigger` | 'pending' \| 'approved' \| 'denied' \| 'expired' | 以下名称仅用于内部状态机。 **状态**:`pending` · `approved` · `denied` · `expired` **事件**:`APPROVE` · `DENY` · `SCOPE.TOGGLE` · `SCOPE.SET` · `NOTE.SET` · `after.timeout` · `CONTROLLED.PENDING` · `CONTROLLED.APPROVE` · `CONTROLLED.DENY` · `CONTROLLED.EXPIRE` · `REQUEST.RESET` · `PRESS.START` · `PRESS.END` **判据**:`isStatusControlled` · `canApprove` · `isEditable` · `canApproveControlled` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `status` | `ApprovalStatus` | | | `settled` | `boolean` | 已判定:两个按钮都收起出口。 | | `loading` | `boolean` | | | `grantedScopes` | `string[]` | | | `note` | `string` | 备注中的文字;未填写时为空串。 | | `canApprove` | `boolean` | 必选项是否全部勾选。 | | `announcement` | `string` | 按 status 选出的播报文本;关闭 announce 时作者不渲染该部件即可。 | | `approve` | `() => void` | | | `deny` | `() => void` | | | `setGrantedScopes` | `(next: string[]) => void` | | | `setNote` | `(next: string) => void` | | | `isScopeGranted` | `(value: string) => boolean` | | | `getRootProps` | `() => T['element']` | | | `getPendingIndicatorProps` | `() => T['element']` | 待决时的呼吸点:判过即收,对读屏隐藏。 | | `getTitleProps` | `() => T['element']` | | | `getDescriptionProps` | `() => T['element']` | | | `getLiveRegionProps` | `() => T['element']` | | | `getGroupProps` | `() => T['element']` | | | `getItemProps` | `(scope: ApprovalScope) => T['element']` | | | `getItemIndicatorProps` | `(scope: ApprovalScope) => T['element']` | | | `getItemTextProps` | `(scope: ApprovalScope) => T['element']` | | | `getNoteProps` | `() => T['input']` | | | `getTimerProps` | `() => T['element']` | | | `getResultProps` | `() => T['element']` | | | `getFooterProps` | `() => T['element']` | | | `getApproveTriggerProps` | `() => T['button']` | | | `getDenyTriggerProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | 焦点在批准按钮上,待决、必选项已勾满、且不在挂起中 | 判为批准,载荷带上已勾选的授权项 | | `Enter` / `Space` | 焦点在拒绝按钮上,待决且不在挂起中 | 判为拒绝 | | `Space` | 焦点在授权项上,待决且该项未禁用 | 勾选或取消该项。Enter 刻意不参与,与原生复选框一致 | | `Enter` / `Space` | 按住批准或拒绝按钮,待决且不在挂起中;批准还要必选项已勾满 | 按住期间该钮投影 data-pressed,与指针 :active 同一副按压面(text 档定尺按钮,按下缩放并换底);抬起、失焦、判定落定或转入挂起撤下 | | `Space` | 按住授权项,待决、不在挂起中且该项未禁用 | 按住期间该行投影 data-pressed,与指针 :active 同一副按压面(row 档只换面不缩放);抬起或失焦撤下。Enter 不是复选框的激活键,不进按压面 | | `Escape` | 焦点在闸门内,待决、未挂起、且开启 denyOnEscape | 判为拒绝。它不是关闭:本组件不提供不作答的出口 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-describedby` | `description` 部件的 id | | `root` | `aria-labelledby` | `title` 部件的 id | | `root` | `role` | 'group' | | `pending-indicator` | `aria-hidden` | 'true' | | `live-region` | `aria-atomic` | 'true' | | `live-region` | `aria-live` | props.live | | `group` | `aria-label` | translations?.scopes | | `group` | `role` | 'group' | | `item` | `aria-checked` | 'true' \| 'false' | | `item` | `aria-disabled` | 'true' \| 'false' | | `item` | `aria-required` | 'true' \| 'false' | | `item` | `role` | 'checkbox' | | `item-indicator` | `aria-hidden` | 'true' | | `note` | `aria-label` | translations?.note | | `timer` | `aria-hidden` | 'true' | | `result` | `aria-hidden` | 'true' | | `approve-trigger` | `aria-busy` | 'true' \| undefined | | `approve-trigger` | `aria-disabled` | 'true' \| 'false' | | `approve-trigger` | `aria-label` | translations?.approve | | `deny-trigger` | `aria-busy` | 'true' \| undefined | | `deny-trigger` | `aria-disabled` | 'true' \| 'false' | | `deny-trigger` | `aria-label` | translations?.deny | - 闸门是 `role=group`,由标题命名、由说明描述。 - 待决时批准键使用 `aria-disabled` 而不是原生 `disabled`:保持可聚焦,读屏可以读出不可用的原因。 - 授权项是 `role=checkbox`,各占一个 Tab 停靠点,只响应 `Space`,与原生复选框一致。 - 剩余时间、结果条与待决的呼吸点都对读屏隐藏:逐秒变化的数字进入活动区域会持续打断,判定结果与截止事件由播报区各读出一次。 - 备注取 `translations.note` 作为可访问名称(默认 `Note`),占位文字取 `translations.notePlaceholder`。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/approval.css` 使用 `[data-scope="approval"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-loading` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `pending-indicator` | `data-state` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-pressed` | ''(条件成立时才出现) | | `item` | `data-state` | 'checked' \| 'unchecked' | | `item` | `data-value` | item.value | | `item` | `data-xh-action-control` | '' | | `item` | `data-xh-action-display` | 'always' | | `item` | `data-xh-action-profile` | 'row' | | `item` | `data-xh-action-size` | props.size | | `item` | `data-xh-action-variant` | 'ghost' | | `item-indicator` | `data-state` | 'checked' \| 'unchecked' | | `item-text` | `data-value` | item.value | | `note` | `data-state` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `timer` | `data-state` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `result` | `data-state` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `approve-trigger` | `data-disabled` | ''(条件成立时才出现) | | `approve-trigger` | `data-loading` | ''(条件成立时才出现) | | `approve-trigger` | `data-pressed` | ''(条件成立时才出现) | | `approve-trigger` | `data-state` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `approve-trigger` | `data-tone` | props.tone | | `approve-trigger` | `data-xh-action-control` | '' | | `approve-trigger` | `data-xh-action-display` | 'always' | | `approve-trigger` | `data-xh-action-profile` | 'text' | | `approve-trigger` | `data-xh-action-size` | props.size | | `approve-trigger` | `data-xh-action-variant` | 'solid' | | `approve-trigger` | `data-xh-ink-surface` | '' | | `deny-trigger` | `data-disabled` | ''(条件成立时才出现) | | `deny-trigger` | `data-loading` | ''(条件成立时才出现) | | `deny-trigger` | `data-pressed` | ''(条件成立时才出现) | | `deny-trigger` | `data-state` | 'pending' \| 'approved' \| 'denied' \| 'expired' | | `deny-trigger` | `data-xh-action-control` | '' | | `deny-trigger` | `data-xh-action-display` | 'always' | | `deny-trigger` | `data-xh-action-profile` | 'text' | | `deny-trigger` | `data-xh-action-size` | props.size | | `deny-trigger` | `data-xh-action-variant` | 'outline' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-approval-action-font-size` | `approve-trigger`
`deny-trigger`
`footer`
`root` | `font-size` | `default`
`loading` | `--xh-text-label-size` | approval 的 approve-trigger、deny-trigger、footer、root 部件 font-size 覆盖槽。 | | `--xh-approval-action-font-weight` | `approve-trigger`
`deny-trigger` | `font-weight` | `default` | `--xh-text-label-weight` | approval 的 approve-trigger、deny-trigger 部件 font-weight 覆盖槽。 | | `--xh-approval-action-h` | `approve-trigger`
`deny-trigger` | `block-size`
`min-block-size` | `default`
`xh-action-profile=row` | `--xh-_approval-action-h` | approval 的 approve-trigger、deny-trigger 部件 block-size、min-block-size 覆盖槽。 | | `--xh-approval-action-px` | `approve-trigger`
`deny-trigger` | `padding-inline` | `default` | `--xh-_approval-action-px` | approval 的 approve-trigger、deny-trigger 部件 padding-inline 覆盖槽。 | | `--xh-approval-action-radius` | `approve-trigger`
`deny-trigger` | `border-radius` | `default` | `--xh-shape-control` | approval 的 approve-trigger、deny-trigger 部件 border-radius 覆盖槽。 | | `--xh-approval-approve-bg` | `approve-trigger` | `--xh-ink-surface`
`background-color` | `default`
`loading`
`xh-ink-surface` | `--xh-_action-variant-bg-loading`
`--xh-_action-variant-bg-rest` | approval 的 approve-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-approval-approve-bg-hover` | `approve-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | approval 的 approve-trigger 部件 background-color 覆盖槽。 | | `--xh-approval-approve-bg-off` | `approve-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`xh-ink-surface` | `--xh-_action-variant-bg-disabled` | approval 的 approve-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-approval-approve-fg` | `approve-trigger` | `color` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-loading`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | approval 的 approve-trigger 部件 color 覆盖槽。 | | `--xh-approval-approve-shadow` | `approve-trigger` | `box-shadow` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_highlight-tone` | approval 的 approve-trigger 部件 box-shadow 覆盖槽。 | | `--xh-approval-bg` | `root` | `background` | `default`
`variant=subtle` | `--xh-bg-subtle`
`--xh-bg-surface` | approval 的 root 部件 background 覆盖槽。 | | `--xh-approval-border` | `root` | `border`
`border-color` | `default`
`tone` | `--xh-_tone`
`--xh-border-default` | approval 的 root 部件 border、border-color 覆盖槽。 | | `--xh-approval-border-settled` | `root` | `border-color` | `not([data-state='pending'])`
`state=pending` | `--xh-border-default` | approval 的 root 部件 border-color 覆盖槽。 | | `--xh-approval-deny-bg` | `deny-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | approval 的 deny-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-approval-deny-bg-hover` | `deny-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | approval 的 deny-trigger 部件 background-color 覆盖槽。 | | `--xh-approval-deny-bg-off` | `deny-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`xh-ink-surface` | `--xh-_action-variant-bg-disabled` | approval 的 deny-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-approval-deny-border` | `deny-trigger` | `border` | `default` | `--xh-_action-variant-border-rest` | approval 的 deny-trigger 部件 border 覆盖槽。 | | `--xh-approval-deny-border-off` | `deny-trigger` | `border-color` | `disabled` | `--xh-_action-variant-border-disabled` | approval 的 deny-trigger 部件 border-color 覆盖槽。 | | `--xh-approval-deny-fg` | `deny-trigger` | `color` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | approval 的 deny-trigger 部件 color 覆盖槽。 | | `--xh-approval-description-fg` | `description` | `color` | `default` | `--xh-fg-muted` | approval 的 description 部件 color 覆盖槽。 | | `--xh-approval-description-font-size` | `description` | `font-size` | `default` | `--xh-text-secondary-size` | approval 的 description 部件 font-size 覆盖槽。 | | `--xh-approval-footer-gap` | `footer` | `gap` | `default` | `--xh-space-2` | approval 的 footer 部件 gap 覆盖槽。 | | `--xh-approval-gap` | `root` | `gap` | `default` | `--xh-_approval-gap` | approval 的 root 部件 gap 覆盖槽。 | | `--xh-approval-group-gap` | `group` | `gap` | `default` | `--xh-space-1` | approval 的 group 部件 gap 覆盖槽。 | | `--xh-approval-icon-size` | `root` | `--xh-icon-size` | `default` | `--xh-_approval-icon-size` | approval 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-approval-indicator-bg-checked` | `item-indicator` | `background` | `state=checked` | `--xh-bg-brand` | approval 的 item-indicator 部件 background 覆盖槽。 | | `--xh-approval-indicator-border` | `item-indicator` | `border` | `default` | `--xh-border-control` | approval 的 item-indicator 部件 border 覆盖槽。 | | `--xh-approval-indicator-border-checked` | `item-indicator` | `border-color` | `state=checked` | `--xh-bg-brand` | approval 的 item-indicator 部件 border-color 覆盖槽。 | | `--xh-approval-indicator-fg` | `item-indicator` | `color` | `default` | `--xh-fg-on-brand` | approval 的 item-indicator 部件 color 覆盖槽。 | | `--xh-approval-indicator-icon-size` | `item-indicator` | `--xh-icon-size` | `default` | `--xh-_approval-indicator-glyph` | approval 的 item-indicator 部件 --xh-icon-size 覆盖槽。 | | `--xh-approval-indicator-radius` | `item-indicator` | `border-radius` | `default` | `--xh-shape-inset` | approval 的 item-indicator 部件 border-radius 覆盖槽。 | | `--xh-approval-indicator-size` | `item-indicator` | `--xh-icon-size`
`block-size`
`inline-size` | `default` | `--xh-control-indicator-size` | approval 的 item-indicator 部件 --xh-icon-size、block-size、inline-size 覆盖槽。 | | `--xh-approval-item-bg-hover` | `item` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | approval 的 item 部件 background-color 覆盖槽。 | | `--xh-approval-item-font-size` | `item` | `font-size` | `default` | `--xh-_approval-item-font-size` | approval 的 item 部件 font-size 覆盖槽。 | | `--xh-approval-item-gap` | `item` | `gap` | `default` | `--xh-space-1_5` | approval 的 item 部件 gap 覆盖槽。 | | `--xh-approval-item-px` | `item` | `padding-inline` | `default` | `--xh-space-2` | approval 的 item 部件 padding-inline 覆盖槽。 | | `--xh-approval-item-py` | `item` | `padding-block` | `xh-action-profile=row` | `--xh-space-1` | approval 的 item 部件 padding-block 覆盖槽。 | | `--xh-approval-item-radius` | `item` | `border-radius` | `default` | `--xh-_action-profile-radius` | approval 的 item 部件 border-radius 覆盖槽。 | | `--xh-approval-item-text-fg` | `item-text` | `color` | `default` | `--xh-fg-muted` | approval 的 item-text 部件 color 覆盖槽。 | | `--xh-approval-item-text-fg-checked` | `item`
`item-text` | `color` | `state=checked` | `--xh-fg-default` | approval 的 item、item-text 部件 color 覆盖槽。 | | `--xh-approval-loading-duration` | `footer`
`root` | `animation` | `loading` | `--xh-motion-loop-spin` | approval 的 footer、root 部件 animation 覆盖槽。 | | `--xh-approval-note-bg` | `note` | `background` | `default` | `--xh-bg-surface` | approval 的 note 部件 background 覆盖槽。 | | `--xh-approval-note-border` | `note` | `border` | `default` | `--xh-border-control` | approval 的 note 部件 border 覆盖槽。 | | `--xh-approval-note-fg` | `note` | `color` | `default` | `--xh-fg-default` | approval 的 note 部件 color 覆盖槽。 | | `--xh-approval-note-font-size` | `note` | `font-size` | `default` | `--xh-_approval-note-font-size` | approval 的 note 部件 font-size 覆盖槽。 | | `--xh-approval-note-px` | `note` | `padding-inline` | `default` | `--xh-space-2` | approval 的 note 部件 padding-inline 覆盖槽。 | | `--xh-approval-note-py` | `note` | `padding-block` | `default` | `--xh-space-1_5` | approval 的 note 部件 padding-block 覆盖槽。 | | `--xh-approval-note-radius` | `note` | `border-radius` | `default` | `--xh-shape-control` | approval 的 note 部件 border-radius 覆盖槽。 | | `--xh-approval-p` | `pending-indicator`
`root` | `inset-block-start`
`inset-inline-end`
`padding` | `default` | `--xh-_approval-p` | approval 的 pending-indicator、root 部件 inset-block-start、inset-inline-end、padding 覆盖槽。 | | `--xh-approval-pending-indicator-color` | `pending-indicator` | `background` | `default` | `--xh-_tone` | approval 的 pending-indicator 部件 background 覆盖槽。 | | `--xh-approval-pending-indicator-radius` | `pending-indicator` | `border-radius` | `default` | `--xh-shape-circle` | approval 的 pending-indicator 部件 border-radius 覆盖槽。 | | `--xh-approval-pending-indicator-size` | `pending-indicator` | `block-size`
`inline-size` | `default` | `--xh-space-2` | approval 的 pending-indicator 部件 block-size、inline-size 覆盖槽。 | | `--xh-approval-radius` | `root` | `border-radius` | `default` | `--xh-shape-surface` | approval 的 root 部件 border-radius 覆盖槽。 | | `--xh-approval-result-bg` | `result` | `background` | `default` | `--xh-fg-success` | approval 的 result 部件 background 覆盖槽。 | | `--xh-approval-result-bg-denied` | `result` | `background` | `is([data-state='denied'], [data-state='expired'])`
`state=denied`
`state=expired` | `--xh-fg-danger` | approval 的 result 部件 background 覆盖槽。 | | `--xh-approval-result-fg` | `result` | `color` | `default` | `--xh-fg-success` | approval 的 result 部件 color 覆盖槽。 | | `--xh-approval-result-fg-denied` | `result` | `color` | `is([data-state='denied'], [data-state='expired'])`
`state=denied`
`state=expired` | `--xh-fg-danger` | approval 的 result 部件 color 覆盖槽。 | | `--xh-approval-result-font-size` | `result` | `font-size` | `default` | `--xh-text-caption-size` | approval 的 result 部件 font-size 覆盖槽。 | | `--xh-approval-result-font-weight` | `result` | `font-weight` | `default` | `--xh-text-label-weight` | approval 的 result 部件 font-weight 覆盖槽。 | | `--xh-approval-result-gap` | `result` | `gap` | `default` | `--xh-space-1_5` | approval 的 result 部件 gap 覆盖槽。 | | `--xh-approval-result-px` | `result` | `padding-inline` | `default` | `--xh-space-2` | approval 的 result 部件 padding-inline 覆盖槽。 | | `--xh-approval-result-py` | `result` | `padding-block` | `default` | `--xh-space-1` | approval 的 result 部件 padding-block 覆盖槽。 | | `--xh-approval-result-radius` | `result` | `border-radius` | `default` | `--xh-shape-pill` | approval 的 result 部件 border-radius 覆盖槽。 | | `--xh-approval-shadow` | `root` | `box-shadow` | `default` | `none` | approval 的 root 部件 box-shadow 覆盖槽。 | | `--xh-approval-timer-fg` | `timer` | `color` | `default` | `--xh-fg-muted` | approval 的 timer 部件 color 覆盖槽。 | | `--xh-approval-timer-font-size` | `timer` | `font-size` | `default` | `--xh-text-caption-size` | approval 的 timer 部件 font-size 覆盖槽。 | | `--xh-approval-title-fg` | `title` | `color` | `default` | `--xh-fg-default` | approval 的 title 部件 color 覆盖槽。 | | `--xh-approval-title-font-size` | `title` | `font-size` | `default` | `--xh-text-label-size` | approval 的 title 部件 font-size 覆盖槽。 | | `--xh-approval-title-font-weight` | `title` | `font-weight` | `default` | `--xh-font-weight-semibold` | approval 的 title 部件 font-weight 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(无锚定弹出) · 列表 · 循环(见[动效规范](../design/motion#角色))。 可覆盖的动效槽:`--xh-approval-loading-duration`。 共享关键帧 `xh-breathe` · `xh-breathe-halo` · `xh-item-in` · `xh-pop-in` · `xh-spin` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`background-color` · `border-color` · `color` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 `prefers-reduced-motion: reduce` 下本组件另有降级规则。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/avatar-group # AvatarGroup 头像组 将若干头像叠成一排,超出上限的部分收为一个计数。 ## 用法 一排叠放的头像:后一个压在前一个上,被压住的边由一圈底色分开 ```vue ``` ```html
曦 寒 懿 承
``` ## 组件结构 加粗的是必需部件。 `data-scope="avatar-group"`:**`root`** · `overflow-item` ## 示例 ### 上限与溢出计数 放置到上限为止,其余收为一个「+N」;截到几个、N 写多少由作者决定,组件只提供该项的身份与位置 ```vue ``` ```html
曦 寒 懿 承 +2
``` ### 尺寸 直径、字号与叠放量在组上写一次,沿自定义属性下发给组内每个头像,「+N」随之更换 ```vue ``` ```html
曦 寒 懿 +3
曦 寒 懿 +3
曦 寒 懿 +3
``` ### 使用者令牌 直径、叠放量、分隔用的底色都保留了槽位,写在组上即整组更换 ```vue ``` ```html
曦 寒 懿 承 +2
``` ## 设计指引 ### 何时使用 - 表示一组参与者,且不需要逐一确认个体身份。 ### 何时不用 - 需要逐个识别或操作时,排成[列表](./list)。 - 只有一个人时,直接使用头像。 ### 特性 - `max` 决定显示数量,其余进入 `overflow-item` 计数。 - 尺寸写在组上,组内头像一并跟随。 ### 组合 - 组内放置[头像](./avatar);溢出计数可以打开一张[气泡卡片](./popover)显示完整名单。 ### 最佳实践 - 溢出计数应能打开查看完整名单。 - 每个头像配[文字提示](./tooltip)给出姓名。 ### 反模式 - 叠放过密,无法分辨人数。 - 上限过大,一排头像占满整行。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhAvatarGroupOverflowItem` `XhAvatarGroupRoot` | | 状态机 | 无,`connect` 直接由 props 算属性 | | 皮肤 | `@xihan-ui/styles/avatar-group.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `max` | `number` | | 展示上限:本组展示的头像数量,其余收进 overflow-item。 头像由作者渲染,因此裁切数量与 +N 中的 N 都由作者决定; 组件把这个上限如实写入根上的 data-max。 | | `size` | `Size` | | 尺寸:sm / md / lg,写入根上并沿继承流下发给组内每一个头像。 | ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `getRootProps` | `() => T['element']` | | | `getOverflowItemProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/avatar-group.css` 使用 `[data-scope="avatar-group"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-avatar-group-font-size` | `overflow-item`
`root` | `--xh-avatar-font-size`
`font-size` | `default`
`size=lg`
`size=sm` | `--xh-control-caption-lg`
`--xh-control-caption-md`
`--xh-control-caption-sm` | avatar-group 的 overflow-item、root 部件 --xh-avatar-font-size、font-size 覆盖槽。 | | `--xh-avatar-group-font-weight` | `overflow-item` | `font-weight` | `default` | `--xh-font-weight-medium` | avatar-group 的 overflow-item 部件 font-weight 覆盖槽。 | | `--xh-avatar-group-overflow-item-bg` | `overflow-item` | `background` | `default` | `--xh-bg-muted-opaque` | avatar-group 的 overflow-item 部件 background 覆盖槽。 | | `--xh-avatar-group-overflow-item-fg` | `overflow-item` | `color` | `default` | `--xh-fg-muted` | avatar-group 的 overflow-item 部件 color 覆盖槽。 | | `--xh-avatar-group-overlap` | `root` | `margin-inline-start` | `default`
`size=lg`
`size=sm` | `--xh-space-2`
`--xh-space-2_5`
`--xh-space-3` | avatar-group 的 root 部件 margin-inline-start 覆盖槽。 | | `--xh-avatar-group-radius` | `overflow-item` | `border-radius` | `default` | `--xh-shape-circle` | avatar-group 的 overflow-item 部件 border-radius 覆盖槽。 | | `--xh-avatar-group-ring` | `root` | `box-shadow` | `default` | `--xh-bg-surface` | avatar-group 的 root 部件 box-shadow 覆盖槽。 | | `--xh-avatar-group-size` | `overflow-item`
`root` | `--xh-avatar-size`
`block-size`
`inline-size` | `default`
`size=lg`
`size=sm` | `--xh-control-h-lg`
`--xh-control-h-md`
`--xh-control-h-sm` | avatar-group 的 overflow-item、root 部件 --xh-avatar-size、block-size、inline-size 覆盖槽。 | ### 动效 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/avatar # Avatar 头像 表示一个人或一个组织的圆形标识:优先显示图片,无法加载时回退到文字或图标。 ## 用法 图片加载失败或未提供时落到 fallback ```vue ``` ```html 曦 XH ``` ## 组件结构 加粗的是必需部件。 `data-scope="avatar"`:`root` · `image` · **`fallback`** ## 示例 ### 加载失败回退 图片地址取不到时切到 fallback,切换由状态机决定而不是 CSS ```vue ``` ```html 回退 无图 ``` ### 尺寸 size 三档只改变直径,回退文字的字号随之缩放;默认档不输出 data-size ```vue ``` ```html
曦 曦 曦 sm / 缺省 / lg
XH XH XH 回退字随档位缩放
``` ### 形状 圆角是一个组件令牌,整圆、圆角方、直角都是同一个槽位换值;图片的圆角从根继承,不必另设 ```vue ``` ```html
曦 曦 曦 整圆(缺省)/ 圆角方 / 直角
XH XH XH
``` ### 图标作为回退 fallback 是普通插槽,放图标与放缩写文字一样;没有名字可写时用图标表示某位用户 ```vue ``` ```html
图元跟着档位一起换,取的是根流下来的前景色
``` ### 自定义直径与配色 三档之外的直径、底色、字色各是一个组件令牌;按人名分配颜色即逐个实例覆盖 ```vue ``` ```html
曦 曦寒 直径 56px
曦 寒 懿 XH 底色与字色逐个给
``` ### 加载状态 status-change 在状态落位时通知,过渡态 idle 不通知;未提供地址等同于无法获取,直接落为 error 由回退接管 ```vue ``` ```html
曦 地址有效 → 等待中
回退 地址取不到 → 等待中
无图 没给地址 → 等待中
``` ### 颜色 tone 改变淡底与回退文字的配色组;不写 tone 即中性默认,直径与字号都不受影响 ```vue ``` ```html
XH 曦 中 成 警 危 信
``` ## 设计指引 ### 何时使用 - 在列表、评论、成员选择中标识身份。 ### 何时不用 - 标识功能或分类时,使用[图标块](./icon-wrapper)。 - 只是展示一张图片时,使用[图片](./image)。 ### 特性 - 加载状态通过回调通知;失败时自动显示 `fallback`。 - 直径与配色都是组件令牌,可逐实例覆盖。 - `tone` 切换淡底与回退文字的配色族;未设置时使用中性默认值。 - 状态点与角标由作者挂在外部,组件不预设。 ### 组合 - 成组展示时使用[头像组](./avatar-group);角标使用[徽标](./badge)。 ### 最佳实践 - 回退内容应有意义:姓名缩写比通用人形图标携带更多信息。 - `alt` 写人名,不写“头像”。 ### 反模式 - 只提供图片、不提供回退:图片失效后会留下空洞。 - 只用头像颜色编码身份而不提供文字。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhAvatarFallback` `XhAvatarImage` `XhAvatarRoot` | | 组合式函数 | `useAvatar` | | 状态机 | `avatarMachine` | | 皮肤 | `@xihan-ui/styles/avatar.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `src` | `string` | | | | `alt` | `string` | | | | `size` | `Size` | | 尺寸:sm / md / lg,默认 md;默认档不输出 data-size | | `tone` | `Tone` | | 颜色:决定底色与回退字使用哪组状态色;未提供时不输出 data-tone,使用皮肤的中性默认 | | `onStatusChange` | `(details: AvatarStatusChangeDetails) => void` | | 状态落定时通知,过渡态 idle 不通知。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `status-change` | `AvatarStatusChangeDetails` | 加载状态变化;detail 为 `{ status: 'loading' \| 'loaded' \| 'error' }` | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'idle' \| 'loading' \| 'loaded' \| 'error' | | `image` | 'idle' \| 'loading' \| 'loaded' \| 'error' | | `fallback` | 'idle' \| 'loading' \| 'loaded' \| 'error' | 以下名称仅用于内部状态机。 **状态**:`idle` · `loading` · `loaded` · `error` **事件**:`SRC.CHANGE` · `IMAGE.LOAD` · `IMAGE.ERROR` **判据**:`hasSrc` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `status` | `AvatarStatus` | | | `loaded` | `boolean` | | | `getRootProps` | `() => T['element']` | | | `getImageProps` | `() => T['img']` | | | `getFallbackProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/avatar.css` 使用 `[data-scope="avatar"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-size` | props.size | | `root` | `data-state` | 'idle' \| 'loading' \| 'loaded' \| 'error' | | `root` | `data-tone` | props.tone | | `image` | `data-state` | 'idle' \| 'loading' \| 'loaded' \| 'error' | | `fallback` | `data-state` | 'idle' \| 'loading' \| 'loaded' \| 'error' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-avatar-bg` | `root` | `background` | `default`
`tone` | `--xh-_tone-subtle`
`--xh-bg-subtle-opaque` | avatar 的 root 部件 background 覆盖槽。 | | `--xh-avatar-fg` | `root` | `color` | `default`
`tone` | `--xh-_tone-fg`
`--xh-fg-muted` | avatar 的 root 部件 color 覆盖槽。 | | `--xh-avatar-font-size` | `root` | `font-size` | `default`
`size=lg`
`size=sm` | `--xh-control-caption-lg`
`--xh-control-caption-sm`
`--xh-text-secondary-size` | avatar 的 root 部件 font-size 覆盖槽。 | | `--xh-avatar-font-weight` | `root` | `font-weight` | `default` | `--xh-font-weight-medium` | avatar 的 root 部件 font-weight 覆盖槽。 | | `--xh-avatar-radius` | `root` | `border-radius` | `default` | `--xh-shape-circle` | avatar 的 root 部件 border-radius 覆盖槽。 | | `--xh-avatar-size` | `root` | `block-size`
`inline-size` | `default`
`size=lg`
`size=sm` | `--xh-control-h-lg`
`--xh-control-h-md`
`--xh-control-h-sm` | avatar 的 root 部件 block-size、inline-size 覆盖槽。 | ### 动效 动效角色:出现(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-fade-in` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 --- 来源:https://ui.docs.xihanfun.com/components/back-top # BackTop 回到顶部 滚动超过指定距离后显示返回入口。 ## 用法 滚动后显示回到顶部按钮 ```vue ``` ```html
概览

概览相关内容

安装

安装相关内容

主题

主题相关内容

发布

发布相关内容

``` ## 组件结构 加粗的是必需部件。 `data-scope="back-top"`:**`root`** · **`trigger`** ## 示例 ### 显示阈值 提前显示回到顶部按钮 ```vue ``` ```html
快速开始

快速开始相关内容

基础配置

基础配置相关内容

主题定制

主题定制相关内容

部署

部署相关内容

``` ### 滚动方式 平滑返回或立即返回 ```vue ``` ```html

平滑 · 概览

平滑 · 配置

平滑 · 接口

平滑 · 发布

立即 · 概览

立即 · 配置

立即 · 接口

立即 · 发布

``` ### 变体 选择与所在表面匹配的样式 ```vue ``` ```html
线框(默认)
实心
幽灵
``` ## 设计指引 ### 何时使用 - 长页面或独立滚动区域。 ### 何时不用 - 短页面不需要返回入口。 - 多个悬浮操作使用[浮动按钮](./float-button)。 ### 特性 - `visibilityHeight` 设置显示阈值。 - `behavior` 支持平滑或立即返回。 - 触发器走 Action Control floating 档:默认 48px 圆形、图标 24px,按下缩放并换底;默认(outline)使用磨砂浮动表面,也可通过 `variant` 切换为 solid / subtle / ghost。 - 减少动效、减少透明度与强制色模式会自动降级。 ### 组合 - 指定 `target` 后监听并滚动该容器;未指定时作用于页面。 ### 最佳实践 - 避开固定工具条和移动端手势区。 - 保持默认的按需显示,不在页面顶部常驻。 ### 反模式 - 在短页面或已有返回入口的位置重复使用。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhBackTopRoot` `XhBackTopTrigger` | | 组合式函数 | `useBackTop` | | 状态机 | `backTopMachine` | | 皮肤 | `@xihan-ui/styles/back-top.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `visibilityHeight` | `number` | | 滚动超过该像素数后按钮才显示,默认 200。 | | `behavior` | `BackTopBehavior` | | 滚回顶部的方式,默认 smooth。 | | `translations` | `Partial` | | | | `variant` | `ActionVariant` | | 形态:solid / subtle / outline / ghost,默认 outline(缺省中性,描边 + 磨砂面;solid 才品牌实心)。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定按钮使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,默认 md;触发器走 Action Control floating 档(40 / 48 / 56px)。 | | `onVisibilityChange` | `(details: BackTopVisibilityChangeDetails) => void` | | 显隐变化时回调。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `visibility-change` | `BackTopVisibilityChangeDetails` | 显隐变化;detail 为 `{ visible: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhBackTopRoot` | `default` | `BackTopRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhBackTopRoot` | `target` | `() => HTMLElement \| null` | | 滚动容器取值器,默认即整页滚动;挂载效应执行时求值。 | | `XhBackTopRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'visible' \| 'hidden' | | `trigger` | 'visible' \| 'hidden' | 以下名称仅用于内部状态机。 **状态**:`hidden` · `visible` **事件**:`SCROLL.RESOLVE` · `TRIGGER.CLICK` · `PRESS.START` · `PRESS.END` · `TRIGGER.RENDERED` **判据**:`shouldShow` · `shouldHide` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `visible` | `boolean` | 按钮当前是否显示。 | | `scrollToTop` | `() => void` | 程序化滚回顶部,与点击按钮走同一路径。 | | `getRootProps` | `() => T['element']` | | | `getTriggerProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/button/) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in trigger | 滚回顶部;按 behavior 决定是一步到位还是平滑滚过去 | | `Enter` / `Space` | held in trigger | 按住期间投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下 | | `Tab` / `Shift+Tab` | trigger 露面时 | 走到按钮上;收起时整个 root 带 hidden,按钮不在 Tab 序列里 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-label` | props.translations.trigger | ## 样式参考 ### 皮肤 `@xihan-ui/styles/back-top.css` 使用 `[data-scope="back-top"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-size` | props.size | | `root` | `data-state` | 'visible' \| 'hidden' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'visible' \| 'hidden' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'floating' | | `trigger` | `data-xh-action-size` | props.size | | `trigger` | `data-xh-action-variant` | props.variant | | `trigger` | `data-xh-ink-surface` | ''(条件成立时才出现) | | `trigger` | `data-xh-liquid` | '' | | `trigger` | `data-xh-material` | 'frosted' \| undefined | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-back-top-bg` | `root`
`trigger` | `--xh-ink-surface`
`background-color` | `default`
`disabled`
`focus-visible`
`variant=outline`
`xh-ink-surface` | `--xh-_action-variant-bg-disabled`
`--xh-_action-variant-bg-focus-visible`
`--xh-_action-variant-bg-rest`
`--xh-_material-bg`
`--xh-_material-bg-focus` | back-top 的 root、trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-back-top-bg-active` | `root`
`trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`variant=outline` | `--xh-_action-variant-bg-pressed`
`--xh-_material-bg-pressed` | back-top 的 root、trigger 部件 background-color 覆盖槽。 | | `--xh-back-top-bg-hover` | `root`
`trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])`
`variant=outline` | `--xh-_action-variant-bg-hover`
`--xh-_material-bg-hover` | back-top 的 root、trigger 部件 background-color 覆盖槽。 | | `--xh-back-top-border` | `root`
`trigger` | `border`
`border-color` | `default`
`disabled`
`focus-visible`
`variant=outline` | `--xh-_action-variant-border-disabled`
`--xh-_action-variant-border-focus-visible`
`--xh-_action-variant-border-rest`
`--xh-_material-border` | back-top 的 root、trigger 部件 border、border-color 覆盖槽。 | | `--xh-back-top-border-hover` | `root`
`trigger` | `border-color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`variant=outline` | `--xh-_action-variant-border-hover`
`--xh-_action-variant-border-pressed`
`--xh-_material-border` | back-top 的 root、trigger 部件 border-color 覆盖槽。 | | `--xh-back-top-fg` | `root`
`trigger` | `color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`variant=outline` | `--xh-_action-variant-fg-focus-visible`
`--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest`
`--xh-_material-fg` | back-top 的 root、trigger 部件 color 覆盖槽。 | | `--xh-back-top-icon-size` | `trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size` | back-top 的 trigger 部件 --xh-icon-size 覆盖槽。 | | `--xh-back-top-inset-block` | `root` | `inset-block-end` | `default` | `--xh-space-8` | back-top 的 root 部件 inset-block-end 覆盖槽。 | | `--xh-back-top-inset-inline` | `root` | `inset-inline-end` | `default` | `--xh-space-8` | back-top 的 root 部件 inset-inline-end 覆盖槽。 | | `--xh-back-top-layer` | `root` | `z-index` | `default` | `--xh-layer-sticky` | back-top 的 root 部件 z-index 覆盖槽。 | | `--xh-back-top-radius` | `trigger` | `border-radius` | `default` | `--xh-_action-profile-radius` | back-top 的 trigger 部件 border-radius 覆盖槽。 | | `--xh-back-top-shadow` | `root`
`trigger` | `box-shadow` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`variant=outline` | `--xh-_material-shadow`
`none` | back-top 的 root、trigger 部件 box-shadow 覆盖槽。 | | `--xh-back-top-size` | `trigger` | `block-size`
`inline-size` | `default`
`xh-action-profile=floating` | `--xh-_action-profile-visual-size` | back-top 的 trigger 部件 block-size、inline-size 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(锚定面板) · 出现(无锚定弹出)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-pop-in` · `xh-pop-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/badge # Badge 徽标 提示某个对象有需要注意的变化:未读数量、当前状态、是否为新。徽标表达“发生了什么”,不表达“这是什么”;后者由[标签](./tag)承担。 ## 用法 被标记的元素写进默认插槽,角标自行贴到它的角上;计数、上限截断与 0 值收起都由角标计算 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="badge"`:**`root`** · **`indicator`** ## 示例 ### 圆点与落点 dot 只表示有而不表示数量;placement 决定挂在哪个角,rtl 下 end 自动落到左边 ```vue ``` ```html
曦 寒
``` ### 语气与尺寸 tone 决定使用哪族颜色:角标实际以未读红点与在线/离线点为主;size 改变圆点直径、两位数时的最小宽度与字号 ```vue ``` ```html
``` ### 自定义角标内容 拆为 Root + Indicator 两件:角标内可自行排版,插槽可得到计算好的计数;不写内容才回落为数字,showZero 让 0 保留显示 ```vue ``` ```html
12 条 NEW 曦
``` ### 呼吸 pulse 让圆点呼吸,表达正在进行、给不出进度的状态;状态仍要写在文字里,减弱动效下圆点停在满亮 ```vue ``` ```html
曦
``` ## 设计指引 ### 何时使用 - 计数角标:未读消息、购物车件数、待办条数。 - 小圆点:只表示有新内容,不表示数量。 - 状态提示:在线 / 离线、进行中、新。 - 附着在按钮、头像、标签页、菜单项上,报告该对象的状态。 ### 何时不用 - 表达分类、技能、筛选条件等实体身份时,使用[标签](./tag),它可以被移除。 - 用户需要点击它进行筛选或删除时,徽标不接受交互,应使用标签。 - 表达进度时,使用[进度条](./progress)。 - 可开关的选项使用[切换按钮](./toggle)。 ### 特性 - 语气与尺寸两轴与其他组件同源;角标只有一种形态,没有形态轴。 - 默认使用 neutral;未读、错误等强提醒显式使用 danger。 - `placement` 决定挂在哪个角,四角可选,跟随文字方向。 - `count` 输出数字,超过 `max`(默认 99)时显示为“99+”。 - 计数为 0 时整个收起,需要显示 0 时开启 `showZero`。 - `dot` 收成一个圆点,只表示存在,不表示数量。 - 计数盒三档最小尺寸为 14 / 20 / 24px,字号为 12 / 13 / 14px,角标只探出宿主四分之一,保持与宿主的视觉连接;sm 是贴在图标按钮角上的小号。 - `label` 为读屏提供完整语句,避免只读出一个数字。 ### 组合 - 挂在[头像](./avatar)、[按钮](./button)、[标签页](./tabs)的标签上作为角标:被标记的对象直接写进默认插槽,定位与偏移由组件承担,不需要外层再提供定位上下文。 ### 最佳实践 - 角标必须给 `label`:读屏只读出一个数字时,用户无法判断它的含义。 - 状态不能只用颜色区分,文字必须说明。 - 数量变化的场景交给 `count` 计算,不自行拼接“99+”,避免上限口径不一致。 ### 反模式 - 将徽标用作分类标签:它不可交互、不可移除。 - 一屏内大量使用高饱和度徽标,会使提醒失去意义。 - 用徽标承载长句子。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhBadge` `XhBadgeIndicator` `XhBadgeRoot` | | 状态机 | 无,`connect` 直接由 props 算属性 | | 皮肤 | `@xihan-ui/styles/badge.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `count` | `number` | | 计数。提供后角标自行显示数字,超过 max 时显示为「max+」。 与 indicator 的默认插槽二选一:插槽有内容时以插槽为准。 | | `dot` | `boolean` | | 只显示圆点,不显示数字。提供后 count 只用于决定是否显示。 | | `label` | `string` | | 读屏朗读该角标的方式。 角标挂在按钮、头像上时只朗读数字无法表达含义,需要由宿主提供「3 条未读」这类完整语句。 | | `max` | `number` | | 计数上限,默认 99:超过时只显示 99+,避免角标变形。 | | `placement` | `BadgePlacement` | | 挂在哪个角,默认 top-end(右上角;rtl 下自动落到左上)。 | | `pulse` | `boolean` | | 圆点呼吸:表达正在进行、给不出进度的状态(直播、录制、通话中)。只在 dot 模式下生效, 数字角标不呼吸——明暗起伏会压低数字的对比度。减弱动效下停在满不透明度。 | | `showZero` | `boolean` | | 计数为 0 时是否仍然显示,默认不显示:没有未读时不应出现角标。 | | `size` | `Size` | | 尺寸:sm / md / lg。影响圆点直径、两位数时的最小宽度与字号。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定使用哪族颜色,默认 neutral。 角标实际使用中主要为 danger(未读红点)与 success / neutral(在线 / 离线点)。 | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhBadge` | `default` | — | | | `XhBadgeIndicator` | `default` | `{ text: string }` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhBadgeIndicator` | `children` | `SlotChildren` | | | ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `visible` | `boolean` | 当前是否应渲染:计数为 0 且未开启 showZero 时为假。 | | `text` | `string` | 计算后的显示文本:超过 max 时显示为「99+」;dot 模式与无 count 时为空串。 | | `getRootProps` | `() => T['element']` | 锚点:被标记的对象(按钮、头像、标签页)放置在其中。 | | `getIndicatorProps` | `() => T['element']` | 角标本身,绝对定位在 root 的某个角。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `indicator` | `aria-label` | props.label | | `indicator` | `role` | 'status' \| undefined | ## 样式参考 ### 皮肤 `@xihan-ui/styles/badge.css` 使用 `[data-scope="badge"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-placement` | props.placement | | `indicator` | `data-dot` | ''(条件成立时才出现) | | `indicator` | `data-placement` | props.placement | | `indicator` | `data-pulse` | ''(条件成立时才出现) | | `indicator` | `data-size` | props.size | | `indicator` | `data-tone` | props.tone | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-badge-bg` | `indicator` | `background` | `default` | `--xh-_tone` | badge 的 indicator 部件 background 覆盖槽。 | | `--xh-badge-dot-radius` | `indicator` | `border-radius` | `dot` | `--xh-shape-circle` | badge 的 indicator 部件 border-radius 覆盖槽。 | | `--xh-badge-dot-size` | `indicator` | `block-size`
`inline-size`
`min-inline-size` | `dot` | `--xh-_badge-dot` | badge 的 indicator 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-badge-fg` | `indicator` | `color` | `default` | `--xh-_tone-on` | badge 的 indicator 部件 color 覆盖槽。 | | `--xh-badge-font-size` | `indicator` | `font-size` | `default` | `--xh-_badge-font` | badge 的 indicator 部件 font-size 覆盖槽。 | | `--xh-badge-font-weight` | `indicator` | `font-weight` | `default` | `--xh-font-weight-medium` | badge 的 indicator 部件 font-weight 覆盖槽。 | | `--xh-badge-min-size` | `indicator` | `block-size`
`min-inline-size` | `default` | `--xh-_badge-min` | badge 的 indicator 部件 block-size、min-inline-size 覆盖槽。 | | `--xh-badge-px` | `indicator` | `padding-inline` | `default` | `--xh-_badge-px` | badge 的 indicator 部件 padding-inline 覆盖槽。 | | `--xh-badge-radius` | `indicator` | `border-radius` | `default` | `--xh-shape-pill` | badge 的 indicator 部件 border-radius 覆盖槽。 | | `--xh-badge-ring` | `indicator` | `border` | `default` | `--xh-bg-surface` | badge 的 indicator 部件 border 覆盖槽。 | ### 动效 动效角色:循环(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-breathe` · `xh-breathe-halo` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 `prefers-reduced-motion: reduce` 下本组件另有降级规则。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/bar-code # BarCode 条形码 `alpha` 将一段文本绘制为一维条形码,`format` 选择码制。 ## 用法 提供 value 即绘制码,默认 Code 128,接受任意 ASCII;人读文字印在条下 ```vue ``` ```html ``` ## 组件结构 加粗的是必需部件。 `data-scope="bar-code"`:**`root`** ## 示例 ### 码制 零售商品用 EAN / UPC,外箱用 ITF-14,工业标签用 Code 39;定长数字码制的校验位可省略,组件补齐 ```vue ``` ```html
EAN-13
EAN-8
UPC-A
UPC-E
ITF-14
Code 39
``` ### GS1-128 gs1 开启后起始符后放置 FNC1;定长 AI 直接连写,变长 AI 后面用 GS(U+001D)与下一个隔开 ```vue ``` ```html
(01)09501101530003 (17)250630 (10)ABC123 (21)SN001
``` ### 尺寸与静区 barWidth 是最窄条的像素宽,整张码等比放大;height 只改条高;margin 是两侧静区的模块数 ```vue ``` ```html
barWidth 1 · height 40
缺省:barWidth 2 · height 64 · margin 10
barWidth 3 · height 40 · margin 2
``` ### 人读文字 text 关闭后只剩条;EAN 的守卫条按规范比数据条长 5X,不随文字变化 ```vue ``` ```html
缺省印文字
text=false
``` ### 换色 颜色不是 props,写两个 CSS 变量即可:条必须比底色深且对比充足,反相码无法扫描 ```vue ``` ```html
缺省
深蓝条
暖底深棕
``` ## 设计指引 ### 何时使用 - 货号、运单号、序列号需要被扫描枪一次读出。 - 商品零售码(EAN / UPC)、外箱码(ITF-14)、GS1 物流标签(GS1-128)。 ### 何时不用 - 内容超过几十个字符或含非 ASCII 字符时,一维码会过长,改用[二维码](./matrix-code)。 - 用户就在当前设备上时,提供可点击的链接或可复制的文本。 ### 特性 - `format` 支持七种码制:`code128`(默认)、`ean13` / `ean8` / `upca` / `upce`、`itf14`、`code39`;未知值不绘制,根进入 error 状态。 - 定长数字码制接受不带校验位的长度(自动补齐)与带校验位的长度(自动核对),不匹配时不绘制。 - `gs1` 把 Code 128 变为 GS1-128:起始符后放 FNC1,内容中的 GS(U+001D)编码为变长 AI 之间的分隔符。 - `text` 控制条下的人读文字;EAN / UPC 的数字逐位落在对应条的下方,守卫条按规范延长。 - `barWidth` 是最窄条的像素宽,整张码等比放大;`height` 是条高;`margin` 是静区,默认取码制的规范值。 - `itf14` 默认带上下承载条;`code39` 可选 mod 43 校验字符。 ### 组合 - 外层放[卡片](./card);旁边配[剪贴板](./clipboard)提供同一内容的文本形式。 ### 最佳实践 - 保留静区,贴边的条码无法扫描;默认值即规范值,压缩时不低于码制要求。 - 屏幕上 `barWidth` 至少为 2:1 像素宽的条在缩放后的屏幕上会模糊。 - 同时给出文本,不是所有人都能扫描。 - 内容含小写或标点时使用 `code128`;`code39` 只支持大写字母、数字与七个符号。 ### 反模式 - 深色主题下直接反色:读码器按深条浅底取样,反相码无法扫描。 - 用 `height` 把条压得过矮,扫描线稍有倾斜就会超出条的范围。 - 自行计算错误的校验位再传入:组件会拒绝绘制,应传不带校验位的长度由组件补齐。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhBarCode` | | 状态机 | 无,`connect` 直接由 props 算属性 | | 皮肤 | `@xihan-ui/styles/bar-code.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `barWidth` | `number` | | 最窄条的像素宽度(X 尺寸),默认 2;整张码的宽度由它乘以模块数得出。 | | `bearerBars` | `boolean` | | 上下承载条:itf14 印在瓦楞纸上防止短读的两根横条,默认绘制; 只对 itf14 有意义,其他码制提供时向诊断通道报告一条警告,按未提供处理。 | | `checksum` | `boolean` | | 附加 mod 43 校验字符。只对 code39 有意义:其余码制的校验位是规范必带的, 提供时向诊断通道报告一条警告,按未提供处理。 | | `format` | `BarCodeFormat` | | 码制,默认 code128。提供未知值时不绘制,根落到 `error` 态。 | | `gs1` | `boolean` | | GS1-128:起始符后放置 FNC1,内容中的 GS(U+001D)编码为变长 AI 之间的分隔。 只对 code128 有意义,其他码制提供时向诊断通道报告一条警告,按未提供处理。 | | `height` | `number` | | 条的像素高度,默认 64;不含守卫条的延长段、人读文字与承载条。 | | `label` | `string` | | 可及名,默认使用 value;提供全空白的名字等同于未提供。 | | `margin` | `number` | | 两侧静区,单位为模块数;默认按码制的规范值(code128 / itf14 / code39 10,ean13 11,upca / upce 9,ean8 7)。 | | `text` | `boolean` | | 条下方是否打印人读文字,默认打印。 | | `value` | `string` | | 要编码的内容;空串不绘制。定长数字码制接受不带或带校验位的两种长度,带校验位时校验。 | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'empty' | ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `format` | `BarCodeFormat` | 解析后的码制。提供未知值时原样透出,使错误信息与 data-format 都指向该值。 | | `runs` | `readonly number[]` | 条空交替的宽度(模块),首元素为条;未绘制时为空数组。 | | `modules` | `number` | 不含静区的模块数;未绘制时为 0。 | | `encoded` | `string` | 实际编入码中的内容,含补齐的校验位;未绘制时为空串。 | | `margin` | `number` | 解析后的静区宽度,单位为模块数。 | | `pixelWidth` | `number` | 根的像素宽高,也是 viewBox 的尺寸。 | | `pixelHeight` | `number` | | | `viewBox` | `string` | 根的 viewBox。 | | `path` | `string` | 全部条(含守卫条的延长段与承载条)合成的 `<path>` 的 d;未绘制时为空串,此时不应生成 path 节点。 | | `text` | `readonly BarCodeTextRun[]` | 人读文字,每段一个 `<text>`;关闭 `text` 或未绘制时为空数组。 | | `fontSize` | `number` | 人读文字的字号,像素。 | | `state` | `BarCodeState` | 当前状态。 | | `error` | `string \| undefined` | 编码失败的原因;其余状态为 undefined。 | | `label` | `string \| undefined` | 解析后的可及名;未提供名字时为 undefined,此时根退出无障碍树。 | | `getRootProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-hidden` | 'true' \| undefined | | `root` | `aria-label` | undefined \| props.label | | `root` | `role` | undefined \| 'img' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/bar-code.css` 使用 `[data-scope="bar-code"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-format` | props.format | | `root` | `data-modules` | undefined \| String(modules) | | `root` | `data-state` | 'empty' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-bar-code-bg` | `root` | `background` | `default` | `--xh-color-neutral-0` | bar-code 的 root 部件 background 覆盖槽。 | | `--xh-bar-code-fg` | `root` | `color` | `default` | `--xh-color-neutral-950` | bar-code 的 root 部件 color 覆盖槽。 | | `--xh-bar-code-font-family` | `root` | `font-family` | `xh-geom=text` | `--xh-font-family-mono` | bar-code 的 root 部件 font-family 覆盖槽。 | | `--xh-bar-code-placeholder-bg` | `root` | `background` | `state=empty`
`state=error` | `--xh-bg-subtle` | bar-code 的 root 部件 background 覆盖槽。 | | `--xh-bar-code-placeholder-border` | `root` | `box-shadow` | `state=empty`
`state=error` | `--xh-border-default` | bar-code 的 root 部件 box-shadow 覆盖槽。 | | `--xh-bar-code-radius` | `root` | `border-radius` | `default` | `--xh-shape-control` | bar-code 的 root 部件 border-radius 覆盖槽。 | | `--xh-bar-code-text-fg` | `root` | `fill` | `xh-geom=text` | `currentColor` | bar-code 的 root 部件 fill 覆盖槽。 | ### 动效 动效角色:状态(见[动效规范](../design/motion#角色))。 `background-color` · `box-shadow` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 --- 来源:https://ui.docs.xihanfun.com/components/breadcrumb # Breadcrumb 面包屑 显示当前页面在信息层级中的位置。 ## 用法 显示当前页面的层级路径 ```vue ``` ```html ``` ## 组件结构 加粗的是必需部件。 `data-scope="breadcrumb"`:**`root`** · **`list`** · **`item`** · **`link`** · `link-icon` · `separator` · `ellipsis` ## 示例 ### 折叠层级 收起过长路径的中间部分 ```vue ``` ```html ``` ### 自定义分隔符 替换层级之间的视觉标记 ```vue ``` ```html ``` ### 尺寸 适配不同的信息密度 ```vue ``` ```html ``` ## 设计指引 ### 何时使用 - 页面具有明确的父子层级。 - 用户可能从搜索或外链直接进入深层页面。 ### 何时不用 - 扁平页面不需要面包屑。 - 流程进度使用[步骤条](./steps)。 ### 特性 - `collection` 可直接生成完整路径,也支持手写部件。 - `maxItems` 将过长路径的中间层折叠为省略号。 - 默认分隔符为箭头,可通过插槽或渲染函数替换。 - 当前页使用 `aria-current="page"`,不参与键盘导航。 ### 组合 - 通常放在页头或正文标题之前。 ### 最佳实践 - 当前项使用清晰的页面标题,避免“详情”等泛化名称。 - 同页有多个 `nav` 地标时给面包屑单独的 `aria-label`。 ### 反模式 - 不要用面包屑表示浏览历史。 - 当前项不要链接到自身。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhBreadcrumbEllipsis` `XhBreadcrumbItem` `XhBreadcrumbLink` `XhBreadcrumbLinkIcon` `XhBreadcrumbList` `XhBreadcrumbRoot` `XhBreadcrumbSeparator` | | 组合式函数 | `useBreadcrumb` | | 状态机 | `breadcrumbMachine` | | 皮肤 | `@xihan-ui/styles/breadcrumb.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `collection` | `readonly BreadcrumbNode[]` | | 层级数据,文字、链接与当前页的事实源。 未提供时回到层级逐个写成部件的方式。 | | `maxItems` | `number` | | 最多展开的层数,超出的中间层折叠为一个省略位;未提供时全部列出。 | | `dir` | `Direction` | | 文字方向,只作用于排版;作者未提供时不写入。 | | `translations` | `Partial` | | | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | ### BreadcrumbNode `collection` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | 层级身份,写入 data-value。 | | `label` | `string` | | 显示文字;默认回退为 value。 | | `href` | `string` | | 链接地址;未提供时渲染为不带 href 的 a。 | | `icon` | `string` | | 图标文本,写入 link-icon 部件;需要放置图形时改用插槽。 | | `current` | `boolean` | | 当前页所在层级。 | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhBreadcrumbLink` | `value` | `string` | | 链接身份,按压通道按它记住正被按住的那一条;未声明时派生一个实例内稳定的键。 | | `XhBreadcrumbLink` | `current` | `boolean` | | 当前页的条目。 | | `XhBreadcrumbRoot` | `renderSeparator` | `() => ReactNode` | | 分隔符的内容;未提供时由皮肤绘制默认箭头。 | | `XhBreadcrumbRoot` | `renderEllipsis` | `(nodes: readonly BreadcrumbNodeMeta[]) => ReactNode` | | 省略位的内容,可得到被折叠的层;未提供时为一个省略号。 | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`PRESS.START` · `PRESS.END` **判据**:`canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `collection` | `readonly BreadcrumbNodeMeta[]` | 由 collection 推导的层级元信息,按数据顺序排列;未提供 collection 时为空数组。 | | `items` | `readonly BreadcrumbItem[]` | 按 maxItems 折叠后的序列,省略位自带被折叠的层级;未提供 collection 时为空数组。 | | `getRootProps` | `() => T['element']` | | | `getListProps` | `() => T['element']` | | | `getItemProps` | `() => T['element']` | | | `getLinkProps` | `(props: BreadcrumbLinkProps) => T['element']` | | | `getLinkIconProps` | `() => T['element']` | | | `getSeparatorProps` | `() => T['element']` | | | `getEllipsisProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` | focus in link, 非当前页 | 跟随链接(原生 <a href> 的激活行为,面包屑自己不监听按键) | | `Enter` / `Space` | held in link, 非当前页 | 按住期间该链接投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下。跟随链接照旧由这一次按键(原生 <a href>)承担,当前页那条不进 | | `Tab` / `Shift+Tab` | focus in root | 逐条走过可点的链接;面包屑不做 roving tabindex,当前页那条带 tabindex=-1 自动脱序 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-label` | props.translations.root | | `link` | `aria-current` | 'page' \| undefined | | `link` | `aria-disabled` | 'true' \| 'false' | | `link-icon` | `aria-hidden` | 'true' | | `separator` | `aria-hidden` | 'true' | | `ellipsis` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/breadcrumb.css` 使用 `[data-scope="breadcrumb"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `link` | `data-current` | ''(条件成立时才出现) | | `link` | `data-pressed` | ''(条件成立时才出现) | | `link` | `data-xh-collection-context` | 'nav' | | `link` | `data-xh-collection-item` | '' | | `link` | `data-xh-collection-size` | props.size | | `link` | `data-xh-collection-terminal` | ''(条件成立时才出现) | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-breadcrumb-ellipsis-size` | `ellipsis` | `inline-size` | `default` | `--xh-space-5` | breadcrumb 的 ellipsis 部件 inline-size 覆盖槽。 | | `--xh-breadcrumb-fg` | `link`
`root` | `color` | `default`
`xh-collection-context=nav` | `--xh-fg-muted` | breadcrumb 的 link、root 部件 color 覆盖槽。 | | `--xh-breadcrumb-font-size` | `link`
`root` | `font-size` | `default` | `--xh-_breadcrumb-font-size` | breadcrumb 的 link、root 部件 font-size 覆盖槽。 | | `--xh-breadcrumb-gap` | `list` | `gap` | `default` | `--xh-_breadcrumb-gap` | breadcrumb 的 list 部件 gap 覆盖槽。 | | `--xh-breadcrumb-icon-size` | `link`
`root` | `--xh-icon-size` | `default` | `--xh-glyph-size-text` | breadcrumb 的 link、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-breadcrumb-leading` | `link`
`root` | `line-height` | `default` | `--xh-leading-tight` | breadcrumb 的 link、root 部件 line-height 覆盖槽。 | | `--xh-breadcrumb-link-bg-hover` | `link` | `background-color` | `disabled`
`error`
`hover`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`xh-collection-context=nav` | `--xh-bg-subtle` | breadcrumb 的 link 部件 background-color 覆盖槽。 | | `--xh-breadcrumb-link-bg-pressed` | `link` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`xh-collection-context=nav` | `--xh-bg-subtle-hover` | breadcrumb 的 link 部件 background-color 覆盖槽。 | | `--xh-breadcrumb-link-fg-current` | `link` | `color` | `current`
`xh-collection-context=nav`
`xh-collection-terminal` | `--xh-_breadcrumb-accent-text` | breadcrumb 的 link 部件 color 覆盖槽。 | | `--xh-breadcrumb-link-fg-hover` | `link` | `color` | `disabled`
`error`
`hover`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`xh-collection-context=nav` | `--xh-_breadcrumb-accent-text` | breadcrumb 的 link 部件 color 覆盖槽。 | | `--xh-breadcrumb-link-font-weight-current` | `link` | `font-weight` | `current`
`xh-collection-context=nav`
`xh-collection-terminal` | `--xh-font-weight-medium` | breadcrumb 的 link 部件 font-weight 覆盖槽。 | | `--xh-breadcrumb-link-gap` | `link` | `gap` | `default` | `--xh-space-1` | breadcrumb 的 link 部件 gap 覆盖槽。 | | `--xh-breadcrumb-link-icon-size` | `link-icon` | `block-size`
`inline-size` | `default` | `--xh-glyph-size-text` | breadcrumb 的 link-icon 部件 block-size、inline-size 覆盖槽。 | | `--xh-breadcrumb-link-max-w` | `link` | `max-inline-size` | `default` | `--xh-nav-link-max-w` | breadcrumb 的 link 部件 max-inline-size 覆盖槽。 | | `--xh-breadcrumb-link-px` | `link` | `padding-inline` | `default` | `--xh-space-1` | breadcrumb 的 link 部件 padding-inline 覆盖槽。 | | `--xh-breadcrumb-link-radius` | `link` | `border-radius` | `default` | `--xh-shape-control` | breadcrumb 的 link 部件 border-radius 覆盖槽。 | | `--xh-breadcrumb-separator-fg` | `ellipsis`
`separator` | `color` | `default` | `--xh-fg-subtle` | breadcrumb 的 ellipsis、separator 部件 color 覆盖槽。 | | `--xh-breadcrumb-separator-size` | `separator` | `inline-size` | `default` | `--xh-glyph-size-text` | breadcrumb 的 separator 部件 inline-size 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### RTL 另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/button-group # ButtonGroup 按钮组 将一组相关操作组合为连续的按钮控件。 ## 用法 组合相关操作 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="button-group"`:**`root`** ## 示例 ### 变体 设置整组外观 ```vue ``` ```html
主要
次要
第三
线框
幽灵
危险
``` ### 尺寸 设置整组尺寸 ```vue ``` ```html
``` ### 方向 水平或垂直排列 ```vue ``` ```html
``` ### 图标与标签 组合图标按钮与文字按钮 ```vue ``` ```html
``` ### 宽度充满 按钮等分可用宽度 ```vue ``` ```html
``` ### 禁用 禁用整组按钮 ```vue ``` ```html
``` ### 无分隔线 省略分隔线部件 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 并列展示作用相近的操作。 - 统一一组按钮的尺寸、变体和颜色。 ### 何时不用 - 需要表达单选或多选状态时,使用[切换按钮组](./toggle-group)。 - 操作之间没有直接关系时,分别放置按钮并保留间距。 ### 特性 - 支持水平和垂直排列。 - 自动合并相邻边界,只保留首尾圆角。 - 支持统一设置尺寸、变体、颜色、禁用状态和宽度:组的变体、颜色与尺寸下发到组内每一段,段自己写了的优先。 - 组的缺省变体是中性淡底 `subtle`,不是单独一枚按钮的品牌实心。 - 默认在相邻按钮之间显示分隔线,可通过 `separators=false` 关闭。 - 按下按钮时不缩放,避免组内边界断开。 ### 组合 - 在组内直接放置[按钮](./button)。 - 将菜单触发器放在末尾,可组成分裂按钮。 ### 最佳实践 - 每组只放置同一任务下的操作。 - 操作较多时,保留常用项,其余收纳到菜单中。 - 窄容器中使用垂直方向,不要让按钮组换行。 - 尺寸和变体优先设置在按钮组上。 ### 反模式 - 不要用按钮组表示已选中项。 - 不要在按钮之间插入说明文字。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhButtonGroup` | | 状态机 | 无,`connect` 直接由 props 算属性 | | 皮肤 | `@xihan-ui/styles/button-group.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `disabled` | `boolean` | | 整组禁用:适配器把它落到组内每一段的原生 disabled 上,段自身声明禁用的仍然禁用。 | | `fullWidth` | `boolean` | | 撑满行宽:整组占满可用宽度,每段等分剩余空间。 | | `orientation` | `'horizontal' \| 'vertical'` | | 排布:horizontal / vertical,决定相邻两段在哪个轴上合并边缘。 | | `separators` | `boolean` | | 是否自动在相邻按钮之间插入分隔线,默认 true。 | | `size` | `Size` | | 尺寸:sm / md / lg,写入根上并下发给组内每一段;段自己写了的优先。 | | `tone` | `Tone` | | 颜色:brand / neutral / success / warning / danger / info,写入根上并下发给组内每一段;段自己写了的优先。 | | `variant` | `ActionVariant` | | 变体:solid / subtle / outline / ghost,默认 subtle(组缺省中性淡底)。 写入根上,并由适配器下发给组内每一段;段自己写了 variant 的优先。 | ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `orientation` | `'horizontal' \| 'vertical'` | | | `disabled` | `boolean` | 整组是否禁用。适配器据此把禁用传给组内每一段:只写 data-* 是假禁用。 | | `separators` | `boolean` | 适配器是否自动生成相邻按钮间的分隔线。 | | `variant` | `ActionVariant` | 组的变体(缺省 subtle)。适配器把它下发给未自写 variant 的每一段,段因此自带形态矩阵属性。 | | `tone` | `Tone \| undefined` | 组的颜色;未写时为 undefined,段沿用自己的。适配器下发给未自写 tone 的每一段。 | | `size` | `Size \| undefined` | 组的尺寸;未写时为 undefined,段沿用自己的。适配器下发给未自写 size 的每一段。 | | `getRootProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `role` | 'group' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/button-group.css` 使用 `[data-scope="button-group"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-full-width` | ''(条件成立时才出现) | | `root` | `data-orientation` | props.orientation | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-button-group-outline-color` | `root` | `border` | `variant=outline` | `--xh-_tone-border-control` | button-group 的 root 部件 border 覆盖槽。 | | `--xh-button-group-radius` | `root` | `border-end-end-radius`
`border-end-start-radius`
`border-radius`
`border-start-end-radius`
`border-start-start-radius` | `first-child`
`last-child`
`orientation=horizontal`
`orientation=vertical`
`variant=outline` | `--xh-shape-control` | button-group 的 root 部件 border-end-end-radius、border-end-start-radius、border-radius、border-start-end-radius、border-start-start-radius 覆盖槽。 | | `--xh-button-group-separator-color` | `root` | `background` | `xh-button-group-separator` | `--xh-fg-default` | button-group 的 root 部件 background 覆盖槽。 | | `--xh-button-group-separator-color-disabled` | `root` | `background` | `disabled`
`xh-button-group-separator` | `--xh-border-subtle` | button-group 的 root 部件 background 覆盖槽。 | | `--xh-button-group-separator-opacity` | `root` | `opacity` | `xh-button-group-separator` | `--xh-control-separator-opacity` | button-group 的 root 部件 opacity 覆盖槽。 | | `--xh-button-group-separator-opacity-disabled` | `root` | `opacity` | `disabled`
`xh-button-group-separator` | `--xh-control-separator-disabled-opacity` | button-group 的 root 部件 opacity 覆盖槽。 | | `--xh-button-group-separator-radius` | `root` | `border-radius` | `xh-button-group-separator` | `--xh-shape-pill` | button-group 的 root 部件 border-radius 覆盖槽。 | | `--xh-button-group-separator-size` | `root` | `block-size`
`inline-size` | `orientation=horizontal`
`orientation=vertical`
`xh-button-group-separator` | `--xh-_group-separator-size` | button-group 的 root 部件 block-size、inline-size 覆盖槽。 | | `--xh-button-group-separator-thickness` | `root` | `block-size`
`inline-size`
`margin-block-start`
`margin-inline-start` | `orientation=horizontal`
`orientation=vertical`
`xh-button-group-separator` | `--xh-stroke-thin` | button-group 的 root 部件 block-size、inline-size、margin-block-start、margin-inline-start 覆盖槽。 | ### 动效 动效角色:状态(见[动效规范](../design/motion#角色))。 `background-color` · `opacity` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/button # Button 按钮 用于触发即时操作。 ## 用法 触发一次操作 ```vue ``` ```html ``` ## 组件结构 加粗的是必需部件。 `data-scope="button"`:**`root`** · `label` · `indicator` · `prefix` · `suffix` ## 示例 ### 变体 设置按钮外观 ```vue ``` ```html ``` ### 尺寸 小、中、大三档 ```vue ``` ```html ``` ### 图标 在文字前后放置图标 ```vue ``` ```html ``` ### 仅图标 紧凑的图标操作 ```vue ``` ```html ``` ### 加载 保留按钮标签并阻止重复操作 ```vue ``` ```html ``` ### 异步操作 点击后显示加载状态 ```vue ``` ```html ``` ### 全宽 占满容器宽度 ```vue ``` ```html ``` ### 禁用 暂时不可执行的操作 ```vue ``` ```html ``` ### 链接 保留原生导航能力 ```vue ``` ```html 了解更多 ``` ## 设计指引 ### 何时使用 - 提交表单或执行命令。 - 打开菜单、对话框等浮层。 - 需要明确主次关系的一组操作。 ### 何时不用 - 导航到其他地址时,将按钮渲染为链接。 - 表达持续的开关状态时,使用[切换按钮](./toggle)。 - 在多个选项中选择时,使用[切换按钮组](./toggle-group)或[单选组](./radio-group)。 ### 特性 - 支持四种变体、六种颜色和三种尺寸。 - 缺省变体是品牌实心 `solid`,这是按钮独有的缺省;其余触发器缺省中性。 - 支持文字、图标、图标加文字与全宽按钮。 - `loading` 保留焦点并阻止重复操作。 - `as="a"` 保留原生链接能力。 - 应用设为 `data-material="liquid"` 时,实心按钮在细指针悬停的一刻有一道光沿描边扫过一次;光只走描边、不进面,文字对比不受影响。粗指针、减弱动效与强制色下不播。 ### 组合 - 使用 `prefix` 与 `suffix` 放置图标。 - 使用 `indicator` 提供加载图形。 - 使用[按钮组](./button-group)组合相关操作。 ### 最佳实践 - 每个视图只保留一个主要操作。 - 图标按钮必须提供 `aria-label`。 - 加载时保留原有标签,避免按钮宽度变化。 ### 反模式 - 不要使用按钮模拟普通链接。 - 不要在按钮中嵌套可聚焦元素。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhButton` `XhButtonIndicator` `XhButtonLabel` `XhButtonPrefix` `XhButtonSuffix` | | 状态机 | `buttonMachine` | | 皮肤 | `@xihan-ui/styles/button.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | `'button' \| 'submit' \| 'reset'` | | | | `disabled` | `boolean` | | | | `loading` | `boolean` | | 加载态:用 aria-disabled + 拦截事件表达,保留焦点。 | | `variant` | `ActionVariant` | | 变体:solid / subtle / outline / ghost,默认 solid——只有 Button 缺省品牌实心,其余触发器缺省中性。 | | `tone` | `Tone` | | 颜色:brand / neutral / success / warning / danger / info。 | | `size` | `Size` | | | | `iconOnly` | `boolean` | | 仅图标:左右内边距清零、宽高相等。宽度跟随当前尺寸档的高度, 不必把档位写进行内样式。图标按钮没有可见文字,作者须自行提供可及名。 | | `ariaLabel` | `string` | | 作者写在根节点上的可及名(aria-label / aria-labelledby)。 宿主只把它们转告连接层,用于判断图标按钮是否有名字;属性本身仍由宿主写入根节点。 | | `ariaLabelledby` | `string` | | | | `fullWidth` | `boolean` | | 撑满行宽:表单末尾的提交按钮与移动端常用。 | | `as` | `ButtonElement` | | 渲染的标签,默认 button。 写为 a 时不再产出 type 与原生 disabled(两者在链接上无效),禁用改由 aria-disabled 表达, 点击仍被拦截。href 由作者自行提供。 | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhButton` | `href` | `ComponentPropsWithRef<'a'>['href']` | | | | `XhButton` | `target` | `ComponentPropsWithRef<'a'>['target']` | | | | `XhButton` | `rel` | `ComponentPropsWithRef<'a'>['rel']` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`PRESS.START` · `PRESS.END` **判据**:`canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `disabled` | `boolean` | | | `loading` | `boolean` | | | `getRootProps` | `() => T['button']` | | | `getLabelProps` | `() => T['element']` | | | `getIndicatorProps` | `() => T['element']` | | | `getPrefixProps` | `() => T['element']` | | | `getSuffixProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/button/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in root, interactive | 激活按钮(原生行为) | | `Enter` / `Space` | held in root, interactive | 按住期间投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-busy` | 'true' \| undefined | | `root` | `aria-disabled` | 'true' \| undefined | | `indicator` | `aria-hidden` | 'true' | | `prefix` | `aria-hidden` | 'true' | | `suffix` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/button.css` 使用 `[data-scope="button"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-full-width` | ''(条件成立时才出现) | | `root` | `data-icon-only` | ''(条件成立时才出现) | | `root` | `data-loading` | ''(条件成立时才出现) | | `root` | `data-pressed` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `root` | `data-xh-action-control` | '' | | `root` | `data-xh-action-display` | 'always' | | `root` | `data-xh-action-profile` | 'icon' \| 'text' | | `root` | `data-xh-action-size` | props.size | | `root` | `data-xh-action-variant` | props.variant | | `root` | `data-xh-ink-surface` | ''(条件成立时才出现) | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-button-bg` | `root` | `--xh-ink-surface`
`background-color` | `default`
`focus-visible`
`loading`
`xh-ink-surface` | `--xh-_action-variant-bg-focus-visible`
`--xh-_action-variant-bg-loading`
`--xh-_action-variant-bg-rest` | button 的 root 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-button-bg-active` | `root` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | button 的 root 部件 background-color 覆盖槽。 | | `--xh-button-bg-hover` | `root` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | button 的 root 部件 background-color 覆盖槽。 | | `--xh-button-fg` | `root` | `color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-focus-visible`
`--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-loading`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | button 的 root 部件 color 覆盖槽。 | | `--xh-button-font-size` | `root` | `font-size` | `default` | `--xh-_button-group-font-size` | button 的 root 部件 font-size 覆盖槽。 | | `--xh-button-font-weight` | `root` | `font-weight` | `default` | `--xh-text-label-weight` | button 的 root 部件 font-weight 覆盖槽。 | | `--xh-button-gap` | `root` | `gap` | `default` | `--xh-_button-group-gap` | button 的 root 部件 gap 覆盖槽。 | | `--xh-button-glint-duration` | `root` | `animation` | `@media (hover: hover) and (pointer: fine) and (forced-colors: none)`
`disabled`
`hover`
`loading`
`material=liquid`
`not([data-disabled])`
`not([data-loading])`
`where([data-material='liquid'])`
`xh-action-variant=solid` | `--xh-motion-duration-glint` | button 的 root 部件 animation 覆盖槽。 | | `--xh-button-h` | `root` | `block-size`
`inline-size` | `default`
`xh-action-profile=icon` | `--xh-_button-group-h` | button 的 root 部件 block-size、inline-size 覆盖槽。 | | `--xh-button-icon-size` | `root` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size` | button 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-button-px` | `root` | `padding-inline` | `default` | `--xh-_button-group-px` | button 的 root 部件 padding-inline 覆盖槽。 | | `--xh-button-radius` | `root` | `border-radius` | `default` | `--xh-_button-radius` | button 的 root 部件 border-radius 覆盖槽。 | | `--xh-button-shadow` | `root` | `box-shadow` | `default` | `none` | button 的 root 部件 box-shadow 覆盖槽。 | | `--xh-button-shadow-hover` | `root` | `box-shadow` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `none` | button 的 root 部件 box-shadow 覆盖槽。 | | `--xh-button-spin-duration` | `indicator`
`root` | `animation` | `loading` | `--xh-motion-loop-spin` | button 的 indicator、root 部件 animation 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 循环(见[动效规范](../design/motion#角色))。 可覆盖的动效槽:`--xh-button-glint-duration` · `--xh-button-spin-duration`。 共享关键帧 `xh-spin` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 `prefers-reduced-motion: reduce` 下本组件另有降级规则。 ### 响应式 皮肤另按输入能力分档:`hover: hover`:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/calendar-picker # CalendarPicker 日历选择器 以天、周、月、季度或年为周期浏览并选择一个或多个日期,也可以在日期格中展示日程内容。 ## 用法 选择日期 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="calendar-picker"`:`root` · `header` · `prev-year-trigger` · `prev-trigger` · `next-trigger` · `next-year-trigger` · `heading` · `heading-year-trigger` · `heading-month-trigger` · **`grid`** · `grid-head` · `week-day` · `grid-body` · `week-row` · `week-number` · **`cell`** · **`cell-trigger`** ## 示例 ### 多选 selection-mode=multiple:点击一次加入,再点击一次移除,集合按日期升序 ```vue ``` ```html
已选:2026-09-08、2026-09-15、2026-09-22 ``` ### 不可选的日期 isDateUnavailable 与 min / max 都只阻止落值不阻止聚焦:方向键照常可以经过 ```vue ``` ```html
``` ### 格子内放置内容 cell-trigger 的内容全部由作者编写,日号之外还可放置自己的标记 ```vue ``` ```html
选中:(未选) ``` ## 设计指引 ### 何时使用 - 需要先看到整段时间的分布再选择日期:日程、排班、可预约情况。 - 需要在格子中显示当天的事件。 - 需要一次选择多个不连续的日期。 ### 何时不用 - 只录入一个日期时,使用[日期选择器](./date-picker)或[日期字段](./date-field)。 - 选择一段连续的起止时,使用[日历范围选择器](./calendar-range-picker)。 ### 特性 - 标准结构由标题栏、前后翻页按钮、星期表头和日期网格组成;网格数据通过插槽作用域交给作者渲染。 - `granularity` 决定周期格的生成方式,`selectionMode` 独立决定单选或多选;两个维度互不绑定。 - 五种粒度统一产出 `CalendarPeriod`:稳定键、周期首尾、标签与相邻容器标记都来自同一份数据。 - `week` 是一级粒度,使用一行一个整周的网格;不通过日格高亮模拟整周选择。 - `isDateUnavailable` 与 `min` / `max` 只阻止取值,不阻止聚焦;粗粒度周期越过任一边界时整格不可选。 - 支持固定六行与显式多面板;翻页时整个视窗一起移动。 - 日期、月份与年份格按下时轻微缩放,松开后复原;减弱动效下自动收敛。 - 年份网格采用三列紧凑滚动面,可由作者按业务上下界铺入连续年份,复用日历格的选中与键盘语义。 - `calendarPeriodValue` 将选中的周期转换为 `{ granularity, start, end, keys }`,可直接用于查询参数。 - 切换粒度会清空旧选择并保留浏览锚点,避免不同周期键之间发生隐式转换。 - 周首日、月份名与星期名跟随 `locale`:`en-US` 周日起、`zh-CN` 周一起。未提供 `locale` 时跟随宿主浏览器语言,读取失败时使用 `en-US`;需要固定排法时显式传入 `locale`。 ### 组合 - 格子中放[徽标](./badge)或一小段[排印](./typography);外层放[卡片](./card)。 - 与[日历范围选择器](./calendar-range-picker)共用同一套部件名与皮肤槽,两者可以并排出现且外观一致。 ### 最佳实践 - 今天使用 1px 品牌环 + 品牌字,选中使用实心强调面,两种状态必须能同时辨认。 - 多选时使用 `aria-multiselectable` 告知读屏用户可以多选,不依赖视觉提示。 - 格子中的内容超出时收起,避免某一行明显高于其他行。 ### 反模式 - 不可选的日期无法聚焦,键盘用户无从知道该位置的内容。 - 将它用作日期输入框。 - 用多选模拟区间:中间的日期不会自动补齐,也没有拖选与预览。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCalendarPickerCell` `XhCalendarPickerCellTrigger` `XhCalendarPickerGrid` `XhCalendarPickerGridBody` `XhCalendarPickerGridHead` `XhCalendarPickerHeader` `XhCalendarPickerHeading` `XhCalendarPickerHeadingMonthTrigger` `XhCalendarPickerHeadingYearTrigger` `XhCalendarPickerNextTrigger` `XhCalendarPickerNextYearTrigger` `XhCalendarPickerPrevTrigger` `XhCalendarPickerPrevYearTrigger` `XhCalendarPickerRoot` `XhCalendarPickerWeekDay` `XhCalendarPickerWeekNumber` `XhCalendarPickerWeekRow` | | 组合式函数 | `useCalendarPicker` | | 状态机 | `calendarPickerMachine` | | 皮肤 | `@xihan-ui/styles/calendar-picker.css` | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `CalendarPickerValueChangeDetails` | 选中集合变化;detail 为 `{ value: string[] }` | | `focused-value-change` | `CalendarFocusChangeDetails` | 聚焦日变化;detail 为 `{ focusedValue: string }` | | `active-view-change` | `CalendarViewChangeDetails` | 切换到另一层级;detail 为 `{ activeView: 'day'\|'week'\|'month'\|'quarter'\|'year' }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCalendarPickerRoot` | `default` | `CalendarPickerRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCalendarPickerCell` | `value` | `string` | 是 | ISO 日期串。 | | `XhCalendarPickerCell` | `index` | `number` | | 属于第几个面板,默认 0。多面板时必须提供:同一天会同时出现在两个面板中 (8 月末的几天也铺在 9 月的首行),是否为本月只有连同面板一起看才能判定。 | | `XhCalendarPickerGrid` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarPickerHeading` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarPickerHeadingMonthTrigger` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarPickerHeadingYearTrigger` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarPickerRoot` | `value` | `string \| string[]` | | | | `XhCalendarPickerRoot` | `defaultValue` | `string \| string[]` | | | | `XhCalendarPickerRoot` | `selectionMode` | `CalendarPickerSelectionMode` | | | | `XhCalendarPickerRoot` | `focusedValue` | `string` | | | | `XhCalendarPickerRoot` | `defaultFocusedValue` | `string` | | | | `XhCalendarPickerRoot` | `min` | `string` | | | | `XhCalendarPickerRoot` | `max` | `string` | | | | `XhCalendarPickerRoot` | `isDateUnavailable` | `(value: string) => boolean` | | | | `XhCalendarPickerRoot` | `invalid` | `boolean` | | 校验失败:根带 data-invalid。 | | `XhCalendarPickerRoot` | `locale` | `string` | | | | `XhCalendarPickerRoot` | `timeZone` | `string` | | | | `XhCalendarPickerRoot` | `disabled` | `boolean` | | | | `XhCalendarPickerRoot` | `readOnly` | `boolean` | | | | `XhCalendarPickerRoot` | `weekdayFormat` | `CalendarWeekdayFormat` | | | | `XhCalendarPickerRoot` | `fixedWeeks` | `boolean` | | | | `XhCalendarPickerRoot` | `granularity` | `CalendarGranularity` | | 选择粒度;与 selectionMode 正交。区间选择是另一个组件(XhCalendarRangePicker)。 | | `XhCalendarPickerRoot` | `activeView` | `CalendarView` | | 面板当前所处的层级;给定即受控,默认跟随 granularity。 | | `XhCalendarPickerRoot` | `defaultActiveView` | `CalendarView` | | 非受控初值,默认同 granularity。 | | `XhCalendarPickerRoot` | `visibleCount` | `number` | | 并排展示几页,默认 1。 | | `XhCalendarPickerRoot` | `translations` | `Partial` | | | | `XhCalendarPickerRoot` | `onValueChange` | `CalendarPickerProps['onValueChange']` | | | | `XhCalendarPickerRoot` | `onFocusedValueChange` | `CalendarPickerProps['onFocusedValueChange']` | | | | `XhCalendarPickerRoot` | `onActiveViewChange` | `CalendarPickerProps['onActiveViewChange']` | | | | `XhCalendarPickerRoot` | `children` | `SlotChildren` | | | | `XhCalendarPickerWeekDay` | `value` | `number \| string` | 是 | 列序 0-6,兼收字符串。 | | `XhCalendarPickerWeekNumber` | `value` | `string` | 是 | 该行行首那一天的 ISO 串。 | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`PRESS.START` · `PRESS.END` **判据**:`canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `selectionMode` | `CalendarPickerSelectionMode` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/#kbd_label) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Tab` / `Shift+Tab` | focus outside the grid | 整张网格只占一个 Tab 位:焦点进入聚焦日那一格 | | `ArrowLeft` | focus in grid | 焦点前移一天;越过月首即翻到上一月并落在那一天。粗粒度视图里走一格(一个月 / 一季 / 一年) | | `ArrowRight` | focus in grid | 焦点后移一天;越过月末即翻到下一月并落在那一天。粗粒度视图里走一格 | | `ArrowUp` | focus in grid | 焦点上移一周(减七天),跨月照样翻页。粗粒度视图里上移一行 | | `ArrowDown` | focus in grid | 焦点下移一周(加七天),跨月照样翻页。粗粒度视图里下移一行 | | `Home` | focus in grid | 焦点移到本周第一天;周首日随 locale 变。粗粒度视图里移到本行头一格 | | `End` | focus in grid | 焦点移到本周最后一天。粗粒度视图里移到本行末一格 | | `PageUp` | focus in grid | 退一个月,日号不变(月末日被目标月夹住:3 月 31 日退成 2 月 29 日)。粗粒度视图里退一整页 | | `PageDown` | focus in grid | 进一个月,日号不变。粗粒度视图里进一整页 | | `Shift+PageUp` | focus in grid | 退一年;粗粒度视图里退十页 | | `Shift+PageDown` | focus in grid | 进一年;粗粒度视图里进十页 | | `Enter` / `Space` | focus in grid, 聚焦周期可用且非只读 | 选中聚焦周期:单选替换、多选切换。还没钻到 granularity 那一档时这一下是往下钻一层 | | `Enter` / `Space` | held in prev-year-trigger / prev-trigger / next-trigger / next-year-trigger / heading-year-trigger / heading-month-trigger / cell-trigger, 该部件可按 | 按住期间该部件投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,按住途中整张转入禁用也撤下。整张禁用时谁都不进;只读时日期格不进(翻页与钻层照常);到界的翻页钮与到顶的标题是原生 disabled,不可选的格子是 aria-disabled,都不进 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `grid` | `aria-disabled` | 'true' \| 'false' | | `grid` | `aria-labelledby` | frame.headingId(panel.index) | | `grid` | `aria-multiselectable` | 'true' \| 'false' | | `grid` | `aria-readonly` | 'true' \| 'false' | | `grid` | `role` | 'grid' | | `grid-head` | `role` | 'rowgroup' | | `week-day` | `aria-label` | meta?.long | | `week-day` | `role` | 'columnheader' | | `grid-body` | `role` | 'rowgroup' | | `week-row` | `role` | 'row' | | `week-number` | `aria-hidden` | 'true' | | `week-number` | `role` | 'rowheader' | | `cell` | `aria-selected` | 'true' \| 'false' | | `cell` | `role` | 'gridcell' | | `cell-trigger` | `aria-disabled` | 'true' \| 'false' | | `cell-trigger` | `aria-label` | frame.dateLabel(state.date, state.period) | | `cell-trigger` | `role` | 'button' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/calendar-picker.css` 使用 `[data-scope="calendar-picker"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `prev-year-trigger` | `data-disabled` | ''(条件成立时才出现) | | `prev-year-trigger` | `data-pressed` | ''(条件成立时才出现) | | `prev-year-trigger` | `data-xh-action-control` | '' | | `prev-year-trigger` | `data-xh-action-display` | 'always' | | `prev-year-trigger` | `data-xh-action-profile` | 'icon' | | `prev-year-trigger` | `data-xh-action-size` | 'sm' | | `prev-year-trigger` | `data-xh-action-variant` | 'ghost' | | `prev-trigger` | `data-disabled` | ''(条件成立时才出现) | | `prev-trigger` | `data-pressed` | ''(条件成立时才出现) | | `prev-trigger` | `data-xh-action-control` | '' | | `prev-trigger` | `data-xh-action-display` | 'always' | | `prev-trigger` | `data-xh-action-profile` | 'icon' | | `prev-trigger` | `data-xh-action-size` | 'sm' | | `prev-trigger` | `data-xh-action-variant` | 'ghost' | | `next-trigger` | `data-disabled` | ''(条件成立时才出现) | | `next-trigger` | `data-pressed` | ''(条件成立时才出现) | | `next-trigger` | `data-xh-action-control` | '' | | `next-trigger` | `data-xh-action-display` | 'always' | | `next-trigger` | `data-xh-action-profile` | 'icon' | | `next-trigger` | `data-xh-action-size` | 'sm' | | `next-trigger` | `data-xh-action-variant` | 'ghost' | | `next-year-trigger` | `data-disabled` | ''(条件成立时才出现) | | `next-year-trigger` | `data-pressed` | ''(条件成立时才出现) | | `next-year-trigger` | `data-xh-action-control` | '' | | `next-year-trigger` | `data-xh-action-display` | 'always' | | `next-year-trigger` | `data-xh-action-profile` | 'icon' | | `next-year-trigger` | `data-xh-action-size` | 'sm' | | `next-year-trigger` | `data-xh-action-variant` | 'ghost' | | `heading` | `data-index` | frame.panelOf(panel).index | | `heading` | `data-view` | view | | `heading-year-trigger` | `data-disabled` | ''(条件成立时才出现) | | `heading-year-trigger` | `data-index` | frame.panelOf(panel).index | | `heading-year-trigger` | `data-pressed` | ''(条件成立时才出现) | | `heading-year-trigger` | `data-view` | view | | `heading-year-trigger` | `data-xh-action-control` | '' | | `heading-year-trigger` | `data-xh-action-display` | 'always' | | `heading-year-trigger` | `data-xh-action-profile` | 'text' | | `heading-year-trigger` | `data-xh-action-size` | 'sm' | | `heading-year-trigger` | `data-xh-action-variant` | 'ghost' | | `heading-month-trigger` | `data-disabled` | ''(条件成立时才出现) | | `heading-month-trigger` | `data-index` | frame.panelOf(panel).index | | `heading-month-trigger` | `data-pressed` | ''(条件成立时才出现) | | `heading-month-trigger` | `data-view` | view | | `heading-month-trigger` | `data-xh-action-control` | '' | | `heading-month-trigger` | `data-xh-action-display` | 'always' | | `heading-month-trigger` | `data-xh-action-profile` | 'text' | | `heading-month-trigger` | `data-xh-action-size` | 'sm' | | `heading-month-trigger` | `data-xh-action-variant` | 'ghost' | | `grid` | `data-disabled` | ''(条件成立时才出现) | | `grid` | `data-index` | frame.panelOf(panel).index | | `grid` | `data-readonly` | ''(条件成立时才出现) | | `grid` | `data-view` | view | | `cell` | `data-disabled` | ''(条件成立时才出现) | | `cell` | `data-focus` | ''(条件成立时才出现) | | `cell` | `data-outside-month` | ''(条件成立时才出现) | | `cell` | `data-selected` | ''(条件成立时才出现) | | `cell` | `data-today` | ''(条件成立时才出现) | | `cell-trigger` | `data-disabled` | ''(条件成立时才出现) | | `cell-trigger` | `data-focus` | ''(条件成立时才出现) | | `cell-trigger` | `data-outside-month` | ''(条件成立时才出现) | | `cell-trigger` | `data-pressed` | ''(条件成立时才出现) | | `cell-trigger` | `data-selected` | ''(条件成立时才出现) | | `cell-trigger` | `data-today` | ''(条件成立时才出现) | | `cell-trigger` | `data-xh-action-control` | '' | | `cell-trigger` | `data-xh-action-display` | 'always' | | `cell-trigger` | `data-xh-action-profile` | 'text' | | `cell-trigger` | `data-xh-action-size` | 'sm' | | `cell-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-calendar-picker-cell-bg-hover` | `cell-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | calendar-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-picker-cell-bg-pressed` | `cell-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | calendar-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-picker-cell-bg-selected` | `cell-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`focus-visible`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])`
`selected`
`xh-ink-surface` | `--xh-bg-brand` | calendar-picker 的 cell-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-picker-cell-bg-selected-active` | `cell-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`selected` | `--xh-bg-brand-active` | calendar-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-picker-cell-bg-selected-disabled` | `cell-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`selected`
`xh-ink-surface` | `--xh-bg-subtle` | calendar-picker 的 cell-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-picker-cell-fg` | `cell-trigger` | `color` | `@media print`
`default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`selected` | `--xh-fg-default` | calendar-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-cell-fg-outside` | `cell-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`outside-month`
`pressed` | `--xh-fg-subtle` | calendar-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-cell-fg-selected` | `cell-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`selected` | `--xh-fg-on-brand` | calendar-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-cell-font-size` | `cell-trigger` | `font-size` | `default` | `--xh-text-body-size` | calendar-picker 的 cell-trigger 部件 font-size 覆盖槽。 | | `--xh-calendar-picker-cell-font-weight` | `cell-trigger` | `font-weight` | `default` | `--xh-font-weight-medium` | calendar-picker 的 cell-trigger 部件 font-weight 覆盖槽。 | | `--xh-calendar-picker-cell-gap` | `cell`
`cell-trigger` | `inset`
`padding` | `default` | `--xh-space-0_5` | calendar-picker 的 cell、cell-trigger 部件 inset、padding 覆盖槽。 | | `--xh-calendar-picker-cell-radius` | `cell-trigger` | `border-radius` | `default` | `--xh-shape-inset` | calendar-picker 的 cell-trigger 部件 border-radius 覆盖槽。 | | `--xh-calendar-picker-cell-size` | `cell-trigger` | `min-inline-size` | `default` | `--xh-control-h-sm` | calendar-picker 的 cell-trigger 部件 min-inline-size 覆盖槽。 | | `--xh-calendar-picker-gap` | `root` | `gap` | `default` | `--xh-space-2` | calendar-picker 的 root 部件 gap 覆盖槽。 | | `--xh-calendar-picker-grid-gap` | `grid` | `gap` | `default` | `--xh-space-1` | calendar-picker 的 grid 部件 gap 覆盖槽。 | | `--xh-calendar-picker-header-gap` | `header` | `gap` | `default` | `--xh-space-2` | calendar-picker 的 header 部件 gap 覆盖槽。 | | `--xh-calendar-picker-heading-fg` | `heading`
`heading-month-trigger`
`heading-year-trigger` | `color` | `default`
`disabled`
`focus-visible`
`not([hidden])` | `--xh-fg-default` | calendar-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-heading-font-size` | `heading`
`heading-month-trigger`
`heading-year-trigger` | `font-size` | `default`
`not([hidden])` | `--xh-text-label-size` | calendar-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 font-size 覆盖槽。 | | `--xh-calendar-picker-heading-font-weight` | `heading`
`heading-month-trigger`
`heading-year-trigger` | `font-weight` | `default`
`not([hidden])` | `--xh-font-weight-semibold` | calendar-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 font-weight 覆盖槽。 | | `--xh-calendar-picker-heading-trigger-bg-pressed` | `heading-month-trigger`
`heading-year-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([hidden])`
`pressed` | `--xh-_action-variant-bg-pressed` | calendar-picker 的 heading-month-trigger、heading-year-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-picker-heading-trigger-fg-hover` | `heading-month-trigger`
`heading-year-trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([hidden])`
`pressed` | `--xh-fg-brand` | calendar-picker 的 heading-month-trigger、heading-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-heading-trigger-px` | `heading-month-trigger`
`heading-year-trigger` | `padding-inline` | `not([hidden])` | `--xh-space-1` | calendar-picker 的 heading-month-trigger、heading-year-trigger 部件 padding-inline 覆盖槽。 | | `--xh-calendar-picker-heading-trigger-radius` | `heading-month-trigger`
`heading-year-trigger` | `border-radius` | `not([hidden])` | `--xh-shape-control` | calendar-picker 的 heading-month-trigger、heading-year-trigger 部件 border-radius 覆盖槽。 | | `--xh-calendar-picker-icon-size` | `root` | `--xh-icon-size` | `default` | `--xh-glyph-size-sm` | calendar-picker 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-calendar-picker-nav-bg` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `--xh-ink-surface`
`background-color` | `default`
`focus-visible`
`xh-ink-surface` | `--xh-_action-variant-bg-focus-visible`
`--xh-_action-variant-bg-rest` | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-picker-nav-bg-hover` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-picker-nav-bg-pressed` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-picker-nav-fg` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `color` | `default`
`focus-visible` | `--xh-fg-muted` | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-nav-fg-hover` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-default` | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-nav-radius` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `border-radius` | `default` | `--xh-shape-control` | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 border-radius 覆盖槽。 | | `--xh-calendar-picker-nav-size` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size` | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-calendar-picker-period-gap` | `grid` | `gap` | `view=month`
`view=quarter`
`view=week`
`view=year` | `--xh-space-1` | calendar-picker 的 grid 部件 gap 覆盖槽。 | | `--xh-calendar-picker-period-py` | `cell-trigger`
`grid` | `padding-block` | `is([data-view='week'], [data-view='month'], [data-view='quarter'], [data-view='year'])`
`view=month`
`view=quarter`
`view=week`
`view=year` | `--xh-space-2` | calendar-picker 的 cell-trigger、grid 部件 padding-block 覆盖槽。 | | `--xh-calendar-picker-period-radius` | `cell-trigger`
`grid` | `border-radius` | `is([data-view='week'], [data-view='month'], [data-view='quarter'], [data-view='year'])`
`view=month`
`view=quarter`
`view=week`
`view=year` | `--xh-shape-control` | calendar-picker 的 cell-trigger、grid 部件 border-radius 覆盖槽。 | | `--xh-calendar-picker-row-gap` | `grid-body`
`grid-head` | `gap` | `default` | `--xh-space-0` | calendar-picker 的 grid-body、grid-head 部件 gap 覆盖槽。 | | `--xh-calendar-picker-today-bg` | `cell-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`focus-visible`
`today`
`xh-ink-surface` | `transparent` | calendar-picker 的 cell-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-picker-today-border` | `cell-trigger` | `border`
`border-color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`today` | `--xh-fg-brand` | calendar-picker 的 cell-trigger 部件 border、border-color 覆盖槽。 | | `--xh-calendar-picker-today-fg` | `cell-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`today` | `--xh-fg-brand` | calendar-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-picker-week-cell-px` | `cell-trigger`
`grid` | `padding-inline` | `view=week` | `--xh-space-3` | calendar-picker 的 cell-trigger、grid 部件 padding-inline 覆盖槽。 | | `--xh-calendar-picker-week-day-fg` | `week-day` | `color` | `default` | `--xh-fg-subtle` | calendar-picker 的 week-day 部件 color 覆盖槽。 | | `--xh-calendar-picker-week-day-font-size` | `week-day` | `font-size` | `default` | `--xh-text-caption-size` | calendar-picker 的 week-day 部件 font-size 覆盖槽。 | | `--xh-calendar-picker-week-day-font-weight` | `week-day` | `font-weight` | `default` | `--xh-font-weight-medium` | calendar-picker 的 week-day 部件 font-weight 覆盖槽。 | | `--xh-calendar-picker-week-day-h` | `week-day` | `block-size` | `default` | `--xh-control-h-sm` | calendar-picker 的 week-day 部件 block-size 覆盖槽。 | | `--xh-calendar-picker-week-number-fg` | `week-number` | `color` | `default` | `--xh-fg-subtle` | calendar-picker 的 week-number 部件 color 覆盖槽。 | | `--xh-calendar-picker-week-number-font-size` | `week-number` | `font-size` | `default` | `--xh-text-caption-size` | calendar-picker 的 week-number 部件 font-size 覆盖槽。 | | `--xh-calendar-picker-week-number-w` | `week-row` | `grid-template-columns` | `has(> [data-part='week-number'])`
`not([hidden])` | `--xh-control-h-md` | calendar-picker 的 week-row 部件 grid-template-columns 覆盖槽。 | | `--xh-calendar-picker-year-grid-max-h` | `grid` | `max-block-size` | `view=year` | `--xh-viewport-h-sm` | calendar-picker 的 grid 部件 max-block-size 覆盖槽。 | | `--xh-calendar-picker-year-grid-pe` | `grid` | `padding-inline-end` | `view=year` | `--xh-space-1` | calendar-picker 的 grid 部件 padding-inline-end 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### 响应式 皮肤另按输入能力分档:`pointer: coarse`:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/calendar-range-picker # CalendarRangePicker 日历范围选择器 在日历网格中先选起点再选终点,选出一段连续的天、周、月、季度或年。 ## 用法 先落下起点再落下终点,也可以按住拖动经过;两端都落定才写值,Escape 撤销起点 ```vue ``` ```html
区间:(未选) ``` ## 组件结构 加粗的是必需部件。 `data-scope="calendar-range-picker"`:`root` · `header` · `prev-year-trigger` · `prev-trigger` · `next-trigger` · `next-year-trigger` · `heading` · `heading-year-trigger` · `heading-month-trigger` · **`grid`** · `grid-head` · `week-day` · `grid-body` · `week-row` · `week-number` · **`cell`** · **`cell-trigger`** ## 示例 ### 并排两个月 visible-count=2:起止常跨月,并排查看两页更便于选择;翻页时整个窗口一起移动 ```vue ``` ```html
区间:2026-09-28 → 2026-10-06 ``` ### 不可用的日期 allows-non-contiguous-ranges 允许区间跨过周末,只是这些日期不铺设轨道;isDateUnavailable 可得到起点,据此限制区间长度 ```vue ``` ```html
区间:(未选) ``` ### 按周选择 granularity=week:一行一个整周,格子直接铺进网格;值是两端两周的周首日;月、季度与年同理 ```vue ``` ```html
周区间:(未选) ``` ## 设计指引 ### 何时使用 - 需要查看整月分布再选择一段起止:预订入住与退房、报表统计区间、排班周期。 - 起止常跨月,需要并排查看两个月再决定。 ### 何时不用 - 只选一天或几个不连续的日期时,使用[日历选择器](./calendar-picker)。 - 需要键入起止日期或放在表单字段中时,使用[日期范围选择器](./date-range-picker)。 ### 特性 - 先选起点再选终点:起点只记录在组件内,两端都落定后才写值;Escape 撤销起点后原区间保持不变。 - 支持按住拖选:按下即落起点,拖到另一格松开即完成;按住已选区间的一端拖动可以直接改写该端;触屏按住片刻才开始拖动,轻点仍是普通点选。 - 焦点离开网格时,未完成的区间在起点到聚焦日之间就地收口,不留悬空的起点。 - 落起点后可选范围默认被夹在两侧最近的不可用日之间,`allowsNonContiguousRanges` 允许跨过它们;`isDateUnavailable` 的第二个参数是当前起点,可据此限制区间长度。 - 已选区间的任一端越界或不可用即标记为不合法,也可以用 `invalid` 显式声明。 - `granularity` 决定周期格的生成方式;周、月、季度和年区间共用同一套 Period 边界判断。 - `visibleCount` 并排展示连续的月份,翻页时整个视窗一起移动;起止常跨月时建议为 2。 - `calendarPeriodValue` 将两端转换为 `{ granularity, start, end, keys }`,可直接用于查询参数。 - 周首日、月份名与星期名跟随 `locale`,与日历选择器使用同一套解析链。 ### 组合 - 内嵌在[日期范围选择器](./date-range-picker)的浮层中,由它持有聚焦日与层级切换。 - 与[日历选择器](./calendar-picker)共用同一套部件名与皮肤槽,只多出区间轨道的几条状态。 ### 最佳实践 - 区间中段保持连续淡色带,起止使用实心端点;未完成的预览与已落定的区间外观一致,悬停预览不显示独立的普通悬停样式。 - 今天使用 1px 品牌环 + 品牌字,落在区间里时环压在淡色带上,仍与起止端点的实心面分得开。 - 周区间按整周格连续预览,月份、季度和年份区间共用同一套 Period 边界判断。 - 落起点后把焦点移动一格,让键盘用户看出正在选择一段而不是一天。 - 起止常跨月时设置 `visibleCount="2"`,避免用户来回翻页。 ### 反模式 - 用两个日历选择器分别选起止:中间的日期不铺轨道,也没有拖选与预览。 - 不可选的日期无法聚焦,键盘用户无从知道该位置的内容。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCalendarRangePickerCell` `XhCalendarRangePickerCellTrigger` `XhCalendarRangePickerGrid` `XhCalendarRangePickerGridBody` `XhCalendarRangePickerGridHead` `XhCalendarRangePickerHeader` `XhCalendarRangePickerHeading` `XhCalendarRangePickerHeadingMonthTrigger` `XhCalendarRangePickerHeadingYearTrigger` `XhCalendarRangePickerNextTrigger` `XhCalendarRangePickerNextYearTrigger` `XhCalendarRangePickerPrevTrigger` `XhCalendarRangePickerPrevYearTrigger` `XhCalendarRangePickerRoot` `XhCalendarRangePickerWeekDay` `XhCalendarRangePickerWeekNumber` `XhCalendarRangePickerWeekRow` | | 组合式函数 | `useCalendarRangePicker` | | 状态机 | `calendarRangePickerMachine` | | 皮肤 | `@xihan-ui/styles/calendar-range-picker.css` | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `CalendarRangePickerValueChangeDetails` | 区间两端都落定;detail 为 `{ value: string[] }`,长度恒为 2 | | `focused-value-change` | `CalendarFocusChangeDetails` | 聚焦日变化;detail 为 `{ focusedValue: string }` | | `active-view-change` | `CalendarViewChangeDetails` | 切换到另一层级;detail 为 `{ activeView: 'day'\|'week'\|'month'\|'quarter'\|'year' }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCalendarRangePickerRoot` | `default` | `CalendarRangePickerRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCalendarRangePickerCell` | `value` | `string` | 是 | ISO 日期串。 | | `XhCalendarRangePickerCell` | `index` | `number` | | 属于第几个面板,默认 0。多面板时必须提供:同一天会同时出现在两个面板中 (8 月末的几天也铺在 9 月的首行),是否为本月只有连同面板一起看才能判定。 | | `XhCalendarRangePickerGrid` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarRangePickerHeading` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarRangePickerHeadingMonthTrigger` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarRangePickerHeadingYearTrigger` | `index` | `number` | | 属于第几个面板,默认 0。单面板时不必写。 | | `XhCalendarRangePickerRoot` | `value` | `string \| string[]` | | | | `XhCalendarRangePickerRoot` | `defaultValue` | `string \| string[]` | | | | `XhCalendarRangePickerRoot` | `focusedValue` | `string` | | | | `XhCalendarRangePickerRoot` | `defaultFocusedValue` | `string` | | | | `XhCalendarRangePickerRoot` | `min` | `string` | | | | `XhCalendarRangePickerRoot` | `max` | `string` | | | | `XhCalendarRangePickerRoot` | `isDateUnavailable` | `(value: string, anchor: string \| null) => boolean` | | | | `XhCalendarRangePickerRoot` | `allowsNonContiguousRanges` | `boolean` | | 区间允许跨过不可用的日期;默认关闭,落下起点后只能选到两侧最近的不可用日为止。 | | `XhCalendarRangePickerRoot` | `invalid` | `boolean` | | 校验失败:根带 data-invalid,区间内的格子报告 aria-invalid。 | | `XhCalendarRangePickerRoot` | `locale` | `string` | | | | `XhCalendarRangePickerRoot` | `timeZone` | `string` | | | | `XhCalendarRangePickerRoot` | `disabled` | `boolean` | | | | `XhCalendarRangePickerRoot` | `readOnly` | `boolean` | | | | `XhCalendarRangePickerRoot` | `weekdayFormat` | `CalendarWeekdayFormat` | | | | `XhCalendarRangePickerRoot` | `fixedWeeks` | `boolean` | | | | `XhCalendarRangePickerRoot` | `granularity` | `CalendarGranularity` | | 选择粒度。 | | `XhCalendarRangePickerRoot` | `activeView` | `CalendarView` | | 面板当前所处的层级;给定即受控,默认跟随 granularity。 | | `XhCalendarRangePickerRoot` | `defaultActiveView` | `CalendarView` | | 非受控初值,默认同 granularity。 | | `XhCalendarRangePickerRoot` | `visibleCount` | `number` | | 并排展示几页,默认 1。 | | `XhCalendarRangePickerRoot` | `translations` | `Partial` | | | | `XhCalendarRangePickerRoot` | `onValueChange` | `CalendarRangePickerProps['onValueChange']` | | | | `XhCalendarRangePickerRoot` | `onFocusedValueChange` | `CalendarRangePickerProps['onFocusedValueChange']` | | | | `XhCalendarRangePickerRoot` | `onActiveViewChange` | `CalendarRangePickerProps['onActiveViewChange']` | | | | `XhCalendarRangePickerRoot` | `children` | `SlotChildren` | | | | `XhCalendarRangePickerWeekDay` | `value` | `number \| string` | 是 | 列序 0-6,兼收字符串。 | | `XhCalendarRangePickerWeekNumber` | `value` | `string` | 是 | 该行行首那一天的 ISO 串。 | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` · `anchored` **事件**:`RANGE.ANCHOR` · `RANGE.COMMIT` · `DRAG.SET` · `HOVER.SET` · `HOVER.CLEAR` · `PRESS.START` · `PRESS.END` **判据**:`startsRange` · `anchorsRange` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `rangeAnchor` | `string \| null` | 区间选到一半时的起点(周期首日的 ISO 串);其余时候为 null。 | | `dragging` | `boolean` | 指针正按在格子上拖动选择区间。 | | `setRangeAnchor` | `(next: string \| null) => void` | 直接改写区间起点;传 null 撤销选到一半的区间。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/#kbd_label) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Tab` / `Shift+Tab` | focus outside the grid | 整张网格只占一个 Tab 位:焦点进入聚焦日那一格 | | `ArrowLeft` | focus in grid | 焦点前移一天;越过月首即翻到上一月并落在那一天。粗粒度视图里走一格(一个月 / 一季 / 一年) | | `ArrowRight` | focus in grid | 焦点后移一天;越过月末即翻到下一月并落在那一天。粗粒度视图里走一格 | | `ArrowUp` | focus in grid | 焦点上移一周(减七天),跨月照样翻页。粗粒度视图里上移一行 | | `ArrowDown` | focus in grid | 焦点下移一周(加七天),跨月照样翻页。粗粒度视图里下移一行 | | `Home` | focus in grid | 焦点移到本周第一天;周首日随 locale 变。粗粒度视图里移到本行头一格 | | `End` | focus in grid | 焦点移到本周最后一天。粗粒度视图里移到本行末一格 | | `PageUp` | focus in grid | 退一个月,日号不变(月末日被目标月夹住:3 月 31 日退成 2 月 29 日)。粗粒度视图里退一整页 | | `PageDown` | focus in grid | 进一个月,日号不变。粗粒度视图里进一整页 | | `Shift+PageUp` | focus in grid | 退一年;粗粒度视图里退十页 | | `Shift+PageDown` | focus in grid | 进一年;粗粒度视图里进十页 | | `Enter` / `Space` | focus in grid, 聚焦周期可用且非只读 | 先落起点再落终点。落起点后焦点自动前进一格(挑不了就退一格),方向键走到哪儿预览就铺到哪儿;落终点那一下把两端一并写出。还没钻到 granularity 那一档时这一下是往下钻一层 | | `Escape` | focus in grid, 区间已落起点 | 撤掉起点,原来的区间原样还在;不拦默认行为,外层浮层照常收起 | | `Tab` / `Shift+Tab` | focus in grid, 区间已落起点 | 焦点离开前把区间收在起点到聚焦日之间;不拦默认行为,焦点照常离开 | | `Enter` / `Space` | held in prev-year-trigger / prev-trigger / next-trigger / next-year-trigger / heading-year-trigger / heading-month-trigger / cell-trigger, 该部件可按 | 按住期间该部件投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,按住途中整张转入禁用也撤下。落起点那一下焦点前进一格,按压面随焦点一起走。整张禁用时谁都不进;只读时日期格不进(翻页与钻层照常);到界的翻页钮与到顶的标题是原生 disabled,不可选的格子是 aria-disabled,都不进 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `grid` | `aria-disabled` | 'true' \| 'false' | | `grid` | `aria-labelledby` | frame.headingId(panel.index) | | `grid` | `aria-multiselectable` | 'true' | | `grid` | `aria-readonly` | 'true' \| 'false' | | `grid` | `role` | 'grid' | | `grid-head` | `role` | 'rowgroup' | | `week-day` | `aria-label` | meta?.long | | `week-day` | `role` | 'columnheader' | | `grid-body` | `role` | 'rowgroup' | | `week-row` | `role` | 'row' | | `week-number` | `aria-hidden` | 'true' | | `week-number` | `role` | 'rowheader' | | `cell` | `aria-selected` | 'true' \| 'false' | | `cell` | `role` | 'gridcell' | | `cell-trigger` | `aria-description` | translations.finishRangeSelectionPrompt \| translations.startRangeSelectionPrompt \| undefined | | `cell-trigger` | `aria-disabled` | 'true' \| 'false' | | `cell-trigger` | `aria-invalid` | 'true' \| undefined | | `cell-trigger` | `aria-label` | frame.dateLabel(state.date, period) | | `cell-trigger` | `role` | 'button' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/calendar-range-picker.css` 使用 `[data-scope="calendar-range-picker"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `prev-year-trigger` | `data-disabled` | ''(条件成立时才出现) | | `prev-year-trigger` | `data-pressed` | ''(条件成立时才出现) | | `prev-year-trigger` | `data-xh-action-control` | '' | | `prev-year-trigger` | `data-xh-action-display` | 'always' | | `prev-year-trigger` | `data-xh-action-profile` | 'icon' | | `prev-year-trigger` | `data-xh-action-size` | 'sm' | | `prev-year-trigger` | `data-xh-action-variant` | 'ghost' | | `prev-trigger` | `data-disabled` | ''(条件成立时才出现) | | `prev-trigger` | `data-pressed` | ''(条件成立时才出现) | | `prev-trigger` | `data-xh-action-control` | '' | | `prev-trigger` | `data-xh-action-display` | 'always' | | `prev-trigger` | `data-xh-action-profile` | 'icon' | | `prev-trigger` | `data-xh-action-size` | 'sm' | | `prev-trigger` | `data-xh-action-variant` | 'ghost' | | `next-trigger` | `data-disabled` | ''(条件成立时才出现) | | `next-trigger` | `data-pressed` | ''(条件成立时才出现) | | `next-trigger` | `data-xh-action-control` | '' | | `next-trigger` | `data-xh-action-display` | 'always' | | `next-trigger` | `data-xh-action-profile` | 'icon' | | `next-trigger` | `data-xh-action-size` | 'sm' | | `next-trigger` | `data-xh-action-variant` | 'ghost' | | `next-year-trigger` | `data-disabled` | ''(条件成立时才出现) | | `next-year-trigger` | `data-pressed` | ''(条件成立时才出现) | | `next-year-trigger` | `data-xh-action-control` | '' | | `next-year-trigger` | `data-xh-action-display` | 'always' | | `next-year-trigger` | `data-xh-action-profile` | 'icon' | | `next-year-trigger` | `data-xh-action-size` | 'sm' | | `next-year-trigger` | `data-xh-action-variant` | 'ghost' | | `heading` | `data-index` | frame.panelOf(panel).index | | `heading` | `data-view` | view | | `heading-year-trigger` | `data-disabled` | ''(条件成立时才出现) | | `heading-year-trigger` | `data-index` | frame.panelOf(panel).index | | `heading-year-trigger` | `data-pressed` | ''(条件成立时才出现) | | `heading-year-trigger` | `data-view` | view | | `heading-year-trigger` | `data-xh-action-control` | '' | | `heading-year-trigger` | `data-xh-action-display` | 'always' | | `heading-year-trigger` | `data-xh-action-profile` | 'text' | | `heading-year-trigger` | `data-xh-action-size` | 'sm' | | `heading-year-trigger` | `data-xh-action-variant` | 'ghost' | | `heading-month-trigger` | `data-disabled` | ''(条件成立时才出现) | | `heading-month-trigger` | `data-index` | frame.panelOf(panel).index | | `heading-month-trigger` | `data-pressed` | ''(条件成立时才出现) | | `heading-month-trigger` | `data-view` | view | | `heading-month-trigger` | `data-xh-action-control` | '' | | `heading-month-trigger` | `data-xh-action-display` | 'always' | | `heading-month-trigger` | `data-xh-action-profile` | 'text' | | `heading-month-trigger` | `data-xh-action-size` | 'sm' | | `heading-month-trigger` | `data-xh-action-variant` | 'ghost' | | `grid` | `data-disabled` | ''(条件成立时才出现) | | `grid` | `data-dragging` | ''(条件成立时才出现) | | `grid` | `data-index` | frame.panelOf(panel).index | | `grid` | `data-readonly` | ''(条件成立时才出现) | | `grid` | `data-view` | view | | `cell` | `data-disabled` | ''(条件成立时才出现) | | `cell` | `data-focus` | ''(条件成立时才出现) | | `cell` | `data-in-range` | ''(条件成立时才出现) | | `cell` | `data-invalid` | ''(条件成立时才出现) | | `cell` | `data-outside-month` | ''(条件成立时才出现) | | `cell` | `data-range-end` | ''(条件成立时才出现) | | `cell` | `data-range-preview` | ''(条件成立时才出现) | | `cell` | `data-range-start` | ''(条件成立时才出现) | | `cell` | `data-selected` | ''(条件成立时才出现) | | `cell` | `data-today` | ''(条件成立时才出现) | | `cell-trigger` | `data-disabled` | ''(条件成立时才出现) | | `cell-trigger` | `data-focus` | ''(条件成立时才出现) | | `cell-trigger` | `data-in-range` | ''(条件成立时才出现) | | `cell-trigger` | `data-invalid` | ''(条件成立时才出现) | | `cell-trigger` | `data-outside-month` | ''(条件成立时才出现) | | `cell-trigger` | `data-pressed` | ''(条件成立时才出现) | | `cell-trigger` | `data-range-end` | ''(条件成立时才出现) | | `cell-trigger` | `data-range-preview` | ''(条件成立时才出现) | | `cell-trigger` | `data-range-start` | ''(条件成立时才出现) | | `cell-trigger` | `data-selected` | ''(条件成立时才出现) | | `cell-trigger` | `data-today` | ''(条件成立时才出现) | | `cell-trigger` | `data-xh-action-control` | '' | | `cell-trigger` | `data-xh-action-display` | 'always' | | `cell-trigger` | `data-xh-action-profile` | 'text' | | `cell-trigger` | `data-xh-action-size` | 'sm' | | `cell-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-calendar-range-picker-cell-bg-hover` | `cell-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | calendar-range-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-cell-bg-pressed` | `cell-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | calendar-range-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-cell-bg-selected` | `cell-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`focus-visible`
`hover`
`in-range`
`is([data-range-start], [data-range-end])`
`loading`
`not([data-disabled])`
`not([data-in-range])`
`not([data-loading])`
`not([data-outside-month])`
`outside-month`
`range-end`
`range-start`
`selected`
`xh-ink-surface` | `--xh-bg-brand` | calendar-range-picker 的 cell-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-range-picker-cell-bg-selected-active` | `cell-trigger` | `background-color` | `disabled`
`in-range`
`is(:active, [data-pressed])`
`is([data-range-start], [data-range-end])`
`loading`
`not([data-disabled])`
`not([data-in-range])`
`not([data-loading])`
`not([data-outside-month])`
`outside-month`
`pressed`
`range-end`
`range-start`
`selected` | `--xh-bg-brand-active` | calendar-range-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-cell-bg-selected-disabled` | `cell-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`selected`
`xh-ink-surface` | `--xh-bg-subtle` | calendar-range-picker 的 cell-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-range-picker-cell-fg` | `cell-trigger` | `color` | `@media print`
`default`
`disabled`
`focus-visible`
`hover`
`in-range`
`is(:active, [data-pressed])`
`is([data-range-start], [data-range-end])`
`loading`
`not([data-disabled])`
`not([data-in-range])`
`not([data-loading])`
`not([data-outside-month])`
`outside-month`
`pressed`
`range-end`
`range-start`
`selected` | `--xh-fg-default` | calendar-range-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-cell-fg-outside` | `cell-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`outside-month`
`pressed` | `--xh-fg-subtle` | calendar-range-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-cell-fg-selected` | `cell-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`in-range`
`is(:active, [data-pressed])`
`is([data-range-start], [data-range-end])`
`loading`
`not([data-disabled])`
`not([data-in-range])`
`not([data-loading])`
`not([data-outside-month])`
`outside-month`
`pressed`
`range-end`
`range-start`
`selected` | `--xh-fg-on-brand` | calendar-range-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-cell-font-size` | `cell-trigger` | `font-size` | `default` | `--xh-text-body-size` | calendar-range-picker 的 cell-trigger 部件 font-size 覆盖槽。 | | `--xh-calendar-range-picker-cell-font-weight` | `cell-trigger` | `font-weight` | `default` | `--xh-font-weight-medium` | calendar-range-picker 的 cell-trigger 部件 font-weight 覆盖槽。 | | `--xh-calendar-range-picker-cell-gap` | `cell`
`cell-trigger` | `inset`
`inset-block`
`padding` | `default`
`in-range`
`not([data-outside-month])`
`outside-month` | `--xh-space-0_5` | calendar-range-picker 的 cell、cell-trigger 部件 inset、inset-block、padding 覆盖槽。 | | `--xh-calendar-range-picker-cell-radius` | `cell`
`cell-trigger`
`grid` | `border-radius` | `default`
`in-range`
`is([data-view='week'], [data-view='month'], [data-view='quarter'], [data-view='year'])`
`view=month`
`view=quarter`
`view=week`
`view=year` | `--xh-shape-inset` | calendar-range-picker 的 cell、cell-trigger、grid 部件 border-radius 覆盖槽。 | | `--xh-calendar-range-picker-cell-size` | `cell-trigger` | `min-inline-size` | `default` | `--xh-control-h-sm` | calendar-range-picker 的 cell-trigger 部件 min-inline-size 覆盖槽。 | | `--xh-calendar-range-picker-gap` | `root` | `gap` | `default` | `--xh-space-2` | calendar-range-picker 的 root 部件 gap 覆盖槽。 | | `--xh-calendar-range-picker-grid-gap` | `grid` | `gap` | `default` | `--xh-space-1` | calendar-range-picker 的 grid 部件 gap 覆盖槽。 | | `--xh-calendar-range-picker-header-gap` | `header` | `gap` | `default` | `--xh-space-2` | calendar-range-picker 的 header 部件 gap 覆盖槽。 | | `--xh-calendar-range-picker-heading-fg` | `heading`
`heading-month-trigger`
`heading-year-trigger` | `color` | `default`
`disabled`
`focus-visible`
`not([hidden])` | `--xh-fg-default` | calendar-range-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-heading-font-size` | `heading`
`heading-month-trigger`
`heading-year-trigger` | `font-size` | `default`
`not([hidden])` | `--xh-text-label-size` | calendar-range-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 font-size 覆盖槽。 | | `--xh-calendar-range-picker-heading-font-weight` | `heading`
`heading-month-trigger`
`heading-year-trigger` | `font-weight` | `default`
`not([hidden])` | `--xh-font-weight-semibold` | calendar-range-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 font-weight 覆盖槽。 | | `--xh-calendar-range-picker-heading-trigger-bg-pressed` | `heading-month-trigger`
`heading-year-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([hidden])`
`pressed` | `--xh-_action-variant-bg-pressed` | calendar-range-picker 的 heading-month-trigger、heading-year-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-heading-trigger-fg-hover` | `heading-month-trigger`
`heading-year-trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([hidden])`
`pressed` | `--xh-fg-brand` | calendar-range-picker 的 heading-month-trigger、heading-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-heading-trigger-px` | `heading-month-trigger`
`heading-year-trigger` | `padding-inline` | `not([hidden])` | `--xh-space-1` | calendar-range-picker 的 heading-month-trigger、heading-year-trigger 部件 padding-inline 覆盖槽。 | | `--xh-calendar-range-picker-heading-trigger-radius` | `heading-month-trigger`
`heading-year-trigger` | `border-radius` | `not([hidden])` | `--xh-shape-control` | calendar-range-picker 的 heading-month-trigger、heading-year-trigger 部件 border-radius 覆盖槽。 | | `--xh-calendar-range-picker-icon-size` | `root` | `--xh-icon-size` | `default` | `--xh-glyph-size-sm` | calendar-range-picker 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-calendar-range-picker-nav-bg` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `--xh-ink-surface`
`background-color` | `default`
`focus-visible`
`xh-ink-surface` | `--xh-_action-variant-bg-focus-visible`
`--xh-_action-variant-bg-rest` | calendar-range-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-range-picker-nav-bg-hover` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | calendar-range-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-nav-bg-pressed` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | calendar-range-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-nav-fg` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `color` | `default`
`focus-visible` | `--xh-fg-muted` | calendar-range-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-nav-fg-hover` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-default` | calendar-range-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-nav-radius` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `border-radius` | `default` | `--xh-shape-control` | calendar-range-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 border-radius 覆盖槽。 | | `--xh-calendar-range-picker-nav-size` | `next-trigger`
`next-year-trigger`
`prev-trigger`
`prev-year-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size` | calendar-range-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-calendar-range-picker-period-gap` | `grid` | `gap` | `view=month`
`view=quarter`
`view=week`
`view=year` | `--xh-space-1` | calendar-range-picker 的 grid 部件 gap 覆盖槽。 | | `--xh-calendar-range-picker-period-py` | `cell-trigger`
`grid` | `padding-block` | `is([data-view='week'], [data-view='month'], [data-view='quarter'], [data-view='year'])`
`view=month`
`view=quarter`
`view=week`
`view=year` | `--xh-space-2` | calendar-range-picker 的 cell-trigger、grid 部件 padding-block 覆盖槽。 | | `--xh-calendar-range-picker-period-radius` | `cell-trigger`
`grid` | `border-radius` | `is([data-view='week'], [data-view='month'], [data-view='quarter'], [data-view='year'])`
`view=month`
`view=quarter`
`view=week`
`view=year` | `--xh-shape-control` | calendar-range-picker 的 cell-trigger、grid 部件 border-radius 覆盖槽。 | | `--xh-calendar-range-picker-range-bg` | `cell` | `background` | `in-range`
`not([data-outside-month])`
`outside-month` | `--xh-bg-brand-subtle` | calendar-range-picker 的 cell 部件 background 覆盖槽。 | | `--xh-calendar-range-picker-range-cap-radius` | `cell` | `border-end-end-radius`
`border-end-start-radius`
`border-start-end-radius`
`border-start-start-radius` | `in-range`
`range-end`
`range-start` | `--xh-shape-inset` | calendar-range-picker 的 cell 部件 border-end-end-radius、border-end-start-radius、border-start-end-radius、border-start-start-radius 覆盖槽。 | | `--xh-calendar-range-picker-range-cell-bg-hover` | `cell-trigger` | `background-color` | `disabled`
`hover`
`in-range`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([data-outside-month], [data-disabled], [data-range-start], [data-range-end])`
`outside-month`
`range-end`
`range-start` | `--xh-bg-brand-subtle-hover` | calendar-range-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-range-cell-bg-pressed` | `cell-trigger` | `background-color` | `disabled`
`in-range`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([data-outside-month], [data-disabled], [data-range-start], [data-range-end])`
`outside-month`
`pressed`
`range-end`
`range-start` | `--xh-bg-brand-subtle-active` | calendar-range-picker 的 cell-trigger 部件 background-color 覆盖槽。 | | `--xh-calendar-range-picker-range-row-radius` | `cell`
`week-number`
`week-row` | `border-end-end-radius`
`border-end-start-radius`
`border-start-end-radius`
`border-start-start-radius` | `first-child`
`in-range`
`last-child` | `--xh-shape-inset` | calendar-range-picker 的 cell、week-number、week-row 部件 border-end-end-radius、border-end-start-radius、border-start-end-radius、border-start-start-radius 覆盖槽。 | | `--xh-calendar-range-picker-row-gap` | `grid-body`
`grid-head` | `gap` | `default` | `--xh-space-0` | calendar-range-picker 的 grid-body、grid-head 部件 gap 覆盖槽。 | | `--xh-calendar-range-picker-today-bg` | `cell-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`focus-visible`
`today`
`xh-ink-surface` | `transparent` | calendar-range-picker 的 cell-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-calendar-range-picker-today-border` | `cell-trigger` | `border`
`border-color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`today` | `--xh-fg-brand` | calendar-range-picker 的 cell-trigger 部件 border、border-color 覆盖槽。 | | `--xh-calendar-range-picker-today-fg` | `cell-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`today` | `--xh-fg-brand` | calendar-range-picker 的 cell-trigger 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-week-cell-px` | `cell-trigger`
`grid` | `padding-inline` | `view=week` | `--xh-space-3` | calendar-range-picker 的 cell-trigger、grid 部件 padding-inline 覆盖槽。 | | `--xh-calendar-range-picker-week-day-fg` | `week-day` | `color` | `default` | `--xh-fg-subtle` | calendar-range-picker 的 week-day 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-week-day-font-size` | `week-day` | `font-size` | `default` | `--xh-text-caption-size` | calendar-range-picker 的 week-day 部件 font-size 覆盖槽。 | | `--xh-calendar-range-picker-week-day-font-weight` | `week-day` | `font-weight` | `default` | `--xh-font-weight-medium` | calendar-range-picker 的 week-day 部件 font-weight 覆盖槽。 | | `--xh-calendar-range-picker-week-day-h` | `week-day` | `block-size` | `default` | `--xh-control-h-sm` | calendar-range-picker 的 week-day 部件 block-size 覆盖槽。 | | `--xh-calendar-range-picker-week-number-fg` | `week-number` | `color` | `default` | `--xh-fg-subtle` | calendar-range-picker 的 week-number 部件 color 覆盖槽。 | | `--xh-calendar-range-picker-week-number-font-size` | `week-number` | `font-size` | `default` | `--xh-text-caption-size` | calendar-range-picker 的 week-number 部件 font-size 覆盖槽。 | | `--xh-calendar-range-picker-week-number-w` | `week-row` | `grid-template-columns` | `has(> [data-part='week-number'])`
`not([hidden])` | `--xh-control-h-md` | calendar-range-picker 的 week-row 部件 grid-template-columns 覆盖槽。 | | `--xh-calendar-range-picker-year-grid-max-h` | `grid` | `max-block-size` | `view=year` | `--xh-viewport-h-sm` | calendar-range-picker 的 grid 部件 max-block-size 覆盖槽。 | | `--xh-calendar-range-picker-year-grid-pe` | `grid` | `padding-inline-end` | `view=year` | `--xh-space-1` | calendar-range-picker 的 grid 部件 padding-inline-end 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 `background-color` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 皮肤另按输入能力分档:`pointer: coarse`:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/card # Card 卡片 用于组织相关内容与操作的中性表面。 ## 用法 Header 放标题与说明,Content 放主体 ```vue ``` ```html
本月账单
账期 7 月 1 日至 7 月 31 日
共 128 笔支出,合计 3,240.00 元。
``` ## 组件结构 加粗的是必需部件。 `data-scope="card"`:**`root`** · `header` · `title` · `description` · `content` · `footer` ## 示例 ### 变体 outline 为默认卡面,subtle 淡底嵌入,ghost 用于嵌套 ```vue ``` ```html
outline
一段用来看表面层级的正文。
subtle
一段用来看表面层级的正文。
ghost
一段用来看表面层级的正文。
``` ### 横向布局 Card 只提供内容面,方向和媒体尺寸由使用场景决定 ```vue ``` ```html
七月总结
收入与支出趋势已生成
更新于今天 09:30
``` ### 带媒体 图片或自绘媒体作为普通子节点放入,由内容自己决定比例与圆角 ```vue ``` ```html
七月总结
媒体与文字共享卡片的统一节奏
本月共完成 18 个里程碑。
``` ## 设计指引 ### 何时使用 - 把一组相关信息收成一个可以整体感知的单元。 - 内容块之间需要视觉边界。 ### 何时不用 - 页面上每一块都使用卡片会使边界失效。 - 只需要一条分隔时,使用[分隔线](./separator)。 ### 特性 - root 必需;header、title、description、content、footer 按内容组合。 - `outline` 是默认卡面,`subtle` 用淡底嵌在别的面里,`ghost` 用于嵌套内容不再画面。 - 根统一提供 16px 内边距、12px 段间距和 surface 圆角;横向布局与媒体比例由使用场景决定。 ### 组合 - 图片等媒体直接作为普通子节点放入;内容区可放[描述列表](./descriptions)、[表格](./table)或[统计数值](./statistic)。 ### 最佳实践 - 整卡可点时提供明显的悬停与聚焦反馈,并让整卡进入 Tab 序列。 - 同组卡片保持相同宽度和内容节奏。 ### 反模式 - 卡片嵌套卡片,两层边界互相削弱。 - 整卡可点的同时卡内还有其他按钮,点击结果不可预期。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCardContent` `XhCardDescription` `XhCardFooter` `XhCardHeader` `XhCardRoot` `XhCardTitle` | | 状态机 | 无,`connect` 直接由 props 算属性 | | 皮肤 | `@xihan-ui/styles/card.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `variant` | `ControlVariant` | | 形态:outline 为带影的抬起面,subtle 为淡底,ghost 无底无影。默认 outline。 | ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `getRootProps` | `() => T['element']` | | | `getHeaderProps` | `() => T['element']` | | | `getTitleProps` | `() => T['element']` | | | `getDescriptionProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getFooterProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/card.css` 使用 `[data-scope="card"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-card-bg` | `root` | `background` | `default`
`variant=subtle` | `--xh-bg-subtle`
`--xh-bg-surface` | card 的 root 部件 background 覆盖槽。 | | `--xh-card-border` | `root` | `border` | `default` | `--xh-border-default` | card 的 root 部件 border 覆盖槽。 | | `--xh-card-content-gap` | `content` | `gap` | `default` | `--xh-space-1` | card 的 content 部件 gap 覆盖槽。 | | `--xh-card-description-fg` | `description` | `color` | `default` | `--xh-fg-muted` | card 的 description 部件 color 覆盖槽。 | | `--xh-card-description-font-size` | `description` | `font-size` | `default` | `--xh-text-secondary-size` | card 的 description 部件 font-size 覆盖槽。 | | `--xh-card-description-leading` | `description` | `line-height` | `default` | `--xh-leading-normal` | card 的 description 部件 line-height 覆盖槽。 | | `--xh-card-fg` | `root` | `color` | `default` | `--xh-fg-default` | card 的 root 部件 color 覆盖槽。 | | `--xh-card-font-size` | `root` | `font-size` | `default` | `--xh-text-label-size` | card 的 root 部件 font-size 覆盖槽。 | | `--xh-card-footer-gap` | `footer` | `gap` | `default` | `--xh-space-2` | card 的 footer 部件 gap 覆盖槽。 | | `--xh-card-gap` | `root` | `gap` | `default` | `--xh-space-3` | card 的 root 部件 gap 覆盖槽。 | | `--xh-card-leading` | `root` | `line-height` | `default` | `--xh-text-body-leading` | card 的 root 部件 line-height 覆盖槽。 | | `--xh-card-p` | `root` | `padding` | `default` | `--xh-surface-pad-lg` | card 的 root 部件 padding 覆盖槽。 | | `--xh-card-radius` | `root` | `border-radius` | `default` | `--xh-shape-surface` | card 的 root 部件 border-radius 覆盖槽。 | | `--xh-card-shadow` | `root` | `box-shadow` | `default`
`variant=ghost`
`variant=subtle` | `--xh-elevation-raised`
`none` | card 的 root 部件 box-shadow 覆盖槽。 | | `--xh-card-title-fg` | `title` | `color` | `default` | `--xh-fg-default` | card 的 title 部件 color 覆盖槽。 | | `--xh-card-title-font-size` | `title` | `font-size` | `default` | `--xh-text-label-size` | card 的 title 部件 font-size 覆盖槽。 | | `--xh-card-title-font-weight` | `title` | `font-weight` | `default` | `--xh-font-weight-semibold` | card 的 title 部件 font-weight 覆盖槽。 | | `--xh-card-title-leading` | `title` | `line-height` | `default` | `--xh-leading-relaxed` | card 的 title 部件 line-height 覆盖槽。 | ### 动效 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 --- 来源:https://ui.docs.xihanfun.com/components/carousel # Carousel 走马灯 在同一块区域内轮播若干张内容,一次显示一屏。 ## 用法 张数由 slideCount 声明而不是从 DOM 计数,页数与指示点数量都由它计算 ```vue ``` ```html
设计系统 一套视觉语言 令牌、皮肤与组件共享同一组设计决策。
无障碍 键盘与读屏一致 交互状态由无头内核统一维护。
跨框架 Vue、React 与 Web Components 同一份行为契约,对齐三种渲染方式。
``` ## 组件结构 加粗的是必需部件。 `data-scope="carousel"`:**`root`** · **`viewport`** · **`list`** · `item` · `prev-trigger` · `next-trigger` · `autoplay-trigger` · `indicator-group` · `indicator` ## 示例 ### 受控 传入 page 后由宿主决定,组件只发 page-change 不自行修改页码,宿主写回后才变化 ```vue ``` ```html ``` ### 一屏多张 slidesPerPage 决定一屏显示几张,一次翻几张默认跟随它,因此仍是整屏翻页 ```vue ``` ```html
第一张
第二张
第三张
第四张
第五张
第六张
``` ### 自动播放与暂停 autoplay 传毫秒即间隔;开启后必须渲染播放开关,自动翻页必须能够停止 ```vue ``` ```html
公告一
公告二
公告三
``` ### 纵向轨道 orientation 换为 vertical 后轨道竖向位移,两端按钮落到上下两端,翻页识别上下方向键 ```vue ``` ```html
09:00 晨会 同步今天的目标与阻塞项。
11:00 客户沟通 确认需求范围与交付节奏。
15:00 联调 核对三端行为与视觉结果。
``` ### 指针拖拽 allowPointerDrag 开启后按住轨道即可拖动,松手落回整页;关闭则只有触摸的原生滚动 ```vue ``` ```html
拖我
再拖
还能拖
最后一张
``` ### 指示点悬停切页 指示点上补一个原生 mouseenter 即为悬停切页,组件自带的点击翻页照常 ```vue ``` ```html
城市夜景
海岸线
雪山
沙漠
鼠标扫过下面的圆点即可换页,当前第 1 / 4 页
``` ### 一次移动一张 slidesPerMove 与 slidesPerPage 分开提供:一屏显示三张、一次只移动一张,页数按剩余张数重新计算 ```vue ``` ```html
A
B
C
D
E
F
``` ### 更换过渡效果 条目的内联样式只有尺寸与间距,位移之外的表现全部归作者:把条目叠放后按当前页调整透明度与缩放,翻页、键盘与指示点一概照常 ```vue ``` ```html
城市夜景
海岸线
雪山
沙漠
``` ## 设计指引 ### 何时使用 - 首屏营销位、图片画廊等内容并列且用户不需要全部浏览的场景。 ### 何时不用 - 每一张都需要被看到时,并排铺开或使用[列表](./list);轮播中第二张之后的点击率很低。 - 内容是导航入口。 ### 特性 - `slidesPerPage` 与 `slidesPerMove` 分开:可以一屏三张、一次移动一张。 - 支持纵向轨道、指针拖拽、循环与自动播放。 - 拖拽松手后轨道带着松手速度落到目标页:轻甩一下也能翻页,往回甩则收回;不循环时首末页往外拖越拉越沉,松手弹回。 - 分页点为 8px 圆点,当前页拉长为 20px 品牌胶囊;自动播放时胶囊按停留间隔显示进度,临时暂停时同步冻结。 - 指示点可以配置为悬停即切页。 - 应用设为 `data-material="liquid"` 时,翻页与播放钮换成液态面,分页点托在一条液态胶囊上:按下层换色调,压在任何媒体上都看得清;按住控制钮时液面随手指形变。 ### 组合 - 每一张放[图片](./image)或[卡片](./card)。 ### 最佳实践 - 开启自动播放时渲染 `autoplay-trigger`:它是唯一能停止自动翻页且不会被其他交互重新启动的入口。 - 自动播放在指针悬停或焦点进入时自动暂停,离开后重新计满一个间隔再翻页。 - 减弱动效时自动播放不会自行启动,播放开关是用户唯一的启动入口。 - 分页点应能看出总屏数与当前位置;自动播放时还应反馈本页剩余时间。翻页与播放按钮走 Action Control floating 档:48px 圆形磨砂面、图标 24px,按下缩放并换底。 ### 反模式 - 自动播放且不能暂停,阅读较慢的用户无法读完。 - 把关键信息或唯一的行动入口放在第三张之后。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCarouselAutoplayTrigger` `XhCarouselIndicator` `XhCarouselIndicatorGroup` `XhCarouselItem` `XhCarouselList` `XhCarouselNextTrigger` `XhCarouselPrevTrigger` `XhCarouselRoot` `XhCarouselViewport` | | 组合式函数 | `useCarousel` | | 状态机 | `carouselMachine` | | 皮肤 | `@xihan-ui/styles/carousel.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `page` | `number` | | 当前页,0 基。提供即受控:内部不再自行修改,只发 onPageChange。 页不等于张:一页可能同时显示多张(见 slidesPerPage)。 | | `defaultPage` | `number` | | 非受控初始页,默认 0。 | | `slideCount` | `number` | | 条目总数,由作者声明,不从 DOM 统计。 | | `slidesPerPage` | `number` | | 一屏显示的张数,默认 1。 | | `slidesPerMove` | `number` | | 一次翻动的张数,默认跟随 slidesPerPage(整屏翻页)。 | | `orientation` | `Orientation` | | 轨道方向,默认 horizontal;方向键的轴随之变化。 | | `dir` | `Direction` | | 文字方向。水平轴上同时作用于排版与位移方向:rtl 下下一张在左侧, 轨道也向正方向位移。纵向轨道不受影响。 | | `loop` | `boolean` | | 到达末尾是否回绕,默认 false。 | | `autoplay` | `boolean \| number` | | 自动播放。true 使用默认间隔,数值即毫秒间隔;未提供 / false / 非正数一律不自动播放。 指针悬停或轮播内任一节点获得焦点时暂停计时,离开后重新计满一个完整间隔再翻页。 减弱动效档下不自动起播:提供间隔也停在 idle,需要由用户按下播放开关。 | | `allowPointerDrag` | `boolean` | | 允许指针拖拽切页,默认 false。鼠标、触摸、触控笔一并门控。 开启后沿轨道轴的原生滚动让位给拖拽,关闭则完全没有拖拽、触摸使用原生滚动。 | | `spacing` | `string` | | 张与张之间的间距,任意 CSS 长度(如 '12px')。落为条目自身的内边距,不影响位移计算。 | | `translations` | `Partial` | | | | `onPageChange` | `(details: CarouselPageChangeDetails) => void` | | 页码变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `page-change` | `CarouselPageChangeDetails` | 页码变化;detail 为 `{ page: number }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCarouselAutoplayTrigger` | `default` | `{ stopped: boolean }` | | | `XhCarouselRoot` | `default` | `CarouselRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCarouselAutoplayTrigger` | `children` | `SlotChildren` | | | | `XhCarouselIndicator` | `index` | `number \| string` | 是 | 指示点对应的页码,0 基;兼收字符串。 | | `XhCarouselItem` | `index` | `number \| string` | 是 | 该张的下标,0 基;兼收字符串。 | | `XhCarouselRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `autoplay-trigger` | 'paused' \| 'running' | 以下名称仅用于内部状态机。 **状态**:`idle` · `playing` · `playing.running` · `playing.paused` **事件**:`PAGE.SET` · `PAGE.PREV` · `PAGE.NEXT` · `AUTOPLAY.START` · `AUTOPLAY.STOP` · `AUTOPLAY.PAUSE` · `AUTOPLAY.RESUME` · `after.autoplay` · `DRAG.START` · `DRAG.MOVE` · `DRAG.END` · `PRESS.START` · `PRESS.END` **判据**:`isLastPauseSource` · `canAdvance` · `hasAutoplay` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `page` | `number` | 当前页,0 基;恒在 [0, max(totalPages-1, 0)] 内,slideCount 减小后也能读到可用的值。 | | `totalPages` | `number` | | | `slideCount` | `number` | 归一化后的条目总数(负数 / 小数 / 未提供都已收敛为非负整数)。 | | `slidesPerPage` | `number` | | | `slidesPerMove` | `number` | | | `orientation` | `Orientation` | | | `slideRange` | `{ start: number, end: number }` | 当前页显示的条目下标区间,0 基闭区间;没有条目时 end < start。 | | `pageSnapPoints` | `number[]` | 每一页的首张下标序列,长度即总页数。 | | `canScrollPrev` | `boolean` | | | `canScrollNext` | `boolean` | | | `autoplaying` | `boolean` | 自动播放的计时进行中。 | | `paused` | `boolean` | 自动播放已开启但被暂停(悬停 / 焦点 / 调用方)。 | | `autoplayStopped` | `boolean` | 自动播放当前是否由用户停止:计时未进行(idle),或由调用方暂停。 与 `paused` 的差别在于它不计入悬停与焦点两路:这两路一离开即自动恢复, 若用它驱动播放 / 暂停开关的名字与图形,鼠标一碰按钮就会在两态之间跳动。 | | `dragging` | `boolean` | | | `isInView` | `(index: number) => boolean` | | | `setPage` | `(page: number) => void` | 页码会被收敛进合法区间(loop 时回绕),越界入参不会写出越界的页。 | | `goToPrev` | `() => void` | | | `goToNext` | `() => void` | | | `play` | `() => void` | 开始自动播放;autoplay prop 未提供正的间隔时无操作。 | | `pause` | `() => void` | 暂停计时(来源记为 api),与悬停 / 焦点叠加计数。 | | `resume` | `() => void` | | | `getRootProps` | `() => T['element']` | | | `getViewportProps` | `() => T['element']` | | | `getListProps` | `() => T['element']` | | | `getItemProps` | `(props: CarouselItemProps) => T['element']` | | | `getPrevTriggerProps` | `() => T['button']` | | | `getNextTriggerProps` | `() => T['button']` | | | `getAutoplayTriggerProps` | `() => T['button']` | 播放 / 暂停开关。未配置自动播放(间隔为 0)时为原生 disabled。 | | `getIndicatorGroupProps` | `() => T['element']` | | | `getIndicatorProps` | `(props: CarouselIndicatorProps) => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `ArrowRight` | orientation=horizontal,焦点在轮播内 | 翻到下一页;rtl 下反向(走上一页) | | `ArrowLeft` | orientation=horizontal,焦点在轮播内 | 翻到上一页;rtl 下反向(走下一页) | | `ArrowDown` | orientation=vertical,焦点在轮播内 | 翻到下一页;横轨下不接管,放行给页面滚动 | | `ArrowUp` | orientation=vertical,焦点在轮播内 | 翻到上一页;横轨下不接管,放行给页面滚动 | | `Home` | 焦点在轮播内 | 跳到第一页 | | `End` | 焦点在轮播内 | 跳到最后一页 | | `Enter` / `Space` | 焦点在上一张 / 下一张按钮上 | 翻一页;由原生按钮的激活行为负责 | | `Enter` / `Space` | 焦点在指示点上 | 跳到该指示点对应的页;由原生按钮的激活行为负责 | | `Enter` / `Space` | held on prev-trigger / next-trigger / autoplay-trigger / indicator, 该按钮未禁用 | 按住期间该按钮投影 data-pressed,与指针 :active 同一副按压面;抬起、失焦或按住途中转禁用(翻到边界、关掉 loop、去掉 autoplay)撤下。翻页与播放 / 暂停照旧由这一次按键的原生激活承担 | | `Tab` / `Shift+Tab` | 任意时刻 | 在两端按钮与各指示点之间逐个停靠;到端点后禁用的按钮自动脱序 | | `方向键` | 焦点在幻灯片内的输入控件上 | 不接管:交还给控件自己做光标移动 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-label` | label.root | | `root` | `aria-roledescription` | 'carousel' | | `root` | `role` | 'region' | | `viewport` | `aria-atomic` | 'false' | | `viewport` | `aria-live` | 'off' \| 'polite' | | `item` | `aria-label` | label.item(index + 1, slideCount) | | `item` | `aria-roledescription` | 'slide' | | `item` | `role` | 'group' | | `prev-trigger` | `aria-controls` | `viewport` 部件的 id | | `prev-trigger` | `aria-label` | label.prevTrigger | | `next-trigger` | `aria-controls` | `viewport` 部件的 id | | `next-trigger` | `aria-label` | label.nextTrigger | | `autoplay-trigger` | `aria-controls` | `viewport` 部件的 id | | `autoplay-trigger` | `aria-label` | label.autoplayTriggerPlay \| label.autoplayTriggerPause | | `indicator-group` | `aria-label` | label.indicatorGroup | | `indicator-group` | `role` | 'group' | | `indicator` | `aria-current` | 'true' \| 'false' | | `indicator` | `aria-label` | label.indicator(index + 1) | ## 样式参考 ### 皮肤 `@xihan-ui/styles/carousel.css` 使用 `[data-scope="carousel"][data-part="root"]` 部件选择器,位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-autoplay` | ''(条件成立时才出现) | | `root` | `data-dragging` | ''(条件成立时才出现) | | `root` | `data-orientation` | props.orientation | | `root` | `data-paused` | ''(条件成立时才出现) | | `viewport` | `data-dragging` | ''(条件成立时才出现) | | `viewport` | `data-orientation` | props.orientation | | `list` | `data-animating` | ''(条件成立时才出现) | | `list` | `data-dragging` | ''(条件成立时才出现) | | `list` | `data-orientation` | props.orientation | | `item` | `data-index` | String(index) | | `item` | `data-inview` | ''(条件成立时才出现) | | `item` | `data-orientation` | props.orientation | | `prev-trigger` | `data-disabled` | ''(条件成立时才出现) | | `prev-trigger` | `data-orientation` | props.orientation | | `prev-trigger` | `data-pressed` | ''(条件成立时才出现) | | `prev-trigger` | `data-xh-action-control` | '' | | `prev-trigger` | `data-xh-action-display` | 'always' | | `prev-trigger` | `data-xh-action-profile` | 'floating' | | `prev-trigger` | `data-xh-action-size` | 'md' | | `prev-trigger` | `data-xh-liquid` | '' | | `prev-trigger` | `data-xh-material` | 'frosted' | | `next-trigger` | `data-disabled` | ''(条件成立时才出现) | | `next-trigger` | `data-orientation` | props.orientation | | `next-trigger` | `data-pressed` | ''(条件成立时才出现) | | `next-trigger` | `data-xh-action-control` | '' | | `next-trigger` | `data-xh-action-display` | 'always' | | `next-trigger` | `data-xh-action-profile` | 'floating' | | `next-trigger` | `data-xh-action-size` | 'md' | | `next-trigger` | `data-xh-liquid` | '' | | `next-trigger` | `data-xh-material` | 'frosted' | | `autoplay-trigger` | `data-disabled` | ''(条件成立时才出现) | | `autoplay-trigger` | `data-pressed` | ''(条件成立时才出现) | | `autoplay-trigger` | `data-state` | 'paused' \| 'running' | | `autoplay-trigger` | `data-xh-action-control` | '' | | `autoplay-trigger` | `data-xh-action-display` | 'always' | | `autoplay-trigger` | `data-xh-action-profile` | 'floating' | | `autoplay-trigger` | `data-xh-action-size` | 'md' | | `autoplay-trigger` | `data-xh-liquid` | '' | | `autoplay-trigger` | `data-xh-material` | 'frosted' | | `indicator-group` | `data-orientation` | props.orientation | | `indicator-group` | `data-xh-liquid` | '' | | `indicator` | `data-current` | ''(条件成立时才出现) | | `indicator` | `data-index` | String(index) | | `indicator` | `data-pressed` | ''(条件成立时才出现) | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-carousel-control-inset` | `autoplay-trigger`
`indicator-group`
`next-trigger`
`prev-trigger`
`root` | `inset-block-end`
`inset-block-start`
`inset-inline-end`
`inset-inline-start` | `default`
`orientation=vertical` | `--xh-space-3` | carousel 的 autoplay-trigger、indicator-group、next-trigger、prev-trigger、root 部件 inset-block-end、inset-block-start、inset-inline-end、inset-inline-start 覆盖槽。 | | `--xh-carousel-duration` | `list` | `transition` | `default` | `--xh-motion-duration-slide` | carousel 的 list 部件 transition 覆盖槽。 | | `--xh-carousel-ease` | `list` | `transition` | `default` | `--xh-motion-ease-slide` | carousel 的 list 部件 transition 覆盖槽。 | | `--xh-carousel-icon-size` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 --xh-icon-size 覆盖槽。 | | `--xh-carousel-indicator-bg` | `indicator` | `background` | `@media (pointer: coarse)`
`default` | `--xh-bg-subtle-hover-opaque` | carousel 的 indicator 部件 background 覆盖槽。 | | `--xh-carousel-indicator-bg-active` | `indicator` | `background` | `@media (pointer: coarse)`
`current`
`is(:active, [data-pressed])`
`not([data-current])`
`pressed` | `--xh-fg-default` | carousel 的 indicator 部件 background 覆盖槽。 | | `--xh-carousel-indicator-bg-hover` | `indicator` | `background` | `current`
`hover`
`not([data-current])` | `--xh-fg-muted` | carousel 的 indicator 部件 background 覆盖槽。 | | `--xh-carousel-indicator-bg-selected` | `indicator`
`root` | `background` | `@media (pointer: coarse)`
`autoplay`
`current`
`not([data-autoplay], [data-paused])`
`paused` | `--xh-bg-brand` | carousel 的 indicator、root 部件 background 覆盖槽。 | | `--xh-carousel-indicator-bg-selected-active` | `indicator` | `background` | `@media (pointer: coarse)`
`current`
`is(:active, [data-pressed])`
`pressed` | `--xh-bg-brand-active` | carousel 的 indicator 部件 background 覆盖槽。 | | `--xh-carousel-indicator-bg-track` | `indicator`
`root` | `background` | `@media (pointer: coarse)`
`autoplay`
`current`
`paused` | `--xh-bg-brand-subtle` | carousel 的 indicator、root 部件 background 覆盖槽。 | | `--xh-carousel-indicator-fg-selected` | `indicator`
`root` | `color` | `autoplay`
`current`
`not([data-autoplay], [data-paused])`
`paused` | `--xh-fg-on-brand` | carousel 的 indicator、root 部件 color 覆盖槽。 | | `--xh-carousel-indicator-gap` | `indicator-group` | `gap` | `default` | `--xh-space-1` | carousel 的 indicator-group 部件 gap 覆盖槽。 | | `--xh-carousel-indicator-group-p` | `indicator-group` | `padding` | `material=liquid`
`where([data-material='liquid'])`
`xh-liquid` | `--xh-space-2` | carousel 的 indicator-group 部件 padding 覆盖槽。 | | `--xh-carousel-indicator-group-radius` | `indicator-group` | `border-radius` | `material=liquid`
`where([data-material='liquid'])`
`xh-liquid` | `--xh-shape-pill` | carousel 的 indicator-group 部件 border-radius 覆盖槽。 | | `--xh-carousel-indicator-inset` | `indicator-group` | `inset-block-end` | `orientation=horizontal` | `--xh-space-3` | carousel 的 indicator-group 部件 inset-block-end 覆盖槽。 | | `--xh-carousel-indicator-radius` | `indicator` | `border-radius` | `@media (pointer: coarse)`
`default` | `--xh-shape-circle` | carousel 的 indicator 部件 border-radius 覆盖槽。 | | `--xh-carousel-indicator-radius-current` | `indicator`
`root` | `border-radius` | `@media (pointer: coarse)`
`autoplay`
`current`
`paused` | `--xh-shape-pill` | carousel 的 indicator、root 部件 border-radius 覆盖槽。 | | `--xh-carousel-indicator-size` | `indicator`
`indicator-group`
`root` | `block-size`
`inline-size` | `@media (pointer: coarse)`
`autoplay`
`current`
`default`
`orientation=vertical`
`paused` | `--xh-space-2` | carousel 的 indicator、indicator-group、root 部件 block-size、inline-size 覆盖槽。 | | `--xh-carousel-indicator-size-current` | `indicator`
`indicator-group`
`root` | `block-size`
`inline-size` | `@media (pointer: coarse)`
`autoplay`
`current`
`orientation=vertical`
`paused` | `--xh-space-5` | carousel 的 indicator、indicator-group、root 部件 block-size、inline-size 覆盖槽。 | | `--xh-carousel-indicator-target-size` | `indicator`
`root` | `min-block-size`
`min-inline-size` | `@media (pointer: coarse)`
`autoplay`
`current`
`hover`
`is(:active, [data-pressed])`
`not([data-autoplay], [data-paused])`
`not([data-current])`
`paused`
`pressed` | `44px` | carousel 的 indicator、root 部件 min-block-size、min-inline-size 覆盖槽。 | | `--xh-carousel-trigger-bg` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `--xh-ink-surface`
`background-color` | `default`
`disabled`
`focus-visible`
`xh-ink-surface` | `--xh-_material-bg`
`--xh-_material-bg-focus` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-carousel-trigger-bg-active` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_material-bg-pressed`
`--xh-bg-subtle-hover-opaque` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 background-color 覆盖槽。 | | `--xh-carousel-trigger-bg-hover` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_material-bg-hover`
`--xh-bg-subtle-opaque` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 background-color 覆盖槽。 | | `--xh-carousel-trigger-border` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `border`
`border-color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_material-border` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 border、border-color 覆盖槽。 | | `--xh-carousel-trigger-fg` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_material-fg` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 color 覆盖槽。 | | `--xh-carousel-trigger-radius` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `border-radius` | `default` | `--xh-_action-profile-radius` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 border-radius 覆盖槽。 | | `--xh-carousel-trigger-shadow` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `box-shadow` | `default`
`disabled`
`focus-visible`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_material-shadow` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 box-shadow 覆盖槽。 | | `--xh-carousel-trigger-shadow-hover` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `box-shadow` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_material-shadow` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 box-shadow 覆盖槽。 | | `--xh-carousel-trigger-size` | `autoplay-trigger`
`next-trigger`
`prev-trigger` | `block-size`
`inline-size` | `default`
`xh-action-profile=floating` | `--xh-_action-profile-visual-size` | carousel 的 autoplay-trigger、next-trigger、prev-trigger 部件 block-size、inline-size 覆盖槽。 | | `--xh-carousel-viewport-radius` | `viewport` | `border-radius` | `default` | `--xh-shape-surface` | carousel 的 viewport 部件 border-radius 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 切换 · 导航(见[动效规范](../design/motion#角色))。 可覆盖的动效槽:`--xh-carousel-duration` · `--xh-carousel-ease`。 关键帧 `xh-carousel-indicator-progress` 随皮肤自带,不引用别处文件里的名字;`background-color` · `block-size` · `inline-size` · `scale` · `translate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:内核按组件所在的作用域判断减弱动效(最近的 `data-motion`、应用级覆盖、系统偏好),据此决定要不要动。 `prefers-reduced-motion: reduce` 下本组件另有降级规则;内核驱动的那段不经令牌层,由内核按元素判断后自行降级。 ### 响应式 皮肤另按输入能力分档:`pointer: coarse`:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/cartesian-chart # CartesianChart 直角坐标图 在直角坐标系里画柱与折线:一根自变量轴(类目、数值或时间),一根数值轴,任意多个系列共用这两根轴。柱状图、条形图、分组与堆叠柱、折线、面积与堆叠面积都是它的不同配置,不是不同的组件。 ## 用法 一个柱系列:x 取类目字段,y 取数值字段,悬停或用方向键逐个查看 ```vue ``` ```html
月度销售额
``` ## 组件结构 加粗的是必需部件。 `data-scope="cartesian-chart"`:**`root`** · `caption` · `legend` · `legend-item` · `legend-swatch` · `legend-label` · **`viewport`** · **`plot`** · `grid` · `grid-line` · `axis` · `axis-line` · `tick` · `tick-label` · `axis-title` · `series` · `bar` · `line` · `area-fill` · `dot` · `point` · `data-label` · `total-label` · `end-label` · `leader-line` · `crosshair` · `focus-ring` · `tooltip` · `tooltip-header` · `tooltip-row` · `tooltip-swatch` · `tooltip-value` · `tooltip-name` · `empty` · `summary` · `table` ## 示例 ### 多系列折线 三条折线共用坐标轴,图例点一下隐藏或恢复一个系列,其余系列颜色不变 ```vue ``` ```html
各端月活跃用户(千人)
``` ### 堆叠柱 同一个 stack 名的柱系列首尾相接,柱高是各部分之和,相邻两段之间留一道表面缝 ```vue ``` ```html
各渠道季度营收(万元)
``` ### 横向条形图 orientation="horizontal" 把整张图转置:类目名竖排可以读全,数值横向延伸 ```vue ``` ```html
本月各团队工单数
``` ### 百分比堆叠面积 stackOffset: 'expand' 把每个键归一到 100%,看的是构成随时间的变化而不是总量 ```vue ``` ```html
访问来源构成
``` ### 时间轴 自变量给 Date,横轴换成时间比例尺:刻度按日期取整,间隔不均的日期按真实间距排开 ```vue ``` ```html
日订单量
``` ### 语义系列 颜色本身带好坏含义时写 tone:收入取成功色、支出取危险色,不再按次序取分类色 ```vue ``` ```html
月度收支(万元)
``` ### 两张图联动 两个量纲不共用一根 y 轴:两张图接到同一个受控的 activeKey,在同一个键上一起指示 ```vue ``` ```html
周订单量
周转化率
``` ### 数据更新 换一组数据时柱从当前高度走到新高度,柱端的数随之滚动;关掉动画后直接画终态 ```vue ``` ```html
``` ### 数据标签与合计 labels="inside" 把每一段的数写在柱内,totals 在整叠外侧写合计;段太矮放不下时不写 ```vue ``` ```html
各季度销售额(万元)
``` ### 线尾标签 endLabel 把系列名与末值写在线尾,末端挨着时上下推开、用引导线连回线尾;折线不多时读者不用对照图例 ```vue ``` ```html
各端月活跃用户(千人)
``` ## 设计指引 ### 何时使用 - 比较若干类目上的数值:各月销售额、各渠道转化量。 - 观察一个量随时间的变化趋势,或几个量的走势是否同步。 - 查看整体由哪几部分构成、各部分占比如何随类目变化(堆叠、百分比堆叠)。 - 类目名较长、或类目较多需要竖向滚读时,用横向的条形图。 ### 何时不用 - 只报告一个数,或一个数与它的同比:使用[统计数值](./statistic)。 - 少量类目在整体中的占比:使用饼图。 - 类目多于 7 个且每个都需要读出准确数字:使用[表格](./table),或表格与图并列。 - 两个量纲(如金额与转化率):画两张图并用 `activeKey` 联动,不在一张图里放第二根 y 轴。两根 y 轴的刻度零点与比例都可以任意选,读者会把两条线的交叉读成含义,而它只是刻度选择的巧合。 - 看强度分布而不是具体数值:使用[热力图](./heatmap)。 ### 特性 - 系列用 `mark` 区分画法,目前有 `bar` 与 `line` 两种。每个系列用字段名把数据的列映射到通道:`x` 是自变量,`y` 是数值。同一张图可以混放柱与折线,它们共用坐标轴。 - 数据是对象数组,组件只读不写。系列 `id` 缺省取 `y` 的字段名,`name` 缺省同 `id`;图例、提示框与数据表显示 `name`,`hiddenSeries` 与部件上的 `data-series-id` 使用 `id`。 - `x` 是自变量轴、`y` 是数值轴,与屏幕方向无关。`orientation="horizontal"` 把整张图转置:自变量竖排、数值横向延伸,即条形图;`xAxis` / `yAxis` 的配置不用跟着对调。 - 比例尺缺省按数据推断:含柱系列或自变量不是数字与日期时为 `band`(类目),自变量是 `Date` 时为 `time`,是数字时为 `linear`;数值轴为 `linear`。`scale` 可显式指定 `band` / `point` / `linear` / `log` / `time` / `utc`。`log` 的定义域必须全为正数,否则报错并在根上写 `data-state="error"`。 - 类目轴的顺序缺省是数据中首次出现的顺序,`xAxis.domain` 可给出显式顺序;只在 `domain` 里、数据中没有的类目也会占位。 - 有柱系列时数值轴强制包含 0:柱的长度就是它编码的量,基线不在 0 时长度之比不再等于数值之比。只有折线时 `zero` 缺省不强制,定义域贴合数据;需要从 0 起时写 `yAxis.zero`。 - 数值轴两端缺省取整到刻度上(`nice`),刻度数量按绘图区长度估算:竖向的数值轴约每 2.5 行字高一个,横向的按最宽的刻度标签加间隙估算;`ticks` 可以给数量提示或显式的刻度值。 - 同一 `stack` 名的系列堆叠在一起:柱逐段累加,折线成为堆叠面积。`stackOffset: 'expand'` 把每个键归一成百分比,数值轴随之换成百分比格式;柱的堆叠含负值时缺省 `diverging`,正值向上、负值向下各自累加。同一堆叠组的 `stackOffset` 必须一致,不一致时报错。 - 多个柱系列不堆叠时并排分组:组内按系列次序排列,柱的厚度不超过 `--xh-chart-bar-max`(缺省 24px),类目很宽时柱不会被拉成大色块,多出的空间留作类目之间的间距。 - 堆叠的相邻两段之间留 `--xh-chart-gap`(2px)的表面缝,靠缝区分而不是靠描边。只有离基线最远的一端有圆角,基线一端始终是直角,读者据此判断柱是从哪里长出来的。 - 折线的 `curve` 缺省 `linear`;`monotone` 平滑且不越过数据点,不会画出数据中没有的峰谷;`step` / `step-before` / `step-after` 画成阶梯,台阶分别落在两点正中、前一点与后一点处,适合价格、库存这类在某一刻跳变的量。`area` 在折线下铺一层系列色的淡洗。缺失值(`null`、`undefined`、`NaN`)处折线断开,`connectNulls` 可改为连上。 - 折线的数据点 `symbols` 缺省 `auto`:相邻点间距不小于 16px 时才画,点密到连成一片时不画。键盘聚焦或悬停到折线上的数据时,那一个点总会画出来作为指示与焦点落点。 - 颜色按系列次序依次取分类色 1–8;`slot` 可把一个系列固定在某一色槽,同一业务实体在不同图表里保持同色。图例把某个系列隐藏后,其余系列的颜色不变。颜色本身带有好坏含义时(收入与支出、达标与超标)改写 `tone`,系列改用语气色;同一张图不混用分类色与语气色。 - 系列多于 8 个时报错:分类色只有 8 个可区分的色槽,第 9 个开始会与前面的系列撞色。需要更多系列时先合并或分成几张图。 - 数据标签由系列的 `labels` 打开:柱写 `inside`(柱内居中)或 `end`(柱的远端外侧,负值翻到另一侧;堆叠中的段写在段内的远端),折线写 `end`(每个点的上方)。柱内的字取与色槽配对的前景色;放不下、与更要紧的标签重叠时不写。柱端外侧的标签写在绘图区里:数值轴两端各收进一截,最高的那根柱上面也有地方写。 - `totals` 让每个堆叠组在整叠外侧写出合计,含负值时正负两端各写一个;百分比堆叠不写合计。 - 折线的 `endLabel` 在线尾写系列名与末值,几条线的末端挤在一起时上下推开,推开的标签用引导线连回线尾,挤不下去掉末值最小的;右边留出它要的地方。 - 标签按重要性落位:合计最先,其次线尾标签,最后逐个数据的标签;它们都只给眼睛看,数值由每个数据的可及名、摘要与数据表承担。 - 提示框缺省 `trigger="axis"`:指针吸附到最近的键,列出该键上全部可见系列;`trigger="item"` 只报告指针命中的那一个数据。键盘聚焦与指针悬停显示同样的内容。`tooltipOrder` 改变提示框里各系列的行序:缺省 `series` 按图例次序,`descending` / `ascending` 按数值排,缺失值排在最后;回调里的 `items` 仍按图例次序。 - 悬停图例项时,其余系列淡出到 `--xh-chart-dim-alpha`,该系列颜色不变;`trigger="item"` 时悬停或聚焦某个数据同样只保留它所在的系列。`axis` 模式不淡出:提示框列出的正是该键上的全部系列。 - 提示框放在根内部,按指针所在的一侧翻转,不越出绘图区;它不进入浮层引擎,不参与浮层的层级与关闭协议。 - 坐标轴标签字体、柱的最大厚度、线宽、点的直径等几何量的真源是 CSS 组件槽:组件从根的计算样式读取它们再计算几何,改写组件槽就能改变几何,不需要布局属性。密度档切换时重新读取。 - 尺寸由视口决定:宽度随容器,高度取 `--xh-cartesian-chart-height`(缺省 `--xh-chart-height`)。视口尺寸变化时重新布局,服务端与首帧只输出空的绘图区、不占位跳动。 - `pending` 表示正在重新取数:保留上一帧、整体降低不透明度并在根上写 `aria-busy`,不闪骨架,也不跳布局。首次取数、手里还没有数据时,空态写 `translations.loadingText`(缺省 Loading…)并转一个圈,取完仍没有数据才写 `emptyText`。 - 首次出现时播放入场:柱沿数值轴从基线长出,折线从头描到尾,数据点等笔尖扫到才出现,面积、坐标轴与标签淡入,多个系列按图例次序错开(至多 5 步)。数据层的标记多于 1000 个时不做几何插值,只淡入淡出。之后的数据变化与图例切换从当前位置插值到新位置:留下的柱原地伸缩,新增的柱从基线长出,隐藏的系列收回基线并淡出后才移除;坐标轴刻度随之移动,数据标签、合计与线尾标签上的数从旧值滚到新值。标记按自变量的值对齐而不是按位置:类目换了次序时柱滑到新位置,时间序列往后推一格时整条线平移、新点从边上进来。`pending` 结束后到来的新数据按更新处理,不再重播入场。 - `animated={false}`(Web Components 写 `animated="false"`)关闭过渡,数据一变直接画终态。系统开了减弱动效或容器写了 `data-motion="reduce"` 时几何直接到位,只保留淡入淡出。视口尺寸变化与字体加载完成后的重排不播过渡。 - 过渡的快慢由动效令牌决定,组件从绘图区的计算样式读取:入场取 `--xh-motion-duration-reveal`(缺省 640ms),数据更新与图例切换取 `--xh-motion-duration-morph`(缺省 400ms)。在图或它的容器上改写它们,例如 `style="--xh-motion-duration-reveal: 1s"`,只影响这张图;入场的曲线取 `--xh-motion-ease-enter-strong`,更新取 `--xh-motion-ease-continuous`。折线的描出与入场同一个时长。 - 没有数据或全部系列被隐藏时显示空态,文字取 `translations.emptyText`;坐标轴在全部隐藏时保留,图例仍可把系列点回来。 - 多张图接到同一个受控的 `activeKey` 上时,十字准线与提示框在同一个键上一起指示。从外部写入的键不触发 `onDatumActive`,只有本图上的指针与键盘才触发,联动不会来回回调。 - `onDatumPress` 只报告被点击或按下 Enter / Space 的数据,图表不内建选中态。 - 三个适配器的作者侧写法不同,最终 DOM 一致:Vue 与 React 不写默认内容时铺开缺省结构(标题、图例、视口与绘图区、空态、提示框),提示框内容可由作用域插槽 / 函数式 children 替换;Web Components 侧作者写外壳(root、caption、legend、viewport 与其中空的 `` plot、tooltip,可选 empty),网格、坐标轴、系列、图例项与提示框的缺省内容由元素生成进去,按标记的 key 复用节点。 - Web Components 侧的数据、系列与坐标轴是对象,只走 JS property;朝向、提示框模式、`pending` 与 `locale` 另有同名属性,类目键的 `activeKey` 可写成 `active-key` 属性,数值与日期键走 property。宿主元素缺省是行内元素,放进 flex / grid 时要给它一个宽度,否则图按标题与图例的宽度收窄。 - 摘要与数据表由组件追加在根的末尾,三个适配器都一样,不需要作者放置。 ### 最佳实践 - 为图写标题:`caption` 是图的可访问名称,也是读者判断这张图在说什么的第一处。 - 折线不超过 4 条时,用 `endLabel` 把系列名写在线尾,读者不用在图例与线之间来回对照。 - 只在读者要读出准确数字时打开数据标签;每根柱都写数的图更像一张表,这时直接给表格。 - 类目名较长时改用横向条形图,而不是让横轴标签斜着排:竖排的类目名可以完整读出。 - 柱状图的类目顺序本身就是信息:没有自然顺序(如月份)的类目按数值排序后再给组件,组件不排序。 - 一张图的系列保持在 5 个以内,超过时读者需要反复对照图例。系列之间需要逐个比较时考虑分成几张小图。 - 折线图的自变量是时间时给 `Date`,不要先格式化成字符串:字符串会被当成类目,间隔不均匀的日期会被画成等距。 - 堆叠只在各部分之和有意义时使用:堆叠后只有最底下一段和总量能准确比较,中间各段的起点不齐,不适合逐段比较。 - 需要可见的表格视图时,把 `api.table` 交给[表格](./table)组件,而不是在图下方另写一份数据。 ### 反模式 - 用双 y 轴在一张图里放两个量纲:两根轴的刻度可以任意选,交叉点没有含义。 - 柱状图的数值轴不从 0 起:组件已强制包含 0,不要用 `min` 把它截掉。 - 用颜色区分同一个系列里的类目:颜色区分的是系列,类目已在坐标轴上。 - 用提示框作为读取数值的唯一途径:提示框对读屏隐藏,数值要能从坐标轴、摘要或数据表读到。 - 把几十个系列放进同一张图,再靠图例逐个点开:这时需要的是表格或筛选。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCartesianChartCaption` `XhCartesianChartEmpty` `XhCartesianChartLegend` `XhCartesianChartPlot` `XhCartesianChartRoot` `XhCartesianChartTooltip` `XhCartesianChartViewport` | | 组合式函数 | `useCartesianChart` | | 状态机 | `cartesianChartMachine` | | 皮肤 | `@xihan-ui/styles/cartesian-chart.css` | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `hidden-series-change` | `ChartHiddenSeriesChangeDetails` | 图例切换显隐;detail 为 `{ hiddenSeries: string[] }` | | `active-key-change` | `ChartActiveKeyChangeDetails` | 指针或键盘换了激活的键;detail 为 `{ activeKey }`,收起时为 null | | `datum-active` | `ChartDatumDetails` | 悬停或聚焦到某个数据;detail 为数据详情,收起时为 null | | `datum-press` | `ChartDatumDetails` | 指针点击、Enter 或 Space 按在某个数据上;detail 为数据详情 | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCartesianChartRoot` | `default` | `CartesianChartRootSlotProps` | 自行摆放部件;不写时铺开缺省结构:图例、视口(绘图区与空态)、提示框。 | | `XhCartesianChartRoot` | `caption` | — | 缺省结构里的标题内容。 | | `XhCartesianChartRoot` | `tooltip` | `CartesianChartTooltipSlotProps` | 缺省结构里的提示框内容。 | | `XhCartesianChartRoot` | `empty` | — | 缺省结构里的空态内容。 | | `XhCartesianChartTooltip` | `default` | `CartesianChartTooltipSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCartesianChartRoot` | `data` | `readonly ChartRow[]` | | 数据:对象数组,系列用字段名把列映射到通道。 | | `XhCartesianChartRoot` | `series` | `readonly CartesianSeries[]` | | | | `XhCartesianChartRoot` | `xAxis` | `CartesianAxis` | | | | `XhCartesianChartRoot` | `yAxis` | `CartesianAxis` | | | | `XhCartesianChartRoot` | `orientation` | `CartesianOrientation` | | 朝向,缺省 vertical。 | | `XhCartesianChartRoot` | `trigger` | `CartesianTrigger` | | 提示框汇报什么,缺省 axis。 | | `XhCartesianChartRoot` | `totals` | `boolean` | | 堆叠柱的合计:每个堆叠组在最外端写出合计。 | | `XhCartesianChartRoot` | `tooltipOrder` | `CartesianTooltipOrder` | | 提示框里各系列的行序,缺省 series(按图例次序)。 | | `XhCartesianChartRoot` | `hiddenSeries` | `string[]` | | 隐藏的系列(受控)。 | | `XhCartesianChartRoot` | `defaultHiddenSeries` | `string[]` | | 初始隐藏的系列(非受控)。 | | `XhCartesianChartRoot` | `activeKey` | `ChartKey \| null` | | 激活的自变量键(受控)。 | | `XhCartesianChartRoot` | `pending` | `boolean` | | 数据重取中:保留上一帧、整体降低不透明度。 | | `XhCartesianChartRoot` | `animated` | `boolean` | | 播放过渡动画,缺省 true;false 时直接画终态。 | | `XhCartesianChartRoot` | `locale` | `string` | | | | `XhCartesianChartRoot` | `translations` | `Partial` | | | | `XhCartesianChartRoot` | `onHiddenSeriesChange` | `CartesianChartProps['onHiddenSeriesChange']` | | | | `XhCartesianChartRoot` | `onActiveKeyChange` | `CartesianChartProps['onActiveKeyChange']` | | | | `XhCartesianChartRoot` | `onDatumActive` | `CartesianChartProps['onDatumActive']` | | | | `XhCartesianChartRoot` | `onDatumPress` | `CartesianChartProps['onDatumPress']` | | | | `XhCartesianChartRoot` | `caption` | `ReactNode` | | 缺省结构里的标题内容。 | | `XhCartesianChartRoot` | `renderTooltip` | `(props: CartesianChartTooltipSlotProps) => ReactNode` | | 缺省结构里的提示框内容。 | | `XhCartesianChartRoot` | `empty` | `ReactNode` | | 缺省结构里的空态内容。 | | `XhCartesianChartRoot` | `children` | `SlotChildren` | | 自行摆放部件;不写时铺开缺省结构:图例、视口(绘图区与空态)、提示框。 | | `XhCartesianChartTooltip` | `children` | `SlotChildren` | | 替换缺省内容;函数式 children 拿到激活的数据与缺省的内容模型。 | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'error' \| undefined | | `tooltip` | 'visible' \| 'hidden' | | `empty` | 'loading' \| undefined | 以下名称仅用于内部状态机。 **状态**:`idle` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `model` | `CartesianModel` | 管线产物:比例尺、布局、场景与无障碍模型。 | | `scene` | `Scene` | 要画的场景;尚未测量时为空场景。 | | `overlay` | `CartesianOverlay` | 前景层:随激活与聚焦变化的标记。绘图区按 back → under → data → over 的次序画: 十字准线与类目带淡底在数据之下,激活的点与焦点环在数据之上。 | | `measured` | `boolean` | 视口尚未测量(服务端与首帧):绘图区只输出空的 svg。 | | `empty` | `boolean` | 没有可画的数据:空态部件据此显示。 | | `legendItems` | `readonly CartesianLegendItem[]` | | | `active` | `ChartDatumDetails \| null` | 激活的数据;没有时为 null。 | | `tooltip` | `CartesianTooltipModel \| null` | 提示框内容;收起时为 null。 | | `summary` | `string` | 摘要文字。 | | `table` | `TableModel` | 数据表模型:视觉隐藏的数据表用它,也可以喂给 Table 组件做可见的表格视图。 | | `emptyText` | `string` | 空态文字。 | | `tableCaption` | `string` | 数据表的标题。 | | `activeKey` | `ChartKey \| null` | 激活的自变量键。 | | `hiddenSeries` | `string[]` | | | `toggleSeries` | `(id: string) => void` | 切换某个系列的显隐。 | | `setFocusedDatum` | `(ref: { seriesId: string, index: number } \| null) => void` | 移动键盘锚点。只改锚点不移动 DOM 焦点,也不派发回调; 需要焦点跟随时自行调用元素的 focus()。 | | `markTag` | `(mark: Mark) => CartesianMarkTag` | 标记画成什么元素。 | | `getRootProps` | `() => T['element']` | | | `getCaptionProps` | `() => T['element']` | | | `getLegendProps` | `() => T['element']` | | | `getLegendItemProps` | `(item: CartesianLegendItem) => T['button']` | | | `getLegendSwatchProps` | `(item: CartesianLegendItem) => T['element']` | | | `getLegendLabelProps` | `(item: CartesianLegendItem) => T['element']` | | | `getViewportProps` | `() => T['element']` | | | `getPlotProps` | `() => T['element']` | | | `getMarkProps` | `(mark: Mark) => T['element']` | 场景里一个标记的属性(含 path 的 d、文字的坐标)。 | | `getTooltipProps` | `() => T['element']` | | | `getTooltipHeaderProps` | `() => T['element']` | | | `getTooltipRowProps` | `(row: CartesianTooltipRow) => T['element']` | | | `getTooltipSwatchProps` | `(row: CartesianTooltipRow) => T['element']` | | | `getTooltipValueProps` | `(row: CartesianTooltipRow) => T['element']` | | | `getTooltipNameProps` | `(row: CartesianTooltipRow) => T['element']` | | | `getEmptyProps` | `() => T['element']` | | | `getSummaryProps` | `() => T['element']` | | | `getTableProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Tab` / `Shift+Tab` | 总是 | 绘图区只占一个 Tab 位:焦点落到锚点数据,首次为第一个可见系列的第一个数据;图例同样只占一个 Tab 位 | | `ArrowRight` | 焦点在绘图区 | 沿自变量方向移到下一个键(跳过缺失值);horizontal 时由 ArrowDown 承担;已在末尾则原地不动 | | `ArrowLeft` | 焦点在绘图区 | 沿自变量方向移到上一个键;horizontal 时由 ArrowUp 承担 | | `ArrowUp` | 焦点在绘图区 | 在同一个键上换到视觉次序的下一个系列(堆叠自下而上、分组自左而右),跳过隐藏系列与缺失值;horizontal 时由 ArrowRight 承担 | | `ArrowDown` | 焦点在绘图区 | 在同一个键上换到上一个系列;horizontal 时由 ArrowLeft 承担 | | `Home` | 焦点在绘图区 | 当前系列的第一个数据 | | `End` | 焦点在绘图区 | 当前系列的最后一个数据 | | `PageUp` / `PageDown` | 焦点在绘图区 | 跨 10% 的键,至少 1 个 | | `Enter` / `Space` | 焦点在绘图区 | 报告聚焦的数据(onDatumPress) | | `Escape` | 提示框显示着 | 收起提示框,焦点留在原处;按键不拦截,外层浮层的关闭仍归它自己 | | `ArrowLeft` / `ArrowRight` / `Home` / `End` | 焦点在图例 | 在图例项之间移动,左右键跟随文字方向的视觉次序 | | `Enter` / `Space` | 焦点在图例项 | 切换该系列的显隐(原生按钮行为) | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-busy` | 'true' \| undefined | | `legend` | `aria-label` | translations.legendLabel | | `legend` | `role` | 'toolbar' | | `legend-item` | `aria-pressed` | 'false' \| 'true' | | `legend-swatch` | `aria-hidden` | 'true' | | `plot` | `aria-describedby` | `summary` 部件的 id | | `plot` | `aria-labelledby` | `caption` 部件的 id | | `plot` | `aria-roledescription` | translations.chartRoleDescription | | `plot` | `role` | 'graphics-document' | | `tooltip` | `aria-hidden` | 'true' | | `mark` | `aria-hidden` | mark.exiting \|\| undefined | | `mark` | `aria-label` | undefined \| spec?.name | | `mark` | `aria-roledescription` | undefined \| translations.seriesRoleDescription | | `mark` | `role` | undefined \| 'graphics-object' | - 根是 `
`,可访问名称来自 `caption`(`
`);不放标题时在根上写 `aria-label`。只有两者都没有时开发期报 `chart.missing-name`。 - 绘图区是 `role="graphics-document"`,`aria-roledescription` 取 `translations.chartRoleDescription`(缺省 chart),`aria-describedby` 指向组件生成的摘要。 - 每个系列是一个 `role="graphics-object"` 的分组,名称是系列名;每根柱、每个焦点代理点是 `role="graphics-symbol"`,名称取 `translations.datumLabel`(缺省“键, 系列名 值”),务必按本地语言改写。 - 坐标轴、网格、十字准线与焦点环一律 `aria-hidden`:它们的信息由每个数据的名称、摘要与数据表承担。 - 组件在根内生成一段摘要与一张数据表,二者视觉隐藏、对读屏可见,服务端即输出。摘要写系列数、自变量的范围以及每个系列的最小值与最大值,模板是 `translations.summary`;数据表首列是自变量,列名缺省取 x 轴标题,其余每个可见系列一列,缺失值写 `translations.missingValue`。 - 绘图区只占一个 Tab 位,进入后焦点落在一个真实的元素上:柱直接获得焦点;折线没有逐点的元素,由绘图区为聚焦的数据生成一个点作为焦点代理,移动时替换并聚焦新点,读屏据此播报新的名称。 - 焦点环是独立的 `focus-ring` 部件,画在标记之外,不依赖 SVG 元素的 outline;只在键盘聚焦时出现。 - 图例是 `role="toolbar"`,名称取 `translations.legendLabel`;每一项是 `

`root` | `stroke-width` | `default` | `--xh-chart-line-width` | cartesian-chart 的 line、root 部件 stroke-width 覆盖槽。 | | `--xh-cartesian-chart-point-size` | `root` | `--xh-_chart-metric-point-size` | `default` | `--xh-chart-point-size` | cartesian-chart 的 root 部件 --xh-_chart-metric-point-size 覆盖槽。 | | `--xh-cartesian-chart-series-color` | `area-fill`
`bar`
`dot`
`legend-swatch`
`line`
`point`
`tooltip-swatch` | `background`
`border`
`fill`
`stroke` | `tone`
`xh-chart-slot=1`
`xh-chart-slot=2`
`xh-chart-slot=3`
`xh-chart-slot=4`
`xh-chart-slot=5`
`xh-chart-slot=6`
`xh-chart-slot=7`
`xh-chart-slot=8` | `--xh-_tone`
`--xh-chart-categorical-1`
`--xh-chart-categorical-2`
`--xh-chart-categorical-3`
`--xh-chart-categorical-4`
`--xh-chart-categorical-5`
`--xh-chart-categorical-6`
`--xh-chart-categorical-7`
`--xh-chart-categorical-8` | cartesian-chart 的 area-fill、bar、dot、legend-swatch、line、point、tooltip-swatch 部件 background、border、fill、stroke 覆盖槽。 | | `--xh-cartesian-chart-tooltip-gap` | `tooltip` | `gap` | `default` | `--xh-space-1` | cartesian-chart 的 tooltip 部件 gap 覆盖槽。 | | `--xh-cartesian-chart-tooltip-px` | `tooltip` | `padding-inline` | `default` | `--xh-surface-pad-sm` | cartesian-chart 的 tooltip 部件 padding-inline 覆盖槽。 | | `--xh-cartesian-chart-tooltip-py` | `tooltip` | `padding-block` | `default` | `--xh-surface-pad-sm` | cartesian-chart 的 tooltip 部件 padding-block 覆盖槽。 | | `--xh-cartesian-chart-tooltip-radius` | `tooltip` | `border-radius` | `default` | `--xh-shape-overlay` | cartesian-chart 的 tooltip 部件 border-radius 覆盖槽。 | | `--xh-cartesian-chart-tooltip-row-gap` | `tooltip-row` | `gap` | `default` | `--xh-space-2` | cartesian-chart 的 tooltip-row 部件 gap 覆盖槽。 | | `--xh-cartesian-chart-tooltip-shadow` | `tooltip` | `box-shadow` | `default` | `--xh-material-frosted-shadow` | cartesian-chart 的 tooltip 部件 box-shadow 覆盖槽。 | | `--xh-cartesian-chart-tooltip-swatch-line-radius` | `tooltip-swatch` | `border-radius` | `mark=line` | `--xh-shape-pill` | cartesian-chart 的 tooltip-swatch 部件 border-radius 覆盖槽。 | | `--xh-cartesian-chart-tooltip-swatch-radius` | `tooltip-swatch` | `border-radius` | `default` | `--xh-shape-inset` | cartesian-chart 的 tooltip-swatch 部件 border-radius 覆盖槽。 | ### 动效 动效角色:状态 · 出现 · 循环(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-draw` · `xh-fade-in` · `xh-spin` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`opacity` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 `prefers-reduced-motion: reduce` 下本组件另有降级规则。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 - 绘图区不随文字方向镜像:坐标系的方向是数据约定,时间在 rtl 页面上同样从左向右,左方向键始终向左。 - 图例、标题与提示框的内容随文字方向排列,图例的左右键跟随视觉次序翻转。 - 横向条形图的类目标签在 rtl 下同样位于左侧,数值轴同样从左向右增长。 --- 来源:https://ui.docs.xihanfun.com/components/cascader # Cascader 级联选择 用于从多层分类中选择完整路径。 ## 用法 按层级选择完整地区路径 ```vue ``` ```html
收货地区
浙江
江苏
杭州
宁波
南京
西湖区
滨江区
海曙区
玄武区
鼓楼区(暂不开放)
``` ## 组件结构 加粗的是必需部件。 `data-scope="cascader"`:`root` · `hidden-input` · `label` · `control` · **`trigger`** · `value-text` · `indicator` · `clear-trigger` · `positioner` · **`content`** · `input` · `search-list` · `search-item` · `column` · `group` · `group-label` · `item` · `item-text` · `item-description` · `item-suffix` · `item-indicator` · `empty` · `loading` · `footer` ## 示例 ### 多选 选择多个分类路径 ```vue ``` ```html
采购清单
水果
蔬菜
苹果
香蕉
番茄
土豆
``` ### 校验状态 清晰标记必填错误 ```vue ``` ```html
所属部门
产品线
技术线
设计组
用研组
前端组
服务端组

这一项必填

``` ### 懒加载 展开分支时加载下一层数据 ```vue ``` ```html
收货地区
浙江
江苏
加载中…
加载中…
``` ### 搜索 按完整路径筛选选项 ```vue ``` ```html
收货地区
浙江 / 杭州 / 西湖区
浙江 / 杭州 / 滨江区
浙江 / 宁波 / 海曙区
江苏 / 南京 / 玄武区
江苏 / 南京 / 鼓楼区(暂不开放)
浙江
江苏
杭州
宁波
南京
西湖区
滨江区
海曙区
玄武区
鼓楼区(暂不开放)
``` ## 设计指引 ### 何时使用 - 选项具有稳定的多层结构,如地区或商品类目。 - 用户需要逐层缩小选择范围。 ### 何时不用 - 不规则层级使用[树选择](./tree-select)。 - 单层选项使用[选择器](./select)。 - 主要通过关键词查找时使用[组合框](./combobox)。 ### 特性 - `changeOnSelect` 允许选择中间层。 - `expandTrigger` 支持点击或悬停展开。 - `multiple`、`cascade` 与 `checkedStrategy` 控制多选及路径收敛方式。 - `searchable` 按完整路径筛选选项。 - 选项可逐条声明语气,不向下传导;搜索结果取整条路径末段的语气。 - 选项可写副文本,第 2 行放一句解释,与标题同列、走 muted 档。 - 选项行尾留一格给作者(计数、徽标)。 - 支持按需加载、空状态、加载状态与原生表单提交。 - 选中项使用末端标记,半选项使用横线。 ### 组合 - 使用 `label`、`control` 与 `value-text` 组成字段外壳。 - 使用 `column`、`item` 与 `item-indicator` 组成分级列表。 ### 最佳实践 - 层级建议控制在三层以内。 - 回显完整路径,避免同名末级选项产生歧义。 - 自定义条目时保留 `item-text` 与 `item-indicator`。 ### 反模式 - 不要在异步加载时隐藏已有列。 - 多选时明确约定 `checkedStrategy`。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCascaderClearTrigger` `XhCascaderColumn` `XhCascaderContent` `XhCascaderControl` `XhCascaderFooter` `XhCascaderGroup` `XhCascaderGroupLabel` `XhCascaderIndicator` `XhCascaderInput` `XhCascaderItem` `XhCascaderItemDescription` `XhCascaderItemIndicator` `XhCascaderItemSuffix` `XhCascaderItemText` `XhCascaderLabel` `XhCascaderLoading` `XhCascaderPositioner` `XhCascaderRoot` `XhCascaderSearchList` `XhCascaderTrigger` `XhCascaderValueText` | | 组合式函数 | `useCascader` | | 状态机 | `cascaderMachine` | | 皮肤 | `@xihan-ui/styles/cascader.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `collection` | `CascaderNode[]` | | 树数据,层级元信息与显示文本的唯一事实源。默认为空树。 | | `value` | `CascaderValue` | | 选中路径。提供即受控:cell 直读 prop,写入只发 onValueChange 不落内部值。 单条路径是简写,内部一律归一为路径集合。 | | `defaultValue` | `CascaderValue` | | | | `name` | `string` | | 原生字段名,每条选中路径提交一项 JSON 字符串数组。 | | `form` | `string` | | 关联的原生表单 ID;指定后覆盖祖先表单归属。 | | `open` | `boolean` | | 展开态。提供即受控:内部不再自行修改,只发 onOpenChange。 | | `defaultOpen` | `boolean` | | | | `expandTrigger` | `CascaderExpandTrigger` | | 子列的展开方式,默认 click。 | | `changeOnSelect` | `boolean` | | 中间层(分支)也可以落值。关闭时点击分支只展开子列,不改变选中值。 | | `multiple` | `boolean` | | 多选:选中为路径集合,选中后浮层不收起、焦点留在列中以便继续选择。 | | `searchable` | `boolean` | | 开启搜索:input 部件可用,输入后整条路径连缀过滤、候选替换列视图。 | | `cascade` | `boolean` | | 多选下父子级联勾选:点击分支整枝传导、子全勾父勾、部分勾选半选, 禁用子树整棵冻结。默认 false(按路径原样切换);单选下无效。 | | `checkedStrategy` | `CascadeStrategy` | | 级联下对外值的收敛策略,默认 child(只收叶);parent = 最高整枝,all = 全部勾选节点。 | | `disabled` | `boolean` | | 整个控件禁用:trigger 使用原生 disabled,浮层不可展开。 | | `readOnly` | `boolean` | | 只读:浮层照常展开与浏览,但选中值不可修改、也不可清空。 | | `invalid` | `boolean` | | 校验失败:trigger 报告 aria-invalid,各角色节点带 data-invalid。 | | `loading` | `boolean` | | 候选加载中:浮层报告 aria-busy;当前视图无候选时显示在途占位。 | | `translations` | `Partial` | | 空态占位的文案覆盖,默认英文。 | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定触发框的描边与底色使用方式。默认 outline。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定聚焦与选中使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,决定触发框与条目的几何档位。 | | `placeholder` | `string` | | 无选中时 value-text 显示的占位文字。 | | `separator` | `string` | | 路径回显的连接符,默认 ' / '。 | | `placement` | `Placement` | | | | `offset` | `number` | | | | `loop` | `boolean` | | 列内上下键到达首尾是否回绕,默认 true。 | | `dir` | `Direction` | | 文字方向,默认 ltr;只对调左右方向键的进入子列 / 返回上一列语义。 | | `onValueChange` | `(details: CascaderValueChangeDetails) => void` | | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 | | `onOpenChange` | `(details: CascaderOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | ### CascaderNode `collection` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | 展示名,也是路径回显的取字来源;默认回退为 value。 | | `disabled` | `boolean` | | 条目禁用:方向键跳过它,但它仍可聚焦、仍是导航起点。不向下传导给子节点。 | | `tone` | `Tone` | | 该条选项自身的性质:已失效的写 danger、需要留意的写 warning。不写即与同列其余条目同档, 也不向下传导给子节点——每一层各自声明。只换字色与悬停 / 按下的面,不表达选中与校验; 展开路径的面、选中的对号与禁用都压过它。搜索结果里取整条路径末段的语气。 | | `description` | `string` | | 副文本,写入 item-description 部件;未提供时本条不铺该部件。 它是第 2 行的说明,跟着条目走 muted 档,不跟语气;放不下一行的解释才用它, 一句话能说清的写进 label。 | | `children` | `CascaderNode[]` | | 子节点。非空数组才视为分支(右侧可以再打开一列)。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `CascaderValueChangeDetails` | 选中路径集合变化;detail 为 `{ value: string[][] }` | | `open-change` | `CascaderOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCascaderRoot` | `default` | `CascaderRootSlotProps` | | | `XhCascaderSearchList` | `item` | `CascaderSearchListItemSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCascaderColumn` | `level` | `number \| string` | 是 | 层号,兼收字符串。 | | `XhCascaderContent` | `empty` | `ReactNode` | | 空态占位的内容;未提供时按视图取无匹配或无数据文案。 | | `XhCascaderGroup` | `value` | `string` | 是 | | | `XhCascaderItem` | `value` | `string` | 是 | | | `XhCascaderPositioner` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhCascaderRoot` | `children` | `SlotChildren` | | | | `XhCascaderSearchList` | `renderItem` | `(result: CascaderSearchResult) => ReactNode` | | 每条候选的自定义内容;未提供时把整条路径连缀为一行。 | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `control` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `indicator` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `search-item` | 'checked' \| 'indeterminate' \| 'unchecked' | | `column` | 'open' \| 'closed' | | `item` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-text` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-description` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-suffix` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-indicator` | 'indeterminate' \| 'checked' \| 'unchecked' | | `footer` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`FORM.RESET` · `OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `ITEM.FOCUS` · `ITEM.EXPAND` · `ITEM.LOST` · `ITEM.SELECT` · `VALUE.SET` · `VALUE.CLEAR` · `PATH.SET` · `INPUT.CHANGE` · `SEARCH.HIGHLIGHT` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` · `isMultiple` · `staysOpenOnSelect` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `collection` | `readonly CascaderNode[]` | 作者提供的原始树数据。 | | `columns` | `readonly CascaderColumn[]` | 当前并排打开的列(含每列的条目):列数 = 展开路径可走通的段数 + 1。 | | `levels` | `readonly CascaderLevel[]` | 按深度展开的静态列,与展开路径无关;不应显示的条目由连接层加 hidden 收起。 | | `value` | `string[][]` | 选中路径集合;单选下长度 ≤ 1,形状不随模式变化。 | | `valuePath` | `string[] \| null` | 单选便利读法:选中的路径,无选中时为 null。 | | `valueText` | `string \| null` | 选中路径的显示文字(整条路径用分隔符连接;多选各条之间用逗号);无选中时为 null。 | | `displayText` | `string` | value-text 实际显示的文字:有选中时取路径文本,否则取 placeholder。 | | `activePath` | `string[]` | 展开路径:并排打开哪几列由它决定。 | | `focusedPath` | `string[] \| null` | 焦点锚点;收起、或它已不在任何可见列中时为 null。 | | `multiple` | `boolean` | | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | | | `canClear` | `boolean` | 清空按钮当前是否可按。 | | `isSelected` | `(value: string) => boolean` | 该条目是否为某条选中路径的末项。 | | `isIndeterminate` | `(value: string) => boolean` | 级联模式下该分支是否半选(有效叶后代部分勾选);非级联恒为 false。 | | `isActive` | `(value: string) => boolean` | 该条目是否落在展开路径上(它的子列已打开,或它自身即为最后一站)。 | | `isVisible` | `(value: string) => boolean` | 该条目当前是否落在某个可见列中。 | | `searching` | `boolean` | 正处于搜索视图(开启 searchable 且输入非空):列视图让位给候选列表。 | | `inputValue` | `string` | 搜索框中的原始串。 | | `searchResults` | `readonly CascaderSearchResult[]` | 过滤后的候选:整条路径连缀匹配,带 pathKey 与禁用标记。 | | `searchHighlightIndex` | `number` | 候选中的虚拟高亮下标,恒落在一条可选候选上;没有候选或整批禁用时为 -1。 | | `translations` | `CascaderTranslations` | 空态占位的文案:实例覆盖并入默认后的完整一份。 | | `setInputValue` | `(next: string) => void` | | | `setOpen` | `(next: boolean) => void` | | | `setValue` | `(next: string[][]) => void` | | | `setActivePath` | `(next: string[]) => void` | | | `select` | `(path: string[]) => void` | 选中一条路径,与点击条目同一语义(分支是否落值仍取决于 changeOnSelect)。 | | `clear` | `() => void` | | | `getRootProps` | `() => T['element']` | | | `getHiddenInputProps` | `(props: { path: readonly string[] }) => T['input']` | 每条路径独立编码,适配器按 value 渲染重复同名字段。 | | `getLabelProps` | `() => T['element']` | | | `getControlProps` | `() => T['element']` | | | `getTriggerProps` | `() => T['button']` | | | `getValueTextProps` | `() => T['element']` | | | `getIndicatorProps` | `() => T['element']` | | | `getClearTriggerProps` | `() => T['button']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getInputProps` | `() => T['input']` | 搜索框:放在 content 顶部;输入即过滤,上下键移动候选、Enter 选中、Escape 先清除输入。 | | `getSearchListProps` | `() => T['element']` | 候选列表容器;不在搜索视图时带 hidden。 | | `getSearchItemProps` | `(props: CascaderSearchItemProps) => T['element']` | 一条候选:身份是整条路径;点击选中(与点击列内条目同一语义)。 | | `getEmptyProps` | `() => T['element']` | 空态占位:当前视图没有条目(搜索无候选,或根列没有条目)时显示,其余时候带 hidden。 | | `getLoadingProps` | `() => T['element']` | 在途占位:当前视图无候选且正在取数时显示;已有候选或祖先列时只保留 aria-busy。 适配器自动提供默认部件,作者显式编写部件即可替换它。 | | `getFooterProps` | `() => T['element']` | 浮层底部的操作区:放在 content 中、与列并列,不进入任何一列的拥有关系,方向键也无法到达。 | | `getGroupProps` | `(props: CascaderGroupProps) => T['element']` | 分组容器:role=group,条目挂在其中;分组标题经 aria-labelledby 关联。 | | `getGroupLabelProps` | `(props: CascaderGroupProps) => T['element']` | 分组标题:不是条目、不进入导航,只作为本组的可及名。 | | `getColumnProps` | `(props: CascaderColumnProps) => T['element']` | | | `getItemProps` | `(props: CascaderItemProps) => T['element']` | | | `getItemTextProps` | `(props: CascaderItemProps) => T['element']` | | | `getItemDescriptionProps` | `(props: CascaderItemProps) => T['element']` | | | `getItemSuffixProps` | `(props: CascaderItemProps) => T['element']` | | | `getItemIndicatorProps` | `(props: CascaderItemProps) => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | closed, focus in trigger | 展开浮层并把焦点落到选中路径的末项(无选中或它已禁用则落该列首个可用条目) | | `ArrowDown` | closed, focus in trigger | 展开浮层并把焦点落到选中条目在它那一列里的下一个可用条目 | | `ArrowUp` | closed, focus in trigger | 展开浮层并把焦点落到选中条目在它那一列里的上一个可用条目 | | `Delete` | focus in trigger, 有值且未禁用、未只读 | 清空全部选中值,浮层不展开、焦点留在 trigger | | `Backspace` | focus in trigger, 有值且未禁用、未只读 | 单选清空;多选去掉最后一个选中路径 | | `ArrowDown` | open, focus in content | 焦点移到当前列的下一个条目(禁用条目跳过;loop 默认开,末项回绕到首项);别的列不动 | | `ArrowUp` | open, focus in content | 焦点移到当前列的上一个条目(禁用条目跳过;loop 默认开,首项回绕到末项) | | `Home` | open, focus in content | 焦点移到当前列的首个可用条目 | | `End` | open, focus in content | 焦点移到当前列的末个可用条目 | | `ArrowRight` | open, 焦点条目有子节点(dir=rtl 时改由 ArrowLeft 承担) | 子列没开时先把它铺出来(焦点不动),已开时焦点移进它的首个可用条目;叶子上什么都不做且不吞键 | | `ArrowLeft` | open, 焦点不在根列(dir=rtl 时改由 ArrowRight 承担) | 焦点退回上一列的父条目,当前这一列随之收起;根列上什么都不做且不吞键 | | `Enter` / `Space` | open, 焦点条目未禁用 | 叶子:落值并收起浮层、焦点归还 trigger。分支:展开它的子列且浮层不收起,changeOnSelect 打开时同时落值 | | `Enter` / `Space` | held in item / clear-trigger, 未禁用、未只读、未加载 | 按住期间该部件投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,条目随浮层收起一并撤下;没有值可清时清空按钮不进 | | `Escape` | open | 收起浮层并把焦点归还 trigger,选中值不变 | | `Tab` / `Shift+Tab` | open | 收起浮层,焦点不归还 trigger,按 Tab 序列自然离开 | | `可打印字符` | open, focus in input, searchable | 改写检索词;trim 后非空即把列视图整个换成候选列表(整条路径连缀匹配),高亮落到首个可选候选 | | `ArrowDown` | open, focus in input, 检索词非空 | 高亮移到下一个候选(禁用整条的候选跳过;loop 默认开,末条回绕到首条),焦点留在检索框 | | `ArrowUp` | open, focus in input, 检索词非空 | 高亮移到上一个候选(禁用整条的候选跳过;loop 默认开,首条回绕到末条),焦点留在检索框 | | `Home` | open, focus in input, 检索词非空 | 高亮移到首个可选候选;检索词为空时不接管,光标照常跳到行首 | | `End` | open, focus in input, 检索词非空 | 高亮移到末个可选候选;检索词为空时不接管,光标照常跳到行尾 | | `Enter` | open, focus in input, 有高亮候选 | 把整条候选路径落成选中值:单选收起浮层、焦点归还 trigger,多选并入集合且浮层不收起;两种都清掉检索词回列视图。无可选候选时不吞这个键 | | `Enter` | open, focus in input, 有高亮候选且未禁用、未只读、未加载,按住 | 按住期间高亮候选投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,候选随浮层收起一并撤下 | | `Escape` | open, focus in input, 检索词非空 | 清掉检索词回到列视图,浮层不收起、焦点留在检索框;检索词已空才轮到收浮层那一档 | | `ArrowDown` / `ArrowUp` | open, focus in input, 检索词为空 | 把焦点交给列视图:有锚点条目就落回它,没有则 ArrowDown 进当前列首个可用条目、ArrowUp 进末个 | | `ArrowLeft` / `ArrowRight` | open, focus in input | 不接管,留给检索框自己移光标;进子列 / 回上一列那一套只在焦点落在条目上时发生 | | `Tab` / `Shift+Tab` | open, focus in input | 收起浮层,焦点不归还 trigger,按 Tab 序列自然离开 | | `输入法组合期间的任意键` | open, focus in input, isComposing | 一律不接管:组合期的 Enter 与上下键属于输入法候选框,既不选中候选也不移高亮 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `trigger` | `aria-haspopup` | 'listbox' | | `trigger` | `aria-invalid` | 'true' \| 'false' | | `trigger` | `aria-labelledby` | `label` 部件的 id `value-text` 部件的 id | | `trigger` | `aria-readonly` | 'true' \| 'false' | | `trigger` | `role` | 'combobox' | | `indicator` | `aria-hidden` | 'true' | | `clear-trigger` | `aria-label` | translations.clearTrigger | | `content` | `aria-busy` | 'true' \| undefined | | `content` | `aria-hidden` | !open \|\| undefined | | `input` | `aria-activedescendant` | `search-item` 部件的 id \| undefined | | `input` | `aria-autocomplete` | 'list' | | `input` | `aria-controls` | `search-list` 部件的 id | | `input` | `aria-label` | translations.searchInput | | `search-list` | `aria-label` | translations.searchList | | `search-list` | `aria-multiselectable` | 'true' \| 'false' | | `search-list` | `role` | 'listbox' | | `search-item` | `aria-checked` | 'true' \| 'mixed' \| 'false' \| undefined | | `search-item` | `aria-disabled` | 'true' \| 'false' | | `search-item` | `aria-selected` | 'true' \| 'false' | | `search-item` | `role` | 'option' | | `column` | `aria-disabled` | 'true' \| 'false' | | `column` | `aria-label` | translations.column \| undefined | | `column` | `aria-labelledby` | `label` 部件的 id `value-text` 部件的 id \| `item` 部件的 id | | `column` | `aria-multiselectable` | 'true' \| 'false' | | `column` | `aria-orientation` | 'vertical' | | `column` | `role` | 'listbox' | | `group` | `aria-labelledby` | `group-label` 部件的 id | | `group` | `role` | 'group' | | `item` | `aria-checked` | 'true' \| 'mixed' \| 'false' \| undefined | | `item` | `aria-disabled` | 'true' \| 'false' | | `item` | `aria-haspopup` | 'listbox' \| undefined | | `item` | `aria-selected` | 'true' \| 'false' | | `item` | `role` | 'option' | | `item-indicator` | `aria-hidden` | 'true' | | `empty` | `role` | 'status' | | `loading` | `role` | 'status' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/cascader.css` 使用 `[data-scope="cascader"][data-part="root"]` 部件选择器,位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-loading` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `label` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-state` | 'open' \| 'closed' | | `control` | `data-variant` | props.variant | | `control` | `data-xh-field-chrome` | '' | | `control` | `data-xh-field-size` | props.size | | `trigger` | `data-disabled` | ''(条件成立时才出现) | | `trigger` | `data-invalid` | ''(条件成立时才出现) | | `trigger` | `data-placeholder` | ''(条件成立时才出现) | | `trigger` | `data-readonly` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `value-text` | `data-disabled` | ''(条件成立时才出现) | | `value-text` | `data-placeholder` | ''(条件成立时才出现) | | `indicator` | `data-clearable` | ''(条件成立时才出现) | | `indicator` | `data-disabled` | ''(条件成立时才出现) | | `indicator` | `data-state` | 'open' \| 'closed' | | `clear-trigger` | `data-pressed` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-control` | '' | | `clear-trigger` | `data-xh-action-display` | 'has-value' | | `clear-trigger` | `data-xh-action-has-value` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-profile` | 'field-inset' | | `clear-trigger` | `data-xh-action-size` | props.size | | `clear-trigger` | `data-xh-action-variant` | 'ghost' | | `positioner` | `data-hidden` | ''(条件成立时才出现) | | `positioner` | `data-placement` | 定位引擎算出的实际落位 | | `positioner` | `data-positioned` | ''(条件成立时才出现) | | `positioner` | `data-size` | props.size | | `positioner` | `data-state` | 'open' \| 'closed' | | `positioner` | `data-tone` | props.tone | | `positioner` | `data-variant` | props.variant | | `content` | `data-empty` | ''(条件成立时才出现) | | `content` | `data-placement` | 定位引擎算出的实际落位 | | `content` | `data-searching` | ''(条件成立时才出现) | | `content` | `data-state` | 'open' \| 'closed' | | `content` | `data-xh-material` | 'frosted' | | `search-list` | `data-empty` | ''(条件成立时才出现) | | `search-item` | `data-disabled` | ''(条件成立时才出现) | | `search-item` | `data-highlighted` | ''(条件成立时才出现) | | `search-item` | `data-pressed` | ''(条件成立时才出现) | | `search-item` | `data-state` | 'checked' \| 'indeterminate' \| 'unchecked' | | `search-item` | `data-tone` | undefined \| metaOf(v)?.tone | | `search-item` | `data-xh-collection-context` | 'overlay' | | `search-item` | `data-xh-collection-item` | '' | | `search-item` | `data-xh-collection-size` | props.size | | `column` | `data-level` | String(column.level) | | `column` | `data-state` | 'open' \| 'closed' | | `group` | `data-disabled` | ''(条件成立时才出现) | | `group-label` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-branch` | ''(条件成立时才出现) | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-highlighted` | ''(条件成立时才出现) | | `item` | `data-in-path` | ''(条件成立时才出现) | | `item` | `data-level` | String(meta.level) \| undefined | | `item` | `data-pressed` | ''(条件成立时才出现) | | `item` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item` | `data-tone` | undefined \| metaOf(v)?.tone | | `item` | `data-xh-collection-context` | 'overlay' | | `item` | `data-xh-collection-item` | '' | | `item` | `data-xh-collection-size` | props.size | | `item-text` | `data-disabled` | ''(条件成立时才出现) | | `item-text` | `data-highlighted` | ''(条件成立时才出现) | | `item-text` | `data-in-path` | ''(条件成立时才出现) | | `item-text` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-text` | `data-xh-collection-slot` | 'text' | | `item-description` | `data-disabled` | ''(条件成立时才出现) | | `item-description` | `data-highlighted` | ''(条件成立时才出现) | | `item-description` | `data-in-path` | ''(条件成立时才出现) | | `item-description` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-description` | `data-xh-collection-slot` | 'description' | | `item-suffix` | `data-disabled` | ''(条件成立时才出现) | | `item-suffix` | `data-highlighted` | ''(条件成立时才出现) | | `item-suffix` | `data-in-path` | ''(条件成立时才出现) | | `item-suffix` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-suffix` | `data-xh-collection-slot` | 'suffix' | | `item-indicator` | `data-disabled` | ''(条件成立时才出现) | | `item-indicator` | `data-highlighted` | ''(条件成立时才出现) | | `item-indicator` | `data-in-path` | ''(条件成立时才出现) | | `item-indicator` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `item-indicator` | `data-xh-collection-slot` | 'indicator' | | `footer` | `data-state` | 'open' \| 'closed' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-cascader-action-bg` | `clear-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | cascader 的 clear-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-cascader-action-bg-active` | `clear-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | cascader 的 clear-trigger 部件 background-color 覆盖槽。 | | `--xh-cascader-action-bg-hover` | `clear-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | cascader 的 clear-trigger 部件 background-color 覆盖槽。 | | `--xh-cascader-action-fg` | `clear-trigger` | `color` | `default` | `--xh-fg-muted` | cascader 的 clear-trigger 部件 color 覆盖槽。 | | `--xh-cascader-action-fg-hover` | `clear-trigger` | `color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-fg-default` | cascader 的 clear-trigger 部件 color 覆盖槽。 | | `--xh-cascader-action-font-size` | `clear-trigger` | `font-size` | `default` | `--xh-text-secondary-size` | cascader 的 clear-trigger 部件 font-size 覆盖槽。 | | `--xh-cascader-action-radius` | `clear-trigger` | `border-radius` | `default` | `--xh-shape-inset` | cascader 的 clear-trigger 部件 border-radius 覆盖槽。 | | `--xh-cascader-action-size` | `clear-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | cascader 的 clear-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-cascader-branch-arrow-fg` | `item` | `background-color` | `branch` | `--xh-fg-subtle` | cascader 的 item 部件 background-color 覆盖槽。 | | `--xh-cascader-branch-arrow-size` | `item` | `block-size`
`inline-size` | `branch` | `--xh-control-indicator-size` | cascader 的 item 部件 block-size、inline-size 覆盖槽。 | | `--xh-cascader-column-divider` | `column` | `border-inline-start` | `default` | `--xh-material-frosted-separator` | cascader 的 column 部件 border-inline-start 覆盖槽。 | | `--xh-cascader-column-gap` | `column` | `gap` | `default` | `--xh-list-option-gap` | cascader 的 column 部件 gap 覆盖槽。 | | `--xh-cascader-column-h` | `column`
`search-list` | `block-size` | `default` | `--xh-viewport-h-sm` | cascader 的 column、search-list 部件 block-size 覆盖槽。 | | `--xh-cascader-column-min-w` | `column`
`empty`
`loading` | `min-inline-size` | `default` | `7rem` | cascader 的 column、empty、loading 部件 min-inline-size 覆盖槽。 | | `--xh-cascader-column-px` | `column` | `padding-inline` | `default` | `--xh-space-1` | cascader 的 column 部件 padding-inline 覆盖槽。 | | `--xh-cascader-column-py` | `column` | `padding-block` | `default` | `--xh-space-1` | cascader 的 column 部件 padding-block 覆盖槽。 | | `--xh-cascader-content-backdrop` | `content` | `-webkit-backdrop-filter`
`backdrop-filter` | `xh-material=frosted` | `--xh-_material-backdrop` | cascader 的 content 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 | | `--xh-cascader-content-bg` | `content` | `background` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-bg` | cascader 的 content 部件 background 覆盖槽。 | | `--xh-cascader-content-border` | `content` | `border` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-border` | cascader 的 content 部件 border 覆盖槽。 | | `--xh-cascader-content-fg` | `content` | `color` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-fg` | cascader 的 content 部件 color 覆盖槽。 | | `--xh-cascader-content-highlight` | `content` | `background` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-highlight` | cascader 的 content 部件 background 覆盖槽。 | | `--xh-cascader-content-max-w` | `content` | `max-inline-size` | `default` | `--xh-overlay-max-w-xl` | cascader 的 content 部件 max-inline-size 覆盖槽。 | | `--xh-cascader-content-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | cascader 的 content 部件 border-radius 覆盖槽。 | | `--xh-cascader-content-shadow` | `content` | `box-shadow` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-shadow` | cascader 的 content 部件 box-shadow 覆盖槽。 | | `--xh-cascader-control-bg` | `control` | `background-color` | `xh-field-chrome` | `--xh-_field-variant-bg-rest` | cascader 的 control 部件 background-color 覆盖槽。 | | `--xh-cascader-control-bg-disabled` | `control` | `background-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-bg-disabled` | cascader 的 control 部件 background-color 覆盖槽。 | | `--xh-cascader-control-bg-hover` | `control` | `background-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-hover` | cascader 的 control 部件 background-color 覆盖槽。 | | `--xh-cascader-control-bg-readonly` | `control` | `background-color` | `readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-read-only` | cascader 的 control 部件 background-color 覆盖槽。 | | `--xh-cascader-control-border` | `control` | `border` | `xh-field-chrome` | `--xh-_field-variant-border-rest` | cascader 的 control 部件 border 覆盖槽。 | | `--xh-cascader-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])`
`xh-field-chrome` | `--xh-_field-variant-border-focus` | cascader 的 control 部件 border-color 覆盖槽。 | | `--xh-cascader-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-border-hover` | cascader 的 control 部件 border-color 覆盖槽。 | | `--xh-cascader-control-border-invalid` | `control` | `border-color` | `invalid`
`xh-field-chrome` | `--xh-_field-variant-border-invalid` | cascader 的 control 部件 border-color 覆盖槽。 | | `--xh-cascader-control-fg` | `control` | `color` | `xh-field-chrome` | `--xh-fg-default` | cascader 的 control 部件 color 覆盖槽。 | | `--xh-cascader-control-gap` | `control` | `gap` | `xh-field-chrome` | `--xh-_cascader-gap` | cascader 的 control 部件 gap 覆盖槽。 | | `--xh-cascader-control-h` | `control` | `block-size`
`min-block-size` | `has([data-xh-field-input][data-xh-field-layout='multi-tag'])`
`has([data-xh-field-input][data-xh-field-layout='single-line'])`
`has([data-xh-field-input][data-xh-field-layout='textarea'])`
`xh-field-chrome`
`xh-field-input`
`xh-field-layout=multi-tag`
`xh-field-layout=single-line`
`xh-field-layout=textarea` | `--xh-_cascader-h` | cascader 的 control 部件 block-size、min-block-size 覆盖槽。 | | `--xh-cascader-control-min-w` | `control`
`root` | `min-inline-size` | `default`
`xh-field-chrome` | `--xh-control-min-w` | cascader 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-cascader-control-px` | `control` | `padding-inline` | `xh-field-chrome` | `--xh-_cascader-px` | cascader 的 control 部件 padding-inline 覆盖槽。 | | `--xh-cascader-control-radius` | `control` | `border-radius` | `xh-field-chrome` | `--xh-shape-control` | cascader 的 control 部件 border-radius 覆盖槽。 | | `--xh-cascader-control-shadow` | `control` | `box-shadow` | `xh-field-chrome` | `none` | cascader 的 control 部件 box-shadow 覆盖槽。 | | `--xh-cascader-control-w` | `root` | `inline-size`
`min-inline-size` | `default` | `--xh-control-w` | cascader 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-cascader-empty-fg` | `empty` | `color` | `default` | `--xh-material-frosted-fg-muted` | cascader 的 empty 部件 color 覆盖槽。 | | `--xh-cascader-empty-min-h` | `empty` | `min-block-size` | `default` | `5rem` | cascader 的 empty 部件 min-block-size 覆盖槽。 | | `--xh-cascader-empty-p` | `empty` | `padding` | `default` | `--xh-space-3` | cascader 的 empty 部件 padding 覆盖槽。 | | `--xh-cascader-footer-border` | `footer` | `border-block-start` | `default` | `--xh-border-subtle` | cascader 的 footer 部件 border-block-start 覆盖槽。 | | `--xh-cascader-footer-fg` | `footer` | `color` | `default` | `--xh-fg-muted` | cascader 的 footer 部件 color 覆盖槽。 | | `--xh-cascader-footer-font-size` | `footer` | `font-size` | `default` | `--xh-text-secondary-size` | cascader 的 footer 部件 font-size 覆盖槽。 | | `--xh-cascader-footer-gap` | `footer` | `gap` | `default` | `--xh-space-2` | cascader 的 footer 部件 gap 覆盖槽。 | | `--xh-cascader-footer-px` | `footer` | `padding-inline` | `default` | `--xh-space-2` | cascader 的 footer 部件 padding-inline 覆盖槽。 | | `--xh-cascader-footer-py` | `footer` | `padding-block` | `default` | `--xh-space-2` | cascader 的 footer 部件 padding-block 覆盖槽。 | | `--xh-cascader-gap` | `root` | `gap` | `default` | `--xh-space-1` | cascader 的 root 部件 gap 覆盖槽。 | | `--xh-cascader-group-gap` | `group` | `gap` | `default` | `--xh-list-option-gap` | cascader 的 group 部件 gap 覆盖槽。 | | `--xh-cascader-group-label-fg` | `group-label` | `color` | `default` | `--xh-material-frosted-fg-muted` | cascader 的 group-label 部件 color 覆盖槽。 | | `--xh-cascader-group-label-font-size` | `group-label` | `font-size` | `default` | `--xh-text-caption-size` | cascader 的 group-label 部件 font-size 覆盖槽。 | | `--xh-cascader-group-label-font-weight` | `group-label` | `font-weight` | `default` | `--xh-font-weight-medium` | cascader 的 group-label 部件 font-weight 覆盖槽。 | | `--xh-cascader-group-label-px` | `group-label` | `padding-inline` | `default` | `--xh-_cascader-row-px` | cascader 的 group-label 部件 padding-inline 覆盖槽。 | | `--xh-cascader-group-label-py` | `group-label` | `padding-block` | `default` | `--xh-space-1` | cascader 的 group-label 部件 padding-block 覆盖槽。 | | `--xh-cascader-group-spacing` | `group` | `margin-block-start` | `default` | `--xh-space-1_5` | cascader 的 group 部件 margin-block-start 覆盖槽。 | | `--xh-cascader-icon-size` | `control`
`item`
`positioner`
`root` | `--xh-icon-size` | `default`
`is([data-part='root'], [data-part='positioner'])`
`size=lg`
`size=sm`
`xh-field-chrome` | `--xh-_collection-glyph-size`
`--xh-_field-size-glyph-size`
`--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | cascader 的 control、item、positioner、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-cascader-indicator-fg` | `indicator` | `color` | `default` | `--xh-fg-muted` | cascader 的 indicator 部件 color 覆盖槽。 | | `--xh-cascader-input-autofill-bg` | `input` | `box-shadow` | `-webkit-autofill`
`autofill` | `--xh-bg-surface` | cascader 的 input 部件 box-shadow 覆盖槽。 | | `--xh-cascader-input-autofill-fg` | `input` | `-webkit-text-fill-color` | `-webkit-autofill`
`autofill` | `--xh-fg-default` | cascader 的 input 部件 -webkit-text-fill-color 覆盖槽。 | | `--xh-cascader-input-font-size` | `input` | `font-size` | `default` | `--xh-_cascader-font-size` | cascader 的 input 部件 font-size 覆盖槽。 | | `--xh-cascader-input-px` | `input` | `padding-inline` | `default` | `--xh-control-px-md` | cascader 的 input 部件 padding-inline 覆盖槽。 | | `--xh-cascader-input-py` | `input` | `padding-block` | `default` | `--xh-space-2` | cascader 的 input 部件 padding-block 覆盖槽。 | | `--xh-cascader-item-active-font-weight` | `item` | `font-weight` | `in-path` | `--xh-font-weight-regular` | cascader 的 item 部件 font-weight 覆盖槽。 | | `--xh-cascader-item-bg-active` | `item` | `background-color` | `in-path` | `--xh-bg-subtle` | cascader 的 item 部件 background-color 覆盖槽。 | | `--xh-cascader-item-bg-hover` | `item`
`search-item` | `background-color` | `disabled`
`error`
`highlighted`
`hover`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle` | cascader 的 item、search-item 部件 background-color 覆盖槽。 | | `--xh-cascader-item-bg-pressed` | `item`
`search-item` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle-hover` | cascader 的 item、search-item 部件 background-color 覆盖槽。 | | `--xh-cascader-item-check-fg` | `item` | `color` | `disabled`
`error`
`highlighted`
`hover`
`in-path`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`state=checked`
`xh-collection-context=overlay`
`xh-collection-slot=indicator` | `--xh-cascader-item-indicator-fg` | cascader 的 item 部件 color 覆盖槽。 | | `--xh-cascader-item-fg` | `item`
`search-item` | `color` | `default`
`disabled`
`error`
`highlighted`
`hover`
`in-path`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-material-frosted-fg` | cascader 的 item、search-item 部件 color 覆盖槽。 | | `--xh-cascader-item-fg-selected` | `item`
`search-item` | `color` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-cascader-item-fg` | cascader 的 item、search-item 部件 color 覆盖槽。 | | `--xh-cascader-item-font-size` | `empty`
`item`
`search-item` | `font-size` | `default` | `--xh-_cascader-font-size` | cascader 的 empty、item、search-item 部件 font-size 覆盖槽。 | | `--xh-cascader-item-font-weight-selected` | `item`
`search-item` | `font-weight` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-font-weight-regular` | cascader 的 item、search-item 部件 font-weight 覆盖槽。 | | `--xh-cascader-item-gap` | `item`
`search-item` | `margin-inline-end`
`margin-inline-start`
`padding-inline-end` | `branch`
`default`
`xh-collection-slot=indicator`
`xh-collection-slot=prefix`
`xh-collection-slot=shortcut`
`xh-collection-slot=suffix` | `--xh-_cascader-gap` | cascader 的 item、search-item 部件 margin-inline-end、margin-inline-start、padding-inline-end 覆盖槽。 | | `--xh-cascader-item-indicator-fg` | `item`
`search-item` | `background-color`
`color` | `default`
`disabled`
`error`
`highlighted`
`hover`
`in-path`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`state=checked`
`xh-collection-context=overlay`
`xh-collection-slot=indicator` | `--xh-_cascader-accent` | cascader 的 item、search-item 部件 background-color、color 覆盖槽。 | | `--xh-cascader-item-indicator-size` | `item-indicator`
`search-item` | `--xh-icon-size`
`block-size`
`inline-size`
`padding-inline-end` | `default` | `--xh-control-indicator-size` | cascader 的 item-indicator、search-item 部件 --xh-icon-size、block-size、inline-size、padding-inline-end 覆盖槽。 | | `--xh-cascader-item-leading` | `item`
`search-item` | `line-height` | `default` | `--xh-leading-normal` | cascader 的 item、search-item 部件 line-height 覆盖槽。 | | `--xh-cascader-item-max-w` | `item` | `max-inline-size` | `default` | `--xh-overlay-max-w` | cascader 的 item 部件 max-inline-size 覆盖槽。 | | `--xh-cascader-item-px` | `item`
`search-item` | `inset-inline-end`
`padding-inline`
`padding-inline-end` | `default` | `--xh-_cascader-row-px` | cascader 的 item、search-item 部件 inset-inline-end、padding-inline、padding-inline-end 覆盖槽。 | | `--xh-cascader-item-py` | `item`
`search-item` | `padding-block` | `default` | `--xh-_cascader-row-py` | cascader 的 item、search-item 部件 padding-block 覆盖槽。 | | `--xh-cascader-item-radius` | `item`
`search-item` | `border-radius` | `default` | `--xh-shape-control` | cascader 的 item、search-item 部件 border-radius 覆盖槽。 | | `--xh-cascader-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | cascader 的 label 部件 color 覆盖槽。 | | `--xh-cascader-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | cascader 的 label 部件 color 覆盖槽。 | | `--xh-cascader-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | cascader 的 label 部件 font-size 覆盖槽。 | | `--xh-cascader-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | cascader 的 label 部件 font-weight 覆盖槽。 | | `--xh-cascader-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | cascader 的 positioner 部件 z-index 覆盖槽。 | | `--xh-cascader-loading-fg` | `loading` | `color` | `default` | `--xh-material-frosted-fg-muted` | cascader 的 loading 部件 color 覆盖槽。 | | `--xh-cascader-loading-font-size` | `loading` | `font-size` | `default` | `--xh-_cascader-font-size` | cascader 的 loading 部件 font-size 覆盖槽。 | | `--xh-cascader-loading-min-h` | `loading` | `min-block-size` | `default` | `5rem` | cascader 的 loading 部件 min-block-size 覆盖槽。 | | `--xh-cascader-loading-min-w` | `loading` | `min-inline-size` | `default` | `--xh-cascader-column-min-w` | cascader 的 loading 部件 min-inline-size 覆盖槽。 | | `--xh-cascader-loading-p` | `loading` | `padding` | `default` | `--xh-space-3` | cascader 的 loading 部件 padding 覆盖槽。 | | `--xh-cascader-placeholder-fg` | `value-text` | `color` | `placeholder` | `--xh-fg-subtle` | cascader 的 value-text 部件 color 覆盖槽。 | | `--xh-cascader-search-divider` | `input` | `border-block-end` | `default` | `--xh-material-frosted-separator` | cascader 的 input 部件 border-block-end 覆盖槽。 | | `--xh-cascader-search-list-gap` | `search-list` | `gap` | `default` | `--xh-list-option-gap` | cascader 的 search-list 部件 gap 覆盖槽。 | | `--xh-cascader-search-p` | `search-list` | `padding` | `default` | `--xh-space-1` | cascader 的 search-list 部件 padding 覆盖槽。 | | `--xh-cascader-trigger-fg` | `trigger` | `color` | `default` | `--xh-fg-default` | cascader 的 trigger 部件 color 覆盖槽。 | | `--xh-cascader-trigger-font-size` | `trigger` | `font-size` | `default` | `--xh-_cascader-font-size` | cascader 的 trigger 部件 font-size 覆盖槽。 | | `--xh-cascader-trigger-gap` | `trigger` | `gap` | `default` | `--xh-_cascader-gap` | cascader 的 trigger 部件 gap 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 切换 · 出现(锚定列表)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-fade-in` · `xh-overlay-slide-in` · `xh-overlay-slide-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`rotate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/checkbox-group # CheckboxGroup 复选框组 从一组选项中选择任意多项。 ## 用法 从一组选项中选择任意多项 ```vue ``` ```html
通知方式
邮件
短信
推送通知
``` ## 组件结构 加粗的是必需部件。 `data-scope="checkbox-group"`:**`root`** · `label` · **`item`** · `indicator` · `item-text` · `hidden-input` · `select-all-trigger` ## 示例 ### 全选与半选 使用 itemValues 计算全选和半选状态 ```vue ``` ```html
通知方式
全选
邮件
短信
推送通知
``` ### 横向排布 使用 orientation 设置排列方向 ```vue ``` ```html
通知渠道
邮件
短信
推送
``` ### 禁用与只读 禁用项不可操作,只读项仍可聚焦 ```vue ``` ```html
整组禁用
芝士
培根
整组只读
芝士
培根
单项禁用
芝士
松露
``` ### 尺寸 size 决定方框与条目文字的几何档位,组标题不随档 ```vue ``` ```html
sm
邮件
短信
md
邮件
短信
lg
邮件
短信
``` ## 设计指引 ### 何时使用 - 用于偏好设置、筛选条件和批量选择。 ### 何时不用 - 选项较多或需要搜索时,使用[选择器](./select)的多选或[穿梭框](./transfer)。 - 选项互斥时,使用[单选组](./radio-group)。 ### 特性 - `collection` 提供选项文本与禁用状态。 - 全选触发器自动计算全选与半选状态。 - `orientation` 设置横向或纵向排列。 - 方框是字段家族的控制盒:不填底、描边与无影,勾中后以语气色填充;整行接 Action Control row 档,悬停 / 按下换面不缩放,方框随行换到承载面阶梯的下一档。 ### 组合 - 每一项都是一个[复选框](./checkbox),全选触发器是组内额外的一项,半选状态由组计算。 - 在[表单](./form)中以整组的值数组作为一个字段参与校验与提交。 ### 最佳实践 - 使用简短、互不重叠的选项标签。 - 保持选项顺序稳定。 ### 反模式 - 用复选框组表达互斥选项。 - 将全选项放在列表末尾。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCheckboxGroupIndicator` `XhCheckboxGroupItem` `XhCheckboxGroupItemText` `XhCheckboxGroupLabel` `XhCheckboxGroupRoot` `XhCheckboxGroupSelectAllTrigger` | | 组合式函数 | `useCheckboxGroup` | | 状态机 | `checkboxGroupMachine` | | 皮肤 | `@xihan-ui/styles/checkbox-group.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `collection` | `CheckboxGroupNode[]` | | 条目数据,显示文本与禁用的事实源。提供后条目部件只需声明 value。 未提供时回到文本与禁用都写在条目部件上的方式。 | | `value` | `string[]` | | 选中值集合。提供即受控:cell 直读 prop,写入只发 onValueChange 不落内部值。 | | `defaultValue` | `string[]` | | | | `itemValues` | `string[]` | | 组内全部条目的值,按书写顺序声明;未提供时 checkedState 退化为 unchecked / indeterminate 两态。 | | `disabled` | `boolean` | | 整组禁用:每一项随之禁用,且隐藏输入不参与提交。 | | `readOnly` | `boolean` | | 只读:仍可聚焦与朗读,但用户不可修改。 | | `invalid` | `boolean` | | 校验失败标注,写入每个条目的 aria-invalid。 | | `name` | `string` | | 表单字段名;提供后每个条目的隐藏输入才带 name,同名多值一并提交。 | | `orientation` | `Orientation` | | 视觉排布,默认 vertical。只输出 data-orientation,不输出 aria-orientation。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定勾选方框使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,决定方框与文字的几何档位。 | | `onValueChange` | `(details: CheckboxGroupValueChangeDetails) => void` | | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 | ### CheckboxGroupNode `collection` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | 展示文本;默认回退为 value。 | | `disabled` | `boolean` | | 条目禁用:仍可聚焦、仍占一个 Tab 停靠点,但不可修改,全选也跳过它。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `CheckboxGroupValueChangeDetails` | 选中值变化;detail 为 `{ value: string[] }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCheckboxGroupRoot` | `default` | `CheckboxGroupRootSlotProps` | | | `XhCheckboxGroupRoot` | `label` | — | | | `XhCheckboxGroupRoot` | `item` | `CheckboxGroupNodeMeta` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCheckboxGroupItem` | `value` | `string` | 是 | | | `XhCheckboxGroupItem` | `disabled` | `boolean` | | 默认交给 connect 查询 collection,写死 false 会覆盖数据中的禁用。 | | `XhCheckboxGroupRoot` | `label` | `ReactNode` | | 标题文字。提供后不必再写 label 部件。 | | `XhCheckboxGroupRoot` | `renderItem` | `(node: CheckboxGroupNodeMeta) => ReactNode` | | 每个条目的自定义内容;未提供时使用 collection 中的 label。 | | `XhCheckboxGroupRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `item` | 'checked' \| 'unchecked' | | `indicator` | 'checked' \| 'unchecked' | | `item-text` | 'checked' \| 'unchecked' | | `hidden-input` | 'checked' \| 'unchecked' | | `select-all-trigger` | resolveCheckedState(value, prop('itemValues') ?? []) | 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`VALUE.SET` · `ITEM.TOGGLE` · `ALL.TOGGLE` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`editable` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string[]` | | | `collection` | `readonly CheckboxGroupNodeMeta[]` | 由 collection 推导的条目元信息,按数据顺序排列;未提供 collection 时为空数组。 | | `checkedState` | `CheckboxGroupCheckedState` | | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | | | `isChecked` | `(value: string) => boolean` | | | `setValue` | `(next: string[]) => void` | 整体替换选中集合。程序化入口,不受 readOnly 拦截。 | | `toggleValue` | `(value: string) => void` | 切换某个值;整组禁用或只读时无效。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['element']` | | | `getItemProps` | `(props: CheckboxGroupItemProps) => T['element']` | | | `getIndicatorProps` | `(props: CheckboxGroupItemProps) => T['element']` | | | `getItemTextProps` | `(props: CheckboxGroupItemProps) => T['element']` | | | `getHiddenInputProps` | `(props: CheckboxGroupItemProps) => T['input']` | 条目的表单影子:一份视觉隐藏的原生 checkbox,由条目内部渲染。 | | `getSelectAllTriggerProps` | `() => T['element']` | 全选 / 半选的父复选框。必须写在 root 之内,它依靠祖先链找到本组。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Tab` / `Shift+Tab` | focus enters or leaves the group | 组内有几个条目就有几个 Tab 停靠点(禁用条目也留一个),容器自己不占位;单选组的"整组一个停靠点"在这里不成立 | | `Space` | focus on item, group editable and item not disabled | 翻转该条目的选中态;改不动时放行按键给页面滚动 | | `Space` | focus on select-all-trigger, group editable | 可用条目未全选则一并勾上,已全选则一并取消;禁用条目不受影响 | | `Space` | held on item / select-all-trigger, group editable and item not disabled | 按住期间该行投影 data-pressed,与指针 :active 同一副按压面(行换面、方框随行换底,不缩放);抬起或失焦撤下,按住途中整组转入禁用或只读也撤下。role=checkbox 只有 Space 是激活键,Enter 不进按压面;选中与按压互相独立 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-labelledby` | `label` 部件的 id | | `root` | `role` | 'group' | | `item` | `aria-checked` | 'true' \| 'false' | | `item` | `aria-disabled` | 'true' \| 'false' | | `item` | `aria-invalid` | 'true' \| 'false' | | `item` | `aria-readonly` | 'true' \| 'false' | | `item` | `role` | 'checkbox' | | `indicator` | `aria-hidden` | 'true' | | `hidden-input` | `aria-hidden` | 'true' | | `select-all-trigger` | `aria-checked` | 'true' \| 'mixed' \| 'false' | | `select-all-trigger` | `aria-disabled` | 'false' \| 'true' | | `select-all-trigger` | `aria-labelledby` | `label` 部件的 id `select-all-trigger` 部件的 id | | `select-all-trigger` | `aria-readonly` | 'true' \| 'false' | | `select-all-trigger` | `role` | 'checkbox' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/checkbox-group.css` 使用 `[data-scope="checkbox-group"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-orientation` | props.orientation | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-pressed` | ''(条件成立时才出现) | | `item` | `data-state` | 'checked' \| 'unchecked' | | `item` | `data-xh-action-control` | '' | | `item` | `data-xh-action-display` | 'always' | | `item` | `data-xh-action-profile` | 'row' | | `item` | `data-xh-action-size` | 'xs' | | `item` | `data-xh-action-variant` | 'ghost' | | `indicator` | `data-disabled` | ''(条件成立时才出现) | | `indicator` | `data-state` | 'checked' \| 'unchecked' | | `item-text` | `data-disabled` | ''(条件成立时才出现) | | `item-text` | `data-state` | 'checked' \| 'unchecked' | | `hidden-input` | `data-disabled` | ''(条件成立时才出现) | | `hidden-input` | `data-state` | 'checked' \| 'unchecked' | | `select-all-trigger` | `data-disabled` | ''(条件成立时才出现) | | `select-all-trigger` | `data-pressed` | ''(条件成立时才出现) | | `select-all-trigger` | `data-readonly` | ''(条件成立时才出现) | | `select-all-trigger` | `data-state` | resolveCheckedState(value, prop('itemValues') ?? []) | | `select-all-trigger` | `data-xh-action-control` | '' | | `select-all-trigger` | `data-xh-action-display` | 'always' | | `select-all-trigger` | `data-xh-action-profile` | 'row' | | `select-all-trigger` | `data-xh-action-size` | 'xs' | | `select-all-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-checkbox-group-gap` | `root` | `gap` | `default` | `--xh-space-2` | checkbox-group 的 root 部件 gap 覆盖槽。 | | `--xh-checkbox-group-icon-size` | `root` | `--xh-icon-size` | `default` | `--xh-_checkbox-group-glyph` | checkbox-group 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-checkbox-group-indicator-bg` | `indicator`
`root`
`select-all-trigger` | `background-color` | `default` | `transparent` | checkbox-group 的 indicator、root、select-all-trigger 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-indicator-bg-checked` | `indicator`
`select-all-trigger` | `background-color` | `is([data-state='checked'], [data-state='indeterminate'])`
`state=checked`
`state=indeterminate` | `--xh-_checkbox-group-accent` | checkbox-group 的 indicator、select-all-trigger 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-indicator-bg-checked-pressed` | `indicator`
`item`
`root`
`select-all-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`is([data-state='checked'], [data-state='indeterminate'])`
`not([data-disabled])`
`not([data-readonly])`
`pressed`
`readonly`
`state=checked`
`state=indeterminate` | `--xh-_tone-active` | checkbox-group 的 indicator、item、root、select-all-trigger 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-indicator-bg-disabled` | `indicator`
`item`
`root`
`select-all-trigger` | `background-color` | `disabled` | `--xh-bg-subtle` | checkbox-group 的 indicator、item、root、select-all-trigger 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-indicator-bg-pressed` | `indicator`
`item`
`root`
`select-all-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`not([data-disabled])`
`not([data-readonly])`
`pressed`
`readonly` | `--xh-_checkbox-group-host-bg-pressed` | checkbox-group 的 indicator、item、root、select-all-trigger 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-indicator-border` | `indicator`
`select-all-trigger` | `border` | `default` | `--xh-border-control` | checkbox-group 的 indicator、select-all-trigger 部件 border 覆盖槽。 | | `--xh-checkbox-group-indicator-border-checked` | `indicator`
`select-all-trigger` | `border-color` | `is([data-state='checked'], [data-state='indeterminate'])`
`state=checked`
`state=indeterminate` | `--xh-_checkbox-group-accent` | checkbox-group 的 indicator、select-all-trigger 部件 border-color 覆盖槽。 | | `--xh-checkbox-group-indicator-border-disabled` | `indicator`
`item`
`root`
`select-all-trigger` | `border-color` | `disabled` | `--xh-border-default` | checkbox-group 的 indicator、item、root、select-all-trigger 部件 border-color 覆盖槽。 | | `--xh-checkbox-group-indicator-border-hover` | `indicator`
`item`
`root`
`select-all-trigger` | `border-color` | `@media (hover: hover)`
`disabled`
`hover`
`invalid`
`not([data-disabled])`
`not([data-invalid])`
`not([data-readonly])`
`not([data-state='checked'])`
`not([data-state='indeterminate'])`
`readonly`
`state=checked`
`state=indeterminate` | `--xh-border-control-hover` | checkbox-group 的 indicator、item、root、select-all-trigger 部件 border-color 覆盖槽。 | | `--xh-checkbox-group-indicator-border-invalid` | `indicator`
`root` | `border-color` | `invalid` | `--xh-border-invalid` | checkbox-group 的 indicator、root 部件 border-color 覆盖槽。 | | `--xh-checkbox-group-indicator-fg` | `indicator`
`select-all-trigger` | `background-color`
`color` | `default`
`state=checked`
`state=indeterminate` | `--xh-_checkbox-group-on-accent` | checkbox-group 的 indicator、select-all-trigger 部件 background-color、color 覆盖槽。 | | `--xh-checkbox-group-indicator-fg-disabled` | `indicator`
`item`
`root`
`select-all-trigger` | `color` | `disabled` | `--xh-fg-disabled` | checkbox-group 的 indicator、item、root、select-all-trigger 部件 color 覆盖槽。 | | `--xh-checkbox-group-indicator-font-size` | `indicator`
`select-all-trigger` | `font-size` | `default`
`state=checked`
`state=indeterminate` | `--xh-_checkbox-group-glyph` | checkbox-group 的 indicator、select-all-trigger 部件 font-size 覆盖槽。 | | `--xh-checkbox-group-indicator-radius` | `indicator`
`select-all-trigger` | `border-radius` | `default` | `--xh-shape-inset` | checkbox-group 的 indicator、select-all-trigger 部件 border-radius 覆盖槽。 | | `--xh-checkbox-group-indicator-shadow` | `indicator`
`select-all-trigger` | `box-shadow` | `default` | `none` | checkbox-group 的 indicator、select-all-trigger 部件 box-shadow 覆盖槽。 | | `--xh-checkbox-group-indicator-size` | `indicator`
`select-all-trigger` | `block-size`
`inline-size`
`margin-inline-start` | `default`
`state=checked`
`state=indeterminate` | `--xh-_checkbox-group-box` | checkbox-group 的 indicator、select-all-trigger 部件 block-size、inline-size、margin-inline-start 覆盖槽。 | | `--xh-checkbox-group-item-bg-hover` | `item` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-bg-subtle` | checkbox-group 的 item 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-item-bg-pressed` | `item` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-bg-subtle-hover` | checkbox-group 的 item 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-item-fg` | `item` | `color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-default` | checkbox-group 的 item 部件 color 覆盖槽。 | | `--xh-checkbox-group-item-fg-disabled` | `item` | `color` | `disabled` | `--xh-fg-disabled` | checkbox-group 的 item 部件 color 覆盖槽。 | | `--xh-checkbox-group-item-font-size` | `item` | `font-size` | `default` | `--xh-_checkbox-group-font-size` | checkbox-group 的 item 部件 font-size 覆盖槽。 | | `--xh-checkbox-group-item-gap` | `item` | `gap` | `default` | `--xh-_checkbox-group-gap` | checkbox-group 的 item 部件 gap 覆盖槽。 | | `--xh-checkbox-group-item-radius` | `item` | `border-radius` | `default` | `--xh-shape-control` | checkbox-group 的 item 部件 border-radius 覆盖槽。 | | `--xh-checkbox-group-label-fg` | `label` | `color` | `default` | `--xh-fg-muted` | checkbox-group 的 label 部件 color 覆盖槽。 | | `--xh-checkbox-group-label-fg-disabled` | `label`
`root` | `color` | `disabled` | `--xh-fg-subtle` | checkbox-group 的 label、root 部件 color 覆盖槽。 | | `--xh-checkbox-group-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | checkbox-group 的 label 部件 font-size 覆盖槽。 | | `--xh-checkbox-group-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | checkbox-group 的 label 部件 font-weight 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-bg-hover` | `select-all-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-bg-subtle` | checkbox-group 的 select-all-trigger 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-bg-pressed` | `select-all-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-bg-subtle-hover` | checkbox-group 的 select-all-trigger 部件 background-color 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-fg` | `select-all-trigger` | `color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-default` | checkbox-group 的 select-all-trigger 部件 color 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-fg-disabled` | `select-all-trigger` | `color` | `disabled` | `--xh-fg-disabled` | checkbox-group 的 select-all-trigger 部件 color 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-font-size` | `select-all-trigger` | `font-size` | `default` | `--xh-_checkbox-group-font-size` | checkbox-group 的 select-all-trigger 部件 font-size 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-font-weight` | `select-all-trigger` | `font-weight` | `default` | `--xh-font-weight-medium` | checkbox-group 的 select-all-trigger 部件 font-weight 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-gap` | `select-all-trigger` | `gap` | `default` | `--xh-_checkbox-group-gap` | checkbox-group 的 select-all-trigger 部件 gap 覆盖槽。 | | `--xh-checkbox-group-select-all-trigger-radius` | `select-all-trigger` | `border-radius` | `default` | `--xh-shape-inset` | checkbox-group 的 select-all-trigger 部件 border-radius 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 `background-color` · `border-color` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 皮肤另按输入能力分档:`hover: hover` · `pointer: coarse`:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/checkbox # Checkbox 复选框 用于选择一个或多个独立选项。 ## 用法 标记一个独立选项 ```vue ``` ```html ``` ## 组件结构 加粗的是必需部件。 `data-scope="checkbox"`:**`root`** · `indicator` · `hidden-input` · `label` · `text` ## 示例 ### 不确定状态 表示部分选中 ```vue ``` ```html ``` ### 颜色 tone 决定勾中后方框使用哪族颜色,因此这里都设为勾中 ```vue ``` ```html
``` ### 尺寸 适配不同的界面密度 ```vue ``` ```html
``` ### 禁用与只读 区分不可用与不可修改状态 ```vue ``` ```html
``` ### 校验状态 标记必须处理的选项 ```vue ``` ```html ``` ## 设计指引 ### 何时使用 - 表单中的同意、订阅或启用选项。 - 需要表达部分选中的汇总状态。 ### 何时不用 - 立即生效的设置使用[开关](./switch)。 - 互斥选择使用[单选组](./radio-group)。 - 管理一组值时使用[复选框组](./checkbox-group)。 ### 特性 - 支持选中、未选中与 `indeterminate` 状态。 - 方框是字段家族的控制盒:不填底、描边与无影,勾中后以语气色填充,按下缩放并换底。 - `readOnly` 仍可聚焦并参与提交,`disabled` 不参与提交。 - 标签、三档尺寸、校验状态和自定义指示器均使用同一状态动画。 - `name` 与 `value` 通过隐藏字段参与原生表单。 ### 组合 - 将文字直接放入默认插槽,整行即可点击。 - 成组选择使用[复选框组](./checkbox-group)。 ### 最佳实践 - 始终提供可见标签或 `aria-label`。 - 半选只用于表示下级选项的汇总状态。 ### 反模式 - 不要用复选框表达互斥选项。 - 不要将半选状态作为第三个业务值。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCheckbox` | | 组合式函数 | `useCheckbox` | | 状态机 | `checkboxMachine` | | 皮肤 | `@xihan-ui/styles/checkbox.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `checked` | `CheckboxCheckedState` | | | | `defaultChecked` | `CheckboxCheckedState` | | | | `disabled` | `boolean` | | | | `readOnly` | `boolean` | | 只读:不可勾选,但仍可聚焦、仍参与提交,对比度不降低。 | | `invalid` | `boolean` | | 校验失败:只改变呈现,不阻止交互。 | | `required` | `boolean` | | 必填:随表单校验一起使用,只发无障碍属性,不自行拦截提交。 | | `name` | `string` | | 表单字段名;提供后 hidden-input 才带 name 并参与提交。 | | `value` | `string` | | 提交的值,默认 'on',与原生复选框一致。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定选中态使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,决定方框边长与勾选符号的字号档位。 | | `onCheckedChange` | `(details: CheckboxCheckedChangeDetails) => void` | | checked 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `checked-change` | `CheckboxCheckedChangeDetails` | checked 状态变化;detail 为 `{ checked: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCheckbox` | `default` | — | 方框旁的文字;未写时只有一个方框。 | | `XhCheckbox` | `indicator` | — | 方框中的图形;未写时由皮肤绘制勾选标记。 | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCheckbox` | `indicator` | `ReactNode` | | 方框中的图形;未写时由皮肤绘制勾选标记。 | | `XhCheckbox` | `children` | `ReactNode` | | 方框旁的文字;未写时只有一个方框。 | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'indeterminate' \| 'checked' \| 'unchecked' | | `indicator` | 'indeterminate' \| 'checked' \| 'unchecked' | | `label` | 'indeterminate' \| 'checked' \| 'unchecked' | | `text` | 'indeterminate' \| 'checked' \| 'unchecked' | 以下名称仅用于内部状态机。 **状态**:`off` · `on` · `indeterminate` **事件**:`TOGGLE` · `CHECK` · `UNCHECK` · `CONTROLLED.ON` · `CONTROLLED.OFF` · `CONTROLLED.INDETERMINATE` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`isCheckedControlled` · `defaultsToChecked` · `defaultsToIndeterminate` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `checked` | `CheckboxCheckedState` | | | `setChecked` | `(next: boolean) => void` | 半选只能由 checked prop 给出,这里只接受全选 / 全不选。 | | `getRootProps` | `() => T['button']` | | | `getIndicatorProps` | `() => T['element']` | | | `getHiddenInputProps` | `() => T['input']` | 表单影子:勾选后才提交,半选按未勾选处理。提供 name 后才带 name。 | | `getLabelProps` | `() => T['label']` | 包裹方框与文字的 <label>:点击文字即切换,方框的可及名来自文字。只在带文字时渲染。 | | `getTextProps` | `() => T['element']` | 方框旁的文字。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Space` / `Enter` | focus in root, not disabled | 切换 checked 状态 | | `Space` / `Enter` | held in root, not disabled, not readOnly | 按住期间投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,按住途中转入禁用或只读也撤下。与勾选态互相独立 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-checked` | 'mixed' \| 'true' \| 'false' | | `root` | `aria-invalid` | 'true' \| 'false' | | `root` | `aria-readonly` | 'true' \| 'false' | | `root` | `aria-required` | 'true' \| 'false' | | `root` | `role` | 'checkbox' | | `indicator` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/checkbox.css` 使用 `[data-scope="checkbox"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-pressed` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-required` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `root` | `data-tone` | props.tone | | `root` | `data-xh-action-control` | '' | | `root` | `data-xh-action-display` | 'always' | | `root` | `data-xh-action-profile` | 'icon' | | `root` | `data-xh-action-size` | props.size | | `root` | `data-xh-action-variant` | 'outline' | | `indicator` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `label` | `data-disabled` | ''(条件成立时才出现) | | `label` | `data-invalid` | ''(条件成立时才出现) | | `label` | `data-readonly` | ''(条件成立时才出现) | | `label` | `data-size` | props.size | | `label` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | | `text` | `data-disabled` | ''(条件成立时才出现) | | `text` | `data-invalid` | ''(条件成立时才出现) | | `text` | `data-state` | 'indeterminate' \| 'checked' \| 'unchecked' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-checkbox-bg` | `root` | `--xh-ink-surface`
`background-color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`readonly`
`xh-ink-surface` | `transparent` | checkbox 的 root 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-checkbox-bg-checked` | `root` | `--xh-ink-surface`
`background-color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`readonly`
`state=checked`
`state=indeterminate`
`xh-ink-surface` | `--xh-_checkbox-accent` | checkbox 的 root 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-checkbox-bg-checked-pressed` | `root` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`state=checked`
`state=indeterminate` | `--xh-_tone-active` | checkbox 的 root 部件 background-color 覆盖槽。 | | `--xh-checkbox-bg-disabled` | `root` | `--xh-ink-surface`
`background-color` | `disabled`
`xh-ink-surface` | `--xh-bg-subtle` | checkbox 的 root 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-checkbox-bg-pressed` | `root` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-bg-subtle-hover` | checkbox 的 root 部件 background-color 覆盖槽。 | | `--xh-checkbox-border` | `label`
`root` | `border`
`border-color` | `@media (hover: hover)`
`contrast=more`
`default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([data-readonly])`
`pressed`
`readonly`
`state=unchecked`
`where([data-contrast='more'])` | `--xh-border-control`
`--xh-border-strong` | checkbox 的 label、root 部件 border、border-color 覆盖槽。 | | `--xh-checkbox-border-checked` | `label`
`root` | `border`
`border-color` | `@media (hover: hover)`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([data-readonly])`
`pressed`
`readonly`
`state=checked`
`state=indeterminate` | `--xh-_checkbox-accent` | checkbox 的 label、root 部件 border、border-color 覆盖槽。 | | `--xh-checkbox-border-disabled` | `root` | `border-color` | `disabled` | `--xh-border-default` | checkbox 的 root 部件 border-color 覆盖槽。 | | `--xh-checkbox-border-hover` | `label`
`root` | `border-color` | `@media (hover: hover)`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([data-readonly])`
`pressed`
`readonly` | `--xh-border-control-hover` | checkbox 的 label、root 部件 border-color 覆盖槽。 | | `--xh-checkbox-border-invalid` | `label`
`root` | `border`
`border-color` | `@media (hover: hover)`
`disabled`
`focus-visible`
`hover`
`invalid`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`not([data-readonly])`
`pressed`
`readonly`
`state=checked`
`state=indeterminate` | `--xh-border-invalid` | checkbox 的 label、root 部件 border、border-color 覆盖槽。 | | `--xh-checkbox-fg` | `root` | `color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_checkbox-on-accent` | checkbox 的 root 部件 color 覆盖槽。 | | `--xh-checkbox-fg-disabled` | `indicator`
`root` | `background-color`
`color` | `disabled`
`state=indeterminate` | `--xh-fg-disabled` | checkbox 的 indicator、root 部件 background-color、color 覆盖槽。 | | `--xh-checkbox-fg-invalid` | `label`
`text` | `color` | `invalid` | `--xh-fg-danger` | checkbox 的 label、text 部件 color 覆盖槽。 | | `--xh-checkbox-icon-size` | `root` | `--xh-icon-size` | `default` | `--xh-_checkbox-glyph` | checkbox 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-checkbox-indicator-fg` | `indicator` | `background-color` | `state=indeterminate` | `--xh-_checkbox-on-accent` | checkbox 的 indicator 部件 background-color 覆盖槽。 | | `--xh-checkbox-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | checkbox 的 label 部件 color 覆盖槽。 | | `--xh-checkbox-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | checkbox 的 label 部件 color 覆盖槽。 | | `--xh-checkbox-label-font-size` | `label` | `font-size` | `default` | `--xh-_checkbox-label-font-size` | checkbox 的 label 部件 font-size 覆盖槽。 | | `--xh-checkbox-label-gap` | `label` | `gap` | `default` | `--xh-_checkbox-label-gap` | checkbox 的 label 部件 gap 覆盖槽。 | | `--xh-checkbox-label-leading` | `label` | `line-height` | `default` | `--xh-leading-normal` | checkbox 的 label 部件 line-height 覆盖槽。 | | `--xh-checkbox-radius` | `root` | `border-radius` | `default` | `--xh-shape-inset` | checkbox 的 root 部件 border-radius 覆盖槽。 | | `--xh-checkbox-shadow` | `root` | `box-shadow` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `none` | checkbox 的 root 部件 box-shadow 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 切换(见[动效规范](../design/motion#角色))。 `opacity` · `scale` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 皮肤另按输入能力分档:`hover: hover` · `pointer: coarse`:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/clipboard # Clipboard 剪贴板 用于复制纯文本并反馈复制状态。 ## 用法 复制安装命令 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="clipboard"`:**`root`** · `label` · `control` · `input` · **`copy-trigger`** · `indicator` · `status` ## 示例 ### 独立按钮 内容已在页面中展示时,只保留复制按钮 ```vue ``` ```html
``` ### 变体 设置复制按钮的外观 ```vue ``` ```html
``` ### 尺寸 使用小、中、大三档尺寸 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 复制命令、链接、密钥或标识符。 - 需要在复制前让用户核对内容。 ### 何时不用 - 复制富文本或图片。 - 内容需要先编辑时,使用[文本字段](./text-field)。 ### 特性 - 只读输入框在聚焦时自动选中文本。 - 复制状态依次为 `idle`、`copying` 与 `copied`。 - `timeout` 控制成功状态的停留时间。 - 输入框、标签与状态提示均可按场景省略。 - 复制按钮缺省是中性淡底 `subtle`,只有 `solid` 才是品牌实心;按下有统一的缩放与换底反馈。 ### 组合 - 与[代码视图](./code-view)组合复制代码。 - 使用 `indicator` 切换复制前后的图标或文字。 ### 最佳实践 - 保留可见文本,让用户可以核对并手动复制。 - 复制按钮使用明确的可访问名称。 - 默认使用中性工具面;只有复制是页面主操作时才使用 `solid`。 - 成功反馈只替换图标与文字,不改变控件尺寸或轮廓。 ### 反模式 - 不要将复制成功作为同步结果处理。 - 不要复制用户无法核对的隐藏内容。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhClipboardControl` `XhClipboardCopyTrigger` `XhClipboardIndicator` `XhClipboardInput` `XhClipboardLabel` `XhClipboardRoot` `XhClipboardStatus` | | 组合式函数 | `useClipboard` | | 状态机 | `clipboardMachine` | | 皮肤 | `@xihan-ui/styles/clipboard.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | | 要复制的文本;未提供时复制空串。 | | `timeout` | `number` | | 复制成功后指示器保持多久(毫秒),默认 3000;<=0 或非有限数表示不自动回落。 | | `disabled` | `boolean` | | 禁用:复制按钮不可点击,作者调用 api.copy() 也无效(守卫在状态机层)。 | | `variant` | `ActionVariant` | | 变体:solid / subtle / outline / ghost,默认 subtle(缺省中性淡底,solid 才品牌实心)。 | | `tone` | `Tone` | | 颜色:brand / neutral / success / warning / danger / info。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `translations` | `Partial` | | | | `onStatusChange` | `(details: ClipboardStatusChangeDetails) => void` | | 状态每次落定时通知一次;挂载时的 idle 是初始态,不通知。 | | `onCopyError` | `(details: ClipboardCopyErrorDetails) => void` | | 写入失败时通知;此时状态已回到 idle。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `status-change` | `ClipboardStatusChangeDetails` | 状态变化;detail 为 `{ status: 'copying' \| 'copied' \| 'idle' }` | | `copy-error` | `ClipboardCopyErrorDetails` | 写入失败;detail 为 `{ error, value }`,此时状态已回到 idle | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhClipboardRoot` | `default` | `ClipboardRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhClipboardIndicator` | `copied` | `boolean` | | 该标记属于哪一侧:true = 复制成功后的对勾,false(默认)= 平时的复制图标。 | | `XhClipboardRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'idle' \| 'copying' \| 'copied' | | `label` | 'idle' \| 'copying' \| 'copied' | | `control` | 'idle' \| 'copying' \| 'copied' | | `input` | 'idle' \| 'copying' \| 'copied' | | `copy-trigger` | 'idle' \| 'copying' \| 'copied' | | `indicator` | 'idle' \| 'copying' \| 'copied' | | `status` | 'idle' \| 'copying' \| 'copied' | 以下名称仅用于内部状态机。 **状态**:`idle` · `copying` · `copied` **事件**:`COPY.TRIGGER` · `COPY.SUCCESS` · `COPY.ERROR` · `after.timeout` · `PRESS.START` · `PRESS.END` **判据**:`isDisabled` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `status` | `ClipboardStatus` | | | `disabled` | `boolean` | | | `announcement` | `string` | 播报区未提供内容时朗读的语句;未达到已复制档时为空串。 | | `copied` | `boolean` | 已复制成功且仍在停留窗口内。指示器与样式的唯一判据。 | | `value` | `string` | 当前要复制的文本(prop 未提供时为空串)。 | | `copy` | `() => void` | 发起一次复制意图,与点击按钮走同一路径。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['label']` | | | `getControlProps` | `() => T['element']` | | | `getInputProps` | `() => T['input']` | | | `getCopyTriggerProps` | `() => T['button']` | | | `getIndicatorProps` | `(props: ClipboardIndicatorProps) => T['element']` | | | `getStatusProps` | `() => T['element']` | 复制成功的播报区,视觉隐藏;未提供内容时朗读 announcement。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | held in copy-trigger, not disabled, not copying | 按住期间投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `input` | `aria-labelledby` | `label` 部件的 id | | `copy-trigger` | `aria-busy` | 'true' \| undefined | | `copy-trigger` | `aria-disabled` | 'true' \| undefined | | `copy-trigger` | `aria-label` | translations?.copy | | `indicator` | `aria-hidden` | indicator.copied !== copied \|\| undefined | | `status` | `aria-atomic` | 'true' | | `status` | `aria-live` | 'polite' | | `status` | `role` | 'status' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/clipboard.css` 使用 `[data-scope="clipboard"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-copied` | ''(条件成立时才出现) | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'idle' \| 'copying' \| 'copied' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `label` | `data-state` | 'idle' \| 'copying' \| 'copied' | | `control` | `data-state` | 'idle' \| 'copying' \| 'copied' | | `input` | `data-state` | 'idle' \| 'copying' \| 'copied' | | `copy-trigger` | `data-copied` | ''(条件成立时才出现) | | `copy-trigger` | `data-disabled` | ''(条件成立时才出现) | | `copy-trigger` | `data-loading` | ''(条件成立时才出现) | | `copy-trigger` | `data-pressed` | ''(条件成立时才出现) | | `copy-trigger` | `data-state` | 'idle' \| 'copying' \| 'copied' | | `copy-trigger` | `data-xh-action-control` | '' | | `copy-trigger` | `data-xh-action-display` | 'always' | | `copy-trigger` | `data-xh-action-profile` | 'text' | | `copy-trigger` | `data-xh-action-size` | props.size | | `copy-trigger` | `data-xh-action-variant` | props.variant | | `copy-trigger` | `data-xh-ink-surface` | ''(条件成立时才出现) | | `indicator` | `data-copied` | ''(条件成立时才出现) | | `indicator` | `data-state` | 'idle' \| 'copying' \| 'copied' | | `status` | `data-state` | 'idle' \| 'copying' \| 'copied' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-clipboard-control-active-layer` | `control`
`copy-trigger`
`input` | `z-index` | `focus-visible`
`hover` | `1` | clipboard 的 control、copy-trigger、input 部件 z-index 覆盖槽。 | | `--xh-clipboard-control-gap` | `control` | `gap` | `default` | `0` | clipboard 的 control 部件 gap 覆盖槽。 | | `--xh-clipboard-control-min-w` | `root` | `min-inline-size` | `has([data-scope='clipboard'][data-part='control'])` | `--xh-control-min-w` | clipboard 的 root 部件 min-inline-size 覆盖槽。 | | `--xh-clipboard-control-w` | `root` | `inline-size`
`min-inline-size` | `has([data-scope='clipboard'][data-part='control'])` | `--xh-control-w` | clipboard 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-clipboard-copy-trigger-attached-radius` | `control`
`copy-trigger` | `border-end-end-radius`
`border-start-end-radius` | `not(:first-child)` | `--xh-clipboard-input-radius` | clipboard 的 control、copy-trigger 部件 border-end-end-radius、border-start-end-radius 覆盖槽。 | | `--xh-clipboard-copy-trigger-bg` | `copy-trigger` | `--xh-ink-surface`
`background-color` | `default`
`focus-visible`
`loading`
`xh-ink-surface` | `--xh-_action-variant-bg-focus-visible`
`--xh-_action-variant-bg-loading`
`--xh-_action-variant-bg-rest` | clipboard 的 copy-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-clipboard-copy-trigger-bg-active` | `copy-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | clipboard 的 copy-trigger 部件 background-color 覆盖槽。 | | `--xh-clipboard-copy-trigger-bg-disabled` | `copy-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`xh-ink-surface` | `--xh-_action-variant-bg-disabled` | clipboard 的 copy-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-clipboard-copy-trigger-bg-hover` | `copy-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | clipboard 的 copy-trigger 部件 background-color 覆盖槽。 | | `--xh-clipboard-copy-trigger-border` | `control`
`copy-trigger` | `border`
`border-color` | `default`
`focus-visible`
`not(:first-child)` | `--xh-_action-variant-border-focus-visible`
`--xh-_action-variant-border-rest`
`--xh-border-control` | clipboard 的 control、copy-trigger 部件 border、border-color 覆盖槽。 | | `--xh-clipboard-copy-trigger-border-disabled` | `copy-trigger` | `border-color` | `disabled` | `--xh-_action-variant-border-disabled` | clipboard 的 copy-trigger 部件 border-color 覆盖槽。 | | `--xh-clipboard-copy-trigger-border-hover` | `control`
`copy-trigger` | `border-color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not(:first-child)`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-border-hover`
`--xh-_action-variant-border-pressed`
`--xh-border-control-hover` | clipboard 的 control、copy-trigger 部件 border-color 覆盖槽。 | | `--xh-clipboard-copy-trigger-fg` | `copy-trigger` | `color` | `copied`
`default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`xh-action-variant=solid` | `--xh-_action-variant-fg-focus-visible`
`--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-loading`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | clipboard 的 copy-trigger 部件 color 覆盖槽。 | | `--xh-clipboard-copy-trigger-fg-copied` | `copy-trigger` | `color` | `copied`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-success` | clipboard 的 copy-trigger 部件 color 覆盖槽。 | | `--xh-clipboard-copy-trigger-font-size` | `copy-trigger` | `font-size` | `default` | `--xh-_clipboard-font-size` | clipboard 的 copy-trigger 部件 font-size 覆盖槽。 | | `--xh-clipboard-copy-trigger-font-weight` | `copy-trigger` | `font-weight` | `default` | `--xh-text-label-weight` | clipboard 的 copy-trigger 部件 font-weight 覆盖槽。 | | `--xh-clipboard-copy-trigger-gap` | `copy-trigger`
`indicator` | `gap` | `default` | `--xh-control-gap-sm` | clipboard 的 copy-trigger、indicator 部件 gap 覆盖槽。 | | `--xh-clipboard-copy-trigger-h` | `copy-trigger` | `block-size` | `default` | `--xh-_clipboard-h` | clipboard 的 copy-trigger 部件 block-size 覆盖槽。 | | `--xh-clipboard-copy-trigger-px` | `copy-trigger` | `padding-inline` | `default` | `--xh-_clipboard-px` | clipboard 的 copy-trigger 部件 padding-inline 覆盖槽。 | | `--xh-clipboard-copy-trigger-radius` | `copy-trigger` | `border-radius` | `default` | `--xh-shape-control` | clipboard 的 copy-trigger 部件 border-radius 覆盖槽。 | | `--xh-clipboard-copy-trigger-shadow-hover` | `copy-trigger` | `box-shadow` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `none` | clipboard 的 copy-trigger 部件 box-shadow 覆盖槽。 | | `--xh-clipboard-gap` | `root` | `gap` | `default` | `--xh-space-1` | clipboard 的 root 部件 gap 覆盖槽。 | | `--xh-clipboard-indicator-fg-copied` | `indicator` | `color` | `copied` | `--xh-fg-success` | clipboard 的 indicator 部件 color 覆盖槽。 | | `--xh-clipboard-indicator-gap` | `indicator` | `gap` | `default` | `--xh-clipboard-copy-trigger-gap` | clipboard 的 indicator 部件 gap 覆盖槽。 | | `--xh-clipboard-input-autofill-bg` | `input` | `box-shadow` | `-webkit-autofill`
`autofill` | `--xh-bg-subtle-opaque` | clipboard 的 input 部件 box-shadow 覆盖槽。 | | `--xh-clipboard-input-autofill-fg` | `input` | `-webkit-text-fill-color` | `-webkit-autofill`
`autofill` | `--xh-fg-default` | clipboard 的 input 部件 -webkit-text-fill-color 覆盖槽。 | | `--xh-clipboard-input-bg` | `input` | `background` | `default` | `--xh-bg-subtle` | clipboard 的 input 部件 background 覆盖槽。 | | `--xh-clipboard-input-border` | `input` | `border` | `default` | `--xh-border-control` | clipboard 的 input 部件 border 覆盖槽。 | | `--xh-clipboard-input-border-focus` | `input` | `border-color` | `focus-visible` | `--xh-border-control-focus` | clipboard 的 input 部件 border-color 覆盖槽。 | | `--xh-clipboard-input-fg` | `input` | `color` | `default` | `--xh-fg-default` | clipboard 的 input 部件 color 覆盖槽。 | | `--xh-clipboard-input-font-size` | `input` | `font-size` | `default` | `--xh-text-body-size` | clipboard 的 input 部件 font-size 覆盖槽。 | | `--xh-clipboard-input-h` | `input` | `block-size` | `default` | `--xh-_clipboard-h` | clipboard 的 input 部件 block-size 覆盖槽。 | | `--xh-clipboard-input-px` | `input` | `padding-inline` | `default` | `--xh-_clipboard-px` | clipboard 的 input 部件 padding-inline 覆盖槽。 | | `--xh-clipboard-input-radius` | `control`
`copy-trigger`
`input` | `border-end-end-radius`
`border-radius`
`border-start-end-radius` | `default`
`not(:first-child)` | `--xh-shape-control` | clipboard 的 control、copy-trigger、input 部件 border-end-end-radius、border-radius、border-start-end-radius 覆盖槽。 | | `--xh-clipboard-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | clipboard 的 label 部件 color 覆盖槽。 | | `--xh-clipboard-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | clipboard 的 label 部件 font-size 覆盖槽。 | | `--xh-clipboard-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | clipboard 的 label 部件 font-weight 覆盖槽。 | | `--xh-clipboard-loading-duration` | `copy-trigger` | `animation` | `default` | `--xh-motion-loop-spin` | clipboard 的 copy-trigger 部件 animation 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现 · 循环(见[动效规范](../design/motion#角色))。 可覆盖的动效槽:`--xh-clipboard-loading-duration`。 共享关键帧 `xh-fade-in` · `xh-fade-out` · `xh-spin` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`opacity` · `visibility` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 `prefers-reduced-motion: reduce` 下本组件另有降级规则。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/code-view # CodeView 代码视图 一段代码的逐行呈现:行号、指定行高亮、超长折叠、文件名,可选语法着色,支持流式追加时的未闭合状态。 ## 用法 代码原文由宿主提供,组件切出逐行结构并铺设记号;渲染文件名后它即成为代码块的可访问名 ```vue ``` ```html
ticker.ts typescript
export function createTicker(intervalTime: number) {
  let handle = 0
  return {
    start(onTick: () => void) {
      handle = setInterval(onTick, intervalTime)
    },
    stop() {
      clearInterval(handle)
    },
  }
}
``` ## 组件结构 加粗的是必需部件。 `data-scope="code-view"`:**`root`** · `header` · `filename` · `lang-label` · **`pre`** · **`code`** · `line` · `line-number` · `line-content` · `token` · `fold-trigger` ## 示例 ### 行号与高亮行 行号由皮肤绘制,复制代码不会带上它;高亮行按行号写,与 startLine 对齐 ```vue ``` ```html
function resolve(input: string) {
  const trimmed = input.trim()
  if (trimmed === '') {
    return null
  }
  return trimmed.toLowerCase()
}
``` ### 折叠超长代码 clamped 是纯受控的:组件只发意图,是否落实由宿主决定,便于全部展开这类操作统一持有 ```vue ``` ```html
const step1 = pipeline.at(0)
const step2 = pipeline.at(1)
const step3 = pipeline.at(2)
const step4 = pipeline.at(3)
const step5 = pipeline.at(4)
const step6 = pipeline.at(5)
const step7 = pipeline.at(6)
const step8 = pipeline.at(7)
const step9 = pipeline.at(8)
const step10 = pipeline.at(9)
const step11 = pipeline.at(10)
const step12 = pipeline.at(11)
const step13 = pipeline.at(12)
const step14 = pipeline.at(13)
const step15 = pipeline.at(14)
const step16 = pipeline.at(15)
const step17 = pipeline.at(16)
const step18 = pipeline.at(17)
const step19 = pipeline.at(18)
const step20 = pipeline.at(19)
const step21 = pipeline.at(20)
const step22 = pipeline.at(21)
const step23 = pipeline.at(22)
const step24 = pipeline.at(23)
``` ### 流式追加 代码仍在写入时默认不着色:不完整代码的词法本就不稳定,每来一个字符整块变色比不着色更差 ```vue ``` ```html
``` ### 头部内建复制 复制交给剪贴板:把它放进头部条,用几个槽把描边按钮压为安静形态,1500 毫秒后自动回落 ```vue ``` ```html
store.ts
export function createStore(reduce: Reducer, initial: State) {
  let state = initial
  return {
    get: () => state,
    dispatch(action: Action) {
      state = reduce(state, action)
    },
  }
}
``` ### 着色端口 着色是可替换的端口:无法识别的语言退回纯文本,接入自己的实现时组件侧无需修改,传 null 则整个关闭 ```vue ``` ```html
``` ### 流式期间也着色 未闭合默认不着色;确需着色时开启 highlight-while-streaming,同一段不完整代码的两种呈现并排对照 ```vue ``` ```html
const stream = await client.chat({
  model: 'demo',
  messages,
  onToken(token) {
    buffer +=
const stream = await client.chat({
  model: 'demo',
  messages,
  onToken(token) {
    buffer +=
``` ### 尺寸 size 切换字号、行高与内边距三档,行号槽与折叠按钮随之变化 ```vue ``` ```html
clamp.sm.ts
export function clamp(n: number, min: number, max: number) {
  return Math.min(Math.max(n, min), max)
}
clamp.md.ts
export function clamp(n: number, min: number, max: number) {
  return Math.min(Math.max(n, min), max)
}
clamp.lg.ts
export function clamp(n: number, min: number, max: number) {
  return Math.min(Math.max(n, min), max)
}
``` ## 设计指引 ### 何时使用 - 在 AI 回复、文档、评审意见中展示代码,需要行号或需要指出某几行。 - 代码是流式生成的,需要边接收边渲染,闭合之后再着色。 - 代码较长,默认只显示前若干行。 ### 何时不用 - 只是一小段行内标识时,使用[排印](./typography)的 `code` 形态。 - 展示运行日志时,使用[日志](./log)。 - 展示改动前后时,使用[差异视图](./diff-view)。 ### 特性 - 逐行切分在连接层完成。一个记号可以横跨多行(未闭合的字符串与块注释),因此行号与高亮行不能由皮肤反推。 - `complete` 标记这段代码是否已经写完。未闭合时默认不着色:半截代码的词法不稳定,逐字符变色比不着色更差。 - `highlighter` 是着色端口,由宿主决定接入哪个着色器;返回 `null` 是合法结果,回到纯文本。适配器默认接 `@xihan-ui/code-highlight`,它是可选 peer:已安装时自动着色,未安装时保持纯文本。适配器显式传 `null` 时不请求默认模块;只有模块缺席才回到纯文本,已安装模块的加载或初始化异常照常抛出。 - 行号由皮肤用 `attr()` 绘制,复制代码不会带上行号,读屏也不会逐行读出数字。 - `clamped` 是纯受控的:折叠状态通常由外部“全部展开 / 全部折叠”统一持有,内建状态会与之冲突。 ### 组合 - 与[剪贴板](./clipboard)配合提供复制;需要非受控折叠时放入[折叠区域](./collapsible)。把剪贴板的三个部件放进 `header`,再用 `--xh-clipboard-copy-trigger-border: transparent`、`--xh-clipboard-copy-trigger-bg: transparent`、`--xh-clipboard-copy-trigger-h: var(--xh-control-h-sm)` 三个槽把按钮调整为头部内的低强调形态。 - 内建词法只区分注释、字符串、数字、关键字、标点五档。需要区分函数名、类型名、属性名时,自行实现 `highlighter` 端口(同步纯函数,可接 Shiki 等)传入,皮肤按记号种类上色的规则不变。 - 放进 AI 回复正文时由[流式正文](./markdown-stream)交付代码块。 ### 最佳实践 - 标出语言,读者与着色器都需要它。 - 高亮行用于指出重点,不一次点亮半屏。 - 折叠阈值取十几行:过少时读者每次都要展开,过多时折叠失去意义。 ### 反模式 - 把代码放进普通段落,空白与换行会被折叠。 - 用行号作为跳转锚点,它是绘制上去的,DOM 中不可选中。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCodeViewCode` `XhCodeViewFilename` `XhCodeViewFoldTrigger` `XhCodeViewHeader` `XhCodeViewLangLabel` `XhCodeViewPre` `XhCodeViewRoot` | | 组合式函数 | `useCodeView` | | 状态机 | `codeViewMachine` | | 皮肤 | `@xihan-ui/styles/code-view.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `code` | `string` | 是 | | | `lang` | `string` | | 围栏语言标注,空白一律落为 plaintext。 | | `filename` | `string` | | 文件名,渲染在 header 中;渲染之后它即为 pre 的可访问名。 | | `labelled` | `boolean` | | 作者渲染了 filename 部件时置真,由适配器统计而不是判断 filename 是否有值。 为假时 pre 用 translations.code 兜底:指向未渲染的 id 会使读屏读空。 | | `complete` | `boolean` | | 代码是否已闭合,未闭合时按行数预撑高度且默认不着色。 | | `wrap` | `boolean` | | 长行自动换行,默认关闭(长行横向滚动)。 | | `lineNumbers` | `boolean` | | 渲染行号槽。 | | `startLine` | `number` | | 首行的行号,默认 1;摘录与 patch 片段需要使用。 | | `highlightLines` | `string \| readonly number[]` | | 要高亮的行号,写为 `'3,7-9'` 或行号数组;非法片段丢弃不报错。 | | `clamp` | `number` | | 超过该行数才视为可折叠。 | | `clamped` | `boolean` | | 折叠态,纯受控:没有 defaultClamped,需要非受控时套用 collapsible。 | | `highlighter` | `HighlighterPort` | | 着色实现。未提供时为纯文本,提供后也允许返回 null(语言未识别等),同样回退为纯文本。 未闭合的块默认不着色,见 {@link highlightWhileStreaming}。 | | `highlightWhileStreaming` | `boolean` | | 块尚未闭合时也着色,默认 false。 默认关闭是因为未闭合代码的词法本身不稳定:引号、括号随时会配对, 每到一个 token 整块变一次色,比不着色更差。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `translations` | `Partial` | | | | `onClampToggle` | `(details: CodeViewClampToggleDetails) => void` | | 折叠态切换的意图回调;clamped 是纯受控的,是否落定由宿主决定。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `clamp-toggle` | `CustomEvent` | 折叠态切换的意图;detail 为 `{ clamped: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCodeViewCode` | `line` | `CodeViewLineSlotProps` | | | `XhCodeViewRoot` | `default` | `CodeViewRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCodeViewCode` | `children` | `SlotChildren` | | 逐行接管该行的正文;未提供时按着色结果铺设。 | | `XhCodeViewFilename` | `filename` | `string` | | 未写 children 时显示它。 | | `XhCodeViewRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `fold-trigger` | 'closed' \| 'open' | 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`PRESS.START` · `PRESS.END` **判据**:`canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `lang` | `string` | | | `lineCount` | `number` | | | `lines` | `readonly CodeLine[]` | 逐行切分后的文本与记号片段。 | | `lineNumberAt` | `(index: number) => number` | 每行的行号,与 lines 同序。 | | `lineNumbers` | `boolean` | 是否渲染行号槽;适配器据此决定是否创建该节点。 | | `foldable` | `boolean` | 折叠可用:提供了正数 clamp 且行数确实超过它。 | | `clamped` | `boolean` | | | `setClamped` | `(next: boolean) => void` | 发出一次折叠意图;与当前态相同时不发。 | | `getRootProps` | `() => T['element']` | | | `getHeaderProps` | `() => T['element']` | | | `getFilenameProps` | `() => T['element']` | | | `getLangLabelProps` | `() => T['element']` | | | `getPreProps` | `() => T['element']` | | | `getCodeProps` | `() => T['element']` | | | `getLineProps` | `(props: CodeViewLineProps) => T['element']` | | | `getLineNumberProps` | `(props: CodeViewLineProps) => T['element']` | | | `getLineContentProps` | `(props: CodeViewLineProps) => T['element']` | | | `getTokenProps` | `(token: CodeToken) => T['element']` | | | `getFoldTriggerProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/WCAG21/Techniques/general/G202) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Tab` | 代码块在 Tab 序列中 | <pre> 自身可聚焦,随后方向键的横向滚动交给浏览器,组件不接管 | | `Enter` / `Space` | 焦点在折叠按钮上 | 翻面折叠态并发出意图;组件只接 click,按键走原生 button 的默认行为 | | `Enter` / `Space` | 按住折叠按钮且代码可折叠 | 按住期间 fold-trigger 投影 data-pressed,与指针 :active 同一副按压面(disclosure trigger 只换面不缩放);抬起、失焦或折叠条收起撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `lang-label` | `aria-hidden` | 'true' | | `pre` | `role` | 'group' | | `line-number` | `aria-hidden` | 'true' | | `fold-trigger` | `aria-controls` | `pre` 部件的 id | | `fold-trigger` | `aria-expanded` | 'false' \| 'true' | | `fold-trigger` | `aria-label` | translations?.expand \| translations?.collapse | - `pre` 可聚焦并带可访问名称:渲染了文件名时指向它,否则使用 `translations.code`。 - 折叠按钮带 `aria-expanded` 与 `aria-controls`,指向 `pre`。 - 语言角标与行号槽都对读屏隐藏,它们是装饰而非内容。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/code-view.css` 使用 `[data-scope="code-view"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-clamped` | ''(条件成立时才出现) | | `root` | `data-complete` | ''(条件成立时才出现) | | `root` | `data-digits` | String(Math.min( String(lineNumberAt(lineCount - 1)).… | | `root` | `data-foldable` | ''(条件成立时才出现) | | `root` | `data-lang` | prop('lang')?.trim() \|\| CODE_VIEW_FALLBACK_LANG | | `root` | `data-line-numbers` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `pre` | `data-complete` | ''(条件成立时才出现) | | `pre` | `data-wrap` | ''(条件成立时才出现) | | `code` | `data-lang` | prop('lang')?.trim() \|\| CODE_VIEW_FALLBACK_LANG | | `code` | `data-wrap` | ''(条件成立时才出现) | | `line` | `data-highlighted` | ''(条件成立时才出现) | | `line` | `data-line-number` | String(lineNumberAt(index)) | | `line-number` | `data-highlighted` | ''(条件成立时才出现) | | `line-number` | `data-line-number` | String(lineNumberAt(index)) | | `line-content` | `data-highlighted` | ''(条件成立时才出现) | | `line-content` | `data-line-number` | String(lineNumberAt(index)) | | `token` | `data-kind` | token.kind | | `fold-trigger` | `data-pressed` | ''(条件成立时才出现) | | `fold-trigger` | `data-state` | 'closed' \| 'open' | | `fold-trigger` | `data-xh-action-control` | '' | | `fold-trigger` | `data-xh-action-display` | 'always' | | `fold-trigger` | `data-xh-action-profile` | 'disclosure-trigger' | | `fold-trigger` | `data-xh-action-size` | props.size | | `fold-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-code-view-bg` | `root` | `background` | `default` | `--xh-bg-surface` | code-view 的 root 部件 background 覆盖槽。 | | `--xh-code-view-border` | `root` | `border` | `default` | `--xh-border-default` | code-view 的 root 部件 border 覆盖槽。 | | `--xh-code-view-comment-fg` | `token` | `color` | `kind=comment` | `--xh-fg-muted` | code-view 的 token 部件 color 覆盖槽。 | | `--xh-code-view-fg` | `root` | `color` | `default` | `--xh-fg-muted` | code-view 的 root 部件 color 覆盖槽。 | | `--xh-code-view-filename-fg` | `filename` | `color` | `default` | `--xh-fg-default` | code-view 的 filename 部件 color 覆盖槽。 | | `--xh-code-view-fold-bg-hover` | `fold-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | code-view 的 fold-trigger 部件 background-color 覆盖槽。 | | `--xh-code-view-fold-fg` | `fold-trigger` | `color` | `default` | `--xh-fg-muted` | code-view 的 fold-trigger 部件 color 覆盖槽。 | | `--xh-code-view-fold-py` | `fold-trigger` | `padding-block` | `xh-action-profile=disclosure-trigger` | `--xh-space-2` | code-view 的 fold-trigger 部件 padding-block 覆盖槽。 | | `--xh-code-view-font` | `code`
`filename` | `font-family` | `default` | `--xh-font-family-mono` | code-view 的 code、filename 部件 font-family 覆盖槽。 | | `--xh-code-view-font-size` | `root` | `font-size` | `default` | `--xh-_code-view-font-size` | code-view 的 root 部件 font-size 覆盖槽。 | | `--xh-code-view-gutter-border` | `line-number` | `border-inline-end` | `default` | `--xh-border-default` | code-view 的 line-number 部件 border-inline-end 覆盖槽。 | | `--xh-code-view-gutter-gap` | `line-number` | `padding-inline-end` | `default` | `--xh-space-1` | code-view 的 line-number 部件 padding-inline-end 覆盖槽。 | | `--xh-code-view-header-border` | `fold-trigger`
`header` | `border`
`border-block-end`
`border-color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-border-subtle` | code-view 的 fold-trigger、header 部件 border、border-block-end、border-color 覆盖槽。 | | `--xh-code-view-header-fg` | `header` | `color` | `default` | `--xh-fg-muted` | code-view 的 header 部件 color 覆盖槽。 | | `--xh-code-view-header-font-size` | `fold-trigger`
`header` | `font-size` | `default` | `--xh-text-secondary-size` | code-view 的 fold-trigger、header 部件 font-size 覆盖槽。 | | `--xh-code-view-header-gap` | `header` | `gap` | `default` | `--xh-space-2` | code-view 的 header 部件 gap 覆盖槽。 | | `--xh-code-view-header-h` | `header` | `min-block-size` | `default` | `--xh-control-h-lg` | code-view 的 header 部件 min-block-size 覆盖槽。 | | `--xh-code-view-header-px` | `header` | `padding-inline` | `default` | `--xh-space-4` | code-view 的 header 部件 padding-inline 覆盖槽。 | | `--xh-code-view-header-py` | `header` | `padding-block` | `default` | `--xh-space-2` | code-view 的 header 部件 padding-block 覆盖槽。 | | `--xh-code-view-highlight-bar` | `line` | `box-shadow`
`outline`
`outline-offset` | `@media print`
`highlighted` | `--xh-stroke-thick` | code-view 的 line 部件 box-shadow、outline、outline-offset 覆盖槽。 | | `--xh-code-view-highlight-bg` | `line` | `background` | `highlighted` | `--xh-bg-brand-subtle` | code-view 的 line 部件 background 覆盖槽。 | | `--xh-code-view-highlight-fg` | `line` | `box-shadow` | `highlighted` | `--xh-bg-brand` | code-view 的 line 部件 box-shadow 覆盖槽。 | | `--xh-code-view-keyword-fg` | `token` | `color` | `kind=keyword` | `--xh-syntax-keyword` | code-view 的 token 部件 color 覆盖槽。 | | `--xh-code-view-keyword-weight` | `token` | `font-weight` | `kind=keyword` | `--xh-font-weight-semibold` | code-view 的 token 部件 font-weight 覆盖槽。 | | `--xh-code-view-label-fg` | `lang-label` | `color` | `default` | `--xh-fg-subtle` | code-view 的 lang-label 部件 color 覆盖槽。 | | `--xh-code-view-label-font-size` | `lang-label` | `font-size` | `default` | `--xh-text-caption-size` | code-view 的 lang-label 部件 font-size 覆盖槽。 | | `--xh-code-view-line-height` | `line`
`pre` | `line-height`
`min-block-size` | `default` | `--xh-text-code-leading` | code-view 的 line、pre 部件 line-height、min-block-size 覆盖槽。 | | `--xh-code-view-number-fg` | `line-number` | `color` | `default` | `--xh-fg-subtle` | code-view 的 line-number 部件 color 覆盖槽。 | | `--xh-code-view-number-font-size` | `line-number` | `font-size` | `default` | `--xh-text-caption-size` | code-view 的 line-number 部件 font-size 覆盖槽。 | | `--xh-code-view-number-token-fg` | `token` | `color` | `kind=number` | `--xh-syntax-number` | code-view 的 token 部件 color 覆盖槽。 | | `--xh-code-view-punctuation-fg` | `token` | `color` | `kind=punctuation` | `--xh-fg-subtle` | code-view 的 token 部件 color 覆盖槽。 | | `--xh-code-view-px` | `fold-trigger`
`line`
`line-content`
`line-number`
`root` | `padding-inline`
`padding-inline-end`
`padding-inline-start` | `default`
`line-numbers`
`not([data-line-numbers])` | `--xh-space-3` | code-view 的 fold-trigger、line、line-content、line-number、root 部件 padding-inline、padding-inline-end、padding-inline-start 覆盖槽。 | | `--xh-code-view-py` | `pre` | `padding-block` | `default` | `--xh-space-3` | code-view 的 pre 部件 padding-block 覆盖槽。 | | `--xh-code-view-radius` | `root` | `border-radius` | `default` | `--xh-shape-surface` | code-view 的 root 部件 border-radius 覆盖槽。 | | `--xh-code-view-shadow` | `root` | `box-shadow` | `default` | `none` | code-view 的 root 部件 box-shadow 覆盖槽。 | | `--xh-code-view-string-fg` | `token` | `color` | `kind=string` | `--xh-syntax-string` | code-view 的 token 部件 color 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 `background-color` · `box-shadow` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/collapsible # Collapsible 折叠区域 一块可以展开与收起的单块内容。 ## 用法 不传 open 即为非受控,defaultOpen 只提供初始值,之后由组件自行维护开合 ```vue ``` ```html
收起时节点不卸载,里面的输入框与滚动位置都留着。
defaultOpen 只影响初始状态。
``` ## 组件结构 加粗的是必需部件。 `data-scope="collapsible"`:`root` · `header` · `trigger` · **`content`** · `indicator` ## 示例 ### 受控 传入 open 后由宿主决定,组件自身不再修改状态,只发 open-change 报告意图 ```vue ``` ```html
当前状态:收起。触发器与上面的按钮改的是同一份状态。
``` ### 禁用 disabled 把触发器整个关停,点击与键盘都不再改变开合,已展开的内容维持原样 ```vue ``` ```html
点不开。
内容停在展开态,收不上。
``` ### 尺寸 size 改变触发按钮的高度、内边距与字号,三档并排对照 ```vue ``` ```html
按钮最矮,字号也最小。
不写 size 就是这一档。
按钮最高,字号也最大。
``` ### 自定义展开标记 在指示符部件中放置自己的图形,转向仍由皮肤按 open 接管 ```vue ``` ```html
创建时间、负责人、标签这些不常用的条件收在这里。
``` ### 展开动画 收起时节点不卸载,作者接管内容区的 display,用一条行高过渡即可平滑展开 ```vue ``` ```html

展开与收起都走同一条过渡,中途再点一次会从当前高度掉头。

``` ### 颜色 tone 落在触发按钮的展开态上,六种颜色各展开一份做对照 ```vue ``` ```html
收起后触发按钮保持默认颜色。
收起后触发按钮保持默认颜色。
收起后触发按钮保持默认颜色。
收起后触发按钮保持默认颜色。
收起后触发按钮保持默认颜色。
收起后触发按钮保持默认颜色。
``` ## 设计指引 ### 何时使用 - 高级选项、补充说明等默认不需要显示的单块内容。 ### 何时不用 - 有多块并列的可折叠内容时,使用[手风琴](./accordion),它负责互斥与整组语义。 - 内容需要浮在页面之上时,使用[气泡卡片](./popover)。 ### 特性 - 触发器与内容通过 `aria-controls` 与 `aria-expanded` 关联。 - 展开动画由皮肤提供,内容高度由组件测量。 - 指示符部件留空时由皮肤绘制箭头,放入图形时以作者提供的为准,两种情形的转向都由皮肤处理。 ### 组合 - 放入[卡片](./card)、[表单](./form)的高级选项区。 ### 最佳实践 - 触发器文字说明内容是什么,不只写“展开”。 - 收起时内容退出 Tab 序列,焦点不落到不可见的位置。 ### 反模式 - 把必填字段放进折叠区,用户提交失败时无法定位错误。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCollapsibleContent` `XhCollapsibleHeader` `XhCollapsibleIndicator` `XhCollapsibleRoot` `XhCollapsibleTrigger` | | 组合式函数 | `useCollapsible` | | 状态机 | `collapsibleMachine` | | 皮肤 | `@xihan-ui/styles/collapsible.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `open` | `boolean` | | | | `defaultOpen` | `boolean` | | | | `disabled` | `boolean` | | | | `tone` | `Tone` | | 颜色:brand / neutral / success / warning / danger / info,决定使用哪组状态色。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `dir` | `Direction` | | 文字方向,只作用于排版;作者未提供时不写入。 | | `onOpenChange` | `(details: CollapsibleOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `open-change` | `CollapsibleOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCollapsibleRoot` | `children` | `ReactNode` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `header` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `indicator` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `CLOSE` · `TOGGLE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `setOpen` | `(next: boolean) => void` | | | `getRootProps` | `() => T['element']` | | | `getHeaderProps` | `() => T['element']` | | | `getTriggerProps` | `() => T['button']` | | | `getContentProps` | `() => T['element']` | | | `getIndicatorProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Space` / `Enter` | focus in trigger, not disabled | 展开/收起 content | | `Enter` / `Space` | held in trigger, not disabled | 按住期间 trigger 投影 data-pressed,与指针 :active 同一副按压面(disclosure trigger 只换面不缩放);抬起、失焦或转禁用撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `indicator` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/collapsible.css` 使用 `[data-scope="collapsible"][data-part="root"]` 部件选择器,位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `root` | `data-tone` | props.tone | | `header` | `data-disabled` | ''(条件成立时才出现) | | `header` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-disabled` | ''(条件成立时才出现) | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'disclosure-trigger' | | `trigger` | `data-xh-action-size` | props.size | | `trigger` | `data-xh-action-variant` | 'ghost' | | `content` | `data-instant` | ''(条件成立时才出现) | | `content` | `data-state` | 'open' \| 'closed' | | `indicator` | `data-disabled` | ''(条件成立时才出现) | | `indicator` | `data-instant` | ''(条件成立时才出现) | | `indicator` | `data-state` | 'open' \| 'closed' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-collapsible-content-fg` | `content` | `color` | `default` | `--xh-fg-muted` | collapsible 的 content 部件 color 覆盖槽。 | | `--xh-collapsible-content-font-size` | `content` | `font-size` | `default` | `--xh-text-secondary-size` | collapsible 的 content 部件 font-size 覆盖槽。 | | `--xh-collapsible-content-pb` | `content` | `padding-block-end` | `@keyframes xh-disclosure-collapse`
`@keyframes xh-disclosure-expand`
`default` | `--xh-_collapsible-content-pb` | collapsible 的 content 部件 padding-block-end 覆盖槽。 | | `--xh-collapsible-content-px` | `content` | `padding-inline` | `default` | `--xh-_collapsible-content-px` | collapsible 的 content 部件 padding-inline 覆盖槽。 | | `--xh-collapsible-header-gap` | `header` | `gap` | `default` | `--xh-_collapsible-trigger-gap` | collapsible 的 header 部件 gap 覆盖槽。 | | `--xh-collapsible-icon-size` | `root`
`trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size`
`--xh-glyph-size-md` | collapsible 的 root、trigger 部件 --xh-icon-size 覆盖槽。 | | `--xh-collapsible-indicator-fg` | `indicator` | `color` | `default` | `--xh-fg-muted` | collapsible 的 indicator 部件 color 覆盖槽。 | | `--xh-collapsible-trigger-bg` | `trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | collapsible 的 trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-collapsible-trigger-bg-hover` | `trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | collapsible 的 trigger 部件 background-color 覆盖槽。 | | `--xh-collapsible-trigger-fg` | `trigger` | `color` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | collapsible 的 trigger 部件 color 覆盖槽。 | | `--xh-collapsible-trigger-fg-disabled` | `trigger` | `color` | `disabled` | `--xh-_action-variant-fg-disabled` | collapsible 的 trigger 部件 color 覆盖槽。 | | `--xh-collapsible-trigger-fg-open` | `trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed`
`state=open` | `--xh-_collapsible-open-fg` | collapsible 的 trigger 部件 color 覆盖槽。 | | `--xh-collapsible-trigger-font-size` | `trigger` | `font-size` | `default` | `--xh-_action-profile-font-size` | collapsible 的 trigger 部件 font-size 覆盖槽。 | | `--xh-collapsible-trigger-font-weight` | `trigger` | `font-weight` | `default` | `--xh-text-label-weight` | collapsible 的 trigger 部件 font-weight 覆盖槽。 | | `--xh-collapsible-trigger-gap` | `trigger` | `gap` | `default` | `--xh-_action-profile-gap` | collapsible 的 trigger 部件 gap 覆盖槽。 | | `--xh-collapsible-trigger-h` | `trigger` | `block-size`
`min-block-size` | `default`
`xh-action-profile=disclosure-trigger` | `--xh-_action-profile-visual-size` | collapsible 的 trigger 部件 block-size、min-block-size 覆盖槽。 | | `--xh-collapsible-trigger-px` | `trigger` | `padding-inline` | `default` | `--xh-_action-profile-padding-inline` | collapsible 的 trigger 部件 padding-inline 覆盖槽。 | | `--xh-collapsible-trigger-py` | `trigger` | `padding-block` | `xh-action-profile=disclosure-trigger` | `--xh-_action-profile-padding-block` | collapsible 的 trigger 部件 padding-block 覆盖槽。 | | `--xh-collapsible-trigger-radius` | `trigger` | `border-radius` | `default` | `--xh-_action-profile-radius` | collapsible 的 trigger 部件 border-radius 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 披露(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-disclosure-collapse` · `xh-disclosure-expand` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`rotate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/color-field # ColorField 颜色字段 可以手动输入颜色串的单行框,旁边显示当前颜色的色块。框内文字是草稿,回车或失焦时提交,提交后按 `format` 重写为规范写法。它属于[文本字段](./text-field)家族,面向已知颜色值直接输入的场景;需要在色域中挑选时使用[颜色选择器](./color-picker)。 ## 用法 输入框中的文字是草稿,回车或失焦提交后按 format 重写;色块绘制的是已提交的值,未完成的输入不会被提交 ```vue ``` ```html
收下的值:#3b82f6
``` ## 组件结构 加粗的是必需部件。 `data-scope="color-field"`:**`root`** · `label` · `control` · `swatch` · **`input`** · `clear-trigger` · `hidden-input` ## 示例 ### 写法与透明度 手动输入的任何写法提交后都按 format 重写;开启 alpha 才保留透明度,配合 rgba 写法一目了然 ```vue ``` ```html
收下的值:rgba(59, 130, 246, 0.5)
``` ### 无法提交的草稿 无法解析的文字留在框中并标为无效,让用户看到自己输入的内容;Escape 放弃草稿回到规范文本 ```vue ``` ```html
已收下
``` ### 状态与尺寸 禁用、只读、无效三态与 sm / lg 两档;色块与清空按钮跟随字段的尺寸档 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 用户持有颜色串(设计稿上的 `#3b82f6`、`rgb()`),需要直接填入表单。 - 主题设置、标注色、图表配色等需要精确到值的场景。 - 与[颜色选择器](./color-picker)并排,选完后可以查看并微调该值。 ### 何时不用 - 用户不知道颜色串、需要看着挑选时,使用[颜色选择器](./color-picker)。 - 只从几个固定颜色中选一个时,使用[颜色色块选择器](./color-swatch-picker)。 - 只展示不编辑时,使用[颜色色块](./color-swatch)。 ### 特性 - 支持 `#rgb` / `#rrggbb(aa)`、`rgb()` / `rgba()`、`hsl()` / `hsla()`,不支持颜色关键字;提交后按 `format`(hex / rgba / hsla)重写,`alpha` 决定是否带透明度。 - 输入过程只保留草稿:值、色块与 `onValueChange` 都不变化,`data-editing` 标记正在编辑;回车或失焦提交,Escape 放弃草稿并回到规范文本。 - 无法提交的草稿留在框内并标记为无效(`aria-invalid`、`data-invalid`),用户可以看到自己的输入;再次修改时移除标记。 - 空串是合法的“无颜色”:`clearable` 开启清空按钮与 Escape 清空,空值时色块只绘制棋盘格。 - 表单出口经 `hidden-input`:提交的是已确认的值,框内未提交的草稿不会随表单提交;提供 `name` 后才参与提交。 - 视觉盒使用 Field Chrome,色块使用 Swatch 家族,清空按钮使用 Action Control 的 field-inset 档,与文本字段外观一致。 ### 组合 - 放入[表单字段](./field):标签、说明与错误由字段渲染并经 aria-describedby 关联到输入框,禁用 / 只读 / 必填 / 无效四轴随字段下发。 - 与[颜色滑块](./color-slider)并排:滑块调整一个通道,字段显示完整颜色串,两者共用一个值。 ### 最佳实践 - 通过 `placeholder` 提示期望的写法(`#rrggbb`),减少提交失败。 - 需要透明度时同时开启 `alpha` 并把 `format` 设为 `rgba` 或 `hsla`,hex 的第四对字符不易辨识。 - 在 `onValueChange` 中取值,不读取输入框:框内可能是尚未提交的草稿。 ### 反模式 - 将它当作自由文本框:无法解析的输入不会成为值,也不会随表单提交。 - 传颜色关键字(`red`)作为初值:无法解析的串会被视为无效值并保持不变。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhColorFieldClearTrigger` `XhColorFieldControl` `XhColorFieldHiddenInput` `XhColorFieldInput` `XhColorFieldLabel` `XhColorFieldRoot` `XhColorFieldSwatch` | | 组合式函数 | `useColorField` | | 状态机 | `colorFieldMachine` | | 皮肤 | `@xihan-ui/styles/color-field.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | | 受控的颜色串;提供后由宿主决定,状态机不自行修改。空串表示没有颜色。 | | `defaultValue` | `string` | | 非受控初值,默认空串。 | | `format` | `ColorFormat` | | 值串的写法,默认 hex。手动输入的任何写法接受后都按它重写。 | | `alpha` | `boolean` | | 带透明度,默认关闭。关闭时接受的颜色恒为不透明。 | | `placeholder` | `string` | | | | `disabled` | `boolean` | | | | `readOnly` | `boolean` | | | | `required` | `boolean` | | | | `invalid` | `boolean` | | | | `name` | `string` | | 表单字段名;提供后才参与提交(经表单影子,输入框中未提交的草稿不会被提交)。 | | `clearable` | `boolean` | | 开启清空能力:有值时显示清空按钮、Escape 接管。关闭时按钮带 hidden 收起。 | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定底色与描边的绘制方式。默认 outline。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定聚焦强调使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,决定输入框、色块与清空按钮的几何档位。 | | `translations` | `Partial` | | 读屏文案;默认英文。 | | `onValueChange` | `(details: ColorFieldValueChangeDetails) => void` | | | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `ColorFieldValueChangeDetails` | 已接受的值变化;detail 为 `{ value: string }`,输入途中不发出 | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhColorFieldRoot` | `default` | `ColorFieldRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhColorFieldRoot` | `children` | `SlotChildren` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`INPUT.CHANGE` · `INPUT.COMMIT` · `INPUT.CANCEL` · `VALUE.SET` · `VALUE.CLEAR` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`canEdit` · `canClear` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string` | 当前值串(与 onValueChange 发出的是同一个);空串表示没有颜色。 | | `empty` | `boolean` | 值为空串。 | | `text` | `string` | 输入框当前应显示的文字:有草稿显示草稿,否则显示值本身。 | | `editing` | `boolean` | 正在编辑:框中有一份尚未接受的草稿。 | | `draftInvalid` | `boolean` | 上一次接受失败,草稿留在框中。 | | `rgba` | `ColorRgba` | 值解析出的颜色;空串或不可解析时为兜底黑,此时参考 empty / valid。 | | `valid` | `boolean` | 值串本身可解析(空串不算有效)。 | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | 作者标记的 invalid,或草稿不可接受。 | | `clearable` | `boolean` | | | `canClear` | `boolean` | 清空按钮当前是否可用(开启 clearable、可编辑、且有值)。 | | `setValue` | `(next: string) => void` | 直接写值:空串清空,不可解析的串保持不变;只受 disabled / readOnly 约束。 | | `clear` | `() => void` | 发起清空意图,受 canClear 约束;无条件清空使用 setValue('')。 | | `commit` | `() => void` | 接受框中的草稿(与回车 / 失焦同一路径)。 | | `getRootProps` | `() => T['element']` | | | `getControlProps` | `() => T['element']` | 视觉盒;描边、底色与聚焦环绘制在该节点上,色块、输入框与清空按钮排列在其中。 | | `getLabelProps` | `() => T['label']` | | | `getSwatchProps` | `() => T['element']` | 当前颜色的色块:纯装饰,颜色已在输入框中;空值或无效时只绘制棋盘格。 | | `getInputProps` | `() => T['input']` | | | `getClearTriggerProps` | `() => T['button']` | | | `getHiddenInputProps` | `() => T['input']` | 表单影子:提交的是已接受的值,框中的草稿不会被提交。提供 name 后才带 name。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://html.spec.whatwg.org/multipage/input.html#text-(type=text)-state-and-search-state-(type=search)) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` | focus in input, 框里有还没收下的草稿 | 收下草稿:解析得了就按 format 重写成值,解析不了保留草稿并标成无效;没在编辑时不接管,回车照常提交表单 | | `Escape` | focus in input, 框里有还没收下的草稿 | 放弃草稿,框里回到当前值的规范文本 | | `Escape` | focus in input, 没有草稿, clearable 且值非空, not disabled/readOnly | 清空值;条件不满足即不接管该键,交回给外层与浏览器 | | `Enter` / `Space` | held in clear-trigger, clearable 且值非空, not disabled/readOnly | 按住期间清空按钮投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,值清空后按钮藏起一并撤下。清空按钮不占 Tab 位,键盘这一路只在焦点落到它身上时有面 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `swatch` | `aria-hidden` | 'true' | | `input` | `aria-invalid` | 'true' \| 'false' | | `input` | `aria-labelledby` | `label` 部件的 id | | `input` | `aria-readonly` | 'true' \| 'false' | | `input` | `aria-required` | 'true' \| 'false' | | `clear-trigger` | `aria-label` | label.clearTrigger | ## 样式参考 ### 皮肤 `@xihan-ui/styles/color-field.css` 使用 `[data-scope="color-field"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-editing` | ''(条件成立时才出现) | | `root` | `data-empty` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `root` | `data-xh-action-owner` | '' | | `label` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-editing` | ''(条件成立时才出现) | | `control` | `data-empty` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-variant` | props.variant | | `control` | `data-xh-field-chrome` | '' | | `control` | `data-xh-field-size` | props.size | | `swatch` | `data-disabled` | ''(条件成立时才出现) | | `swatch` | `data-empty` | ''(条件成立时才出现) | | `swatch` | `data-xh-swatch` | '' | | `swatch` | `data-xh-swatch-size` | props.size | | `input` | `data-disabled` | ''(条件成立时才出现) | | `input` | `data-editing` | ''(条件成立时才出现) | | `input` | `data-empty` | ''(条件成立时才出现) | | `input` | `data-invalid` | ''(条件成立时才出现) | | `input` | `data-xh-field-input` | '' | | `input` | `data-xh-field-layout` | 'single-line' | | `clear-trigger` | `data-pressed` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-control` | '' | | `clear-trigger` | `data-xh-action-display` | 'has-value' | | `clear-trigger` | `data-xh-action-has-value` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-profile` | 'field-inset' | | `clear-trigger` | `data-xh-action-size` | props.size | | `clear-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-color-field-action-bg` | `clear-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | color-field 的 clear-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-color-field-action-bg-active` | `clear-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | color-field 的 clear-trigger 部件 background-color 覆盖槽。 | | `--xh-color-field-action-bg-hover` | `clear-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | color-field 的 clear-trigger 部件 background-color 覆盖槽。 | | `--xh-color-field-action-fg` | `clear-trigger` | `color` | `default` | `--xh-fg-muted` | color-field 的 clear-trigger 部件 color 覆盖槽。 | | `--xh-color-field-action-fg-hover` | `clear-trigger` | `color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-fg-default` | color-field 的 clear-trigger 部件 color 覆盖槽。 | | `--xh-color-field-action-font-size` | `clear-trigger` | `font-size` | `default` | `--xh-_color-field-action-font-size` | color-field 的 clear-trigger 部件 font-size 覆盖槽。 | | `--xh-color-field-action-radius` | `clear-trigger` | `border-radius` | `default` | `--xh-shape-inset` | color-field 的 clear-trigger 部件 border-radius 覆盖槽。 | | `--xh-color-field-action-size` | `clear-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | color-field 的 clear-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-color-field-control-bg` | `control` | `background-color` | `xh-field-chrome` | `--xh-_field-variant-bg-rest` | color-field 的 control 部件 background-color 覆盖槽。 | | `--xh-color-field-control-bg-disabled` | `control` | `background-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-bg-disabled` | color-field 的 control 部件 background-color 覆盖槽。 | | `--xh-color-field-control-bg-hover` | `control` | `background-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-hover` | color-field 的 control 部件 background-color 覆盖槽。 | | `--xh-color-field-control-bg-readonly` | `control` | `background-color` | `readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-read-only` | color-field 的 control 部件 background-color 覆盖槽。 | | `--xh-color-field-control-border` | `control` | `border` | `xh-field-chrome` | `--xh-_field-variant-border-rest` | color-field 的 control 部件 border 覆盖槽。 | | `--xh-color-field-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])`
`xh-field-chrome` | `--xh-_field-variant-border-focus` | color-field 的 control 部件 border-color 覆盖槽。 | | `--xh-color-field-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-border-hover` | color-field 的 control 部件 border-color 覆盖槽。 | | `--xh-color-field-control-border-invalid` | `control` | `border-color` | `invalid`
`xh-field-chrome` | `--xh-_field-variant-border-invalid` | color-field 的 control 部件 border-color 覆盖槽。 | | `--xh-color-field-control-fg` | `control` | `color` | `xh-field-chrome` | `--xh-fg-default` | color-field 的 control 部件 color 覆盖槽。 | | `--xh-color-field-control-gap` | `control` | `gap` | `xh-field-chrome` | `--xh-_color-field-gap` | color-field 的 control 部件 gap 覆盖槽。 | | `--xh-color-field-control-h` | `control` | `block-size`
`min-block-size` | `has([data-xh-field-input][data-xh-field-layout='multi-tag'])`
`has([data-xh-field-input][data-xh-field-layout='single-line'])`
`has([data-xh-field-input][data-xh-field-layout='textarea'])`
`xh-field-chrome`
`xh-field-input`
`xh-field-layout=multi-tag`
`xh-field-layout=single-line`
`xh-field-layout=textarea` | `--xh-_color-field-h` | color-field 的 control 部件 block-size、min-block-size 覆盖槽。 | | `--xh-color-field-control-min-w` | `control`
`root` | `min-inline-size` | `default`
`xh-field-chrome` | `--xh-control-min-w` | color-field 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-color-field-control-px` | `control` | `padding-inline` | `xh-field-chrome` | `--xh-_color-field-px` | color-field 的 control 部件 padding-inline 覆盖槽。 | | `--xh-color-field-control-radius` | `control` | `border-radius` | `xh-field-chrome` | `--xh-shape-control` | color-field 的 control 部件 border-radius 覆盖槽。 | | `--xh-color-field-control-shadow` | `control` | `box-shadow` | `xh-field-chrome` | `none` | color-field 的 control 部件 box-shadow 覆盖槽。 | | `--xh-color-field-control-w` | `root` | `inline-size`
`min-inline-size` | `default` | `--xh-control-w` | color-field 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-color-field-gap` | `root` | `gap` | `default` | `--xh-space-1` | color-field 的 root 部件 gap 覆盖槽。 | | `--xh-color-field-icon-size` | `control`
`root` | `--xh-icon-size` | `default`
`size=lg`
`size=sm`
`xh-field-chrome` | `--xh-_field-size-glyph-size`
`--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | color-field 的 control、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-color-field-input-autofill-bg` | `input` | `box-shadow` | `-webkit-autofill`
`autofill`
`xh-field-input` | `--xh-bg-canvas` | color-field 的 input 部件 box-shadow 覆盖槽。 | | `--xh-color-field-input-autofill-fg` | `input` | `-webkit-text-fill-color` | `-webkit-autofill`
`autofill`
`xh-field-input` | `--xh-fg-default` | color-field 的 input 部件 -webkit-text-fill-color 覆盖槽。 | | `--xh-color-field-input-fg` | `input` | `color` | `xh-field-input` | `--xh-fg-default` | color-field 的 input 部件 color 覆盖槽。 | | `--xh-color-field-input-font-size` | `input` | `font-size` | `xh-field-input` | `--xh-_color-field-font-size` | color-field 的 input 部件 font-size 覆盖槽。 | | `--xh-color-field-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | color-field 的 label 部件 color 覆盖槽。 | | `--xh-color-field-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | color-field 的 label 部件 color 覆盖槽。 | | `--xh-color-field-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | color-field 的 label 部件 font-size 覆盖槽。 | | `--xh-color-field-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | color-field 的 label 部件 font-weight 覆盖槽。 | | `--xh-color-field-placeholder-fg` | `input` | `color` | `placeholder`
`xh-field-input` | `--xh-fg-subtle` | color-field 的 input 部件 color 覆盖槽。 | | `--xh-color-field-swatch-border` | `swatch` | `--xh-swatch-border` | `default` | `--xh-border-default` | color-field 的 swatch 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-field-swatch-radius` | `swatch` | `--xh-swatch-radius` | `default` | `--xh-shape-inset` | color-field 的 swatch 部件 --xh-swatch-radius 覆盖槽。 | | `--xh-color-field-swatch-size` | `swatch` | `--xh-swatch-size` | `default` | `--xh-_swatch-size` | color-field 的 swatch 部件 --xh-swatch-size 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/color-picker # ColorPicker 颜色选择器 在色域中自由选取一个颜色:触发按钮显示当前色,浮层内包含取色面、色相与透明度两条滑块、数值框、屏幕取色与预设色板。它是颜色家族的组合件:两条滑块是[颜色滑块](./color-slider),预设色板是[颜色色块选择器](./color-swatch-picker),触发按钮内的色块与[颜色色块](./color-swatch)同族;只需要其中一件时不使用完整的选择器。 ## 用法 取色面选择饱和度与明度,下方一条色相滑块;滑块是内嵌的颜色滑块组件,Vue / React 的挂载点不写子节点即自动铺开 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="color-picker"`:`root` · `label` · `control` · **`trigger`** · `value-text` · `swatch` · `positioner` · **`content`** · **`saturation-area`** · **`area-thumb`** · `hue-slider` · `alpha-slider` · `channel-input` · `eye-dropper-trigger` · `swatch-picker` · `hidden-input` ## 示例 ### 预设色板 swatches 提供一组常用颜色,浮层中内嵌一台色块选择器:方向键在格子间移动、按颜色比较选中 ```vue ``` ```html
``` ### 禁用 禁止更改颜色 ```vue ``` ```html
``` ### 透明度 alpha 开启后值串带透明度,浮层中多一条透明度滑块;两条滑块共用同一份工作色,调节色相不会把透明度归 1 ```vue ``` ```html
``` ### 精确输入 输入色值或使用屏幕取色 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 用户需要自定义主题色、标注色或画布颜色,且不限于固定选项。 - 既需要可视化挑选(取色面、滑块)也需要精确输入(十六进制、分量框)。 - 需要从屏幕取色。 ### 何时不用 - 只从几个固定颜色中选一个时,使用[颜色色块选择器](./color-swatch-picker)。 - 只调整一个通道(色相、透明度)时,使用[颜色滑块](./color-slider)。 - 用户已知颜色串并直接输入时,使用[颜色字段](./color-field)。 - 只展示一个颜色时,使用[颜色色块](./color-swatch)。 ### 特性 - 工作色始终是 HSVA:取色面两轴是饱和度与明度,纯黑与灰度处的色相由锚点保持,拖到黑色再拉回时色相不丢失。 - `format` 决定值串写法(hex / rgba / hsla),`alpha` 决定是否带透明度;关闭时透明度滑块与输入框整体禁用。 - 色相与透明度两条滑块是内嵌的颜色滑块:整份工作色交给它们,调整色相不会把透明度归 1;键盘(方向键、PageUp / PageDown、Home / End、Shift 大步)与拖动都由滑块自身处理。 - 预设色板是内嵌的色块选择器:方向键在格子间移动并选中,当前颜色所在格按颜色比较(写法不同也能匹配)。 - 数值框输入只保留草稿,可解析时立即取值;不可解析时保留原文并报输入错误,回车同时拦截表单提交。 - 屏幕取色通过浮层内的按钮触发,环境不提供 EyeDropper 时始终禁用;取到的颜色与色板、外部 setValue 走同一条取值路径。 - 格式、输入、颜色解析与屏幕取色四路错误相互独立,修正一路不影响其他路。 - 受控 `value` 与 `open`:宿主不写回时界面不变化,回调照常发出;表单出口经 `hidden-input` 提交当前值串。 ### 组合 - 三个挂载点 `hue-slider` / `alpha-slider` / `swatch-picker` 同时充当内嵌组件的根节点,内部放置的是[颜色滑块](./color-slider)与[颜色色块选择器](./color-swatch-picker)自己的部件;不写子节点时自动铺开最简结构。 - 放入[表单字段](./field)承接标题、说明与错误信息,`disabled` / `readOnly` 随字段下发。 - 与[颜色字段](./color-field)并排:选择器挑颜色,字段显示并微调该值。 ### 最佳实践 - 通过 `swatches` 提供常用色,多数用户从这里即可完成选择。 - 触发按钮内同时放色块与值串,读屏与视觉各有一路。 - 需要精确输入时放数值框;一个十六进制框比四个分量框更节省空间。 ### 反模式 - 只显示颜色不显示数值,颜色不能是唯一的信息通道。 - 有对比度要求的场景不提供校验反馈。 - 关闭 `alpha` 后仍保留透明度滑块,整条禁用的控件只会造成困惑。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhColorPickerAlphaSlider` `XhColorPickerAreaThumb` `XhColorPickerChannelInput` `XhColorPickerContent` `XhColorPickerControl` `XhColorPickerEyeDropperTrigger` `XhColorPickerHiddenInput` `XhColorPickerHueSlider` `XhColorPickerLabel` `XhColorPickerPositioner` `XhColorPickerRoot` `XhColorPickerSaturationArea` `XhColorPickerSwatch` `XhColorPickerSwatchPicker` `XhColorPickerTrigger` `XhColorPickerValueText` | | 组合式函数 | `useColorPicker` | | 状态机 | `colorPickerMachine` | | 皮肤 | `@xihan-ui/styles/color-picker.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | | 颜色值串。提供即受控:cell 直读 prop,写入只发 onValueChange 不落内部值。 | | `defaultValue` | `string` | | | | `format` | `ColorFormat` | | 值串的写法,默认 hex。修改它只改变对外的序列化,工作色恒为 HSVA。 | | `open` | `boolean` | | 展开态。提供即受控:内部不再自行修改,只发 onOpenChange。 | | `defaultOpen` | `boolean` | | | | `disabled` | `boolean` | | 整个控件禁用:trigger 与两个按钮使用原生 disabled,取色区与滑杆退出 Tab 序列。 | | `readOnly` | `boolean` | | 只读:浮层照常展开(可查看当前颜色),但任何改值的动作都不发生。 | | `swatches` | `string[]` | | 预设色板:交给内嵌的色块选择器铺格,选中的格按颜色比较。 | | `name` | `string` | | 表单字段名;提供后表单影子才带 name 并参与提交。 | | `alpha` | `boolean` | | 带透明度,默认关闭。关闭时值串恒为不透明,透明度滑杆与输入框整条禁用。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `dir` | `Direction` | | 文字方向。只改写横轴(取色区的饱和度、通道滑杆)上左右两键与指针的语义。 | | `placement` | `Placement` | | | | `offset` | `number` | | | | `translations` | `Partial` | | | | `onValueChange` | `(details: ColorPickerValueChangeDetails) => void` | | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 | | `onOpenChange` | `(details: ColorPickerOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `onColorError` | `(details: ColorPickerErrorDetails) => void` | | 格式、文本、颜色解析或屏幕取色失败;与 value / open 事件独立。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `ColorPickerValueChangeDetails` | 颜色变化;detail 为 `{ value: string }` | | `open-change` | `ColorPickerOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | | `color-error` | `ColorPickerErrorDetails` | 格式、输入、颜色解析或屏幕取色失败;detail 为判别式错误对象 | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhColorPickerRoot` | `default` | `ColorPickerRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhColorPickerChannelInput` | `channel` | `ColorPickerInputChannel` | | 该输入框编辑的通道:hex 是整串,r/g/b 是分量,a 是透明度百分数;默认或无法识别时按 hex 处理。 | | `XhColorPickerPositioner` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhColorPickerRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `label` | 'open' \| 'closed' | | `control` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `value-text` | 'open' \| 'closed' | | `swatch` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `saturation-area` | 'open' \| 'closed' | | `area-thumb` | 'open' \| 'closed' | | `channel-input` | 'open' \| 'closed' | | `eye-dropper-trigger` | 'picking' \| 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`closed` · `open` · `open.idle` · `open.dragging` · `open.picking` **事件**:`OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `VALUE.SET` · `AREA.SET` · `AREA.STEP` · `AREA.TO_EDGE` · `HSVA.SET` · `INPUT.CHANGE` · `INPUT.COMMIT` · `DRAG.START` · `DRAG.MOVE` · `DRAG.END` · `EYE_DROPPER.OPEN` · `EYE_DROPPER.RESULT` · `EYE_DROPPER.CANCEL` · `EYE_DROPPER.ERROR` · `ERROR.CLEAR` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` · `canInteract` · `canPick` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `value` | `string` | 当前值串(与 onValueChange 发出的是同一个)。 | | `rgba` | `ColorRgba` | | | `hsva` | `ColorHsva` | 工作色。取色区与色相滑杆读取的都是它。 | | `format` | `ColorFormat` | | | `alpha` | `boolean` | | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `dragging` | `boolean` | 指针正在拖动某一部位。 | | `picking` | `boolean` | 屏幕取色正在进行。 | | `eyeDropperSupported` | `boolean` | | | `errors` | `ColorPickerErrors` | 格式、文本、颜色解析与屏幕取色四路互不覆盖的错误。 | | `swatches` | `string[]` | 预设色板(原样透传 swatches prop,默认为空数组)。 | | `hueSlider` | `ColorSliderApi` | 色相颜色滑块的 api:部件属性与取值都从这里获取,DOM 带 data-scope="color-slider"。 | | `alphaSlider` | `ColorSliderApi` | 透明度颜色滑块的 api。 | | `swatchPicker` | `ColorSwatchPickerApi` | 预设色板的 api,DOM 带 data-scope="color-swatch-picker"。 | | `inputText` | `(channel: ColorPickerInputChannel) => string` | 某个数值框当前应显示的文字(有草稿显示草稿,否则显示规范文本)。 | | `setOpen` | `(next: boolean) => void` | | | `setValue` | `(next: string) => void` | | | `clearError` | `() => void` | 清除四路显式错误;屏幕取色重试也会先清除自己那一路。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['label']` | | | `getControlProps` | `() => T['element']` | | | `getTriggerProps` | `() => T['button']` | | | `getValueTextProps` | `() => T['element']` | | | `getSwatchProps` | `() => T['element']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getSaturationAreaProps` | `() => T['element']` | | | `getAreaThumbProps` | `() => T['element']` | | | `getHueSliderProps` | `() => T['element']` | 色相滑块的挂载点,同时充当该滑块的根节点:滑块 root 的状态标记同步写在它身上。 | | `getAlphaSliderProps` | `() => T['element']` | 透明度滑块的挂载点,同上。 | | `getChannelInputProps` | `(props: ColorPickerInputProps) => T['input']` | | | `getEyeDropperTriggerProps` | `() => T['button']` | | | `getSwatchPickerProps` | `() => T['element']` | 预设色板的挂载点,同时充当色板的根节点(role=radiogroup 与键盘处理都在它身上)。 | | `getHiddenInputProps` | `() => T['input']` | 表单影子:值随表单提交。提供 name 后才带 name,未提供时不参与提交。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/slider/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `ArrowRight` / `ArrowLeft` | focus in area-thumb, not disabled/readOnly | 按 1 调饱和度;RTL 下左右对调,语义恒是"朝饱和走一格" | | `ArrowUp` / `ArrowDown` | focus in area-thumb, not disabled/readOnly | 按 1 调明度,屏幕向上恒是变亮,与 dir 无关 | | `Shift+ArrowRight` / `Shift+ArrowLeft` / `Shift+ArrowUp` / `Shift+ArrowDown` | focus in area-thumb, not disabled/readOnly | 同上,但一步走 10 | | `Home` / `End` | focus in area-thumb, not disabled/readOnly | 饱和度取 0 / 100(与 aria-valuenow 报的是同一条轴) | | `Enter` | focus in channel-input | 收下框里的字;收不了就保留草稿并报告输入错误。一并拦住表单提交 | | `Escape` | open(本层在层栈顶) | 收起浮层,焦点归还触发器 | | `Enter` / `Space` | held on eye-dropper-trigger, not disabled | 按住期间取色按钮投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,屏幕取色一开(窗口随即失焦)或浮层收起时一并撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `trigger` | `aria-haspopup` | 'dialog' | | `trigger` | `aria-labelledby` | `label` 部件的 id `value-text` 部件的 id | | `swatch` | `aria-hidden` | 'true' | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-labelledby` | `label` 部件的 id | | `content` | `aria-modal` | 'false' | | `content` | `role` | 'dialog' | | `area-thumb` | `aria-disabled` | 'true' \| 'false' | | `area-thumb` | `aria-label` | label.area | | `area-thumb` | `aria-valuemax` | '100' | | `area-thumb` | `aria-valuemin` | '0' | | `area-thumb` | `aria-valuenow` | String(Math.round(hsva.s)) | | `area-thumb` | `aria-valuetext` | label.areaValueText(Math.round(hsva.s), Math.round(hs… | | `area-thumb` | `role` | 'slider' | | `channel-input` | `aria-invalid` | 'true' \| 'false' | | `channel-input` | `aria-label` | label.input(channel) | | `eye-dropper-trigger` | `aria-label` | label.eyeDropperTrigger | - 触发按钮是原生按钮,`aria-haspopup="dialog"`,名称由标题与当前值串合成;浮层是非模态 `role="dialog"`。 - 取色面的拇指是 `role="slider"`:`aria-valuenow` 报告饱和度,明度写入 `aria-valuetext`。 - 两条滑块的名称与带单位的播报文本取自 `translations.channel` / `channelValueText`,由内嵌滑块读出。 - 色板是 `role="radiogroup"`,每格 `role="radio"`;整组名称取 `translations.swatchGroup`,每格读 `translations.swatch(value)`。 - Escape 收起浮层并把焦点归还触发按钮。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/color-picker.css` 使用 `[data-scope="color-picker"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `label` | `data-disabled` | ''(条件成立时才出现) | | `label` | `data-readonly` | ''(条件成立时才出现) | | `label` | `data-state` | 'open' \| 'closed' | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-disabled` | ''(条件成立时才出现) | | `trigger` | `data-readonly` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `value-text` | `data-disabled` | ''(条件成立时才出现) | | `value-text` | `data-readonly` | ''(条件成立时才出现) | | `value-text` | `data-state` | 'open' \| 'closed' | | `swatch` | `data-disabled` | ''(条件成立时才出现) | | `swatch` | `data-readonly` | ''(条件成立时才出现) | | `swatch` | `data-state` | 'open' \| 'closed' | | `swatch` | `data-value` | context.get('value') | | `swatch` | `data-xh-swatch` | '' | | `swatch` | `data-xh-swatch-size` | props.size | | `positioner` | `data-hidden` | ''(条件成立时才出现) | | `positioner` | `data-placement` | 定位引擎算出的实际落位 | | `positioner` | `data-positioned` | ''(条件成立时才出现) | | `positioner` | `data-size` | props.size | | `positioner` | `data-state` | 'open' \| 'closed' | | `content` | `data-disabled` | ''(条件成立时才出现) | | `content` | `data-placement` | 定位引擎算出的实际落位 | | `content` | `data-readonly` | ''(条件成立时才出现) | | `content` | `data-state` | 'open' \| 'closed' | | `saturation-area` | `data-disabled` | ''(条件成立时才出现) | | `saturation-area` | `data-dragging` | ''(条件成立时才出现) | | `saturation-area` | `data-readonly` | ''(条件成立时才出现) | | `saturation-area` | `data-state` | 'open' \| 'closed' | | `area-thumb` | `data-disabled` | ''(条件成立时才出现) | | `area-thumb` | `data-dragging` | ''(条件成立时才出现) | | `area-thumb` | `data-readonly` | ''(条件成立时才出现) | | `area-thumb` | `data-state` | 'open' \| 'closed' | | `hue-slider` | `data-channel` | 'hue' | | `alpha-slider` | `data-channel` | 'alpha' | | `alpha-slider` | `data-disabled` | ''(条件成立时才出现) | | `channel-input` | `data-channel` | channel | | `channel-input` | `data-disabled` | ''(条件成立时才出现) | | `channel-input` | `data-invalid` | ''(条件成立时才出现) | | `channel-input` | `data-readonly` | ''(条件成立时才出现) | | `channel-input` | `data-state` | 'open' \| 'closed' | | `eye-dropper-trigger` | `data-disabled` | ''(条件成立时才出现) | | `eye-dropper-trigger` | `data-pressed` | ''(条件成立时才出现) | | `eye-dropper-trigger` | `data-readonly` | ''(条件成立时才出现) | | `eye-dropper-trigger` | `data-state` | 'picking' \| 'open' \| 'closed' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-color-picker-action-bg` | `eye-dropper-trigger` | `background` | `default` | `transparent` | color-picker 的 eye-dropper-trigger 部件 background 覆盖槽。 | | `--xh-color-picker-action-bg-active` | `eye-dropper-trigger` | `background` | `is(:active, [data-pressed])`
`not(:disabled)`
`pressed`
`state=picking` | `--xh-bg-subtle-active` | color-picker 的 eye-dropper-trigger 部件 background 覆盖槽。 | | `--xh-color-picker-action-bg-hover` | `eye-dropper-trigger` | `background` | `hover`
`not(:disabled)` | `--xh-bg-subtle-hover` | color-picker 的 eye-dropper-trigger 部件 background 覆盖槽。 | | `--xh-color-picker-action-border` | `eye-dropper-trigger` | `border` | `default` | `--xh-border-control` | color-picker 的 eye-dropper-trigger 部件 border 覆盖槽。 | | `--xh-color-picker-action-border-active` | `eye-dropper-trigger` | `border-color` | `state=picking` | `--xh-bg-brand` | color-picker 的 eye-dropper-trigger 部件 border-color 覆盖槽。 | | `--xh-color-picker-action-fg` | `eye-dropper-trigger` | `color` | `default` | `--xh-fg-muted` | color-picker 的 eye-dropper-trigger 部件 color 覆盖槽。 | | `--xh-color-picker-action-fg-hover` | `eye-dropper-trigger` | `color` | `hover`
`not(:disabled)` | `--xh-fg-default` | color-picker 的 eye-dropper-trigger 部件 color 覆盖槽。 | | `--xh-color-picker-action-font-size` | `eye-dropper-trigger` | `font-size` | `default` | `--xh-text-secondary-size` | color-picker 的 eye-dropper-trigger 部件 font-size 覆盖槽。 | | `--xh-color-picker-action-radius` | `eye-dropper-trigger` | `border-radius` | `default` | `--xh-shape-control` | color-picker 的 eye-dropper-trigger 部件 border-radius 覆盖槽。 | | `--xh-color-picker-action-size` | `eye-dropper-trigger` | `block-size`
`inline-size` | `default` | `--xh-control-action-size` | color-picker 的 eye-dropper-trigger 部件 block-size、inline-size 覆盖槽。 | | `--xh-color-picker-alpha-slider-gap` | `alpha-slider` | `gap` | `default` | `--xh-stack-gap-md` | color-picker 的 alpha-slider 部件 gap 覆盖槽。 | | `--xh-color-picker-content-bg` | `content` | `background` | `default` | `--xh-bg-surface` | color-picker 的 content 部件 background 覆盖槽。 | | `--xh-color-picker-content-border` | `content` | `border` | `default` | `--xh-border-default` | color-picker 的 content 部件 border 覆盖槽。 | | `--xh-color-picker-content-fg` | `content` | `color` | `default` | `--xh-fg-default` | color-picker 的 content 部件 color 覆盖槽。 | | `--xh-color-picker-content-gap` | `content` | `gap` | `default` | `--xh-space-3` | color-picker 的 content 部件 gap 覆盖槽。 | | `--xh-color-picker-content-px` | `content` | `padding-inline` | `default` | `--xh-space-3` | color-picker 的 content 部件 padding-inline 覆盖槽。 | | `--xh-color-picker-content-py` | `content` | `padding-block` | `default` | `--xh-space-3` | color-picker 的 content 部件 padding-block 覆盖槽。 | | `--xh-color-picker-content-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | color-picker 的 content 部件 border-radius 覆盖槽。 | | `--xh-color-picker-content-shadow` | `content` | `box-shadow` | `default` | `--xh-elevation-floating` | color-picker 的 content 部件 box-shadow 覆盖槽。 | | `--xh-color-picker-content-w` | `content` | `inline-size` | `default` | `--xh-overlay-min-w` | color-picker 的 content 部件 inline-size 覆盖槽。 | | `--xh-color-picker-control-bg` | `control` | `background` | `default` | `transparent` | color-picker 的 control 部件 background 覆盖槽。 | | `--xh-color-picker-control-bg-disabled` | `control` | `background` | `disabled` | `--xh-bg-subtle` | color-picker 的 control 部件 background 覆盖槽。 | | `--xh-color-picker-control-bg-hover` | `control` | `background` | `disabled`
`hover`
`not([data-disabled], [data-readonly])`
`readonly` | `--xh-bg-subtle` | color-picker 的 control 部件 background 覆盖槽。 | | `--xh-color-picker-control-bg-readonly` | `control` | `background` | `readonly` | `--xh-bg-subtle` | color-picker 的 control 部件 background 覆盖槽。 | | `--xh-color-picker-control-border` | `control` | `border` | `default` | `--xh-border-control` | color-picker 的 control 部件 border 覆盖槽。 | | `--xh-color-picker-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])` | `--xh-_tone` | color-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-color-picker-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`not([data-disabled], [data-readonly])`
`readonly` | `--xh-border-control-hover` | color-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-color-picker-control-fg` | `control` | `color` | `default` | `--xh-fg-default` | color-picker 的 control 部件 color 覆盖槽。 | | `--xh-color-picker-control-gap` | `control` | `gap` | `default` | `--xh-_color-picker-gap` | color-picker 的 control 部件 gap 覆盖槽。 | | `--xh-color-picker-control-h` | `control` | `block-size` | `default` | `--xh-_color-picker-h` | color-picker 的 control 部件 block-size 覆盖槽。 | | `--xh-color-picker-control-min-w` | `control`
`root` | `min-inline-size` | `default` | `--xh-control-min-w` | color-picker 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-color-picker-control-px` | `control` | `padding-inline` | `default` | `--xh-_color-picker-px` | color-picker 的 control 部件 padding-inline 覆盖槽。 | | `--xh-color-picker-control-radius` | `control` | `border-radius` | `default` | `--xh-shape-control` | color-picker 的 control 部件 border-radius 覆盖槽。 | | `--xh-color-picker-control-shadow` | `control` | `box-shadow` | `default` | `--xh-elevation-raised` | color-picker 的 control 部件 box-shadow 覆盖槽。 | | `--xh-color-picker-control-w` | `root` | `inline-size`
`min-inline-size` | `default` | `--xh-control-w` | color-picker 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-color-picker-gap` | `root` | `gap` | `default` | `--xh-space-1` | color-picker 的 root 部件 gap 覆盖槽。 | | `--xh-color-picker-hue-slider-gap` | `hue-slider` | `gap` | `default` | `--xh-stack-gap-md` | color-picker 的 hue-slider 部件 gap 覆盖槽。 | | `--xh-color-picker-input-bg` | `channel-input` | `background` | `default` | `transparent` | color-picker 的 channel-input 部件 background 覆盖槽。 | | `--xh-color-picker-input-bg-disabled` | `channel-input` | `background` | `disabled` | `--xh-bg-subtle` | color-picker 的 channel-input 部件 background 覆盖槽。 | | `--xh-color-picker-input-bg-readonly` | `channel-input` | `background` | `readonly` | `--xh-bg-subtle` | color-picker 的 channel-input 部件 background 覆盖槽。 | | `--xh-color-picker-input-border` | `channel-input` | `border` | `default` | `--xh-border-control` | color-picker 的 channel-input 部件 border 覆盖槽。 | | `--xh-color-picker-input-border-focus` | `channel-input` | `border-color` | `focus-visible` | `--xh-_tone` | color-picker 的 channel-input 部件 border-color 覆盖槽。 | | `--xh-color-picker-input-border-invalid` | `channel-input` | `border-color` | `invalid` | `--xh-border-invalid` | color-picker 的 channel-input 部件 border-color 覆盖槽。 | | `--xh-color-picker-input-font-size` | `channel-input` | `font-size` | `default` | `--xh-text-secondary-size` | color-picker 的 channel-input 部件 font-size 覆盖槽。 | | `--xh-color-picker-input-h` | `channel-input` | `block-size` | `default` | `--xh-control-h-sm` | color-picker 的 channel-input 部件 block-size 覆盖槽。 | | `--xh-color-picker-input-px` | `channel-input` | `padding-inline` | `default` | `--xh-control-px-sm` | color-picker 的 channel-input 部件 padding-inline 覆盖槽。 | | `--xh-color-picker-input-radius` | `channel-input` | `border-radius` | `default` | `--xh-shape-control` | color-picker 的 channel-input 部件 border-radius 覆盖槽。 | | `--xh-color-picker-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | color-picker 的 label 部件 color 覆盖槽。 | | `--xh-color-picker-label-font-size` | `label` | `font-size` | `default` | `--xh-_color-picker-label-font-size` | color-picker 的 label 部件 font-size 覆盖槽。 | | `--xh-color-picker-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | color-picker 的 label 部件 font-weight 覆盖槽。 | | `--xh-color-picker-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | color-picker 的 positioner 部件 z-index 覆盖槽。 | | `--xh-color-picker-max-h` | `content` | `max-block-size` | `default` | `--xh-viewport-h-md` | color-picker 的 content 部件 max-block-size 覆盖槽。 | | `--xh-color-picker-saturation-area-h` | `saturation-area` | `block-size` | `default` | `9rem` | color-picker 的 saturation-area 部件 block-size 覆盖槽。 | | `--xh-color-picker-saturation-area-radius` | `saturation-area` | `border-radius` | `default` | `--xh-shape-control` | color-picker 的 saturation-area 部件 border-radius 覆盖槽。 | | `--xh-color-picker-slider-thumb-size` | `alpha-slider`
`hue-slider` | `--xh-_thumb-size` | `default` | `--xh-track-thumb-size` | color-picker 的 alpha-slider、hue-slider 部件 --xh-_thumb-size 覆盖槽。 | | `--xh-color-picker-slider-track-thickness` | `alpha-slider`
`hue-slider` | `--xh-_track-thickness` | `default` | `--xh-space-3` | color-picker 的 alpha-slider、hue-slider 部件 --xh-_track-thickness 覆盖槽。 | | `--xh-color-picker-swatch-border` | `swatch` | `--xh-swatch-border` | `default` | `--xh-border-default` | color-picker 的 swatch 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-picker-swatch-cell` | `swatch-picker` | `--xh-_color-swatch-picker-cell` | `default` | `--xh-control-h-sm` | color-picker 的 swatch-picker 部件 --xh-_color-swatch-picker-cell 覆盖槽。 | | `--xh-color-picker-swatch-gap` | `swatch-picker` | `gap` | `default` | `--xh-control-gap-sm` | color-picker 的 swatch-picker 部件 gap 覆盖槽。 | | `--xh-color-picker-swatch-icon-size` | `swatch-picker` | `--xh-icon-size` | `default` | `--xh-_color-swatch-picker-mark` | color-picker 的 swatch-picker 部件 --xh-icon-size 覆盖槽。 | | `--xh-color-picker-swatch-picker-gap` | `swatch-picker` | `gap` | `default` | `--xh-_color-swatch-picker-gap` | color-picker 的 swatch-picker 部件 gap 覆盖槽。 | | `--xh-color-picker-swatch-radius` | `swatch` | `--xh-swatch-radius` | `default` | `--xh-shape-inset` | color-picker 的 swatch 部件 --xh-swatch-radius 覆盖槽。 | | `--xh-color-picker-swatch-size` | `swatch` | `--xh-swatch-size` | `default` | `--xh-_swatch-size` | color-picker 的 swatch 部件 --xh-swatch-size 覆盖槽。 | | `--xh-color-picker-thumb-border` | `area-thumb` | `border` | `default` | `--xh-color-neutral-0` | color-picker 的 area-thumb 部件 border 覆盖槽。 | | `--xh-color-picker-thumb-radius` | `area-thumb` | `border-radius` | `default` | `--xh-shape-circle` | color-picker 的 area-thumb 部件 border-radius 覆盖槽。 | | `--xh-color-picker-thumb-scale-dragging` | `area-thumb` | `scale` | `dragging` | `--xh-motion-scale-drag` | color-picker 的 area-thumb 部件 scale 覆盖槽。 | | `--xh-color-picker-thumb-shadow` | `area-thumb` | `box-shadow` | `default` | `--xh-elevation-raised` | color-picker 的 area-thumb 部件 box-shadow 覆盖槽。 | | `--xh-color-picker-thumb-size` | `area-thumb` | `block-size`
`inline-size`
`margin-block-start`
`margin-inline-start` | `default` | `14px` | color-picker 的 area-thumb 部件 block-size、inline-size、margin-block-start、margin-inline-start 覆盖槽。 | | `--xh-color-picker-trigger-fg` | `trigger` | `color` | `default` | `--xh-fg-default` | color-picker 的 trigger 部件 color 覆盖槽。 | | `--xh-color-picker-trigger-font-size` | `trigger` | `font-size` | `default` | `--xh-text-body-size` | color-picker 的 trigger 部件 font-size 覆盖槽。 | | `--xh-color-picker-trigger-gap` | `trigger` | `gap` | `default` | `--xh-_color-picker-gap` | color-picker 的 trigger 部件 gap 覆盖槽。 | | `--xh-color-picker-value-fg` | `value-text` | `color` | `default` | `--xh-fg-default` | color-picker 的 value-text 部件 color 覆盖槽。 | | `--xh-color-picker-value-font-size` | `value-text` | `font-size` | `default` | `--xh-text-body-size` | color-picker 的 value-text 部件 font-size 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 切换 · 出现(锚定列表)(见[动效规范](../design/motion#角色))。 可覆盖的动效槽:`--xh-color-picker-thumb-scale-dragging`。 共享关键帧 `xh-overlay-slide-in` · `xh-overlay-slide-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`background-color` · `border-color` · `scale` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 - 浮层的宽度与高度分别受可用空间约束,窄视口下面板不会超出屏幕,容纳不下时在面板内滚动。 - 粗指针下命中区是取色面、滑块整条与色板整格。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 - `dir="rtl"` 只对调横轴(取色面的饱和度、两条滑块)上左右方向键与指针的语义,上下方向键始终是屏幕向上为增大。 --- 来源:https://ui.docs.xihanfun.com/components/color-slider # ColorSlider 颜色滑块 只调整颜色某一个通道的滑杆:色相、饱和度、明度、透明度,或红、绿、蓝。值是完整的颜色串,轨道显示该通道从最小值到最大值的颜色变化,拇指填充当前值对应的颜色。多条并排即可组成自定义的调色面板;[颜色选择器](./color-picker)浮层内的色相带与透明度带就是它。 ## 用法 一条滑杆只调节颜色的一个通道,默认是色相:值是整个颜色串,轨道绘制的是该通道从头到尾的颜色 ```vue ``` ```html
#3b82f6
``` ## 组件结构 加粗的是必需部件。 `data-scope="color-slider"`:`root` · `label` · **`control`** · **`track`** · **`thumb`** · `value-text` · `hidden-input` ## 示例 ### 通道并排 几条共用同一个值、各调节自己的通道;开启 alpha 使调节色相时透明度不丢失,即组成一个 HSV 调色面板 ```vue ``` ```html
#3b82f680
``` ### 红绿蓝与写法 调节 RGB 三通道使用 0-255;format 决定写回的写法,这里按 rgba() 输出 ```vue ``` ```html
rgba(59, 130, 246, 1)
``` ### 竖直与状态 orientation 竖排时渐变自下而上;禁用时标签换禁用前景、颜色带压暗,只读保留 Tab 位但不可调节 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 用户只需要调整颜色的一个分量:透明度、明暗、色相。 - 多条并排,按 HSV 或 RGB 组成内嵌的调色面板,不使用浮层。 - 需要在页面上常驻、随时可拖动的颜色调节。 ### 何时不用 - 用户需要在色域中自由取色时,使用[颜色选择器](./color-picker),它带二维取色区。 - 只从几个固定颜色中选一个时,使用[颜色色块选择器](./color-swatch-picker)。 - 调整的是普通数值时,使用[滑块](./slider)。 ### 特性 - `channel` 七选一:`hue`(0-360)、`saturation` / `brightness` / `alpha`(0-100)、`red` / `green` / `blue`(0-255);步长 1,PageUp / PageDown 走 10。 - 值始终是完整颜色串,`format` 决定写法(hex / rgba / hsla);`alpha` 决定串中是否带透明度,默认调整透明度通道时带、其余不带。与透明度滑块并排时显式开启它,否则调整色相会把透明度归 1。 - 轨道渐变由连接层按当前颜色实时计算:其余分量不变,只让本通道从 min 走到 max;透明度通道从全透明走到实色,底部垫棋盘格。 - 灰度与纯黑处色相无定义,把明度调到 0 再拉回时色相由锚点保持,不塌为 0。 - 拖动、键盘、RTL 方向与竖直排布全部取自内嵌的[滑块](./slider);`onValueChange` 在拖动中连续发出,`onValueChangeEnd` 在松手时只发一次。 - 拇指按未取整的工作色定位,比按整格计算更贴近当前颜色;`aria-valuetext` 带单位播报。 - 尺寸 sm / md / lg 改变拇指直径与颜色带厚度;禁用时标签换到禁用前景、颜色带与拇指压暗且拇指不再抬起,只读保留 Tab 位但不可调整,`invalid` 只改变拇指描边,保留当前颜色的面。 ### 组合 - 放入[表单字段](./field):标签、说明与错误由字段渲染并经 aria-describedby 关联到拇指,禁用 / 只读 / 无效三轴随字段下发。 - 与[颜色色块](./color-swatch)并排:色块显示完整颜色,滑块调整其中一个通道。 ### 最佳实践 - 多条并排时共用同一个值,每条只改自己的通道;开启 `alpha` 让透明度在其他通道调整时不丢失。 - 提供 `label` 部件或 `translations.label`,渐变带本身无法说明调整的是什么。 - 需要持久化时监听 `onValueChangeEnd`,拖动过程中的连续回调只用于预览。 ### 反模式 - 用它调整普通数值:渐变、单位与区间都按颜色通道固定。 - 传颜色关键字(`red`):不在支持的写法内,会被视为无效值并保持不变。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhColorSliderControl` `XhColorSliderHiddenInput` `XhColorSliderLabel` `XhColorSliderRoot` `XhColorSliderThumb` `XhColorSliderTrack` `XhColorSliderValueText` | | 组合式函数 | `useColorSlider` | | 状态机 | `colorSliderMachine` | | 皮肤 | `@xihan-ui/styles/color-slider.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | | 颜色值串。提供即受控:cell 直读 prop,写入只发 onValueChange 不落内部值。 | | `defaultValue` | `string` | | | | `channel` | `ColorChannel` | | 推动的通道:色相 / 饱和度 / 明度 / 透明度 / 红 / 绿 / 蓝,默认 hue。 | | `format` | `ColorFormat` | | 值串的写法,默认 hex。修改它只改变对外的序列化,工作色恒为 HSVA。 | | `hsva` | `ColorHsva` | | 受控的工作色。提供时本通道以外的分量、灰度处的色相都以它为准,不再从值串反解: 取色器把同一份工作色交给多条并排的滑块,推动色相时饱和度与明度不会被值串抹除。 单独使用一条滑块时不必提供,滑块自行记录锚点。 | | `alpha` | `boolean` | | 值串是否带透明度。默认随通道决定:推动透明度通道时带,其余不带。 显式提供 true 时其他通道也保留透明度(与一条透明度滑块并排时需开启,否则推动色相会把透明度归 1)。 | | `orientation` | `Orientation` | | | | `dir` | `Direction` | | 文字方向。只改写水平轨道上左右两键与指针的语义。 | | `disabled` | `boolean` | | | | `readOnly` | `boolean` | | | | `invalid` | `boolean` | | | | `size` | `Size` | | 尺寸:sm / md / lg,决定拇指直径与轨道厚度。 | | `name` | `string` | | 表单字段名;提供后表单影子才带 name 并参与提交。 | | `translations` | `Partial` | | | | `onValueChange` | `(details: ColorSliderValueChangeDetails) => void` | | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。拖动过程中连续发出。 | | `onValueChangeEnd` | `(details: ColorSliderValueChangeDetails) => void` | | 只在一次操作结束时发出一次,适合用于发起请求。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `ColorSliderValueChangeDetails` | 颜色变化;detail 为 `{ value: string }`,拖动过程中连续发出 | | `value-change-end` | `ColorSliderValueChangeDetails` | 一次推动结束;detail 为 `{ value: string }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhColorSliderRoot` | `default` | `ColorSliderRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhColorSliderRoot` | `children` | `SlotChildren` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`VALUE.SET` · `CHANNEL.SET` · `CHANGE.END` · `FORM.RESET` **判据**:`canInteract` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string` | 当前值串(与 onValueChange 发出的是同一个)。 | | `channel` | `ColorChannel` | | | `channelValue` | `number` | 本通道当前的对外数值(色相为角度,饱和度 / 明度 / 透明度为百分数,红绿蓝为 0-255)。 | | `percent` | `number` | 值在轨道上的位置,0-1。按未取整的工作色计算,比滑杆按整格计算的值更贴近当前颜色。 | | `min` | `number` | | | `max` | `number` | | | `hsva` | `ColorHsva` | | | `rgba` | `ColorRgba` | | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `dragging` | `boolean` | 指针正在拖动拇指。 | | `setValue` | `(next: string) => void` | | | `setChannelValue` | `(next: number) => void` | 直接把本通道推到某个对外数值。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['label']` | | | `getControlProps` | `() => T['element']` | | | `getTrackProps` | `() => T['element']` | 轨道:渐变由连接层按当前颜色计算并写为内联 background-image。 | | `getThumbProps` | `() => T['element']` | | | `getValueTextProps` | `() => T['element']` | | | `getHiddenInputProps` | `() => T['input']` | 表单影子:值随表单提交。提供 name 后才带 name,未提供时不参与提交。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `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 格) | | `PageDown` | focus in thumb, not disabled/readOnly | 按 largeStep 减小 | | `Home` | focus in thumb, not disabled/readOnly | 取本通道的 min | | `End` | focus in thumb, not disabled/readOnly | 取本通道的 max | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `thumb` | `aria-disabled` | 'true' \| 'false' | | `thumb` | `aria-label` | label.label(channel) | | `thumb` | `aria-labelledby` | `label` 部件的 id | | `thumb` | `aria-orientation` | props.orientation | | `thumb` | `aria-valuemax` | String(range.max) | | `thumb` | `aria-valuemin` | String(range.min) | | `thumb` | `aria-valuenow` | String(channelValue) | | `thumb` | `aria-valuetext` | label.valueText(channel, channelValue) | | `thumb` | `role` | 'slider' | | `value-text` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/color-slider.css` 使用 `[data-scope="color-slider"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-channel` | colorToChannel(prop('channel')) | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-dragging` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-orientation` | props.orientation | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-value` | context.get('value') | | `label` | `data-channel` | colorToChannel(prop('channel')) | | `label` | `data-disabled` | ''(条件成立时才出现) | | `label` | `data-dragging` | ''(条件成立时才出现) | | `label` | `data-invalid` | ''(条件成立时才出现) | | `label` | `data-orientation` | props.orientation | | `label` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-channel` | colorToChannel(prop('channel')) | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-dragging` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-orientation` | props.orientation | | `control` | `data-readonly` | ''(条件成立时才出现) | | `track` | `data-channel` | colorToChannel(prop('channel')) | | `track` | `data-disabled` | ''(条件成立时才出现) | | `track` | `data-dragging` | ''(条件成立时才出现) | | `track` | `data-invalid` | ''(条件成立时才出现) | | `track` | `data-orientation` | props.orientation | | `track` | `data-readonly` | ''(条件成立时才出现) | | `thumb` | `data-channel` | colorToChannel(prop('channel')) | | `thumb` | `data-disabled` | ''(条件成立时才出现) | | `thumb` | `data-dragging` | ''(条件成立时才出现) | | `thumb` | `data-invalid` | ''(条件成立时才出现) | | `thumb` | `data-orientation` | props.orientation | | `thumb` | `data-readonly` | ''(条件成立时才出现) | | `value-text` | `data-channel` | colorToChannel(prop('channel')) | | `value-text` | `data-disabled` | ''(条件成立时才出现) | | `value-text` | `data-dragging` | ''(条件成立时才出现) | | `value-text` | `data-invalid` | ''(条件成立时才出现) | | `value-text` | `data-orientation` | props.orientation | | `value-text` | `data-readonly` | ''(条件成立时才出现) | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-color-slider-checker` | `track` | `background-image` | `channel=alpha` | `--xh-color-neutral-300` | color-slider 的 track 部件 background-image 覆盖槽。 | | `--xh-color-slider-checker-base` | `track` | `background-color` | `channel=alpha` | `--xh-bg-surface` | color-slider 的 track 部件 background-color 覆盖槽。 | | `--xh-color-slider-gap` | `root` | `gap` | `default` | `--xh-space-1` | color-slider 的 root 部件 gap 覆盖槽。 | | `--xh-color-slider-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | color-slider 的 label 部件 color 覆盖槽。 | | `--xh-color-slider-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | color-slider 的 label 部件 color 覆盖槽。 | | `--xh-color-slider-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | color-slider 的 label 部件 font-size 覆盖槽。 | | `--xh-color-slider-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | color-slider 的 label 部件 font-weight 覆盖槽。 | | `--xh-color-slider-thumb-bg` | `thumb` | `background` | `default` | `--xh-_color-slider-thumb-color` | color-slider 的 thumb 部件 background 覆盖槽。 | | `--xh-color-slider-thumb-border` | `thumb` | `border` | `default` | `--xh-border-default-opaque` | color-slider 的 thumb 部件 border 覆盖槽。 | | `--xh-color-slider-thumb-border-invalid` | `thumb` | `border-color` | `invalid` | `--xh-border-invalid` | color-slider 的 thumb 部件 border-color 覆盖槽。 | | `--xh-color-slider-thumb-radius` | `thumb` | `border-radius` | `default` | `--xh-shape-circle` | color-slider 的 thumb 部件 border-radius 覆盖槽。 | | `--xh-color-slider-thumb-scale-dragging` | `thumb` | `scale` | `dragging` | `--xh-motion-scale-drag` | color-slider 的 thumb 部件 scale 覆盖槽。 | | `--xh-color-slider-thumb-shadow` | `thumb` | `box-shadow` | `default` | `--xh-elevation-raised` | color-slider 的 thumb 部件 box-shadow 覆盖槽。 | | `--xh-color-slider-thumb-shadow-disabled` | `thumb` | `box-shadow` | `disabled` | `none` | color-slider 的 thumb 部件 box-shadow 覆盖槽。 | | `--xh-color-slider-thumb-shadow-dragging` | `thumb` | `box-shadow` | `dragging` | `--xh-elevation-lifted` | color-slider 的 thumb 部件 box-shadow 覆盖槽。 | | `--xh-color-slider-thumb-size` | `control`
`root`
`thumb` | `block-size`
`inline-size`
`margin-block-end`
`margin-block-start`
`margin-inline-start` | `default`
`orientation=horizontal`
`orientation=vertical`
`size=lg`
`size=sm` | `--xh-space-3`
`--xh-space-6`
`--xh-track-thumb-size` | color-slider 的 control、root、thumb 部件 block-size、inline-size、margin-block-end、margin-block-start、margin-inline-start 覆盖槽。 | | `--xh-color-slider-track-border` | `track` | `box-shadow` | `default` | `--xh-border-subtle` | color-slider 的 track 部件 box-shadow 覆盖槽。 | | `--xh-color-slider-track-radius` | `track` | `border-radius` | `default` | `--xh-shape-pill` | color-slider 的 track 部件 border-radius 覆盖槽。 | | `--xh-color-slider-track-thickness` | `control`
`root`
`track` | `block-size`
`inline-size` | `default`
`orientation=horizontal`
`orientation=vertical`
`size=lg`
`size=sm` | `--xh-space-2`
`--xh-space-3`
`--xh-space-4` | color-slider 的 control、root、track 部件 block-size、inline-size 覆盖槽。 | | `--xh-color-slider-value-text-bg` | `value-text` | `background` | `default` | `--xh-bg-brand` | color-slider 的 value-text 部件 background 覆盖槽。 | | `--xh-color-slider-value-text-fg` | `value-text` | `color` | `default` | `--xh-fg-on-brand` | color-slider 的 value-text 部件 color 覆盖槽。 | | `--xh-color-slider-value-text-font-size` | `value-text` | `font-size` | `default` | `--xh-text-caption-size` | color-slider 的 value-text 部件 font-size 覆盖槽。 | | `--xh-color-slider-value-text-offset` | `value-text` | `margin-block-end`
`margin-inline` | `default`
`orientation=vertical` | `--xh-space-2` | color-slider 的 value-text 部件 margin-block-end、margin-inline 覆盖槽。 | | `--xh-color-slider-value-text-px` | `value-text` | `padding-inline` | `default` | `--xh-space-2` | color-slider 的 value-text 部件 padding-inline 覆盖槽。 | | `--xh-color-slider-value-text-py` | `value-text` | `padding-block` | `default` | `--xh-space-0_5` | color-slider 的 value-text 部件 padding-block 覆盖槽。 | | `--xh-color-slider-value-text-radius` | `value-text` | `border-radius` | `default` | `--xh-shape-control` | color-slider 的 value-text 部件 border-radius 覆盖槽。 | | `--xh-color-slider-vertical-length` | `control` | `block-size` | `orientation=vertical` | `--xh-overlay-menu-min-w` | color-slider 的 control 部件 block-size 覆盖槽。 | ### 动效 动效角色:状态 · 切换(见[动效规范](../design/motion#角色))。 可覆盖的动效槽:`--xh-color-slider-thumb-scale-dragging`。 `box-shadow` · `opacity` · `scale` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/color-swatch-picker # ColorSwatchPicker 颜色色块选择器 从一组固定颜色中选择一个:主题色、标签色、高亮色。每格是一个 `role=radio` 的色块,整组是一个 `radiogroup`,即把选项绘制为颜色的[单选组](./radio-group)。需要自由调出任意颜色时使用[颜色选择器](./color-picker),它内嵌的预设色板使用的就是本组件。 ## 用法 提供一组颜色数据即自动铺开;每格是一个 radio,方向键在格子间移动并选中 ```vue ``` ```html
主题色
当前:#3b82f6
``` ## 组件结构 加粗的是必需部件。 `data-scope="color-swatch-picker"`:**`root`** · `label` · **`item`** · **`swatch`** · `indicator` · `hidden-input` ## 示例 ### 手写格子 不提供数据也可以:每格自行声明 value,名字与禁用写在格子上;半透明颜色铺在棋盘格上 ```vue ``` ```html
高亮色
当前:rgb(225, 29, 72)
``` ### 状态 禁用整组置灰、只读只阻止落值不阻止焦点、无效把描边转为警示色 ```vue ``` ```html
禁用
只读
必填未选
``` ### 尺寸与语气 格子边长跟随控件行高分三档;tone 决定选中描边与选中徽标使用哪族颜色 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 可选颜色是有限的一组,且每个颜色都有含义(品牌色、状态色、日历分类色)。 - 需要一次看到全部选项再挑选,不打开浮层。 - 表单需要提交一个颜色串,且不需要自由调色。 ### 何时不用 - 需要自由调出任意颜色时,使用[颜色选择器](./color-picker),它把色板与取色面组合在一起。 - 只展示一个颜色、不接受选择时,使用[颜色色块](./color-swatch)。 - 需要手动输入颜色串时,使用[颜色字段](./color-field)。 - 选项不是颜色而是文字时,使用[单选组](./radio-group)。 ### 特性 - 选中按颜色比较而不按字符串比较:`rgb(255, 0, 0)` 与 `#ff0000` 是同一格,受控 `value` 使用任一写法都能匹配。 - 与单选组同一套 roving tabindex:整组只占一个 Tab 位,四个方向键在格子间移动焦点并选中,到末端回绕,禁用格跳过;Space 选中当前格。 - 焦点从组外进入时落在已选中的格子,没有选中时落在第一格。 - `swatches` 提供数据:可访问名称与禁用从数据中读取,格子部件只需报告 `value`;不写默认内容时按数据自动铺开。 - 每格的色块面由 Swatch 家族绘制:无法解析的串只显示棋盘格,半透明颜色铺在棋盘格上。 - `readOnly` 时方向键照常移动焦点但不取值;`disabled` 整组置灰,格子仍可聚焦。 - 尺寸 sm / md / lg 三档:格子边长跟随控件行高,与旁边的按钮、字段等高。 - 选中徽标与色块的品牌描边随 `data-tone` 变化;徽标自带一圈画布色描边,落在任何颜色上都可见。 - 高对比模式下色块保留原色,选中描边与徽标换用系统高亮色;打印时徽标改为实边。 ### 组合 - 内嵌在[颜色选择器](./color-picker)的浮层中作为预设色板。 - 与[颜色字段](./color-field)并排:色板选择常用色,字段输入精确值。 - 放入[表单字段](./field)承接标题、说明与错误信息,`disabled` / `readOnly` / `invalid` / `required` 随字段下发。 ### 最佳实践 - 每格提供名称(`swatches[].label` 或部件的 `label`),读屏用户听到的应是“品牌红”而不是 `#e11d48`。 - 色板的颜色数量控制在一眼可扫完的范围,更多时改用[颜色选择器](./color-picker)。 - 有初始值时使用 `defaultValue`,焦点进组时直接落在该格。 ### 反模式 - 用它做多选:一格只能选中一个;需要多选颜色时使用[复选框组](./checkbox-group)配合[颜色色块](./color-swatch)。 - 把颜色写成 `red` 等关键字:不在支持的写法内,该格只显示棋盘格。 - 同一组内放同一颜色的不同写法:它们会同时算作选中。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhColorSwatchPickerItem` `XhColorSwatchPickerLabel` `XhColorSwatchPickerRoot` | | 组合式函数 | `useColorSwatchPicker` | | 状态机 | `colorSwatchPickerMachine` | | 皮肤 | `@xihan-ui/styles/color-swatch-picker.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `swatches` | `ColorSwatchPickerNode[]` | | 格子数据,可及名与禁用的事实源。提供后格子部件只需声明 value。 未提供时回到名字与禁用都写在格子部件上的方式。 | | `value` | `string \| null` | | 选中的颜色串。提供即受控:写入只发 onValueChange 不落内部值。写法不同的同一颜色也视为选中。 | | `defaultValue` | `string \| null` | | | | `disabled` | `boolean` | | | | `readOnly` | `boolean` | | 只读:不可选择,但仍可聚焦、方向键照常移动焦点,对比度不降低。 | | `invalid` | `boolean` | | 校验失败:只改变呈现,不阻止交互。 | | `required` | `boolean` | | 必填:随表单校验一起使用,只发无障碍属性,不自行拦截提交。 | | `dir` | `Direction` | | 文字方向,默认 'ltr';只改写左右两键的语义。 | | `name` | `string` | | 表单字段名。 | | `size` | `Size` | | 尺寸:sm / md / lg,影响格子的边长与间距。 | | `tone` | `Tone` | | 语气:决定选中环与选中标记使用哪族颜色。 | | `translations` | `Partial` | | | | `onValueChange` | `(details: ColorSwatchPickerValueChangeDetails) => void` | | value 变化回调。 | ### ColorSwatchPickerNode `swatches` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | 颜色串,也是该格的身份。 | | `label` | `string` | | 读屏朗读该格的方式,例如「品牌红」;默认朗读颜色串。 | | `disabled` | `boolean` | | 该格禁用:方向键跳过它,但它仍可聚焦、仍是导航起点。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `ColorSwatchPickerValueChangeDetails` | 选中值变化;detail 为 `{ value: string \| null }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhColorSwatchPickerRoot` | `default` | `ColorSwatchPickerRootSlotProps` | | | `XhColorSwatchPickerRoot` | `label` | — | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhColorSwatchPickerItem` | `value` | `string` | 是 | | | `XhColorSwatchPickerItem` | `label` | `string` | | 读屏朗读该格子的方式;默认交给 connect 查询 swatches,都没有时朗读颜色串。 | | `XhColorSwatchPickerItem` | `disabled` | `boolean` | | 默认交给 connect 查询 swatches,写死 false 会覆盖数据中的禁用。 | | `XhColorSwatchPickerRoot` | `label` | `ReactNode` | | 标题文字。提供后不必再写 label 部件。 | | `XhColorSwatchPickerRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `item` | 'checked' \| 'unchecked' | | `swatch` | 'checked' \| 'unchecked' | | `indicator` | 'checked' \| 'unchecked' | | `hidden-input` | 'checked' \| 'unchecked' | 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`VALUE.SET` · `ITEM.SELECT` · `ITEM.FOCUS` · `GROUP.BLUR` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string \| null` | | | `swatches` | `readonly ColorSwatchPickerNodeMeta[]` | 由 swatches 推导的格子元信息,按数据顺序排列;未提供 swatches 时为空数组。 | | `focusedValue` | `string \| null` | 焦点在组外时为 null。 | | `isSelected` | `(value: string) => boolean` | 某个颜色串是否为当前选中的格:写法不同(`#f00` 与 `rgb(255,0,0)`)也视为同一颜色。 | | `setValue` | `(next: string \| null) => void` | | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['element']` | | | `getItemProps` | `(props: ColorSwatchPickerItemProps) => T['element']` | 一格:role=radio,颜色串是它的身份。 | | `getSwatchProps` | `(props: ColorSwatchPickerItemProps) => T['element']` | 格内的色块面:使用 Swatch 家族绘制颜色,纯装饰。 | | `getIndicatorProps` | `(props: ColorSwatchPickerItemProps) => T['element']` | 选中标记(对号),纯装饰。 | | `getHiddenInputProps` | `(props: ColorSwatchPickerItemProps) => T['input']` | 格子对应的隐藏原生 radio 输入,用于表单提交。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/radio/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Tab` / `Shift+Tab` | focus outside the group | 整组只占一个 Tab 位:焦点进入锚点格子(即选中的那格);落到容器上时由容器转投锚点格子,锚点缺席或被禁用才落首个可停留格 | | `ArrowDown` / `ArrowRight` | focus in group, group not disabled | 焦点移到下一个可停留格并选中,末格回绕到首格;dir=rtl 时改由 ArrowLeft 承担 | | `ArrowUp` / `ArrowLeft` | focus in group, group not disabled | 焦点移到上一个可停留格并选中,首格回绕到末格;dir=rtl 时改由 ArrowRight 承担 | | `Space` | focus on item, item not disabled | 选中当前格 | | `Space` | held on item, 格子未禁用且组未禁用、非只读 | 按住期间该格投影 data-pressed,与指针 :active 同一副按压面(换描边并缩放);抬起或失焦撤下,按住途中整组转入禁用或只读也撤下。Enter 不是 radio 的激活键,按住它没有按压面 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-invalid` | 'true' \| 'false' | | `root` | `aria-label` | label.group | | `root` | `aria-labelledby` | `label` 部件的 id | | `root` | `aria-readonly` | 'true' \| 'false' | | `root` | `aria-required` | 'true' \| 'false' | | `root` | `role` | 'radiogroup' | | `item` | `aria-checked` | 'true' \| 'false' | | `item` | `aria-disabled` | 'true' \| 'false' | | `item` | `aria-label` | itemLabel(item) | | `item` | `role` | 'radio' | | `swatch` | `aria-hidden` | 'true' | | `indicator` | `aria-hidden` | 'true' | | `hidden-input` | `aria-hidden` | 'true' | - 根是 `role=radiogroup`,名称取 label 部件,未提供时读 `translations.group`。 - 每格是 `role=radio` 并显式输出 `aria-checked`;名称依次取 `label`、`swatches` 中的 `label`、`translations.swatch(value)`,颜色串无法表达含义时务必提供名称。 - 禁用格用 `aria-disabled` 表达,仍可聚焦,仍是方向键的起点。 - 每格内有一个 `inert` 的隐藏原生 radio 承接表单提交,不进入焦点序列与可访问树。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/color-swatch-picker.css` 使用 `[data-scope="color-swatch-picker"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-required` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `label` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-invalid` | ''(条件成立时才出现) | | `item` | `data-pressed` | ''(条件成立时才出现) | | `item` | `data-readonly` | ''(条件成立时才出现) | | `item` | `data-state` | 'checked' \| 'unchecked' | | `swatch` | `data-disabled` | ''(条件成立时才出现) | | `swatch` | `data-invalid` | ''(条件成立时才出现) | | `swatch` | `data-readonly` | ''(条件成立时才出现) | | `swatch` | `data-state` | 'checked' \| 'unchecked' | | `swatch` | `data-xh-swatch` | '' | | `swatch` | `data-xh-swatch-size` | props.size | | `indicator` | `data-disabled` | ''(条件成立时才出现) | | `indicator` | `data-invalid` | ''(条件成立时才出现) | | `indicator` | `data-readonly` | ''(条件成立时才出现) | | `indicator` | `data-state` | 'checked' \| 'unchecked' | | `hidden-input` | `data-disabled` | ''(条件成立时才出现) | | `hidden-input` | `data-invalid` | ''(条件成立时才出现) | | `hidden-input` | `data-readonly` | ''(条件成立时才出现) | | `hidden-input` | `data-state` | 'checked' \| 'unchecked' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-color-swatch-picker-gap` | `label`
`root` | `gap`
`margin-block-end` | `default` | `--xh-_color-swatch-picker-gap` | color-swatch-picker 的 label、root 部件 gap、margin-block-end 覆盖槽。 | | `--xh-color-swatch-picker-icon-size` | `root` | `--xh-icon-size` | `default` | `--xh-_color-swatch-picker-mark` | color-swatch-picker 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-color-swatch-picker-indicator-bg` | `indicator` | `background` | `default` | `--xh-_color-swatch-picker-accent` | color-swatch-picker 的 indicator 部件 background 覆盖槽。 | | `--xh-color-swatch-picker-indicator-border` | `indicator` | `border` | `default` | `--xh-bg-canvas` | color-swatch-picker 的 indicator 部件 border 覆盖槽。 | | `--xh-color-swatch-picker-indicator-fg` | `indicator` | `background-color`
`color` | `default`
`empty` | `--xh-_tone-on` | color-swatch-picker 的 indicator 部件 background-color、color 覆盖槽。 | | `--xh-color-swatch-picker-indicator-size` | `indicator` | `block-size`
`inline-size` | `default` | `--xh-_color-swatch-picker-indicator` | color-swatch-picker 的 indicator 部件 block-size、inline-size 覆盖槽。 | | `--xh-color-swatch-picker-item-radius` | `item`
`swatch` | `--xh-swatch-radius`
`border-radius` | `default` | `--xh-shape-inset` | color-swatch-picker 的 item、swatch 部件 --xh-swatch-radius、border-radius 覆盖槽。 | | `--xh-color-swatch-picker-item-size` | `item` | `block-size`
`inline-size` | `default` | `--xh-_color-swatch-picker-cell` | color-swatch-picker 的 item 部件 block-size、inline-size 覆盖槽。 | | `--xh-color-swatch-picker-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | color-swatch-picker 的 label 部件 color 覆盖槽。 | | `--xh-color-swatch-picker-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | color-swatch-picker 的 label 部件 color 覆盖槽。 | | `--xh-color-swatch-picker-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | color-swatch-picker 的 label 部件 font-size 覆盖槽。 | | `--xh-color-swatch-picker-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | color-swatch-picker 的 label 部件 font-weight 覆盖槽。 | | `--xh-color-swatch-picker-label-gap` | `label` | `margin-block-end` | `default` | `--xh-space-1` | color-swatch-picker 的 label 部件 margin-block-end 覆盖槽。 | | `--xh-color-swatch-picker-ring` | `item`
`swatch` | `--xh-swatch-border` | `state=checked` | `--xh-_tone` | color-swatch-picker 的 item、swatch 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-swatch-picker-swatch-border` | `item`
`swatch` | `--xh-swatch-border` | `default` | `--xh-border-default` | color-swatch-picker 的 item、swatch 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-swatch-picker-swatch-border-hover` | `item`
`swatch` | `--xh-swatch-border` | `disabled`
`hover`
`not([data-disabled], [data-readonly], [data-state='checked'])`
`readonly`
`state=checked` | `--xh-border-control-hover` | color-swatch-picker 的 item、swatch 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-swatch-picker-swatch-border-invalid` | `swatch` | `--xh-swatch-border` | `invalid` | `--xh-border-invalid` | color-swatch-picker 的 swatch 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-swatch-picker-swatch-border-pressed` | `item`
`swatch` | `--xh-swatch-border` | `disabled`
`is(:active, [data-pressed])`
`not([data-disabled], [data-readonly])`
`pressed`
`readonly` | `--xh-_tone` | color-swatch-picker 的 item、swatch 部件 --xh-swatch-border 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 切换(见[动效规范](../design/motion#角色))。 `border-color` · `opacity` · `scale` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 - 格子排成可换行的网格,一行放不下时换行;粗指针下命中区是整格。 ### RTL - `dir="rtl"` 只对调左右方向键的语义,上下键不受影响;格子的排列顺序由文档方向决定。 --- 来源:https://ui.docs.xihanfun.com/components/color-swatch # ColorSwatch 颜色色块 把一个颜色绘制为一小块用于展示:主题色一览、图例中的一格、当前选中的颜色。它只负责表达“这是什么颜色”,不接受点击、不修改值;需要挑选颜色时使用[颜色选择器](./color-picker)。 ## 用法 把颜色串绘制为一小块:颜色旁边写出串本身,可见也可读 ```vue ``` ```html
#e11d48 #f59e0b #10b981 #3b82f6 #8b5cf6
``` ## 组件结构 加粗的是必需部件。 `data-scope="color-swatch"`:**`root`** ## 示例 ### 尺寸 sm / md / lg 三档改变边长与棋盘格粒度,圆角恒为内嵌档 ```vue ``` ```html
``` ### 透明度 半透明颜色铺在棋盘格上,可以看出这是带透明度的颜色;三种写法解析为同一个颜色 ```vue ``` ```html
#e11d48 #e11d48bf rgba(225, 29, 72, 0.5) hsla(347, 77%, 50%, 0.25)
``` ### 图例与无效值 label 为读屏提供有含义的名字;无法解析的串只剩棋盘格并标为无效 ```vue ``` ```html
  • 已完成
  • 进行中
  • 已逾期
tomato(无效:不认颜色关键字)
``` ## 设计指引 ### 何时使用 - 在文字旁直观标出一个颜色:图例、标签、主题预览。 - 与颜色串并排展示,让用户既能看到也能读出。 - 作为其他控件中表示当前颜色的小块(颜色选择器的触发按钮使用的就是这一族)。 ### 何时不用 - 用户需要在若干固定颜色中选一个时,使用[颜色色块选择器](./color-swatch-picker),它每格的色块面使用的就是这一族。 - 用户需要自由调色时,使用[颜色选择器](./color-picker)。 - 只是给一段文字加底色或强调时,属于排版范畴,不使用颜色色块。 ### 特性 - 支持 `#rgb` / `#rrggbb(aa)`、`rgb()` / `rgba()`、`hsl()` / `hsla()`;无法解析时只绘制棋盘格底并带 `data-invalid`,不静默绘制为黑色。 - 半透明颜色铺在棋盘格上,能看出这是带透明度的颜色,而不是与页面底色混合成另一种颜色。 - 尺寸 sm / md / lg 三档改变边长与棋盘格粒度,圆角固定为内嵌档。 - 读屏按 `label` 读出,其次读颜色串;两者都没有时整块视为装饰。 - 高对比模式下退出强制着色以保留原色,描边换成系统前景色勾出轮廓;打印时保留底色。 ### 组合 - 与[标签](./tag)、[列表](./list)条目并排作为图例;与[颜色选择器](./color-picker)搭配时,选择器的触发按钮自带同族色块,不需要再放一个。 ### 最佳实践 - 颜色有含义时提供 `label`(“品牌红”“已完成”),只读出 `#e11d48` 无法表达含义。 - 色块旁写出颜色串或名称,颜色不能是唯一的信息通道。 ### 反模式 - 将它当作按钮:色块不接受交互。 - 传颜色关键字(`red`、`transparent`):不在支持的写法内,会被判定为无效。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhColorSwatch` | | 状态机 | 无,`connect` 直接由 props 算属性 | | 皮肤 | `@xihan-ui/styles/color-swatch.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `label` | `string` | | 读屏朗读该颜色的方式,例如「品牌红」。 未提供时朗读颜色串本身;串也没有时整块视为装饰,不进入可访问树。 | | `size` | `Size` | | 尺寸:sm / md / lg,影响色块的边长与棋盘格粒度。 | | `value` | `string` | | 要展示的颜色串。识别 `#rgb` / `#rrggbb(aa)`、`rgb()` / `rgba()`、`hsl()` / `hsla()`, 不识别颜色关键字。无法解析时色块只保留棋盘格底,并带 `data-invalid`。 | ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string` | 原样透出的颜色串(未解析时也是它,使 data-value 与作者书写的一致)。 | | `valid` | `boolean` | 颜色串解析成功。失败时 rgba 为兜底黑、色块只绘制棋盘格。 | | `rgba` | `ColorRgba` | | | `css` | `string` | 绘制进色块的 CSS 颜色(rgba() 写法,带透明度);解析失败时为空串。 | | `getRootProps` | `() => T['element']` | 色块本体:role=img,名字取 label,其次取颜色串。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-hidden` | undefined \| 'true' | | `root` | `aria-label` | props.label | | `root` | `role` | 'img' \| undefined | ## 样式参考 ### 皮肤 `@xihan-ui/styles/color-swatch.css` 使用 `[data-scope="color-swatch"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-value` | value \|\| undefined | | `root` | `data-xh-swatch` | '' | | `root` | `data-xh-swatch-size` | props.size | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-color-swatch-border` | `root` | `--xh-swatch-border` | `default` | `--xh-border-default` | color-swatch 的 root 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-swatch-border-invalid` | `root` | `--xh-swatch-border` | `invalid` | `--xh-border-invalid` | color-swatch 的 root 部件 --xh-swatch-border 覆盖槽。 | | `--xh-color-swatch-radius` | `root` | `--xh-swatch-radius` | `default` | `--xh-shape-inset` | color-swatch 的 root 部件 --xh-swatch-radius 覆盖槽。 | | `--xh-color-swatch-size` | `root` | `--xh-swatch-size` | `default` | `--xh-_swatch-size` | color-swatch 的 root 部件 --xh-swatch-size 覆盖槽。 | ### 动效 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 --- 来源:https://ui.docs.xihanfun.com/components/combobox # Combobox 组合框 将输入框与候选列表结合,用于搜索并选择选项。 ## 用法 搜索并选择城市 ```vue ``` ```html
Beijing 北京
Berlin 柏林
Bern 伯尔尼
Busan 釜山(禁用)
London 伦敦
无匹配城市
``` ## 组件结构 加粗的是必需部件。 `data-scope="combobox"`:`root` · `label` · **`control`** · **`input`** · `trigger` · `clear-trigger` · `positioner` · **`content`** · `item` · `item-prefix` · `item-text` · `item-description` · `item-suffix` · `item-indicator` · `group` · `group-label` · `empty` · `loading` · `hidden-input` ## 示例 ### 多选 选择多个城市 ```vue ``` ```html
Beijing 北京
Berlin 柏林
Chengdu 成都
London 伦敦
无匹配城市
``` ### 自定义值 选择候选项或输入新值 ```vue ``` ```html
Vue
React
Svelte
按 Enter 使用当前输入
``` ### 分组 按分类组织候选项 ```vue ``` ```html
亚洲
Beijing 北京
Chengdu 成都
欧洲
Berlin 柏林
London 伦敦
无匹配城市
``` ### 变体 设置输入框外观 ```vue ``` ```html
苹果
香蕉
樱桃
苹果
香蕉
樱桃
苹果
香蕉
樱桃
``` ### 校验状态 标记无效输入 ```vue ``` ```html
Beijing 北京
Berlin 柏林
Chengdu 成都
``` ### 异步候选 查询远程数据 ```vue ``` ```html
查询中…
无匹配城市
``` ### 自定义内容 在候选项中显示辅助信息 ```vue ``` ```html
name@gmail.com Google 邮箱
name@qq.com QQ 邮箱
name@163.com 网易邮箱
没有匹配的邮箱
``` ## 设计指引 ### 何时使用 - 选项较多,需要通过输入快速筛选。 - 候选来自远程数据或允许输入自定义值。 ### 何时不用 - 选项固定且不多时,使用[选择器](./select)。 - 在正文中插入引用时,使用[提及](./mention)。 - 输入一组自由标签时,使用[标签输入](./tags-input)。 ### 特性 - 支持单选、多选、分组和自定义值。 - 候选可逐条声明语气,失效或需要留意的那条自带该族字色与高亮底。 - 候选可写副文本,第 2 行放一句解释,与标题同列、走 muted 档。 - 行首与行尾两格各有逐条钩子:只想加个图标或计数,不必把整条重搭。 - 输入值、选中值与展开状态均可独立受控。 - `loading` 与 `empty` 分别表示加载和空结果。 - 支持自定义过滤、异步候选和自定义条目内容。 - 通过隐藏输入参与原生表单提交。 ### 组合 - 放进[表单字段](./field)获得标签、说明与错误信息,字段状态会接到输入框上。 - 候选列表是常驻的[列表框](./listbox)收进浮层的形态;选项固定且不需要输入时换成[选择器](./select)。 - 多选时的已选项可用[标签组](./tag-group)排在输入框前。 ### 最佳实践 - 为异步查询提供加载和空结果反馈。 - 远程过滤应使用防抖,并取消过期请求。 - 允许自定义值时,明确提示 Enter 会采用当前输入。 - 使用标签说明字段含义,使用占位文本提示搜索方式。 ### 反模式 - 未经说明就接受候选列表之外的值。 - 每次按键都立即发起远程请求。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhComboboxClearTrigger` `XhComboboxContent` `XhComboboxControl` `XhComboboxEmpty` `XhComboboxGroup` `XhComboboxGroupLabel` `XhComboboxHiddenInput` `XhComboboxInput` `XhComboboxItem` `XhComboboxItemDescription` `XhComboboxItemIndicator` `XhComboboxItemPrefix` `XhComboboxItemSuffix` `XhComboboxItemText` `XhComboboxLabel` `XhComboboxLoading` `XhComboboxPositioner` `XhComboboxRoot` `XhComboboxTrigger` | | 组合式函数 | `useCombobox` | | 状态机 | `comboboxMachine` | | 皮肤 | `@xihan-ui/styles/combobox.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `collection` | `ComboboxNode[]` | | 候选数据,显示文本与禁用的事实源。过滤仍由调用方完成:传入的即当前应显示的候选。 提供后条目部件只需声明 value,显示文本也不再从 DOM 查询。 未提供时回到文本写在条目中、从 DOM 查询的方式。 | | `value` | `string \| string[]` | | 选中值。提供即受控:cell 直读 prop,写入只发 onValueChange 不落内部值。 单选写为裸串是简写,内部一律归一为数组。 | | `defaultValue` | `string \| string[]` | | | | `inputValue` | `string` | | 输入框中的字符串。提供即受控,与选中值各自独立。 过滤不由组件完成:调用方用该串筛选条目,把筛选结果重新渲染进来。 | | `defaultInputValue` | `string` | | | | `open` | `boolean` | | 展开态。提供即受控:内部不再自行修改,只发 onOpenChange。 | | `defaultOpen` | `boolean` | | | | `name` | `string` | | 表单字段名;hidden-input 按选中值逐个生成同名字段,不使用分隔符编码。 | | `form` | `string` | | 原生表单 ID;显式关联外部表单,提交与 reset 使用同一所有者。 | | `multiple` | `boolean` | | | | `disabled` | `boolean` | | 整个控件禁用:输入框与两个按钮都使用原生 disabled。 | | `readOnly` | `boolean` | | 只读:文字可选可复制,但展开、选中、清空一概不发生。 | | `invalid` | `boolean` | | 校验失败:输入框报告 aria-invalid,各角色节点带 data-invalid。 | | `loading` | `boolean` | | 候选加载中:列表报告 aria-busy,显示在途占位、隐藏空态占位。 | | `loop` | `boolean` | | 方向键到达末尾是否回绕,默认 true。 | | `placeholder` | `string` | | 输入框占位文字。 | | `translations` | `Partial` | | 读屏文案;未提供的键使用英文默认值。 | | `allowCustomValue` | `boolean` | | 允许提交候选列表中没有的值(回车与失焦时把输入串本身收为选中值)。 | | `openOnClick` | `boolean` | | 点击输入框即展开,默认 false(只有触发按钮与方向键展开)。 | | `inputBehavior` | `ComboboxInputBehavior` | | 输入行为,默认 none。 | | `placement` | `Placement` | | | | `dir` | `Direction` | | 文字方向,默认 ltr。只改写浮层在行内轴上 start 与 end 的落点。 | | `offset` | `number` | | | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定输入行的描边与底色使用方式。默认 outline。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定聚焦与选中强调使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,决定输入行高度、内边距与字号档位。 | | `onValueChange` | `(details: ComboboxValueChangeDetails) => void` | | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 | | `onInputValueChange` | `(details: ComboboxInputValueChangeDetails) => void` | | 输入串变化回调:调用方据此重新过滤候选。 | | `onOpenChange` | `(details: ComboboxOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | ### ComboboxNode `collection` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | 展示文本,也是选中后回填输入框的取字来源;默认回退为 value。 | | `disabled` | `boolean` | | 候选禁用:方向键跳过它,点击与回车都不选中它。 | | `tone` | `Tone` | | 该条候选自身的性质:危险选项写 danger、需要留意的写 warning。不写即与其余候选同档。 只换字色与悬停 / 按下的面,不表达选中与校验;选中的标记与禁用都压过它。 彩字不是唯一通道,要紧的差别仍要配图标或文案。整个组合框的 tone 不下发给候选。 | | `description` | `string` | | 副文本,写入 item-description 部件;未提供时本条不铺该部件。 它是第 2 行的说明,跟着条目走 muted 档,不跟语气;放不下一行的解释才用它, 一句话能说清的写进 label。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `ComboboxValueChangeDetails` | 选中集合变化;detail 为 `{ value: string[] }` | | `input-value-change` | `ComboboxInputValueChangeDetails` | 输入串变化;detail 为 `{ inputValue: string }`,作者据此过滤候选 | | `open-change` | `ComboboxOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhComboboxRoot` | `default` | `ComboboxRootSlotProps` | | | `XhComboboxRoot` | `label` | — | | | `XhComboboxRoot` | `empty` | — | | | `XhComboboxRoot` | `item` | `ComboboxNodeMeta` | 只填条目的文字槽,副文本与首尾两格照旧各归各的 | | `XhComboboxRoot` | `item-prefix` | `ComboboxNodeMeta` | 只接管行首那一格,其余槽照旧由数据铺 | | `XhComboboxRoot` | `item-suffix` | `ComboboxNodeMeta` | 只接管行尾那一格(计数、徽标、次级图标),其余槽照旧由数据铺 | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhComboboxGroup` | `value` | `string` | 是 | | | `XhComboboxInput` | `as` | `ComboboxInputHost` | | 输入框渲染为哪个标签,默认 input。 写 textarea 即多行宿主:connect 随之撤销 type、role 与 aria-expanded。 | | `XhComboboxItem` | `value` | `string` | 是 | | | `XhComboboxItem` | `disabled` | `boolean` | | 默认交给 connect 查询 collection,写死 false 会覆盖数据中的禁用。 | | `XhComboboxPositioner` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhComboboxRoot` | `label` | `ReactNode` | | 标题文字。提供后不必再写 label 部件。 | | `XhComboboxRoot` | `empty` | `ReactNode` | | 无匹配时的提示语。提供后不必再写 empty 部件。 | | `XhComboboxRoot` | `clearable` | `boolean` | | 自动铺开时是否渲染清空按钮;手写部件模式不使用它,写了节点即可清空。 | | `XhComboboxRoot` | `renderItem` | `(node: ComboboxNodeMeta) => ReactNode` | | 每个候选的自定义内容;未提供时使用 collection 中的 label。 | | `XhComboboxRoot` | `renderItemPrefix` | `(node: ComboboxNodeMeta) => ReactNode` | | 只接管条目行首那一格;其余槽仍由数据铺。 | | `XhComboboxRoot` | `renderItemSuffix` | `(node: ComboboxNodeMeta) => ReactNode` | | 只接管条目行尾那一格;其余槽仍由数据铺。 | | `XhComboboxRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `control` | 'open' \| 'closed' | | `input` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `item` | 'checked' \| 'unchecked' | | `item-prefix` | 'checked' \| 'unchecked' | | `item-text` | 'checked' \| 'unchecked' | | `item-description` | 'checked' \| 'unchecked' | | `item-suffix` | 'checked' \| 'unchecked' | | `item-indicator` | 'checked' \| 'unchecked' | | `empty` | 'open' \| 'closed' | | `loading` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `ESCAPE` · `INPUT.CHANGE` · `INPUT.SET` · `INPUT.BLUR` · `ITEM.HIGHLIGHT` · `HIGHLIGHT.CLEAR` · `ITEM.SELECT` · `VALUE.COMMIT` · `VALUE.SET` · `VALUE.CLEAR` · `ITEMS.SYNC` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` · `isMultiple` · `hasHighlight` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `collection` | `readonly ComboboxNodeMeta[]` | 由 collection 推导的候选元信息,按数据顺序排列;未提供 collection 时为空数组。 | | `value` | `string[]` | 选中集合;单选模式下长度 ≤ 1,形状不随模式变化。 | | `inputValue` | `string` | 输入框中的字符串。 | | `valueText` | `string \| null` | 单选选中项的显示文本;无选中或多选时为 null。 | | `highlightedValue` | `string \| null` | 高亮候选;收起时为 null。焦点不在它身上,只经 aria-activedescendant 上报。 | | `multiple` | `boolean` | | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | | | `empty` | `boolean` | 候选为空(已结算且条数为 0)且当前展开:empty 角色节点据此显示。 | | `canClear` | `boolean` | 清空按钮当前是否可按。 | | `isSelected` | `(value: string) => boolean` | | | `setOpen` | `(next: boolean) => void` | | | `setValue` | `(next: string[]) => void` | | | `setInputValue` | `(next: string) => void` | | | `clear` | `() => void` | | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['label']` | | | `getControlProps` | `() => T['element']` | | | `getInputProps` | `(props?: ComboboxInputProps) => T['input']` | 不传参即单行 input,产出与增加此参数前逐字相同。 | | `getTriggerProps` | `() => T['button']` | | | `getClearTriggerProps` | `() => T['button']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getGroupProps` | `(props: ComboboxGroupProps) => T['element']` | | | `getGroupLabelProps` | `(props: ComboboxGroupProps) => T['element']` | | | `getItemProps` | `(props: ComboboxItemProps) => T['element']` | | | `getItemPrefixProps` | `(props: ComboboxItemProps) => T['element']` | | | `getItemTextProps` | `(props: ComboboxItemProps) => T['element']` | | | `getItemDescriptionProps` | `(props: ComboboxItemProps) => T['element']` | | | `getItemSuffixProps` | `(props: ComboboxItemProps) => T['element']` | | | `getItemIndicatorProps` | `(props: ComboboxItemProps) => T['element']` | | | `getEmptyProps` | `() => T['element']` | | | `getLoadingProps` | `() => T['element']` | 在途占位:与空态占位同一位置,两者不同时显示:加载期间显示它,空态让位。 与 content 是兄弟,同样不进入 role=listbox。 | | `getHiddenInputProps` | `(props: { value: string }) => T['input']` | 单值表单出口;按 api.value 逐个调用并生成同名 input,零选中不生成提交项。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `ArrowDown` | closed, focus in input | 展开候选列表并把高亮落到首个可选候选 | | `ArrowUp` | closed, focus in input | 展开候选列表并把高亮落到末个可选候选 | | `Alt+ArrowDown` | closed, focus in input | 展开候选列表但不预选任何候选 | | `ArrowDown` | open | 高亮移到下一个候选(禁用项跳过、尽头按 loop 回绕),焦点不动 | | `ArrowUp` | open | 高亮移到上一个候选(禁用项跳过、尽头按 loop 回绕),焦点不动 | | `Home` | open | 高亮移到首个可选候选;收起态不接管,光标照常跳到行首 | | `End` | open | 高亮移到末个可选候选;收起态不接管,光标照常跳到行尾 | | `Enter` | open, 有高亮且未禁用 | 选中高亮候选:单选把输入串换成它的文本并收起,多选把它并入集合、清空输入串且不收起 | | `Enter` | open, 有高亮且未禁用、未只读、未加载,按住 | 按住期间高亮候选投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,候选随浮层收起一并撤下。展开钮与清空钮在焦点落到自己身上时由 Enter / Space 按住投影,没有东西可清时清空钮不进 | | `Enter` | open, 无高亮且 allowCustomValue | 把输入串本身收成选中值 | | `Escape` | open | 先清除高亮;高亮已空时才收起列表,选中值不变 | | `Alt+ArrowUp` | open | 收起列表,选中值不变 | | `Tab` / `Shift+Tab` | open | 收起列表且不拦按键,焦点按 Tab 序列自然离开 | | `Backspace` | multiple, 输入串为空且已有选中 | 删掉最后一个已选项 | | `可打印字符` | focus in input | 改写输入串并展开列表;过滤由调用方按 onInputValueChange 自己做 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `input` | `aria-activedescendant` | `item` 部件的 id \| undefined | | `input` | `aria-autocomplete` | 'both' \| 'list' | | `input` | `aria-controls` | `content` 部件的 id | | `input` | `aria-expanded` | undefined \| 'true' \| 'false' | | `input` | `aria-haspopup` | 'listbox' | | `input` | `aria-invalid` | 'true' \| 'false' | | `input` | `aria-labelledby` | `label` 部件的 id | | `input` | `role` | undefined \| 'combobox' | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-label` | props.translations.trigger | | `clear-trigger` | `aria-label` | props.translations.clearTrigger | | `content` | `aria-busy` | 'true' \| undefined | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-labelledby` | `label` 部件的 id | | `content` | `aria-multiselectable` | 'true' \| 'false' | | `content` | `role` | 'listbox' | | `item` | `aria-disabled` | 'true' \| 'false' | | `item` | `aria-selected` | 'true' \| 'false' | | `item` | `role` | 'option' | | `item-prefix` | `aria-hidden` | 'true' | | `item-indicator` | `aria-hidden` | 'true' | | `group` | `aria-labelledby` | `group-label` 部件的 id | | `group` | `role` | 'group' | | `empty` | `role` | 'status' | | `loading` | `role` | 'status' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/combobox.css` 使用 `[data-scope="combobox"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-loading` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `label` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-state` | 'open' \| 'closed' | | `control` | `data-variant` | props.variant | | `control` | `data-xh-field-chrome` | '' | | `control` | `data-xh-field-size` | props.size | | `input` | `data-disabled` | ''(条件成立时才出现) | | `input` | `data-invalid` | ''(条件成立时才出现) | | `input` | `data-readonly` | ''(条件成立时才出现) | | `input` | `data-state` | 'open' \| 'closed' | | `input` | `data-xh-field-input` | '' | | `input` | `data-xh-field-layout` | 'textarea' \| 'single-line' | | `trigger` | `data-disabled` | ''(条件成立时才出现) | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'field-inset' | | `trigger` | `data-xh-action-size` | props.size | | `trigger` | `data-xh-action-variant` | 'ghost' | | `clear-trigger` | `data-pressed` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-control` | '' | | `clear-trigger` | `data-xh-action-display` | 'has-value' | | `clear-trigger` | `data-xh-action-has-value` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-profile` | 'field-inset' | | `clear-trigger` | `data-xh-action-size` | props.size | | `clear-trigger` | `data-xh-action-variant` | 'ghost' | | `positioner` | `data-hidden` | ''(条件成立时才出现) | | `positioner` | `data-placement` | 定位引擎算出的实际落位 | | `positioner` | `data-positioned` | ''(条件成立时才出现) | | `positioner` | `data-size` | props.size | | `positioner` | `data-state` | 'open' \| 'closed' | | `positioner` | `data-tone` | props.tone | | `positioner` | `data-variant` | props.variant | | `content` | `data-placement` | 定位引擎算出的实际落位 | | `content` | `data-state` | 'open' \| 'closed' | | `content` | `data-xh-material` | 'frosted' | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-highlighted` | ''(条件成立时才出现) | | `item` | `data-pressed` | ''(条件成立时才出现) | | `item` | `data-state` | 'checked' \| 'unchecked' | | `item` | `data-tone` | metaOf.get(item.value)?.tone | | `item` | `data-xh-collection-context` | 'overlay' | | `item` | `data-xh-collection-item` | '' | | `item` | `data-xh-collection-size` | props.size | | `item-prefix` | `data-disabled` | ''(条件成立时才出现) | | `item-prefix` | `data-highlighted` | ''(条件成立时才出现) | | `item-prefix` | `data-state` | 'checked' \| 'unchecked' | | `item-prefix` | `data-xh-collection-slot` | 'prefix' | | `item-text` | `data-disabled` | ''(条件成立时才出现) | | `item-text` | `data-highlighted` | ''(条件成立时才出现) | | `item-text` | `data-state` | 'checked' \| 'unchecked' | | `item-text` | `data-xh-collection-slot` | 'text' | | `item-description` | `data-disabled` | ''(条件成立时才出现) | | `item-description` | `data-highlighted` | ''(条件成立时才出现) | | `item-description` | `data-state` | 'checked' \| 'unchecked' | | `item-description` | `data-xh-collection-slot` | 'description' | | `item-suffix` | `data-disabled` | ''(条件成立时才出现) | | `item-suffix` | `data-highlighted` | ''(条件成立时才出现) | | `item-suffix` | `data-state` | 'checked' \| 'unchecked' | | `item-suffix` | `data-xh-collection-slot` | 'suffix' | | `item-indicator` | `data-disabled` | ''(条件成立时才出现) | | `item-indicator` | `data-highlighted` | ''(条件成立时才出现) | | `item-indicator` | `data-state` | 'checked' \| 'unchecked' | | `item-indicator` | `data-xh-collection-slot` | 'indicator' | | `empty` | `data-state` | 'open' \| 'closed' | | `loading` | `data-state` | 'open' \| 'closed' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-combobox-action-bg` | `clear-trigger`
`trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | combobox 的 clear-trigger、trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-combobox-action-bg-active` | `clear-trigger`
`trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | combobox 的 clear-trigger、trigger 部件 background-color 覆盖槽。 | | `--xh-combobox-action-bg-hover` | `clear-trigger`
`trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | combobox 的 clear-trigger、trigger 部件 background-color 覆盖槽。 | | `--xh-combobox-action-fg` | `clear-trigger`
`trigger` | `color` | `default` | `--xh-fg-muted` | combobox 的 clear-trigger、trigger 部件 color 覆盖槽。 | | `--xh-combobox-action-fg-hover` | `clear-trigger`
`trigger` | `color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-fg-default` | combobox 的 clear-trigger、trigger 部件 color 覆盖槽。 | | `--xh-combobox-action-font-size` | `clear-trigger`
`trigger` | `font-size` | `default` | `--xh-text-secondary-size` | combobox 的 clear-trigger、trigger 部件 font-size 覆盖槽。 | | `--xh-combobox-action-radius` | `clear-trigger`
`trigger` | `border-radius` | `default` | `--xh-shape-inset` | combobox 的 clear-trigger、trigger 部件 border-radius 覆盖槽。 | | `--xh-combobox-action-size` | `clear-trigger`
`trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | combobox 的 clear-trigger、trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-combobox-content-backdrop` | `content` | `-webkit-backdrop-filter`
`backdrop-filter` | `xh-material=frosted` | `--xh-_material-backdrop` | combobox 的 content 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 | | `--xh-combobox-content-bg` | `content` | `background` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-bg` | combobox 的 content 部件 background 覆盖槽。 | | `--xh-combobox-content-border` | `content` | `border` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-border` | combobox 的 content 部件 border 覆盖槽。 | | `--xh-combobox-content-fg` | `content` | `color` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-fg` | combobox 的 content 部件 color 覆盖槽。 | | `--xh-combobox-content-gap` | `content` | `gap` | `default` | `--xh-list-option-gap` | combobox 的 content 部件 gap 覆盖槽。 | | `--xh-combobox-content-highlight` | `content` | `background` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-highlight` | combobox 的 content 部件 background 覆盖槽。 | | `--xh-combobox-content-max-h` | `content` | `max-block-size` | `default` | `--xh-overlay-max-h` | combobox 的 content 部件 max-block-size 覆盖槽。 | | `--xh-combobox-content-max-w` | `content` | `max-inline-size` | `default` | `--xh-overlay-max-w` | combobox 的 content 部件 max-inline-size 覆盖槽。 | | `--xh-combobox-content-min-h` | `content` | `min-block-size` | `default` | `--xh-_combobox-h` | combobox 的 content 部件 min-block-size 覆盖槽。 | | `--xh-combobox-content-min-w` | `content` | `min-inline-size` | `default` | `--xh-overlay-min-w` | combobox 的 content 部件 min-inline-size 覆盖槽。 | | `--xh-combobox-content-px` | `content` | `padding-inline` | `default` | `--xh-space-1` | combobox 的 content 部件 padding-inline 覆盖槽。 | | `--xh-combobox-content-py` | `content` | `padding-block` | `default` | `--xh-space-1` | combobox 的 content 部件 padding-block 覆盖槽。 | | `--xh-combobox-content-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | combobox 的 content 部件 border-radius 覆盖槽。 | | `--xh-combobox-content-shadow` | `content` | `box-shadow` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-shadow` | combobox 的 content 部件 box-shadow 覆盖槽。 | | `--xh-combobox-control-bg` | `control` | `background-color` | `xh-field-chrome` | `--xh-_field-variant-bg-rest` | combobox 的 control 部件 background-color 覆盖槽。 | | `--xh-combobox-control-bg-disabled` | `control` | `background-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-bg-disabled` | combobox 的 control 部件 background-color 覆盖槽。 | | `--xh-combobox-control-bg-hover` | `control` | `background-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-hover` | combobox 的 control 部件 background-color 覆盖槽。 | | `--xh-combobox-control-bg-readonly` | `control` | `background-color` | `readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-read-only` | combobox 的 control 部件 background-color 覆盖槽。 | | `--xh-combobox-control-border` | `control` | `border` | `xh-field-chrome` | `--xh-_field-variant-border-rest` | combobox 的 control 部件 border 覆盖槽。 | | `--xh-combobox-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])`
`xh-field-chrome` | `--xh-_field-variant-border-focus` | combobox 的 control 部件 border-color 覆盖槽。 | | `--xh-combobox-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-border-hover` | combobox 的 control 部件 border-color 覆盖槽。 | | `--xh-combobox-control-border-invalid` | `control` | `border-color` | `invalid`
`xh-field-chrome` | `--xh-_field-variant-border-invalid` | combobox 的 control 部件 border-color 覆盖槽。 | | `--xh-combobox-control-fg` | `control`
`input` | `color` | `xh-field-chrome`
`xh-field-input` | `--xh-fg-default` | combobox 的 control、input 部件 color 覆盖槽。 | | `--xh-combobox-control-gap` | `control` | `gap` | `xh-field-chrome` | `--xh-_combobox-gap` | combobox 的 control 部件 gap 覆盖槽。 | | `--xh-combobox-control-h` | `control` | `block-size`
`min-block-size` | `has([data-xh-field-input][data-xh-field-layout='multi-tag'])`
`has([data-xh-field-input][data-xh-field-layout='single-line'])`
`has([data-xh-field-input][data-xh-field-layout='textarea'])`
`xh-field-chrome`
`xh-field-input`
`xh-field-layout=multi-tag`
`xh-field-layout=single-line`
`xh-field-layout=textarea` | `--xh-_combobox-h` | combobox 的 control 部件 block-size、min-block-size 覆盖槽。 | | `--xh-combobox-control-min-w` | `control`
`root` | `min-inline-size` | `default`
`xh-field-chrome` | `--xh-control-min-w` | combobox 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-combobox-control-px` | `control` | `padding-inline` | `xh-field-chrome` | `--xh-_combobox-px` | combobox 的 control 部件 padding-inline 覆盖槽。 | | `--xh-combobox-control-radius` | `control` | `border-radius` | `xh-field-chrome` | `--xh-shape-control` | combobox 的 control 部件 border-radius 覆盖槽。 | | `--xh-combobox-control-shadow` | `control` | `box-shadow` | `xh-field-chrome` | `none` | combobox 的 control 部件 box-shadow 覆盖槽。 | | `--xh-combobox-control-w` | `root` | `inline-size`
`min-inline-size` | `default` | `--xh-control-w` | combobox 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-combobox-empty-fg` | `empty` | `color` | `default` | `--xh-material-frosted-fg-muted` | combobox 的 empty 部件 color 覆盖槽。 | | `--xh-combobox-empty-font-size` | `empty` | `font-size` | `default` | `--xh-_combobox-font-size` | combobox 的 empty 部件 font-size 覆盖槽。 | | `--xh-combobox-empty-px` | `empty` | `padding-inline` | `default` | `--xh-control-px-md` | combobox 的 empty 部件 padding-inline 覆盖槽。 | | `--xh-combobox-empty-py` | `empty` | `padding-block` | `default` | `--xh-space-3` | combobox 的 empty 部件 padding-block 覆盖槽。 | | `--xh-combobox-gap` | `root` | `gap` | `default` | `--xh-space-1` | combobox 的 root 部件 gap 覆盖槽。 | | `--xh-combobox-group-gap` | `group` | `gap` | `default` | `--xh-list-option-gap` | combobox 的 group 部件 gap 覆盖槽。 | | `--xh-combobox-group-label-fg` | `group-label` | `color` | `default` | `--xh-material-frosted-fg-muted` | combobox 的 group-label 部件 color 覆盖槽。 | | `--xh-combobox-group-label-font-size` | `group-label` | `font-size` | `default` | `--xh-text-caption-size` | combobox 的 group-label 部件 font-size 覆盖槽。 | | `--xh-combobox-group-label-font-weight` | `group-label` | `font-weight` | `default` | `--xh-font-weight-medium` | combobox 的 group-label 部件 font-weight 覆盖槽。 | | `--xh-combobox-group-label-px` | `group-label` | `padding-inline` | `default` | `--xh-control-px-md` | combobox 的 group-label 部件 padding-inline 覆盖槽。 | | `--xh-combobox-group-label-py` | `group-label` | `padding-block` | `default` | `--xh-space-1` | combobox 的 group-label 部件 padding-block 覆盖槽。 | | `--xh-combobox-group-spacing` | `group` | `margin-block-start` | `default` | `--xh-space-1_5` | combobox 的 group 部件 margin-block-start 覆盖槽。 | | `--xh-combobox-icon-size` | `control`
`positioner`
`root` | `--xh-icon-size` | `is([data-part='root'], [data-part='positioner'])`
`size=lg`
`size=sm`
`xh-field-chrome` | `--xh-_field-size-glyph-size`
`--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | combobox 的 control、positioner、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-combobox-input-autofill-bg` | `input` | `box-shadow` | `-webkit-autofill`
`autofill`
`xh-field-input` | `--xh-bg-canvas` | combobox 的 input 部件 box-shadow 覆盖槽。 | | `--xh-combobox-input-autofill-fg` | `input` | `-webkit-text-fill-color` | `-webkit-autofill`
`autofill`
`xh-field-input` | `--xh-fg-default` | combobox 的 input 部件 -webkit-text-fill-color 覆盖槽。 | | `--xh-combobox-input-fg` | `input` | `color` | `xh-field-input` | `--xh-combobox-control-fg` | combobox 的 input 部件 color 覆盖槽。 | | `--xh-combobox-input-font-size` | `input` | `font-size` | `xh-field-input` | `--xh-_combobox-font-size` | combobox 的 input 部件 font-size 覆盖槽。 | | `--xh-combobox-item-bg-hover` | `item` | `background-color` | `disabled`
`error`
`highlighted`
`hover`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle` | combobox 的 item 部件 background-color 覆盖槽。 | | `--xh-combobox-item-bg-pressed` | `item` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle-hover` | combobox 的 item 部件 background-color 覆盖槽。 | | `--xh-combobox-item-check-fg` | `item` | `color` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`state=checked`
`xh-collection-context=overlay`
`xh-collection-slot=indicator` | `--xh-combobox-item-indicator-fg` | combobox 的 item 部件 color 覆盖槽。 | | `--xh-combobox-item-fg` | `item` | `color` | `default`
`disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-material-frosted-fg` | combobox 的 item 部件 color 覆盖槽。 | | `--xh-combobox-item-fg-selected` | `item` | `color` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-combobox-item-fg` | combobox 的 item 部件 color 覆盖槽。 | | `--xh-combobox-item-font-size` | `item` | `font-size` | `default` | `--xh-_combobox-font-size` | combobox 的 item 部件 font-size 覆盖槽。 | | `--xh-combobox-item-font-weight-selected` | `item` | `font-weight` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-font-weight-regular` | combobox 的 item 部件 font-weight 覆盖槽。 | | `--xh-combobox-item-gap` | `item` | `margin-inline-end`
`margin-inline-start` | `xh-collection-slot=indicator`
`xh-collection-slot=prefix`
`xh-collection-slot=shortcut`
`xh-collection-slot=suffix` | `--xh-_combobox-gap` | combobox 的 item 部件 margin-inline-end、margin-inline-start 覆盖槽。 | | `--xh-combobox-item-indicator-fg` | `item` | `color` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`state=checked`
`xh-collection-context=overlay`
`xh-collection-slot=indicator` | `--xh-_combobox-accent` | combobox 的 item 部件 color 覆盖槽。 | | `--xh-combobox-item-indicator-size` | `item-indicator` | `--xh-icon-size`
`block-size`
`inline-size` | `default` | `--xh-control-indicator-size` | combobox 的 item-indicator 部件 --xh-icon-size、block-size、inline-size 覆盖槽。 | | `--xh-combobox-item-leading` | `item` | `line-height` | `default` | `--xh-leading-normal` | combobox 的 item 部件 line-height 覆盖槽。 | | `--xh-combobox-item-px` | `item` | `padding-inline` | `default` | `--xh-_combobox-item-px` | combobox 的 item 部件 padding-inline 覆盖槽。 | | `--xh-combobox-item-py` | `item` | `padding-block` | `default` | `--xh-_combobox-item-py` | combobox 的 item 部件 padding-block 覆盖槽。 | | `--xh-combobox-item-radius` | `item` | `border-radius` | `default` | `--xh-shape-control` | combobox 的 item 部件 border-radius 覆盖槽。 | | `--xh-combobox-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | combobox 的 label 部件 color 覆盖槽。 | | `--xh-combobox-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | combobox 的 label 部件 color 覆盖槽。 | | `--xh-combobox-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | combobox 的 label 部件 font-size 覆盖槽。 | | `--xh-combobox-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | combobox 的 label 部件 font-weight 覆盖槽。 | | `--xh-combobox-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | combobox 的 positioner 部件 z-index 覆盖槽。 | | `--xh-combobox-loading-fg` | `loading` | `color` | `default` | `--xh-material-frosted-fg-muted` | combobox 的 loading 部件 color 覆盖槽。 | | `--xh-combobox-loading-font-size` | `loading` | `font-size` | `default` | `--xh-_combobox-font-size` | combobox 的 loading 部件 font-size 覆盖槽。 | | `--xh-combobox-loading-px` | `loading` | `padding-inline` | `default` | `--xh-control-px-md` | combobox 的 loading 部件 padding-inline 覆盖槽。 | | `--xh-combobox-loading-py` | `loading` | `padding-block` | `default` | `--xh-space-3` | combobox 的 loading 部件 padding-block 覆盖槽。 | | `--xh-combobox-placeholder-fg` | `input` | `color` | `placeholder`
`xh-field-input` | `--xh-fg-subtle` | combobox 的 input 部件 color 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 切换 · 出现(锚定列表)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-overlay-slide-in` · `xh-overlay-slide-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`rotate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/command # Command 命令面板 覆盖在页面上的检索面板:输入筛选命令,方向键选择,回车执行。 ## 用法 提供一份命令清单,过滤、归组与空态都由组件处理 ```vue ``` ```html
页面
用户管理
角色管理
个人资料
动作
导出报表
邀请成员
归档项目
没有匹配的命令
正在取命令…
方向键选择 · Enter 执行 · Esc 关闭

还没执行过命令

``` ## 组件结构 加粗的是必需部件。 `data-scope="command"`:`trigger` · `backdrop` · `positioner` · **`content`** · **`input`** · **`list`** · `group` · `group-label` · `item` · `item-prefix` · `item-text` · `item-description` · `item-shortcut` · `item-suffix` · `empty` · `loading` · `footer` ## 示例 ### 快捷键唤起与手写部件 Mod+K 打开,命中的文字由文本高亮标出,行尾挂载各命令自己的快捷键 ```vue ``` ```html
按一下唤起命令面板
文件
视图
没有匹配的命令
方向键选择 · Enter 执行 · Esc 关闭
``` ### 遮罩形态 variant 只落在 backdrop 层:opaque 压一层底色、blur 模糊背后、transparent 只阻挡点击 ```vue ``` ```html
用户管理
角色管理
导出报表
没有匹配的命令
用户管理
角色管理
导出报表
没有匹配的命令
用户管理
角色管理
导出报表
没有匹配的命令
``` ### 远程检索 filter 关闭:传入的 collection 就是当前应显示的条目,筛选归服务端;取数期间 loading 显示在途占位、列表压暗一档,空态让位 ```vue ``` ```html
名册里没有这个人
正在从服务端取…
不输字时给的是最近协作过的三位
还没选过人
``` ## 设计指引 ### 何时使用 - 功能分散在多层菜单中,用户知道要做什么但找不到入口。 - 需要一条跨页面的统一入口:搜索页面、设置、数据,执行动作。 - 熟练用户需要全程键盘操作:唤起、输入、回车。 ### 何时不用 - 只是从一份清单中选一个值填入表单时,使用[组合框](./combobox)或[选择器](./select)。 - 只是右键菜单或按钮菜单时,使用[右键菜单](./context-menu)或[菜单](./menu)。 - 面板内需要放表单或分步骤时,使用[对话框](./dialog)。 ### 特性 - 内置过滤:传入清单后按检索串逐词筛选、按 `group` 归组,空组自动移除。`keywords` 让一条命令同时匹配英文名、拼音与旧称。 - 过滤可以关闭(`filter` 置否),改由调用方筛选;远端检索使用这一档。 - 命令可逐条声明语气,删除一类命令自带该族字色与高亮底。 - 命令可写副文本,第 2 行放一句解释,不进检索串。 - 命令可写快捷键提示,贴行尾、与说明同档同色;纯装饰,不进检索串。 - 行首与行尾两格各有逐条钩子:只想加个图标或计数,不必把整条重搭。 - 面板默认是模态浮层:捕获焦点、锁定滚动、背景失活,Escape 与点击遮罩收起,收起后焦点归还触发按钮。`modal=false` 时不渲染遮罩、不拦截页面指针,也不启用这些模态约束;展开期间切换会立即同步。 - 焦点全程在检索框,活动候选经 `aria-activedescendant` 报告给读屏;活动候选同步 `aria-selected=true`,其余候选显式为 `false`,输入后活动候选自动回到首条。 - 这里的 `aria-selected` 遵循 [WAI-ARIA 组合框规范](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/)中“选中随焦点移动”的模式,只描述当前活动建议;命令执行后不保留持久选中状态,视觉上也不绘制对号或选中底。 - 两种非条目相位各有部件:空(`empty`)与在途(`loading`)。取数期间显示在途占位,空态让位,两者不同时出现。 - 零可见命令时列表不保留额外空白行;搜索输入和作者提供的状态、底栏仍然在场。未提供 Empty / Loading 文案时不显示空白占位,也不自动生成提示文字。 - `collection` 与过滤结果保持数据语义;已挂载节点上的 `hidden` 会排除对应条目或分组的交互与 ARIA 高亮。未挂载或虚拟候选不按隐藏推断;展开期间替换列表节点后,可见性观察会切换到新节点。 - `closeOnSelect` 决定选中后是否收起;连续执行多条命令时关闭它。 ### 组合 - 唤起可用[键盘按键](./kbd):显示 `Mod + K` 并开启 `register`,在回调中执行 `setOpen(true)`。 - 条目文字中标出命中的字符使用[文本高亮](./highlight),检索串即关键词。 - 行尾的快捷键提示和注册行为共用同一个 Kbd,避免展示与实际绑定不一致。 ### 最佳实践 - 命令名写成“动词 + 宾语”(新建用户、导出报表),用户按动作查找。 - 分组按用户的心智模型划分(页面 / 设置 / 动作),不按代码模块划分。 - 底部提示条说明三件事:上下键选择、回车执行、Escape 关闭。 - 命令来自远端时提供在途占位,不让面板停留在空白状态。 ### 反模式 - 把后台的每个按钮都放进来,面板变成第二份菜单树,检索反而更慢。 - 只匹配命令的中文全名:用户输入 `export` 时搜不到,别名应写进 `keywords`。 - 选中后没有任何反馈:命令应当场生效、导航过去,或给出一条轻提示。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhCommandContent` `XhCommandEmpty` `XhCommandFooter` `XhCommandGroup` `XhCommandGroupLabel` `XhCommandInput` `XhCommandItem` `XhCommandItemDescription` `XhCommandItemPrefix` `XhCommandItemShortcut` `XhCommandItemSuffix` `XhCommandItemText` `XhCommandList` `XhCommandLoading` `XhCommandRoot` `XhCommandTrigger` | | 组合式函数 | `useCommand` | | 状态机 | `commandMachine` | | 皮肤 | `@xihan-ui/styles/command.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `collection` | `readonly CommandNode[]` | | 命令清单,标题、别名、归组与禁用的事实源。 | | `groups` | `readonly CommandGroup[]` | | 分组声明,决定组名与组序;清单中出现而这里未声明的组排在后面。 | | `open` | `boolean` | | | | `defaultOpen` | `boolean` | | | | `inputValue` | `string` | | | | `defaultInputValue` | `string` | | | | `filter` | `boolean` | | 内置过滤,默认开启。关闭后由调用方自行筛选,传入的 collection 即当前应显示的命令。 | | `caseSensitive` | `boolean` | | 过滤区分大小写,默认不区分。 | | `closeOnSelect` | `boolean` | | 选中一条命令后收起面板,默认 true。 | | `modal` | `boolean` | | 模态(陷焦点、锁滚动、遮罩交互外关闭),默认 true。 | | `closeOnEscape` | `boolean` | | | | `closeOnInteractOutside` | `boolean` | | | | `restoreFocus` | `boolean` | | | | `loop` | `boolean` | | 方向键到达末尾是否回绕,默认 true。 | | `loading` | `boolean` | | 命令加载中:列表报告 aria-busy,显示在途占位,隐藏空态占位。 | | `placeholder` | `string` | | 检索框的占位文字。 | | `dir` | `Direction` | | 文字方向,默认 ltr。 | | `size` | `Size` | | 尺寸:sm / md / lg。只影响面板宽度与条目的几何档位。 | | `variant` | `OverlayBackdropVariant` | | 遮罩形态:opaque / blur / transparent。写在 backdrop 上,只影响该层的底色与模糊。 | | `translations` | `Partial` | | | | `onOpenChange` | `(details: CommandOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `onInputValueChange` | `(details: CommandInputValueChangeDetails) => void` | | 检索串变化意图回调。 | | `onSelect` | `(details: CommandSelectDetails) => void` | | 选中一条命令:库不执行任何动作,后续行为全部由这里决定。 | ### CommandNode `collection` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | 展示文本,也是检索取字来源;默认回退为 value。 | | `keywords` | `readonly string[]` | | 标题之外一并参与检索的别名,例如英文名、拼音、旧称。 | | `group` | `string` | | 所属分组;未声明时不归组,渲染时不套分组外壳。 | | `disabled` | `boolean` | | 条目禁用:方向键跳过它,点击与确认键都不选中。 | | `tone` | `Tone` | | 该条命令自身动作的性质:删除写 danger、停用写 warning。不写即与其余命令同档。 只换字色与悬停 / 按下的面,不改字重与缩进,也不表达选中或校验;禁用压过它。 红字不是唯一通道,破坏性命令仍要配图标。 | | `description` | `string` | | 副文本,写入 item-description 部件;未提供时本条不铺该部件。 它是第 2 行的说明,跟着条目走 muted 档,不跟语气;放不下一行的解释才用它, 一句话能说清的写进 label。 | | `shortcut` | `string` | | 快捷键提示,写入 item-shortcut 部件;未提供时本条不铺该部件。 纯装饰:读屏从命令文字取意,不念它;只为真正注册了的组合写提示。 | ### CommandGroup `groups` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | 分组标题;默认回退为 value。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `open-change` | `CommandOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean, reason?: string }` | | `input-value-change` | `CommandInputValueChangeDetails` | 检索串变化;detail 为 `{ inputValue: string }` | | `select` | `CommandSelectDetails` | 选中一条命令;detail 为 `{ value: string, label: string }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhCommandRoot` | `default` | `CommandRootSlotProps` | | | `XhCommandRoot` | `trigger` | — | 铺开时的触发按钮内容;未提供时不渲染触发器(面板改由快捷键或 v-model:open 唤起)。 | | `XhCommandRoot` | `item` | `CommandNodeMeta` | 只填条目的文字槽,副文本与首尾两格照旧各归各的 | | `XhCommandRoot` | `item-prefix` | `CommandNodeMeta` | 只接管行首那一格,其余槽照旧由数据铺 | | `XhCommandRoot` | `item-suffix` | `CommandNodeMeta` | 只接管行尾那一格(计数、徽标、次级图标),其余槽照旧由数据铺 | | `XhCommandRoot` | `empty` | — | | | `XhCommandRoot` | `footer` | — | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhCommandContent` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhCommandGroup` | `value` | `string` | 是 | | | `XhCommandItem` | `value` | `string` | 是 | | | `XhCommandItem` | `disabled` | `boolean` | | 默认交给 connect 查询清单,写死 false 会覆盖数据中的禁用。 | | `XhCommandRoot` | `empty` | `ReactNode` | | 无匹配时的提示语。提供后不必再写 empty 部件。 | | `XhCommandRoot` | `trigger` | `ReactNode` | | 铺开时的触发按钮内容;未提供时不渲染触发器(面板改由快捷键或受控 open 唤起)。 | | `XhCommandRoot` | `footer` | `ReactNode` | | 铺开时浮层底部的操作区内容;未提供时不渲染 footer 部件。 | | `XhCommandRoot` | `renderItem` | `(node: CommandNodeMeta) => ReactNode` | | 每条命令的自定义内容;未提供时使用清单中的 label。 | | `XhCommandRoot` | `renderItemPrefix` | `(node: CommandNodeMeta) => ReactNode` | | 只接管条目行首那一格;其余槽仍由数据铺。 | | `XhCommandRoot` | `renderItemSuffix` | `(node: CommandNodeMeta) => ReactNode` | | 只接管条目行尾那一格;其余槽仍由数据铺。 | | `XhCommandRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `trigger` | 'open' \| 'closed' | | `backdrop` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `input` | 'open' \| 'closed' | | `list` | 'open' \| 'closed' | | `empty` | 'open' \| 'closed' | | `loading` | 'open' \| 'closed' | | `footer` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `INPUT.CHANGE` · `INPUT.SET` · `ITEM.HIGHLIGHT` · `HIGHLIGHT.CLEAR` · `ITEM.SELECT` · `PRESS.START` · `PRESS.END` · `ARRIVALS.TRACKED` **判据**:`isOpenControlled` · `keepsOpenOnSelect` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `inputValue` | `string` | 当前检索串。 | | `groups` | `readonly CommandGroupMeta[]` | 过滤归组之后当前应显示的命令,空组已移除。 | | `results` | `readonly CommandNodeMeta[]` | 上述分组视图展平的结果,次序即方向键的移动次序。 | | `highlightedValue` | `string \| null` | 键盘锚点;收起时为 null。 | | `empty` | `boolean` | 没有剩余条目。 | | `loading` | `boolean` | | | `setOpen` | `(next: boolean) => void` | | | `setInputValue` | `(next: string) => void` | | | `select` | `(value: string) => void` | 直接选中某条命令,等同于在它上面按回车。 | | `getTriggerProps` | `() => T['button']` | | | `getBackdropProps` | `() => T['element']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getInputProps` | `() => T['input']` | | | `getListProps` | `() => T['element']` | | | `getGroupProps` | `(props: CommandGroupProps) => T['element']` | | | `getGroupLabelProps` | `(props: CommandGroupProps) => T['element']` | | | `getItemProps` | `(props: CommandItemProps) => T['element']` | | | `getItemPrefixProps` | `(props: CommandItemProps) => T['element']` | | | `getItemTextProps` | `(props: CommandItemProps) => T['element']` | | | `getItemDescriptionProps` | `(props: CommandItemProps) => T['element']` | | | `getItemShortcutProps` | `(props: CommandItemProps) => T['element']` | | | `getItemSuffixProps` | `(props: CommandItemProps) => T['element']` | | | `getEmptyProps` | `() => T['element']` | 空态占位:放在 content 中、list 的兄弟。 提供 collection 时由连接层按条数收放;条目手写时不写 hidden,是否显示由作者决定。 | | `getLoadingProps` | `() => T['element']` | 在途占位:与空态占位同一位置,两者不同时显示。 | | `getFooterProps` | `() => T['element']` | 面板底部的提示条:内容由作者决定,这里只提供位置与观感。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in trigger | 打开面板并把焦点移入检索框 | | `Escape` | open | 关闭面板并把焦点还给 trigger | | `ArrowDown` | open | 锚点移到下一条命令,禁用的跳过 | | `ArrowUp` | open | 锚点移到上一条命令,禁用的跳过 | | `Home` | open | 锚点移到首条命令 | | `End` | open | 锚点移到末条命令 | | `Enter` | open, 锚点落在可用命令上 | 选中该命令;长按连发的重复键不重复选中 | | `Enter` | open, 锚点落在可用命令上且未加载,按住 | 按住期间锚点命令投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,命令随面板收起一并撤下 | | `Tab` | open, modal | 在面板内循环焦点 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `trigger` | `aria-haspopup` | 'dialog' | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-label` | translations?.title | | `content` | `aria-modal` | 'true' \| 'false' | | `content` | `role` | 'dialog' | | `input` | `aria-activedescendant` | `item` 部件的 id \| undefined | | `input` | `aria-autocomplete` | 'list' | | `input` | `aria-controls` | `list` 部件的 id | | `input` | `aria-expanded` | 'true' | | `input` | `aria-haspopup` | 'listbox' | | `input` | `aria-label` | translations?.input | | `input` | `role` | 'combobox' | | `list` | `aria-busy` | 'true' \| undefined | | `list` | `aria-label` | translations?.list | | `list` | `role` | 'listbox' | | `group` | `aria-labelledby` | `group-label` 部件的 id | | `group` | `role` | 'group' | | `item` | `aria-disabled` | 'true' \| 'false' | | `item` | `aria-selected` | 'true' \| 'false' | | `item` | `role` | 'option' | | `item-prefix` | `aria-hidden` | 'true' | | `item-shortcut` | `aria-hidden` | 'true' | | `empty` | `role` | 'status' | | `loading` | `role` | 'status' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/command.css` 使用 `[data-scope="command"][data-part="trigger"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `data-state` | 'open' \| 'closed' | | `backdrop` | `data-state` | 'open' \| 'closed' | | `backdrop` | `data-variant` | props.variant | | `positioner` | `data-positioned` | '' | | `positioner` | `data-size` | props.size | | `positioner` | `data-state` | 'open' \| 'closed' | | `content` | `data-size` | props.size | | `content` | `data-state` | 'open' \| 'closed' | | `input` | `data-state` | 'open' \| 'closed' | | `list` | `data-instant` | ''(条件成立时才出现) | | `list` | `data-state` | 'open' \| 'closed' | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-highlighted` | ''(条件成立时才出现) | | `item` | `data-pressed` | ''(条件成立时才出现) | | `item` | `data-tone` | metaOf.get(item.value)?.tone | | `item` | `data-xh-collection-context` | 'overlay' | | `item` | `data-xh-collection-item` | '' | | `item` | `data-xh-collection-size` | props.size | | `item-prefix` | `data-disabled` | ''(条件成立时才出现) | | `item-prefix` | `data-highlighted` | ''(条件成立时才出现) | | `item-prefix` | `data-xh-collection-slot` | 'prefix' | | `item-text` | `data-disabled` | ''(条件成立时才出现) | | `item-text` | `data-highlighted` | ''(条件成立时才出现) | | `item-text` | `data-xh-collection-slot` | 'text' | | `item-description` | `data-disabled` | ''(条件成立时才出现) | | `item-description` | `data-highlighted` | ''(条件成立时才出现) | | `item-description` | `data-xh-collection-slot` | 'description' | | `item-shortcut` | `data-disabled` | ''(条件成立时才出现) | | `item-shortcut` | `data-highlighted` | ''(条件成立时才出现) | | `item-shortcut` | `data-xh-collection-slot` | 'shortcut' | | `item-suffix` | `data-disabled` | ''(条件成立时才出现) | | `item-suffix` | `data-highlighted` | ''(条件成立时才出现) | | `item-suffix` | `data-xh-collection-slot` | 'suffix' | | `empty` | `data-state` | 'open' \| 'closed' | | `loading` | `data-state` | 'open' \| 'closed' | | `footer` | `data-state` | 'open' \| 'closed' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-command-backdrop-bg` | `backdrop` | `background` | `default` | `--xh-bg-overlay` | command 的 backdrop 部件 background 覆盖槽。 | | `--xh-command-backdrop-blur` | `backdrop` | `backdrop-filter` | `variant=blur` | `--xh-overlay-backdrop-blur` | command 的 backdrop 部件 backdrop-filter 覆盖槽。 | | `--xh-command-backdrop-layer` | `backdrop` | `z-index` | `default` | `--xh-_layer` | command 的 backdrop 部件 z-index 覆盖槽。 | | `--xh-command-bg` | `content` | `background` | `default` | `--xh-material-elevated-bg` | command 的 content 部件 background 覆盖槽。 | | `--xh-command-border` | `content` | `border` | `default` | `--xh-material-elevated-border` | command 的 content 部件 border 覆盖槽。 | | `--xh-command-empty-fg` | `empty` | `color` | `default` | `--xh-fg-subtle` | command 的 empty 部件 color 覆盖槽。 | | `--xh-command-empty-font-size` | `empty` | `font-size` | `default` | `--xh-_command-font-size` | command 的 empty 部件 font-size 覆盖槽。 | | `--xh-command-empty-px` | `empty` | `padding-inline` | `default` | `--xh-_command-px` | command 的 empty 部件 padding-inline 覆盖槽。 | | `--xh-command-empty-py` | `empty` | `padding-block` | `default` | `--xh-space-6` | command 的 empty 部件 padding-block 覆盖槽。 | | `--xh-command-fg` | `content` | `color` | `default` | `--xh-material-elevated-fg` | command 的 content 部件 color 覆盖槽。 | | `--xh-command-footer-border` | `footer` | `border-block-start` | `default` | `--xh-border-subtle` | command 的 footer 部件 border-block-start 覆盖槽。 | | `--xh-command-footer-fg` | `footer` | `color` | `default` | `--xh-fg-muted` | command 的 footer 部件 color 覆盖槽。 | | `--xh-command-footer-font-size` | `footer` | `font-size` | `default` | `--xh-text-caption-size` | command 的 footer 部件 font-size 覆盖槽。 | | `--xh-command-footer-gap` | `footer` | `gap` | `default` | `--xh-space-2` | command 的 footer 部件 gap 覆盖槽。 | | `--xh-command-footer-px` | `footer` | `padding-inline` | `default` | `--xh-_command-px` | command 的 footer 部件 padding-inline 覆盖槽。 | | `--xh-command-footer-py` | `footer` | `padding-block` | `default` | `--xh-space-2` | command 的 footer 部件 padding-block 覆盖槽。 | | `--xh-command-group-gap` | `group` | `gap` | `default` | `--xh-list-option-gap` | command 的 group 部件 gap 覆盖槽。 | | `--xh-command-group-label-fg` | `group-label` | `color` | `default` | `--xh-fg-subtle` | command 的 group-label 部件 color 覆盖槽。 | | `--xh-command-group-label-font-size` | `group-label` | `font-size` | `default` | `--xh-text-caption-size` | command 的 group-label 部件 font-size 覆盖槽。 | | `--xh-command-group-label-font-weight` | `group-label` | `font-weight` | `default` | `--xh-font-weight-medium` | command 的 group-label 部件 font-weight 覆盖槽。 | | `--xh-command-group-label-px` | `group-label` | `padding-inline` | `default` | `--xh-_command-px` | command 的 group-label 部件 padding-inline 覆盖槽。 | | `--xh-command-group-label-py` | `group-label` | `padding-block` | `default` | `--xh-space-1` | command 的 group-label 部件 padding-block 覆盖槽。 | | `--xh-command-group-spacing` | `group` | `margin-block-start` | `default` | `--xh-space-1_5` | command 的 group 部件 margin-block-start 覆盖槽。 | | `--xh-command-icon-size` | `content`
`positioner` | `--xh-icon-size` | `is([data-part='positioner'], [data-part='content'])`
`size=lg`
`size=sm` | `--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | command 的 content、positioner 部件 --xh-icon-size 覆盖槽。 | | `--xh-command-input-autofill-bg` | `input` | `box-shadow` | `-webkit-autofill`
`autofill` | `--xh-bg-surface` | command 的 input 部件 box-shadow 覆盖槽。 | | `--xh-command-input-autofill-fg` | `input` | `-webkit-text-fill-color` | `-webkit-autofill`
`autofill` | `--xh-fg-default` | command 的 input 部件 -webkit-text-fill-color 覆盖槽。 | | `--xh-command-input-border` | `input` | `border-block-end` | `default` | `--xh-border-subtle` | command 的 input 部件 border-block-end 覆盖槽。 | | `--xh-command-input-fg` | `input` | `color` | `default` | `--xh-fg-default` | command 的 input 部件 color 覆盖槽。 | | `--xh-command-input-font-size` | `input` | `font-size` | `default` | `--xh-text-body-size` | command 的 input 部件 font-size 覆盖槽。 | | `--xh-command-input-h` | `input` | `block-size` | `default` | `--xh-_command-input-h` | command 的 input 部件 block-size 覆盖槽。 | | `--xh-command-input-px` | `input` | `padding-inline` | `default` | `--xh-_command-px` | command 的 input 部件 padding-inline 覆盖槽。 | | `--xh-command-inset-block-start` | `positioner` | `padding-block-start` | `default` | `--xh-space-8` | command 的 positioner 部件 padding-block-start 覆盖槽。 | | `--xh-command-item-bg-hover` | `item` | `background-color` | `disabled`
`error`
`highlighted`
`hover`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle` | command 的 item 部件 background-color 覆盖槽。 | | `--xh-command-item-bg-pressed` | `item` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle-hover` | command 的 item 部件 background-color 覆盖槽。 | | `--xh-command-item-fg` | `item` | `color` | `default`
`disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-fg-default` | command 的 item 部件 color 覆盖槽。 | | `--xh-command-item-font-size` | `item` | `font-size` | `default` | `--xh-_command-font-size` | command 的 item 部件 font-size 覆盖槽。 | | `--xh-command-item-gap` | `item` | `gap` | `default` | `--xh-_command-gap` | command 的 item 部件 gap 覆盖槽。 | | `--xh-command-item-leading` | `item` | `line-height` | `default` | `--xh-leading-normal` | command 的 item 部件 line-height 覆盖槽。 | | `--xh-command-item-px` | `item` | `padding-inline` | `default` | `--xh-_command-px` | command 的 item 部件 padding-inline 覆盖槽。 | | `--xh-command-item-py` | `item` | `padding-block` | `default` | `--xh-_command-item-py` | command 的 item 部件 padding-block 覆盖槽。 | | `--xh-command-item-radius` | `item` | `border-radius` | `default` | `--xh-shape-control` | command 的 item 部件 border-radius 覆盖槽。 | | `--xh-command-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | command 的 positioner 部件 z-index 覆盖槽。 | | `--xh-command-list-busy-opacity` | `list` | `opacity` | `default` | `--xh-state-disabled-opacity` | command 的 list 部件 opacity 覆盖槽。 | | `--xh-command-list-gap` | `list` | `gap` | `default` | `--xh-list-option-gap` | command 的 list 部件 gap 覆盖槽。 | | `--xh-command-list-px` | `list` | `padding-inline` | `default` | `--xh-space-2` | command 的 list 部件 padding-inline 覆盖槽。 | | `--xh-command-list-py` | `list` | `padding-block` | `default` | `--xh-space-2` | command 的 list 部件 padding-block 覆盖槽。 | | `--xh-command-loading-fg` | `loading` | `color` | `default` | `--xh-fg-subtle` | command 的 loading 部件 color 覆盖槽。 | | `--xh-command-loading-font-size` | `loading` | `font-size` | `default` | `--xh-_command-font-size` | command 的 loading 部件 font-size 覆盖槽。 | | `--xh-command-loading-px` | `loading` | `padding-inline` | `default` | `--xh-_command-px` | command 的 loading 部件 padding-inline 覆盖槽。 | | `--xh-command-loading-py` | `loading` | `padding-block` | `default` | `--xh-space-6` | command 的 loading 部件 padding-block 覆盖槽。 | | `--xh-command-max-h` | `content` | `max-block-size` | `default` | `--xh-overlay-max-h` | command 的 content 部件 max-block-size 覆盖槽。 | | `--xh-command-max-w` | `content` | `max-inline-size` | `default` | `--xh-_command-max-w` | command 的 content 部件 max-inline-size 覆盖槽。 | | `--xh-command-placeholder-fg` | `input` | `color` | `placeholder` | `--xh-fg-subtle` | command 的 input 部件 color 覆盖槽。 | | `--xh-command-positioner-pb` | `positioner` | `padding-block-end` | `default` | `--xh-space-4` | command 的 positioner 部件 padding-block-end 覆盖槽。 | | `--xh-command-positioner-px` | `positioner` | `padding-inline` | `default` | `--xh-space-4` | command 的 positioner 部件 padding-inline 覆盖槽。 | | `--xh-command-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | command 的 content 部件 border-radius 覆盖槽。 | | `--xh-command-shadow` | `content` | `box-shadow` | `default` | `--xh-material-elevated-shadow` | command 的 content 部件 box-shadow 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(锚定面板)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-fade-in` · `xh-fade-out` · `xh-overlay-pop-in` · `xh-pop-out` · `xh-rise-in` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/context-menu # ContextMenu 右键菜单 通过右键或长按在指针位置打开命令菜单。 ## 用法 在目标区域右键打开命令菜单 ```vue ``` ```html
设计规范.pdf 右键打开菜单
打开
重命名
创建副本
移到回收站
``` ## 组件结构 加粗的是必需部件。 `data-scope="context-menu"`:`root` · **`trigger`** · `positioner` · **`content`** · **`item`** · `item-text` · `item-indicator` · `item-description` · `item-shortcut` · `item-suffix` · `separator` · `group` · `group-label` · `arrow` ## 示例 ### 分组 使用标题与分隔线组织命令 ```vue ``` ```html
右键设置文件视图
排序方式
按名称
按修改时间
视图
列表
网格
``` ### 图标与快捷键 在命令两侧补充识别信息 ```vue ``` ```html
右键编辑 notes.md
复制⌘ C
重命名F2
移到回收站⌫
``` ### 子菜单 将相关命令收进下一层 ```vue ``` ```html
右键管理项目
打开
重命名
发送到
邮件
消息
移到回收站
``` ## 设计指引 ### 何时使用 - 为文件、表格行或画布对象提供快捷操作。 ### 何时不用 - 主要操作应保留可见入口。 - 以触摸操作为主的界面不应只依赖长按。 - 选择值时使用[选择器](./select)。 ### 特性 - 菜单默认贴近指针位置。 - 支持分组、分隔线、标记位和子菜单。 - 条目可逐条声明语气,删除一类命令自带该族字色与高亮底。 - 说明与快捷键提示都可写进 `collection`;快捷键贴行尾,与说明同档同色。 - `typeahead` 控制首字符检索,`longPressDelay` 设置长按时间。 - 条目可组合图标、文字、说明和快捷键提示。 - 选中任意层级的命令后发出根级 `select` 并关闭菜单链。 ### 组合 - 使用 `XhContextMenuSub` 创建子菜单。 ### 最佳实践 - 右键菜单只作为快捷入口,不替代页面上的主要操作。 - 条目较多时按功能分组。 - 仅为有意义的命令添加图标或快捷键提示。 ### 反模式 - 不要在整页范围覆盖浏览器原生右键菜单。 - 不要移除复制、打开链接等原生能力而不提供等价入口。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhContextMenuArrow` `XhContextMenuContent` `XhContextMenuGroup` `XhContextMenuGroupLabel` `XhContextMenuItem` `XhContextMenuItemDescription` `XhContextMenuItemIndicator` `XhContextMenuItemShortcut` `XhContextMenuItemSuffix` `XhContextMenuItemText` `XhContextMenuPositioner` `XhContextMenuRoot` `XhContextMenuSeparator` `XhContextMenuSub` `XhContextMenuSubTrigger` `XhContextMenuTrigger` | | 组合式函数 | `useContextMenu` | | 状态机 | `contextMenuMachine` | | 皮肤 | `@xihan-ui/styles/context-menu.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `collection` | `ContextMenuNode[]` | | 条目数据,显示文本、禁用、标记位与分组的事实源。提供后条目部件只需声明 value。 未提供时回到文本与禁用全部写在条目部件上的方式。 | | `open` | `boolean` | | 展开态。提供即受控:内部不再自行修改,只发 onOpenChange。 | | `defaultOpen` | `boolean` | | | | `placement` | `Placement` | | 相对光标位置的首选放置位,默认 bottom-start。 | | `offset` | `number` | | 浮层与光标的间距(px),默认 0:右键菜单需要贴近光标。 | | `loop` | `boolean` | | 方向键到达末尾是否回绕,默认 true。 | | `dir` | `Direction` | | 文字方向,默认 ltr。 | | `typeahead` | `boolean` | | 连打检索,默认开启。关闭后可打印字符一律放行给页面。 | | `translations` | `Partial` | | 读屏文案,默认英文。 | | `longPressDelay` | `number` | | 触摸端长按多久视为触发(ms),默认 700。 | | `tone` | `Tone` | | 整张菜单的语气:brand / neutral / success / warning / danger / info。 只为浮层与作者放进来的内容备好该族颜色,不下发给条目——条目保持中性档, 逐条的语气写在 collection 的 `tone` 上(见 ContextMenuNode)。 | | `size` | `Size` | | 尺寸:sm / md / lg,决定条目高度、内边距与字号档位。 | | `onOpenChange` | `(details: ContextMenuOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `onSelect` | `(details: ContextMenuSelectDetails) => void` | | 条目被选中;菜单随之关闭。 | ### ContextMenuNode `collection` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | | 展示文本,也是连打检索的取字来源;默认回退为 value。 | | `disabled` | `boolean` | | 条目禁用:方向键跳过它,但它仍可聚焦、仍是导航起点。 | | `tone` | `Tone` | | 该条命令自身动作的性质:删除写 danger、停用写 warning。不写即与其余条目同档。 只换字色与悬停 / 按下的面,不改字重与缩进,也不表达选中或校验;禁用压过它。 红字不是唯一通道,破坏性命令仍要配图标。整张菜单的 tone 不下发给条目。 | | `indicator` | `string` | | 标记位文字(勾选符号等装饰);未提供时本条不铺 item-indicator。 | | `description` | `string` | | 副文本,写入 item-description 部件;未提供时本条不铺该部件。 | | `shortcut` | `string` | | 快捷键提示,写入 item-shortcut 部件;未提供时本条不铺该部件。 纯装饰:读屏从条目文字取意,不念它;只为真正注册了的组合写提示。 | | `group` | `string` | | 归属分组的身份值;相邻同值的条目收进同一个 group 部件。未提供时本条直接落在 content 上。 | | `groupLabel` | `string` | | 分组标题文字,取本组首个提供它的条目;本组无人提供时不铺 group-label。 | | `separatorBefore` | `boolean` | | 本条之前绘制一条分隔线;写在首条上不产出分隔线。本条领头一个分组时,分隔线绘制在分组外。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `open-change` | `ContextMenuOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | | `select` | `ContextMenuSelectDetails` | 条目被选中(菜单随之关闭);detail 为 `{ value: string }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhContextMenuRoot` | `default` | `ContextMenuRootSlotProps` | | | `XhContextMenuRoot` | `trigger` | — | | | `XhContextMenuRoot` | `item` | `ContextMenuNodeMeta` | 只填条目的文字槽,标记位、副文本与快捷键照旧由数据铺 | | `XhContextMenuRoot` | `item-prefix` | `ContextMenuNodeMeta` | 只接管行首那一格,其余槽照旧由数据铺 | | `XhContextMenuRoot` | `item-suffix` | `ContextMenuNodeMeta` | 只接管行尾那一格(计数、徽标、次级图标),其余槽照旧由数据铺 | | `XhContextMenuSub` | `default` | `ContextMenuSubSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhContextMenuGroup` | `value` | `string` | 是 | | | `XhContextMenuItem` | `value` | `string` | 是 | | | `XhContextMenuItem` | `disabled` | `boolean` | | 默认交给 connect 查询 collection,写死 false 会覆盖数据中的禁用。 | | `XhContextMenuPositioner` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhContextMenuRoot` | `trigger` | `ReactNode` | | 触发区中放置的内容;只提供 collection 时由它承载。 | | `XhContextMenuRoot` | `renderItem` | `(node: ContextMenuNodeMeta) => ReactNode` | | 每个条目的自定义内容;未提供时使用 collection 中的 label。 | | `XhContextMenuRoot` | `renderItemPrefix` | `(node: ContextMenuNodeMeta) => ReactNode` | | 只接管条目行首那一格;其余槽仍由数据铺。 | | `XhContextMenuRoot` | `renderItemSuffix` | `(node: ContextMenuNodeMeta) => ReactNode` | | 只接管条目行尾那一格(计数、徽标、次级图标);其余槽仍由数据铺。 | | `XhContextMenuRoot` | `children` | `SlotChildren` | | | | `XhContextMenuSub` | `value` | `string` | 是 | 它在父右键菜单中的条目身份。 | | `XhContextMenuSub` | `disabled` | `boolean` | | | | `XhContextMenuSub` | `collection` | `MenuNode[]` | | | | `XhContextMenuSub` | `placement` | `Placement` | | | | `XhContextMenuSub` | `offset` | `number` | | | | `XhContextMenuSub` | `loop` | `boolean` | | | | `XhContextMenuSub` | `openOnHover` | `boolean` | | | | `XhContextMenuSub` | `hoverOpenDelay` | `number` | | | | `XhContextMenuSub` | `hoverCloseDelay` | `number` | | | | `XhContextMenuSub` | `dir` | `Direction` | | 文字方向;默认继承父层。子层被迁移到浮层落点,无法继承父层的方向。 | | `XhContextMenuSub` | `tone` | `Tone` | | 语气;默认继承父层。子层是浮层落点下的同级节点,CSS 私有槽无法继承。 | | `XhContextMenuSub` | `size` | `Size` | | 尺寸;默认继承父层,理由同 tone。 | | `XhContextMenuSub` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`closed` · `pressing` · `open` **事件**:`CONTEXT.MENU` · `OPEN` · `CLOSE` · `PRESS.START` · `PRESS.MOVE` · `PRESS.END` · `after.longPressDelay` · `ITEM.PRESS.START` · `ITEM.PRESS.END` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `ITEM.FOCUS` · `FOCUS.CLEAR` · `ITEM.LOST` · `ITEM.SELECT` **判据**:`isOpenControlled` · `movedBeyondTolerance` · `canPressItem` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `collection` | `readonly ContextMenuNodeMeta[]` | 由 collection 推导的条目元信息,按数据顺序排列;未提供 collection 时为空数组。 | | `pressing` | `boolean` | 长按计时进行中;触发区据此提供按压反馈。 | | `point` | `ContextMenuPoint \| null` | 当前锚点坐标;从未打开过时为 null。 | | `focusedValue` | `string \| null` | 焦点锚点;收起时为 null。 | | `setOpen` | `(next: boolean) => void` | 收起经 CLOSE;展开沿用最近一次锚点坐标,从未有过坐标时锚定在触发区的起始角。 | | `openAt` | `(x: number, y: number) => void` | 命令式展开到指定视口坐标。 | | `getRootProps` | `() => T['element']` | | | `getTriggerProps` | `() => T['element']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getItemProps` | `(props: ContextMenuItemProps) => T['element']` | | | `getItemTextProps` | `(props: ContextMenuItemProps) => T['element']` | | | `getItemIndicatorProps` | `(props: ContextMenuItemProps) => T['element']` | | | `getItemDescriptionProps` | `(props: ContextMenuItemProps) => T['element']` | | | `getItemShortcutProps` | `(props: ContextMenuItemProps) => T['element']` | | | `getItemSuffixProps` | `(props: ContextMenuItemProps) => T['element']` | | | `getSeparatorProps` | `() => T['element']` | | | `getGroupProps` | `(props: ContextMenuGroupProps) => T['element']` | | | `getGroupLabelProps` | `(props: ContextMenuGroupProps) => T['element']` | | | `getArrowProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/menu/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `ContextMenu` / `Shift+F10` | focus in trigger | 在触发区起始角展开菜单并把焦点落到首个可用条目 | | `ArrowDown` | open, focus in content | 焦点移到下一个条目(禁用项跳过、尽头按 loop 回绕) | | `ArrowUp` | open, focus in content | 焦点移到上一个条目(禁用项跳过、尽头按 loop 回绕) | | `Home` | open, focus in content | 焦点移到首个可用条目 | | `End` | open, focus in content | 焦点移到末个可用条目 | | `单个可打印字符` | open, typeahead 未关 | 连打检索把焦点移到首字母匹配的条目,不选中它 | | `Enter` / `Space` | focus in item, not disabled | 派发选中详情并关闭菜单,焦点归还触发区 | | `Enter` / `Space` | held in item, not disabled | 按住期间该条目投影 data-pressed,与指针 :active 同一副按压面;抬起、失焦或菜单收起撤下 | | `Escape` | open | 关闭菜单并把焦点归还触发区 | | `Tab` / `Shift+Tab` | open | 关闭菜单,焦点不归还触发区,按 Tab 序列自然离开 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-haspopup` | 'menu' | | `trigger` | `aria-keyshortcuts` | 'Shift+F10' | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-label` | props.translations.content | | `content` | `role` | 'menu' | | `item` | `aria-disabled` | 'true' \| 'false' | | `item` | `role` | 'menuitem' | | `item-indicator` | `aria-hidden` | 'true' | | `item-shortcut` | `aria-hidden` | 'true' | | `separator` | `aria-orientation` | 'horizontal' | | `separator` | `role` | 'separator' | | `group` | `aria-labelledby` | `group-label` 部件的 id | | `group` | `role` | 'group' | | `arrow` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/context-menu.css` 使用 `[data-scope="context-menu"][data-part="root"]` 部件选择器,位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `root` | `data-tone` | props.tone | | `trigger` | `data-pressing` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `positioner` | `data-hidden` | ''(条件成立时才出现) | | `positioner` | `data-placement` | 定位引擎算出的实际落位 | | `positioner` | `data-positioned` | ''(条件成立时才出现) | | `positioner` | `data-size` | props.size | | `positioner` | `data-state` | 'open' \| 'closed' | | `positioner` | `data-tone` | props.tone | | `content` | `data-placement` | 定位引擎算出的实际落位 | | `content` | `data-state` | 'open' \| 'closed' | | `content` | `data-xh-material` | 'frosted' | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-highlighted` | ''(条件成立时才出现) | | `item` | `data-pressed` | ''(条件成立时才出现) | | `item` | `data-tone` | metaOf.get(item.value)?.tone | | `item` | `data-xh-collection-context` | 'overlay' | | `item` | `data-xh-collection-item` | '' | | `item` | `data-xh-collection-size` | props.size | | `item-text` | `data-disabled` | ''(条件成立时才出现) | | `item-text` | `data-highlighted` | ''(条件成立时才出现) | | `item-text` | `data-xh-collection-slot` | 'text' | | `item-indicator` | `data-disabled` | ''(条件成立时才出现) | | `item-indicator` | `data-highlighted` | ''(条件成立时才出现) | | `item-indicator` | `data-xh-collection-slot` | 'prefix' | | `item-description` | `data-disabled` | ''(条件成立时才出现) | | `item-description` | `data-highlighted` | ''(条件成立时才出现) | | `item-description` | `data-xh-collection-slot` | 'description' | | `item-shortcut` | `data-disabled` | ''(条件成立时才出现) | | `item-shortcut` | `data-highlighted` | ''(条件成立时才出现) | | `item-shortcut` | `data-xh-collection-slot` | 'shortcut' | | `item-suffix` | `data-disabled` | ''(条件成立时才出现) | | `item-suffix` | `data-highlighted` | ''(条件成立时才出现) | | `item-suffix` | `data-xh-collection-slot` | 'suffix' | | `separator` | `data-xh-collection-separator` | '' | | `arrow` | `data-placement` | 定位引擎算出的实际落位 | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-context-menu-arrow-size` | `arrow` | `--xh-_overlay-arrow-size` | `default` | `--xh-overlay-arrow-size` | context-menu 的 arrow 部件 --xh-_overlay-arrow-size 覆盖槽。 | | `--xh-context-menu-backdrop` | `content` | `-webkit-backdrop-filter`
`backdrop-filter` | `xh-material=frosted` | `--xh-_material-backdrop` | context-menu 的 content 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 | | `--xh-context-menu-border` | `arrow`
`content` | `border` | `default`
`not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-border`
`--xh-material-frosted-border` | context-menu 的 arrow、content 部件 border 覆盖槽。 | | `--xh-context-menu-content-bg` | `arrow`
`content` | `background` | `default`
`not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-bg`
`--xh-material-frosted-bg` | context-menu 的 arrow、content 部件 background 覆盖槽。 | | `--xh-context-menu-content-fg` | `content` | `color` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-fg` | context-menu 的 content 部件 color 覆盖槽。 | | `--xh-context-menu-content-gap` | `content` | `gap` | `default` | `--xh-list-option-gap` | context-menu 的 content 部件 gap 覆盖槽。 | | `--xh-context-menu-content-px` | `content` | `padding-inline` | `default` | `--xh-surface-pad-xs` | context-menu 的 content 部件 padding-inline 覆盖槽。 | | `--xh-context-menu-content-py` | `content` | `padding-block` | `default` | `--xh-surface-pad-xs` | context-menu 的 content 部件 padding-block 覆盖槽。 | | `--xh-context-menu-content-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | context-menu 的 content 部件 border-radius 覆盖槽。 | | `--xh-context-menu-content-shadow` | `content` | `box-shadow` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-shadow` | context-menu 的 content 部件 box-shadow 覆盖槽。 | | `--xh-context-menu-group-gap` | `group` | `gap` | `default` | `--xh-list-option-gap` | context-menu 的 group 部件 gap 覆盖槽。 | | `--xh-context-menu-group-label-fg` | `group-label` | `color` | `default` | `--xh-material-frosted-fg-muted` | context-menu 的 group-label 部件 color 覆盖槽。 | | `--xh-context-menu-group-label-font-size` | `group-label` | `font-size` | `default` | `--xh-text-caption-size` | context-menu 的 group-label 部件 font-size 覆盖槽。 | | `--xh-context-menu-group-label-font-weight` | `group-label` | `font-weight` | `default` | `--xh-font-weight-medium` | context-menu 的 group-label 部件 font-weight 覆盖槽。 | | `--xh-context-menu-group-label-px` | `group-label` | `padding-inline` | `default` | `--xh-_context-menu-item-px` | context-menu 的 group-label 部件 padding-inline 覆盖槽。 | | `--xh-context-menu-group-label-py` | `group-label` | `padding-block` | `default` | `--xh-space-1` | context-menu 的 group-label 部件 padding-block 覆盖槽。 | | `--xh-context-menu-highlight` | `content` | `background` | `not([data-xh-action-control])`
`xh-material=frosted` | `--xh-_material-highlight` | context-menu 的 content 部件 background 覆盖槽。 | | `--xh-context-menu-icon-size` | `positioner`
`root` | `--xh-icon-size` | `is([data-part='root'], [data-part='positioner'])`
`size=lg`
`size=sm` | `--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | context-menu 的 positioner、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-context-menu-item-bg-active` | `item` | `background-color` | `in-path` | `--xh-bg-subtle` | context-menu 的 item 部件 background-color 覆盖槽。 | | `--xh-context-menu-item-bg-hover` | `item` | `background-color` | `disabled`
`error`
`highlighted`
`hover`
`is(:focus-visible, [data-highlighted])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])` | `--xh-bg-subtle` | context-menu 的 item 部件 background-color 覆盖槽。 | | `--xh-context-menu-item-bg-pressed` | `item` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed` | `--xh-bg-subtle-hover` | context-menu 的 item 部件 background-color 覆盖槽。 | | `--xh-context-menu-item-description-fg` | `item-description` | `color` | `default` | `--xh-material-frosted-fg-muted` | context-menu 的 item-description 部件 color 覆盖槽。 | | `--xh-context-menu-item-description-font-size` | `item-description` | `font-size` | `default` | `--xh-text-caption-size` | context-menu 的 item-description 部件 font-size 覆盖槽。 | | `--xh-context-menu-item-fg` | `item` | `color` | `default`
`disabled`
`error`
`highlighted`
`hover`
`in-path`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed` | `--xh-material-frosted-fg` | context-menu 的 item 部件 color 覆盖槽。 | | `--xh-context-menu-item-font-size` | `item` | `font-size` | `default` | `--xh-_context-menu-font-size` | context-menu 的 item 部件 font-size 覆盖槽。 | | `--xh-context-menu-item-gap` | `item` | `gap` | `default` | `--xh-_context-menu-item-gap` | context-menu 的 item 部件 gap 覆盖槽。 | | `--xh-context-menu-item-indicator-fg` | `item-indicator` | `color` | `default` | `--xh-_tone` | context-menu 的 item-indicator 部件 color 覆盖槽。 | | `--xh-context-menu-item-indicator-size` | `item-indicator` | `--xh-icon-size`
`block-size`
`inline-size` | `default` | `--xh-control-indicator-size` | context-menu 的 item-indicator 部件 --xh-icon-size、block-size、inline-size 覆盖槽。 | | `--xh-context-menu-item-leading` | `item` | `line-height` | `default` | `--xh-leading-normal` | context-menu 的 item 部件 line-height 覆盖槽。 | | `--xh-context-menu-item-px` | `item` | `padding-inline` | `default` | `--xh-_context-menu-item-px` | context-menu 的 item 部件 padding-inline 覆盖槽。 | | `--xh-context-menu-item-py` | `item` | `padding-block` | `default` | `--xh-_context-menu-item-py` | context-menu 的 item 部件 padding-block 覆盖槽。 | | `--xh-context-menu-item-radius` | `item` | `border-radius` | `default` | `--xh-shape-control` | context-menu 的 item 部件 border-radius 覆盖槽。 | | `--xh-context-menu-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | context-menu 的 positioner 部件 z-index 覆盖槽。 | | `--xh-context-menu-max-h` | `content` | `max-block-size` | `default` | `--xh-overlay-menu-max-h` | context-menu 的 content 部件 max-block-size 覆盖槽。 | | `--xh-context-menu-max-w` | `content` | `max-inline-size` | `default` | `--xh-overlay-max-w` | context-menu 的 content 部件 max-inline-size 覆盖槽。 | | `--xh-context-menu-min-w` | `content` | `min-inline-size` | `default` | `--xh-overlay-menu-min-w` | context-menu 的 content 部件 min-inline-size 覆盖槽。 | | `--xh-context-menu-separator-color` | `separator` | `background` | `default` | `--xh-material-frosted-separator` | context-menu 的 separator 部件 background 覆盖槽。 | | `--xh-context-menu-separator-my` | `separator` | `margin-block` | `default` | `--xh-space-0_5` | context-menu 的 separator 部件 margin-block 覆盖槽。 | | `--xh-context-menu-separator-radius` | `separator` | `border-radius` | `default` | `--xh-shape-pill` | context-menu 的 separator 部件 border-radius 覆盖槽。 | | `--xh-context-menu-separator-thickness` | `separator` | `block-size` | `default` | `--xh-stroke-thin` | context-menu 的 separator 部件 block-size 覆盖槽。 | | `--xh-context-menu-submenu-indicator-fg` | `item` | `background-color` | `default` | `--xh-material-frosted-fg-muted` | context-menu 的 item 部件 background-color 覆盖槽。 | | `--xh-context-menu-submenu-indicator-size` | `item` | `block-size`
`inline-size` | `default` | `--xh-control-indicator-size` | context-menu 的 item 部件 block-size、inline-size 覆盖槽。 | | `--xh-context-menu-trigger-bg-pressing` | `trigger` | `background` | `pressing` | `--xh-bg-subtle` | context-menu 的 trigger 部件 background 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(锚定列表)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-overlay-slide-in` · `xh-overlay-slide-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`background-color` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/date-field # DateField 日期字段 按年、月、日逐段输入日期,适合已经知道目标日期、无需浏览日历的场景。 ## 用法 输入日期 ```vue ``` ```html
/ /
``` ## 组件结构 加粗的是必需部件。 `data-scope="date-field"`:**`root`** · `label` · **`control`** · `segment-group` · **`segment`** · `clear-trigger` · `hidden-input` ## 示例 ### 地区格式 根据 locale 调整日期顺序 ```vue ``` ```html
年 月 日
/ /
``` ### 日期范围 限制可输入日期 ```vue ``` ```html
年 月 日
年 月 日
``` ### 状态 禁用、只读与校验失败 ```vue ``` ```html
年 月 日
年 月 日
年 月 日
``` ### 变体 设置输入框外观 ```vue ``` ```html
年 月 日
年 月 日
年 月 日
``` ### 日期与时间 输入精确到分钟的日期 ```vue ``` ```html
年 月 日   :
``` ## 设计指引 ### 何时使用 - 用户已知确切日期,例如生日或证件有效期。 - 需要使用键盘快速逐段输入。 ### 何时不用 - 需要查看月份或星期信息时,使用[日期选择器](./date-picker)。 - 只需要时间不需要日期时,使用[时间字段](./time-field)。 ### 特性 - `locale` 决定日期段的顺序和分隔方式。 - `min` 与 `max` 限制可输入范围。 - `granularity` 支持日期或精确到分钟的日期时间。 - `year + week` 段集使用 ISO 周历,固定周一到周日,不随显示语言改变。 - 标准组合包含标签、输入框、日期段和隐藏表单输入;支持受控值与原生表单提交。 - 聚焦只强调正在编辑的日期段,错误段使用独立的危险色反馈。 - 清空按钮默认收起,输入任一段后出现;点按后回到第一段,聚焦边界平滑过渡。 ### 组合 - [日期选择器](./date-picker)与[日期范围选择器](./date-range-picker)的输入区就是这一套逐段输入,只是多了日历浮层。 - 日期与时间分开录入时与[时间字段](./time-field)并排;只用一个字段时通过 `granularity` 精确到分钟。 - 在[表单](./form)中以 ISO 日期字符串参与校验与提交。 ### 最佳实践 - 使用清晰的字段标签。 - 给参与表单提交的字段设置 `name`,并渲染隐藏输入部件。 - 有业务范围限制时设置 `min` 与 `max`。 - 对外统一使用 ISO 日期字符串。 ### 反模式 - 使用普通文本输入接收日期并自行解析。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDateFieldClearTrigger` `XhDateFieldControl` `XhDateFieldHiddenInput` `XhDateFieldLabel` `XhDateFieldRoot` `XhDateFieldSegment` `XhDateFieldSegmentGroup` | | 组合式函数 | `useDateField` | | 状态机 | `dateFieldMachine` | | 皮肤 | `@xihan-ui/styles/date-field.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string \| null` | | 受控值,ISO 串('2026-07-28' / '2026-07-28T13:45');null 表示空。提供即受控。 | | `defaultValue` | `string \| null` | | 非受控初值,同样是 ISO 串。 | | `min` | `string` | | 下界,ISO 串。参与各段区间的收窄,并决定 outOfRange。 | | `max` | `string` | | 上界,ISO 串。 | | `locale` | `string` | | BCP 47 语言标记,决定年月日三段的先后。未提供时按宿主语言,宿主也没有时按 en-US(月日年)排列。 | | `timeZone` | `string` | | IANA 时区名,只用于取今天:空段上按上下键时从今天的对应位起步。 | | `granularity` | `DateGranularity` | | 精度,默认 day(只有年月日三段)。提供 segments 时它不再生效。 | | `segments` | `DateSegmentSet` | | 段集:该控件由哪几段组成,提供后以它为准,granularity 让位。写 `['year', 'quarter']` 得到「2026 Q2」、`['year', 'week']` 得到「2026 33」。归一后为空(如 `[]`)视同未提供。 值仍是 ISO 日期(时间)串,因此段集中必须有 year,否则段位可编辑但无法拼出值。 | | `disabled` | `boolean` | | | | `readOnly` | `boolean` | | | | `invalid` | `boolean` | | | | `required` | `boolean` | | | | `name` | `string` | | 表单字段名;提供后隐藏输入才带 name,ISO 串随表单一并提交。 | | `placeholder` | `{ readonly [K in DateSegmentType]?: string }` | | 各段未填时显示的占位串,逐段覆盖内置默认(yyyy / mm / dd / hh / mm / ss)。 | | `translations` | `DateFieldTranslations` | | 各段的读屏名字,逐段覆盖内置默认。段是 spinbutton,没有名字时读屏只能朗读一串数字。 | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定底色与描边的绘制方式。默认 outline。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定聚焦与强调使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `onValueChange` | `(details: DateFieldValueChangeDetails) => void` | | | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `DateFieldValueChangeDetails` | 值变化;detail 为 `{ value: string \| null }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhDateFieldRoot` | `default` | `DateFieldRootSlotProps` | | | `XhDateFieldSegment` | `default` | `DateFieldSegmentSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDateFieldRoot` | `children` | `SlotChildren` | | | | `XhDateFieldSegment` | `index` | `number \| string` | | 下标由作者声明,对应哪一段由 locale 与段集计算;兼收字符串。 | | `XhDateFieldSegment` | `segment` | `DateSegmentType` | | 按段名声明该格。段集中没有该段时它收起;与 index 二选一,两个都写时按段名计算。 | | `XhDateFieldSegment` | `children` | `SlotChildren` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`VALUE.SET` · `VALUE.CLEAR` · `SEGMENT.STEP` · `SEGMENT.TYPE` · `SEGMENT.CLEAR` · `SEGMENT.PERIOD` · `SEGMENT.FOCUS` · `SEGMENT.BLUR` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`canEdit` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string \| null` | ISO 串;段位未填齐时为 null。 | | `valueAsDate` | `Date \| null` | 同一个值的原生 Date;空值或无法计算时为 null。按 timeZone 换算。 | | `segments` | `DateFieldSegmentState[]` | 逐段投影,文档序即当前的段序(提供 segments 时是其归一后的顺序,否则由 locale 排列)。 | | `complete` | `boolean` | 段位已填齐(value 非 null)。 | | `empty` | `boolean` | 没有任何段已填。 | | `outOfRange` | `boolean` | 已填齐但落在 min / max 之外。 | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | | | `focusedSegment` | `DateSegmentType \| null` | 焦点所在的段;焦点在组外时为 null。 | | `locale` | `string` | | | `granularity` | `DateGranularity` | | | `setValue` | `(next: string \| null) => void` | 直接写整份值;传 null 等于清空。 | | `clear` | `() => void` | 清空全部段位;disabled / readOnly 下不生效。 | | `canClear` | `boolean` | 清空按钮当前是否可用:有段已填值、且可编辑。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['element']` | 标题不是原生 label(段位是 div,不可被 label 标注),点击它由连接层代为把焦点送进首段。 | | `getControlProps` | `() => T['element']` | role=group 的分段容器。 | | `getSegmentGroupProps` | `() => T['element']` | 段位与分隔符的外壳:占满盒内剩余宽度,把清空按钮推到框内末端。 | | `segmentOf` | `(props: DateFieldSegmentProps) => DateFieldSegmentState \| undefined` | 作者的声明落在哪一段上;段集中没有该段(或下标越界)时缺席。文字由适配器按它渲染。 | | `getSegmentProps` | `(props: DateFieldSegmentProps) => T['element']` | | | `getClearTriggerProps` | `() => T['button']` | 清空按钮:不占 Tab 位,无值或不可编辑时收起;点击后焦点回到首段。 | | `getHiddenInputProps` | `() => T['input']` | 表单出口:一份 type=hidden 的原生输入,值是 ISO 串。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `ArrowUp` | focus in a segment, not disabled/readOnly | 本段加一,到区间上界回绕到下界;空段则落到今天的对应位 | | `ArrowDown` | focus in a segment, not disabled/readOnly | 本段减一,到区间下界回绕到上界;空段则落到今天的对应位 | | `ArrowRight` | focus in a segment, not disabled | 焦点移到下一段(跳过收起的段);已在末段则不动,不回绕 | | `ArrowLeft` | focus in a segment, not disabled | 焦点移到上一段;已在首段则不动,不回绕 | | `Home` | focus in a segment, not disabled | 焦点移到首段 | | `End` | focus in a segment, not disabled | 焦点移到末段 | | `Backspace` | focus in a segment, not disabled/readOnly | 清掉本段,焦点不动;整份值随之变成 null | | `0` / `1` / `2` / `3` / `4` / `5` / `6` / `7` / `8` / `9` | focus in a segment, not disabled/readOnly | 往本段补一位数字;补满(再补一位必溢出或位数用尽)即自动跳下一段。上下午段没有数字位,不收数字 | | `Enter` / `Space` | held in clear-trigger, 填了哪怕一段, not disabled/readOnly | 按住期间清空按钮投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,段位清空后按钮藏起一并撤下。清空按钮不占 Tab 位,键盘这一路只在焦点落到它身上时有面 | | `a` / `p` | focus in 上下午段, not disabled/readOnly | 直接指定上午 / 下午;上下键在两者之间翻面 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `control` | `aria-disabled` | 'true' \| 'false' | | `control` | `aria-labelledby` | `label` 部件的 id | | `control` | `role` | 'group' | | `segment` | `aria-disabled` | undefined \| 'true' \| 'false' | | `segment` | `aria-invalid` | undefined \| 'true' \| 'false' | | `segment` | `aria-label` | item?.label | | `segment` | `aria-readonly` | undefined \| 'true' \| 'false' | | `segment` | `aria-required` | undefined \| 'true' \| 'false' | | `segment` | `aria-valuemax` | undefined \| String(item.max) | | `segment` | `aria-valuemin` | undefined \| String(item.min) | | `segment` | `aria-valuenow` | undefined \| String(item.value) | | `segment` | `aria-valuetext` | item?.text | | `segment` | `role` | undefined \| 'spinbutton' | | `clear-trigger` | `aria-label` | props.translations.clearTrigger | ## 样式参考 ### 皮肤 `@xihan-ui/styles/date-field.css` 使用 `[data-scope="date-field"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-complete` | ''(条件成立时才出现) | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-empty` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-out-of-range` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `label` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-variant` | props.variant | | `control` | `data-xh-field-chrome` | '' | | `control` | `data-xh-field-size` | props.size | | `segment-group` | `data-disabled` | ''(条件成立时才出现) | | `segment-group` | `data-invalid` | ''(条件成立时才出现) | | `segment-group` | `data-readonly` | ''(条件成立时才出现) | | `segment` | `data-disabled` | ''(条件成立时才出现) | | `segment` | `data-focus` | ''(条件成立时才出现) | | `segment` | `data-index` | String(index) \| undefined | | `segment` | `data-invalid` | ''(条件成立时才出现) | | `segment` | `data-placeholder` | ''(条件成立时才出现) | | `segment` | `data-readonly` | ''(条件成立时才出现) | | `segment` | `data-segment` | item?.type | | `clear-trigger` | `data-pressed` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-control` | '' | | `clear-trigger` | `data-xh-action-display` | 'has-value' | | `clear-trigger` | `data-xh-action-has-value` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-profile` | 'field-inset' | | `clear-trigger` | `data-xh-action-size` | props.size | | `clear-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-date-field-action-bg` | `clear-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | date-field 的 clear-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-date-field-action-bg-active` | `clear-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | date-field 的 clear-trigger 部件 background-color 覆盖槽。 | | `--xh-date-field-action-bg-hover` | `clear-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | date-field 的 clear-trigger 部件 background-color 覆盖槽。 | | `--xh-date-field-action-fg` | `clear-trigger` | `color` | `default` | `--xh-fg-muted` | date-field 的 clear-trigger 部件 color 覆盖槽。 | | `--xh-date-field-action-fg-hover` | `clear-trigger` | `color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-fg-default` | date-field 的 clear-trigger 部件 color 覆盖槽。 | | `--xh-date-field-action-font-size` | `clear-trigger` | `font-size` | `default` | `--xh-text-secondary-size` | date-field 的 clear-trigger 部件 font-size 覆盖槽。 | | `--xh-date-field-action-radius` | `clear-trigger` | `border-radius` | `default` | `--xh-shape-inset` | date-field 的 clear-trigger 部件 border-radius 覆盖槽。 | | `--xh-date-field-action-size` | `clear-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | date-field 的 clear-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-date-field-control-bg` | `control` | `background-color` | `xh-field-chrome` | `--xh-_field-variant-bg-rest` | date-field 的 control 部件 background-color 覆盖槽。 | | `--xh-date-field-control-bg-disabled` | `control` | `background-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-bg-disabled` | date-field 的 control 部件 background-color 覆盖槽。 | | `--xh-date-field-control-bg-hover` | `control` | `background-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-hover` | date-field 的 control 部件 background-color 覆盖槽。 | | `--xh-date-field-control-bg-readonly` | `control` | `background-color` | `readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-read-only` | date-field 的 control 部件 background-color 覆盖槽。 | | `--xh-date-field-control-border` | `control` | `border` | `xh-field-chrome` | `--xh-_field-variant-border-rest` | date-field 的 control 部件 border 覆盖槽。 | | `--xh-date-field-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])`
`xh-field-chrome` | `--xh-_field-variant-border-focus` | date-field 的 control 部件 border-color 覆盖槽。 | | `--xh-date-field-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-border-hover` | date-field 的 control 部件 border-color 覆盖槽。 | | `--xh-date-field-control-border-invalid` | `control` | `border-color` | `invalid`
`xh-field-chrome` | `--xh-_field-variant-border-invalid` | date-field 的 control 部件 border-color 覆盖槽。 | | `--xh-date-field-control-fg` | `control` | `color` | `xh-field-chrome` | `--xh-fg-default` | date-field 的 control 部件 color 覆盖槽。 | | `--xh-date-field-control-gap` | `control` | `gap` | `xh-field-chrome` | `--xh-_date-field-gap` | date-field 的 control 部件 gap 覆盖槽。 | | `--xh-date-field-control-h` | `control` | `block-size`
`min-block-size` | `has([data-xh-field-input][data-xh-field-layout='multi-tag'])`
`has([data-xh-field-input][data-xh-field-layout='single-line'])`
`has([data-xh-field-input][data-xh-field-layout='textarea'])`
`xh-field-chrome`
`xh-field-input`
`xh-field-layout=multi-tag`
`xh-field-layout=single-line`
`xh-field-layout=textarea` | `--xh-_date-field-control-h` | date-field 的 control 部件 block-size、min-block-size 覆盖槽。 | | `--xh-date-field-control-min-w` | `control`
`root` | `min-inline-size` | `default`
`xh-field-chrome` | `--xh-control-min-w` | date-field 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-date-field-control-px` | `control` | `padding-inline` | `xh-field-chrome` | `--xh-_date-field-control-px` | date-field 的 control 部件 padding-inline 覆盖槽。 | | `--xh-date-field-control-radius` | `control` | `border-radius` | `xh-field-chrome` | `--xh-shape-control` | date-field 的 control 部件 border-radius 覆盖槽。 | | `--xh-date-field-control-shadow` | `control` | `box-shadow` | `xh-field-chrome` | `none` | date-field 的 control 部件 box-shadow 覆盖槽。 | | `--xh-date-field-control-w` | `root` | `inline-size`
`min-inline-size` | `default` | `--xh-control-w` | date-field 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-date-field-font-size` | `control` | `font-size` | `default` | `--xh-_date-field-font-size` | date-field 的 control 部件 font-size 覆盖槽。 | | `--xh-date-field-gap` | `root` | `gap` | `default` | `--xh-space-1` | date-field 的 root 部件 gap 覆盖槽。 | | `--xh-date-field-icon-size` | `control`
`root` | `--xh-icon-size` | `default`
`size=lg`
`size=sm`
`xh-field-chrome` | `--xh-_field-size-glyph-size`
`--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | date-field 的 control、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-date-field-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | date-field 的 label 部件 color 覆盖槽。 | | `--xh-date-field-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | date-field 的 label 部件 color 覆盖槽。 | | `--xh-date-field-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | date-field 的 label 部件 font-size 覆盖槽。 | | `--xh-date-field-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | date-field 的 label 部件 font-weight 覆盖槽。 | | `--xh-date-field-literal-fg` | `segment-group` | `color` | `not([data-scope])` | `--xh-fg-subtle` | date-field 的 segment-group 部件 color 覆盖槽。 | | `--xh-date-field-placeholder-fg` | `segment` | `color` | `placeholder` | `--xh-fg-subtle` | date-field 的 segment 部件 color 覆盖槽。 | | `--xh-date-field-segment-bg-focus` | `segment` | `background` | `focus`
`focus-visible` | `--xh-_date-field-segment-bg` | date-field 的 segment 部件 background 覆盖槽。 | | `--xh-date-field-segment-bg-invalid-focus` | `segment` | `background` | `focus`
`invalid`
`is([data-focus], :focus-visible)` | `--xh-bg-subtle` | date-field 的 segment 部件 background 覆盖槽。 | | `--xh-date-field-segment-fg-focus` | `segment` | `color` | `focus`
`focus-visible`
`placeholder` | `--xh-_date-field-segment-fg` | date-field 的 segment 部件 color 覆盖槽。 | | `--xh-date-field-segment-fg-invalid` | `segment` | `color` | `invalid` | `--xh-fg-danger` | date-field 的 segment 部件 color 覆盖槽。 | | `--xh-date-field-segment-fg-invalid-focus` | `segment` | `color` | `focus`
`invalid`
`is([data-focus], :focus-visible)` | `--xh-fg-danger` | date-field 的 segment 部件 color 覆盖槽。 | | `--xh-date-field-segment-px` | `segment` | `padding-inline` | `default` | `--xh-space-0_5` | date-field 的 segment 部件 padding-inline 覆盖槽。 | | `--xh-date-field-segment-py` | `segment` | `padding-block` | `default` | `--xh-space-0` | date-field 的 segment 部件 padding-block 覆盖槽。 | | `--xh-date-field-segment-radius` | `segment` | `border-radius` | `default` | `--xh-shape-inset` | date-field 的 segment 部件 border-radius 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/date-picker # DatePicker 日期选择器 将可键入的分段日期框、日历触发器和选择浮层组合成一个字段。 ## 用法 输入或选择日期 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="date-picker"`:`root` · `label` · **`control`** · `segment-group` · `trigger` · `clear-trigger` · `positioner` · **`content`** · `preset-group` · `preset` · **`calendar`** · `time-column` · `time-item` · `confirm-trigger` ## 示例 ### 不可用日期 禁止选择周末 ```vue ``` ```html
``` ### 状态 禁用、只读与校验失败 ```vue ``` ```html
``` ### 快捷选项 提供常用日期 ```vue ``` ```html
``` ### 日期与时间 同时选择日期和时间 ```vue ``` ```html
``` ### 周期选择 granularity 决定输入行铺设哪几段、浮层铺设哪一档格子 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 用户需要查看月份和星期信息后选择日期。 - 需要选择日期时间,或一次挑多个不连续的日子。 ### 何时不用 - 用户已知确切日期且只需要键盘输入时,使用[日期字段](./date-field)。 - 选择一段起止时,使用[日期范围选择器](./date-range-picker)。 - 只需要时间时,使用[时间选择器](./time-picker)。 ### 特性 - `granularity` 支持 day / week / month / quarter / year,`selectionMode` 独立控制单选或多选。 - 周选择直接渲染整周周期格;不再需要 `weekSelection` 特殊开关。 - `min`、`max` 与 `isDateUnavailable` 限制可选日期。 - `presets` 提供常用日期快捷项。 - `showTime` 在 `granularity=day + selectionMode=single` 时让输入行显示完整日期时间,并加入时、分或秒选择列。 - 日期时间组合面板让时间列与日期内容区从同一水平线开始;各时间列只纵向滚动,底部操作独占一行。 - 输入值、展开状态和聚焦日期均可受控。 - 点击输入行可以继续逐段键入,点击日历图标则把焦点送入日历;展开期间输入框保持激活边界。 - 填齐了但越界即整个字段标为不合法,也可以用 `invalid` 显式声明。 - 切换粒度会清空旧选择,输入段、网格和周期边界随后一起切换,不做隐式转换。 - 空值时显示日历入口;有值且渲染了清空按钮时,由清空按钮原位接替日历图标。 - 聚焦边界、段位强调与日期格按压都使用短过渡;减弱动效仍由全局动效轴收敛。 ### 组合 - 浮层内嵌[日历选择器](./calendar-picker),翻月、层级切换与键盘导航由它负责。 - 输入行内嵌[日期字段](./date-field)的段位,逐段键入与加减由它负责。 ### 最佳实践 - 使用明确的字段标签。 - 标准输入行应同时包含清空按钮与日历图标触发器;二者按值互斥显示。参与表单时同时渲染隐藏输入。 - 默认只展开一个日历面板;需要多面板时显式传 `visibleCount`。 - 需要统一查询值时,使用 `calendarPeriodValue(granularity, 'single', value)` 得到周期首尾与回显键。 - 快速选年可在 `year` 网格中渲染受范围约束的年份集合;网格使用三列紧凑布局与内部滚动。 - 不可用日期应同时提供原因。 - 常用日期优先提供快捷项。 ### 反模式 - 未经说明就预先选择今天。 - 让浮层遮挡当前输入值。 - 用多选模拟区间:中间的日期不会自动补齐,也没有拖选与预览。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDatePickerCalendar` `XhDatePickerCell` `XhDatePickerCellTrigger` `XhDatePickerClearTrigger` `XhDatePickerConfirmTrigger` `XhDatePickerContent` `XhDatePickerControl` `XhDatePickerGrid` `XhDatePickerGridBody` `XhDatePickerGridHead` `XhDatePickerHeader` `XhDatePickerHeading` `XhDatePickerHeadingMonthTrigger` `XhDatePickerHeadingYearTrigger` `XhDatePickerHiddenInput` `XhDatePickerLabel` `XhDatePickerNextTrigger` `XhDatePickerNextYearTrigger` `XhDatePickerPositioner` `XhDatePickerPreset` `XhDatePickerPresetGroup` `XhDatePickerPrevTrigger` `XhDatePickerPrevYearTrigger` `XhDatePickerRoot` `XhDatePickerSegment` `XhDatePickerSegmentGroup` `XhDatePickerTimePanel` `XhDatePickerTrigger` `XhDatePickerWeekDay` `XhDatePickerWeekNumber` `XhDatePickerWeekRow` | | 组合式函数 | `useDatePicker` | | 状态机 | `datePickerMachine` | | 皮肤 | `@xihan-ui/styles/date-picker.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string \| string[]` | | 选中值,ISO 串。提供即受控:读取直取 prop,写入只发 onValueChange 不落内部值。 单选可写裸串,内部一律归一为数组。 | | `defaultValue` | `string \| string[]` | | | | `open` | `boolean` | | 展开态。提供即受控:内部不再自行修改,只发 onOpenChange。 | | `defaultOpen` | `boolean` | | | | `min` | `string` | | 可选范围下界(含当天),ISO 串。日历与分段输入共用这一条。 | | `max` | `string` | | 可选范围上界(含当天),ISO 串。 | | `locale` | `string` | | 决定周首日、月份文案与段位先后(zh-CN 年月日、en-US 月日年)。 未提供时按宿主语言,宿主也没有时按 en-US。 | | `timeZone` | `string` | | 判定今天与格式化文案使用的时区,默认取宿主本地时区。 | | `selectionMode` | `CalendarPickerSelectionMode` | | 选择模式,默认 single。区间选择是另一个组件(日期范围选择器)。 | | `isDateUnavailable` | `(value: string) => boolean` | | 不可用判定,接收 ISO 串。界外与判定为真的日期同等处理。 | | `disabled` | `boolean` | | 整个控件禁用:trigger 为原生 disabled,段位退出 Tab 序列,日历格子全部为 aria-disabled。 | | `readOnly` | `boolean` | | 只读:浮层照常展开、日历照常翻月浏览,但选中值不可修改。 | | `invalid` | `boolean` | | 校验失败:段位报告 aria-invalid,各角色节点带 data-invalid。 未提供时也会自行判定:已填齐但越界。 | | `required` | `boolean` | | 必填标注,写入每一段的 aria-required。 | | `name` | `string` | | 表单字段名;提供后隐藏输入才带 name,ISO 串随表单一并提交。 | | `granularity` | `CalendarGranularity` | | 选择粒度;与 selectionMode 正交。输入行与周期网格都由它决定。 | | `activeView` | `CalendarView` | | 面板当前所在的层级。提供即受控;未提供时跟随 granularity,每次展开都回到目标粒度。 点击标题中的年 / 月会修改它。 没有配套的 defaultActiveView:面板每次展开都会重置该档,非受控初值没有生效时刻, 提供后也观察不到任何效果。修改初始层级使用 granularity。 | | `segments` | `DateSegmentSet` | | 输入行铺设的段。未提供时按 granularity 推导:按周为「2026-33」、按月为「2026-05」、 按季度为「2026-Q2」、按年为「2026」,按天则按 locale 排列年月日。 | | `presets` | `DatePickerPreset[]` | | 快捷选项(「今天」「明天」等)。提供后浮层中多出一列,点击即整份写入选中值。 日期需计算后传入:连接层每帧求值,把 `today()` 放进渲染期会跨零点得出两个结果。 与 selectionMode 不匹配(单选提供了多条)、落在 min / max 之外或被 isDateUnavailable 判定不可用的选项 自动不可按下;showTime 下写入的日期附带当前已选的时间。 | | `visibleCount` | `number` | | 展示的连续日历面板数;默认 1。 | | `fixedWeeks` | `boolean` | | 日历恒渲染六行,默认开启。关闭后网格按当月实际周数收缩,翻页时浮层高度随之变化。 | | `defaultFocusedValue` | `string` | | 初始聚焦日,ISO 串;同时决定展开时先落在哪一页。 未提供时回退为首个选中值,再回退为今天。表单重置回到该值。 | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定输入行的描边与底色使用方式。默认 outline。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定聚焦与选中强调使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,输入行与浮层中的日历格一并换档。 | | `placement` | `Placement` | | | | `dir` | `Direction` | | 文字方向,默认 ltr。只改写浮层在行内轴上 start 与 end 的落点。 | | `offset` | `number` | | | | `translations` | `Partial` | | | | `closeOnSelect` | `boolean` | | 选完即收起,默认 true。多选不收起。 | | `showTime` | `boolean` | | 一体化时间:值升格为 'YYYY-MM-DDTHH:mm[:ss]',面板中多出时间列, 选完日期不收起、由确认按钮收口。只在 day + single 下生效。 | | `timeGranularity` | `DatePickerTimeGranularity` | | showTime 的时间段精度,默认 minute。 | | `onValueChange` | `(details: DatePickerValueChangeDetails) => void` | | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 | | `onOpenChange` | `(details: DatePickerOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `onFocusedValueChange` | `(details: DatePickerFocusChangeDetails) => void` | | 聚焦日变化(方向键、翻月、展开、段位输入都会发出)。 网格由外部渲染,不监听该事件时日历不会换月。 | | `onActiveViewChange` | `(details: CalendarViewChangeDetails) => void` | | 面板所在层级变化(点击标题向上、点击格子向下都会发出);受控时是唯一出口。 | ### DatePickerPreset `presets` 的元素。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | 是 | | | `label` | `string` | 是 | 显示文案,同时是该项的可及名。 | | `disabled` | `boolean` | | 禁用该项:方向键仍可停留,但按下不写值。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `DatePickerValueChangeDetails` | 选中集合变化;detail 为 `{ value: string[] }` | | `open-change` | `DatePickerOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | | `focused-value-change` | `DatePickerFocusChangeDetails` | 聚焦日变化(展示月可能随之变化);detail 为 `{ focusedValue: string }`,作者据此重绘网格 | | `active-view-change` | `CalendarViewChangeDetails` | 切换到另一层级(点击标题向上、点击格子向下);detail 为 `{ activeView: 'day'\|'week'\|'month'\|'quarter'\|'year' }`,作者据此重绘网格 | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhDatePickerPreset` | `default` | — | 条目内容;未写时使用数据中的 label。 | | `XhDatePickerPresetGroup` | `default` | `DatePickerPresetsSlotProps` | 自行铺设条目;未写时按 presets 数据自动铺设,两者产出的 DOM 一致。 | | `XhDatePickerRoot` | `default` | `DatePickerRootSlotProps` | | | `XhDatePickerSegment` | `default` | `DatePickerSegmentSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDatePickerCalendar` | `index` | `number \| string` | | 并排的第几张面板,默认 0。写在这里,面板内的标题、网格与格子就不必各写一遍。 | | `XhDatePickerCell` | `value` | `string` | 是 | ISO 日期串。 | | `XhDatePickerCell` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。同一天会同时出现在两个面板中 (8 月末的几天也铺在 9 月的首行),是否为本月只有连同面板一起看才能判定。 | | `XhDatePickerGrid` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDatePickerHeading` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDatePickerHeadingMonthTrigger` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDatePickerHeadingYearTrigger` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDatePickerPositioner` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhDatePickerPreset` | `value` | `string` | 是 | 该条目的身份,与 presets 数据中的 value 逐字对应。 | | `XhDatePickerPresetGroup` | `children` | `SlotChildren` | | 自行铺设条目;未写时按 presets 数据自动铺设,两者产出的 DOM 一致。 | | `XhDatePickerRoot` | `children` | `SlotChildren` | | | | `XhDatePickerSegment` | `index` | `number \| string` | | 段位下标,兼收字符串。 | | `XhDatePickerSegment` | `segment` | `DateSegmentType` | | 按段名声明该格。段集中没有该段时它收起;与 index 二选一,两个都写时按段名计算。 | | `XhDatePickerSegment` | `children` | `SlotChildren` | | | | `XhDatePickerWeekDay` | `value` | `number \| string` | 是 | 列序 0-6,兼收字符串。 | | `XhDatePickerWeekNumber` | `value` | `string` | 是 | 该行行首那一天的 ISO 串。 | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `control` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `preset` | 'checked' \| 'unchecked' | | `calendar` | 'open' \| 'closed' | | `time-item` | 'checked' \| 'unchecked' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `VALUE.SET` · `VALUE.CLEAR` · `FOCUSED.SET` · `VIEW.SET` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` · `closesOnSelect` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `value` | `string[]` | 选中集合,ISO 串;形状不随模式变化。 | | `valueAsString` | `string \| null` | 首个选中值;无选中时为 null。 | | `selectionMode` | `CalendarPickerSelectionMode` | | | `periodValue` | `CalendarPeriodValue \| null` | single 的规范化周期值;multiple 没有连续区间语义,返回 null。 | | `focusedValue` | `string` | 生效聚焦日(三路收口后的结果),恒非空。日历展示哪个月由它决定。 | | `granularity` | `CalendarGranularity` | 作者选择的粒度。 | | `activeView` | `CalendarView` | 面板当前所在的层级。 | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | 校验失败:作者标记的或越界。 | | `canClear` | `boolean` | 清空按钮当前是否可按。 | | `setOpen` | `(next: boolean) => void` | | | `setValue` | `(next: string[]) => void` | | | `clear` | `() => void` | | | `setActiveView` | `(next: CalendarView) => void` | 直接切换到某一层级。 | | `presets` | `readonly DatePickerPresetState[]` | 快捷选项逐条的状态,数据顺序。未提供 presets 时为空数组。 | | `showTime` | `boolean` | showTime 生效(已开启且为单选模式)。 | | `timeColumns` | `readonly TimePickerColumn[]` | 时间列(时 / 分[/ 秒]);未开启 showTime 时为空数组。 | | `timeValue` | `string \| null` | 当前时间段('HH:mm[:ss]');尚无值时为 null。 | | `calendar` | `CalendarPickerApi` | 内嵌日历:选日期、翻月、键盘导航都在它身上。 | | `field` | `DatePickerFieldApi` | 内嵌分段输入。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['element']` | | | `getControlProps` | `() => T['element']` | | | `getSegmentGroupProps` | `() => T['element']` | role=group 的分段容器,段位挂在其中。 | | `getTriggerProps` | `() => T['button']` | | | `getClearTriggerProps` | `() => T['button']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getPresetGroupProps` | `() => T['element']` | 快捷选项列(role=listbox);未提供 presets 时带 hidden。 | | `getPresetProps` | `(props: DatePickerPresetProps) => T['element']` | 一条快捷选项(role=option):点击把整份日期写入选中值。 | | `getCalendarProps` | `() => T['element']` | 内嵌日历的挂载点,同时充当日历的根节点。 | | `getTimeColumnProps` | `(props: DatePickerTimeColumnProps) => T['element']` | 时间列容器(时 / 分[/ 秒]各一列);未开启 showTime 时带 hidden。 | | `getTimeItemProps` | `(props: DatePickerTimeItemProps) => T['element']` | 时间选项:点击把该单位写入值(没有日期时以聚焦日作为日期段起值)。 | | `getConfirmTriggerProps` | `() => T['button']` | 确认按钮:showTime 的收口;未开启 showTime 时带 hidden。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/#kbd_label) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in trigger, closed | 展开日历浮层,焦点落到当前聚焦日那一格 | | `Enter` / `Space` | focus in trigger, open | 收起浮层,焦点回到 trigger | | `Escape` | open | 收起浮层并把焦点还给展开前那个控件(通常是 trigger),选中值不变 | | `Tab` / `Shift+Tab` | open | 不拦按键:焦点按 Tab 序列自然离开,浮层随即收起且不抢回焦点 | | `Enter` / `Space` | open, focus in grid | 选中聚焦日(由日历完成);closeOnSelect 时收起浮层,多选不收起 | | `ArrowUp` / `ArrowDown` / `Home` / `End` | open, focus in 快捷选项列 | 在快捷选项之间移动焦点,到头回绕;不写值 | | `Enter` / `Space` | open, focus in 某条快捷选项 | 把这条快捷选项整份写进选中值;closeOnSelect 时收起浮层 | | `Alt+ArrowDown` | focus in 某一段, closed, not disabled | 展开浮层并把焦点移入;触发按钮是可选部件,键盘入口不能只挂在它上面 | | `Enter` | focus in 某一段, open | 收起浮层。段位里敲出来的值不触发「选完即收」(那时人还在打字),这是那条路的收口手势 | | `Enter` / `Space` | held on trigger / confirm-trigger(not disabled)、clear-trigger(可清)、preset 或 time-item(open, not disabled/readOnly, 该条可按) | 按住期间该部件投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,浮层收起时一并撤下。日历里的部件由 calendar-picker 自己投影 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `segment-group` | `aria-disabled` | 'true' \| 'false' | | `segment-group` | `aria-labelledby` | `label` 部件的 id | | `segment-group` | `role` | 'group' | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `trigger` | `aria-haspopup` | 'dialog' | | `trigger` | `aria-labelledby` | `label` 部件的 id | | `clear-trigger` | `aria-label` | label.clearTrigger | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-labelledby` | `label` 部件的 id | | `content` | `aria-modal` | 'false' | | `content` | `role` | 'dialog' | | `preset-group` | `aria-disabled` | 'true' \| 'false' | | `preset-group` | `aria-label` | label.presets | | `preset-group` | `aria-multiselectable` | 'false' | | `preset-group` | `aria-orientation` | 'vertical' | | `preset-group` | `role` | 'listbox' | | `preset` | `aria-disabled` | 'true' \| 'false' | | `preset` | `aria-selected` | 'true' \| 'false' | | `preset` | `role` | 'option' | | `time-column` | `aria-disabled` | 'true' \| 'false' | | `time-column` | `aria-label` | label[unit] | | `time-column` | `aria-multiselectable` | 'false' | | `time-column` | `aria-orientation` | 'vertical' | | `time-column` | `role` | 'listbox' | | `time-item` | `aria-selected` | 'true' \| 'false' | | `time-item` | `role` | 'option' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/date-picker.css` 使用 `[data-scope="date-picker"][data-part="root"]` 部件选择器,位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `label` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-state` | 'open' \| 'closed' | | `control` | `data-variant` | props.variant | | `control` | `data-xh-field-chrome` | '' | | `control` | `data-xh-field-size` | props.size | | `segment-group` | `data-complete` | ''(条件成立时才出现) | | `segment-group` | `data-disabled` | ''(条件成立时才出现) | | `segment-group` | `data-empty` | ''(条件成立时才出现) | | `segment-group` | `data-invalid` | ''(条件成立时才出现) | | `segment-group` | `data-out-of-range` | ''(条件成立时才出现) | | `segment-group` | `data-readonly` | ''(条件成立时才出现) | | `trigger` | `data-disabled` | ''(条件成立时才出现) | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'field-inset' | | `trigger` | `data-xh-action-size` | props.size | | `trigger` | `data-xh-action-variant` | 'ghost' | | `clear-trigger` | `data-pressed` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-control` | '' | | `clear-trigger` | `data-xh-action-display` | 'has-value' | | `clear-trigger` | `data-xh-action-has-value` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-profile` | 'field-inset' | | `clear-trigger` | `data-xh-action-size` | props.size | | `clear-trigger` | `data-xh-action-variant` | 'ghost' | | `positioner` | `data-hidden` | ''(条件成立时才出现) | | `positioner` | `data-placement` | 定位引擎算出的实际落位 | | `positioner` | `data-positioned` | ''(条件成立时才出现) | | `positioner` | `data-size` | props.size | | `positioner` | `data-state` | 'open' \| 'closed' | | `positioner` | `data-tone` | props.tone | | `positioner` | `data-variant` | props.variant | | `content` | `data-placement` | 定位引擎算出的实际落位 | | `content` | `data-state` | 'open' \| 'closed' | | `preset` | `data-disabled` | ''(条件成立时才出现) | | `preset` | `data-pressed` | ''(条件成立时才出现) | | `preset` | `data-state` | 'checked' \| 'unchecked' | | `preset` | `data-value` | v | | `preset` | `data-xh-collection-context` | 'overlay' | | `preset` | `data-xh-collection-item` | '' | | `preset` | `data-xh-collection-size` | props.size | | `calendar` | `data-disabled` | ''(条件成立时才出现) | | `calendar` | `data-readonly` | ''(条件成立时才出现) | | `calendar` | `data-state` | 'open' \| 'closed' | | `time-column` | `data-unit` | live[at]!.getAttribute('data-unit') as DatePickerTime… | | `time-item` | `data-pressed` | ''(条件成立时才出现) | | `time-item` | `data-state` | 'checked' \| 'unchecked' | | `time-item` | `data-unit` | live[at]!.getAttribute('data-unit') as DatePickerTime… | | `time-item` | `data-value` | v | | `time-item` | `data-xh-collection-context` | 'overlay' | | `time-item` | `data-xh-collection-item` | '' | | `time-item` | `data-xh-collection-size` | props.size | | `confirm-trigger` | `data-pressed` | ''(条件成立时才出现) | | `confirm-trigger` | `data-xh-action-control` | '' | | `confirm-trigger` | `data-xh-action-display` | 'always' | | `confirm-trigger` | `data-xh-action-profile` | 'text' | | `confirm-trigger` | `data-xh-action-size` | 'sm' | | `confirm-trigger` | `data-xh-action-variant` | 'solid' | | `confirm-trigger` | `data-xh-ink-surface` | '' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-date-picker-action-bg` | `clear-trigger`
`trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | date-picker 的 clear-trigger、trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-date-picker-action-bg-active` | `clear-trigger`
`trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | date-picker 的 clear-trigger、trigger 部件 background-color 覆盖槽。 | | `--xh-date-picker-action-bg-hover` | `clear-trigger`
`trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])`
`state=open`
`xh-ink-surface` | `--xh-_action-variant-bg-hover` | date-picker 的 clear-trigger、trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-date-picker-action-fg` | `clear-trigger`
`trigger` | `color` | `default` | `--xh-fg-muted` | date-picker 的 clear-trigger、trigger 部件 color 覆盖槽。 | | `--xh-date-picker-action-fg-hover` | `clear-trigger`
`trigger` | `color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])`
`state=open` | `--xh-fg-default` | date-picker 的 clear-trigger、trigger 部件 color 覆盖槽。 | | `--xh-date-picker-action-font-size` | `clear-trigger`
`trigger` | `font-size` | `default` | `--xh-text-secondary-size` | date-picker 的 clear-trigger、trigger 部件 font-size 覆盖槽。 | | `--xh-date-picker-action-radius` | `clear-trigger`
`trigger` | `border-radius` | `default` | `--xh-shape-inset` | date-picker 的 clear-trigger、trigger 部件 border-radius 覆盖槽。 | | `--xh-date-picker-action-size` | `clear-trigger`
`trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | date-picker 的 clear-trigger、trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-date-picker-calendar-gap` | `calendar`
`time-column` | `gap`
`margin-block-start` | `default` | `--xh-space-2` | date-picker 的 calendar、time-column 部件 gap、margin-block-start 覆盖槽。 | | `--xh-date-picker-column-divider` | `calendar`
`preset-group`
`time-column` | `background`
`border-block-end`
`border-inline-end`
`border-inline-start` | `@media (min-width: 768px)`
`default`
`has(+ [data-part='time-column'])` | `--xh-material-frosted-separator` | date-picker 的 calendar、preset-group、time-column 部件 background、border-block-end、border-inline-end、border-inline-start 覆盖槽。 | | `--xh-date-picker-confirm-trigger-bg` | `confirm-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | date-picker 的 confirm-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-date-picker-confirm-trigger-bg-active` | `confirm-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | date-picker 的 confirm-trigger 部件 background-color 覆盖槽。 | | `--xh-date-picker-confirm-trigger-bg-hover` | `confirm-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | date-picker 的 confirm-trigger 部件 background-color 覆盖槽。 | | `--xh-date-picker-confirm-trigger-fg` | `confirm-trigger` | `color` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | date-picker 的 confirm-trigger 部件 color 覆盖槽。 | | `--xh-date-picker-confirm-trigger-h` | `confirm-trigger` | `block-size`
`inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | date-picker 的 confirm-trigger 部件 block-size、inline-size 覆盖槽。 | | `--xh-date-picker-confirm-trigger-px` | `confirm-trigger` | `padding-inline` | `default` | `--xh-_action-profile-padding-inline` | date-picker 的 confirm-trigger 部件 padding-inline 覆盖槽。 | | `--xh-date-picker-confirm-trigger-radius` | `confirm-trigger` | `border-radius` | `default` | `--xh-shape-control` | date-picker 的 confirm-trigger 部件 border-radius 覆盖槽。 | | `--xh-date-picker-confirm-trigger-shadow` | `confirm-trigger` | `box-shadow` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `none` | date-picker 的 confirm-trigger 部件 box-shadow 覆盖槽。 | | `--xh-date-picker-content-bg` | `content` | `background` | `default` | `--xh-bg-surface` | date-picker 的 content 部件 background 覆盖槽。 | | `--xh-date-picker-content-border` | `content` | `border` | `default` | `--xh-border-default` | date-picker 的 content 部件 border 覆盖槽。 | | `--xh-date-picker-content-fg` | `content` | `color` | `default` | `--xh-fg-default` | date-picker 的 content 部件 color 覆盖槽。 | | `--xh-date-picker-content-px` | `content` | `padding-inline` | `@media not all and (min-width: 768px)`
`default` | `--xh-space-2` | date-picker 的 content 部件 padding-inline 覆盖槽。 | | `--xh-date-picker-content-py` | `content` | `padding-block` | `default` | `--xh-space-2` | date-picker 的 content 部件 padding-block 覆盖槽。 | | `--xh-date-picker-content-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | date-picker 的 content 部件 border-radius 覆盖槽。 | | `--xh-date-picker-content-shadow` | `content` | `box-shadow` | `default` | `--xh-elevation-floating` | date-picker 的 content 部件 box-shadow 覆盖槽。 | | `--xh-date-picker-control-bg` | `control` | `background-color` | `xh-field-chrome` | `--xh-_field-variant-bg-rest` | date-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-picker-control-bg-disabled` | `control` | `background-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-bg-disabled` | date-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-picker-control-bg-hover` | `control` | `background-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-hover` | date-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-picker-control-bg-readonly` | `control` | `background-color` | `readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-read-only` | date-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-picker-control-border` | `control` | `border` | `xh-field-chrome` | `--xh-_field-variant-border-rest` | date-picker 的 control 部件 border 覆盖槽。 | | `--xh-date-picker-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])`
`xh-field-chrome` | `--xh-_field-variant-border-focus` | date-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-date-picker-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-border-hover` | date-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-date-picker-control-border-invalid` | `control` | `border-color` | `invalid`
`xh-field-chrome` | `--xh-_field-variant-border-invalid` | date-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-date-picker-control-fg` | `control` | `color` | `xh-field-chrome` | `--xh-fg-default` | date-picker 的 control 部件 color 覆盖槽。 | | `--xh-date-picker-control-gap` | `control` | `gap` | `xh-field-chrome` | `--xh-_date-picker-gap` | date-picker 的 control 部件 gap 覆盖槽。 | | `--xh-date-picker-control-h` | `control` | `block-size`
`min-block-size` | `has([data-xh-field-input][data-xh-field-layout='multi-tag'])`
`has([data-xh-field-input][data-xh-field-layout='single-line'])`
`has([data-xh-field-input][data-xh-field-layout='textarea'])`
`xh-field-chrome`
`xh-field-input`
`xh-field-layout=multi-tag`
`xh-field-layout=single-line`
`xh-field-layout=textarea` | `--xh-_date-picker-control-h` | date-picker 的 control 部件 block-size、min-block-size 覆盖槽。 | | `--xh-date-picker-control-min-w` | `control`
`root` | `min-inline-size` | `default`
`xh-field-chrome` | `--xh-control-min-w` | date-picker 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-date-picker-control-px` | `control` | `padding-inline` | `xh-field-chrome` | `--xh-_date-picker-control-px` | date-picker 的 control 部件 padding-inline 覆盖槽。 | | `--xh-date-picker-control-radius` | `control` | `border-radius` | `xh-field-chrome` | `--xh-shape-control` | date-picker 的 control 部件 border-radius 覆盖槽。 | | `--xh-date-picker-control-shadow` | `control` | `box-shadow` | `xh-field-chrome` | `none` | date-picker 的 control 部件 box-shadow 覆盖槽。 | | `--xh-date-picker-control-w` | `root` | `inline-size`
`min-inline-size` | `default` | `--xh-control-w` | date-picker 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-date-picker-font-size` | `segment-group` | `font-size` | `default` | `--xh-_date-picker-font-size` | date-picker 的 segment-group 部件 font-size 覆盖槽。 | | `--xh-date-picker-gap` | `root` | `gap` | `default` | `--xh-space-1` | date-picker 的 root 部件 gap 覆盖槽。 | | `--xh-date-picker-icon-size` | `control`
`positioner`
`root` | `--xh-icon-size` | `default`
`is([data-part='root'], [data-part='positioner'])`
`size=lg`
`size=sm`
`xh-field-chrome` | `--xh-_field-size-glyph-size`
`--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | date-picker 的 control、positioner、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-date-picker-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | date-picker 的 label 部件 color 覆盖槽。 | | `--xh-date-picker-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | date-picker 的 label 部件 color 覆盖槽。 | | `--xh-date-picker-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | date-picker 的 label 部件 font-size 覆盖槽。 | | `--xh-date-picker-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | date-picker 的 label 部件 font-weight 覆盖槽。 | | `--xh-date-picker-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | date-picker 的 positioner 部件 z-index 覆盖槽。 | | `--xh-date-picker-literal-fg` | `segment-group` | `color` | `not([data-scope])` | `--xh-fg-subtle` | date-picker 的 segment-group 部件 color 覆盖槽。 | | `--xh-date-picker-max-h` | `content` | `max-block-size` | `default` | `--xh-viewport-h-lg` | date-picker 的 content 部件 max-block-size 覆盖槽。 | | `--xh-date-picker-panel-divider` | `calendar` | `border-block-start`
`border-inline-start` | `@media (min-width: 768px)`
`default` | `--xh-material-frosted-separator` | date-picker 的 calendar 部件 border-block-start、border-inline-start 覆盖槽。 | | `--xh-date-picker-panel-gap` | `calendar`
`preset-group` | `padding-block-start`
`padding-inline-start` | `@media (min-width: 768px)`
`default` | `--xh-space-3` | date-picker 的 calendar、preset-group 部件 padding-block-start、padding-inline-start 覆盖槽。 | | `--xh-date-picker-preset-bg-hover` | `preset` | `background-color` | `disabled`
`error`
`highlighted`
`hover`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle` | date-picker 的 preset 部件 background-color 覆盖槽。 | | `--xh-date-picker-preset-bg-pressed` | `preset` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle-hover` | date-picker 的 preset 部件 background-color 覆盖槽。 | | `--xh-date-picker-preset-check-fg` | `preset` | `background-color` | `default` | `--xh-_date-picker-check-fg` | date-picker 的 preset 部件 background-color 覆盖槽。 | | `--xh-date-picker-preset-check-size` | `preset` | `block-size`
`inline-size`
`padding-inline-end` | `default` | `--xh-glyph-size-sm` | date-picker 的 preset 部件 block-size、inline-size、padding-inline-end 覆盖槽。 | | `--xh-date-picker-preset-fg-disabled` | `preset` | `background-color`
`color` | `default`
`disabled` | `--xh-fg-disabled` | date-picker 的 preset 部件 background-color、color 覆盖槽。 | | `--xh-date-picker-preset-fg-selected` | `preset` | `color` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-fg-default` | date-picker 的 preset 部件 color 覆盖槽。 | | `--xh-date-picker-preset-group-gap` | `preset-group` | `gap` | `default` | `--xh-list-option-gap` | date-picker 的 preset-group 部件 gap 覆盖槽。 | | `--xh-date-picker-preset-group-h` | `preset-group` | `max-block-size` | `default` | `--xh-viewport-h-lg` | date-picker 的 preset-group 部件 max-block-size 覆盖槽。 | | `--xh-date-picker-preset-group-padding` | `preset-group` | `padding` | `default` | `--xh-space-1` | date-picker 的 preset-group 部件 padding 覆盖槽。 | | `--xh-date-picker-preset-px` | `preset` | `inset-inline-end`
`padding-inline`
`padding-inline-end` | `default` | `--xh-space-3` | date-picker 的 preset 部件 inset-inline-end、padding-inline、padding-inline-end 覆盖槽。 | | `--xh-date-picker-preset-py` | `preset` | `padding-block` | `default` | `--xh-space-1` | date-picker 的 preset 部件 padding-block 覆盖槽。 | | `--xh-date-picker-preset-radius` | `preset` | `border-radius` | `default` | `--xh-shape-control` | date-picker 的 preset 部件 border-radius 覆盖槽。 | | `--xh-date-picker-time-column-gap` | `time-column` | `gap` | `default` | `0` | date-picker 的 time-column 部件 gap 覆盖槽。 | | `--xh-date-picker-time-column-h` | `time-column` | `block-size` | `default` | `--xh-viewport-h-md` | date-picker 的 time-column 部件 block-size 覆盖槽。 | | `--xh-date-picker-time-column-min-w` | `time-column` | `min-inline-size` | `default` | `--xh-overlay-column-min-w` | date-picker 的 time-column 部件 min-inline-size 覆盖槽。 | | `--xh-date-picker-time-column-min-w-mobile` | `time-column` | `min-inline-size` | `@media not all and (min-width: 768px)` | `2.75rem` | date-picker 的 time-column 部件 min-inline-size 覆盖槽。 | | `--xh-date-picker-time-column-offset` | `time-column` | `margin-block-start` | `default` | `--xh-control-h-sm` | date-picker 的 time-column 部件 margin-block-start 覆盖槽。 | | `--xh-date-picker-time-column-padding` | `time-column` | `padding-block` | `default` | `--xh-space-1` | date-picker 的 time-column 部件 padding-block 覆盖槽。 | | `--xh-date-picker-time-column-px` | `time-column` | `padding-inline` | `default` | `0` | date-picker 的 time-column 部件 padding-inline 覆盖槽。 | | `--xh-date-picker-time-column-px-mobile` | `time-column` | `padding-inline` | `@media not all and (min-width: 768px)` | `0` | date-picker 的 time-column 部件 padding-inline 覆盖槽。 | | `--xh-date-picker-time-item-bg-hover` | `time-item` | `background-color` | `disabled`
`error`
`highlighted`
`hover`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle` | date-picker 的 time-item 部件 background-color 覆盖槽。 | | `--xh-date-picker-time-item-bg-pressed` | `time-item` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle-hover` | date-picker 的 time-item 部件 background-color 覆盖槽。 | | `--xh-date-picker-time-item-check-fg` | `time-item` | `background-color` | `default` | `--xh-_date-picker-check-fg` | date-picker 的 time-item 部件 background-color 覆盖槽。 | | `--xh-date-picker-time-item-check-size` | `time-item` | `block-size`
`inline-size`
`inset-inline-end`
`padding-inline` | `@media not all and (min-width: 768px)`
`default` | `--xh-_date-picker-time-item-check-size` | date-picker 的 time-item 部件 block-size、inline-size、inset-inline-end、padding-inline 覆盖槽。 | | `--xh-date-picker-time-item-fg` | `time-item` | `color` | `default`
`disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-material-frosted-fg` | date-picker 的 time-item 部件 color 覆盖槽。 | | `--xh-date-picker-time-item-fg-selected` | `time-item` | `color` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-date-picker-time-item-fg` | date-picker 的 time-item 部件 color 覆盖槽。 | | `--xh-date-picker-time-item-font-weight-selected` | `time-item` | `font-weight` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-font-weight-regular` | date-picker 的 time-item 部件 font-weight 覆盖槽。 | | `--xh-date-picker-time-item-h` | `time-item` | `block-size` | `default` | `--xh-control-h-sm` | date-picker 的 time-item 部件 block-size 覆盖槽。 | | `--xh-date-picker-time-item-px` | `time-item` | `inset-inline-end`
`padding-inline` | `default` | `--xh-space-0_5` | date-picker 的 time-item 部件 inset-inline-end、padding-inline 覆盖槽。 | | `--xh-date-picker-time-item-py` | `time-item` | `padding-block` | `default` | `0` | date-picker 的 time-item 部件 padding-block 覆盖槽。 | | `--xh-date-picker-time-item-radius` | `time-item` | `border-radius` | `default` | `--xh-shape-control` | date-picker 的 time-item 部件 border-radius 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(锚定列表)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-overlay-slide-in` · `xh-overlay-slide-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`opacity` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 皮肤按视口分档:`min-width: 768px`。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/date-range-picker # DateRangePicker 日期范围选择器 将起止两组可键入的分段日期框、范围分隔符、日历触发器和范围日历浮层组合成一个字段。 ## 用法 输入或选择起止日期 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="date-range-picker"`:`root` · `label` · **`control`** · `segment-group` · `range-separator` · `trigger` · `clear-trigger` · `positioner` · **`content`** · `preset-group` · `preset` · **`calendar`** ## 示例 ### 两页并排 起止常跨月时设置 visibleCount=2,两页一起翻 ```vue ``` ```html

区间:(未选)

``` ### 快捷选项 常用区间一键写入两端 ```vue ``` ```html
``` ### 不可用日期 周末不可选,区间允许跨过不可用的日期 ```vue ``` ```html
``` ### 周期区间 granularity 决定两组输入行铺设哪几段、浮层铺设哪一档格子 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 用户需要选择一段起止日期:报表区间、入住与退房、有效期。 - 需要先查看月份分布再确定起止,或直接键入两端。 ### 何时不用 - 只选一天或几个不连续的日期时,使用[日期选择器](./date-picker)。 - 不需要输入行、只在页面上放一张日历选择区间时,使用[日历范围选择器](./calendar-range-picker)。 - 只需要时间段时,使用[时间范围选择器](./time-range-picker)。 ### 特性 - 值始终为区间两端 `[start, end]`,按位存放:只填了终点时是 `['', 终点]`,受控回写按同一下标对应。 - 起止各一组段位,`range-separator` 隔在中间;方向键换段不跨组,`name` 与 `endName` 各自决定两份隐藏输入参不参与提交。 - 浮层内是[日历范围选择器](./calendar-range-picker):先选起点再选终点,两端都落定后才写值并收起浮层;支持按住拖选与拖动已选区间的一端。 - `granularity` 支持 day / week / month / quarter / year,输入段、网格和周期边界一起切换。 - `min`、`max`、`isDateUnavailable`(第二个参数是当前起点)与 `allowsNonContiguousRanges` 一并转给日历。 - `presets` 提供“近 7 天”“本月”等整段快捷项,值使用 ISO 8601 的区间写法拼接两端。 - 终点早于起点、任一端越界或不可用时整个字段标为不合法,也可以用 `invalid` 显式声明。 - 输入值、展开状态和聚焦日期均可受控;点击输入行可以继续逐段键入,点击日历图标则把焦点送入日历。 ### 组合 - 浮层内嵌[日历范围选择器](./calendar-range-picker),翻月、层级切换、拖选与键盘导航由它负责。 - 输入行内嵌两组[日期字段](./date-field)的段位,逐段键入与加减由它负责。 ### 最佳实践 - 使用明确的字段标签,两组段位各自报告“开始日期”“结束日期”。 - 起止段组之间必须渲染 `range-separator`,不依赖空白区分两端。 - 起止常跨月时显式传 `visibleCount="2"`,并排查看两页。 - 需要统一查询值时,读取 `api.periodValue` 得到周期首尾与回显键。 - 常用区间优先提供快捷项,日期在 computed / memo 中计算后再传入。 ### 反模式 - 用两个日期选择器拼接一个区间:两端之间没有轨道、没有拖选,也不校验先后顺序。 - 未经说明就预先选好一段。 - 让浮层遮挡当前输入值。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDateRangePickerCalendar` `XhDateRangePickerCell` `XhDateRangePickerCellTrigger` `XhDateRangePickerClearTrigger` `XhDateRangePickerContent` `XhDateRangePickerControl` `XhDateRangePickerGrid` `XhDateRangePickerGridBody` `XhDateRangePickerGridHead` `XhDateRangePickerHeader` `XhDateRangePickerHeading` `XhDateRangePickerHeadingMonthTrigger` `XhDateRangePickerHeadingYearTrigger` `XhDateRangePickerHiddenInput` `XhDateRangePickerLabel` `XhDateRangePickerNextTrigger` `XhDateRangePickerNextYearTrigger` `XhDateRangePickerPositioner` `XhDateRangePickerPreset` `XhDateRangePickerPresetGroup` `XhDateRangePickerPrevTrigger` `XhDateRangePickerPrevYearTrigger` `XhDateRangePickerRangeSeparator` `XhDateRangePickerRoot` `XhDateRangePickerSegment` `XhDateRangePickerSegmentGroup` `XhDateRangePickerTrigger` `XhDateRangePickerWeekDay` `XhDateRangePickerWeekNumber` `XhDateRangePickerWeekRow` | | 组合式函数 | `useDateRangePicker` | | 状态机 | `dateRangePickerMachine` | | 皮肤 | `@xihan-ui/styles/date-range-picker.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string[]` | | 区间两端,ISO 串。提供即受控:读取直取 prop,写入只发 onValueChange 不落内部值。 按位存放,空缺的一端为空串。 | | `defaultValue` | `string[]` | | | | `open` | `boolean` | | 展开态。提供即受控:内部不再自行修改,只发 onOpenChange。 | | `defaultOpen` | `boolean` | | | | `min` | `string` | | 可选范围下界(含当天),ISO 串。日历与分段输入共用这一条。 | | `max` | `string` | | 可选范围上界(含当天),ISO 串。 | | `locale` | `string` | | 决定周首日、月份文案与段位先后(zh-CN 年月日、en-US 月日年)。 未提供时按宿主语言,宿主也没有时按 en-US。 | | `timeZone` | `string` | | 判定今天与格式化文案使用的时区,默认取宿主本地时区。 | | `isDateUnavailable` | `(value: string, anchor: string \| null) => boolean` | | 不可用判定,接收 ISO 串。界外与判定为真的日期同等处理。 第二个参数是区间选到一半时的起点,其余时候为 null。 | | `allowsNonContiguousRanges` | `boolean` | | 区间允许跨过不可用的日期,默认关闭;关闭时落下起点之后只能选到两侧最近的不可用日为止。 | | `disabled` | `boolean` | | 整个控件禁用:trigger 为原生 disabled,段位退出 Tab 序列,日历格子全部为 aria-disabled。 | | `readOnly` | `boolean` | | 只读:浮层照常展开、日历照常翻月浏览,但选中值不可修改。 | | `invalid` | `boolean` | | 校验失败:段位报告 aria-invalid,各角色节点带 data-invalid。 未提供时也会自行判定:任一端越界,或终点早于起点。 | | `required` | `boolean` | | 必填标注,写入每一段的 aria-required。 | | `name` | `string` | | 起点隐藏输入的表单字段名;提供后才带 name,ISO 串随表单一并提交。 | | `endName` | `string` | | 终点隐藏输入的表单字段名;未提供时终点不参与提交。 | | `granularity` | `CalendarGranularity` | | 选择粒度。输入行与周期网格都由它决定。 | | `activeView` | `CalendarView` | | 面板当前所在的层级。提供即受控;未提供时跟随 granularity,每次展开都回到目标粒度。 点击标题中的年 / 月会修改它。 | | `segments` | `DateSegmentSet` | | 输入行铺设的段。未提供时按 granularity 推导:按周为「2026-33」、按月为「2026-05」、 按季度为「2026-Q2」、按年为「2026」,按天则按 locale 排列年月日。 | | `presets` | `DateRangePickerPreset[]` | | 快捷选项(「近 7 天」「本月」等)。提供后浮层中多出一列,点击即整份写入两端。 日期需计算后传入:连接层每帧求值,把 `today()` 放进渲染期会跨零点得出两个结果。 不是恰好两端、落在 min / max 之外或被 isDateUnavailable 判定不可用的选项自动不可按下。 | | `visibleCount` | `number` | | 展示的连续日历面板数;默认 1。起止常跨月,并排两页时显式提供 2。 | | `fixedWeeks` | `boolean` | | 日历恒渲染六行,默认开启。关闭后网格按当月实际周数收缩,翻页时浮层高度随之变化。 | | `defaultFocusedValue` | `string` | | 初始聚焦日,ISO 串;同时决定展开时先落在哪一页。 未提供时回退为起点,再回退为今天。表单重置回到该值。 | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定输入行的描边与底色使用方式。默认 outline。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定聚焦与选中强调使用哪族颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,输入行与浮层中的日历格一并换档。 | | `placement` | `Placement` | | | | `dir` | `Direction` | | 文字方向,默认 ltr。只改写浮层在行内轴上 start 与 end 的落点。 | | `offset` | `number` | | | | `translations` | `Partial` | | | | `closeOnSelect` | `boolean` | | 选完即收起,默认 true。两端都落定才视为选完。 | | `onValueChange` | `(details: DateRangePickerValueChangeDetails) => void` | | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 | | `onOpenChange` | `(details: DateRangePickerOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `onFocusedValueChange` | `(details: DateRangePickerFocusChangeDetails) => void` | | 聚焦日变化(方向键、翻月、展开、段位输入都会发出)。 网格由外部渲染,不监听该事件时日历不会换月。 | | `onActiveViewChange` | `(details: CalendarViewChangeDetails) => void` | | 面板所在层级变化(点击标题向上、点击格子向下都会发出);受控时是唯一出口。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `DateRangePickerValueChangeDetails` | 区间两端变化;detail 为 `{ value: string[] }`,只填终点时为 `['', end]` | | `open-change` | `DateRangePickerOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | | `focused-value-change` | `DateRangePickerFocusChangeDetails` | 聚焦日变化(展示月可能随之变化);detail 为 `{ focusedValue: string }`,作者据此重绘网格 | | `active-view-change` | `CalendarViewChangeDetails` | 切换到另一层级(点击标题向上、点击格子向下);detail 为 `{ activeView: 'day'\|'week'\|'month'\|'quarter'\|'year' }`,作者据此重绘网格 | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhDateRangePickerPreset` | `default` | — | 条目内容;未写时使用数据中的 label。 | | `XhDateRangePickerPresetGroup` | `default` | `DateRangePickerPresetsSlotProps` | 自行铺设条目;未写时按 presets 数据自动铺设,两者产出的 DOM 一致。 | | `XhDateRangePickerRoot` | `default` | `DateRangePickerRootSlotProps` | | | `XhDateRangePickerSegment` | `default` | `DateRangePickerSegmentSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDateRangePickerCalendar` | `index` | `number \| string` | | 并排的第几张面板,默认 0。写在这里,面板内的标题、网格与格子就不必各写一遍。 | | `XhDateRangePickerCell` | `value` | `string` | 是 | ISO 日期串。 | | `XhDateRangePickerCell` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。同一天会同时出现在两个面板中 (8 月末的几天也铺在 9 月的首行),是否为本月只有连同面板一起看才能判定。 | | `XhDateRangePickerGrid` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDateRangePickerHeading` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDateRangePickerHeadingMonthTrigger` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDateRangePickerHeadingYearTrigger` | `index` | `number \| string` | | 属于第几个面板;未写时跟随所在的日历。 | | `XhDateRangePickerHiddenInput` | `index` | `number \| string` | | 写在分段容器外面时用它指明属于哪一端;写在容器内时不必提供,跟随容器。 | | `XhDateRangePickerPositioner` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhDateRangePickerPreset` | `value` | `string` | 是 | 该条目的身份,与 presets 数据中的 value 逐字对应。 | | `XhDateRangePickerPresetGroup` | `children` | `SlotChildren` | | 自行铺设条目;未写时按 presets 数据自动铺设,两者产出的 DOM 一致。 | | `XhDateRangePickerRoot` | `children` | `SlotChildren` | | | | `XhDateRangePickerSegment` | `index` | `number \| string` | | 段位下标,兼收字符串。 | | `XhDateRangePickerSegment` | `segment` | `DateSegmentType` | | 按段名声明该格。段集中没有该段时它收起;与 index 二选一,两个都写时按段名计算。 | | `XhDateRangePickerSegment` | `children` | `SlotChildren` | | | | `XhDateRangePickerSegmentGroup` | `index` | `number \| string` | | 组号:0 起点、1 终点,兼收字符串。 | | `XhDateRangePickerWeekDay` | `value` | `number \| string` | 是 | 列序 0-6,兼收字符串。 | | `XhDateRangePickerWeekNumber` | `value` | `string` | 是 | 该行行首那一天的 ISO 串。 | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `control` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | | `preset` | 'checked' \| 'unchecked' | | `calendar` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `VALUE.SET` · `VALUE.CLEAR` · `FOCUSED.SET` · `VIEW.SET` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` · `closesOnSelect` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `value` | `string[]` | 区间两端,ISO 串;按位存放,空缺的一端为空串。 | | `start` | `string \| null` | 起点;未填时为 null。 | | `end` | `string \| null` | 终点;未填时为 null。 | | `periodValue` | `CalendarPeriodValue \| null` | 两端都落定时的规范化周期值;缺少一端时为 null。 | | `focusedValue` | `string` | 生效聚焦日(三路收口后的结果),恒非空。日历展示哪个月由它决定。 | | `granularity` | `CalendarGranularity` | 作者选择的粒度。 | | `activeView` | `CalendarView` | 面板当前所在的层级。 | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | 校验失败:作者标记的、任一端越界,或终点早于起点。 | | `canClear` | `boolean` | 清空按钮当前是否可按。 | | `setOpen` | `(next: boolean) => void` | | | `setValue` | `(next: string[]) => void` | | | `clear` | `() => void` | | | `setActiveView` | `(next: CalendarView) => void` | 直接切换到某一层级。 | | `presets` | `readonly DateRangePickerPresetState[]` | 快捷选项逐条的状态,数据顺序。未提供 presets 时为空数组。 | | `calendar` | `CalendarRangePickerApi` | 内嵌日历:选区间、翻月、键盘导航都在它身上。 | | `field` | `DateRangePickerFieldApi` | 起点分段输入。 | | `fieldEnd` | `DateRangePickerFieldApi` | 终点分段输入。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['element']` | | | `getControlProps` | `() => T['element']` | | | `getSegmentGroupProps` | `(props?: DateRangePickerSegmentGroupProps) => T['element']` | role=group 的分段容器,段位挂在其中。index 选择起止两组,不传即起点。 | | `getRangeSeparatorProps` | `() => T['element']` | 起止输入之间的视觉分隔。 | | `getTriggerProps` | `() => T['button']` | | | `getClearTriggerProps` | `() => T['button']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getPresetGroupProps` | `() => T['element']` | 快捷选项列(role=listbox);未提供 presets 时带 hidden。 | | `getPresetProps` | `(props: DateRangePickerPresetProps) => T['element']` | 一条快捷选项(role=option):点击把整段区间写入两端。 | | `getCalendarProps` | `() => T['element']` | 内嵌日历的挂载点,同时充当日历的根节点。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/#kbd_label) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in trigger, closed | 展开日历浮层,焦点落到当前聚焦日那一格 | | `Enter` / `Space` | focus in trigger, open | 收起浮层,焦点回到 trigger | | `Escape` | open | 收起浮层并把焦点还给展开前那个控件(通常是 trigger),两端不变;区间挑到一半时先撤掉起点 | | `Tab` / `Shift+Tab` | open | 不拦按键:焦点按 Tab 序列自然离开,浮层随即收起且不抢回焦点 | | `Enter` / `Space` | open, focus in grid | 先落起点再落终点(由日历完成);closeOnSelect 时两端都落定才收起浮层 | | `ArrowUp` / `ArrowDown` / `Home` / `End` | open, focus in 快捷选项列 | 在快捷选项之间移动焦点,到头回绕;不写值 | | `Enter` / `Space` | open, focus in 某条快捷选项 | 把这条快捷选项的两端整份写进去;closeOnSelect 时收起浮层 | | `Alt+ArrowDown` | focus in 某一段, closed, not disabled | 展开浮层并把焦点移入;触发按钮是可选部件,键盘入口不能只挂在它上面 | | `Enter` | focus in 某一段, open | 收起浮层。段位里敲出来的值不触发「选完即收」(那时人还在打字),这是那条路的收口手势 | | `Enter` / `Space` | held on trigger(not disabled)、clear-trigger(可清)或 preset(open, not disabled/readOnly, 该条可按) | 按住期间该部件投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,浮层收起时一并撤下。日历里的部件由 calendar-range-picker 自己投影 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `segment-group` | `aria-disabled` | 'true' \| 'false' | | `segment-group` | `aria-label` | label.endDate \| label.startDate | | `segment-group` | `role` | 'group' | | `range-separator` | `aria-hidden` | 'true' | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `trigger` | `aria-haspopup` | 'dialog' | | `trigger` | `aria-labelledby` | `label` 部件的 id | | `clear-trigger` | `aria-label` | label.clearTrigger | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-labelledby` | `label` 部件的 id | | `content` | `aria-modal` | 'false' | | `content` | `role` | 'dialog' | | `preset-group` | `aria-disabled` | 'true' \| 'false' | | `preset-group` | `aria-label` | label.presets | | `preset-group` | `aria-multiselectable` | 'false' | | `preset-group` | `aria-orientation` | 'vertical' | | `preset-group` | `role` | 'listbox' | | `preset` | `aria-disabled` | 'true' \| 'false' | | `preset` | `aria-selected` | 'true' \| 'false' | | `preset` | `role` | 'option' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/date-range-picker.css` 使用 `[data-scope="date-range-picker"][data-part="root"]` 部件选择器,位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `label` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-state` | 'open' \| 'closed' | | `control` | `data-variant` | props.variant | | `control` | `data-xh-field-chrome` | '' | | `control` | `data-xh-field-size` | props.size | | `segment-group` | `data-complete` | ''(条件成立时才出现) | | `segment-group` | `data-disabled` | ''(条件成立时才出现) | | `segment-group` | `data-empty` | ''(条件成立时才出现) | | `segment-group` | `data-index` | String(index) | | `segment-group` | `data-invalid` | ''(条件成立时才出现) | | `segment-group` | `data-out-of-range` | ''(条件成立时才出现) | | `segment-group` | `data-readonly` | ''(条件成立时才出现) | | `trigger` | `data-disabled` | ''(条件成立时才出现) | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'field-inset' | | `trigger` | `data-xh-action-size` | props.size | | `trigger` | `data-xh-action-variant` | 'ghost' | | `clear-trigger` | `data-pressed` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-control` | '' | | `clear-trigger` | `data-xh-action-display` | 'has-value' | | `clear-trigger` | `data-xh-action-has-value` | ''(条件成立时才出现) | | `clear-trigger` | `data-xh-action-profile` | 'field-inset' | | `clear-trigger` | `data-xh-action-size` | props.size | | `clear-trigger` | `data-xh-action-variant` | 'ghost' | | `positioner` | `data-hidden` | ''(条件成立时才出现) | | `positioner` | `data-placement` | 定位引擎算出的实际落位 | | `positioner` | `data-positioned` | ''(条件成立时才出现) | | `positioner` | `data-size` | props.size | | `positioner` | `data-state` | 'open' \| 'closed' | | `positioner` | `data-tone` | props.tone | | `positioner` | `data-variant` | props.variant | | `content` | `data-placement` | 定位引擎算出的实际落位 | | `content` | `data-state` | 'open' \| 'closed' | | `preset` | `data-disabled` | ''(条件成立时才出现) | | `preset` | `data-pressed` | ''(条件成立时才出现) | | `preset` | `data-state` | 'checked' \| 'unchecked' | | `preset` | `data-value` | v | | `preset` | `data-xh-collection-context` | 'overlay' | | `preset` | `data-xh-collection-item` | '' | | `preset` | `data-xh-collection-size` | props.size | | `calendar` | `data-disabled` | ''(条件成立时才出现) | | `calendar` | `data-readonly` | ''(条件成立时才出现) | | `calendar` | `data-state` | 'open' \| 'closed' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-date-range-picker-action-bg` | `clear-trigger`
`trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | date-range-picker 的 clear-trigger、trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-date-range-picker-action-bg-active` | `clear-trigger`
`trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | date-range-picker 的 clear-trigger、trigger 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-action-bg-hover` | `clear-trigger`
`trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])`
`state=open`
`xh-ink-surface` | `--xh-_action-variant-bg-hover` | date-range-picker 的 clear-trigger、trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-date-range-picker-action-fg` | `clear-trigger`
`trigger` | `color` | `default` | `--xh-fg-muted` | date-range-picker 的 clear-trigger、trigger 部件 color 覆盖槽。 | | `--xh-date-range-picker-action-fg-hover` | `clear-trigger`
`trigger` | `color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])`
`state=open` | `--xh-fg-default` | date-range-picker 的 clear-trigger、trigger 部件 color 覆盖槽。 | | `--xh-date-range-picker-action-font-size` | `clear-trigger`
`trigger` | `font-size` | `default` | `--xh-text-secondary-size` | date-range-picker 的 clear-trigger、trigger 部件 font-size 覆盖槽。 | | `--xh-date-range-picker-action-radius` | `clear-trigger`
`trigger` | `border-radius` | `default` | `--xh-shape-inset` | date-range-picker 的 clear-trigger、trigger 部件 border-radius 覆盖槽。 | | `--xh-date-range-picker-action-size` | `clear-trigger`
`trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | date-range-picker 的 clear-trigger、trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | | `--xh-date-range-picker-calendar-gap` | `calendar` | `gap` | `default` | `--xh-space-2` | date-range-picker 的 calendar 部件 gap 覆盖槽。 | | `--xh-date-range-picker-column-divider` | `preset-group` | `border-block-end`
`border-inline-end` | `@media (min-width: 768px)`
`default` | `--xh-material-frosted-separator` | date-range-picker 的 preset-group 部件 border-block-end、border-inline-end 覆盖槽。 | | `--xh-date-range-picker-content-bg` | `content` | `background` | `default` | `--xh-bg-surface` | date-range-picker 的 content 部件 background 覆盖槽。 | | `--xh-date-range-picker-content-border` | `content` | `border` | `default` | `--xh-border-default` | date-range-picker 的 content 部件 border 覆盖槽。 | | `--xh-date-range-picker-content-fg` | `content` | `color` | `default` | `--xh-fg-default` | date-range-picker 的 content 部件 color 覆盖槽。 | | `--xh-date-range-picker-content-px` | `content` | `padding-inline` | `default` | `--xh-space-2` | date-range-picker 的 content 部件 padding-inline 覆盖槽。 | | `--xh-date-range-picker-content-py` | `content` | `padding-block` | `default` | `--xh-space-2` | date-range-picker 的 content 部件 padding-block 覆盖槽。 | | `--xh-date-range-picker-content-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | date-range-picker 的 content 部件 border-radius 覆盖槽。 | | `--xh-date-range-picker-content-shadow` | `content` | `box-shadow` | `default` | `--xh-elevation-floating` | date-range-picker 的 content 部件 box-shadow 覆盖槽。 | | `--xh-date-range-picker-control-bg` | `control` | `background-color` | `xh-field-chrome` | `--xh-_field-variant-bg-rest` | date-range-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-control-bg-disabled` | `control` | `background-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-bg-disabled` | date-range-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-control-bg-hover` | `control` | `background-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-hover` | date-range-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-control-bg-readonly` | `control` | `background-color` | `readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-read-only` | date-range-picker 的 control 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-control-border` | `control` | `border` | `xh-field-chrome` | `--xh-_field-variant-border-rest` | date-range-picker 的 control 部件 border 覆盖槽。 | | `--xh-date-range-picker-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])`
`xh-field-chrome` | `--xh-_field-variant-border-focus` | date-range-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-date-range-picker-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-border-hover` | date-range-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-date-range-picker-control-border-invalid` | `control` | `border-color` | `invalid`
`xh-field-chrome` | `--xh-_field-variant-border-invalid` | date-range-picker 的 control 部件 border-color 覆盖槽。 | | `--xh-date-range-picker-control-fg` | `control` | `color` | `xh-field-chrome` | `--xh-fg-default` | date-range-picker 的 control 部件 color 覆盖槽。 | | `--xh-date-range-picker-control-gap` | `control`
`range-separator` | `gap`
`margin-inline` | `default`
`xh-field-chrome` | `--xh-_date-range-picker-gap` | date-range-picker 的 control、range-separator 部件 gap、margin-inline 覆盖槽。 | | `--xh-date-range-picker-control-h` | `control` | `block-size`
`min-block-size` | `has([data-xh-field-input][data-xh-field-layout='multi-tag'])`
`has([data-xh-field-input][data-xh-field-layout='single-line'])`
`has([data-xh-field-input][data-xh-field-layout='textarea'])`
`xh-field-chrome`
`xh-field-input`
`xh-field-layout=multi-tag`
`xh-field-layout=single-line`
`xh-field-layout=textarea` | `--xh-_date-range-picker-control-h` | date-range-picker 的 control 部件 block-size、min-block-size 覆盖槽。 | | `--xh-date-range-picker-control-min-w` | `control`
`root` | `min-inline-size` | `default`
`xh-field-chrome` | `--xh-control-min-w` | date-range-picker 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-date-range-picker-control-px` | `control` | `padding-inline` | `xh-field-chrome` | `--xh-_date-range-picker-control-px` | date-range-picker 的 control 部件 padding-inline 覆盖槽。 | | `--xh-date-range-picker-control-radius` | `control` | `border-radius` | `xh-field-chrome` | `--xh-shape-control` | date-range-picker 的 control 部件 border-radius 覆盖槽。 | | `--xh-date-range-picker-control-shadow` | `control` | `box-shadow` | `xh-field-chrome` | `none` | date-range-picker 的 control 部件 box-shadow 覆盖槽。 | | `--xh-date-range-picker-control-w` | `root` | `inline-size` | `default` | `max-content` | date-range-picker 的 root 部件 inline-size 覆盖槽。 | | `--xh-date-range-picker-font-size` | `segment-group` | `font-size` | `default` | `--xh-_date-range-picker-font-size` | date-range-picker 的 segment-group 部件 font-size 覆盖槽。 | | `--xh-date-range-picker-gap` | `root` | `gap` | `default` | `--xh-space-1` | date-range-picker 的 root 部件 gap 覆盖槽。 | | `--xh-date-range-picker-icon-size` | `control`
`positioner`
`root` | `--xh-icon-size` | `default`
`is([data-part='root'], [data-part='positioner'])`
`size=lg`
`size=sm`
`xh-field-chrome` | `--xh-_field-size-glyph-size`
`--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | date-range-picker 的 control、positioner、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-date-range-picker-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | date-range-picker 的 label 部件 color 覆盖槽。 | | `--xh-date-range-picker-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | date-range-picker 的 label 部件 color 覆盖槽。 | | `--xh-date-range-picker-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | date-range-picker 的 label 部件 font-size 覆盖槽。 | | `--xh-date-range-picker-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | date-range-picker 的 label 部件 font-weight 覆盖槽。 | | `--xh-date-range-picker-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | date-range-picker 的 positioner 部件 z-index 覆盖槽。 | | `--xh-date-range-picker-literal-fg` | `segment-group` | `color` | `not([data-scope])` | `--xh-fg-subtle` | date-range-picker 的 segment-group 部件 color 覆盖槽。 | | `--xh-date-range-picker-max-h` | `content` | `max-block-size` | `default` | `--xh-viewport-h-lg` | date-range-picker 的 content 部件 max-block-size 覆盖槽。 | | `--xh-date-range-picker-panel-divider` | `calendar` | `border-block-start`
`border-inline-start` | `@media (min-width: 768px)`
`default` | `--xh-material-frosted-separator` | date-range-picker 的 calendar 部件 border-block-start、border-inline-start 覆盖槽。 | | `--xh-date-range-picker-panel-gap` | `calendar`
`preset-group` | `padding-block-start`
`padding-inline-start` | `@media (min-width: 768px)`
`default` | `--xh-space-3` | date-range-picker 的 calendar、preset-group 部件 padding-block-start、padding-inline-start 覆盖槽。 | | `--xh-date-range-picker-preset-bg-hover` | `preset` | `background-color` | `disabled`
`error`
`highlighted`
`hover`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle` | date-range-picker 的 preset 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-preset-bg-pressed` | `preset` | `background-color` | `disabled`
`error`
`is(:active, [data-pressed])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-bg-subtle-hover` | date-range-picker 的 preset 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-preset-check-fg` | `preset` | `background-color` | `default` | `--xh-_date-range-picker-check-fg` | date-range-picker 的 preset 部件 background-color 覆盖槽。 | | `--xh-date-range-picker-preset-check-size` | `preset` | `block-size`
`inline-size`
`padding-inline-end` | `default` | `--xh-glyph-size-sm` | date-range-picker 的 preset 部件 block-size、inline-size、padding-inline-end 覆盖槽。 | | `--xh-date-range-picker-preset-fg-disabled` | `preset` | `background-color`
`color` | `default`
`disabled` | `--xh-fg-disabled` | date-range-picker 的 preset 部件 background-color、color 覆盖槽。 | | `--xh-date-range-picker-preset-fg-selected` | `preset` | `color` | `disabled`
`error`
`highlighted`
`hover`
`is(:active, [data-pressed])`
`is(:focus-visible, [data-highlighted])`
`is([aria-selected='true'], [data-selected])`
`not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])`
`pressed`
`selected`
`xh-collection-context=overlay` | `--xh-fg-default` | date-range-picker 的 preset 部件 color 覆盖槽。 | | `--xh-date-range-picker-preset-group-gap` | `preset-group` | `gap` | `default` | `--xh-list-option-gap` | date-range-picker 的 preset-group 部件 gap 覆盖槽。 | | `--xh-date-range-picker-preset-group-h` | `preset-group` | `max-block-size` | `default` | `--xh-viewport-h-lg` | date-range-picker 的 preset-group 部件 max-block-size 覆盖槽。 | | `--xh-date-range-picker-preset-group-padding` | `preset-group` | `padding` | `default` | `--xh-space-1` | date-range-picker 的 preset-group 部件 padding 覆盖槽。 | | `--xh-date-range-picker-preset-px` | `preset` | `inset-inline-end`
`padding-inline`
`padding-inline-end` | `default` | `--xh-space-3` | date-range-picker 的 preset 部件 inset-inline-end、padding-inline、padding-inline-end 覆盖槽。 | | `--xh-date-range-picker-preset-py` | `preset` | `padding-block` | `default` | `--xh-space-1` | date-range-picker 的 preset 部件 padding-block 覆盖槽。 | | `--xh-date-range-picker-preset-radius` | `preset` | `border-radius` | `default` | `--xh-shape-control` | date-range-picker 的 preset 部件 border-radius 覆盖槽。 | | `--xh-date-range-picker-range-separator-fg` | `range-separator` | `color` | `default` | `--xh-fg-subtle` | date-range-picker 的 range-separator 部件 color 覆盖槽。 | | `--xh-date-range-picker-range-separator-mx` | `range-separator` | `margin-inline` | `default` | `--xh-_date-range-picker-range-separator-mx` | date-range-picker 的 range-separator 部件 margin-inline 覆盖槽。 | | `--xh-date-range-picker-range-separator-px` | `range-separator` | `margin-inline`
`padding-inline` | `default` | `--xh-space-1` | date-range-picker 的 range-separator 部件 margin-inline、padding-inline 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(锚定列表)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-overlay-slide-in` · `xh-overlay-slide-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`opacity` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 皮肤按视口分档:`min-width: 768px`。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/descriptions # Descriptions 描述列表 成对的标签与值,按列排开。 ## 用法 标签与取值的配对依靠 dl / dt / dd 表达,组件只提供身份与排版;不传 columns 即每行一组 ```vue ``` ```html
订单号
XH-20260810-0042
下单时间
2026-08-10 09:31
支付方式
余额支付
``` ## 组件结构 加粗的是必需部件。 `data-scope="descriptions"`:**`root`** · `item` · `label` · `value` ## 示例 ### 列数 columns 决定每行排几组,一到六列;排版使用 CSS Grid,不使用表格 ```vue ``` ```html
姓名
张三
工号
A-1024
部门
技术部
岗位
前端工程师
入职
2024-03-01
座机
8021
``` ### 标签位置 placement 决定标签在上还是在左,不传即在上 ```vue ``` ```html

标签在上(默认)

订单号
XH-20260810-0042
下单时间
2026-08-10 09:31

标签在左

订单号
XH-20260810-0042
下单时间
2026-08-10 09:31
``` ### 外框 variant="outline" 绘制一圈描边,并在格与格之间补上网格线 ```vue ``` ```html
商品
机械键盘
单价
¥499.00
数量
2
小计
¥998.00
``` ### 尺寸 size 改变每格的内边距、组与组的间距与整体字号,不传 size 即默认档 ```vue ``` ```html
小 · 状态
已发货
小 · 承运商
顺丰速运
默认 · 状态
已发货
默认 · 承运商
顺丰速运
大 · 状态
已发货
大 · 承运商
顺丰速运
``` ### 跨列 一格写 span 横跨几列,上限是当前列数;长文本字段因此不必另开一份描述列表 ```vue ``` ```html
订单号
XH-20260810-0042
下单时间
2026-08-10 09:31
支付方式
余额支付
收货地址
浙江省杭州市余杭区文一西路 969 号
联系电话
138 0000 0000
备注
工作日 09:00–18:00 送达,到前电联。
``` ## 设计指引 ### 何时使用 - 详情页的属性列表:订单信息、设备参数、用户资料。 ### 何时不用 - 数据是多行同构的记录时,使用[表格](./table)。 - 只有一两对时,直接书写。 ### 特性 - 语义是 `dt` / `dd`,组件只提供身份与排版。 - `columns` 决定每行几组,不传时每行一组。 - 标签位置可以在值的上方或左侧;`variant="outline"` 提供外框与网格线。 - 每一格可以通过 `span` 横跨多列,上限是当前列数;窄档一行只放一组时忽略该值。 ### 组合 - 放入[卡片](./card)或[页头](./page-header)的页脚。 ### 最佳实践 - 值为空时写“—”,不留空白,避免用户无法区分没有值与未加载。 - 标签左置时给它们统一宽度,值才能对齐。 - 长文本字段用 `span` 占满整行,不为它另开一份描述列表。 ### 反模式 - 用它排版表格。 - 标签比值更长。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDescriptionsItem` `XhDescriptionsLabel` `XhDescriptionsRoot` `XhDescriptionsValue` | | 状态机 | 无,`connect` 直接由 props 算属性 | | 皮肤 | `@xihan-ui/styles/descriptions.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `columns` | `DescriptionsColumns` | | 每行放置几组,一到六列;未提供时每行一组。 | | `placement` | `DescriptionsPlacement` | | 标签的位置:top / left;未提供时标签在上。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `variant` | `ControlVariant` | | 形态:ghost 不画壳(默认),outline 绘制外框并在格与格之间补网格线,subtle 淡底。默认 ghost。 | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDescriptionsItem` | `as` | `ElementType` | | 每一格渲染为哪个标签,默认 div。 | | `XhDescriptionsItem` | `span` | `number` | | 该格横跨几列,未写即占一列;上限是根上的 columns。 | | `XhDescriptionsLabel` | `as` | `ElementType` | | 标签渲染为哪个标签,默认 dt。 | | `XhDescriptionsRoot` | `as` | `ElementType` | | 根渲染为哪个标签,默认 dl。 | | `XhDescriptionsValue` | `as` | `ElementType` | | 取值渲染为哪个标签,默认 dd。 | ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `getRootProps` | `() => T['element']` | | | `getItemProps` | `(props?: DescriptionsItemProps) => T['element']` | | | `getLabelProps` | `() => T['element']` | | | `getValueProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/descriptions.css` 使用 `[data-scope="descriptions"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-descriptions-bg` | `root` | `background` | `variant=outline`
`variant=subtle` | `--xh-bg-subtle`
`--xh-bg-surface` | descriptions 的 root 部件 background 覆盖槽。 | | `--xh-descriptions-border` | `root` | `border` | `variant=outline` | `--xh-border-default` | descriptions 的 root 部件 border 覆盖槽。 | | `--xh-descriptions-divider` | `item`
`root` | `border-block-start`
`border-inline-start` | `variant=outline` | `--xh-border-subtle` | descriptions 的 item、root 部件 border-block-start、border-inline-start 覆盖槽。 | | `--xh-descriptions-fg` | `root` | `color` | `default` | `--xh-fg-default` | descriptions 的 root 部件 color 覆盖槽。 | | `--xh-descriptions-font-size` | `root` | `font-size` | `default` | `--xh-_descriptions-font-size` | descriptions 的 root 部件 font-size 覆盖槽。 | | `--xh-descriptions-gap` | `root` | `gap` | `default` | `--xh-_descriptions-gap` | descriptions 的 root 部件 gap 覆盖槽。 | | `--xh-descriptions-item-px` | `item`
`root` | `padding-inline` | `variant=outline` | `--xh-_descriptions-px` | descriptions 的 item、root 部件 padding-inline 覆盖槽。 | | `--xh-descriptions-item-py` | `item`
`root` | `padding-block` | `variant=outline` | `--xh-_descriptions-py` | descriptions 的 item、root 部件 padding-block 覆盖槽。 | | `--xh-descriptions-label-fg` | `label` | `color` | `default` | `--xh-fg-muted` | descriptions 的 label 部件 color 覆盖槽。 | | `--xh-descriptions-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | descriptions 的 label 部件 font-weight 覆盖槽。 | | `--xh-descriptions-label-gap` | `item`
`root` | `column-gap` | `@media (min-width: 768px)`
`placement=left` | `--xh-_descriptions-label-gap` | descriptions 的 item、root 部件 column-gap 覆盖槽。 | | `--xh-descriptions-label-w` | `item`
`root` | `grid-template-columns` | `@media (min-width: 768px)`
`placement=left` | `--xh-_descriptions-label-w` | descriptions 的 item、root 部件 grid-template-columns 覆盖槽。 | | `--xh-descriptions-pair-gap` | `item` | `gap` | `default` | `--xh-_descriptions-pair-gap` | descriptions 的 item 部件 gap 覆盖槽。 | | `--xh-descriptions-radius` | `root` | `border-radius` | `variant=outline`
`variant=subtle` | `--xh-shape-surface` | descriptions 的 root 部件 border-radius 覆盖槽。 | | `--xh-descriptions-value-fg` | `value` | `color` | `default` | `--xh-fg-default` | descriptions 的 value 部件 color 覆盖槽。 | ### 动效 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### 响应式 皮肤按视口分档:`min-width: 1024px` · `min-width: 768px`。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/dialog # Dialog 对话框 浮在页面之上的一层,通常需要用户处理完成后才能回到页面。 ## 用法 不传 open 即为非受控;Esc 或点击遮罩关闭,关闭后焦点回到触发按钮 ```vue ``` ```html

确认发布

发布后这篇文档对所有人可见,之后仍可撤回。

``` ## 组件结构 加粗的是必需部件。 `data-scope="dialog"`:`trigger` · `backdrop` · `positioner` · **`content`** · `header` · `indicator` · `title` · `description` · `body` · `footer` · `close-trigger` ## 示例 ### 受控 传入 open 后由宿主决定,组件自身不再修改状态;Esc、点击遮罩、按关闭按钮都只回写 open ```vue ``` ```html
当前:收起

受控对话框

这里没有 trigger,开合完全由外面那颗按钮与 open 决定。

``` ### 警示对话框 role=alertdialog 交给读屏更强的语气;关闭 Esc 与点击遮罩后,只剩内部两个按钮可以离开 ```vue ``` ```html

删除后不可恢复

设备上的离线数据会一并清除,请确认这是你要的结果。

``` ### 尺寸 size 写为 content 的 data-size,只改变面板的最大宽度;三档各自一个对话框,打开后才可见宽窄差异 ```vue ``` ```html

sm 窄对话框

内边距与字号三档一致,只有宽度上限不同。

缺省对话框

内边距与字号三档一致,只有宽度上限不同。

lg 宽对话框

内边距与字号三档一致,只有宽度上限不同。

``` ### 头尾固定、正文滚动 header / body / footer 把面板切为三段:头与尾固定在原处,只有正文一段滚动 ```vue ``` ```html

服务条款

写了 body 那一段,面板自己封顶、正文自己滚,头尾不跟着走。

第 1 条 条款正文

第 2 条 条款正文

第 3 条 条款正文

第 4 条 条款正文

第 5 条 条款正文

第 6 条 条款正文

第 7 条 条款正文

第 8 条 条款正文

第 9 条 条款正文

第 10 条 条款正文

第 11 条 条款正文

第 12 条 条款正文

第 13 条 条款正文

第 14 条 条款正文

第 15 条 条款正文

第 16 条 条款正文

``` ### 异步确认 提交期间按钮显示加载,Esc 与点击遮罩两条出口一并封闭,落定之后才把 open 写回 false ```vue ``` ```html

归档项目

归档后项目转为只读,随时可以恢复。

未归档
``` ### 命令式确认框 一次函数调用把描述符推入表中并展开对话框;返回的对象随后可修改标题、正文与按钮状态,表中即当前所有实例 ```vue ``` ```html
  1. (还没有描述符)

``` ### 拖动标题栏移动窗口 指针按在标题上,沿 DOM 找到 content 部件,把累计位移写进它的 translate;入场动画使用 transform,两者互不覆盖 ```vue ``` ```html

拖住这一行挪窗口

位移是相对居中位置累计的,收起再打开会回到正中。

当前位移:0 / 0

``` ### 命令式服务 createDialogService 的 confirm 与单按钮预设:一行调用弹出,onOk 返回 Promise 时确认按钮自动 pending 并阻止关闭;多次调用排队依次弹出 ```vue ``` ## 设计指引 ### 何时使用 - 需要用户做出决定且不能忽略(确认删除、填写必要信息)。 - 一段独立的子任务,完成后回到原处。 ### 何时不用 - 只提示一条结果时,使用[轻提示](./toast)。 - 内容是页面主流程的一部分时,直接展开在页面内。 - 内容很长或是完整表单时,使用[抽屉](./drawer)或单独页面。 ### 特性 - `modal` 决定是否锁住下层:非模态不创建遮罩,页面仍可点击、聚焦和滚动;展开期间切换会同步更新这些约束。 - 焦点进入时落在 `initialFocus`,关闭后归还触发器。 - `closeOnEscape` 与 `closeOnInteractOutside` 可分别关闭,避免填写中的表单因误点外部而丢失。 - 内容区可以内部滚动,标题栏可以拖动移动窗口。Body 是模态滚动面:滚到头不带动页面,内容高度变化时保留稳定的滚动条空道。 - 面板走 M4 sheet 三件套(描边、不透明底、投影)。触发器与关闭按钮走 Action Control 家族配方:触发器为 text 档中性描边,关闭按钮为 icon 档 ghost 面,悬停与按下沿画布承载阶梯换底,Space / Enter 与触屏按住期间投影 `data-pressed`。标题为 heading-3,说明文字为 13px 说明档。 - 关闭时内容立即失活并退出可访问树,内容与遮罩的有限退场动画全部完成后再释放模态资源,并发出 `onExitComplete` / `exit-complete`。重开撤销旧退出,卸载立即清理。 - 另有命令式服务,业务代码一次调用即可弹出。 - 命令式服务与声明式组件共用 `Header / Body / Footer` 三段:标题和徽记在 Header,字符串、函数正文及取值表单在 Body,操作按钮在 Footer。长内容只滚动 Body,头尾保留在面板内。 - 命令式服务的 `onOk` 返回 `false` 只阻止关闭;同步抛错或 Promise 拒绝会保持对话框打开,设置独立 `service.actionError` 并触发 `onActionError({ cause })`。`cause` 保留原始异常,不直接转成用户提示。 - 失败提示通过服务的 `actionErrorText` 本地化:Vue 支持字符串/ref/getter,React 支持字符串/getter,Web Components 使用字符串,与各端按钮文案合同一致;提示位于 Body 的 `role=alert` 实时区。重试先清理旧异常,关闭或切换请求后旧 Promise 不再写回。 - 服务宿主或函数正文渲染失败会拒绝所属请求,`onActionError` 通知自身失败也会拒绝所属请求;业务需要处理返回 Promise 的拒绝。显式 `target` 必须是当前文档中已经连接的元素,无法展示时不会解析为取消或永久等待。 ### 组合 - 内容区放[滚动区域](./scroll-area);按钮行使用[按钮组](./button-group);轻量确认场景改用[弹出确认](./popconfirm)。 ### 最佳实践 - 标题说明本次要做什么,不写“提示”。 - 确认按钮的文字写具体动作(“删除”),不写“确定”。 - 破坏性操作使用危险语气,并让取消成为默认焦点。 ### 反模式 - 在对话框内再打开对话框。 - 内部有未保存的输入却允许点击外部关闭。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDialogBody` `XhDialogCloseTrigger` `XhDialogContent` `XhDialogDescription` `XhDialogFooter` `XhDialogHeader` `XhDialogIndicator` `XhDialogRoot` `XhDialogTitle` `XhDialogTrigger` | | 组合式函数 | `useDialog` | | 状态机 | `dialogMachine` | | 皮肤 | `@xihan-ui/styles/dialog.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `open` | `boolean` | | | | `defaultOpen` | `boolean` | | | | `modal` | `boolean` | | | | `role` | `'dialog' \| 'alertdialog'` | | | | `closeOnEscape` | `boolean` | | | | `closeOnInteractOutside` | `boolean` | | | | `restoreFocus` | `boolean` | | | | `initialFocus` | `string` | | 展开后先聚焦到 content 内匹配该选择器的元素;选择器不匹配时回退为默认聚焦顺序。 | | `size` | `Size` | | 尺寸:sm / md / lg。只影响 content 的最大宽度,写在 content 上(本组件没有 root 部件)。 | | `variant` | `OverlayBackdropVariant` | | 遮罩形态:opaque / blur / transparent。写在 backdrop 上,只影响该层的底色与模糊。 | | `translations` | `Partial` | | | | `onOpenChange` | `(details: DialogOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `onExitComplete` | `() => void` | | 退出动画结束或取消,且本层资源全部释放后通知;卸载和重新打开不通知。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `exit-complete` | `CustomEvent` | 退出完成且本层资源已释放 | | `open-change` | `DialogOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhDialogRoot` | `default` | `DialogRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDialogContent` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | | `XhDialogRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `trigger` | 'open' \| 'closed' | | `backdrop` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `setOpen` | `(next: boolean) => void` | | | `getTriggerProps` | `() => T['button']` | | | `getBackdropProps` | `() => T['element']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getHeaderProps` | `() => T['element']` | | | `getIndicatorProps` | `() => T['element']` | | | `getTitleProps` | `() => T['element']` | | | `getDescriptionProps` | `() => T['element']` | | | `getBodyProps` | `() => T['element']` | | | `getFooterProps` | `() => T['element']` | | | `getCloseTriggerProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in trigger | 打开对话框并把焦点移入 content | | `Escape` | open | 关闭并把焦点还给 trigger | | `Tab` | open | 在 content 内向后循环焦点 | | `Shift+Tab` | open | 在 content 内向前循环焦点 | | `Enter` / `Space` | held in trigger / close-trigger | 按住期间该按钮投影 data-pressed,与指针 :active 同一副按压面;抬起、失焦或面板收起撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `trigger` | `aria-haspopup` | 'dialog' | | `content` | `aria-describedby` | `description` 部件的 id | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-labelledby` | `title` 部件的 id | | `content` | `aria-modal` | 'true' \| 'false' | | `content` | `role` | props.role | | `indicator` | `aria-hidden` | 'true' | | `close-trigger` | `aria-label` | props.translations.close | ## 样式参考 ### 皮肤 `@xihan-ui/styles/dialog.css` 使用 `[data-scope="dialog"][data-part="trigger"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'text' | | `trigger` | `data-xh-action-size` | 'md' | | `trigger` | `data-xh-action-variant` | 'outline' | | `backdrop` | `data-state` | 'open' \| 'closed' | | `backdrop` | `data-variant` | props.variant | | `positioner` | `data-positioned` | '' | | `positioner` | `data-state` | 'open' \| 'closed' | | `content` | `data-size` | props.size | | `content` | `data-state` | 'open' \| 'closed' | | `close-trigger` | `data-pressed` | ''(条件成立时才出现) | | `close-trigger` | `data-xh-action-control` | '' | | `close-trigger` | `data-xh-action-display` | 'always' | | `close-trigger` | `data-xh-action-profile` | 'icon' | | `close-trigger` | `data-xh-action-size` | 'sm' | | `close-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-dialog-backdrop-bg` | `backdrop` | `background` | `default` | `--xh-bg-overlay` | dialog 的 backdrop 部件 background 覆盖槽。 | | `--xh-dialog-backdrop-blur` | `backdrop` | `-webkit-backdrop-filter`
`backdrop-filter` | `variant=blur` | `--xh-overlay-backdrop-blur` | dialog 的 backdrop 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 | | `--xh-dialog-backdrop-filter` | `backdrop`
`content` | `-webkit-backdrop-filter`
`backdrop-filter` | `default`
`variant=blur` | `--xh-dialog-backdrop-blur`
`--xh-material-elevated-backdrop` | dialog 的 backdrop、content 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 | | `--xh-dialog-backdrop-layer` | `backdrop` | `z-index` | `default` | `--xh-_layer` | dialog 的 backdrop 部件 z-index 覆盖槽。 | | `--xh-dialog-bg` | `content` | `background` | `@media (forced-colors: active)`
`default` | `--xh-material-elevated-bg` | dialog 的 content 部件 background 覆盖槽。 | | `--xh-dialog-border` | `content` | `border` | `default` | `--xh-material-elevated-border` | dialog 的 content 部件 border 覆盖槽。 | | `--xh-dialog-close-bg-active` | `close-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | dialog 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-dialog-close-bg-focus` | `close-trigger` | `background-color` | `focus-visible` | `--xh-_action-variant-bg-focus-visible` | dialog 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-dialog-close-bg-hover` | `close-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | dialog 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-dialog-close-fg` | `close-trigger` | `color` | `default` | `--xh-fg-muted` | dialog 的 close-trigger 部件 color 覆盖槽。 | | `--xh-dialog-close-fg-focus` | `close-trigger` | `color` | `focus-visible` | `--xh-dialog-close-fg-hover` | dialog 的 close-trigger 部件 color 覆盖槽。 | | `--xh-dialog-close-fg-hover` | `close-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-focus-visible`
`--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed` | dialog 的 close-trigger 部件 color 覆盖槽。 | | `--xh-dialog-close-radius` | `close-trigger` | `border-radius` | `default` | `--xh-shape-control` | dialog 的 close-trigger 部件 border-radius 覆盖槽。 | | `--xh-dialog-close-size` | `close-trigger`
`content`
`title` | `block-size`
`inline-size`
`padding-inline-end` | `default`
`has([data-scope='dialog'][data-part='close-trigger'])`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size`
`--xh-control-h-sm` | dialog 的 close-trigger、content、title 部件 block-size、inline-size、padding-inline-end 覆盖槽。 | | `--xh-dialog-content-backdrop-filter` | `content` | `-webkit-backdrop-filter`
`backdrop-filter` | `default` | `--xh-dialog-backdrop-filter` | dialog 的 content 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 | | `--xh-dialog-content-lens-bg` | `content` | `background` | `default` | `--xh-dialog-header-bg` | dialog 的 content 部件 background 覆盖槽。 | | `--xh-dialog-content-lens-depth` | `content` | `background` | `default` | `--xh-dialog-header-lens-depth` | dialog 的 content 部件 background 覆盖槽。 | | `--xh-dialog-description-fg` | `description` | `color` | `default` | `--xh-fg-muted` | dialog 的 description 部件 color 覆盖槽。 | | `--xh-dialog-description-font-size` | `description` | `font-size` | `default` | `--xh-text-secondary-size` | dialog 的 description 部件 font-size 覆盖槽。 | | `--xh-dialog-fg` | `content` | `color` | `default` | `--xh-material-elevated-fg` | dialog 的 content 部件 color 覆盖槽。 | | `--xh-dialog-footer-gap` | `footer` | `gap` | `default` | `--xh-control-gap-md` | dialog 的 footer 部件 gap 覆盖槽。 | | `--xh-dialog-footer-pt` | `footer` | `padding-block-start` | `default` | `--xh-space-2` | dialog 的 footer 部件 padding-block-start 覆盖槽。 | | `--xh-dialog-gap` | `content` | `gap` | `default` | `--xh-stack-gap-md` | dialog 的 content 部件 gap 覆盖槽。 | | `--xh-dialog-header-bg` | `content` | `background` | `default` | `--xh-material-elevated-bg` | dialog 的 content 部件 background 覆盖槽。 | | `--xh-dialog-header-gap` | `header` | `gap` | `default` | `--xh-stack-gap-sm` | dialog 的 header 部件 gap 覆盖槽。 | | `--xh-dialog-header-lens-depth` | `content` | `background` | `default` | `--xh-dialog-py` | dialog 的 content 部件 background 覆盖槽。 | | `--xh-dialog-header-pb` | `header` | `padding-block-end` | `default` | `--xh-space-2` | dialog 的 header 部件 padding-block-end 覆盖槽。 | | `--xh-dialog-highlight` | `content` | `background` | `default` | `--xh-material-elevated-highlight` | dialog 的 content 部件 background 覆盖槽。 | | `--xh-dialog-icon-size` | `close-trigger`
`content`
`trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size`
`--xh-glyph-size-md` | dialog 的 close-trigger、content、trigger 部件 --xh-icon-size 覆盖槽。 | | `--xh-dialog-indicator-bg` | `indicator` | `background` | `default` | `--xh-_tone-subtle` | dialog 的 indicator 部件 background 覆盖槽。 | | `--xh-dialog-indicator-fg` | `indicator` | `color` | `default` | `--xh-_tone-fg` | dialog 的 indicator 部件 color 覆盖槽。 | | `--xh-dialog-indicator-mark-size` | `indicator` | `--xh-icon-size` | `default` | `--xh-dialog-indicator-size` | dialog 的 indicator 部件 --xh-icon-size 覆盖槽。 | | `--xh-dialog-indicator-radius` | `indicator` | `border-radius` | `default` | `--xh-shape-circle` | dialog 的 indicator 部件 border-radius 覆盖槽。 | | `--xh-dialog-indicator-size` | `indicator` | `--xh-icon-size`
`block-size`
`inline-size` | `default` | `--xh-glyph-size-md` | dialog 的 indicator 部件 --xh-icon-size、block-size、inline-size 覆盖槽。 | | `--xh-dialog-layer` | `positioner` | `z-index` | `default` | `--xh-_layer` | dialog 的 positioner 部件 z-index 覆盖槽。 | | `--xh-dialog-max-w` | `content` | `max-inline-size` | `default` | `--xh-_dialog-max-w` | dialog 的 content 部件 max-inline-size 覆盖槽。 | | `--xh-dialog-positioner-padding` | `positioner` | `padding-block-end`
`padding-block-start`
`padding-inline` | `default` | `--xh-space-4` | dialog 的 positioner 部件 padding-block-end、padding-block-start、padding-inline 覆盖槽。 | | `--xh-dialog-px` | `content` | `padding-inline` | `default` | `--xh-surface-px-md` | dialog 的 content 部件 padding-inline 覆盖槽。 | | `--xh-dialog-py` | `content` | `background`
`padding-block` | `default` | `--xh-surface-py-md` | dialog 的 content 部件 background、padding-block 覆盖槽。 | | `--xh-dialog-radius` | `content` | `border-radius` | `default` | `--xh-shape-overlay` | dialog 的 content 部件 border-radius 覆盖槽。 | | `--xh-dialog-separator` | `content`
`footer`
`header` | `border-block-end`
`border-block-start` | `has([data-scope='dialog'][data-part='body'])`
`has([data-scope='dialog'][data-part='footer'])` | `--xh-material-elevated-separator` | dialog 的 content、footer、header 部件 border-block-end、border-block-start 覆盖槽。 | | `--xh-dialog-shadow` | `content` | `box-shadow` | `default` | `--xh-material-elevated-shadow` | dialog 的 content 部件 box-shadow 覆盖槽。 | | `--xh-dialog-title-fg` | `title` | `color` | `default` | `--xh-fg-default` | dialog 的 title 部件 color 覆盖槽。 | | `--xh-dialog-title-font-size` | `title` | `font-size` | `default` | `--xh-text-heading-3-size` | dialog 的 title 部件 font-size 覆盖槽。 | | `--xh-dialog-title-font-weight` | `title` | `font-weight` | `default` | `--xh-text-heading-3-weight` | dialog 的 title 部件 font-weight 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(面板)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-fade-in` · `xh-fade-out` · `xh-sheet-in` · `xh-sheet-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/diff-view # DiffView 差异视图 一份改动的逐行呈现:并排或单栏、双侧行号、变更类型的读屏文字,以及远离变更处的折叠。 ## 用法 两个入口归一到同一个模型:这里用新旧两版全文计算,着色在建模时一次算好 ```vue ``` ```html
src/clamp.ts
``` ## 组件结构 加粗的是必需部件。 `data-scope="diff-view"`:**`root`** · `header` · `summary` · **`viewport`** · **`body`** · `row` · `line-number` · `line-content` · `change-label` · `inline-change` · `token` · `gap` · `gap-cell` · `gap-trigger` · `empty` · `truncation` ## 示例 ### 并排与折叠 并排两列都发出格子,空的一侧照发;远离变更的连续上下文折为一格,点击即展开 ```vue ``` ```html
src/pipeline.ts
``` ### 长行换行与词级差异 开启 wrap 使长行原地折行;配对的删改行之间再比较一次词,只有真正改动的片段上底色 ```vue ``` ```html
src/client.ts
``` ### 超长差异的截断提示 超过 maxLines 的部分被截去,提示条向读者说明截去了多少行 ```vue ``` ```html
src/items.ts
``` ### 尺寸 size 改变字号、行高与行号槽的宽度,三档并列对照 ```vue ``` ```html
src/clamp.ts · sm
src/clamp.ts · md
src/clamp.ts · lg
``` ## 设计指引 ### 何时使用 - 展示 AI 提议的代码改动,或两版文本的对比。 - 已有统一格式的补丁,或有新旧两版全文。 ### 何时不用 - 只展示一段代码时,使用[代码视图](./code-view)。 - 展示 AI 提议的数据编辑并逐条取舍时,使用带多选的[表格](./table)。 ### 特性 - 两个入口归一到同一个模型:`computeTextDiff(before, after)` 用两版全文计算,`parseUnifiedPatch(patch)` 解析补丁;组件只接受模型。 - 自定义渲染器可调用 `diffViewSides(view)` 取得列序:单栏为旧侧,分栏按旧侧、新侧排列。 - 着色在建模时一次计算,不在连接层执行:`computeTextDiff` 持有完整文本,整体切分一次再按行取用,跨行的块注释与多行字符串才不会着错色。`parseUnifiedPatch` 拿不到完整文件,因此一律不着色。 - 词级差异:配对的一条删除行与一条新增行之间再比较一次词,只有实际变动的片段加底色;整行改写与超长行不比较。两个入口都产出,`wordDiff: false` 关闭。 - `contextLines` 把 hunk 内远离变更的连续上下文折成一格,点击展开。展开集合可受控,便于“全部展开”等操作统一持有。 - `wrap` 让长行原地折行,容器不再横向滚动;窄栏与并排视图下尤其有用。 - 头部自带增删统计位 `summary`,增删各一个,数字取自模型,着色跟随变更类型。 - `maxLines` 是必需的上限:AI 可能输出超大文件,新旧两侧各自超出时从尾部截断。截断行数由模型带出,`truncation` 提示条向读者说明。 - 行号与列号一律从模型计算,不从 DOM 反推。 ### 组合 - 单栏与并排的切换使用[切换按钮组](./toggle-group);增删统计已有成品位(只需要数字时可用 `diffStats(model)`)。 - 放入[工具调用](./tool-call)的详情区,展示本次调用的改动。 - 需要对 AI 提议的编辑逐条取舍并应用时,使用[表格](./table)的选择机制承载行级取舍,单元格内放[复选框](./checkbox),页脚的计数与“应用”使用[按钮](./button)。差异视图本身只读,不接这套交互。 ### 最佳实践 - 并排视图给足宽度:两列各自还要横向滚动;窄栏下单栏更易读,或开启 `wrap` 让长行折行。 - 折叠阈值取三到五行:过少时需要频繁展开,过多时折叠失去意义。 ### 反模式 - 截断后不渲染 `truncation`:截断的差异看起来仍像完整差异,评审者会误以为已经看完。 - 对补丁计算出的差异着色:文本不完整,跨行的记号必然切错。 - 用颜色作为变更类型的唯一线索:色觉障碍与高对比度模式下会失效。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDiffViewBody` `XhDiffViewEmpty` `XhDiffViewHeader` `XhDiffViewRoot` `XhDiffViewSummary` `XhDiffViewTruncation` `XhDiffViewViewport` | | 组合式函数 | `useDiffView` | | 状态机 | `diffViewMachine` | | 皮肤 | `@xihan-ui/styles/diff-view.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `model` | `DiffModel` | | 差异模型,唯一入口。补丁与新旧两版文本都先归一到它。 | | `view` | `DiffViewMode` | | | | `contextLines` | `number` | | 变更两侧各显示的上下文行数,其余折叠;未提供或非有限值时不折叠。 | | `expandedValue` | `readonly string[]` | | 展开的折叠格 id 集合,提供即受控。 | | `defaultExpandedValue` | `readonly string[]` | | | | `wrap` | `boolean` | | 长行原地折行,不再横向滚动;默认关闭。 | | `size` | `Size` | | | | `translations` | `Partial` | | | | `onExpandedValueChange` | `(details: DiffViewExpandedValueChangeDetails) => void` | | | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `expanded-value-change` | `DiffViewExpandedValueChangeDetails` | 展开集合变化;detail 为 `{ value: string[] }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhDiffViewRoot` | `default` | `DiffViewRootSlotProps` | | | `XhDiffViewSummary` | `default` | `{ count: number }` | | | `XhDiffViewTruncation` | `default` | `{ count: number }` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDiffViewRoot` | `children` | `SlotChildren` | | | | `XhDiffViewSummary` | `change` | `Extract` | 是 | | | `XhDiffViewSummary` | `children` | `SlotChildren<{ count: number }>` | | | | `XhDiffViewTruncation` | `children` | `SlotChildren<{ count: number }>` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`GAP.EXPAND` · `GAP.COLLAPSE` · `CONTROLLED.EXPANDED.SET` · `PRESS.START` · `PRESS.END` **判据**:`isExpandedControlled` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `view` | `DiffViewMode` | | | `rows` | `readonly DiffViewRow[]` | 折叠后的可见行序,含折叠的格。 | | `expandedValue` | `string[]` | | | `stats` | `{ added: number, removed: number }` | 增删的行数。 | | `truncated` | `boolean` | 模型被上限截断过。 | | `truncatedLines` | `number` | 被上限截断、未进入模型的源文本行数;未截断时为 0。 | | `truncationText` | `string` | 截断提示条的文字,已代入行数;未截断时为空串。 | | `isEmpty` | `boolean` | 没有任何变更。 | | `setExpandedValue` | `(next: string[]) => void` | | | `toggleGap` | `(id: string) => void` | | | `getRootProps` | `() => T['element']` | | | `getHeaderProps` | `() => T['element']` | | | `getSummaryProps` | `(props: { change: DiffChange }) => T['element']` | 头部右侧的增删统计位,增删各一个。 | | `getViewportProps` | `() => T['element']` | | | `getBodyProps` | `() => T['element']` | | | `getRowProps` | `(props: DiffViewRowProps) => T['element']` | | | `getLineNumberProps` | `(props: DiffViewCellProps) => T['element']` | | | `getLineContentProps` | `(props: DiffViewCellProps) => T['element']` | | | `getChangeLabelProps` | `(props: { change: DiffChange }) => T['element']` | | | `getInlineChangeProps` | `(props: DiffViewInlineChangeProps) => T['element']` | | | `getTokenProps` | `(token: CodeToken) => T['element']` | | | `getGapProps` | `(props: DiffViewGapProps) => T['element']` | | | `getGapCellProps` | `() => T['element']` | | | `getGapTriggerProps` | `(props: DiffViewGapProps) => T['button']` | | | `getEmptyProps` | `() => T['element']` | | | `getTruncationProps` | `() => T['element']` | 截断提示条;未截断时带 hidden。 | | `changeLabel` | `(change: DiffChange) => string` | 变更类型对应的读屏文字,写入视觉隐藏的格。 | | `cellText` | `(props: DiffViewCellProps) => string \| undefined` | 该行在该侧的文本;split 下空侧为 undefined。 | | `cellNumber` | `(props: DiffViewCellProps) => number \| undefined` | 该行在该侧的行号;不存在时为 undefined。 | | `cellTokens` | `(props: DiffViewCellProps) => readonly CodeToken[]` | 该行在该侧的着色片段;不着色或空侧时为空数组。 | | `cellSegments` | `(props: DiffViewCellProps) => readonly DiffViewSegment[]` | 该行在该侧的词级片段,着色记号已按片段边界切分。 未计算词级差异时为空数组,此时按 cellTokens / cellText 铺设。 | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/WCAG21/Techniques/general/G202) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Tab` | 差异视图在 Tab 序列中 | 滚动容器自身可聚焦,随后方向键的横纵滚动交给浏览器,组件不接管 | | `Enter` / `Space` | 焦点在展开按钮上 | 展开该处折起来的上下文行;组件只接 click,按键走原生 button 的默认行为 | | `Enter` / `Space` | 按住展开按钮 | 按住期间该格的 gap-trigger 投影 data-pressed,与指针 :active 同一副按压面(disclosure trigger 只换面不缩放);抬起、失焦或该格展开撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `body` | `aria-colcount` | 2 \| 1 | | `body` | `aria-label` | undefined \| translations?.diff | | `body` | `aria-labelledby` | `header` 部件的 id \| undefined | | `body` | `aria-rowcount` | rows.length | | `body` | `role` | 'table' | | `row` | `aria-rowindex` | rowIndex | | `row` | `role` | 'row' | | `line-number` | `aria-hidden` | 'true' | | `line-content` | `aria-colindex` | 2 \| 1 | | `line-content` | `role` | 'cell' | | `gap` | `role` | 'row' | | `gap-cell` | `aria-colindex` | 1 | | `gap-cell` | `role` | 'cell' | | `gap-trigger` | `aria-expanded` | 'true' \| 'false' | | `gap-trigger` | `aria-label` | expandGapLabel(hiddenCountOf(gapId)) | - 表格语义:`role=table` 配 `role=row` 与 `role=cell`,带 `aria-rowcount` / `aria-rowindex` / `aria-colcount` / `aria-colindex`。列数只计算实际暴露的内容列,行号不算列。 - 每一行都带一段视觉隐藏的变更类型文字,变更不只靠颜色传达。 - 变更行还有一条非颜色线索:新增绘制实心色条,删除绘制同宽的斜纹条,灰度与高对比度下也可区分。 - 行号对读屏隐藏,由皮肤用 `attr()` 绘制,复制差异不会带上行号。 - 不采用表格的行级 roving:只读差异不是网格,吞掉方向键的焦点组会抢走页面滚动,读屏本身也有表格浏览模式。这是显式决定。 ## 样式参考 ### 皮肤 `@xihan-ui/styles/diff-view.css` 使用 `[data-scope="diff-view"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-size` | props.size | | `root` | `data-truncated` | ''(条件成立时才出现) | | `root` | `data-view` | props.view | | `root` | `data-wrap` | ''(条件成立时才出现) | | `summary` | `data-change` | change | | `row` | `data-change` | lineAt(rowIndex)?.change | | `row` | `data-revealed` | ''(条件成立时才出现) | | `line-number` | `data-change` | lineAt(rowIndex)?.change | | `line-number` | `data-line-number` | cellNumber({ rowIndex, side })?.toString() | | `line-number` | `data-side` | side | | `line-content` | `data-change` | lineAt(rowIndex)?.change | | `line-content` | `data-empty` | ''(条件成立时才出现) | | `line-content` | `data-side` | side | | `change-label` | `data-change` | change | | `inline-change` | `data-change` | lineAt(rowIndex)?.change \| undefined | | `token` | `data-kind` | token.kind | | `gap` | `data-expanded` | ''(条件成立时才出现) | | `gap` | `data-value` | hunkIndex:0 | | `gap-trigger` | `data-pressed` | ''(条件成立时才出现) | | `gap-trigger` | `data-value` | hunkIndex:0 | | `gap-trigger` | `data-xh-action-control` | '' | | `gap-trigger` | `data-xh-action-display` | 'always' | | `gap-trigger` | `data-xh-action-profile` | 'disclosure-trigger' | | `gap-trigger` | `data-xh-action-size` | props.size | | `gap-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-diff-view-added-bg` | `row` | `background` | `change=added` | `--xh-diff-added-bg` | diff-view 的 row 部件 background 覆盖槽。 | | `--xh-diff-view-added-fg` | `inline-change`
`line-content`
`line-number`
`row`
`summary` | `background`
`box-shadow`
`color` | `change=added` | `--xh-diff-added-fg` | diff-view 的 inline-change、line-content、line-number、row、summary 部件 background、box-shadow、color 覆盖槽。 | | `--xh-diff-view-bg` | `root` | `background` | `default` | `--xh-bg-surface` | diff-view 的 root 部件 background 覆盖槽。 | | `--xh-diff-view-border` | `root` | `border` | `default` | `--xh-border-default` | diff-view 的 root 部件 border 覆盖槽。 | | `--xh-diff-view-change-bar` | `row` | `background`
`box-shadow` | `change=added`
`change=removed` | `--xh-stroke-thick` | diff-view 的 row 部件 background、box-shadow 覆盖槽。 | | `--xh-diff-view-comment-fg` | `token` | `color` | `kind=comment` | `--xh-fg-muted` | diff-view 的 token 部件 color 覆盖槽。 | | `--xh-diff-view-divider` | `header`
`line-number`
`root` | `border-block-end`
`border-inline-end`
`border-inline-start` | `@media (min-width: 1024px)`
`default`
`side=new`
`view=split` | `--xh-border-subtle` | diff-view 的 header、line-number、root 部件 border-block-end、border-inline-end、border-inline-start 覆盖槽。 | | `--xh-diff-view-empty-bg` | `line-content` | `background` | `empty` | `--xh-bg-subtle` | diff-view 的 line-content 部件 background 覆盖槽。 | | `--xh-diff-view-empty-fg` | `empty` | `color` | `default` | `--xh-fg-muted` | diff-view 的 empty 部件 color 覆盖槽。 | | `--xh-diff-view-font` | `body`
`header` | `font-family` | `default` | `--xh-font-family-mono` | diff-view 的 body、header 部件 font-family 覆盖槽。 | | `--xh-diff-view-font-size` | `body`
`gap-trigger` | `font-size` | `default` | `--xh-_diff-view-font-size` | diff-view 的 body、gap-trigger 部件 font-size 覆盖槽。 | | `--xh-diff-view-gap-bg` | `gap` | `background` | `default` | `--xh-bg-subtle` | diff-view 的 gap 部件 background 覆盖槽。 | | `--xh-diff-view-gap-bg-hover` | `gap-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | diff-view 的 gap-trigger 部件 background-color 覆盖槽。 | | `--xh-diff-view-gap-fg` | `gap-trigger` | `color` | `default` | `--xh-fg-muted` | diff-view 的 gap-trigger 部件 color 覆盖槽。 | | `--xh-diff-view-gutter` | `line-number` | `inline-size` | `default` | `4ch` | diff-view 的 line-number 部件 inline-size 覆盖槽。 | | `--xh-diff-view-header-fg` | `header` | `color` | `default` | `--xh-fg-muted` | diff-view 的 header 部件 color 覆盖槽。 | | `--xh-diff-view-header-font-size` | `empty`
`header`
`truncation` | `font-size` | `default` | `--xh-text-secondary-size` | diff-view 的 empty、header、truncation 部件 font-size 覆盖槽。 | | `--xh-diff-view-header-gap` | `header` | `gap` | `default` | `--xh-space-2` | diff-view 的 header 部件 gap 覆盖槽。 | | `--xh-diff-view-icon-size` | `root` | `--xh-icon-size` | `default`
`size=lg`
`size=sm` | `--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | diff-view 的 root 部件 --xh-icon-size 覆盖槽。 | | `--xh-diff-view-inline-change-radius` | `inline-change` | `border-radius` | `change` | `--xh-shape-inset` | diff-view 的 inline-change 部件 border-radius 覆盖槽。 | | `--xh-diff-view-keyword-fg` | `token` | `color` | `kind=keyword` | `--xh-syntax-keyword` | diff-view 的 token 部件 color 覆盖槽。 | | `--xh-diff-view-keyword-weight` | `token` | `font-weight` | `kind=keyword` | `--xh-font-weight-semibold` | diff-view 的 token 部件 font-weight 覆盖槽。 | | `--xh-diff-view-line-height` | `body`
`gap`
`gap-trigger`
`row` | `block-size`
`line-height`
`min-block-size` | `default`
`xh-action-profile=disclosure-trigger` | `--xh-text-code-leading` | diff-view 的 body、gap、gap-trigger、row 部件 block-size、line-height、min-block-size 覆盖槽。 | | `--xh-diff-view-max-h` | `viewport` | `max-block-size` | `default` | `--xh-viewport-max-h` | diff-view 的 viewport 部件 max-block-size 覆盖槽。 | | `--xh-diff-view-number-fg` | `line-number` | `color` | `default` | `--xh-fg-subtle` | diff-view 的 line-number 部件 color 覆盖槽。 | | `--xh-diff-view-number-token-fg` | `token` | `color` | `kind=number` | `--xh-syntax-number` | diff-view 的 token 部件 color 覆盖槽。 | | `--xh-diff-view-punctuation-fg` | `token` | `color` | `kind=punctuation` | `--xh-fg-subtle` | diff-view 的 token 部件 color 覆盖槽。 | | `--xh-diff-view-px` | `empty`
`gap-trigger`
`header`
`line-content`
`line-number`
`truncation` | `padding-inline`
`padding-inline-end` | `default` | `--xh-_diff-view-px` | diff-view 的 empty、gap-trigger、header、line-content、line-number、truncation 部件 padding-inline、padding-inline-end 覆盖槽。 | | `--xh-diff-view-py` | `empty`
`header`
`truncation` | `padding-block` | `default` | `--xh-_diff-view-py` | diff-view 的 empty、header、truncation 部件 padding-block 覆盖槽。 | | `--xh-diff-view-radius` | `root` | `border-radius` | `default` | `--xh-shape-surface` | diff-view 的 root 部件 border-radius 覆盖槽。 | | `--xh-diff-view-removed-bg` | `row` | `background` | `change=removed` | `--xh-diff-removed-bg` | diff-view 的 row 部件 background 覆盖槽。 | | `--xh-diff-view-removed-fg` | `inline-change`
`line-content`
`line-number`
`row`
`summary` | `background`
`color` | `change=removed` | `--xh-diff-removed-fg` | diff-view 的 inline-change、line-content、line-number、row、summary 部件 background、color 覆盖槽。 | | `--xh-diff-view-shadow` | `root` | `box-shadow` | `default` | `none` | diff-view 的 root 部件 box-shadow 覆盖槽。 | | `--xh-diff-view-string-fg` | `token` | `color` | `kind=string` | `--xh-syntax-string` | diff-view 的 token 部件 color 覆盖槽。 | | `--xh-diff-view-truncation-bg` | `truncation` | `background` | `default` | `--xh-fg-warning` | diff-view 的 truncation 部件 background 覆盖槽。 | | `--xh-diff-view-truncation-border` | `truncation` | `border-block-start` | `default` | `--xh-border-subtle` | diff-view 的 truncation 部件 border-block-start 覆盖槽。 | | `--xh-diff-view-truncation-fg` | `truncation` | `color` | `default` | `--xh-fg-warning` | diff-view 的 truncation 部件 color 覆盖槽。 | | `--xh-diff-view-truncation-gap` | `truncation` | `gap` | `default` | `--xh-space-2` | diff-view 的 truncation 部件 gap 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-drop-in` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### 响应式 皮肤按视口分档:`min-width: 1024px`。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/download-trigger # DownloadTrigger 下载触发器 用于将文本或 Blob 保存为本地文件。 ## 用法 下载文本文件 ```vue ``` ```html ``` ## 组件结构 加粗的是必需部件。 `data-scope="download-trigger"`:**`root`** ## 示例 ### 异步内容 点击后获取下载内容 ```vue ``` ```html ``` ### Blob 下载 JSON 文件 ```vue ``` ```html ``` ### 变体 设置触发器外观 ```vue ``` ```html ``` ### 尺寸 使用小、中、大三档尺寸 ```vue ``` ```html ``` ### 禁用 禁止触发下载 ```vue ``` ```html ``` ## 设计指引 ### 何时使用 - 导出 CSV、JSON、日志或配置文件。 - 点击后才获取或生成下载内容。 ### 何时不用 - 文件已有稳定地址时,使用原生 ``。 - 复制少量文字时,使用[剪贴板](./clipboard)。 - 接收用户文件时,使用[文件上传](./file-upload)。 ### 特性 - 接受字符串、Blob 与异步数据函数。 - `preparing` 期间保留焦点并阻止重复触发。 - 通过完成与失败事件返回本次文件名和错误。 - 缺省是中性淡底 `subtle`,只有 `solid` 才是品牌实心;按下有统一的缩放与换底反馈。 ### 组合 - 与[进度条](./progress)组合展示可量化的长任务。 - 通过变体与颜色调整操作层级。 ### 最佳实践 - 文件名应包含正确扩展名。 - 保留下载图标与可见文字;只有下载是页面主操作时才使用 `solid`。 - 大文件优先使用服务端下载地址。 - 失败事件应连接可见反馈。 ### 反模式 - 不要将“下载已发起”等同于“文件已写入磁盘”。 - 不要在页面加载时预先生成大文件。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDownloadTrigger` | | 组合式函数 | `useDownloadTrigger` | | 状态机 | `downloadTriggerMachine` | | 皮肤 | `@xihan-ui/styles/download-trigger.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `data` | `DownloadTriggerData` | | 要下载的内容:文本、Blob,或点击时才调用的取数函数(可返回 Promise)。 | | `fileName` | `string` | | 写出的文件名;未提供或空串时回退为内建默认名。 | | `mimeType` | `string` | | 内容类型;提供后以它为准,Blob 自带的类型也按它重新包装。未提供时文本按纯文本处理。 | | `disabled` | `boolean` | | 禁用:按钮不可聚焦、不可点击。 | | `variant` | `ActionVariant` | | 变体:solid / subtle / outline / ghost,默认 subtle(缺省中性淡底,solid 才品牌实心)。 | | `tone` | `Tone` | | 颜色:brand / neutral / success / warning / danger / info。 | | `size` | `Size` | | 尺寸:sm / md / lg。 | | `translations` | `Partial` | | | | `onDownloadComplete` | `(details: DownloadTriggerCompleteDetails) => void` | | 数据已交给浏览器时通知一次。此时只说明下载已发起,浏览器是否把文件写入磁盘组件无法感知。 | | `onDownloadError` | `(details: DownloadTriggerErrorDetails) => void` | | 取数失败或无法创建下载时通知;此时状态已回到 idle。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `download-complete` | `DownloadTriggerCompleteDetails` | 数据已交给浏览器;detail 为 `{ fileName }` | | `download-error` | `DownloadTriggerErrorDetails` | 取数失败或无法创建下载;detail 为 `{ error, fileName }`,此时状态已回到 idle | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhDownloadTrigger` | `default` | `DownloadTriggerSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDownloadTrigger` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'idle' \| 'preparing' | 以下名称仅用于内部状态机。 **状态**:`idle` · `preparing` **事件**:`DOWNLOAD.TRIGGER` · `DOWNLOAD.SUCCESS` · `DOWNLOAD.ERROR` · `PRESS.START` · `PRESS.END` **判据**:`isDisabled` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `status` | `DownloadTriggerStatus` | | | `preparing` | `boolean` | 数据获取中。按钮不因此禁用,只是期间再次点击不会重复发起。 | | `disabled` | `boolean` | | | `fileName` | `string` | 本次将写出的文件名(prop 未提供时是内建默认名)。 | | `download` | `() => void` | 发起一次下载意图,与点击按钮走同一路径:禁用时不生效,取数在途时不重复发起。 | | `getRootProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/button/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in root, 未禁用 | 发起一次下载;取数在途时这两个键同样不会重复发起 | | `Enter` / `Space` | held in root, not disabled, not preparing | 按住期间投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-busy` | 'true' \| undefined | | `root` | `aria-disabled` | 'true' \| undefined | | `root` | `aria-label` | props.translations.trigger | - 触发器使用原生 `

筛选条件

面板贴住右边,这是 side 的默认值。

``` ## 组件结构 加粗的是必需部件。 `data-scope="drawer"`:**`root`** · `trigger` · `backdrop` · `positioner` · **`content`** · `header` · `title` · `description` · `body` · `footer` · `close-trigger` ## 示例 ### 贴边方向 side 只写为 data-side,面板贴在哪条边由皮肤按该值决定;root 与 content 报告的是同一条边 ```vue ``` ```html

左侧抽屉

当前 data-side 是 left。

右侧抽屉

当前 data-side 是 right。

顶部抽屉

当前 data-side 是 top。

底部抽屉

当前 data-side 是 bottom。

``` ### 受控 传入 open 后由宿主决定;Escape、点击面板外、按关闭按钮都只回写 open,不自行修改状态 ```vue ``` ```html
当前:收起

受控抽屉

这里没有 trigger,开合完全跟着外面那颗按钮与 open 走。

``` ### 尺寸 size 写为 content 的 data-size,只改变面板贴边方向上的厚度;三档各自一个抽屉,打开后才可见厚度差异 ```vue ``` ```html

sm 薄抽屉

面板贴住右边,三档只有厚度不同。

缺省抽屉

面板贴住右边,三档只有厚度不同。

lg 厚抽屉

面板贴住右边,三档只有厚度不同。

``` ### 头尾固定、正文滚动 header / body / footer 把面板切为三段:头与尾固定在原处,只有正文一段滚动 ```vue ``` ```html

操作记录

共 24 条,往下翻。

``` ### 关闭前拦截 受控时组件不自行修改状态:Escape、点击面板外、按关闭按钮都只发一次收起意图,是否写回由宿主决定 ```vue ``` ```html

编辑草稿

这里假定草稿一直有未保存的改动,任何一次收起意图都要先问一句。

试试按 Escape、点面板外,或者按右上角的叉。

``` ### 拖动边缘改变厚度 面板中放一根把手,拖动时把新厚度写进 content 的 --xh-drawer-size;该槽覆盖 size 三档,滑入滑出仍按面板自身宽度计算 ```vue ``` ```html

字段设置

拖面板左边缘,厚度在 260 到 560 像素之间取值。

当前厚度:默认

``` ### 局部抽屉 把抽屉收进某块区域:遮罩与定位层从 fixed 换为 absolute,只覆盖该区域而不是整屏 ```vue ``` ```html

这块区域就是抽屉的容器:展开时遮罩只盖住它,页面其余部分照常可点。

局部抽屉

它贴的是这个容器的右沿,不是视口的右沿。

不写 contained 时遮罩与定位层还是 fixed,抽屉照旧铺满整屏——浮层写在哪里就在哪里, 自定义元素这一侧没有搬运这一步。

``` ## 设计指引 ### 何时使用 - 内容比对话框长(完整表单、详情),但仍属于当前上下文。 - 窄屏上的导航或筛选面板。 ### 何时不用 - 只确认一件事时,使用[对话框](./dialog)或[弹出确认](./popconfirm)。 - 内容需要与页面主体对照查看时,并排展开,不遮挡。 ### 特性 - `side` 决定滑出方向;`contained` 让它只占据某个容器而不是整个视口。 - `modal=false` 时不渲染遮罩,定位层也不截获页面指针;页面可以与抽屉并行交互。展开期间切换 `modal`,滚动锁、背景失活与焦点陷阱会同步切换。 - 可以拖动边缘调整厚度。 - 关闭时内容立即失活并退出可访问树;面板与遮罩全部完成退场后释放模态资源并发出 `onExitComplete` / `exit-complete`。退场中重开不会被旧完成关闭,卸载立即清理。 - 关闭前可以拦截,例如有未保存改动时先确认。 - 面板走 M4 sheet 三件套(1px 描边、不透明底、投影),边界由描边承担,不只靠影分层;入场是整面板从画外推入的大尺度位移,走 slide 时长与曲线,退场仍走 exit 档。 - 触发器与关闭按钮走 Action Control 家族配方:触发器为 text 档中性描边,展开期间压住为悬停同档的中性面;关闭按钮为 icon 档 ghost 面,悬停与按下沿画布承载阶梯换底;Space / Enter 与触屏按住期间投影 `data-pressed`。标题为 heading-3,说明文字为 13px 说明档。 - Body 是模态滚动面:滚到头不带动页面,内容高度变化时保留稳定的滚动条空道;不用三段结构时 content 自身是唯一滚动层。 ### 组合 - 内部放[表单](./form)、[侧栏导航](./side-nav);内容区使用[滚动区域](./scroll-area)。 ### 最佳实践 - 提交与取消固定在底部,用户不需要滚动到底部查找。 - 有未保存改动时拦截关闭。 ### 反模式 - 在抽屉内再打开抽屉。 - 在宽屏上用抽屉承载可以直接展开的内容。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhDrawerBody` `XhDrawerCloseTrigger` `XhDrawerContent` `XhDrawerDescription` `XhDrawerFooter` `XhDrawerHeader` `XhDrawerRoot` `XhDrawerTitle` `XhDrawerTrigger` | | 组合式函数 | `useDrawer` | | 状态机 | `drawerMachine` | | 皮肤 | `@xihan-ui/styles/drawer.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `open` | `boolean` | | | | `defaultOpen` | `boolean` | | | | `modal` | `boolean` | | 是否启用模态约束,默认 true。false 时不提供遮罩,页面其余部分保持可交互; 展开期间可以切换,滚动锁、背景失活与焦点陷阱会同步更新。 | | `contained` | `boolean` | | 浮层挂在局部容器中而不是视口:遮罩与定位层从 fixed 改为 absolute, 因此只覆盖该容器、不再覆盖整屏。 挂到哪个容器由适配器决定(Vue 由 root 的 container 决定,WC 本身是 Light DOM、 作者写在何处即在何处),这里只表达按局部容器绘制这一点。 | | `side` | `DrawerSide` | | 滑出的边,默认 'right'。只影响输出的 data-side,不参与状态转移。 | | `role` | `'dialog' \| 'alertdialog'` | | | | `closeOnEscape` | `boolean` | | | | `closeOnInteractOutside` | `boolean` | | | | `restoreFocus` | `boolean` | | | | `size` | `Size` | | 尺寸:sm / md / lg。横向放置时影响面板宽度、纵向放置时影响面板高度,随 side 而定。 | | `variant` | `OverlayBackdropVariant` | | 遮罩形态:opaque / blur / transparent。写在 backdrop 上,只影响该层的底色与模糊。 | | `translations` | `Partial` | | | | `onOpenChange` | `(details: DrawerOpenChangeDetails) => void` | | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | | `onExitComplete` | `() => void` | | 退出动画结束或取消,且本层资源全部释放后通知;卸载和重新打开不通知。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `exit-complete` | `CustomEvent` | 退出完成且本层资源已释放 | | `open-change` | `DrawerOpenChangeDetails` | open 状态变化;detail 为 `{ open: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhDrawerRoot` | `default` | `DrawerRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhDrawerRoot` | `container` | `() => Element \| null` | | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 提供后即为局部抽屉:遮罩与定位层从 fixed 换为 absolute,只覆盖该容器而不是整屏。 该容器要自带 position(relative 等),否则 absolute 会向上找到其他定位祖先。 | | `XhDrawerRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'open' \| 'closed' | | `trigger` | 'open' \| 'closed' | | `backdrop` | 'open' \| 'closed' | | `positioner` | 'open' \| 'closed' | | `content` | 'open' \| 'closed' | 以下名称仅用于内部状态机。 **状态**:`open` · `closed` **事件**:`OPEN` · `TOGGLE` · `CLOSE` · `CONTROLLED.OPEN` · `CONTROLLED.CLOSE` · `PRESS.START` · `PRESS.END` **判据**:`isOpenControlled` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `open` | `boolean` | | | `side` | `DrawerSide` | 已解析的滑出边(prop 未提供时是默认值),作者据此配置动画。 | | `setOpen` | `(next: boolean) => void` | | | `getRootProps` | `() => T['element']` | | | `getTriggerProps` | `() => T['button']` | | | `getBackdropProps` | `() => T['element']` | | | `getPositionerProps` | `() => T['element']` | | | `getContentProps` | `() => T['element']` | | | `getHeaderProps` | `() => T['element']` | | | `getTitleProps` | `() => T['element']` | | | `getDescriptionProps` | `() => T['element']` | | | `getBodyProps` | `() => T['element']` | | | `getFooterProps` | `() => T['element']` | | | `getCloseTriggerProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/#keyboardinteraction) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | focus in trigger | 打开抽屉并把焦点移入 content | | `Escape` | open | 关闭并把焦点还给 trigger | | `Tab` | open 且 modal | 在 content 内向后循环焦点 | | `Shift+Tab` | open 且 modal | 在 content 内向前循环焦点 | | `Enter` / `Space` | held in trigger / close-trigger | 按住期间该按钮投影 data-pressed,与指针 :active 同一副按压面;抬起、失焦或抽屉收起撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `trigger` | `aria-controls` | `content` 部件的 id | | `trigger` | `aria-expanded` | 'true' \| 'false' | | `trigger` | `aria-haspopup` | 'dialog' | | `content` | `aria-describedby` | `description` 部件的 id | | `content` | `aria-hidden` | !open \|\| undefined | | `content` | `aria-labelledby` | `title` 部件的 id | | `content` | `aria-modal` | 'true' \| 'false' | | `content` | `role` | props.role | | `close-trigger` | `aria-label` | props.translations.close | ## 样式参考 ### 皮肤 `@xihan-ui/styles/drawer.css` 使用 `[data-scope="drawer"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-contained` | ''(条件成立时才出现) | | `root` | `data-side` | props.side | | `root` | `data-size` | props.size | | `root` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-pressed` | ''(条件成立时才出现) | | `trigger` | `data-state` | 'open' \| 'closed' | | `trigger` | `data-xh-action-control` | '' | | `trigger` | `data-xh-action-display` | 'always' | | `trigger` | `data-xh-action-profile` | 'text' | | `trigger` | `data-xh-action-size` | 'md' | | `trigger` | `data-xh-action-variant` | 'outline' | | `backdrop` | `data-contained` | ''(条件成立时才出现) | | `backdrop` | `data-state` | 'open' \| 'closed' | | `backdrop` | `data-variant` | props.variant | | `positioner` | `data-contained` | ''(条件成立时才出现) | | `positioner` | `data-positioned` | '' | | `positioner` | `data-state` | 'open' \| 'closed' | | `content` | `data-contained` | ''(条件成立时才出现) | | `content` | `data-side` | props.side | | `content` | `data-size` | props.size | | `content` | `data-state` | 'open' \| 'closed' | | `close-trigger` | `data-pressed` | ''(条件成立时才出现) | | `close-trigger` | `data-xh-action-control` | '' | | `close-trigger` | `data-xh-action-display` | 'always' | | `close-trigger` | `data-xh-action-profile` | 'icon' | | `close-trigger` | `data-xh-action-size` | 'sm' | | `close-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-drawer-backdrop-bg` | `backdrop` | `background` | `default` | `--xh-bg-overlay` | drawer 的 backdrop 部件 background 覆盖槽。 | | `--xh-drawer-backdrop-blur` | `backdrop` | `-webkit-backdrop-filter`
`backdrop-filter` | `variant=blur` | `--xh-overlay-backdrop-blur` | drawer 的 backdrop 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 | | `--xh-drawer-backdrop-layer` | `backdrop` | `z-index` | `default` | `--xh-_layer` | drawer 的 backdrop 部件 z-index 覆盖槽。 | | `--xh-drawer-bg` | `content` | `background` | `default` | `--xh-material-elevated-bg` | drawer 的 content 部件 background 覆盖槽。 | | `--xh-drawer-border` | `content` | `border` | `default` | `--xh-material-elevated-border` | drawer 的 content 部件 border 覆盖槽。 | | `--xh-drawer-close-bg-active` | `close-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | drawer 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-drawer-close-bg-focus` | `close-trigger` | `background-color` | `focus-visible` | `--xh-_action-variant-bg-focus-visible` | drawer 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-drawer-close-bg-hover` | `close-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | drawer 的 close-trigger 部件 background-color 覆盖槽。 | | `--xh-drawer-close-fg` | `close-trigger` | `color` | `default` | `--xh-fg-muted` | drawer 的 close-trigger 部件 color 覆盖槽。 | | `--xh-drawer-close-fg-focus` | `close-trigger` | `color` | `focus-visible` | `--xh-drawer-close-fg-hover` | drawer 的 close-trigger 部件 color 覆盖槽。 | | `--xh-drawer-close-fg-hover` | `close-trigger` | `color` | `disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-focus-visible`
`--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed` | drawer 的 close-trigger 部件 color 覆盖槽。 | | `--xh-drawer-close-radius` | `close-trigger` | `border-radius` | `default` | `--xh-shape-control` | drawer 的 close-trigger 部件 border-radius 覆盖槽。 | | `--xh-drawer-close-size` | `close-trigger`
`content`
`title` | `block-size`
`inline-size`
`padding-inline-end` | `default`
`has([data-scope='drawer'][data-part='close-trigger'])`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size`
`--xh-control-h-sm` | drawer 的 close-trigger、content、title 部件 block-size、inline-size、padding-inline-end 覆盖槽。 | | `--xh-drawer-description-fg` | `description` | `color` | `default` | `--xh-fg-muted` | drawer 的 description 部件 color 覆盖槽。 | | `--xh-drawer-description-font-size` | `description` | `font-size` | `default` | `--xh-text-secondary-size` | drawer 的 description 部件 font-size 覆盖槽。 | | `--xh-drawer-fg` | `content` | `color` | `default` | `--xh-material-elevated-fg` | drawer 的 content 部件 color 覆盖槽。 | | `--xh-drawer-footer-gap` | `footer` | `gap` | `default` | `--xh-control-gap-md` | drawer 的 footer 部件 gap 覆盖槽。 | | `--xh-drawer-footer-pt` | `footer` | `padding-block-start` | `default` | `--xh-space-2` | drawer 的 footer 部件 padding-block-start 覆盖槽。 | | `--xh-drawer-gap` | `content` | `gap` | `default` | `--xh-stack-gap-md` | drawer 的 content 部件 gap 覆盖槽。 | | `--xh-drawer-header-gap` | `header` | `gap` | `default` | `--xh-stack-gap-sm` | drawer 的 header 部件 gap 覆盖槽。 | | `--xh-drawer-header-pb` | `header` | `padding-block-end` | `default` | `--xh-space-2` | drawer 的 header 部件 padding-block-end 覆盖槽。 | | `--xh-drawer-icon-size` | `close-trigger`
`content`
`root`
`trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size`
`--xh-glyph-size-md` | drawer 的 close-trigger、content、root、trigger 部件 --xh-icon-size 覆盖槽。 | | `--xh-drawer-layer` | `content`
`positioner` | `z-index` | `default` | `--xh-_layer` | drawer 的 content、positioner 部件 z-index 覆盖槽。 | | `--xh-drawer-px` | `content` | `padding-inline` | `contained`
`default` | `--xh-surface-px-md` | drawer 的 content 部件 padding-inline 覆盖槽。 | | `--xh-drawer-py` | `content` | `padding-block-end`
`padding-block-start` | `contained`
`default` | `--xh-surface-py-md` | drawer 的 content 部件 padding-block-end、padding-block-start 覆盖槽。 | | `--xh-drawer-radius` | `content` | `border-end-end-radius`
`border-end-start-radius`
`border-start-end-radius`
`border-start-start-radius` | `side=bottom`
`side=left`
`side=right`
`side=top` | `--xh-shape-overlay` | drawer 的 content 部件 border-end-end-radius、border-end-start-radius、border-start-end-radius、border-start-start-radius 覆盖槽。 | | `--xh-drawer-shadow` | `content` | `box-shadow` | `default` | `--xh-material-elevated-shadow` | drawer 的 content 部件 box-shadow 覆盖槽。 | | `--xh-drawer-size` | `content` | `block-size`
`inline-size` | `side=bottom`
`side=left`
`side=right`
`side=top` | `--xh-_drawer-size` | drawer 的 content 部件 block-size、inline-size 覆盖槽。 | | `--xh-drawer-title-fg` | `title` | `color` | `default` | `--xh-fg-default` | drawer 的 title 部件 color 覆盖槽。 | | `--xh-drawer-title-font-size` | `title` | `font-size` | `default` | `--xh-text-heading-3-size` | drawer 的 title 部件 font-size 覆盖槽。 | | `--xh-drawer-title-font-weight` | `title` | `font-weight` | `default` | `--xh-text-heading-3-weight` | drawer 的 title 部件 font-weight 覆盖槽。 | | `--xh-drawer-trigger-bg` | `trigger` | `--xh-ink-surface`
`background-color` | `default`
`focus-visible`
`xh-ink-surface` | `--xh-_action-variant-bg-focus-visible`
`--xh-_action-variant-bg-rest` | drawer 的 trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-drawer-trigger-bg-active` | `trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | drawer 的 trigger 部件 background-color 覆盖槽。 | | `--xh-drawer-trigger-bg-hover` | `trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | drawer 的 trigger 部件 background-color 覆盖槽。 | | `--xh-drawer-trigger-bg-open` | `trigger` | `--xh-ink-surface`
`background-color` | `focus-visible`
`state=open`
`xh-ink-surface` | `--xh-bg-subtle` | drawer 的 trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-drawer-trigger-border` | `trigger` | `border`
`border-color` | `default`
`focus-visible` | `--xh-_action-variant-border-focus-visible`
`--xh-_action-variant-border-rest` | drawer 的 trigger 部件 border、border-color 覆盖槽。 | | `--xh-drawer-trigger-border-hover` | `trigger` | `border-color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-border-hover`
`--xh-_action-variant-border-pressed` | drawer 的 trigger 部件 border-color 覆盖槽。 | | `--xh-drawer-trigger-border-open` | `trigger` | `border`
`border-color` | `focus-visible`
`state=open` | `--xh-border-control-hover` | drawer 的 trigger 部件 border、border-color 覆盖槽。 | | `--xh-drawer-trigger-fg` | `trigger` | `color` | `default`
`disabled`
`focus-visible`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-fg-focus-visible`
`--xh-_action-variant-fg-hover`
`--xh-_action-variant-fg-pressed`
`--xh-_action-variant-fg-rest` | drawer 的 trigger 部件 color 覆盖槽。 | | `--xh-drawer-trigger-font-size` | `trigger` | `font-size` | `default` | `--xh-_action-profile-font-size` | drawer 的 trigger 部件 font-size 覆盖槽。 | | `--xh-drawer-trigger-font-weight` | `trigger` | `font-weight` | `default` | `--xh-text-label-weight` | drawer 的 trigger 部件 font-weight 覆盖槽。 | | `--xh-drawer-trigger-gap` | `trigger` | `gap` | `default` | `--xh-_action-profile-gap` | drawer 的 trigger 部件 gap 覆盖槽。 | | `--xh-drawer-trigger-h` | `trigger` | `block-size`
`inline-size` | `default`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size` | drawer 的 trigger 部件 block-size、inline-size 覆盖槽。 | | `--xh-drawer-trigger-px` | `trigger` | `padding-inline` | `default` | `--xh-_action-profile-padding-inline` | drawer 的 trigger 部件 padding-inline 覆盖槽。 | | `--xh-drawer-trigger-radius` | `trigger` | `border-radius` | `default` | `--xh-shape-control` | drawer 的 trigger 部件 border-radius 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 出现 · 导航(整幅滑入)(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-fade-in` · `xh-fade-out` · `xh-slide-fade-in` · `xh-slide-fade-out` · `xh-slide-in` · `xh-slide-out` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/editable # Editable 就地编辑 用于在当前位置查看和编辑短文本。 ## 用法 点击文本就地编辑 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="editable"`:**`root`** · `label` · `control` · **`preview`** · **`input`** · `edit-trigger` · `submit-trigger` · `cancel-trigger` ## 示例 ### 提交方式 使用失焦或回车提交 ```vue ``` ```html
``` ### 状态 禁用、只读与空值 ```vue ``` ```html
``` ### 变体 设置编辑框外观 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 编辑标题、昵称或简短备注。 - 在列表和表格中快速修改单个值。 ### 何时不用 - 一次修改多个字段时,使用表单或[对话框](./dialog)。 - 值需要复杂校验或多步确认。 ### 特性 - `submitMode` 设置回车、失焦或显式按钮提交。 - `activationMode` 设置单击、双击或按钮激活。 - 支持提交、取消、受控值和受控编辑状态。 - `autoResize` 让输入框随内容调整宽度。 - 标题在上;预览文字或输入框与右侧动作组共用一个字段边框和背景,不把动作按钮挂在编辑框外。预览态只显示编辑按钮,编辑态只显示确认与取消按钮。 - 三个动作使用图标呈现:编辑、确认、取消;图标按钮必须提供可访问名称。 ### 组合 - 常放在[列表](./list)、[表格](./table)单元格或[页头](./page-header)标题中,就地修改一个值。 - 三个动作按钮可替换为自定义[图标](./icon);需要修改多个字段时改用[表单](./form)加[对话框](./dialog)。 ### 最佳实践 - 为展示态提供清晰的可编辑提示。 - 保持预览态和编辑态高度一致。 - 保留 Escape 取消并还原原值。 - 使用空按钮时由皮肤绘制默认图标,并通过 `aria-label` 写明编辑、确认与取消。 ### 反模式 - 使用失焦提交但不提供取消方式。 - 用就地编辑处理长文本或复杂表单。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhEditableCancelTrigger` `XhEditableControl` `XhEditableEditTrigger` `XhEditableInput` `XhEditableLabel` `XhEditablePreview` `XhEditableRoot` `XhEditableSubmitTrigger` | | 组合式函数 | `useEditable` | | 状态机 | `editableMachine` | | 皮肤 | `@xihan-ui/styles/editable.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | | 受控值;提供后由宿主决定,状态机不自行修改(cell 原生受控,无影子事件)。 | | `defaultValue` | `string` | | 非受控初值。 | | `edit` | `boolean` | | 受控编辑态;提供后由宿主决定,用户交互只发 onEditChange。 | | `defaultEdit` | `boolean` | | 非受控初始编辑态。为真时挂载即进入编辑态并把焦点移入输入框。 | | `placeholder` | `string` | | 值为空时预览区显示它,输入框也将其用作占位。 | | `disabled` | `boolean` | | 禁用:无法进入编辑态,输入框带原生 disabled。 | | `readOnly` | `boolean` | | 只读:无法进入编辑态,但已在编辑态时仍能退出(撤销 / 提交都可用)。 | | `invalid` | `boolean` | | 校验失败标注。 | | `maxLength` | `number` | | 字符数上限;同时落为原生 maxlength 与状态机侧截断。 | | `name` | `string` | | 表单字段名;提供后输入框才参与提交。 | | `submitMode` | `EditableSubmitMode` | | 编辑态的收尾方式,默认 both。 | | `activationMode` | `EditableActivationMode` | | 预览区的激活方式,默认 click。 | | `selectOnFocus` | `boolean` | | 进入编辑态时全选已有内容,默认开启。关闭则光标停在原处。 | | `autoResize` | `boolean` | | 输入框宽度跟随内容:连接层把字符数写为原生 size 属性。 | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定底色与描边的绘制方式。默认 outline。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定 control 的聚焦描边与焦点环颜色。 | | `size` | `Size` | | 尺寸:sm / md / lg,决定 control、预览区、输入框与三个动作按钮的几何档位。 | | `onValueChange` | `(details: EditableValueChangeDetails) => void` | | 值变化意图回调;编辑途中每次输入都发出,受控时是唯一出口。 | | `onValueCommit` | `(details: EditableValueCommitDetails) => void` | | 提交时才发出;编辑途中的输入不触发它。 | | `onValueRevert` | `(details: EditableValueRevertDetails) => void` | | 撤销时发出(Escape、取消按钮、不视为提交的离场)。 | | `onEditChange` | `(details: EditableEditChangeDetails) => void` | | 编辑态变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `EditableValueChangeDetails` | 编辑途中的值变化;detail 为 `{ value: string }` | | `value-commit` | `EditableValueCommitDetails` | 提交;detail 为 `{ value: string, previousValue: string }` | | `value-revert` | `EditableValueRevertDetails` | 撤销;detail 为 `{ value: string, discardedValue: string }` | | `edit-change` | `EditableEditChangeDetails` | 编辑态变化;detail 为 `{ edit: boolean }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhEditableRoot` | `default` | `EditableRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhEditableRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `root` | 'edit' \| 'preview' | | `label` | 'edit' \| 'preview' | | `control` | 'edit' \| 'preview' | | `preview` | 'edit' \| 'preview' | | `input` | 'edit' \| 'preview' | | `edit-trigger` | 'edit' \| 'preview' | | `submit-trigger` | 'edit' \| 'preview' | | `cancel-trigger` | 'edit' \| 'preview' | 以下名称仅用于内部状态机。 **状态**:`preview` · `edit` **事件**:`EDIT.START` · `EDIT.SUBMIT` · `EDIT.CANCEL` · `EDIT.LEAVE` · `VALUE.SET` · `CONTROLLED.EDIT` · `CONTROLLED.PREVIEW` · `FORM.RESET` · `PRESS.START` · `PRESS.END` **判据**:`isEditControlled` · `canEdit` · `submitsOnLeave` · `canPressEditTrigger` · `canPressEditControls` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string` | 当前的值(编辑途中即输入框中的内容)。 | | `committedValue` | `string` | 上一次提交的值,也是撤销的落点。 | | `editing` | `boolean` | 处于编辑态。 | | `empty` | `boolean` | 值为空串。 | | `displayValue` | `string` | 预览区当前应显示的文字:值为空时回退为 placeholder。 | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | | | `interactive` | `boolean` | 可以进入编辑态(既未禁用也不只读)。 | | `setValue` | `(next: string) => void` | 直接写值,只受 disabled / readOnly 与 maxLength 约束,与编辑态无关。 | | `edit` | `() => void` | 进入编辑态;禁用或只读时不生效。 | | `submit` | `() => void` | 提交当前的值并回到预览态。 | | `cancel` | `() => void` | 撤销回上一次提交的值并回到预览态。 | | `getRootProps` | `() => T['element']` | | | `getLabelProps` | `() => T['label']` | | | `getPreviewProps` | `() => T['element']` | | | `getInputProps` | `() => T['input']` | | | `getEditTriggerProps` | `() => T['button']` | | | `getSubmitTriggerProps` | `() => T['button']` | | | `getCancelTriggerProps` | `() => T['button']` | | | `getControlProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://html.spec.whatwg.org/multipage/input.html#text-(type=text)-state-and-search-state-(type=search)) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` | focus in input, submitMode 为 enter 或 both | 提交当下的值并回到预览态;其余模式不接管该键,交回给浏览器与外层表单 | | `Escape` | focus in input | 撤销回上一次提交的值并回到预览态 | | `Tab` / `Shift+Tab` | focus in input | 按 submitMode 收尾(blur/both 提交,enter/none 撤销);不拦默认行为,焦点照常移出 | | `Enter` / `Space` | held on edit-trigger(预览态,not disabled/readOnly)或 submit-trigger / cancel-trigger(编辑态) | 按住期间这颗钮投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,进出编辑态后按钮藏起一并撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `aria-labelledby` | `label` 部件的 id | | `root` | `role` | 'group' | | `preview` | `aria-disabled` | 'true' \| 'false' | | `input` | `aria-invalid` | 'true' \| 'false' | | `input` | `aria-labelledby` | `label` 部件的 id | | `edit-trigger` | `aria-controls` | `input` 部件的 id | ## 样式参考 ### 皮肤 `@xihan-ui/styles/editable.css` 使用 `[data-scope="editable"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 `forced-colors: active` 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-empty` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `root` | `data-size` | props.size | | `root` | `data-state` | 'edit' \| 'preview' | | `root` | `data-tone` | props.tone | | `root` | `data-variant` | props.variant | | `label` | `data-disabled` | ''(条件成立时才出现) | | `label` | `data-state` | 'edit' \| 'preview' | | `control` | `data-disabled` | ''(条件成立时才出现) | | `control` | `data-invalid` | ''(条件成立时才出现) | | `control` | `data-readonly` | ''(条件成立时才出现) | | `control` | `data-state` | 'edit' \| 'preview' | | `control` | `data-variant` | props.variant | | `control` | `data-xh-field-chrome` | '' | | `control` | `data-xh-field-size` | props.size | | `preview` | `data-activation-mode` | props.activationMode | | `preview` | `data-disabled` | ''(条件成立时才出现) | | `preview` | `data-invalid` | ''(条件成立时才出现) | | `preview` | `data-placeholder` | ''(条件成立时才出现) | | `preview` | `data-readonly` | ''(条件成立时才出现) | | `preview` | `data-state` | 'edit' \| 'preview' | | `input` | `data-auto-resize` | ''(条件成立时才出现) | | `input` | `data-disabled` | ''(条件成立时才出现) | | `input` | `data-invalid` | ''(条件成立时才出现) | | `input` | `data-readonly` | ''(条件成立时才出现) | | `input` | `data-state` | 'edit' \| 'preview' | | `input` | `data-xh-field-input` | '' | | `input` | `data-xh-field-layout` | 'single-line' | | `edit-trigger` | `data-disabled` | ''(条件成立时才出现) | | `edit-trigger` | `data-pressed` | ''(条件成立时才出现) | | `edit-trigger` | `data-state` | 'edit' \| 'preview' | | `edit-trigger` | `data-xh-action-control` | '' | | `edit-trigger` | `data-xh-action-display` | 'always' | | `edit-trigger` | `data-xh-action-profile` | 'field-inset' | | `edit-trigger` | `data-xh-action-size` | props.size | | `edit-trigger` | `data-xh-action-variant` | 'ghost' | | `submit-trigger` | `data-disabled` | ''(条件成立时才出现) | | `submit-trigger` | `data-pressed` | ''(条件成立时才出现) | | `submit-trigger` | `data-state` | 'edit' \| 'preview' | | `submit-trigger` | `data-xh-action-control` | '' | | `submit-trigger` | `data-xh-action-display` | 'always' | | `submit-trigger` | `data-xh-action-profile` | 'field-inset' | | `submit-trigger` | `data-xh-action-size` | props.size | | `submit-trigger` | `data-xh-action-variant` | 'ghost' | | `cancel-trigger` | `data-disabled` | ''(条件成立时才出现) | | `cancel-trigger` | `data-pressed` | ''(条件成立时才出现) | | `cancel-trigger` | `data-state` | 'edit' \| 'preview' | | `cancel-trigger` | `data-xh-action-control` | '' | | `cancel-trigger` | `data-xh-action-display` | 'always' | | `cancel-trigger` | `data-xh-action-profile` | 'field-inset' | | `cancel-trigger` | `data-xh-action-size` | props.size | | `cancel-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-editable-control-bg` | `control` | `background-color` | `xh-field-chrome` | `--xh-_field-variant-bg-rest` | editable 的 control 部件 background-color 覆盖槽。 | | `--xh-editable-control-bg-disabled` | `control` | `background-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-bg-disabled` | editable 的 control 部件 background-color 覆盖槽。 | | `--xh-editable-control-bg-hover` | `control` | `background-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-hover` | editable 的 control 部件 background-color 覆盖槽。 | | `--xh-editable-control-bg-readonly` | `control` | `background-color` | `readonly`
`xh-field-chrome` | `--xh-_field-variant-bg-read-only` | editable 的 control 部件 background-color 覆盖槽。 | | `--xh-editable-control-border` | `control` | `border` | `xh-field-chrome` | `--xh-_field-variant-border-rest` | editable 的 control 部件 border 覆盖槽。 | | `--xh-editable-control-border-disabled` | `control` | `border-color` | `disabled`
`xh-field-chrome` | `--xh-_field-variant-border-disabled` | editable 的 control 部件 border-color 覆盖槽。 | | `--xh-editable-control-border-focus` | `control` | `border-color` | `disabled`
`focus-within`
`not([data-disabled])`
`xh-field-chrome` | `--xh-_field-variant-border-focus` | editable 的 control 部件 border-color 覆盖槽。 | | `--xh-editable-control-border-hover` | `control` | `border-color` | `disabled`
`hover`
`invalid`
`loading`
`not([data-disabled])`
`not([data-invalid])`
`not([data-loading])`
`not([data-readonly])`
`readonly`
`xh-field-chrome` | `--xh-_field-variant-border-hover` | editable 的 control 部件 border-color 覆盖槽。 | | `--xh-editable-control-border-invalid` | `control` | `border-color` | `invalid`
`xh-field-chrome` | `--xh-_field-variant-border-invalid` | editable 的 control 部件 border-color 覆盖槽。 | | `--xh-editable-control-fg` | `control` | `color` | `xh-field-chrome` | `--xh-fg-default` | editable 的 control 部件 color 覆盖槽。 | | `--xh-editable-control-gap` | `control` | `gap` | `xh-field-chrome` | `--xh-_editable-control-gap` | editable 的 control 部件 gap 覆盖槽。 | | `--xh-editable-control-h` | `control` | `block-size`
`min-block-size` | `has([data-xh-field-input][data-xh-field-layout='multi-tag'])`
`has([data-xh-field-input][data-xh-field-layout='single-line'])`
`has([data-xh-field-input][data-xh-field-layout='textarea'])`
`xh-field-chrome`
`xh-field-input`
`xh-field-layout=multi-tag`
`xh-field-layout=single-line`
`xh-field-layout=textarea` | `--xh-_editable-h` | editable 的 control 部件 block-size、min-block-size 覆盖槽。 | | `--xh-editable-control-min-w` | `control`
`root` | `min-inline-size` | `default`
`xh-field-chrome` | `--xh-control-min-w` | editable 的 control、root 部件 min-inline-size 覆盖槽。 | | `--xh-editable-control-px` | `control` | `padding-inline` | `xh-field-chrome` | `0` | editable 的 control 部件 padding-inline 覆盖槽。 | | `--xh-editable-control-radius` | `control` | `border-radius` | `xh-field-chrome` | `--xh-shape-control` | editable 的 control 部件 border-radius 覆盖槽。 | | `--xh-editable-control-shadow` | `control` | `box-shadow` | `xh-field-chrome` | `none` | editable 的 control 部件 box-shadow 覆盖槽。 | | `--xh-editable-control-w` | `root` | `inline-size`
`min-inline-size` | `default` | `--xh-control-w` | editable 的 root 部件 inline-size、min-inline-size 覆盖槽。 | | `--xh-editable-gap` | `root` | `gap` | `default` | `--xh-space-1` | editable 的 root 部件 gap 覆盖槽。 | | `--xh-editable-icon-size` | `control`
`root` | `--xh-icon-size` | `default`
`size=lg`
`size=sm`
`xh-field-chrome` | `--xh-_field-size-glyph-size`
`--xh-glyph-size-lg`
`--xh-glyph-size-md`
`--xh-glyph-size-sm` | editable 的 control、root 部件 --xh-icon-size 覆盖槽。 | | `--xh-editable-input-autofill-bg` | `input` | `box-shadow` | `-webkit-autofill`
`autofill`
`xh-field-input` | `--xh-bg-canvas` | editable 的 input 部件 box-shadow 覆盖槽。 | | `--xh-editable-input-autofill-fg` | `input` | `-webkit-text-fill-color` | `-webkit-autofill`
`autofill`
`xh-field-input` | `--xh-fg-default` | editable 的 input 部件 -webkit-text-fill-color 覆盖槽。 | | `--xh-editable-input-fg` | `input` | `color` | `xh-field-input` | `--xh-fg-default` | editable 的 input 部件 color 覆盖槽。 | | `--xh-editable-input-font-size` | `input` | `font-size` | `xh-field-input` | `--xh-_editable-font-size` | editable 的 input 部件 font-size 覆盖槽。 | | `--xh-editable-input-px` | `input` | `padding-inline` | `default` | `--xh-_editable-px` | editable 的 input 部件 padding-inline 覆盖槽。 | | `--xh-editable-label-fg` | `label` | `color` | `default` | `--xh-fg-default` | editable 的 label 部件 color 覆盖槽。 | | `--xh-editable-label-fg-disabled` | `label` | `color` | `disabled` | `--xh-fg-subtle` | editable 的 label 部件 color 覆盖槽。 | | `--xh-editable-label-font-size` | `label` | `font-size` | `default` | `--xh-text-label-size` | editable 的 label 部件 font-size 覆盖槽。 | | `--xh-editable-label-font-weight` | `label` | `font-weight` | `default` | `--xh-text-label-weight` | editable 的 label 部件 font-weight 覆盖槽。 | | `--xh-editable-placeholder-fg` | `input`
`preview` | `color` | `placeholder`
`xh-field-input` | `--xh-fg-subtle` | editable 的 input、preview 部件 color 覆盖槽。 | | `--xh-editable-preview-fg` | `preview` | `color` | `default` | `--xh-fg-default` | editable 的 preview 部件 color 覆盖槽。 | | `--xh-editable-preview-font-size` | `preview` | `font-size` | `default` | `--xh-_editable-font-size` | editable 的 preview 部件 font-size 覆盖槽。 | | `--xh-editable-preview-min-h` | `preview` | `min-block-size` | `default` | `--xh-_editable-h` | editable 的 preview 部件 min-block-size 覆盖槽。 | | `--xh-editable-preview-px` | `preview` | `padding-inline` | `default` | `--xh-_editable-px` | editable 的 preview 部件 padding-inline 覆盖槽。 | | `--xh-editable-trigger-bg` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-editable-trigger-bg-active` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 background-color 覆盖槽。 | | `--xh-editable-trigger-bg-disabled` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `--xh-ink-surface`
`background-color` | `disabled`
`xh-ink-surface` | `--xh-_action-variant-bg-disabled` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-editable-trigger-bg-hover` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 background-color 覆盖槽。 | | `--xh-editable-trigger-divider` | `edit-trigger`
`submit-trigger` | `background-image` | `default` | `--xh-material-soft-separator` | editable 的 edit-trigger、submit-trigger 部件 background-image 覆盖槽。 | | `--xh-editable-trigger-divider-h` | `edit-trigger`
`submit-trigger` | `background-size` | `default` | `--xh-_editable-divider-h` | editable 的 edit-trigger、submit-trigger 部件 background-size 覆盖槽。 | | `--xh-editable-trigger-fg` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `color` | `default` | `--xh-fg-default` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 color 覆盖槽。 | | `--xh-editable-trigger-fg-hover` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-fg-default` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 color 覆盖槽。 | | `--xh-editable-trigger-font-size` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `font-size` | `default` | `--xh-text-secondary-size` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 font-size 覆盖槽。 | | `--xh-editable-trigger-radius` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `border-radius` | `default` | `--xh-shape-inset` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 border-radius 覆盖槽。 | | `--xh-editable-trigger-size` | `cancel-trigger`
`edit-trigger`
`submit-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=field-inset` | `--xh-_action-profile-visual-size` | editable 的 cancel-trigger、edit-trigger、submit-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | ### 动效 动效角色:按压 · 状态(见[动效规范](../design/motion#角色))。 本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像;另有按 `dir` 分支的规则。 --- 来源:https://ui.docs.xihanfun.com/components/empty-state # EmptyState 空状态 没有数据时的占位区域:说明为什么为空,以及可以做什么。 空状态与结果页共用同一套结构:图标、标题、说明、操作四段与整页结果完全一致,404、403、500 等结果页也使用本组件。`status` 只接受这三个状态码,只落为 root 的 `data-status`,皮肤据此把图标区并入最接近的一族语气色,不改变语义、不带插画资源;成功、警示、出错、提示等通用结果使用全库统一的 `tone` 轴。 ## 用法 图标、标题、说明、操作四个槽都可选,只有 root 是必需的 ```vue ``` ```html
∅

还没有任何工单

新建一条工单,或者换个筛选条件再看看。

``` ## 组件结构 加粗的是必需部件。 `data-scope="empty-state"`:**`root`** · `media` · `indicator` · `title` · `description` · `action` ## 示例 ### 尺寸 size 只改变留白与字号,语义不变;不传即 md ```vue ``` ```html
∅

sm

塞进侧栏或卡片里的那一档。

∅

md

缺省档,列表与表格用它。

∅

lg

整页只有这一块时用它。

``` ### 播报方式 默认 polite 使 root 成为活区,筛选完成后就地播报;off 使它只是一个普通容器 ```vue ``` ```html
∅

没有匹配「曦寒」的结果

换个词,或者去掉几个筛选条件。

``` ### 用作结果页 同一套部件也承载 404、403 等结果:status 为图标区上语气色,操作槽中放置回退出口 ```vue ``` ```html
?

404 页面不存在

地址可能敲错了,或者这条记录已经被删掉。

⊘

403 没有权限

这块内容需要更高的角色,找管理员要一下。

!

500 服务出错

请求没能处理完,稍后再试一次。

``` ### 图标自带语气 图标槽中放置一个带 tone 的图标,着色落在图标自身上,不经过根上的 tone ```vue ``` ```html

全部导入成功

128 条记录已入库,没有需要人工处理的行。

部分行被跳过

有 6 行缺少必填字段,这次没有导入它们。

导入没有完成

文件读到一半中断,这次改动已经整体回滚。

``` ### 颜色 tone 为图标区上语气色,与全库同一根轴;绘制什么图标仍由作者放置 ```vue ``` ```html

全部导入成功

128 条记录已入库。

部分行被跳过

有 6 行缺少必填字段。

导入没有完成

这次改动已经整体回滚。

任务已排队

前面还有 3 个任务在跑。

``` ## 设计指引 ### 何时使用 - 列表、表格、搜索结果为空。 - 首次使用、还没有任何数据。 ### 何时不用 - 数据加载中时,使用[骨架屏](./skeleton)或[加载指示器](./spinner)。 - 一次轻量操作的反馈使用[轻提示](./toast)。 ### 特性 - 图标、标题、描述、操作四段都可选。 - `live` 决定内容出现时读屏如何播报,搜索结果变空时尤其重要。 - `status` 只接受 404 / 403 / 500 三个状态码,各并入最接近的一族语气色;`tone` 直接指定语气,两者都写时以 `tone` 为准。 - 开幕只在出现时播放:页面加载完成之前挂上或服务端渲染后水合的空状态直接呈现;筛选、删除或新数据带来的出现,以及 root 从 `hidden` 恢复显示,图标、标题、说明、操作依次开幕。 ### 组合 - 图标使用[图标块](./icon-wrapper);操作使用[按钮](./button)。 ### 最佳实践 - 区分三种空:从未有数据、筛选后为空、搜索无结果,三者的文案完全不同。 - 提供一条出路:新建、清除筛选、更换关键词。 - 用作结果页时每一页都提供回退出口:回首页、重试、联系支持,403 与 500 尤其需要。 - 失败页提供可追溯的标识(请求号、时间),便于用户报障。 ### 反模式 - 只显示一个空盒子加“暂无数据”,用户不知道下一步做什么。 - 首次使用的空状态与筛选无结果的外观相同。 - 只写“出错了”,不说明错误内容,也不提供下一步。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhEmptyStateAction` `XhEmptyStateDescription` `XhEmptyStateIndicator` `XhEmptyStateMedia` `XhEmptyStateRoot` `XhEmptyStateTitle` | | 状态机 | `emptyStateMachine` | | 皮肤 | `@xihan-ui/styles/empty-state.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `live` | `EmptyStateLive` | | 默认 polite。 | | `size` | `Size` | | 尺寸档位,只影响留白与字号,不改变语义。 | | `status` | `EmptyStateStatus` | | 结果页的状态码,只写为 root 的 data-status;皮肤据此把图标区并入最接近的一族语气色,图标内容由作者放入图标槽。 | | `tone` | `Tone` | | 语气:brand / neutral / success / warning / danger / info,决定图标区使用哪族颜色;与 status 都提供时以它为准。未提供时保持中性。 | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`APPEARANCE.RELEASE` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `live` | `EmptyStateLive` | 生效的播报方式,默认值补齐后的结果。 | | `getRootProps` | `() => T['element']` | | | `getMediaProps` | `() => T['element']` | 插画槽:按自身的尺寸档测量,与字形槽二选一。 | | `getIndicatorProps` | `() => T['element']` | | | `getTitleProps` | `() => T['element']` | | | `getDescriptionProps` | `() => T['element']` | | | `getActionProps` | `() => T['element']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/practices/live-regions/) 无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。 ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `role` | undefined \| 'status' | | `media` | `aria-hidden` | 'true' | | `indicator` | `aria-hidden` | 'true' | ## 样式参考 ### 皮肤 `@xihan-ui/styles/empty-state.css` 使用 `[data-scope="empty-state"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-size` | props.size | | `root` | `data-status` | props.status | | `root` | `data-tone` | props.tone | | `media` | `data-instant` | ''(条件成立时才出现) | | `indicator` | `data-instant` | ''(条件成立时才出现) | | `title` | `data-instant` | ''(条件成立时才出现) | | `description` | `data-instant` | ''(条件成立时才出现) | | `action` | `data-instant` | ''(条件成立时才出现) | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-empty-state-action-gap` | `action` | `gap` | `default` | `--xh-space-2` | empty-state 的 action 部件 gap 覆盖槽。 | | `--xh-empty-state-description-fg` | `description` | `color` | `default` | `--xh-fg-muted` | empty-state 的 description 部件 color 覆盖槽。 | | `--xh-empty-state-description-font-size` | `description` | `font-size` | `default` | `--xh-text-secondary-size` | empty-state 的 description 部件 font-size 覆盖槽。 | | `--xh-empty-state-description-leading` | `description` | `line-height` | `default` | `--xh-leading-normal` | empty-state 的 description 部件 line-height 覆盖槽。 | | `--xh-empty-state-description-max-w` | `description` | `max-inline-size` | `default` | `--xh-measure-prose` | empty-state 的 description 部件 max-inline-size 覆盖槽。 | | `--xh-empty-state-fg` | `root` | `color` | `default` | `--xh-fg-default` | empty-state 的 root 部件 color 覆盖槽。 | | `--xh-empty-state-gap` | `root` | `gap` | `default` | `--xh-_empty-state-gap` | empty-state 的 root 部件 gap 覆盖槽。 | | `--xh-empty-state-icon-size` | `indicator` | `--xh-icon-size`
`block-size`
`inline-size` | `default` | `--xh-_empty-state-icon-size` | empty-state 的 indicator 部件 --xh-icon-size、block-size、inline-size 覆盖槽。 | | `--xh-empty-state-indicator-fg` | `indicator` | `color` | `default` | `--xh-_empty-state-accent` | empty-state 的 indicator 部件 color 覆盖槽。 | | `--xh-empty-state-indicator-font-size` | `indicator` | `font-size` | `default` | `--xh-_empty-state-icon-size` | empty-state 的 indicator 部件 font-size 覆盖槽。 | | `--xh-empty-state-media-fg` | `media` | `color` | `default` | `--xh-_empty-state-accent` | empty-state 的 media 部件 color 覆盖槽。 | | `--xh-empty-state-media-size` | `media` | `block-size` | `default` | `--xh-_empty-state-icon-size` | empty-state 的 media 部件 block-size 覆盖槽。 | | `--xh-empty-state-px` | `root` | `padding-inline` | `default` | `--xh-space-6` | empty-state 的 root 部件 padding-inline 覆盖槽。 | | `--xh-empty-state-py` | `root` | `padding-block` | `default` | `--xh-_empty-state-py` | empty-state 的 root 部件 padding-block 覆盖槽。 | | `--xh-empty-state-title-fg` | `title` | `color` | `default` | `--xh-fg-default` | empty-state 的 title 部件 color 覆盖槽。 | | `--xh-empty-state-title-font-size` | `title` | `font-size` | `default` | `--xh-_empty-state-title-size` | empty-state 的 title 部件 font-size 覆盖槽。 | | `--xh-empty-state-title-font-weight` | `title` | `font-weight` | `default` | `--xh-font-weight-semibold` | empty-state 的 title 部件 font-weight 覆盖槽。 | | `--xh-empty-state-title-leading` | `title` | `line-height` | `default` | `--xh-leading-tight` | empty-state 的 title 部件 line-height 覆盖槽。 | ### 动效 动效角色:出现(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-rise-in` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/field-array # FieldArray 字段数组 用于管理可添加、删除和排序的重复字段。 ## 用法 添加和删除重复字段 ```vue ``` ```html
``` ## 组件结构 加粗的是必需部件。 `data-scope="field-array"`:**`root`** · `item` · `item-label` · `item-content` · `item-action` · `add-trigger` · `item-delete-trigger` · `move-up-trigger` · `move-down-trigger` ## 示例 ### 数量限制 设置最少和最多行数 ```vue ``` ```html
``` ### 排序 上移或下移字段 ```vue ``` ```html
``` ### 多字段行 每行包含多个输入框 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - 联系方式、规格参数、收件人等数量可变的字段。 ### 何时不用 - 行数固定时直接使用普通字段。 - 每项只是短文本时使用[标签输入](./tags-input)。 ### 特性 - `min` 与 `max` 限制行数。 - `movable` 启用上移和下移操作。 - `createItem` 设置新增行的初始值。 - 每行可以包含一个或多个字段。 - 在 Form 中会同步迁移数组子字段的值、规则和错误。 - 行的增删与移动有进退场:首次渲染时已有的行直接呈现,新增的行淡入,删掉的行在原处淡出,上移、下移与增删带来的换位滑到新位置。行照常按 `items` 渲染,删掉即卸载。 ### 组合 - 每一行放[表单字段](./field),行内多个字段用行布局排列;整组挂在[表单](./form)下由它迁移值、规则与错误。 - 行序也可以交给[排序](./sortable)拖拽调整;`movable` 只提供上移、下移两个按钮。 ### 最佳实践 - 新增后将焦点移到新行的第一个输入框。 - 删除按钮应说明目标行。 - 到达数量限制时保持操作按钮可见并禁用。 ### 反模式 - 删除后无法撤销。 - 只在提交时提示数量限制。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhFieldArrayAddTrigger` `XhFieldArrayItem` `XhFieldArrayItemAction` `XhFieldArrayItemContent` `XhFieldArrayItemDeleteTrigger` `XhFieldArrayItemLabel` `XhFieldArrayMoveDownTrigger` `XhFieldArrayMoveUpTrigger` `XhFieldArrayRoot` | | 组合式函数 | `useFieldArray` | | 状态机 | `fieldArrayMachine` | | 皮肤 | `@xihan-ui/styles/field-array.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `unknown[]` | | 受控数据数组;提供后由宿主决定,状态机不自行修改,只发 onValueChange。 | | `defaultValue` | `unknown[]` | | 非受控初始数据数组。 | | `min` | `number` | | 最少行数。到达该数值时删除把手不可按下。默认 0。 | | `max` | `number` | | 最多行数。到达该数值时新增把手不可按下。默认不限。 | | `createItem` | `() => unknown` | | 新增一行时创建一个空项。未提供时插入 null。 | | `movable` | `boolean` | | 是否显示换序把手。关闭(默认)时两个换序把手一律收起。 | | `disabled` | `boolean` | | 禁用:新增、删除、换序三路都不可按下。 | | `readOnly` | `boolean` | | 只读:行数不可修改(新增、删除、换序都不可按下),行内的控件仍由作者自行设置只读。 | | `invalid` | `boolean` | | 校验失败标注:写在根与每一行上。 | | `name` | `FormPath` | | 整份数组的表单字段名。嵌套在 Form 中时会自动接入其值、规则、错误与校验真源; 每一行经 `item.name` 获得显式数组 FormPath,不拼接字符串下标。 | | `translations` | `Partial` | | | | `onValueChange` | `(details: FieldArrayValueChangeDetails) => void` | | | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `FieldArrayValueChangeDetails` | 数据数组变化;detail 为 `{ value: unknown[] }` | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhFieldArrayRoot` | `default` | `FieldArrayRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhFieldArrayItem` | `index` | `number \| string` | 是 | 下标由作者声明;兼收字符串,与另外两个适配器的属性口径对齐。 | | `XhFieldArrayRoot` | `children` | `SlotChildren` | | | ### 状态 以下名称仅用于内部状态机。 **状态**:`idle` **事件**:`VALUE.SET` · `ITEM.ADD` · `ITEM.REMOVE` · `ITEM.MOVE` · `FORM.RESET` · `PRESS.START` · `PRESS.END` · `LIST.TRACKED` **判据**:`canAdd` · `canRemove` · `canMove` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `unknown[]` | | | `items` | `FieldArrayItem[]` | 逐行的读侧投影,含渲染用的 key。 | | `count` | `number` | | | `empty` | `boolean` | | | `disabled` | `boolean` | | | `readOnly` | `boolean` | | | `invalid` | `boolean` | | | `movable` | `boolean` | | | `atMin` | `boolean` | 已到下限:再删除会少于 min。 | | `atMax` | `boolean` | 已到上限:再新增会多于 max。 | | `canAdd` | `boolean` | | | `setValue` | `(next: unknown[]) => void` | 整份替换,不受 min / max 约束。 | | `add` | `() => void` | | | `remove` | `(index: number) => void` | | | `move` | `(from: number, to: number) => void` | | | `moveUp` | `(index: number) => void` | | | `moveDown` | `(index: number) => void` | | | `getRootProps` | `() => T['element']` | | | `getItemProps` | `(item: FieldArrayItemProps) => T['element']` | | | `getItemLabelProps` | `(item: FieldArrayItemProps) => T['element']` | 行前的行号或名目:纯标注,不与行内的控件建立 for 关联。 | | `getItemContentProps` | `(item: FieldArrayItemProps) => T['element']` | | | `getItemActionProps` | `(item: FieldArrayItemProps) => T['element']` | | | `getAddTriggerProps` | `() => T['button']` | | | `getItemDeleteTriggerProps` | `(item: FieldArrayItemProps) => T['button']` | | | `getMoveUpTriggerProps` | `(item: FieldArrayItemProps) => T['button']` | | | `getMoveDownTriggerProps` | `(item: FieldArrayItemProps) => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/ARIA/apg/) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` / `Space` | held on add-trigger / item-delete-trigger / move-up-trigger / move-down-trigger, not aria-disabled | 按住期间该把手投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,删除 / 换序落地后把手随行离场或换位时一并撤下 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `add-trigger` | `aria-disabled` | 'false' \| 'true' | | `item-delete-trigger` | `aria-disabled` | 'false' \| 'true' | | `item-delete-trigger` | `aria-label` | label.deleteItem(item.index + 1, count) | ## 样式参考 ### 皮肤 `@xihan-ui/styles/field-array.css` 使用 `[data-scope="field-array"][data-part="root"]` 部件选择器,位于 `xihan.components` 层。覆盖样式使用 `xihan.overrides`。 ### 数据属性 由 `connect` 生成;条件不成立时不输出无值属性。 | 部件 | 属性 | 值 | | --- | --- | --- | | `root` | `data-at-max` | ''(条件成立时才出现) | | `root` | `data-at-min` | ''(条件成立时才出现) | | `root` | `data-disabled` | ''(条件成立时才出现) | | `root` | `data-empty` | ''(条件成立时才出现) | | `root` | `data-instant` | ''(条件成立时才出现) | | `root` | `data-invalid` | ''(条件成立时才出现) | | `root` | `data-movable` | ''(条件成立时才出现) | | `root` | `data-readonly` | ''(条件成立时才出现) | | `item` | `data-at-max` | ''(条件成立时才出现) | | `item` | `data-at-min` | ''(条件成立时才出现) | | `item` | `data-disabled` | ''(条件成立时才出现) | | `item` | `data-first` | ''(条件成立时才出现) | | `item` | `data-index` | String(item.index) | | `item` | `data-invalid` | ''(条件成立时才出现) | | `item` | `data-last` | ''(条件成立时才出现) | | `item` | `data-readonly` | ''(条件成立时才出现) | | `item-label` | `data-at-max` | ''(条件成立时才出现) | | `item-label` | `data-at-min` | ''(条件成立时才出现) | | `item-label` | `data-disabled` | ''(条件成立时才出现) | | `item-label` | `data-index` | String(item.index) | | `item-label` | `data-invalid` | ''(条件成立时才出现) | | `item-label` | `data-readonly` | ''(条件成立时才出现) | | `item-content` | `data-at-max` | ''(条件成立时才出现) | | `item-content` | `data-at-min` | ''(条件成立时才出现) | | `item-content` | `data-disabled` | ''(条件成立时才出现) | | `item-content` | `data-index` | String(item.index) | | `item-content` | `data-invalid` | ''(条件成立时才出现) | | `item-content` | `data-readonly` | ''(条件成立时才出现) | | `item-action` | `data-at-max` | ''(条件成立时才出现) | | `item-action` | `data-at-min` | ''(条件成立时才出现) | | `item-action` | `data-disabled` | ''(条件成立时才出现) | | `item-action` | `data-index` | String(item.index) | | `item-action` | `data-invalid` | ''(条件成立时才出现) | | `item-action` | `data-readonly` | ''(条件成立时才出现) | | `add-trigger` | `data-disabled` | ''(条件成立时才出现) | | `add-trigger` | `data-pressed` | ''(条件成立时才出现) | | `add-trigger` | `data-xh-action-control` | '' | | `add-trigger` | `data-xh-action-display` | 'always' | | `add-trigger` | `data-xh-action-profile` | 'text' | | `add-trigger` | `data-xh-action-size` | 'md' | | `add-trigger` | `data-xh-action-variant` | 'outline' | | `item-delete-trigger` | `data-at-max` | ''(条件成立时才出现) | | `item-delete-trigger` | `data-at-min` | ''(条件成立时才出现) | | `item-delete-trigger` | `data-disabled` | ''(条件成立时才出现) | | `item-delete-trigger` | `data-index` | String(item.index) | | `item-delete-trigger` | `data-invalid` | ''(条件成立时才出现) | | `item-delete-trigger` | `data-pressed` | ''(条件成立时才出现) | | `item-delete-trigger` | `data-readonly` | ''(条件成立时才出现) | | `item-delete-trigger` | `data-xh-action-control` | '' | | `item-delete-trigger` | `data-xh-action-display` | 'always' | | `item-delete-trigger` | `data-xh-action-profile` | 'icon' | | `item-delete-trigger` | `data-xh-action-size` | 'xs' | | `item-delete-trigger` | `data-xh-action-variant` | 'ghost' | | `move-up-trigger` | `data-pressed` | ''(条件成立时才出现) | | `move-up-trigger` | `data-xh-action-control` | '' | | `move-up-trigger` | `data-xh-action-display` | 'always' | | `move-up-trigger` | `data-xh-action-profile` | 'icon' | | `move-up-trigger` | `data-xh-action-size` | 'xs' | | `move-up-trigger` | `data-xh-action-variant` | 'ghost' | | `move-down-trigger` | `data-pressed` | ''(条件成立时才出现) | | `move-down-trigger` | `data-xh-action-control` | '' | | `move-down-trigger` | `data-xh-action-display` | 'always' | | `move-down-trigger` | `data-xh-action-profile` | 'icon' | | `move-down-trigger` | `data-xh-action-size` | 'xs' | | `move-down-trigger` | `data-xh-action-variant` | 'ghost' | ### CSS 变量 本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。 | 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 | | --- | --- | --- | --- | --- | --- | | `--xh-field-array-action-gap` | `add-trigger`
`item-action` | `gap` | `default` | `--xh-space-1` | field-array 的 add-trigger、item-action 部件 gap 覆盖槽。 | | `--xh-field-array-add-bg` | `add-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | field-array 的 add-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-field-array-add-bg-active` | `add-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | field-array 的 add-trigger 部件 background-color 覆盖槽。 | | `--xh-field-array-add-bg-hover` | `add-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | field-array 的 add-trigger 部件 background-color 覆盖槽。 | | `--xh-field-array-add-border` | `add-trigger` | `border` | `default` | `--xh-_action-variant-border-rest` | field-array 的 add-trigger 部件 border 覆盖槽。 | | `--xh-field-array-add-border-disabled` | `add-trigger` | `border-color` | `disabled` | `--xh-_action-variant-border-disabled` | field-array 的 add-trigger 部件 border-color 覆盖槽。 | | `--xh-field-array-add-border-hover` | `add-trigger` | `border-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-border-hover` | field-array 的 add-trigger 部件 border-color 覆盖槽。 | | `--xh-field-array-add-fg` | `add-trigger` | `color` | `default`
`disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-brand` | field-array 的 add-trigger 部件 color 覆盖槽。 | | `--xh-field-array-add-font-size` | `add-trigger` | `font-size` | `default` | `--xh-text-label-size` | field-array 的 add-trigger 部件 font-size 覆盖槽。 | | `--xh-field-array-add-height` | `add-trigger` | `block-size`
`inline-size` | `default`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size` | field-array 的 add-trigger 部件 block-size、inline-size 覆盖槽。 | | `--xh-field-array-add-px` | `add-trigger` | `padding-inline` | `default` | `--xh-_action-profile-padding-inline` | field-array 的 add-trigger 部件 padding-inline 覆盖槽。 | | `--xh-field-array-add-radius` | `add-trigger` | `border-radius` | `default` | `--xh-shape-control` | field-array 的 add-trigger 部件 border-radius 覆盖槽。 | | `--xh-field-array-content-gap` | `item-content` | `gap` | `default` | `--xh-space-2` | field-array 的 item-content 部件 gap 覆盖槽。 | | `--xh-field-array-gap` | `root` | `gap` | `default` | `--xh-space-2` | field-array 的 root 部件 gap 覆盖槽。 | | `--xh-field-array-icon-size` | `add-trigger`
`item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size` | field-array 的 add-trigger、item-delete-trigger、move-down-trigger、move-up-trigger 部件 --xh-icon-size 覆盖槽。 | | `--xh-field-array-item-delete-fg-hover` | `item-delete-trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-danger-hover` | field-array 的 item-delete-trigger 部件 color 覆盖槽。 | | `--xh-field-array-item-gap` | `item` | `gap` | `default` | `--xh-space-2` | field-array 的 item 部件 gap 覆盖槽。 | | `--xh-field-array-item-label-fg` | `item-label` | `color` | `default` | `--xh-fg-muted` | field-array 的 item-label 部件 color 覆盖槽。 | | `--xh-field-array-item-label-font-size` | `item-label` | `font-size` | `default` | `--xh-text-secondary-size` | field-array 的 item-label 部件 font-size 覆盖槽。 | | `--xh-field-array-item-padding` | `item` | `padding` | `default` | `--xh-space-0` | field-array 的 item 部件 padding 覆盖槽。 | | `--xh-field-array-item-radius` | `item` | `border-radius` | `default` | `--xh-shape-surface` | field-array 的 item 部件 border-radius 覆盖槽。 | | `--xh-field-array-trigger-bg` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `--xh-ink-surface`
`background-color` | `default`
`xh-ink-surface` | `--xh-_action-variant-bg-rest` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 | | `--xh-field-array-trigger-bg-active` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `background-color` | `disabled`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-_action-variant-bg-pressed` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 background-color 覆盖槽。 | | `--xh-field-array-trigger-bg-hover` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `background-color` | `disabled`
`hover`
`loading`
`not([data-disabled])`
`not([data-loading])` | `--xh-_action-variant-bg-hover` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 background-color 覆盖槽。 | | `--xh-field-array-trigger-fg` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `color` | `default` | `--xh-fg-muted` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 color 覆盖槽。 | | `--xh-field-array-trigger-fg-hover` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `color` | `disabled`
`hover`
`is(:active, [data-pressed])`
`loading`
`not([data-disabled])`
`not([data-loading])`
`pressed` | `--xh-fg-default` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 color 覆盖槽。 | | `--xh-field-array-trigger-font-size` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `font-size` | `default` | `--xh-text-secondary-size` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 font-size 覆盖槽。 | | `--xh-field-array-trigger-radius` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `border-radius` | `default` | `--xh-shape-control` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 border-radius 覆盖槽。 | | `--xh-field-array-trigger-size` | `item-delete-trigger`
`move-down-trigger`
`move-up-trigger` | `block-size`
`inline-size`
`min-inline-size` | `default`
`xh-action-profile=icon` | `--xh-_action-profile-visual-size` | field-array 的 item-delete-trigger、move-down-trigger、move-up-trigger 部件 block-size、inline-size、min-inline-size 覆盖槽。 | ### 动效 动效角色:按压 · 状态 · 指示与换位 · 出现 · 列表(见[动效规范](../design/motion#角色))。 共享关键帧 `xh-fade-out` · `xh-item-in` 由 `family/motion.css` 提供,皮肤 `@import` 它,单独引入仍成立;`translate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion),改令牌即改全局节奏。 系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。 ### RTL 皮肤用逻辑属性排布(`inline-start` 一族),`dir="rtl"` 下自动镜像。 --- 来源:https://ui.docs.xihanfun.com/components/field # Field 表单字段 为表单控件提供标签、说明、错误信息和状态关联。 ## 用法 为控件添加标签与说明 ```vue ``` ```html

用于接收账单与安全提醒

``` ## 组件结构 加粗的是必需部件。 `data-scope="field"`:**`root`** · `label` · **`control`** · `description` · `error-text` ## 示例 ### 必填与校验 显示字段错误 ```vue ``` ```html

用于接收账单与安全提醒

邮箱格式不正确

``` ### 禁用 禁止编辑字段 ```vue ``` ```html

账号创建后不可更改

``` ### 标签左置 将标签放在控件左侧 ```vue ``` ```html

留空表示使用默认端口

端口只能是数字

``` ## 设计指引 ### 何时使用 - 表单控件需要可见标签、说明或错误信息。 - 需要统一管理必填、禁用、只读和无效状态。 ### 何时不用 - 不需要可见标签的紧凑控件直接提供 `aria-label`。 - 需要管理整张表单的值和提交时,使用[表单](./form)。 ### 特性 - 自动关联标签、说明、错误信息与控件。 - `disabled`、`readOnly`、`invalid` 和 `required` 可传递给内部控件。 - 说明和错误信息可以同时显示。 - `FieldControl` 默认将属性合并到唯一子节点。 ### 组合 - 包裹任何单一控件:[文本字段](./text-field)、[选择器](./select)、[开关](./switch)等会把字段状态接到实际控件上。 - 多个字段一起提交与校验时放入[表单](./form);一组相关字段使用[字段集](./fieldset)分区。 ### 最佳实践 - 使用持续可见的明确标签。 - 把 `control` 标在真控件(``、`

``` ## 组件结构 加粗的是必需部件。 `data-scope="prompt-input"`:**`root`** · `control` · **`input`** · **`submit-trigger`** ## 示例 ### 与消息流组成一个对话 发送键原位变为停止;提交后粘底跟随到最新一条,生成期间仍可继续编辑下一句 ```vue ``` ```html
助手
问点什么试试。
``` ### 竖排布局与兜底字形 写一层输入行,root 即切换为竖排:输入行在上、动作行在下;按钮留空时皮肤按身份绘制箭头或停止方块 ```vue ``` ```html
0 字

``` ### 三档提交按键 enter 档回车即发送、mod-enter 档只有 Ctrl/Cmd+Enter 发送、none 档两种按法都换行,提交只剩发送按钮 ```vue ``` ```html
(还没发过)
``` ### 禁用与空值 disabled 覆盖整框并使用原生 disabled;输入为空或只有空白时发送按钮转灰,但位置保留不收起 ```vue ``` ```html
``` ### 框内的附加节点 root 中除三件部件外还可放置自己的按钮与计数;值的读写归宿主,原生属性照常直接落到输入框上 ```vue ``` ```html
0 / 40
(还没发过)
``` ### 随内容增高 输入框的高度跟随内容,rows 决定起始行数;不手动拖拽,也不写死高度 ```vue ``` ```html
``` ### 聚焦与选中 输入部件就是一个原生 textarea,取得它的节点即可聚焦、全选、失焦;发送后把焦点送回,继续输入下一条 ```vue ``` ```html
(还没发过)
``` ### 发送失败的错误态 判定是否出错由宿主决定:属性直接落到真实元素上,整框换色依靠覆盖公开变量,原因由活区播报 ```vue ``` ```html
(还没发过)
``` ### 颜色 tone 切换聚焦描边与发送按钮使用哪族颜色,输入与提交链路不受影响 ```vue ``` ```html
``` ## 设计指引 ### 何时使用 - AI 对话、聊天或任何输入一段话后提交的界面。 - 生成期间需要一键停止。 ### 何时不用 - 只是表单中的多行文本域时,使用[文本字段](./text-field)配[表单字段](./field)。 - 需要 @提及或斜杠命令时,整体使用[提及](./mention)作为输入器,见下方的组合。 ### 特性 - 发送与停止原位共用一个节点:正在按它的用户不会按空。生成期间按钮始终可用,此时它的语义是停止。 - `submitKey` 一个 prop 表达三档:`enter` 档 Enter 提交、Shift+Enter 换行、Mod+Enter 也提交;`mod-enter` 档 Enter 换行,只有 Mod+Enter 提交;`none` 档两种按法都换行,不保留任何键盘提交出口,只剩发送按钮与程序化的 `submit()`。 - 输入法组合期间的 Enter 一律放行,该按键用于确认候选词。 - 同一个输入框上叠加了其他处理器且它已处理该按键时,组件让位。 - 自动长高是两行 CSS,不进入状态机;引擎不支持时退化为 `rows` 决定的固定行数。 - 两种排布同一份皮肤:直接把输入框与按钮放进 root 是单行;套一层输入行后 root 变为竖排,输入行上下两侧可以再放附件条与动作行。 - 默认形态是 outline:不填底、`--xh-border-control` 描边、无影,不画顶光与背景模糊;输入段透明,底由外框承担。发送按钮与 Button 缺省同为品牌实心,生成中降为中性淡底的停止身份。 - 发送按钮留空时皮肤绘制兜底字形:发送身份为上箭头,停止身份为圆角方块;放入自定义图标或文案即覆盖。 ### 组合 - 附件使用[文件上传](./file-upload):它已覆盖 accept、大小校验、拖拽投放与逐条删除;有附件而正文为空时把 `allowEmptySubmit` 置真。附件条放在输入行上方,动作行放在下方,两者都是 root 的直接子节点,与输入行并列。 - 粘贴上传由作者在输入框上自行挂 `onPaste`,处理器会与组件的处理器链式组合。 - 模型选择器使用[选择器](./select)或[组合框](./combobox),工具开关使用[切换按钮组](./toggle-group),它们连同自己的容器一起放进输入行下方。 - 与[消息流](./message-feed)组合即是最小对话界面。 ### 最佳实践 - 受控用法下提交后由宿主清空;`clearOnSubmit` 关闭时组件不改动值。 - 生成期间把 `loading` 置真而不是禁用整个输入框,用户仍需要编辑下一句。 - 需要胶囊形状时不必更换形态轴:在任意祖先上写 `--xh-prompt-input-radius: var(--xh-shape-pill)`,按钮另有 `--xh-prompt-input-submit-radius`。形态轴只决定底与描边的画法。 ### 反模式 - 另起一个停止按钮放在旁边:两个按钮的位置会互相挤压,按下的瞬间位置也会变化。 - 用 `disabled` 表达正在生成:会连输入一起挡住,也关闭了停止的出口。 ## API 参考 ### 产物 | 层 | 值 | | --- | --- | | 自定义元素 | `` | | Vue 组件 | `XhPromptInputControl` `XhPromptInputInput` `XhPromptInputRoot` `XhPromptInputSubmitTrigger` | | 组合式函数 | `usePromptInput` | | 状态机 | `promptInputMachine` | | 皮肤 | `@xihan-ui/styles/prompt-input.css` | ### Props | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | `string` | | | | `defaultValue` | `string` | | | | `disabled` | `boolean` | | | | `loading` | `boolean` | | 正在生成:按钮换为停止身份,所有提交路径被拦截。 使用一个布尔而不是四档运行态字符串:组件只需要二值判断, 本轮进行到哪一步是宿主的事,透传为 data 属性属于作者的容器。 | | `submitKey` | `PromptInputSubmitKey` | | 按哪一档提交,默认 enter。 | | `allowEmptySubmit` | `boolean` | | 允许空值提交,默认 false;有附件时由作者置真。这是唯一为附件保留的钩子。 | | `clearOnSubmit` | `boolean` | | 提交后清空,默认 true。 | | `variant` | `ControlVariant` | | 形态:outline / subtle / ghost,决定底色与描边的绘制方式。默认 outline。 | | `tone` | `Tone` | | | | `size` | `Size` | | | | `translations` | `Partial` | | | | `onValueChange` | `(details: PromptInputValueChangeDetails) => void` | | | | `onSubmit` | `(details: PromptInputSubmitDetails) => void` | | | | `onStop` | `() => void` | | | ### 事件 自定义元素将载荷放在 `detail`;Vue 使用同名 emit。 | 事件 | 载荷 | 说明 | | --- | --- | --- | | `value-change` | `PromptInputValueChangeDetails` | 值变化;detail 为 `{ value: string }` | | `submit` | `PromptInputSubmitDetails` | 提交;detail 为 `{ value: string }`,清空发生在派发之后。 与原生表单提交同名,故不冒泡,请直接在 `<xh-prompt-input>` 元素上监听 | | `stop` | `` | 生成期间按下停止;无 detail | ### 插槽 仅列出带载荷的插槽。 | Vue 组件 | 插槽 | 载荷 | 说明 | | --- | --- | --- | --- | | `XhPromptInputRoot` | `default` | `PromptInputRootSlotProps` | | ### React 适配器 props 只列各组件自己声明的那些:继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。 | React 组件 | 属性 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | --- | | `XhPromptInputRoot` | `children` | `SlotChildren` | | | ### 状态 公开状态写入 `data-state`。 | 部件 | 取值 | | --- | --- | | `input` | 'empty' \| 'editing' \| 'disabled' | 以下名称仅用于内部状态机。 **状态**:`empty` · `editing` · `disabled` **事件**:`VALUE.SET` · `COMPOSITION.START` · `COMPOSITION.END` · `KEY.SUBMIT` · `SUBMIT` · `STOP` · `CONTROLLED.DISABLE` · `CONTROLLED.ENABLE` · `CONTROLLED.VALUE.EMPTY` · `PRESS.START` · `PRESS.END` · `CONTROLLED.VALUE.FILLED` **判据**:`canSubmit` · `isLoading` · `isValueEmpty` · `isNextValueEmpty` · `canPress` ### connect API `getXxxProps()` 返回对应部件的宿主属性。 | 成员 | 类型 | 说明 | | --- | --- | --- | | `value` | `string` | | | `isComposing` | `boolean` | | | `canSubmit` | `boolean` | 是否可以提交。比状态机守卫多一条非禁用,供按钮置灰使用。 | | `loading` | `boolean` | | | `disabled` | `boolean` | | | `setValue` | `(next: string) => void` | | | `submit` | `() => void` | | | `stop` | `() => void` | | | `getRootProps` | `() => T['element']` | | | `getControlProps` | `() => T['element']` | 可选的输入行容器:渲染它后,输入框与按钮并排收在这一行中,root 改为纵向排列。 | | `getInputProps` | `() => T['textarea']` | | | `getSubmitTriggerProps` | `() => T['button']` | | ## 无障碍 ### 键盘 规格出处:[W3C APG](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) | 按键 | 生效条件 | 行为 | | --- | --- | --- | | `Enter` | 焦点在输入框、submitKey 为 enter、非组合态、可提交,且这一下还没被别的处理器处理过 | 提交,并按 clearOnSubmit 决定清不清空 | | `Shift+Enter` | 焦点在输入框 | 不归组件管:原样放行,浏览器插入换行 | | `Control+Enter` / `Meta+Enter` | 焦点在输入框、submitKey 为 enter 或 mod-enter、非组合态、可提交 | 提交,并按 clearOnSubmit 决定清不清空 | | `Enter` / `Control+Enter` / `Meta+Enter` | 焦点在输入框、submitKey 为 none | 都不提交也不拦截:原样放行,浏览器插入换行;提交只剩按钮与程序化两条路 | | `Enter` | 输入法组合中 | 不提交也不拦截:这一下是在确认候选词 | | `Enter` | 同一个输入框上叠了别的处理器且它已经处理过这一下 | 让位,本组件什么都不做 | | `Enter` / `Space` | 焦点在发送按钮上 | 按当前身份触发提交或停止(原生按钮激活) | | `Enter` / `Space` | held on submit-trigger, not disabled(发送身份要可提交,停止身份恒可用) | 按住期间按钮投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,身份随 loading 切换或提交后清空使按钮转禁用时一并撤下 | | `Escape` | 任何时候 | 不接管:留给叠在输入框上的浮层与页面 | ### ARIA 以下属性由 `connect` 生成。 | 部件 | 属性 | 值 | | --- | --- | --- | | `input` | `aria-label` | translations?.input | | `submit-trigger` | `aria-label` | translations?.stop \| translations?.send | - 输入框的可访问名称只在提供 `translations.input` 时才发出:无条件发出会覆盖作者自己的 `