CalendarPicker 日历选择器
以天、周、月、季度或年为周期浏览并选择一个或多个日期,也可以在日期格中展示日程内容。
用法
选择日期
组件结构
加粗的是必需部件。
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:点击一次加入,再点击一次移除,集合按日期升序
不可选的日期
isDateUnavailable 与 min / max 都只阻止落值不阻止聚焦:方向键照常可以经过
格子内放置内容
cell-trigger 的内容全部由作者编写,日号之外还可放置自己的标记
设计指引
何时使用
- 需要先看到整段时间的分布再选择日期:日程、排班、可预约情况。
- 需要在格子中显示当天的事件。
- 需要一次选择多个不连续的日期。
何时不用
特性
- 标准结构由标题栏、前后翻页按钮、星期表头和日期网格组成;网格数据通过插槽作用域交给作者渲染。
granularity决定周期格的生成方式,selectionMode独立决定单选或多选;两个维度互不绑定。- 五种粒度统一产出
CalendarPeriod:稳定键、周期首尾、标签与相邻容器标记都来自同一份数据。 week是一级粒度,使用一行一个整周的网格;不通过日格高亮模拟整周选择。isDateUnavailable与min/max只阻止取值,不阻止聚焦;粗粒度周期越过任一边界时整格不可选。- 支持固定六行与显式多面板;翻页时整个视窗一起移动。
- 日期、月份与年份格按下时轻微缩放,松开后复原;减弱动效下自动收敛。
- 年份网格采用三列紧凑滚动面,可由作者按业务上下界铺入连续年份,复用日历格的选中与键盘语义。
calendarPeriodValue将选中的周期转换为{ granularity, start, end, keys },可直接用于查询参数。- 切换粒度会清空旧选择并保留浏览锚点,避免不同周期键之间发生隐式转换。
- 周首日、月份名与星期名跟随
locale:en-US周日起、zh-CN周一起。未提供locale时跟随宿主浏览器语言,读取失败时使用en-US;需要固定排法时显式传入locale。
组合
最佳实践
- 今天使用 1px 品牌环 + 品牌字,选中使用实心强调面,两种状态必须能同时辨认。
- 多选时使用
aria-multiselectable告知读屏用户可以多选,不依赖视觉提示。 - 格子中的内容超出时收起,避免某一行明显高于其他行。
反模式
- 不可选的日期无法聚焦,键盘用户无从知道该位置的内容。
- 将它用作日期输入框。
- 用多选模拟区间:中间的日期不会自动补齐,也没有拖选与预览。
API 参考
产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-calendar-picker> |
| 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 |
状态
以下名称仅用于内部状态机。
状态:idle
事件:PRESS.START · PRESS.END
判据:canPress
connect API
getXxxProps() 返回对应部件的宿主属性。
| 成员 | 类型 | 说明 |
|---|---|---|
selectionMode | CalendarPickerSelectionMode |
无障碍
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
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 | disabledhoverloadingnot([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 | disabledis(:active, [data-pressed])loadingnot([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 | background-color | disabledfocus-visiblehoverloadingnot([data-disabled])not([data-loading])selected | --xh-bg-brand | calendar-picker 的 cell-trigger 部件 background-color 覆盖槽。 |
--xh-calendar-picker-cell-bg-selected-active | cell-trigger | background-color | disabledis(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedselected | --xh-bg-brand-active | calendar-picker 的 cell-trigger 部件 background-color 覆盖槽。 |
--xh-calendar-picker-cell-bg-selected-disabled | cell-trigger | background-color | disabledselected | --xh-bg-subtle | calendar-picker 的 cell-trigger 部件 background-color 覆盖槽。 |
--xh-calendar-picker-cell-fg | cell-trigger | color | @media printdefaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedselected | --xh-fg-default | calendar-picker 的 cell-trigger 部件 color 覆盖槽。 |
--xh-calendar-picker-cell-fg-outside | cell-trigger | color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])outside-monthpressed | --xh-fg-subtle | calendar-picker 的 cell-trigger 部件 color 覆盖槽。 |
--xh-calendar-picker-cell-fg-selected | cell-trigger | color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedselected | --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 | cellcell-trigger | insetpadding | 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 | headingheading-month-triggerheading-year-trigger | color | defaultdisabledfocus-visiblenot([hidden]) | --xh-fg-default | calendar-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 color 覆盖槽。 |
--xh-calendar-picker-heading-font-size | headingheading-month-triggerheading-year-trigger | font-size | defaultnot([hidden]) | --xh-text-label-size | calendar-picker 的 heading、heading-month-trigger、heading-year-trigger 部件 font-size 覆盖槽。 |
--xh-calendar-picker-heading-font-weight | headingheading-month-triggerheading-year-trigger | font-weight | defaultnot([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-triggerheading-year-trigger | background-color | disabledis(:active, [data-pressed])loadingnot([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-triggerheading-year-trigger | color | disabledhoveris(:active, [data-pressed])loadingnot([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-triggerheading-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-triggerheading-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-triggernext-year-triggerprev-triggerprev-year-trigger | background-color | defaultfocus-visible | --xh-_action-variant-bg-focus-visible--xh-_action-variant-bg-rest | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 background-color 覆盖槽。 |
--xh-calendar-picker-nav-bg-hover | next-triggernext-year-triggerprev-triggerprev-year-trigger | background-color | disabledhoverloadingnot([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-triggernext-year-triggerprev-triggerprev-year-trigger | background-color | disabledis(:active, [data-pressed])loadingnot([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-triggernext-year-triggerprev-triggerprev-year-trigger | color | defaultfocus-visible | --xh-fg-muted | calendar-picker 的 next-trigger、next-year-trigger、prev-trigger、prev-year-trigger 部件 color 覆盖槽。 |
--xh-calendar-picker-nav-fg-hover | next-triggernext-year-triggerprev-triggerprev-year-trigger | color | disabledhoveris(:active, [data-pressed])loadingnot([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-triggernext-year-triggerprev-triggerprev-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-triggernext-year-triggerprev-triggerprev-year-trigger | block-sizeinline-sizemin-inline-size | defaultxh-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=monthview=quarterview=weekview=year | --xh-space-1 | calendar-picker 的 grid 部件 gap 覆盖槽。 |
--xh-calendar-picker-period-py | cell-triggergrid | padding-block | is([data-view='week'], [data-view='month'], [data-view='quarter'], [data-view='year'])view=monthview=quarterview=weekview=year | --xh-space-2 | calendar-picker 的 cell-trigger、grid 部件 padding-block 覆盖槽。 |
--xh-calendar-picker-period-radius | cell-triggergrid | border-radius | is([data-view='week'], [data-view='month'], [data-view='quarter'], [data-view='year'])view=monthview=quarterview=weekview=year | --xh-shape-control | calendar-picker 的 cell-trigger、grid 部件 border-radius 覆盖槽。 |
--xh-calendar-picker-row-gap | grid-bodygrid-head | gap | default | --xh-space-0 | calendar-picker 的 grid-body、grid-head 部件 gap 覆盖槽。 |
--xh-calendar-picker-today-bg | cell-trigger | background-color | disabledfocus-visibletoday | transparent | calendar-picker 的 cell-trigger 部件 background-color 覆盖槽。 |
--xh-calendar-picker-today-border | cell-trigger | borderborder-color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedtoday | --xh-fg-brand | calendar-picker 的 cell-trigger 部件 border、border-color 覆盖槽。 |
--xh-calendar-picker-today-fg | cell-trigger | color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedtoday | --xh-fg-brand | calendar-picker 的 cell-trigger 部件 color 覆盖槽。 |
--xh-calendar-picker-week-cell-px | cell-triggergrid | 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-numberweek-row | grid-template-columns | has(> [data-part='week-number'])not([hidden]) | --xh-control-h-md | calendar-picker 的 week-number、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 覆盖槽。 |
动效
本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。
响应式
皮肤另按输入能力分档:pointer: coarse:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。
RTL
皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像;另有按 dir 分支的规则。
