日历
以日期网格显示一个月,可以放在页面里,也可以放在选择器里;用户可以从中选择一个日期或一段日期范围。月份是一列连续的周,跟随滚动和拖动在月份之间移动。
- 类别
- 选择
- 令牌
comp.calendar- 层级
- 基础
- Web
tanrei-calendarschedule - SwiftUI
TanreiCalendarradio_button_unchecked - Jetpack Compose
TanreiCalendarradio_button_unchecked - Flutter
TanreiCalendarradio_button_unchecked - React Native
Calendarradio_button_unchecked
生成自 docs/status/status.json, specs/calendar.spec.json
日历是嵌在页面里的月份网格。它没有自己的表面、内边距和层级,所以可以和其他内容并排放在页面里,例如预订表单或报表的筛选条件;也可以放在 DateTimePicker 里,后者原样组合它。它选择一个日期(mode 为 single),或一段连续的日期范围(mode 为 range)。
目前仅在 Web 上;其他平台在计划中
Web 组件按本页的描述实现了日历:月份是一列连续的周,跟随滚动和拖动。本页的演示就是这个组件。SwiftUI、Compose、Flutter 和 React Native 尚未实现它。它们的包里仍保留着已停用的日期选择器的代码,本页不对其作说明。
一个日期。滚动或拖动日期,或者使用按钮,可以切换月份
用户选择某一天时,日历会自行设置 value(或 start 和 end),然后触发 change。你也可以自己设置它们:如果设置的日期不在当前显示的月份里,它所在的月份会被显示出来。
<tanrei-calendar id="check-in" label="入住日期" value="2026-09-17" min="2026-09-01"></tanrei-calendar>
<!-- 范围选择,空余的单元格里显示相邻月份的日期。 -->
<tanrei-calendar id="stay" mode="range" label="住宿" show-adjacent-days
disabled-dates="2026-09-21 2026-09-22"></tanrei-calendar>
<script type="module">
import '@tanrei/web/components/calendar';
const checkIn = document.querySelector('#check-in');
// 列表无法表达的规则通过属性传入:一个接收 ISO 日期、返回布尔值的函数。
checkIn.isDateDisabled = (iso) => bookedDays.has(iso);
// change 触发时,元素已经持有新的日期。
checkIn.addEventListener('change', (e) => save(e.detail.value));
// 范围选择在每次选择后都会触发 change;只选了开始日期时,end 为 ""。
document.querySelector('#stay').addEventListener('change', (e) => show(e.detail.start, e.detail.end));
</script>import { Calendar } from '@tanrei/react';
<Calendar
label="入住日期"
value={checkIn}
min="2026-09-01"
isDateDisabled={(iso) => bookedDays.has(iso)}
onChange={(e) => setCheckIn(e.detail.value)}
/>
<Calendar
mode="range"
label="住宿"
start={stay.start}
end={stay.end}
showAdjacentDays
disabledDates={['2026-09-21', '2026-09-22']}
onChange={(e) => setStay({ start: e.detail.start, end: e.detail.end })}
/>这里没有 SwiftUI、Compose、Flutter 和 React Native 的示例,因为这些平台尚未实现日历。它在这些平台上将使用的名称,见规格中的平台对照表。
日期和时间用两个组件来选择:这个日历,以及容纳它的 DateTimePicker。
| 以前 | 现在 |
|---|---|
| 日期选择器,模态或停靠式 | date 模式的 DateTimePicker,modal 或 docked |
| 放在页面里的月份网格 | 日历 |
| 带范围的日期选择器 | mode 为 range 的日历,或带 range 的 DateTimePicker |
| 日期选择器的文本输入形式 | type 为 date 的文本字段,它的选择器按钮会打开 DateTimePicker |
| 日期选择器的令牌 | 网格使用 comp.calendar.*,表面、页眉和操作使用 comp.date-time-picker.* |
在 Web 上,以前用来选择日期的元素已经不存在。原先把它的网格直接画在页面里的地方,改用 <tanrei-calendar>,value、min、max、disabled-dates、today、locale 和 first-day-of-week 保持不变;原先把它作为选择器打开的地方,改用 <tanrei-date-time-picker>。
- Material 3 的停靠式和模态日期选择器,是
date模式的 DateTimePicker。它的日期范围选择器是同一个组件加上range。m3-like 预设用 Material 的容器和圆角绘制它们,横向翻页切换月份,并把文本操作放在末尾。 - 放在页面里的日历,对应 Flutter 的
CalendarDatePicker和 SwiftUI 的图形式日期选择器样式:在 SwiftUI、Compose 和 Flutter 上是TanreiCalendar,在 React Native 上是Calendar。 - Material 用来切换到文本输入的铅笔和键盘切换按钮没有对应物:日期在文本字段里输入。
- Apple 的图形式日期选择器是轴为水平方向的日历,cupertino 预设就是这样设置的。
构成
| 部件 | 说明 |
|---|---|
1 navigation-row | The top row: the view toggle at the start, and the previous and next buttons at the end. |
2 view-toggle | The displayed month and year with a chevron (expand_more). Pressing it switches between the day view and the year view. It is drawn with the unselected condition's colors. |
3 navigation | The previous-month and next-month buttons: Tanrei icon-only Buttons of the text variant whose arrows follow the axis the months move along. They are hidden in the year view. A scroll of a wheel or a trackpad, a drag with a finger, a pen or a mouse, and Page Up and Page Down move the month too. |
4 weekday-row | The narrow names of the seven weekdays, from the first day of the week. |
5 day | One day in the grid (an item the calendar generates): its number on a round container that carries the day's condition and its state answer. The containers of the days inside a range join into one band. On the vertical axis the days are one unbroken column of weeks: a week two months share is one row, which a month change carries from its place in the one view to its place in the other. |
6 year · 可选 | One year of the year view (an item the calendar generates), on a container that carries the year's condition and its state answer. |
7 focus-ring | Gap-then-ring indicator around the container of the day, year or view toggle that has keyboard focus. |
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mode | single | range | "single" | single: one date, in value. range: one contiguous span of dates, from start to end inclusive. |
value | string | "" | The chosen date in single mode, as an ISO 8601 calendar date ("2026-09-17"); empty when none is chosen. The calendar sets it when the person chooses a day and emits change; the product may also set it. A calendar date has no time and no time zone. |
start | string | "" | The first date of the chosen range in range mode, as an ISO 8601 calendar date; empty when none is chosen. |
end | string | "" | The last date of the chosen range in range mode, as an ISO 8601 calendar date, never before start; empty while only start is chosen. |
min | string | "" | The earliest date that can be chosen, as an ISO 8601 calendar date; empty for no limit. |
max | string | "" | The latest date that can be chosen, as an ISO 8601 calendar date; empty for no limit. |
disabledDates | strings | | Dates inside min and max that cannot be chosen (booked, closed), as ISO 8601 calendar dates. Every platform also takes a predicate (isDateDisabled) for a rule no list can hold, such as every weekend; a day is unavailable when either says so. |
today | string | "" | The date drawn as today, as an ISO 8601 calendar date. Empty uses the device's current date in its own time zone. Set it when the product's today is another (a venue's time zone, a server's date) and in tests. |
firstDayOfWeek | locale | sunday | monday | tuesday | wednesday | thursday | friday | saturday | "locale" | The weekday in the first column. locale takes it from the locale (Sunday in the United States and Japan, Monday in most of Europe). |
locale | string | "" | A BCP 47 language tag ("en-US", "ja-JP") for month and weekday names, digits and the first day of the week. Empty uses the platform's current locale. The calendar is the Gregorian calendar in every locale. |
startView | day | year | "day" | Whether the calendar first shows the days of a month or the list of years. year suits a date far from today, such as a birthday. |
showAdjacentDays | boolean | false | Fills the grid's spare cells with the days of the previous and next month, drawn quieter than the month's own. Such a day can be chosen when it is available, and a range may start or end on it; choosing one also shows its month. |
label | string | "" | The accessible name of the whole calendar ("Check-in date"). Needed when a page has more than one calendar, or when nothing near it says what the date is for. A picker that holds the calendar names it with its own label. |
startLabel | string | "" | Range mode: the description of the start day, in the product's language ("Start date"). The product's: Tanrei has no words for it. Empty reads the product's words. |
endLabel | string | "" | Range mode: the description of the end day, in the product's language ("End date"). The product's: Tanrei has no words for it. Empty reads the product's words. |
previousMonthLabel | string | "" | The accessible name of the previous-month button, in the product's language ("Previous month"). The product's: Tanrei has no words for it. Empty reads the product's words. Required: a glyph alone is not a name. |
nextMonthLabel | string | "" | The accessible name of the next-month button, in the product's language ("Next month"). The product's: Tanrei has no words for it. Empty reads the product's words. Required: a glyph alone is not a name. |
yearViewLabel | string | "" | The description of the view toggle, whose name is the month and year it shows, in the product's language ("Choose a year"). The product's: Tanrei has no words for it. Empty reads the product's words. |
todayLabel | string | "" | Read after today's date and the current year, where the platform has no property that says so (the web's aria-current), in the product's language ("Today"). The product's: Tanrei has no words for it. Empty reads the product's words. With neither, the platform's own word for the present day in the locale, where it has one. |
事件
| 事件 | 说明 |
|---|---|
change | The person chose a day that changes the choice. Carries value in single mode, or start and end in range mode (end empty after a start alone). The calendar has already set the props it carries. |
条目
该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。
day
One day in the grid. The calendar generates the days from the month, the bounds, the disabled dates and showAdjacentDays; a product and a fixture never list them.
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
date | string | "" | The day, as an ISO 8601 calendar date. |
disabled | boolean | false | Whether the day is unavailable: before min, after max, in disabledDates or named by isDateDisabled. |
adjacent | boolean | false | Whether the day belongs to the previous or next month, shown because showAdjacentDays is true. |
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 条目 | renderDay | TanreiCalendarDay | TanreiCalendarDay | TanreiCalendarDay | CalendarDay |
date · 属性 | date | date | date | date | date |
disabled · 属性 | disabled | disabled | disabled | disabled | disabled |
adjacent · 属性 | adjacent | isAdjacent | adjacent | adjacent | adjacent |
| 备注 | A gridcell button in the shadow root with part="day", data-date and, for a day of the neighbouring month, data-adjacent, rendered by renderDay. |
year
One year of the year view, from min's year to max's year. The calendar generates them; a product and a fixture never list them.
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
year | number | 0 | The year. |
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 条目 | renderYear | TanreiCalendarYear | TanreiCalendarYear | TanreiCalendarYear | CalendarYear |
year · 属性 | year | year | year | year | year |
| 备注 | A button in the shadow root with part="year", rendered by renderYear. |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| ID | 需求 |
|---|---|
CAL-1 | THE calendar SHALL lay out, from top to bottom, the navigation row, the weekday row and six rows of seven cells, each cell a square of comp.calendar.cell.size but never smaller than sys.target.touch for touch input and sys.target.pointer for fine pointers (a platform that cannot tell the input type SHALL use sys.target.touch), because the cells are the days' hit areas and cannot extend over each other; the first day of the displayed month SHALL sit in the column of its weekday, and all six rows SHALL be kept when the month needs fewer, so that the calendar keeps its height when the month changes. The navigation row and the weekday row are each one cell tall. THE calendar SHALL draw no surface and no padding of its own: it is as wide as its seven columns, and the page or the picker that holds it gives it its background and inset. |
CAL-2 | THE calendar SHALL draw each day from exactly one condition, by precedence: selected for the date of single mode and for a range's start and end; in-range for a day strictly between a range's start and end; today for the date today names (or the device's current date) when it is neither; adjacent for a shown day of the previous or next month (CAL-15) when it is none of these; otherwise unselected. It SHALL draw each year of the year view from selected for the displayed year, today for the current year when it is not displayed, and otherwise unselected, and the view toggle from unselected. WHILE a day is unavailable THE calendar SHALL draw it from the disabled tokens of its condition. |
CAL-3 | THE calendar SHALL draw a day's number in the locale's digits, centred in its cell, in comp.calendar.day.label.typescale and the label color of its condition (comp.calendar.unselected.label.color, and so on), on a container of side comp.calendar.day.container.size centred in the cell, with comp.calendar.day.container.shape corners eased by sys.shape.corner-smoothing, in the container color of its condition, with an outline of comp.calendar.outline.width in the outline color of its condition. The container of an in-range day SHALL instead span the full width of its cell at the same height, so that the containers of consecutive in-range days join into one band that breaks at the ends of a row with square ends; WHILE a range's start and end differ each of them SHALL also draw, behind its own container, the half of its cell toward the other in comp.calendar.in-range.container.color, so that the band runs into it. The band SHALL cover every day the grid shows between start and end, and no empty cell. |
CAL-4 | THE calendar SHALL draw each weekday's narrow name from the locale ("S", "M" in English; "日", "月" in Japanese) centred in its column, in comp.calendar.weekday.typescale and comp.calendar.weekday.color, and SHALL expose it as a column header named by the weekday's full name; weekday names SHALL take no focus. |
CAL-5 | THE calendar SHALL draw the navigation row with the view toggle at the start and, in the day view, the previous-month and next-month buttons at the end, comp.calendar.navigation.gap apart, each a Tanrei Button of the text variant and the medium size with an icon and no label, named previousMonthLabel and nextMonthLabel. WHILE comp.calendar.navigation.axis is vertical their glyphs SHALL be arrow_upward and arrow_downward; WHILE it is horizontal they SHALL be arrow_back for the previous month and arrow_forward for the next in every layout direction, pointing toward the start and the end: in a right-to-left layout the icon helper mirrors both, because the icon set marks them as mirrored, and the calendar SHALL NOT exchange the two names or mirror a glyph itself. The previous button SHALL be disabled while the displayed month is min's month and the next button while it is max's month, and in the year view both SHALL be hidden. THE calendar SHALL NOT change the buttons' variant, size or tokens. |
CAL-6 | THE calendar SHALL draw the view toggle as a container at least comp.calendar.view-toggle.container.height tall with comp.calendar.view-toggle.container.shape corners eased by sys.shape.corner-smoothing, padded by comp.calendar.view-toggle.container.padding-inline at each end, holding the displayed month and year in the locale's form ("September 2026", "2026年9月") in comp.calendar.view-toggle.label.typescale and, comp.calendar.view-toggle.gap after it, the expand_more glyph at comp.calendar.view-toggle.icon.size with the family and fill of an unselected icon, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, both in comp.calendar.unselected.label.color on comp.calendar.unselected.container.color; the glyph SHALL be turned half a turn while the year view is shown, over comp.calendar.motion.state-duration with comp.calendar.motion.state-easing (at once where motion is reduced). WHERE the view toggle is shorter than the minimum target it SHALL extend its hit area to sys.target.touch for touch input and sys.target.pointer for fine pointers without changing its visual size. WHEN it is activated it SHALL switch between the day view and the year view. |
CAL-7 | WHEN the year view is shown THE calendar SHALL draw, in the space of the weekday row and the six rows of days, the years from min's year to max's year (1900 to 2100 when min or max is empty) in rows of three cells, each a third of the grid's width and comp.calendar.cell.size tall, each year centred in its cell in the locale's digits in comp.calendar.year.label.typescale on a container of comp.calendar.year.container.width by comp.calendar.year.container.height with comp.calendar.year.container.shape corners, colored and outlined as a day of its condition is; the list SHALL scroll, and SHALL open scrolled so that the displayed year's row is the third row in view, or as near to it as the list's ends allow. WHEN the user chooses a year THE calendar SHALL show the day view of the month with the displayed month's number in that year, clamped between min's and max's months, move focus to its active day and SHALL NOT change the choice; WHEN the view toggle is activated in the year view it SHALL show the day view unchanged. |
CAL-8 | WHILE mode is single, WHEN the user chooses an available day by pointer, touch, Enter or Space THE calendar SHALL set value to it and, if it differs from the value before, emit change once with it. |
CAL-9 | WHILE mode is range, WHEN the user chooses an available day THE calendar SHALL make it the start and clear the end when no start is chosen, when both are chosen, or when the day is before the start, and otherwise SHALL make it the end (a start and end on the same day make a one-day range); after each choice it SHALL set start and end and emit change once with both, end empty after a start alone. A range SHALL be one contiguous span from start to end inclusive; unavailable days between them SHALL stay inside it, drawn and exposed as unavailable (a product that cannot accept such a span says so after change). |
CAL-10 | THE calendar SHALL give the day grid one tab stop, the active day: WHEN a month is shown the active day SHALL be the chosen date (single mode) or start (range mode) when it lies in that month, otherwise today when it does, otherwise the first available day of the month, otherwise its first day; only the active day SHALL take focus from Tab, and the keys of CAL-11 move it. |
CAL-11 | WHILE focus is on a day THE calendar SHALL move the active day and focus with Right Arrow and Left Arrow one day in the direction the arrow points (the next and the previous day left to right, the previous and the next right to left), with Down Arrow and Up Arrow one week later and earlier, with Home and End to the first and last day of its week row, with Page Down and Page Up to the same day of the next and previous month (the last day of that month when it is shorter), and with Shift+Page Down and Shift+Page Up to the same day of the next and previous year, whatever comp.calendar.navigation.axis is; WHEN the new active day lies in another month THE calendar SHALL show that month; it SHALL NOT move the active day before min or after max; unavailable days SHALL be reachable but SHALL NOT be chosen; Enter and Space SHALL choose the active day. |
CAL-12 | WHILE focus is in the year view THE calendar SHALL give the list one tab stop, the displayed year, and SHALL move focus with Right Arrow and Left Arrow one year in the direction the arrow points, with Down Arrow and Up Arrow one row, and with Home and End to the first and last year, scrolling the focused year into view; Enter and Space SHALL choose the focused year (CAL-7). |
CAL-13 | WHEN the previous-month or next-month button is activated, a step of a detented wheel moves the month (CAL-33), a scroll or a drag ends on another month than the one shown when it began (CAL-34, CAL-31) or a day of a neighbouring month is chosen (CAL-15), THE calendar SHALL show that month, keep focus where it was, make the active day the same day number of the new month (its last day when the month is shorter; the chosen day, WHEN a day was chosen) and announce the new month and year once, politely; WHEN a year is chosen in the year view it SHALL announce the month and year it shows the same way. A month that a scroll only passes through is shown without an announcement (CAL-34). |
CAL-14 | WHEN the displayed month changes by a button, a key, a step of a detented wheel (CAL-33) or a chosen day THE calendar SHALL play the change over comp.calendar.motion.page-duration with comp.calendar.motion.page-easing: WHILE comp.calendar.navigation.axis is vertical as one continuous scroll of the weeks, every frame of it being the in-between look of CAL-29 at the progress the easing gives, so that the two months are never drawn as two separate grids and a week they share is neither drawn twice nor replaced; WHILE the axis is horizontal as two pages, the old month's grid sliding out toward the start and the new one in from the end for a later month, and the reverse for an earlier one. Every way of moving the month SHALL move the same thing — on the vertical axis the one progress of CAL-29, on the horizontal one the place of the two pages — and none SHALL have a look of its own: a scroll of a wheel or a trackpad is followed as CAL-33 says and settles as CAL-34 says, and a drag is followed as CAL-30 says and settles as CAL-31 says. WHEN one of them begins while the days are still moving — a month change is playing, or the days are settling after a scroll or a drag — THE calendar SHALL carry on from what is on screen, without a jump and without waiting for the motion to end. WHEN it switches between the day view and the year view it SHALL cross-fade them over the same duration and easing; WHERE motion is reduced it SHALL make all of these changes at once. The weekday row, the navigation row and the calendar's size SHALL NOT move. |
CAL-15 | WHILE showAdjacentDays is true THE calendar SHALL fill the cells before the displayed month's first day and after its last with the days of the previous and next month, in order, drawn from the adjacent condition unless a higher one applies (CAL-2), and treated as any day for availability (CAL-16), choosing (CAL-8, CAL-9) and the range's band (CAL-3); WHEN the user chooses such a day by pointer or touch THE calendar SHALL choose it as any day, emitting change as CAL-8 and CAL-9 say, and SHALL then show that day's month (CAL-13) with the motion of CAL-14, where the chosen day is one of the month's own and the active day, taking focus if focus was in the grid; a range's start and its end SHALL both do so. A press on an unavailable one SHALL choose nothing and show no other month. WHILE showAdjacentDays is false those cells SHALL be empty, take no press and be hidden from assistive technology. |
CAL-16 | THE calendar SHALL treat as unavailable every day before min, after max, listed in disabledDates or named by the platform's isDateDisabled predicate. WHILE a day is unavailable THE calendar SHALL render its container, label, outline and opacity from the disabled tokens of its condition (comp.calendar.unselected.container.color at rest, and so on), SHALL NOT make it the chosen date, start or end, SHALL expose it to assistive technology as unavailable (disabled), and SHALL keep it reachable by the arrow keys. WHEN value, start or end names an unavailable day THE calendar SHALL still draw it as chosen, from the disabled tokens of the selected condition, until the person chooses another. |
CAL-17 | WHEN the calendar is first shown THE calendar SHALL show the month of value or start, otherwise of today, clamped between min's month and max's month, or the year view WHERE startView is year; WHEN the product sets value, start or end to a date outside the displayed month THE calendar SHALL show that date's month, without the motion of CAL-14. |
CAL-18 | WHILE a day, a year or the view toggle is hovered, keyboard-focused or pressed THE calendar SHALL render its container, label, outline and opacity from the component tokens of its condition in the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. WHEN these colors change between states THE calendar SHALL animate them over comp.calendar.motion.state-duration with comp.calendar.motion.state-easing, and SHALL change them at once where motion is reduced. |
CAL-19 | WHEN a day, a year or the view toggle receives keyboard focus THE calendar SHALL show the focus ring around its container — around the circle of comp.calendar.day.container.size for every day, an in-range day included — without animation; focus gained by pointer or touch SHALL NOT show it. |
CAL-20 | WHEN the calendar's day, year or view toggle is pressed by pointer, touch, Enter or Space THE calendar SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner. |
CAL-21 | WHILE pressed THE calendar's day, year or view toggle SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; label, outline, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.calendar.motion.feedback-duration with comp.calendar.motion.state-easing, fades over comp.calendar.motion.state-duration with comp.calendar.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. |
CAL-22 | THE calendar's day, year and view toggle SHALL ease their container corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show their keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. |
CAL-23 | THE calendar SHALL expose itself as a group named by label while label is not empty, the day grid as a grid named by the displayed month and year with the weekday names as its column headers, and each shown day as a cell that can be activated, named by its full date in the locale ("Thursday, September 17, 2026") followed by todayLabel for today where the platform has no property for the current date, with its selected state (true for the chosen date and for every day of a range), described by startLabel or endLabel for a range's start and end, and with its disabled state while unavailable; empty cells SHALL be hidden. It SHALL expose the view toggle as a button named by its visible month and year, described by yearViewLabel, with its expanded state (true while the year view is shown), and each year as a button with its selected state and todayLabel for the current year. |
CAL-24 | THE calendar SHALL take month and weekday names, digits and the locale's first day of the week from locale, or from the platform's current locale while locale is empty, and SHALL use firstDayOfWeek instead of the locale's while it is not locale; WHERE the platform has no week data for the locale it SHALL use the first days of CLDR's weekData, which the platform package embeds. Every date SHALL be a Gregorian calendar date without a time or a time zone, and WHILE today is empty THE calendar SHALL read the device's current date in its own time zone WHEN it is first shown and WHEN the date changes while it is shown. |
CAL-25 | THE calendar's days, years, weekday names and view toggle SHALL grow with enlarged text rather than clip it: a day's container SHALL grow to hold its number. WHERE the space given to the calendar is narrower than seven minimum targets, the cells SHALL narrow equally so that the grid fits it, keeping their numbers at their size, and the calendar SHALL never be wider than the space it is given. |
CAL-26 | WHEN the layout direction is right-to-left THE calendar SHALL mirror its layout — the first day of the week in the rightmost column, the view toggle at the right and the navigation buttons at the left, and, on the horizontal axis, the slide of CAL-14 and the side each neighbouring month lies on (the next month toward the left, so that a drag toward the right brings it, CAL-30) — and SHALL NOT mirror the vertical scroll of the weeks, a vertical drag or the vertical arrows. A drag SHALL move the days where the pointer goes, and a scroll of a wheel or a trackpad SHALL move them where it moves the content of a scrolled list, in every layout direction. |
CAL-27 | WHEN mode, startView or firstDayOfWeek names a value the calendar does not define THE calendar SHALL behave as its default value and in debug builds report the unknown value; WHEN value, start, end, min, max, today or an entry of disabledDates is not an ISO 8601 calendar date, end is before start, value is set in range mode or start or end in single mode THE calendar SHALL ignore it and in debug builds report it; WHEN min is later than max THE calendar SHALL ignore both and report it in debug builds. |
CAL-28 | THE calendar SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the calendar is shown THE calendar SHALL draw itself for the new direction at once, without a motion of its own, keeping the displayed month, the selection and its focus. |
CAL-29 | WHILE comp.calendar.navigation.axis is vertical THE calendar SHALL treat the days as one unbroken column of week rows, in which a month's view is the six rows from the row of its first day, and SHALL draw every look between the views of two neighbouring months from one progress, 0 at the one and 1 at the other, with no threshold. The rows SHALL be offset along the column by the progress times the distance between the two views' first rows (four or five rows, each comp.calendar.cell.size tall) and clipped to the grid's six rows, so that a week the two views share is one row that travels from its place in the one to its place in the other. Every day SHALL be drawn at the linear interpolation by the progress between what the one view draws for it and what the other draws: its container, label and outline colors between the tokens of the adjacent condition and those of the unselected condition (comp.calendar.adjacent.label.color and comp.calendar.unselected.label.color, and so on, in the state the day is in) WHERE it is a neighbouring month's day in the one view and the month's own in the other (CAL-2), and its opacity between 1 where a view draws it and 0 where a view does not — a row outside that view's six, or a cell that view leaves empty because showAdjacentDays is false — so that a day coming into view fades in and a day leaving it fades out as far as the weeks have moved. A day that both views draw alike SHALL NOT change, and the band of a range (CAL-3) SHALL follow the same rule, each piece with its day. WHERE the two months are not neighbours (a step of a year, CAL-11; a date the keys reach in a further month) the distance SHALL be six rows, as if the later month's view began where the earlier one's ends. |
CAL-30 | WHILE the day view is shown, WHEN the user presses the day grid with touch, a pen or the primary button of a mouse and drags past the platform's touch slop (8 points where the platform names none, for every kind of pointer), further along the axis comp.calendar.navigation.axis names than across it, THE calendar SHALL take the drag: the press SHALL choose no day when it ends (CAL-8, CAL-9), the day under it SHALL leave its pressed state as it does when a press ends (CAL-20), and from then until the drag ends THE calendar SHALL move the days by the distance the pointer has travelled along the axis since the press, in the frame the pointer moves and without a motion of its own — toward the next month for a drag toward the top (vertical) or the start (horizontal), toward the previous month for the reverse — never further than the view of the month next to the displayed one, and never past min's or max's month, where the days SHALL stop. WHILE the axis is vertical THE calendar SHALL draw the in-between look of CAL-29 at the progress that travel gives (the travel over the distance between the two views), so that the scroll of the weeks, the fading in of the days coming into view and the change of a neighbouring month's day into one of the month's own all follow the pointer continuously; WHILE the axis is horizontal the two pages of CAL-14 SHALL travel with the pointer, side by side. A press that ends before it has passed the slop SHALL choose the day under it as CAL-8 and CAL-9 say, with every kind of pointer, so that a click of a mouse on a day still chooses it. WHILE a drag the calendar has taken lasts, THE calendar SHALL follow the pointer wherever it goes, outside the grid and the calendar included; no day SHALL be drawn hovered; nothing the calendar draws SHALL be selected as text or picked up by the platform's drag and drop; and a fine pointer SHALL show the platform's cursor for something held and dragged (grabbing) until the drag ends. At rest the cursor over a day SHALL stay the one a pressable day shows: the grid does not advertise the drag, which is a shortcut. A press of another mouse button SHALL take no drag. A drag that is further across the axis than along it when it passes the slop SHALL be left to the scrolling ancestor for its whole length; a scroll of a wheel or a trackpad is followed as CAL-33 says. |
CAL-31 | WHEN the user releases a drag the calendar has taken THE calendar SHALL choose one month: the neighbouring month the days have moved toward WHERE they are a quarter or more of the distance from the displayed month's view toward its view, and otherwise the displayed month — the same quarter that ends a scroll (CAL-34) — or, WHEN the release is a fling along the axis, the one of the two the fling is toward; a fling is a release faster than the platform's minimum fling velocity (Android's ViewConfiguration.getScaledMinimumFlingVelocity, Flutter's kMinFlingVelocity) or, where the platform names none (the web, SwiftUI, React Native), 0.5 points per millisecond, with every kind of pointer. A release's velocity is the pointer's over its last moves: WHERE the pointer has not moved for longer than the time after which the platform's velocity tracker takes a pointer as stopped (40 milliseconds in Android's VelocityTracker, Jetpack Compose's VelocityTracker and Flutter's VelocityTracker) or, where the platform names none (the web, SwiftUI, React Native), 40 milliseconds before the release THE calendar SHALL take the release as having no velocity, so that a drag that stops where it is wanted and is let go there is no fling, however fast it moved before it stopped. WHERE the chosen month is not the displayed one THE calendar SHALL show it as CAL-13 says and bring the days from where the release left them to its view over comp.calendar.motion.page-duration with comp.calendar.motion.page-easing; WHERE it is the displayed month THE calendar SHALL return them over the same motion and announce nothing. The quarter decides only what a release chooses; nothing drawn changes at it. The release SHALL NOT also choose the day under the pointer, and a drag SHALL never change value, start or end, nor emit change. |
CAL-32 | WHEN a drag the calendar has taken is cancelled — the platform cancels the pointer (a system gesture, a lost capture), Escape is pressed while it lasts, the product sets value, start or end (CAL-17), or the year view is shown — THE calendar SHALL return the days to the view of the displayed month (the month the product's date has just named, where it named one) over comp.calendar.motion.page-duration with comp.calendar.motion.page-easing and announce nothing; WHEN the product sets value, start or end, or the year view is shown, while a scroll is being followed THE calendar SHALL do the same and SHALL ignore what is left of that scroll (CAL-34). WHILE a drag or a scroll holds the days between two views THE calendar SHALL NOT change the month and year that the view toggle and the grid's name show, the active day, keyboard focus or what it exposes to assistive technology: the days of the month coming into view SHALL be hidden from assistive technology and take no focus and no press until that month is the displayed one, which it becomes by the release that chooses it (CAL-31), by the end of the scroll that chooses it or by the scroll reaching its view (CAL-34). WHERE motion is reduced THE calendar SHALL still move the days with the pointer and with the scroll and draw the in-between look, which the person's own hand moves, and SHALL put them at the chosen month's view at once when the drag is released or cancelled or the scroll ends. |
CAL-33 | WHILE the day view is shown, WHEN a wheel or a trackpad scrolls over the day grid further along the axis comp.calendar.navigation.axis names than across it, THE calendar SHALL take the scroll and move the days by the distance the scroll reports, one point for each point and in the frame it arrives, as the content of a scrolled list moves — toward the next month for a scroll toward the bottom (vertical) or toward the end (horizontal), toward the previous month for the reverse — without a motion of its own and with no least amount: the slightest scroll moves the days by that much and no further. WHILE the axis is vertical THE calendar SHALL draw the in-between look of CAL-29 at the progress that travel gives; WHILE it is horizontal the two pages of CAL-14 SHALL travel side by side. A scroll that goes on SHALL go on through month after month: WHEN the days reach the view of the neighbouring month THE calendar SHALL make that month the displayed one (CAL-34) and carry on toward the month after it with the distance that is left, in the same frame, so that a scroll never waits for an earlier one to end and no part of it is dropped. The days SHALL stop at the view of min's or max's month; a scroll along the axis that can move them no further SHALL still be the calendar's and SHALL NOT be passed on to a scrolling ancestor. A scroll further across the axis than along it SHALL be left to the scrolling ancestor, and so SHALL a scroll that zooms (a pinch on a trackpad, a wheel turned with the platform's zoom key). WHERE the platform reports a scroll as steps of a detented wheel and not as a distance (a delta in lines or pages on the web; a mouse wheel's notch where the platform tells it from a trackpad), each step SHALL instead move one month, as a press of the previous or next button does (CAL-13, CAL-14), one step after another without waiting for the motion to end. |
CAL-34 | THE calendar SHALL take a scroll it follows (CAL-33) as ended WHEN the platform says that it has ended, its momentum included, where the platform reports that; where it does not (the web), WHEN no scroll event has arrived for 150 milliseconds, or WHEN the scroll has slowed below 0.1 points per millisecond, measured over its events of the last 100 milliseconds — the tail of a momentum scroll, which need not be waited out: THE calendar SHALL then ignore the events that follow for as long as they are no faster than that, until a faster one arrives or 150 milliseconds pass without any. WHEN a scroll ends with the days between two views THE calendar SHALL choose one of the two — the neighbouring month WHERE the days are a quarter or more of the distance from the displayed month's view toward its view, and otherwise the displayed month, so that a slight scroll returns and a deliberate one need not reach halfway — and SHALL bring the days to the chosen view over comp.calendar.motion.page-duration with comp.calendar.motion.page-easing (at once where motion is reduced, CAL-32). The displayed month SHALL be the month whose view the days last rested on or reached: WHEN a scroll that goes on reaches a neighbouring month's view THE calendar SHALL show that month in the view toggle and the grid's name at once, make its days the ones that take focus and presses, and move the active day and a focused day as CAL-13 says, but SHALL announce nothing yet; the quarter is then counted from that view. THE calendar SHALL announce the month and year once, politely, WHEN a scroll has ended and the days have been given their view, and only WHERE that month is not the one that was displayed when the scroll began; a scroll that returns SHALL announce nothing. WHEN a faster scroll event arrives, a drag is taken or a button or a key moves the month while the days are settling THE calendar SHALL carry on from what is on screen (CAL-14). A scroll SHALL never change value, start or end, nor emit change. |
CAL-35 | THE calendar SHALL have no words of its own: it SHALL take startLabel, endLabel, previousMonthLabel, nextMonthLabel, yearViewLabel and todayLabel from the instance's prop and, WHILE that is empty, from the product's words of the same name; WHILE neither gives todayLabel it SHALL take the word from the platform's locale data WHERE the platform has the locale's own word for the present day, and it SHALL take no other word from anywhere. IF neither gives a navigation button its name, or a range's start or end its description, THE calendar SHALL still draw the part and keep it working, and in debug builds SHALL report the missing words once, naming the prop. WHILE neither gives yearViewLabel the view toggle SHALL have no description: its name and its expanded state (CAL-23) still say what it shows. |
CAL-36 | WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE calendar SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on its group, which it SHALL be WHILE it has a name: its label, or one given so — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE label is not empty it is the group's name, over a name given so. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE calendar SHALL take it off that element. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-calendar | TanreiCalendar | TanreiCalendar | TanreiCalendar | Calendar |
mode · 属性 | mode | range | mode | mode | mode |
value · 属性 | value | selection | value | value | value |
start · 属性 | start | range | start | start | start |
end · 属性 | end | range | end | end | end |
min · 属性 | min | minDate | min | firstDate | min |
max · 属性 | max | maxDate | max | lastDate | max |
disabledDates · 属性 | disabledDates | disabledDates | disabledDates | disabledDates | disabledDates |
today · 属性 | today | today | today | currentDate | today |
firstDayOfWeek · 属性 | firstDayOfWeek | firstDayOfWeek | firstDayOfWeek | firstDayOfWeek | firstDayOfWeek |
locale · 属性 | locale | locale | locale | locale | locale |
startView · 属性 | startView | startView | startView | startView | startView |
showAdjacentDays · 属性 | showAdjacentDays | showsAdjacentDays | showAdjacentDays | showAdjacentDays | showAdjacentDays |
label · 属性 | label | label | label | label | accessibilityLabel |
startLabel · 属性 | startLabel | startLabel | startLabel | startLabel | startLabel |
endLabel · 属性 | endLabel | endLabel | endLabel | endLabel | endLabel |
previousMonthLabel · 属性 | previousMonthLabel | previousMonthLabel | previousMonthLabel | previousMonthLabel | previousMonthLabel |
nextMonthLabel · 属性 | nextMonthLabel | nextMonthLabel | nextMonthLabel | nextMonthLabel | nextMonthLabel |
yearViewLabel · 属性 | yearViewLabel | yearViewLabel | yearViewLabel | yearViewLabel | yearViewLabel |
todayLabel · 属性 | todayLabel | todayLabel | todayLabel | todayLabel | todayLabel |
change · 事件 | change | selection | onChange | onChanged | onChange |
| 备注 | Attributes are kebab-case (disabled-dates is a space-separated list of ISO dates, first-day-of-week, start-view, show-adjacent-days, start-label, …); isDateDisabled is a property only (a function from an ISO date string to a boolean). value, start and end are set before change fires, as a native control does. The navigation buttons are <tanrei-button variant="text"> elements with a slotted glyph and no label, in the shadow root. The calendar is not form-associated: the product's field is. A vertical axis reads wheel events with deltaY and a horizontal axis deltaX: a delta in pixels (deltaMode 0) is a distance and is followed point for point (CAL-33), a delta in lines or pages is a step of one month; an event with ctrlKey is a zoom and is left alone. The web reports neither the end of a scroll nor its momentum, so a scroll ends by the 150 ms of silence or the slow-down of CAL-34, and a mouse wheel that the browser reports in pixels is followed as a distance like a trackpad (the web cannot tell them apart). Drags of touch, a pen and a mouse's primary button are followed along the axis (pointer events with setPointerCapture once the drag is taken; touch-action: pan-x on a vertical grid and pan-y on a horizontal one, so the page keeps its pan across it); while a drag is taken the grid shows cursor: grabbing, draws no hover, and the click that follows the release chooses nothing. On the vertical axis the day grid is one keyed list of week rows — the rows of the displayed month and, while a change plays or a drag lasts, those of the month it moves toward — offset by one custom property for the progress (CAL-29), which a drag and a scroll write in each frame and a button's change and a settle transition; a day's colors are mixed by that progress with color-mix() and its opacity follows it, so a shared week is one element throughout. The rows coming into view are inert and aria-hidden until their month is the displayed one. The slop is 8 points for every pointer and a fling 0.5 points per millisecond, as the tabs' and the sheets'. The axis is read from the resolved comp.calendar.navigation.axis custom property once per system. Locale data comes from Intl.DateTimeFormat and Intl.Locale's week info where the browser has it; otherwise the embedded CLDR table. Without locale it reads the nearest lang attribute, then navigator.language. Snapshot harness: the element on a 400 × 440 stage of the surface color (CAL-1's verify). Words: the element has no words of its own. start-label, end-label, previous-month-label, next-month-label, year-view-label and today-label left empty read the product's words, set once with setWords() from @tanrei/web; the element draws again when they change. The present day's word, where neither the instance nor the product gives one, is Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(0, "day"). | Planned. A view: TanreiCalendar(selection: Binding<DateComponents?>, …) for a date and TanreiCalendar(range: Binding<TanreiDateRange>, …) for a range, where TanreiDateRange holds start and end as DateComponents?; the initializer is the mode. Dates are DateComponents with year, month and day in the Gregorian calendar, so no time zone enters. Writing the binding is change. minDate, maxDate, today and disabledDates (Set<DateComponents>) take DateComponents; isDateDisabled is a closure. The words are fields of a TanreiCalendarLabels passed as labels:, whose field names are the spec's; label is the view's own. locale defaults to the \.locale environment. Arrow keys through .onKeyPress; the drag of CAL-30 through a DragGesture(minimumDistance:) on the axis whose predictedEndTranslation gives the fling, which a mouse drives on macOS and iPadOS as a finger does; on macOS the scroll through an NSEvent monitor, whose phase and momentumPhase say when a scroll ends (CAL-34) and whose hasPreciseScrollingDeltas tells a trackpad's distance from a wheel's steps (CAL-33). On the vertical axis the weeks are one column offset by an animatable progress (animatableData), not two pages (CAL-29). The navigation buttons are TanreiButtons with an icon and no label. Built from the date picker's calendar view, which the DateTimePicker also shows. The product's words are read from the \.tanreiWords environment value. | Planned. TanreiCalendar(mode, value, start, end, onChange, min, max, disabledDates, isDateDisabled, today, firstDayOfWeek, locale, startView, showAdjacentDays, label, labels, modifier). Dates are TanreiDate(year, month, day), the date picker's value class (API 23 has no java.time.LocalDate). onChange receives a TanreiDateSelection(value, start, end); the caller keeps the state (hoisting). The other words are fields of a TanreiCalendarLabels. Compose has no grid role: the grid sets collectionInfo (7 columns) and each day collectionItemInfo, Role.Button, selected and disabled(). The drag of CAL-30 through pointerInput on the axis with a VelocityTracker for the fling, for touch, a stylus and a mouse alike; a scroll (CAL-33) through PointerEventType.Scroll, a mouse wheel's being steps. On the horizontal axis a HorizontalPager with one page a month is the likely primitive; on the vertical axis the weeks are one column offset by a progress with lerp for the colors (CAL-29), which a pager cannot draw, since it lays out two pages. Arrow keys through onPreviewKeyEvent. The navigation buttons are TanreiButtons with an icon and no label. The product's words are read from LocalTanreiWords. | Planned. A widget, as Flutter's own CalendarDatePicker is an inline calendar: TanreiCalendar(mode:, value:, start:, end:, onChanged:, firstDate:, lastDate:, currentDate:, …); firstDate, lastDate and currentDate keep Flutter's names. Dates are DateTimes of which only the year, month and day are read; isDateDisabled is a function. onChanged receives a TanreiDateSelection. Month and weekday names come from TanreiDateLocalizations (English built in, no intl dependency), the first day of the week from the embedded CLDR table. On the horizontal axis paging through a PageView; on the vertical axis the weeks are one column driven by an AnimationController whose value is the progress of CAL-29 (Color.lerp for the colors), moved by a GestureDetector's vertical drag callbacks, with DragEndDetails.primaryVelocity for the fling, for every PointerDeviceKind; a trackpad's scroll (CAL-33) arrives as PointerPanZoomUpdateEvents, whose end event ends it (CAL-34), and a mouse wheel's PointerScrollEvent is a step. The navigation buttons are TanreiButtons with an icon and no label. The product's words are read from the TanreiWords inherited widget, which the app fills from its own localizations. | Planned. Controlled: value (or start and end) and onChange({ value, start, end }); the caller moves them. Dates are ISO 8601 strings, never Date objects, so no time zone enters. isDateDisabled is a function prop. The grid has the grid role; days the button role with accessibilityState selected and disabled. The drag of CAL-30 through a PanResponder on the axis (the release's velocity gives the fling) and, on the web renderer, onWheel read as the web package reads it (CAL-33, CAL-34); on the vertical axis the weeks are one column moved by an Animated.Value that is the progress of CAL-29, with interpolate for the colors and the opacity. Locale data from Hermes's Intl.DateTimeFormat; the first day of the week from the embedded CLDR table. The navigation buttons are the package's Button with an icon and no label. The product's words are read from the TanreiWordsProvider context. |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--tanrei-comp-calendar-adjacent-container-color | — | |
--tanrei-comp-calendar-adjacent-disabled-container-color | — | |
--tanrei-comp-calendar-adjacent-disabled-label-color | — | |
--tanrei-comp-calendar-adjacent-disabled-outline-color | — | |
--tanrei-comp-calendar-adjacent-disabled-state-layer-color | — | |
--tanrei-comp-calendar-adjacent-focused-container-color | — | |
--tanrei-comp-calendar-adjacent-focused-label-color | — | |
--tanrei-comp-calendar-adjacent-focused-outline-color | — | |
--tanrei-comp-calendar-adjacent-focused-state-layer-color | — | |
--tanrei-comp-calendar-adjacent-hovered-container-color | — | |
--tanrei-comp-calendar-adjacent-hovered-label-color | — | |
--tanrei-comp-calendar-adjacent-hovered-outline-color | — | |
--tanrei-comp-calendar-adjacent-hovered-state-layer-color | — | |
--tanrei-comp-calendar-adjacent-label-color | sys.color.on-surface-variant | |
--tanrei-comp-calendar-adjacent-outline-color | — | |
--tanrei-comp-calendar-adjacent-pressed-container-color | — | |
--tanrei-comp-calendar-adjacent-pressed-label-color | — | |
--tanrei-comp-calendar-adjacent-pressed-outline-color | — | |
--tanrei-comp-calendar-adjacent-pressed-state-layer-color | — | |
--tanrei-comp-calendar-adjacent-state-layer-color | comp.calendar.adjacent.label.color | |
--tanrei-comp-calendar-cell-size | — | 48px |
--tanrei-comp-calendar-day-container-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-calendar-day-container-size | — | 40px |
--tanrei-comp-calendar-disabled-opacity | — | 1 |
--tanrei-comp-calendar-focused-opacity | — | 1 |
--tanrei-comp-calendar-hovered-opacity | — | 1 |
--tanrei-comp-calendar-in-range-container-color | sys.color.surface-dim | |
--tanrei-comp-calendar-in-range-disabled-container-color | — | |
--tanrei-comp-calendar-in-range-disabled-label-color | — | |
--tanrei-comp-calendar-in-range-disabled-outline-color | — | |
--tanrei-comp-calendar-in-range-disabled-state-layer-color | — | |
--tanrei-comp-calendar-in-range-focused-container-color | — | |
--tanrei-comp-calendar-in-range-focused-label-color | — | |
--tanrei-comp-calendar-in-range-focused-outline-color | — | |
--tanrei-comp-calendar-in-range-focused-state-layer-color | — | |
--tanrei-comp-calendar-in-range-hovered-container-color | — | |
--tanrei-comp-calendar-in-range-hovered-label-color | — | |
--tanrei-comp-calendar-in-range-hovered-outline-color | — | |
--tanrei-comp-calendar-in-range-hovered-state-layer-color | — | |
--tanrei-comp-calendar-in-range-label-color | sys.color.on-surface | |
--tanrei-comp-calendar-in-range-outline-color | — | |
--tanrei-comp-calendar-in-range-pressed-container-color | — | |
--tanrei-comp-calendar-in-range-pressed-label-color | — | |
--tanrei-comp-calendar-in-range-pressed-outline-color | — | |
--tanrei-comp-calendar-in-range-pressed-state-layer-color | — | |
--tanrei-comp-calendar-in-range-state-layer-color | comp.calendar.in-range.label.color | |
--tanrei-comp-calendar-motion-feedback-duration | sys.motion.role.feedback | 450ms |
--tanrei-comp-calendar-motion-page-duration | sys.motion.duration.medium1 | 250ms |
--tanrei-comp-calendar-motion-page-easing | sys.motion.easing.standard | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-calendar-motion-state-duration | sys.motion.role.state.compact | 200ms |
--tanrei-comp-calendar-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-calendar-navigation-axis | — | 0 |
--tanrei-comp-calendar-navigation-gap | sys.space.space100 | 8px |
--tanrei-comp-calendar-outline-width | sys.border.hairline | 1px |
--tanrei-comp-calendar-pressed-opacity | — | 1 |
--tanrei-comp-calendar-selected-container-color | sys.color.primary | |
--tanrei-comp-calendar-selected-disabled-container-color | — | |
--tanrei-comp-calendar-selected-disabled-label-color | — | |
--tanrei-comp-calendar-selected-disabled-outline-color | — | |
--tanrei-comp-calendar-selected-disabled-state-layer-color | — | |
--tanrei-comp-calendar-selected-focused-container-color | — | |
--tanrei-comp-calendar-selected-focused-label-color | — | |
--tanrei-comp-calendar-selected-focused-outline-color | — | |
--tanrei-comp-calendar-selected-focused-state-layer-color | — | |
--tanrei-comp-calendar-selected-hovered-container-color | — | |
--tanrei-comp-calendar-selected-hovered-label-color | — | |
--tanrei-comp-calendar-selected-hovered-outline-color | — | |
--tanrei-comp-calendar-selected-hovered-state-layer-color | — | |
--tanrei-comp-calendar-selected-label-color | sys.color.on-primary | |
--tanrei-comp-calendar-selected-outline-color | — | |
--tanrei-comp-calendar-selected-pressed-container-color | — | |
--tanrei-comp-calendar-selected-pressed-label-color | — | |
--tanrei-comp-calendar-selected-pressed-outline-color | — | |
--tanrei-comp-calendar-selected-pressed-state-layer-color | — | |
--tanrei-comp-calendar-selected-state-layer-color | comp.calendar.selected.label.color | |
--tanrei-comp-calendar-today-container-color | — | |
--tanrei-comp-calendar-today-disabled-container-color | — | |
--tanrei-comp-calendar-today-disabled-label-color | — | |
--tanrei-comp-calendar-today-disabled-outline-color | — | |
--tanrei-comp-calendar-today-disabled-state-layer-color | — | |
--tanrei-comp-calendar-today-focused-container-color | — | |
--tanrei-comp-calendar-today-focused-label-color | — | |
--tanrei-comp-calendar-today-focused-outline-color | — | |
--tanrei-comp-calendar-today-focused-state-layer-color | — | |
--tanrei-comp-calendar-today-hovered-container-color | — | |
--tanrei-comp-calendar-today-hovered-label-color | — | |
--tanrei-comp-calendar-today-hovered-outline-color | — | |
--tanrei-comp-calendar-today-hovered-state-layer-color | — | |
--tanrei-comp-calendar-today-label-color | sys.color.primary | |
--tanrei-comp-calendar-today-outline-color | sys.color.primary | |
--tanrei-comp-calendar-today-pressed-container-color | — | |
--tanrei-comp-calendar-today-pressed-label-color | — | |
--tanrei-comp-calendar-today-pressed-outline-color | — | |
--tanrei-comp-calendar-today-pressed-state-layer-color | — | |
--tanrei-comp-calendar-today-state-layer-color | comp.calendar.today.label.color | |
--tanrei-comp-calendar-unselected-container-color | — | |
--tanrei-comp-calendar-unselected-disabled-container-color | — | |
--tanrei-comp-calendar-unselected-disabled-label-color | — | |
--tanrei-comp-calendar-unselected-disabled-outline-color | — | |
--tanrei-comp-calendar-unselected-disabled-state-layer-color | — | |
--tanrei-comp-calendar-unselected-focused-container-color | — | |
--tanrei-comp-calendar-unselected-focused-label-color | — | |
--tanrei-comp-calendar-unselected-focused-outline-color | — | |
--tanrei-comp-calendar-unselected-focused-state-layer-color | — | |
--tanrei-comp-calendar-unselected-hovered-container-color | — | |
--tanrei-comp-calendar-unselected-hovered-label-color | — | |
--tanrei-comp-calendar-unselected-hovered-outline-color | — | |
--tanrei-comp-calendar-unselected-hovered-state-layer-color | — | |
--tanrei-comp-calendar-unselected-label-color | sys.color.on-surface | |
--tanrei-comp-calendar-unselected-outline-color | — | |
--tanrei-comp-calendar-unselected-pressed-container-color | — | |
--tanrei-comp-calendar-unselected-pressed-label-color | — | |
--tanrei-comp-calendar-unselected-pressed-outline-color | — | |
--tanrei-comp-calendar-unselected-pressed-state-layer-color | — | |
--tanrei-comp-calendar-unselected-state-layer-color | comp.calendar.unselected.label.color | |
--tanrei-comp-calendar-view-toggle-container-height | — | 36px |
--tanrei-comp-calendar-view-toggle-container-padding-inline | sys.space.space150 | 12px |
--tanrei-comp-calendar-view-toggle-container-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-calendar-view-toggle-gap | sys.space.space50 | 4px |
--tanrei-comp-calendar-view-toggle-icon-size | sys.icon.small | 20px |
--tanrei-comp-calendar-weekday-color | sys.color.on-surface-variant | |
--tanrei-comp-calendar-year-container-height | — | 36px |
--tanrei-comp-calendar-year-container-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-calendar-year-container-width | — | 72px |
--tanrei-comp-calendar-day-label-typescale | sys.typescale.body | 400 14px/1.5714 |
--tanrei-comp-calendar-view-toggle-label-typescale | sys.typescale.label | 500 14px/1.4286 |
--tanrei-comp-calendar-weekday-typescale | sys.typescale.label-small | 500 12px/1.3333 |
--tanrei-comp-calendar-year-label-typescale | sys.typescale.body | 400 14px/1.5714 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
single
value: 2026-09-17 today: 2026-09-24 locale: en-US previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today empty
today: 2026-09-24 locale: en-US previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today range
mode: range start: 2026-09-10 end: 2026-09-16 today: 2026-09-24 locale: en-US startLabel: Start date endLabel: End date previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today range-start
mode: range start: 2026-09-10 today: 2026-09-24 locale: en-US startLabel: Start date endLabel: End date previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today monday
value: 2026-09-17 today: 2026-09-24 firstDayOfWeek: monday locale: en-US previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today ja
value: 2026-09-17 today: 2026-09-24 locale: ja-JP previousMonthLabel: 前の月 nextMonthLabel: 次の月 yearViewLabel: 年を選択 todayLabel: 今日 unavailable
value: 2026-09-12 min: 2026-09-03 max: 2026-09-22 disabledDates: ["2026-09-12"] today: 2026-09-24 locale: en-US previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today range-unavailable
mode: range start: 2026-09-10 end: 2026-09-16 disabledDates: ["2026-09-13"] today: 2026-09-24 locale: en-US startLabel: Start date endLabel: End date previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today adjacent-days
value: 2026-09-17 today: 2026-09-24 locale: en-US showAdjacentDays: true previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today adjacent-days-range
mode: range start: 2026-09-28 end: 2026-10-02 today: 2026-09-24 locale: en-US showAdjacentDays: true startLabel: Start date endLabel: End date previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today year
value: 2026-09-17 today: 2027-01-15 locale: en-US startView: year previousMonthLabel: Previous month nextMonthLabel: Next month yearViewLabel: Choose a year todayLabel: Today 生成自 specs/calendar.spec.json, tokens/comp/calendar.tokens.json, design/tokens/
当所选日期前后的日期也有意义时,使用日历:住宿、预订、报表的时间范围,或者星期几很重要的日期。如果选择日期是页面任务的一部分,就把日历放在页面里。如果日期只是表单中的一项,请使用 type 为 date 的文本字段,并从它的选择器按钮打开 DateTimePicker。已经知道日期的人,直接输入比在日历里找更快。
- 一个日期。 在
single模式下,在可选的日期上按下或按 Enter,会让它成为value并触发change。 - 一段范围。 在
range模式下,第一次选择的是开始日期,第二次选择的是结束日期。选择早于开始日期的一天,或者在两端都已确定后再选择,都会重新开始。每次选择后都会触发change,只选了开始日期时end为空,所以产品可以显示选到一半的范围。开始日期和结束日期是同一天时,就是一天的范围。 - 值是 ISO 8601 的日历日期(
2026-09-17),不含时间和时区。today默认是设备的当前日期;你可以按场地所在的时区设置它,或者为测试设置它。
一段范围。先选开始日期,再选结束日期;第三次选择会重新开始
范围是从开始日期到结束日期的一条色带。它在一周的行尾断开,在下一行继续。
不可选的日期由 min、max、disabledDates 以及平台的 isDateDisabled 判定函数决定;后者用于列表无法表达的规则,例如每个周末。这些日期画得更淡,不能被选择,但仍然可以用方向键到达,这样用户能知道这个日期是存在的。“上一个”和“下一个”按钮在 min 和 max 所在的月份停下,滚动和拖动也是如此。
跨过不可选日期的范围会把它包含在内;如果你的产品不能接受这样的范围,请在 change 之后告知用户。
9 月 7 日至 10 月 20 日,其中 9 月 21 日和 22 日已被占用
导航行的起始侧是视图切换按钮(“2026年9月”和一个 V 形箭头),末尾侧是“上一个”和“下一个”按钮。有四种方式可以移动月份,它们移动的是同一样东西,所以任何一种都可以中途接替另一种,画面不会跳动:
- 按钮和按键(Page Up 和 Page Down,或者移出当前月份的方向键)以一段短促的动效完成切换。
- 用触控板或滚轮在日期上滚动,日期会随滚动量移动;只要滚动不停,就一个月接一个月地移过去。滚动停止时,日历会停在一个月份上:日期已经向相邻月份移动了四分之一,就停在相邻月份,否则回到原来的月份。按整步报告的滚轮,每一步移动一个月。
- 用手指、触控笔或鼠标拖动日期,效果相同。松开时按同样的四分之一规则停下;如果是快速滑动,则停在滑动所朝的那一侧。没有移动的按下,仍然选择它下面的那一天。
在默认系统中,月份沿垂直轴排列,是一列不间断的周。一个月是这一列上六行高的一个窗口。两个月共有的一周是同一行,它从自己在一个月里的位置移到在另一个月里的位置;进入视野的日期淡入;随着周的移动,相邻月份的日期会变成本月的日期。日历从不绘制两个分开的月份网格,无论显示哪个月,它的高度都不变。
在减弱动态效果时,按钮或按键会立即切换月份。日期仍然跟随滚动和拖动,因为那是用户自己的手在移动它们;操作结束时,日期立即就位。
轴是产品级的设置 comp.calendar.navigation.axis,而不是属性,所以一个产品里的每个日历都以相同的方式移动:
| 轴 | 按钮 | 月份 | 滚动和拖动 |
|---|---|---|---|
vertical(默认) |
向上和向下的箭头 | 一列连续的周 | 垂直方向的移动日期;水平方向的交给页面 |
horizontal |
向后和向前的箭头 | 并排的两页,较晚的月份靠末尾侧 | 水平方向的移动日期;垂直方向的交给页面 |
按键从不随轴变化。
水平轴:月份是一页一页的
m3-like 显示指针位于日期上方时,沿轴方向的滚动归日历所有;在第一个月和最后一个月,日期停住不动时也是如此。在以相同方向滚动的页面上,从这里开始的滚动不会滚动页面:在长页面上,请在日历旁边或周围留出空间,让滚动能够到达页面。星期行、导航行和日历周围的一切,都照常滚动页面。
视图切换按钮会切换到年份视图,这是一个可滚动的列表,从 min 的年份到 max 的年份(没有设置时为 1900 到 2100)。选择一个年份会显示该年的同一个月,不改变任何已选内容。startView 设为 year 时,日历会先打开年份视图,适合生日这类离今天很远的日期。
为选择生日,先打开年份视图
showAdjacentDays 用相邻月份的日期填满网格中空余的单元格,并把它们画得更淡。这些日期只要可选就能被选择,范围也可以在其中一个上开始或结束。用指针选择这样的日期时,日历先选中它,再显示它所在的月份,在那里它是本月的日期。不设置这个属性时,这些单元格是空的,不响应按下。
显示相邻月份的日期:选择 10 月 1 日会显示 10 月
星期名称、月份标题和数字都取自区域设置(locale,或平台的区域设置)。firstDayOfWeek 也取自区域设置,除非你指定了星期几。在任何区域设置下,日历都是公历。不属于区域数据的文字——导航按钮的名称、视图切换按钮的描述、范围的开始和结束——是你的:Tanrei 不自带任何文字。可以把它们写在日历上,也可以像本页这样,按语言为整个产品只给一次:
import { setWords } from '@tanrei/web';
setWords(
{
previousMonthLabel: '上个月',
nextMonthLabel: '下个月',
yearViewLabel: '选择年份',
todayLabel: '今天',
startLabel: '开始日期',
endLabel: '结束日期',
},
{ lang: 'zh-CN' },
);这段调用就是本页实际运行的那一段:页面上的每个日历都由这些文字命名,示例里一个也没有写。某个日历的说法不同时,由它自己给出——下面的日语日历用属性给出了它的四个文字,而属性总是优先。一个都不给时,导航按钮仍然可以使用,但没有名称,调试构建会逐一报告缺少的文字。只有表示今天的那个词,在你没有给出时取自浏览器的区域数据。
从星期一开始的一周,以及日语区域设置
- 页面上有不止一个日历,或者附近没有任何内容说明这个日期的用途时,给日历设置
label(“入住日期”)。 - 让
min和max只包含确实可以选择的日期,这样导航会在可选范围的尽头停下。 - 对于用户已经知道的日期,在日历旁边提供文本字段,或者用文本字段代替日历。
- 让按钮始终触手可及:滚动和拖动是捷径,并不是每个人都能使用。
- 不要按屏幕分别设置轴。它是整个产品的一项决定。
- 不要隐藏不可选的日期。把它们显示为不可选,日历的网格才保持完整。
- 不要把垂直轴的日历贴着两侧边缘放在垂直滚动的页面里。从它的日期上开始的滚动永远到不了页面。
- 不要把月份进入视野当作一次选择。滚动和拖动改变的是显示的内容,从不改变
value,也不触发任何事件。
- 角色
group- 标注
- The calendar is a group named by label when it has one. The grid is named by the month and year it shows and its column headers are the weekdays' full names. Each day is named by its full date in the locale, with todayLabel after today's where the platform has no current-date property, and exposes its selected and disabled states; a range's start and end are described by startLabel and endLabel. The view toggle is named by its visible month and year, described by yearViewLabel, and exposes whether the year view is expanded. The navigation buttons are named by previousMonthLabel and nextMonthLabel.
- 最小目标
- 即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| Tab / Shift+Tab | Moves through the view toggle, the previous and next buttons and the active day (or the displayed year). |
| Arrow keys | Move the active day by one day (Left and Right, in the direction the arrow points) or one week (Up and Down), changing the month as needed, whatever axis the months move along; in the year view, by one year or one row. |
| Home / End | The first or last day of the week row; the first or last year. |
| Page Up / Page Down | The same day of the previous or next month. |
| Shift+Page Up / Shift+Page Down | The same day of the previous or next year. |
| Enter / Space | Choose the active day or the focused year; activate the focused button. |
备注
- The cells are the days' hit areas and are never smaller than the minimum target (CAL-1); the view toggle extends its hit area (CAL-6). The navigation buttons are Tanrei Buttons, which meet the target themselves (BTN-5).
- Unavailable days stay reachable by the arrow keys, as the WAI-ARIA date picker dialog pattern recommends, so a screen-reader user learns that a date exists and cannot be chosen.
- Scrolling and dragging to move the month are pointer and touch shortcuts, with a finger, a pen, a mouse, a wheel or a trackpad. The buttons and Page Up / Page Down do the same, so nothing depends on a gesture, and a drag is never the only way (WCAG 2.5.7). While a drag lasts nothing the calendar exposes changes; while a scroll goes on through several months the title follows each month it reaches, but the month is announced once, when the scroll or the release has chosen it (CAL-31, CAL-32, CAL-34), so a screen reader is not read every month that passes.
- A scroll along the calendar's axis is the calendar's while the pointer is over the day grid (CAL-33): a page that scrolls the same way does not scroll from there. The weekday row, the navigation row and everything around the calendar scroll the page as usual, and a calendar with a horizontal axis leaves the page's vertical scroll alone.
- A day of a neighbouring month that is chosen by pointer brings its month into view (CAL-15), announced as any month change is. The keys never need it: the arrow keys show a day's month before Enter can choose it (CAL-11).
- Typing a date is the Text field's job, not the calendar's: a product that wants both puts a Text field beside it, or uses a DateTimePicker opened from the field's trailing button. Typing is the fastest route for many people and the most direct one for screen reader users.
- The calendar has no words of its own. Month and weekday names, digits and date forms are the platform's locale data, in the locale the calendar was given. Every other word it shows or announces is the product's: a prop of this instance, or the product's words set once.
- The web exposes the grid with role grid, rows, column headers and gridcells with aria-selected and aria-disabled, and today with aria-current="date" (so todayLabel is not added there). The platforms without a grid role expose the rows and columns through their collection semantics where they have them (Compose's collection info), and otherwise each day as a button in reading order.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.
生成自 specs/calendar.spec.json, tokens/sys/measure.tokens.json
- 日历是一个以
label命名的组。网格以它显示的月份命名,列标题是星期的全称,每一天以完整的日期命名,并暴露它是否选中、是否不可选、是否是今天。范围的两端由startLabel和endLabel描述。 - Tab 顺序中只有一天是活动日期。方向键把它移动一天或一周,Home 和 End 移到一周的两端,Page Up 和 Page Down 移动一个月,加上 Shift 则移动一年,并在需要时切换月份。Enter 或 Space 选择它。
- 视图切换按钮是一个按钮,以它显示的月份和年份命名,由
yearViewLabel描述,并表明年份视图是否打开。年份列表也是一个 Tab 停靠点:方向键移动一年或一行,Home 和 End 移到列表两端。 - 月份变化时,会以礼貌(polite)的方式朗读一次。滚动经过多个月份时,标题跟随每一个月份,但只朗读滚动结束时所在的月份;回到原处的滚动或拖动不朗读任何内容。
- 拖动把日期停在两个月之间时,日历暴露的内容没有任何变化:标题、活动日期和焦点保持不动;进入视野的日期在它们的月份成为当前显示的月份之前,不接受焦点,也不响应按下。
- 滚动和拖动是捷径:按钮以及 Page Up 和 Page Down 能做同样的事,所以没有任何操作依赖手势。
- 每个单元格都不小于最小目标,文本放大后依然如此。在比七个目标更窄的空间里,单元格等比例变窄,数字保持原来的大小,所以日历永远不会比分配给它的空间更宽。导航按钮是 Tanrei 的按钮。
- 从右到左的布局会镜像网格、导航、水平方向的箭头,以及下一页所在的一侧。周的垂直滚动不镜像,拖动时日期始终朝指针移动的方向移动。
已知的平台差距:
- SwiftUI、Compose、Flutter 和 React Native 尚未实现日历:各平台跟上时会按这份规格来实现。
- Web:年份的选中状态是它的按钮上的
aria-current,因为按钮本身没有选中状态。 - Web:浏览器既不报告滚动的结束,也不报告滚动的惯性,所以日历根据短暂的静默或减速来判断滚动结束。如果浏览器对某个滚轮鼠标每次只报告几个像素,慢慢转动它时,每转过一格,日历都会回到原来的月份。
- Web:拖动尚未在真机上用手指检查过,并且有一些测试在 Firefox 和 WebKit 中失败。
- 设置
label,并给出日历使用的文字(previousMonthLabel、nextMonthLabel、yearViewLabel、todayLabel、startLabel、endLabel):写在日历上,或者用setWords()为整个产品只给一次。Tanrei 不自带任何文字。没有这些文字时,导航按钮可以使用,但没有名称,范围的两端没有描述,每一处都会在开发时报告;只有表示今天的那个词取自浏览器的区域数据。参见产品的文字。 - 提供输入日期的方式,例如
type为date的文本字段。对许多人来说,这是最直接的途径。 - 如果你的产品不能接受跨过不可选日期的范围,请在
change之后告知用户。