Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/month-year-wheels-move-the-date.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@astryxdesign/core': patch
---

[fix] Under a finger, the month and year wheels behind the calendar title in the DateInput and DateTimeInput bottom sheets now behave like the iOS date picker's:

- Turning a wheel moves a chosen date to the new month and keeps its day, so October 15 becomes May 15. A day the month lacks becomes its last day, the result stays within `min` and `max`, and DateTimeInput keeps the time. A date that `dateConstraints` refuses is not chosen; the calendar still moves.
- With no date chosen, the wheels only move the calendar.

With a mouse, the wheels still move only the calendar.

@vjeux
22 changes: 12 additions & 10 deletions docs/specs/components/DateInput/DateInput.spec.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/specs/components/DateTimeInput/DateTimeInput.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ Consumer migration instructions belong in consumer docs and release notes.
| FR11 | The pointer date segment follows `component:DateInput` FR20 for typed entry in the resolved calendar: it reads the dates `Intl.DateTimeFormat` writes for the locale and calendar, and in the Gregorian calendar, on Enter or blur, text it cannot otherwise read and a date the locale's field order lacks, such as `13/5/2026` in `en-US`, when `parseDateInput` reads it, with a one- or three-digit year not a date, reads years of the calendar, rejects a date the calendar does not have without adjusting it, shows and announces an out-of-range error with the bounds in the calendar, fires `onChange` while typing only for complete input and at most once for each distinct date in one typing session, shows and announces each rejection once as DateInput does, and writes the committed date as `date_long` in the calendar (`spec:AST-111` FR9–FR13). Its Calendar shows the same calendar, and the combined value stays a Gregorian ISO date-time (`spec:AST-111` FR3). | `spec:AST-111` FR3, FR9–FR13; current source; calendar typed-entry tests |
| FR12 | With a calendar other than `gregory` or `iso8601`, every presentation that would show native controls — `adaptive-native`, the default, deprecated `touch` and `always`, and an explicit `native` — shows the Astryx popovers on a fine pointer and the bottom sheet on a coarse pointer, for the date and the time alike, because the native date control is Gregorian (`spec:AST-111` FR15). An explicit `native` also warns once in development. The bottom sheet's Date panel follows `component:DateInput` FR21: calendar months in six-row panes, the month wheel of the selected calendar year with leap months, the common counterpart on a year change, and year labels with their era; its date segment writes the date as `date_long` in the calendar (`spec:AST-111` FR16). The combined value stays a Gregorian ISO date-time (`spec:AST-111` FR3). | `spec:AST-111` FR3, FR15, FR16; current source; calendar presentation and touch calendar tests |
| FR13 | While disabled or busy, the date segment keeps or drops typed text as `component:DateInput` FR12 states, judging it on the next Enter or blur once editable. | `spec:AST-085` FR2; current source and tests |
| FR14 | Under a coarse primary pointer, the date sheet's month and year wheels MUST move a chosen date as `component:DateInput` FR23 does, keeping its time. | `component:DateInput` FR23; UIDatePicker on the iOS 26.2 simulator |

### Allowed variation

Expand Down Expand Up @@ -312,6 +313,7 @@ leading clock glyph uses `date-time-input-clock-icon`.
| FR11, AR5 | `DateTimeInput.calendar.test.tsx` ("reports one Persian date typed in Persian digits, only when complete", "writes the committed date in the calendar", "rejects a date the calendar does not have and names the calendar", "shows an out-of-range error with the bounds in the calendar", "names the calendar and shows the hint when focused and empty", "shows a caller placeholder instead of the format hint", "names no calendar when the field uses the provider calendar", "names the Gregorian calendar inside a Persian provider", "commits Gregorian text that only parseDateInput reads on blur", "commits a day-first date that does not exist month-first, on blur only", "fires once for a day-first Gregorian date typed one key at a time"); the parser and round-trip suites under `component:DateInput` FR20 | Persian digits, Hebrew leap year, Japanese era bound, Gregorian day-first typing | A typed date reported before it is complete, an adjusted date, a shared rejection message, a hint that replaces a caller placeholder, a calendar description that is missing or that repeats the provider's calendar, or a non-ISO value fails. Evidence gap: no real-browser run drives the DateTimeInput date segment itself. | `audit:DateTimeInput/behavior` |
| FR12 | `DateInput.calendarPresentation.test.tsx` DateTimeInput cases (every presentation and deprecated `nativePicker` value in Gregorian and Persian on fine and coarse pointers; "warns once when an explicit native cannot show the calendar"; "writes the sheet date segment in %s %s with its era" for `en` and `fa-IR` Persian and `th-TH` Buddhist); `DateInputTouch.calendar.test.tsx` "shows Persian months and the date in the calendar"; `DateInput.noTemporal.test.tsx` "shows a Persian DateTimeInput as Gregorian native controls" | Persian on fine and coarse pointers, explicit `native`, no `Temporal` | A native date or time control for a non-Gregorian calendar, a missing warning for an explicit `native`, or a Date panel in another calendar, fails | `audit:DateTimeInput/surfaces` |
| FR13 | `DateTimeInput.test.tsx` "typed text while the field is disabled or busy" cases | Loading, disabled, and saves; unfinished, valid, reported, rejected, new `value` | A commit, `onChange`, reset, or rejection while disabled or busy, a rejection that does not return once editable, typed text that outlives a new `value`, or reported text that outlives the lock fails. | `audit:DateTimeInput/state` |
| FR14 | `DateTimeInputTouch.test.tsx` "under a finger: moves the chosen date with the month wheel, keeping the day and the time", "under a finger: a date the month wheel moves runs changeAction after onChange, shows at once, and is busy until it settles (family:input-fields FR6)", "under a finger: opening the month/year wheels on a date outside the range changes nothing", "under a finger: keeps a date a custom constraint refuses, while the calendar moves" | A month turn with a time; a pending Action; opened out of range; a refused date | A wheel that drops the time, leaves the date behind, changes the value on opening, or chooses a refused date fails. | `audit:DateTimeInput/surfaces` |
| Theming anatomy map | `scripts/check-knowledge.mjs`, `themingTargets.test.ts` | root, segments, icons, listbox, options | Missing, undocumented, or wrongly placed targets fail knowledge or target checks. | `audit:DateTimeInput/theming` |

## Current-state impact
Expand Down
38 changes: 36 additions & 2 deletions packages/core/src/DateInput/DateInputTouch.calendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ import {
type ISODateTimeString,
} from '../DateTimeInput/DateTimeInput';
import {InternationalizationProvider} from '../i18n/InternationalizationProvider';
import {createMonthModel} from './monthGeometry';
import type {CalendarSystem} from '../utils/calendarSystem';
import {createMonthModel, dateInMonth} from './monthGeometry';
import {
type CalendarSystem,
calendarDateFromFields,
calendarDayOfMonth,
} from '../utils/calendarSystem';
import type {ISODateString} from '../utils/dateTypes';
import {plainDateAddDays, plainDateFromISO} from '../utils/plainDate';

Expand Down Expand Up @@ -476,4 +480,34 @@ describe('the month model', () => {
month: 6,
});
});

it('moves a date by calendar months, keeping its day of the month', () => {
const persian = createMonthModel('persian');
const mehr = persian.indexOfYearMonth(1405, 7);
// Shahrivar has 31 days and Mehr 30, so the 31st becomes the 30th.
for (const [day, expected] of [
[15, 15],
[31, 30],
] as const) {
const shahrivar = calendarDateFromFields('persian', {
year: 1405,
month: 6,
day,
})!;
const moved = dateInMonth(persian, shahrivar, mehr);
expect(persian.indexOf(moved)).toBe(mehr);
expect(calendarDayOfMonth('persian', moved)).toBe(expected);
}
// Adar I has 30 days in the leap year 5784; Adar of 5785 has 29.
const hebrew = createMonthModel('hebrew');
const adar = hebrew.indexOfYearMonth(5785, 6);
const adarI30 = calendarDateFromFields('hebrew', {
year: 5784,
month: 6,
day: 30,
})!;
const moved = dateInMonth(hebrew, adarI30, adar);
expect(hebrew.indexOf(moved)).toBe(adar);
expect(calendarDayOfMonth('hebrew', moved)).toBe(29);
});
});
Loading
Loading