Skip to content

feat(DateInput, DateTimeInput): type dates in the active calendar - #9643

Open
nynexman4464 wants to merge 8 commits into
nynexman4464/feat/calendar-date-parserfrom
nynexman4464/feat/calendar-typed-entry
Open

nynexman4464 wants to merge 8 commits into
nynexman4464/feat/calendar-date-parserfrom
nynexman4464/feat/calendar-typed-entry

Conversation

@nynexman4464

@nynexman4464 nynexman4464 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Base is #9792's branch. Only the typed-entry commit is in this diff. It retargets down the stack as the PRs below it land.

This implements spec:AST-111 (draft, #9605) for the text fields of DateInput and DateTimeInput: FR9–FR14, AR2, AR3 and FR3, plus their calendar prop. Fixes #8958.

User need

People who use a calendar other than the Gregorian calendar can't type dates in it. The field reads only English month names and ASCII digits, and its committed text is Gregorian.

Gregorian fields also report wrong dates while someone is typing (#8958). A caller that saves or fetches on onChange therefore acts on partial input.

Problem and solution fit

  • useTypedDate reads the field's text with the calendar-date parser from the PR below, for every calendar, Gregorian included.
  • The fields:
    • take calendar and otherwise follow the provider's calendar;
    • fire onChange while typing only for complete input, once per date (FR12);
    • keep a date the calendar does not have, or an out-of-range date, in the field with its error (FR11);
    • announce a different message for each kind of rejection (AR3);
    • show an example date in the empty focused field, unless the caller passes placeholder. The example is today when the field accepts it, else the nearest date it accepts (the shared nearestAcceptedDate helper from feat(core): read typed dates in every calendar #9792). The description names the calendar only when it differs from the provider's calendar; gregory and iso8601 count as one (AR2);
    • write committed text, and the date, date_long and date_weekday formats, in the calendar (FR13); system_date stays ISO;
    • open their popover Calendar in the field's calendar.
  • Gregorian compatibility: text that only parseDateInput reads still commits on Enter or blur. So does text the new parser reads as a date that doesn't exist when parseDateInput reads it another way: 13/5/2026 in en-US commits as 13 May 2026, as it does on the latest release. parseDateInput itself is unchanged.
  • One-digit years are not dates: 1/5/2 stays in the field on Enter or blur with the unreadable message, as nonexistent and out-of-range text do, and never commits.
  • Supporting changes: tests, the English catalog strings for the new messages, docs, a story, the records and the API report.

Current specification

  • spec:AST-111 FR3, FR9–FR14, AR2 and AR3 (draft, spec(AST-111): date components use the calendar the application chooses #9605).
  • This PR adds FR16/FR10, AR9/AR5 and the spec:AST-013 FR12 exception to component:DateInput and component:DateTimeInput.
  • The provider and Calendar records now state that DateInput and DateTimeInput follow the provider calendar, while DateRangeInput stays Gregorian.

Public delta and caller burden

  • New optional prop on both components: calendar?: CalendarSystem. It defaults to the provider's calendar, else 'gregory'. Values stay Gregorian ISO.
  • Gregorian typing changes:
    • onChange fires only for complete input, and once per date.
    • A two-digit year, or no year, commits on Enter or blur.
    • An out-of-range typed date stays in the field with an error naming the range, instead of reverting.
  • Example: before, <DateInput label="Start" value={d} onChange={setD} />. After, for Persian, <DateInput label="Start" calendar="persian" value={d} onChange={setD} />.
  • Buddhist and ROC: builders using these calendars should set min and max.
  • Not yet: the bottom sheet and the native picker still show Gregorian dates; the next PRs in the stack change that.

Design and alternatives

  • The fields follow spec:AST-111 DEC-4 (one parser for every calendar, years read literally) and DEC-1 (values stay Gregorian ISO). DEC-4 rejects segmented fields.

Evidence

  • Real browsers: Chromium 149 and Firefox 151, each with native Temporal, with Temporal removed and the polyfill loaded, and with no Temporal, against the Storybook story Core/DateInput/CalendarSystems. All checks pass:
    • Persian digits commit.
    • 30 Esfand 1404 is rejected and stays in the field.
    • A plain "Adar" in 5784 commits as Adar II.
    • Reiwa 1 commits, while Heisei 31-05-01 is rejected.
    • A Gregorian year in a Buddhist field shows the Buddhist bounds.
    • Without Temporal, the Persian field falls back to Gregorian with a warning.
  • Unchanged path: the existing DateInput, DateTimeInput, DateRangeInput, Calendar, i18n and utils suites pass. Only the assertions listed below changed.

Changed assertions:

  • Enter and IME commit tests (DateInput and DateTimeInput) now type a two-digit year, because a complete date already fires while typing and fires only once (FR12).
  • "keeps a typed date that violates constraints and shows the bounds on blur" replaces the revert-on-blur test (FR11).
  • feat(Calendar): show the months of the selected calendar system #9622's calendar test: DateInput and DateTimeInput open their popover in the field's calendar. DateRangeInput stays Gregorian.

Screenshots

Chromium 149 with native Temporal unless noted.

State Screenshot
Persian field: format hint and calendar name Persian field: format hint and calendar name
Persian digits typed and committed Persian digits typed and committed
30 Esfand 1404 does not exist: rejected and kept 30 Esfand 1404 does not exist: rejected and kept
A plain "Adar" in 5784 commits Adar II <img src="https://raw.githubusercontent.com/nynexman4464/astryx/44eaee20eaeccdc1d23ef0c6530f873578306338/9643/chromium-04-hebrew-adar.png" alt="A plain "Adar" in 5784 commits Adar II" width="520">
Adar I in a common year is rejected Adar I in a common year is rejected
Reiwa 1 commits Reiwa 1 commits
Heisei 31-05-01 is outside its era: rejected Heisei 31-05-01 is outside its era: rejected
A Gregorian year in a Buddhist field shows the Buddhist bounds A Gregorian year in a Buddhist field shows the Buddhist bounds
Buddhist, th-TH digits Buddhist, th-TH digits
WebKit (Safari 26.5) with the polyfill: Persian committed WebKit (Safari 26.5) with the polyfill: Persian committed

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Not included: the parser (the PR below), bottom-sheet, native-picker and DateRangeInput calendars (FR15–FR17), and shared formatters (FR18).

Testing

  • pnpm exec vitest run packages/core/src/DateInput packages/core/src/DateTimeInput packages/core/src/DateRangeInput packages/core/src/Calendar packages/core/src/i18n packages/core/src/utils packages/core/src/docPropLiterals.test.ts: 88 files, 1940 tests passed.
  • pnpm exec tsc --noEmit -p packages/core: clean apart from the existing missing generated locales/pseudo.json.
  • pnpm check:knowledge, pnpm check:changesets: pass.
  • pnpm review:pre --base nynexman4464/feat/calendar-date-parser: 1,641 authored lines, no findings.
  • Playwright against Storybook in Chromium and Firefox, with native Temporal, with the polyfill, and with no Temporal.

Writing rubric results

Records: component:DateInput, component:DateTimeInput, component:InternationalizationProvider and component:Calendar.

  • R1 narration: none. The compatibility lines state the change classes in the present tense, and the provider and Calendar lines state which components follow the calendar.
  • R2 research: none in the records. The round-trip matrix lives in tests.
  • R3 evidence: the new verification cells cite test names and real-browser runs. No pull request or commit is cited.
  • R4 rejected alternatives: none added.
  • R5 change references: none added. One existing test title, quoted verbatim, contains an issue number.
  • R6 open questions: none added.

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 4, 2026 12:29am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 2, 2026
@astracat-bot

astracat-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 4, 2026, 12:51 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved. The new calendar prop and calendar typing for DateInput and DateTimeInput implement spec:AST-111, which is not on main, where the internationalization architecture's INV10 still keeps date parsing Gregorian. An engineering owner must approve that direction, along with the new DateInput FR16/AR9, DateTimeInput FR10/AR5 and Temporal exceptions: land AST-111 and the INV10 rewrite first, or comment /approve-spec d1ab11e46fec54626a2a133b92747e6b19faab1a here (the author may do this as an engineering owner). Whatever the decision, these need fixing:

  • Incomplete text such as Nov or 11/2/202 is dropped on Enter or blur with no alert; announce it as the unreadable rejection.
  • Nonexistent and out-of-range errors are announced twice, by the field's own alert and by the field status.
  • DateTimeInput repeats DateInput's new rejection, focus and commit logic; move it into useTypedDate.
  • Two existing cases in no-raw-intl-locale.test.mjs are overwritten by the new keys; close each object before the new case.
  • calendar-systems-core.md says both fields pick dates in the calendar, but the bottom-sheet and native surfaces stay Gregorian.
  • At about 3,600 authored lines, the change should be split into the parser, the calendar wiring and the Gregorian typing fix.

[Automated review]

Comment on lines +84 to +90
if (
(result.kind === 'nonexistent' || result.kind === 'outOfRange') &&
(result.isComplete || isCommitted)
) {
return result.kind;
}
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An incomplete result never becomes an error here, and the commit paths then revert it silently. Nov, 12/0 or 11/2/202 plus Enter or blur drops the text with no alert or aria-invalid; the base announced "Invalid date" for Nov and 12/0. Treat incomplete as unreadable when isCommitted is true.

*/}
<VisuallyHidden as="div" role="alert" aria-live="assertive">
{!isInputValid ? t('@astryx.dateInput.invalidDate') : ''}
{typedErrorMessage ?? ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the rejection is also shown as fieldStatus, FieldStatus already announces it through useAnnounce, so the nonexistent and out-of-range messages are announced twice (two role="alert" regions with the same text in Chromium). Keep only unreadable messages in this region.

: null;
// A readable date that cannot commit stays in the field, so its reason is
// shown as well as announced.
const fieldStatus: InputStatus | undefined =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block, the focus/hint state and the complete-only change and kept-rejection commit repeat DateInput.tsx line for line. Moving them into useTypedDate gives both fields one path, so a fix such as the incomplete-text announcement lands in both.

{
code: `new Intl.NumberFormat(locale, {useGrouping: false}).format(day);`,
filename: CALENDAR_SYSTEM_INFRA_FILE,
code: `new Intl.DateTimeFormat(locale, {calendar, timeZone: 'UTC'}).formatToParts(ms);`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code/filename pair lands inside the previous object (no },{ before it), so it overwrites the calendar-system case new Intl.NumberFormat(locale, {useGrouping: false}).format(day);. The suite grows 107 to 111 for six new cases.

code: `const {NumberFormat} = Intl;`,
filename: CALENDAR_SYSTEM_INFRA_FILE,
errors: [rawIntlReference],
code: `new Intl.DateTimeFormat(undefined, {calendar: 'persian'});`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: this overwrites the invalid case const {NumberFormat} = Intl; for the calendar-system file. Close the object above before this case.

Comment thread .changeset/calendar-systems-core.md Outdated
---

[feat] Calendar shows dates in other calendar systems. `InternationalizationProvider` and `Calendar` take a `calendar` prop (`CalendarSystem`, exported from `@astryxdesign/core/i18n`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`; a `-u-ca-` keyword in the provider locale, such as `th-TH-u-ca-buddhist`, selects one too. A locale alone never changes the calendar, so `fa-IR` keeps Gregorian dates until you set `calendar="persian"`. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput`, `DateRangeInput`, and `DateTimeInput`, including their popover calendars, keep showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.
[feat] Calendar shows dates in other calendar systems. `InternationalizationProvider` and `Calendar` take a `calendar` prop (`CalendarSystem`, exported from `@astryxdesign/core/i18n`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`; a `-u-ca-` keyword in the provider locale, such as `th-TH-u-ca-buddhist`, selects one too. A locale alone never changes the calendar, so `fa-IR` keeps Gregorian dates until you set `calendar="persian"`. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput` and `DateTimeInput` read, write, and pick dates in the same calendar; `DateRangeInput`, including its popover calendar, keeps showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"DateInput and DateTimeInput read, write, and pick dates in the same calendar" holds only for the popover field; the bottom-sheet and native surfaces (the default on touch) stay Gregorian. Suggested: "... in the same calendar in their popover field; their bottom-sheet and native surfaces, and DateRangeInput, keep Gregorian dates."

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved. The new calendar prop and calendar-aware typing for DateInput and DateTimeInput follow draft spec:AST-111, which is not on main, while main's architecture:internationalization INV10 still keeps date parsing Gregorian. An engineering owner can settle the calendar prop, typed and committed text in the resolved calendar, and the Gregorian typing changes by landing AST-111 and the INV10 rewrite on main, or by commenting /approve-spec 90a16cdd9f970ce895235ae4fd8a75ef2a17d9e3 here; the author may do this as an engineering owner.

These need fixing either way:

  • Typing a day the month lacks in year-first text reports a different date first: 1404/12/30 in a Persian field fires onChange('2026-02-22') at 1404/12/3, and 2026-02-30 in sv-SE fires 2026-02-03 (calendarDateParser.ts:1115).
  • Incomplete text such as Nov, 12/0 or 11/2/202 now reverts on Enter or blur with no alert and no aria-invalid, which DateInput AR3 requires.
  • Each nonexistent or out-of-range error is announced twice, once by FieldStatus and once by the field's own live region.
  • DateTimeInput repeats DateInput's new typed-entry state and commit logic instead of keeping it in useTypedDate.
  • The new DateInput FR16 and DateTimeInput FR10 reuse IDs that main already gives to the Enter-and-form-submit requirement.
  • Two new lint-rule test cases are written into existing objects and overwrite two calendar-system cases (no-raw-intl-locale.test.mjs:154-159 and :310-317).
  • The base changeset says both fields "read, write, and pick dates in the same calendar", but their bottom-sheet and native surfaces stay Gregorian.
  • At 3654 authored lines, the parser, the calendar wiring and the Gregorian completeness fix should be split into separate pull requests.

[Automated review]

Comment on lines +1115 to +1116
const length = date == null ? 31 : daysInMonth(lex.calendar, date);
return value * 10 > length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single-digit day closes against this month's real length, so 1404/12/3 in a fa-IR Persian field (and 2026-02-3 in sv-SE) counts as complete and fires onChange for the 3rd before the person types the 0 of a 30th the month does not have. Comparing against the calendar's longest month (31) would leave 3 open until another digit, Enter or blur; a one-key-at-a-time test for 1404/12/30 would cover it.

| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. Implementation gap: a wheel that rests on a disabled row returns to its committed row instead of continuing to the nearest enabled row. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. Implementation gap: a shown wheel jumps to a value changed from outside. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR16 | The pointer field reads and writes typed dates in the resolved calendar (`spec:AST-111` FR2), and its Calendar shows that calendar. It reads the dates `Intl.DateTimeFormat` writes for the locale and calendar, in any Unicode decimal digits and with month and era names of the locale and English; in the Gregorian calendar, text that matches none of these forms still commits on Enter or blur when `parseDateInput` reads it (`spec:AST-111` FR9). A typed year is a year of the calendar (`spec:AST-111` FR10). It rejects a date the calendar does not have and never adjusts it; a typed date outside `min`, `max`, or `dateConstraints` shows and announces an out-of-range error that writes the bounds in the calendar, and Enter or blur keeps both kinds of rejected text so the person can correct it, while unreadable text reverts (`spec:AST-111` FR11). While a person types, `onChange` fires only for complete input, at most once for each distinct value; a two-digit year and input without a year commit on Enter or blur, in every calendar including Gregorian (`spec:AST-111` FR12). `date`, `date_long`, and `date_weekday` write the committed text in the calendar and read back as the same date; `system_date` stays Gregorian ISO and the field reads ISO input with it (`spec:AST-111` FR13). Text from a custom `format` function that the field cannot read back cannot be re-committed after an edit (`spec:AST-111` FR14). Values stay Gregorian ISO dates (`spec:AST-111` FR3). Implementation gap: the bottom-sheet and native surfaces show Gregorian dates for every calendar (`spec:AST-111` FR15, FR16). | `spec:AST-111` FR3, FR9–FR14; current source; parser, round-trip, and calendar typed-entry tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, DateInput FR16 already states the Enter-and-form-submit requirement (and DateTimeInput FR10 the same for its date segment). These rows, and the component:DateInput FR16 reference in Calendar.spec.md, need the next free IDs when this retargets to main.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved. The new calendar prop and calendar-aware typing for DateInput and DateTimeInput follow draft spec:AST-111, which is not on main, while main's architecture:internationalization INV10 still keeps date parsing Gregorian. An engineering owner can settle the calendar prop, typed and committed text in the resolved calendar, and the Gregorian typing changes by landing AST-111 and the INV10 rewrite on main, or by commenting /approve-spec 79e1235d24cb159fe1b1f018e071b438914cd6e8 here; the author may do this as an engineering owner.

These need fixing either way:

  • Typing a day the month lacks in year-first text reports a different date first: 1404/12/30 in a Persian field fires onChange('2026-02-22') at 1404/12/3, and 2026-02-30 in sv-SE fires 2026-02-03, because the fields fire on the parser's completeness rule, which closes a single-digit day against the current month's length.
  • Incomplete text such as Nov, 12/0 or 11/2/202 now reverts on Enter or blur with no alert and no aria-invalid, which DateInput AR3 requires.
  • Each nonexistent or out-of-range error is announced twice, once by FieldStatus and once by the field's own live region.
  • DateTimeInput repeats DateInput's new typed-entry state and commit logic instead of keeping it in useTypedDate.
  • The rewritten calendar-systems-core.md sentence and the provider docs say both fields "read, write, and pick dates in the same calendar", but their bottom-sheet and native surfaces stay Gregorian.
  • The new DateInput FR16 and DateTimeInput FR10 reuse IDs that main already gives to the Enter-and-form-submit requirement.

[Automated review]

const parsedISO = plainDateToISO(parsed);
// Report a date only once more typing cannot change it.
const result = typedDate.parse(newValue);
if (result.kind !== 'value' || !result.isComplete) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fires as soon as the parser calls the text complete, and #9792's parser closes a single-digit day against the current month's length (calendarDateParser.ts:1114-1116). So 1404/12/3 in a fa-IR Persian field (and 2026-02-3 in sv-SE) fires onChange for the 3rd before the person types the 0 of a 30th the month does not have. Comparing against the calendar's longest month (31) leaves 3 open until another digit, Enter or blur; a one-key-at-a-time test for 1404/12/30 here would cover it.

) {
return result.kind;
}
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An incomplete result never becomes an error here, and both commit paths then revert it silently: Nov, 12/0 or 11/2/202 plus Enter or blur drops the text with no alert or aria-invalid, where the base announced "Invalid date" for Nov. Treat incomplete as unreadable when isCommitted is true.

*/}
<VisuallyHidden as="div" role="alert" aria-live="assertive">
{!isInputValid ? t('@astryx.dateInput.invalidDate') : ''}
{typedErrorMessage ?? ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the rejection is also shown as fieldStatus, FieldStatus already announces it through useAnnounce, so nonexistent and out-of-range messages are spoken twice (two role="alert" regions with the same text in Chromium). Keep only unreadable messages in this region; DateTimeInput.tsx:1888 has the same pair.

: null;
// A readable date that cannot commit stays in the field, so its reason is
// shown as well as announced.
const fieldStatus: InputStatus | undefined =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block, the pending, focus and hint state, and the complete-only change and kept-rejection commit repeat DateInput.tsx line for line. Moving them into useTypedDate gives both fields one path, so a fix such as the incomplete-text announcement lands in both.

Comment thread .changeset/calendar-systems-core.md Outdated
---

[feat] Calendar shows dates in other calendar systems. `Calendar` takes a `calendar` prop (`CalendarSystem`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`, and otherwise shows the `InternationalizationProvider` calendar. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput`, `DateRangeInput`, and `DateTimeInput`, including their popover calendars, keep showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.
[feat] Calendar shows dates in other calendar systems. `Calendar` takes a `calendar` prop (`CalendarSystem`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`, and otherwise shows the `InternationalizationProvider` calendar. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput` and `DateTimeInput` read, write, and pick dates in the same calendar; `DateRangeInput`, including its popover calendar, keeps showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"DateInput and DateTimeInput read, write, and pick dates in the same calendar" holds only for the popover field; the bottom-sheet and native surfaces, the default on touch, stay Gregorian. Suggested: "… in the same calendar in their popover field; their bottom-sheet and native surfaces, and DateRangeInput, keep Gregorian dates."

| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. Implementation gap: a wheel that rests on a disabled row returns to its committed row instead of continuing to the nearest enabled row. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. Implementation gap: a shown wheel jumps to a value changed from outside. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR16 | The pointer field reads and writes typed dates in the resolved calendar (`spec:AST-111` FR2), and its Calendar shows that calendar. It reads the dates `Intl.DateTimeFormat` writes for the locale and calendar, in any Unicode decimal digits and with month and era names of the locale and English; in the Gregorian calendar, text that matches none of these forms still commits on Enter or blur when `parseDateInput` reads it (`spec:AST-111` FR9). A typed year is a year of the calendar (`spec:AST-111` FR10). It rejects a date the calendar does not have and never adjusts it; a typed date outside `min`, `max`, or `dateConstraints` shows and announces an out-of-range error that writes the bounds in the calendar, and Enter or blur keeps both kinds of rejected text so the person can correct it, while unreadable text reverts (`spec:AST-111` FR11). While a person types, `onChange` fires only for complete input, at most once for each distinct value; a two-digit year and input without a year commit on Enter or blur, in every calendar including Gregorian (`spec:AST-111` FR12). `date`, `date_long`, and `date_weekday` write the committed text in the calendar and read back as the same date; `system_date` stays Gregorian ISO and the field reads ISO input with it (`spec:AST-111` FR13). Text from a custom `format` function that the field cannot read back cannot be re-committed after an edit (`spec:AST-111` FR14). Values stay Gregorian ISO dates (`spec:AST-111` FR3). Implementation gap: the bottom-sheet and native surfaces show Gregorian dates for every calendar (`spec:AST-111` FR15, FR16). | `spec:AST-111` FR3, FR9–FR14; current source; parser, round-trip, and calendar typed-entry tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, DateInput FR16 already states the Enter-and-form-submit requirement (and DateTimeInput FR10 the same for its date segment). This row, the DateTimeInput FR10 row and the component:DateInput FR16 reference in Calendar.spec.md need the next free IDs when this rebases onto main.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved. The new calendar prop and calendar-aware typing for DateInput and DateTimeInput follow draft spec:AST-111, which is not on main, while main's architecture:internationalization INV10 still keeps date parsing Gregorian. An engineering owner can settle the calendar prop, typed and committed text in the resolved calendar, and the Gregorian typing changes by landing AST-111 and the INV10 rewrite on main, or by commenting /approve-spec d3fbd6269c7df93b9ae2d4dc0e6ae7e40f11c2fd here; the author may do this as an engineering owner.

These need fixing either way:

  • Typing a day the month lacks in year-first text reports a different date first: 1404/12/30 in a Persian field fires onChange('2026-02-22') at 1404/12/3, and 2026-02-30 in sv-SE fires 2026-02-03, because the fields fire on the parser's completeness rule, which closes a single-digit day against the current month's length.
  • A controlled caller that keeps its value (it rejects the date, or a changeAction resolves without updating) never gets onChange again when the person retypes the same date, because lastFiredValueRef and lastFiredDateRef are cleared only when value changes.
  • Incomplete text such as Nov, 12/0 or 11/2/202 now reverts on Enter or blur with no alert and no aria-invalid, which DateInput AR3 requires.
  • Each nonexistent or out-of-range error is announced twice, once through the field's status and once by the field's own live region.
  • DateTimeInput repeats DateInput's new typed-entry state and commit logic instead of keeping it in useTypedDate.
  • The rewritten calendar-systems-core.md sentence and the provider docs say both fields "read, write, and pick dates in the same calendar", but their bottom-sheet and native surfaces stay Gregorian.
  • The new DateInput FR16 and DateTimeInput FR10 reuse IDs that main already gives to the Enter-and-form-submit requirement.

[Automated review]

const parsedISO = plainDateToISO(parsed);
// Report a date only once more typing cannot change it.
const result = typedDate.parse(newValue);
if (result.kind !== 'value' || !result.isComplete) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fires as soon as the parser calls the text complete, and the parser closes a single-digit day against the current month's length (calendarDateParser.ts:1114-1116). So 1404/12/3 in a fa-IR Persian field fires onChange('2026-02-22') before the final 0 makes the date one Esfand lacks, and 2026-02-3 in sv-SE fires 2026-02-03. Closing a single-digit day only when ten times it exceeds 31 avoids it.

return;
}
const parsedISO = plainDateToISO(result.date);
if (parsedISO !== value && parsedISO !== lastFiredValueRef.current) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastFiredValueRef is cleared only when value changes. If the caller keeps its value (it rejects the date, or a changeAction resolves without updating), a retyped 11/2/2026 plus Enter fires nothing and the field reverts; the base fires again. Clearing the ref when the pending text is committed or reverted keeps the once-per-date rule inside one typing session. DateTimeInput.tsx:1127 and :1160 have the same guard.

if (result == null) {
return null;
}
if (result.kind === 'unreadable') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An incomplete result never becomes an error here, and both commit paths then revert it silently: Nov, 12/0 or 11/2/202 plus Enter or blur drops the text with no alert or aria-invalid, where the base announced "Invalid date" for Nov and 12/0. Treating incomplete as unreadable when committed would keep DateInput AR3's assertive alert.

*/}
<VisuallyHidden as="div" role="alert" aria-live="assertive">
{!isInputValid ? t('@astryx.dateInput.invalidDate') : ''}
{typedErrorMessage ?? ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the rejection is also shown as fieldStatus, Field already announces it through useAnnounce, so nonexistent and out-of-range messages are spoken twice (two assertive regions with the same text in Chromium). Announcing only unreadable here avoids the repeat. DateTimeInput.tsx:1891 is the same.

: null;
// A readable date that cannot commit stays in the field, so its reason is
// shown as well as announced.
const fieldStatus: InputStatus | undefined =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block, the pending, focus and hint state, and the complete-only change and kept-rejection commit repeat DateInput.tsx line for line. Moving them into useTypedDate gives both fields one path, so a fix such as the two above lands once.

Comment thread .changeset/calendar-systems-core.md Outdated
---

[feat] Calendar shows dates in other calendar systems. `Calendar` takes a `calendar` prop (`CalendarSystem`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`, and otherwise shows the `InternationalizationProvider` calendar. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput`, `DateRangeInput`, and `DateTimeInput`, including their popover calendars, keep showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.
[feat] Calendar shows dates in other calendar systems. `Calendar` takes a `calendar` prop (`CalendarSystem`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`, and otherwise shows the `InternationalizationProvider` calendar. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput` and `DateTimeInput` read, write, and pick dates in the same calendar; `DateRangeInput`, including its popover calendar, keeps showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"DateInput and DateTimeInput read, write, and pick dates in the same calendar" holds only for the popover field; the bottom-sheet and native surfaces, the default on touch, stay Gregorian. Suggested: "… in the same calendar in their popover field; their bottom-sheet and native surfaces, and DateRangeInput, keep Gregorian dates." The provider docs and record repeat the same claim.

| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. Implementation gap: a wheel that rests on a disabled row returns to its committed row instead of continuing to the nearest enabled row. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. Implementation gap: a shown wheel jumps to a value changed from outside. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR16 | The pointer field reads and writes typed dates in the resolved calendar (`spec:AST-111` FR2), and its Calendar shows that calendar. It reads the dates `Intl.DateTimeFormat` writes for the locale and calendar, in any Unicode decimal digits and with month and era names of the locale and English; in the Gregorian calendar, text that matches none of these forms, or that these forms read as a date the calendar does not have, still commits on Enter or blur when `parseDateInput` reads it, so `13/5/2026` in `en-US` commits as 13 May 2026 (`spec:AST-111` FR9). A typed year is a year of the calendar (`spec:AST-111` FR10). It rejects a date the calendar does not have and never adjusts it; a typed date outside `min`, `max`, or `dateConstraints` shows and announces an out-of-range error that writes the bounds in the calendar, and Enter or blur keeps both kinds of rejected text so the person can correct it, while unreadable text reverts (`spec:AST-111` FR11). While a person types, `onChange` fires only for complete input, at most once for each distinct value; a two-digit year and input without a year commit on Enter or blur, in every calendar including Gregorian (`spec:AST-111` FR12). `date`, `date_long`, and `date_weekday` write the committed text in the calendar and read back as the same date; `system_date` stays Gregorian ISO and the field reads ISO input with it (`spec:AST-111` FR13). Text from a custom `format` function that the field cannot read back cannot be re-committed after an edit (`spec:AST-111` FR14). Values stay Gregorian ISO dates (`spec:AST-111` FR3). Implementation gap: the bottom-sheet and native surfaces show Gregorian dates for every calendar (`spec:AST-111` FR15, FR16). | `spec:AST-111` FR3, FR9–FR14; current source; parser, round-trip, and calendar typed-entry tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, DateInput FR16 already states the Enter-and-form-submit requirement (and DateTimeInput FR10 the same for its date segment). This row, the DateTimeInput FR10 row and the component:DateInput FR16 reference in Calendar.spec.md need the next free IDs when this rebases onto main.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved. The new calendar prop and calendar-aware typing for DateInput and DateTimeInput follow draft spec:AST-111, which is not on main, while main's architecture:internationalization INV10 still keeps date parsing Gregorian. An engineering owner can settle the calendar prop, typed and committed text in the resolved calendar, and the Gregorian typing changes by landing AST-111 and the INV10 rewrite on main, or by commenting /approve-spec 2187c77f73ac1b729ae526cc707eb95d28472cd4 here; the author may do this as an engineering owner.

These need fixing either way:

  • Typing a day the month lacks reports a different date first: 1404/12/30 in a Persian field fires onChange('2026-02-22') at 1404/12/3, and 2026-02-30 in sv-SE fires 2026-02-03, because the fields fire on the parser's completeness rule, which closes a single-digit day against the current month's length.
  • A controlled caller that keeps its value (it rejects the date, or a changeAction fails) never gets onChange again when the person retypes the same date, because lastFiredValueRef and lastFiredDateRef are cleared only when value changes.
  • Incomplete text such as 11/2/202 or Nov now stays in the field after Enter or blur with only a hidden "Invalid date" alert and no visible error, so sighted users see text that never committed (DateInput AR3).
  • Each nonexistent or out-of-range error is announced twice, once through the field's error status and once by the field's own alert region.
  • DateTimeInput repeats DateInput's new typed-entry state and commit logic instead of keeping it in useTypedDate.
  • The rewritten calendar-systems-core.md sentence and the provider docs say both fields "read, write, and pick dates in the same calendar", but their bottom-sheet and native surfaces stay Gregorian.
  • The new DateInput FR16 and DateTimeInput FR10 reuse IDs that main already gives to the Enter-and-form-submit requirement.

[Automated review]

const parsedISO = plainDateToISO(parsed);
// Report a date only once more typing cannot change it.
const result = typedDate.parse(newValue);
if (result.kind !== 'value' || !result.isComplete) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fires as soon as the parser calls the text complete, and the base parser closes a single-digit day against the current month's length. Typing 1404/12/30 one key at a time in a fa-IR Persian field fires onChange('2026-02-22') at 1404/12/3, and 2026-02-30 in sv-SE fires 2026-02-03. Closing a single-digit day only above the calendar's longest month (31) avoids it.

}
const parsedISO = plainDateToISO(result.date);
if (parsedISO !== value && parsedISO !== lastFiredValueRef.current) {
lastFiredValueRef.current = parsedISO;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastFiredValueRef is cleared only when value changes. If the caller keeps its value (it rejects the date, or a changeAction fails), retyping the same date and pressing Enter fires nothing. Clearing the ref whenever pending text is committed or reverted keeps the guard to one typing session. DateTimeInput.tsx:1129 has the same issue.

const fieldStatus: InputStatus | undefined =
status ??
(typedError === 'nonexistent' || typedError === 'outOfRange'
? {type: 'error', message: typedErrorMessage ?? undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete text is now kept on Enter or blur, but only nonexistent and outOfRange become the field status. 11/2/202 then Tab stays in the field with aria-invalid and a hidden "Invalid date" alert, and no visible error, while the value is unchanged. Showing the unreadable message here whenever rejected text is kept would match the other two kinds.

*/}
<VisuallyHidden as="div" role="alert" aria-live="assertive">
{!isInputValid ? t('@astryx.dateInput.invalidDate') : ''}
{typedErrorMessage ?? ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the rejection is also shown as fieldStatus, FieldStatus announces it through useAnnounce, so nonexistent and out-of-range messages are spoken twice (two assertive regions with the same text in Chromium). This region could carry only rejections that no field status shows.

: null;
// A readable date that cannot commit stays in the field, so its reason is
// shown as well as announced.
const fieldStatus: InputStatus | undefined =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block, the pending, rejection and focus state, the hint condition, and the complete-only change and kept-rejection commit repeat DateInput.tsx line for line. Moving them into useTypedDate gives each field one call, so a fix to one cannot miss the other.

Comment thread .changeset/calendar-systems-core.md Outdated
---

[feat] Calendar shows dates in other calendar systems. `Calendar` takes a `calendar` prop (`CalendarSystem`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`, and otherwise shows the `InternationalizationProvider` calendar. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput`, `DateRangeInput`, and `DateTimeInput`, including their popover calendars, keep showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.
[feat] Calendar shows dates in other calendar systems. `Calendar` takes a `calendar` prop (`CalendarSystem`), such as `'persian'`, `'hebrew'`, `'islamic-umalqura'`, or `'buddhist'`, and otherwise shows the `InternationalizationProvider` calendar. Values do not change: `value`, `min`, `max`, `focusDate`, and every callback stay Gregorian ISO dates. `DateInput` and `DateTimeInput` read, write, and pick dates in the same calendar; `DateRangeInput`, including its popover calendar, keeps showing Gregorian dates. In the selected calendar, the grid shows that calendar's months, including leap months such as Adar I and Adar II and the five- or six-day thirteenth month of the Ethiopic and Coptic calendars. Its heading, day names, and announcements use the calendar; Previous, Next, PageUp, and PageDown move one calendar month; and Shift+PageUp and Shift+PageDown move one calendar year to the same month. Day numbers use the locale's digits, and week numbers stay ISO 8601. The Gregorian-aligned calendars (`gregory`, `iso8601`, `buddhist`, `roc`, `japanese`) work everywhere. The others need `Temporal`; where the runtime lacks it, such as Safari or Node.js 24, load a polyfill like `temporal-polyfill/full/global` on the server and in the browser, or those calendars show Gregorian dates and warn once in development. A server-rendered app loads `Temporal` on the server too, since a server without it renders Gregorian markup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"DateInput and DateTimeInput read, write, and pick dates in the same calendar" holds only for the popover field; the bottom-sheet and native surfaces, the default on touch, stay Gregorian, as calendar-typed-entry.md says. The provider docs and record repeat the broader claim.

| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. Implementation gap: a wheel that rests on a disabled row returns to its committed row instead of continuing to the nearest enabled row. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. Implementation gap: a shown wheel jumps to a value changed from outside. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR16 | The pointer field reads and writes typed dates in the resolved calendar (`spec:AST-111` FR2), and its Calendar shows that calendar. It reads the dates `Intl.DateTimeFormat` writes for the locale and calendar, in any Unicode decimal digits and with month and era names of the locale and English; in the Gregorian calendar, text that matches none of these forms, or that these forms read as a date the calendar does not have, still commits on Enter or blur when `parseDateInput` reads it, so `13/5/2026` in `en-US` commits as 13 May 2026 (`spec:AST-111` FR9). A typed year is a year of the calendar (`spec:AST-111` FR10). It rejects a date the calendar does not have and never adjusts it; a typed date outside `min`, `max`, or `dateConstraints` shows and announces an out-of-range error that writes the bounds in the calendar, and Enter or blur keeps both kinds of rejected text so the person can correct it, while unreadable text reverts (`spec:AST-111` FR11). While a person types, `onChange` fires only for complete input, at most once for each distinct value; a two-digit year and input without a year commit on Enter or blur, in every calendar including Gregorian, and a one-digit year is not a date: like other text the field cannot finish, Enter or blur keeps it and announces the unreadable message (`spec:AST-111` FR12, AR3). `date`, `date_long`, and `date_weekday` write the committed text in the calendar and read back as the same date; `system_date` stays Gregorian ISO and the field reads ISO input with it (`spec:AST-111` FR13). Text from a custom `format` function that the field cannot read back cannot be re-committed after an edit (`spec:AST-111` FR14). Values stay Gregorian ISO dates (`spec:AST-111` FR3). Implementation gap: the bottom-sheet and native surfaces show Gregorian dates for every calendar (`spec:AST-111` FR15, FR16). | `spec:AST-111` FR3, FR9–FR14; current source; parser, round-trip, and calendar typed-entry tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, DateInput FR16 is the Enter-and-form-submit requirement, and DateTimeInput FR10 the same for its date segment. This row and the new DateTimeInput FR10 reuse those IDs, so they need new numbers after the rebase.

Both inputs take a calendar prop and otherwise follow the provider's calendar. useTypedDate reads typed text with the calendar-date parser, commits once per complete date, and names unreadable, missing, and out-of-range dates. The popover calendar and the date formats follow the calendar; values stay Gregorian ISO dates.
… fallback

- The accessible description names the calendar only when the field's
  calendar differs from the provider's calendar, after the Gregorian
  fallback, so an all-Persian app never repeats it and a Gregorian field
  in a Persian app names the Gregorian calendar.
- Gregorian text that the calendar parser reads as a nonexistent date
  falls back to parseDateInput on Enter or blur, so 13/5/2026 in en-US
  still commits as 13 May 2026. A date neither reads stays rejected.
- A caller placeholder replaces the format hint in every state.
- The hint's example date is today when the field accepts it, else the
  nearest accepted date, from a new nearestAcceptedDate helper.
…r the Enter requirement

DateInput FR16 and DateTimeInput FR10 already name the Enter-and-form-submit rule, so typed entry in a calendar is DateInput FR17 and DateTimeInput FR11.
… reading

Letter-free text that parseDateInput does not read in a day-first locale is not a date, so the Gregorian fallback leaves it unread.
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/calendar-date-parser branch from 3bfcb3f to 96920a3 Compare November 3, 2026 22:40
…retyped date again

Text kept on Enter or blur shows its error as the field status, and a status the field shows is not announced a second time by the field's own alert. A commit ends the typing session, so a date retyped after the caller keeps its value is reported again. The provider docs and record say that the popover field follows the calendar while the bottom-sheet and native surfaces stay Gregorian, and the records state that an explicit -u-ca- locale is the application's request.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved. The new calendar prop and calendar-aware typing for DateInput and DateTimeInput follow draft spec:AST-111, which is not on main, while main's architecture:internationalization INV10 still keeps date parsing Gregorian. An engineering owner can settle the calendar prop, typed and committed text in the resolved calendar, and the Gregorian typing changes (including two-digit years in day-first locales, which main's DateInput FR6 does not read as dates) by landing AST-111 and the INV10 rewrite on main, or by commenting /approve-spec b3b17f4b6fc5d08c9672954bb2e8a2720859f9c1 here; the author may do this as an engineering owner.

These need fixing either way:

  • apps/storybook/stories/DateInput.stories.tsx imports InternationalizationProvider twice (line 9 and lines 14–17), so Storybook fails to index the file and does not start.
  • When the caller sets status, text kept after a rejected entry shows only the caller's message, so the reason ("This date does not exist…" or the allowed range) is announced but never shown (DateInput AR3).
  • DateTimeInput repeats DateInput's new pending, rejection, status and commit logic instead of keeping it in useTypedDate, so each fix has to be made twice.
  • The new calendar requirement is numbered DateInput FR17, which main already uses for sheet-wheel keyboard behavior, so it needs a new number after the rebase.

The earlier issues with wrong dates while typing, retyped dates, kept incomplete text, double announcements and the changeset wording are fixed on this head.

[Automated review]

Comment on lines +14 to +17
import {
type CalendarSystem,
InternationalizationProvider,
} from '@astryxdesign/core/i18n';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 9 already imports InternationalizationProvider, so this second import makes the file fail to parse ("Identifier 'InternationalizationProvider' has already been declared"), and Storybook stops indexing and exits. Importing type CalendarSystem in the line 9 import and deleting this block fixes it.

(typedError !== 'unreadable' || isRejectionCommitted)
? {type: 'error', message: typedErrorMessage}
: undefined;
const fieldStatus = status ?? typedStatus;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a caller status, kept rejected text shows only the caller's message; "This date does not exist…" or the bounds are announced through the alert but never shown (Storybook WithWarningStatus, 2/30/2026 then Tab). Showing the typed rejection while its text is kept (for example typedStatus ?? status, or both messages) keeps the reason visible. DateTimeInput.tsx:811 has the same line.

});

// --- Date input state ---
const [datePendingInput, setDatePendingInput] = useState<string | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block, the kept-rejection, status and alert derivation, the hint condition, and the complete-only change and kept-rejection commit repeat DateInput.tsx line for line, and the copies already differ (only DateInput's alert checks inputGroup). Moving them into useTypedDate gives each field one call, so a fix such as the caller-status one lands once.

| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR16 | Enter in the pointer field's text input commits typed text that has not been committed and is then held back, so a form never submits the value from before the commit. With no typed text pending, Enter is not prevented, so a form submits as it does from any text field, except on a disabled or busy field, where Enter does nothing (`spec:AST-085` FR2). |
| FR17 | The pointer field reads and writes typed dates in the resolved calendar (`spec:AST-111` FR2), and its Calendar shows that calendar. It reads the dates `Intl.DateTimeFormat` writes for the locale and calendar, in any Unicode decimal digits and with month and era names of the locale and English; in the Gregorian calendar, text that matches none of these forms, or that these forms read as a date the calendar does not have, still commits on Enter or blur when `parseDateInput` reads it, so `13/5/2026` in `en-US` commits as 13 May 2026 (`spec:AST-111` FR9). A typed year is a year of the calendar (`spec:AST-111` FR10). It rejects a date the calendar does not have and never adjusts it; a typed date outside `min`, `max`, or `dateConstraints` shows and announces an out-of-range error that writes the bounds in the calendar, and Enter or blur keeps both kinds of rejected text so the person can correct it, while unreadable text reverts (`spec:AST-111` FR11). While a person types, `onChange` fires only for complete input, at most once for each distinct value in one typing session, and a one-digit day stays open until another digit, Enter, or blur, so a day the month lacks is never reported as another date; a two-digit year and input without a year commit on Enter or blur, in every calendar including Gregorian, and a one-digit year is not a date: like other text the field cannot finish, Enter or blur keeps it and shows the unreadable message as the field status (`spec:AST-111` FR12, AR3). Each rejection is announced once: a shown status announces itself, and the field's alert carries only a rejection that no status shows. `date`, `date_long`, and `date_weekday` write the committed text in the calendar and read back as the same date; `system_date` stays Gregorian ISO and the field reads ISO input with it (`spec:AST-111` FR13). Text from a custom `format` function that the field cannot read back cannot be re-committed after an edit (`spec:AST-111` FR14). Values stay Gregorian ISO dates (`spec:AST-111` FR3). Implementation gap: the bottom-sheet and native surfaces show Gregorian dates for every calendar (`spec:AST-111` FR15, FR16). | `spec:AST-111` FR3, FR9–FR14; current source; parser, round-trip, and calendar typed-entry tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, DateInput FR17 is the sheet-wheel Arrow/Page/Home/End requirement and FR18 the wheel's selection band. This row, the DateTimeInput FR11 reference and Calendar.spec.md:236 need the next free number after the rebase.

Live region announcing invalid typed input to assistive technology.
The value silently reverts on blur, so without this a screen-reader
user would get no feedback that their entry was rejected (WCAG 3.3.1).
Live region announcing rejected typed input to assistive technology,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (INV29): this region now carries only a rejection that no field status shows. Suggested: "Announces a typed rejection that no field status shows. A shown status announces itself." DateTimeInput.tsx:1903 has the same comment.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved. The new calendar prop and calendar-aware typing for DateInput and DateTimeInput follow draft spec:AST-111, which is not on main, while main's architecture:internationalization INV10 still keeps date parsing Gregorian and reserves non-Gregorian component semantics for a human decision.

To settle it, an engineering owner either lands spec:AST-111 and the INV10 rewrite on main, or approves this head's proposed records (the calendar prop, typed entry and committed text in the calendar, the example-date hint and calendar description, and the Gregorian typing changes) by commenting /approve-spec 97e625073f851e3953048349d90ee5d7af078af2; the author may do this as an engineering owner.

These fixes are needed either way:

  • When the caller sets status, kept rejected text shows only the caller's message and grey text (DateInput.tsx:643, DateTimeInput.tsx:811), so show the typed rejection while its text stays, since component:DateInput AR3 asks for more than colour.
  • DateTimeInput repeats DateInput's new typed-entry state and decisions, so move them into useTypedDate and let each field make one call.
  • The changeset says every out-of-range error names the allowed range, but a date that only dateConstraints rejects gets "This date is not available", so reword that line.
  • The new DateInput FR17 reuses an ID that main already gives the sheet-wheel keyboard requirement, so renumber it and its two citations when the stack rebases.

[Automated review]

(typedError !== 'unreadable' || isRejectionCommitted)
? {type: 'error', message: typedErrorMessage}
: undefined;
const fieldStatus = status ?? typedStatus;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a caller status, kept rejected text shows only the caller's message and is marked only by grey text; "This date does not exist…" or the bounds are announced but never shown (Storybook WithWarningStatus, 2/30/2026 then Tab). Showing the typed rejection while its text is kept (for example typedStatus ?? status, or both messages) meets AR3. DateTimeInput.tsx:811 has the same line.

});

// --- Date input state ---
const [datePendingInput, setDatePendingInput] = useState<string | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block, the status and alert derivation, the hint condition, and the complete-only change and kept-rejection commit repeat DateInput.tsx line for line. Moving them into useTypedDate gives each field one call, so a fix such as the caller-status one lands once.

Typing changes for Gregorian fields too:

- `onChange` fires while typing only once the date is complete, and once for each date, so partial input such as `12/0` or `12/01/202` no longer reports wrong dates. A two-digit year, such as `12/01/90`, or a date without a year commits on Enter or blur, and is read in the locale's field order. A one-digit year, such as `1/5/2`, is not a date: Enter or blur keeps it and shows it as invalid. Other text that `parseDateInput` reads still commits on Enter or blur, including a day-first date such as `13/5/2026` in `en-US`.
- A typed date outside `min`, `max`, or `dateConstraints` stays in the field with an error that names the allowed range, instead of reverting on blur. Unreadable text, a date that does not exist, and an out-of-range date each announce their own message, once.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A date that only a dateConstraints rule rejects gets "This date is not available", which names no range. Suggested: "… with an error that names the allowed range, or says the date is not available when a dateConstraints rule rejects it".

| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR16 | Enter in the pointer field's text input commits typed text that has not been committed and is then held back, so a form never submits the value from before the commit. With no typed text pending, Enter is not prevented, so a form submits as it does from any text field, except on a disabled or busy field, where Enter does nothing (`spec:AST-085` FR2). |
| FR17 | The pointer field reads and writes typed dates in the resolved calendar (`spec:AST-111` FR2), and its Calendar shows that calendar. It reads the dates `Intl.DateTimeFormat` writes for the locale and calendar, in any Unicode decimal digits and with month and era names of the locale and English; in the Gregorian calendar, text that matches none of these forms, or that these forms read as a date the calendar does not have, still commits on Enter or blur when `parseDateInput` reads it, so `13/5/2026` in `en-US` commits as 13 May 2026 (`spec:AST-111` FR9). A typed year is a year of the calendar (`spec:AST-111` FR10). It rejects a date the calendar does not have and never adjusts it; a typed date outside `min`, `max`, or `dateConstraints` shows and announces an out-of-range error that writes the bounds in the calendar, and Enter or blur keeps both kinds of rejected text so the person can correct it, while unreadable text reverts (`spec:AST-111` FR11). While a person types, `onChange` fires only for complete input, at most once for each distinct value in one typing session, and a one-digit day stays open until another digit, Enter, or blur, so a day the month lacks is never reported as another date; a two-digit year and input without a year commit on Enter or blur, in every calendar including Gregorian, and a one-digit year is not a date: like other text the field cannot finish, Enter or blur keeps it and shows the unreadable message as the field status (`spec:AST-111` FR12, AR3). Each rejection is announced once: a shown status announces itself, and the field's alert carries only a rejection that no status shows. `date`, `date_long`, and `date_weekday` write the committed text in the calendar and read back as the same date; `system_date` stays Gregorian ISO and the field reads ISO input with it (`spec:AST-111` FR13). Text from a custom `format` function that the field cannot read back cannot be re-committed after an edit (`spec:AST-111` FR14). Values stay Gregorian ISO dates (`spec:AST-111` FR3). Implementation gap: the bottom-sheet and native surfaces show Gregorian dates for every calendar (`spec:AST-111` FR15, FR16). | `spec:AST-111` FR3, FR9–FR14; current source; parser, round-trip, and calendar typed-entry tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, DateInput FR17 is the sheet-wheel Arrow/Page/Home/End requirement and FR18 the wheel's selection band. This row, the DateTimeInput FR11 citation and Calendar.spec.md:236 need the next free number after the rebase.

Live region announcing invalid typed input to assistive technology.
The value silently reverts on blur, so without this a screen-reader
user would get no feedback that their entry was rejected (WCAG 3.3.1).
Live region announcing rejected typed input to assistive technology,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (INV29): this region now carries only a rejection that no field status shows. Suggested: "Announces a typed rejection that no field status shows. A shown status announces itself." DateTimeInput.tsx:1903 has the same comment.

This branch was successfully deployed

1 active deployment
Preview — 97e62507 Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant