Skip to content
Draft
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
2d9fb81
fix(DateInput, DateTimeInput): the sheet's calendar pages like UIDate…
vjeux Nov 5, 2026
9954d25
refactor(DateInput): the month pager's sheet split is a mode of useOw…
vjeux Nov 5, 2026
77e8a8a
refactor(DateInput): MonthPager's Effects name what they sync, and it…
vjeux Nov 5, 2026
d0da0a1
fix(DateInput): the month pager follows a trackpad through the shared…
vjeux Nov 5, 2026
2a0703f
refactor(DateInput): the scroller and the pager share one month pane
vjeux Nov 5, 2026
8669954
refactor(DateInput): the scroller and the pager share one keyboard-fo…
vjeux Nov 5, 2026
2473006
test(DateInput): the sheet split's proof samples in the page
vjeux Nov 5, 2026
7def994
fix(DateInput): the arrows' step names its FR45 gap, and the title re…
vjeux Nov 5, 2026
e40b2f8
test(DateInput): four MonthPager proofs no longer hang on a loaded ru…
vjeux Nov 5, 2026
4c21450
test(DateInput): month paging follows the active calendar's months
vjeux Nov 5, 2026
9a04272
test(DateInput): the trackpad paging cases run on fake timers
vjeux Nov 5, 2026
87374bb
test(DateInput): the toggle's re-render proof spies on the pane's hook
vjeux Nov 5, 2026
6f62fc8
test(DateInput): the month pager's tests follow main
vjeux Nov 7, 2026
7ea993f
merge: main 3918a54ad0 into the month pager
vjeux Nov 8, 2026
4040d72
fix(DateInput, DateTimeInput): the calendar's toggle and selection fo…
vjeux Nov 5, 2026
9c453d6
fix(DateInput, DateTimeInput): the toggle's handler decides the focus…
vjeux Nov 5, 2026
f613d13
fix(DateInput, DateTimeInput): Done hands focus to the shown month, a…
vjeux Nov 5, 2026
289ef00
spec(DateInput, DateTimeInput): the title chevron and Done's focus ha…
vjeux Nov 5, 2026
081c564
test(DateInput): the toggle's storyboard holds the fade as it starts
vjeux Nov 5, 2026
f003f8a
fix(DateTimeInput): with a mouse, Done hands focus to the month in view
vjeux Nov 5, 2026
cd32c30
fix(DateInput, DateTimeInput): the grid's tab stop records no focus, …
vjeux Nov 5, 2026
5400c34
fix(DateInput, DateTimeInput): the month/year toggle plays the theme'…
vjeux Nov 5, 2026
a15489a
fix(DateInput, DateTimeInput): only a transition the toggle starts en…
vjeux Nov 5, 2026
a9368ec
test(DateInput): the toggle spec waits for the sheet and the wheels' …
vjeux Nov 5, 2026
5db1b15
fix(DateInput): a finger's tapped day selects at once; a mouse or pen…
vjeux Nov 5, 2026
11ab876
docs(DateInput, DateTimeInput): state the toggle's shield and focus
vjeux Nov 5, 2026
73e8382
docs(changeset): the toggle's note says what the wheels' warm-up does
vjeux Nov 6, 2026
c4fe3f8
test(DateInput): the toggle's tests follow main
vjeux Nov 7, 2026
dc2e1fc
docs(changeset): the toggle's note says what the warm-up spares it
vjeux Nov 7, 2026
90051f2
merge: main c5cc8f4c43 into the month pager
vjeux Nov 9, 2026
8173818
Merge main
vjeux Nov 10, 2026
d07b6eb
Merge main
vjeux Nov 10, 2026
947120e
refactor(DateInput): reach the month track without a data attribute
vjeux Nov 10, 2026
b5a487a
refactor(DateInput): reach the month track without a data attribute
vjeux Nov 10, 2026
9f26718
Merge main
vjeux Nov 11, 2026
8ddf2ae
chore(sandbox): date sheets on a page the document scrolls
vjeux Nov 11, 2026
184de63
Merge #8200
vjeux Nov 11, 2026
e0fc1d6
refactor(DateInput): keep the month grid in MonthScroller
vjeux Nov 11, 2026
6300c56
Merge #8200
vjeux Nov 11, 2026
1eba81b
refactor(DateInput): one hook for the month/year toggle
vjeux Nov 11, 2026
6404e3d
docs(DateInput): name the month/year toggle's hook in FR25
vjeux Nov 11, 2026
1e91935
Merge main
vjeux Nov 11, 2026
a675cf8
chore(DateInput): plain, short comments in the month pager files
vjeux Nov 11, 2026
f26df1b
docs(DateInput): name the month grid parts that MonthScroller exports
vjeux Nov 11, 2026
af33cf0
fix(DateTimeInput): keep the calendar drawn under the month/year wheels
vjeux Nov 11, 2026
a277007
chore(DateInput): split the toggle comments into shorter sentences
vjeux Nov 11, 2026
0295280
docs(DateInput): one month/year toggle look for both touch fields
vjeux Nov 11, 2026
6113b9d
Merge #8200
vjeux Nov 11, 2026
6c86a5a
chore(DateInput): keep TouchDateField's imports above its code
vjeux Nov 11, 2026
75d06f9
Merge main
vjeux Nov 11, 2026
0e1ac4d
Merge #8200
vjeux Nov 11, 2026
f34d87f
Merge main
vjeux Nov 11, 2026
65a8bb1
Merge #8200
vjeux Nov 11, 2026
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
16 changes: 16 additions & 0 deletions .changeset/calendar-pages-like-uidatepicker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@astryxdesign/core': patch
---

[fix] Under a finger, the calendar in the DateInput and DateTimeInput bottom sheets now pages its months like the iOS date picker:
Comment thread
astracat-bot[bot] marked this conversation as resolved.
Resolved

- A swipe moves at most one month, chosen at the lift by iOS's paging rule: a quick flick, or a drag past half a month. It settles on iOS's paging curve.
- The month title follows the drag and the settle. It changes when the next month is more than half in view.
- An arrow scrolls one month. A second tap mid-scroll goes on to the month after.
- In the sheet, a drag is decided once it has moved 10 px either way. It pages the calendar if it has moved 10 px sideways by then, and moves the sheet otherwise.
- A horizontal trackpad swipe moves the calendar with it and pages at the release, like a drag.
- With Reduce Motion on, the arrows' step and a swipe's settle cross-fade in 200 ms instead of travelling.

With a mouse, the calendar still scrolls natively. With any pointer, PageUp and PageDown now move the keyboard focus by a month, and by a year with Shift.

@vjeux
18 changes: 18 additions & 0 deletions .changeset/calendar-toggle-uidatepicker-timing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@astryxdesign/core': patch
---

[feat] Under a finger, the calendar in the DateInput and DateTimeInput bottom sheets now toggles to its month and year wheels like the iOS date picker:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Miss: these touch claims still have no Android emulator run at this head, and the description's iOS table (9–23 % of the fade lost before its first frame) contradicts the 54–82 % the newest commit measures here, with card 36's real-device result unstated. spec:AST-094 Platform support and DEC-27 ask for real-device iOS evidence and, for Android Chrome, Chrome with Gboard in the emulator at the exact head, with animations at normal scale and removed, plus a slow-device profile.


- The month/year toggle cross-fades over `--duration-medium-min` (225 ms in the neutral theme, the token nearest iOS's 0.25 s there), where DateInput used a quick linear fade and DateTimeInput an instant swap. The wheels arrive on the theme's entrance curve and leave on its exit curve. As in UIKit, the header's month arrows and Reset and the footer's buttons take no tap during the fade, and a second tap on the title turns it around.
- The hidden month/year wheels are laid out once the sheet settles, so a toggle after that does not lay them out.
- The title's chevron points along the reading direction and turns a quarter turn to point down while the wheels show, on the theme's move curve. In DateInput it was a down chevron that flipped up; in DateTimeInput it was a down chevron that stayed still.
- A finger's tapped day takes its selection at once, with no colour transition. A mouse or pen pick keeps the fade, in the touch sheet too.

With a mouse or a pen, the toggle stays as it is.

When the title or Done toggles with keyboard focus in the outgoing panel, focus moves into the incoming one, with any pointer. In DateTimeInput, Done lands on the shown month's day.

With Reduce Motion on, nothing travels: under a finger the toggle cross-fades in 200 ms, and the chevron dips instead of turning, its turn landing mid-dip. With a mouse it changes at once.

@vjeux
1 change: 1 addition & 0 deletions apps/sandbox/scripts/next-export-routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"/pages/mobile-prototypes/",
"/pages/mobile-spacing/",
"/pages/mobile-type/",
"/pages/month-year-toggle/",
"/pages/motion-examples/",
"/pages/motion-lab/",
"/pages/motion-lab/bugs/",
Expand Down
64 changes: 64 additions & 0 deletions apps/sandbox/src/app/(raw)/pages/month-year-toggle/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

/**
* @file page.tsx
* @input None
* @output A bare page with a DateInput sheet over two centuries, a
* DateTimeInput sheet, and rows that make the document scroll
* @position A raw page, with no sandbox chrome, for trying the month and year
* toggle in both sheets on a phone. The page scrolls with the document, as
* an app's page does.
*/

import {useState} from 'react';
import type {ISODateString} from '@astryxdesign/core/Calendar';
import {DateInput} from '@astryxdesign/core/DateInput';
import {DateTimeInput} from '@astryxdesign/core/DateTimeInput';
import type {ISODateTimeString} from '@astryxdesign/core/DateTimeInput';
import {VStack} from '@astryxdesign/core/Layout';
import {Heading, Text} from '@astryxdesign/core/Text';

const ROWS = Array.from({length: 40}, (_, index) => `Row ${index + 1}`);

export default function MonthYearTogglePage() {
const [date, setDate] = useState<ISODateString | undefined>(
'2026-03-15' as ISODateString,
);
const [dateTime, setDateTime] = useState<ISODateTimeString | undefined>(
undefined,
);
return (
<VStack gap={4} padding={4}>
<VStack gap={1}>
<Heading level={1}>Month and year toggle</Heading>
<Text color="secondary">
Tap a field, then tap the month title in its sheet.
</Text>
</VStack>
<DateInput
label="Date of birth"
presentation="bottom-sheet"
min={'1900-01-01' as ISODateString}
max={'2099-12-31' as ISODateString}
value={date}
onChange={setDate}
/>
<DateTimeInput
label="Meeting time"
placeholder="Select a date"
presentation="bottom-sheet"
value={dateTime}
onChange={setDateTime}
/>
<VStack gap={2}>
{ROWS.map(row => (
<Text key={row} color="secondary">
{row}
</Text>
))}
</VStack>
</VStack>
);
}
6 changes: 6 additions & 0 deletions apps/sandbox/src/app/sandboxPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export const categories: SandboxCategory[] = [
description:
'Every built-in Text type at the default size, on a bare page that scrolls',
},
{
name: 'Month and Year Toggle',
href: '/pages/month-year-toggle/',
description:
'The DateInput and DateTimeInput sheets, whose month title opens the month and year wheels, on a page that scrolls with the document',
},
],
},
{
Expand Down
19 changes: 19 additions & 0 deletions apps/storybook/stories/DateInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,25 @@ export const WithMinMax: Story = {
},
};

/**
* The bottom-sheet calendar pages its months like the iOS date picker. One
* month moves per swipe, and the title follows the drag.
*/
export const BottomSheetCalendar: Story = {
render: args => {
const [value, setValue] = useState<ISODateString | undefined>(
'2026-10-15' as ISODateString,
);
return <DateInput {...args} value={value} onChange={setValue} />;
},
args: {
label: 'Date',
presentation: 'bottom-sheet',
min: '2026-01-01' as ISODateString,
max: '2026-12-31' as ISODateString,
},
};

export const WithMaxDateInLayout: Story = {
render: args => {
const [value, setValue] = useState<ISODateString | undefined>(undefined);
Expand Down
17 changes: 17 additions & 0 deletions apps/storybook/stories/DateTimeInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,23 @@ export const WithTimeIncrement: Story = {
},
};

/**
* The bottom sheet: the calendar with its month/year toggle, and the time
* wheels.
*/
export const BottomSheetCalendar: Story = {
render: args => {
const [value, setValue] = useState<ISODateTimeString | undefined>(
'2026-03-15T09:00' as ISODateTimeString,
);
return <DateTimeInput {...args} value={value} onChange={setValue} />;
},
args: {
label: 'Time slot',
presentation: 'bottom-sheet',
},
};

/**
* `timeOptionInterval` turns the time field into a combobox over a list of
* preset times at that cadence. Click the time field or press Alt+ArrowDown to
Expand Down
72 changes: 72 additions & 0 deletions apps/storybook/stories/MonthPagerProofs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/**
* @file MonthPagerProofs.stories.tsx
* @input Uses React and the internal MonthPager (packages/core/src/DateInput)
* @output The month pager alone, whatever the primary pointer: 2026 from
* March, its shown month in a read-only field
* @position A checked-in reproduction path for the trackpad proofs in
* packages/core/src/DateInput/__tests__/MonthPager.interaction.browser.spec.ts.
* WebKit's driver sends a trackpad's precise deltas only from a desktop
* context, where the sheets show the mouse calendar instead.
*/

import type {Meta, StoryObj} from '@storybook/react';
import {useState} from 'react';
import {MonthPager} from '../../../packages/core/src/DateInput/MonthPager';
import {
createMonthModel,
fromMonthIndex,
toMonthIndex,
} from '../../../packages/core/src/DateInput/monthGeometry';

const FIRST = toMonthIndex(2026, 1);
const LAST = toMonthIndex(2026, 12);
const MARCH = toMonthIndex(2026, 3);
const TODAY = {year: 2026, month: 3, day: 15};
const GREGORY = createMonthModel('gregory');

/**
* The readout takes the theme's text colour on the theme's surface, not the
* browser's white field.
*/
const READOUT_STYLE = {color: 'inherit', backgroundColor: 'transparent'};

const meta = {
title: 'Core/MonthPager',
tags: ['no-visual', '!autodocs'],
parameters: {layout: 'centered'},
} satisfies Meta;

export default meta;
type Story = StoryObj<typeof meta>;

/** The pager alone. Its field names the month it shows. */
export const Pager: Story = {
render: () => {
const [shown, setShown] = useState(MARCH);
const {year, month} = fromMonthIndex(shown);
return (
<div style={{display: 'grid', gap: 16, inlineSize: 360}}>
<input
aria-label="Shown month"
readOnly
style={READOUT_STYLE}
value={`${year}-${String(month).padStart(2, '0')}`}
/>
<MonthPager
monthModel={GREGORY}
minMonthIndex={FIRST}
maxMonthIndex={LAST}
initialMonthIndex={MARCH}
onVisibleMonthChange={setShown}
selectedDate={null}
today={TODAY}
isDateDisabled={() => false}
weekStartsOn={0}
onSelect={() => {}}
/>
</div>
);
},
};
Loading