radix-vue 日历组件详解:CalendarGrid 网格容器的 Props、语义与无障碍实现
【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue
导读
CalendarGrid是 radix-vue(Reka UI)日历组件体系中负责承载日期网格的核心容器,默认渲染为 HTML<table>,并承担了日历在只读/禁用状态下的无障碍语义暴露。本文以 CalendarGrid.md 的 Props 文档为主体,结合仓库源码与测试用例,深入讲解as/asChild两个 Props 的用法、默认渲染元素、role="application"与aria-*属性的实现机制,并给出一个可复制运行的完整日历网格示例。读完后你将能独立定制日历网格的渲染元素,并理解其状态传播与无障碍设计。
CalendarGrid 在日历结构中的定位
在 calendar.md 的 Anatomy(结构解剖)中,CalendarGrid位于CalendarRoot之下,与CalendarGridHead、CalendarGridBody、CalendarGridRow、CalendarHeadCell、CalendarCell等部件共同拼装出标准的日期表格:
<script setup> import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNext, CalendarPrev, CalendarRoot, } from 'reka-ui' </script> <template> <CalendarRoot> <CalendarHeader> <CalendarPrev /> <CalendarHeading /> <CalendarNext /> </CalendarHeader> <CalendarGrid> <CalendarGridHead> <CalendarGridRow> <CalendarHeadCell /> </CalendarGridRow> </CalendarGridHead> <CalendarGridBody> <CalendarGridRow> <CalendarCell> <CalendarCellTrigger /> </CalendarCell> </CalendarGridRow> </CalendarGridBody> </CalendarGrid> </CalendarRoot> </template>从上述结构可以看到:CalendarGrid是整个日期表格的"外壳",负责把星期表头(Head)和日期单元(Body)组织进同一个表格容器中。它的状态(禁用/只读)由 CalendarRoot.vue 通过createContext建立的CalendarRootContext注入,这一点在下方"状态传播"小节会详细展开。
Props 完整参考(继承自官方文档)
根据 CalendarGrid.md,CalendarGrid共暴露两个 Props:
| Name | Description | Type | Required | Default |
|---|---|---|---|---|
as | The element or component this component should render as. Can be overwritten by asChild. | AsTag \| Component | No | "table" |
asChild | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | boolean | No | - |
as:自定义渲染元素
as接受一个 HTML 标签名(AsTag)或一个 Vue 组件(Component),用于改变CalendarGrid最终渲染出的 DOM 元素。默认值为"table",即直接产出原生<table>。
常见使用场景:
- 在样式框架(如 Tailwind)中需要给
<table>外层再加包裹元素时,将其改为div、section等块级元素; - 与 UI 库的表格组件组合,例如将
as指向某个封装好的表格组件; - 需要精确控制表格语义时,保持默认
table即可。
<CalendarGrid as="section"> <!-- 渲染为 <section> 而不是 <table> --> </CalendarGrid>asChild:以子元素替换默认元素
asChild为布尔类型,没有默认值。当设置为true时,CalendarGrid不再渲染自身的默认标签,而是把组件内部的所有 Props 与行为"合并"到作为子元素传入的那个节点上。子元素可以是任意元素或组件,最终只渲染这一个子元素。
<CalendarGrid as-child> <table> <!-- CalendarGrid 的 tabindex、role、aria-* 等属性与行为会合并到 <table> 上 --> </table> </CalendarGrid>asChild与as的关系是:as定义默认渲染元素,而asChild可以覆盖(overwrite)as的决定——一旦传入子元素,就以子元素为准。这也是文档描述中"Can be overwritten by asChild"(可被asChild覆盖)的确切含义。更详细的组合语义可参考仓库中的 Composition 指南(docs/content/docs/guides 目录下相关文档)。
源码级实现:默认元素与状态注入
CalendarGrid的实现位于 CalendarGrid.vue,核心代码非常精简,全部逻辑都建立在Primitive组件之上:
const props = withDefaults(defineProps<CalendarGridProps>(), { as: 'table' }) const rootContext = injectCalendarRootContext() const disabled = computed(() => rootContext.disabled.value ? true : undefined) const readonly = computed(() => rootContext.readonly.value ? true : undefined)<Primitive v-bind="props" tabindex="-1" role="application" :aria-readonly="readonly" :aria-disabled="disabled" :data-readonly="readonly && ''" :data-disabled="disabled && ''" > <slot /> </Primitive>可以提炼出三个实现要点:
- 默认值来自
withDefaults:as的默认值'table'在源码中以withDefaults声明,与文档表格完全一致; - 状态来自 Root 上下文:
disabled与readonly并非CalendarGrid自身的 Props,而是通过injectCalendarRootContext()从 CalendarRoot.vue 提供的上下文中读取的响应式引用,并在本地用computed转换为布尔值。这意味着你只需在CalendarRoot上设置disabled/readonly,整棵日历树(包括网格)的状态会自动联动; - 固定携带表格无关语义属性:组件无条件渲染
tabindex="-1"与role="application"——前者保证网格本身可被程序化聚焦但不会进入 Tab 键序,后者向辅助技术声明这是一个可交互的应用程序区域。
传递链:从 Root 到 Grid
状态传递的完整链路为:
- 用户在
CalendarRoot上设置disabled/readonlyProps; CalendarRoot将其转为Ref<boolean>后放入provideCalendarRootContext({ disabled, readonly, ... })(见 CalendarRoot.vue);CalendarGrid通过injectCalendarRootContext()取回并计算aria-readonly/aria-disabled。
无障碍与状态语义:data-* / aria-* 属性
calendar.md 中为 Grid 部件列出了两张数据属性表,可作为CalendarGrid状态语义的官方补充:
| Attribute | Values |
|---|---|
[data-readonly] | Present when readonly |
[data-disabled] | Present when disabled |
对应到源码 CalendarGrid.vue:
aria-readonly:仅在CalendarRoot.readonly为真时输出"true";aria-disabled:仅在CalendarRoot.disabled为真时输出"true";data-readonly/data-disabled:同时以data-*属性暴露,供 CSS 选择器或测试定位使用。
这些行为有测试用例背书。在 Calendar.test.ts 的'prevents selection but allows focus when readonly is true'用例中,仓库明确断言了网格的只读语义:
const grid = getByTestId('grid-1') expect(grid).toHaveAttribute('aria-readonly', 'true') expect(grid).toHaveAttribute('data-readonly')同时,测试还验证了只读与禁用的行为差异:只读模式下日期仍可聚焦(firstDayOfMonth.focus()后仍持有焦点),但点击不会选中;而禁用模式下日期不可聚焦且aria-disabled为真(见同文件 Calendar.test.ts 附近的用例)。这说明CalendarGrid的语义属性不是装饰,而是与整棵日历树的交互策略严格同步的。
与相邻网格部件的分工
CalendarGrid只负责外层容器,其内部的表头、表体与行均有各自的独立组件,默认渲染元素各不相同(均来自对应源码的withDefaults):
| 组件 | 默认渲染元素 | 额外语义 | 源码位置 |
|---|---|---|---|
CalendarGrid | table | tabindex="-1"、role="application"、aria-readonly、aria-disabled | CalendarGrid.vue |
CalendarGridHead | thead | aria-hidden="true"(对辅助技术隐藏表头) | CalendarGridHead.vue |
CalendarGridBody | tbody | — | CalendarGridBody.vue |
CalendarGridRow | tr | — | CalendarGridRow.vue |
CalendarHeadCell | th | — | CalendarHeadCell.vue |
值得注意的设计细节:CalendarGridHead默认带aria-hidden="true",因为星期几的标签信息已经通过CalendarRoot的fullCalendarLabel(完整日历标签,作为aria-label挂载在 Root 上,并在组件内部以隐藏的role="heading"元素输出)提供给辅助技术,表头视觉元素不再需要重复朗读。这与CalendarGrid的role="application"共同构成了完整的 WAI-ARIA 日期网格语义。
以上所有组件均继承PrimitiveProps,因此as/asChild的能力同样适用于它们,可实现整棵表格树的元素级定制。
完整可运行示例:带状态的日历网格
结合前述内容,下面是一个同时使用as定制与只读状态的完整示例(假设已安装@internationalized/date与 radix-vue,并引入所需部件):
<script setup> import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNext, CalendarPrev, CalendarRoot } from 'reka-ui' import { ref } from 'vue' // 日历是否只读,由 CalendarRoot 统一控制 const readonly = ref(false) </script> <template> <CalendarRoot :readonly="readonly" class="calendar" > <CalendarHeader> <CalendarPrev /> <CalendarHeading /> <CalendarNext /> </CalendarHeader> <!-- 保持默认 table,但可通过 as 随时替换渲染元素 --> <CalendarGrid> <CalendarGridHead> <CalendarGridRow> <CalendarHeadCell v-for="day in 7" :key="day" /> </CalendarGridRow> </CalendarGridHead> <CalendarGridBody> <CalendarGridRow v-for="row in 6" :key="row"> <CalendarCell v-for="col in 7" :key="col"> <CalendarCellTrigger /> </CalendarCell> </CalendarGridRow> </CalendarGridBody> </CalendarGrid> </CalendarRoot> </template> <style> /* 当 readonly 为真时,网格会带上 contenteditable="false">【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue
项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考