Skip to content
Open
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
26 changes: 26 additions & 0 deletions .changeset/date-input-native-touch-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@astryxdesign/core': patch
---

[feat] DateInput: on a touch device the field now opens the browser/OS date
picker instead of the in-page calendar

A phone tapping the calendar toggle got a 320px month grid in a popover: a
desktop control shrunk onto a screen where the platform already ships a date
picker every user knows, with system-sized hit areas, momentum scrolling, and
the OS locale and accessibility settings applied for free.

DateInput now renders `<input type="date">` when the pointer is coarse, so iOS
shows its wheel and Android its calendar dialog. The new `nativePicker` prop
takes `'touch'` (the default — native on touch), `'always'`, or `'never'` for a
field that must look identical on every device. The switch is client-side, so
SSR still renders the text field; a browser without `type="date"` support keeps
it too.

In native mode the browser owns the field, so `format`, `numberOfMonths`, and
`weekStartsOn` no longer apply — the control shows the OS locale's date format.
`placeholder` still shows. `min` and `max` carry over; `dateConstraints` cannot
(a native picker only expresses a contiguous range), so `'touch'` keeps the
Calendar popover whenever constraints are set.

@imdreamrunner
53 changes: 53 additions & 0 deletions apps/storybook/stories/DateInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ const meta: Meta<typeof DateInput> = {
description:
"Display format for the committed value, reusing Timestamp's vocabulary. Defaults to 'date_long' (long-month date).",
},
nativePicker: {
control: 'radio',
options: ['touch', 'always', 'never'],
description:
"Whether the browser/OS date picker replaces the Calendar popover. 'touch' (default) switches on touch devices.",
},
},
};

Expand Down Expand Up @@ -438,6 +444,53 @@ export const AllVariations: Story = {
},
};

/**
* On a touch device DateInput hands date picking to the browser, rendering
* `<input type="date">` so the platform's own picker opens — the iOS wheel,
* the Android calendar dialog. Open this story on a phone, or in a desktop
* browser's device emulation (which reports a coarse pointer), to see the
* default field switch.
*/
export const NativePicker: Story = {
name: 'Native picker on touch',
render: () => {
const [touch, setTouch] = useState<ISODateString | undefined>('2026-03-21');
const [always, setAlways] = useState<ISODateString | undefined>(
'2026-03-21',
);
const [never, setNever] = useState<ISODateString | undefined>('2026-03-21');
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
<DateInput
label="Touch (the default)"
description="Native control on a coarse pointer, Calendar popover on a mouse"
value={touch}
onChange={setTouch}
hasClear
/>
<DateInput
label="Always native"
description="Native control on this device too"
nativePicker="always"
value={always}
onChange={setAlways}
min="2026-01-01"
max="2026-12-31"
hasClear
/>
<DateInput
label="Never native"
description="Calendar popover everywhere, phones included"
nativePicker="never"
value={never}
onChange={setNever}
hasClear
/>
</div>
);
},
};

export const Clearable: Story = {
render: args => {
const [value, setValue] = useState<ISODateString | undefined>('2026-04-06');
Expand Down
23 changes: 20 additions & 3 deletions packages/core/src/DateInput/DateInput.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ export const docs = {
"How the committed date value is displayed. Named values are reused from Timestamp's format vocabulary: 'date' shows 'Mar 21, 2026', 'date_long' shows 'March 21, 2026', 'date_weekday' shows 'Wed, Mar 21, 2026', 'system_date' shows '2026-03-21'. A function receives the ISO value and returns a custom string. Applies only to the committed value, never to text being typed.",
default: "'date_long'",
},
{
name: 'nativePicker',
type: "'touch' | 'always' | 'never'",
description:
"Whether date picking is handed to the browser/OS instead of the built-in Calendar popover. 'touch' uses the native control on touch devices (coarse pointer) and the Calendar popover on mouse-driven ones; 'always' uses it wherever the browser supports input type=date; 'never' always uses the Calendar popover. In native mode the browser owns the field, so format, numberOfMonths, and weekStartsOn no longer apply (placeholder still shows, rendered over the empty control in place of the OS mm/dd/yyyy hint); min and max carry over but dateConstraints cannot, so 'touch' keeps the Calendar popover whenever dateConstraints is set.",
default: "'touch'",
},
{
name: 'width',
type: 'SizeValue',
Expand Down Expand Up @@ -236,7 +243,7 @@ export const docs = {
name: 'Text input',
required: true,
description:
'A field where the user can type a date directly. Parses common formats like MM/DD/YYYY.',
'A field where the user can type a date directly. Parses common formats like MM/DD/YYYY. On touch devices it becomes a native date control the OS picker fills in.',
},
{
name: 'Calendar icon',
Expand All @@ -248,7 +255,7 @@ export const docs = {
name: 'Calendar popover',
required: false,
description:
'A month grid that appears when the icon is clicked or the input is focused.',
'A month grid that appears when the icon is clicked or the input is focused. On touch devices the browser/OS date picker takes its place.',
},
{
name: 'Clear button',
Expand Down Expand Up @@ -437,6 +444,13 @@ export const docsZh = {
"已选日期的显示格式。命名值复用 Timestamp 的格式词汇:'date' 显示 'Mar 21, 2026','date_long' 显示 'March 21, 2026','date_weekday' 显示 'Wed, Mar 21, 2026','system_date' 显示 '2026-03-21'。函数接收 ISO 值并返回自定义字符串。仅作用于已提交的值,不影响正在输入的文本。",
default: "'date_long'",
},
{
name: 'nativePicker',
type: "'touch' | 'always' | 'never'",
description:
"是否将日期选择交给浏览器/操作系统,而非内置的日历弹出层。'touch' 在触摸设备(粗指针)上使用原生控件,在鼠标设备上使用日历弹出层;'always' 在支持 input type=date 的浏览器上始终使用原生控件;'never' 始终使用日历弹出层。原生模式下字段由浏览器绘制,因此 format、numberOfMonths 和 weekStartsOn 不再生效(placeholder 仍会显示:在空白控件上覆盖渲染,取代系统自带的 mm/dd/yyyy 提示);min 和 max 会传递给原生控件,但 dateConstraints 无法传递,所以设置 dateConstraints 时 'touch' 会保留日历弹出层。",
default: "'touch'",
},
{
name: 'xstyle',
type: 'StyleXStyles',
Expand All @@ -459,7 +473,8 @@ export const docsZh = {

/** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
export const docsDense = {
description: 'text input w/ calendar popover for picking a date',
description:
'text input w/ calendar popover for picking a date; native OS picker on touch',
usage: {
description:
'DateInput lets the user type or pick a date from a calendar popover. Use for scheduling, deadlines, booking dates, or any form field needing a calendar date.',
Expand Down Expand Up @@ -537,6 +552,8 @@ export const docsDense = {
weekStartsOn: 'first day of week in calendar (0=Sunday, or name e.g. "mon")',
format:
"committed-value display: 'date_long' (default, March 21, 2026), 'date' (Mar 21, 2026), 'date_weekday' (Wed, Mar 21, 2026), 'system_date' (2026-03-21), or (iso)=>string; reuses Timestamp vocabulary. Committed value only, not while typing.",
nativePicker:
"browser/OS date picker instead of the Calendar popover: 'touch' (default) = native on touch (coarse pointer), 'always', 'never'. Native mode ignores format/numberOfMonths/weekStartsOn (placeholder still renders over the empty control); min+max carry over, dateConstraints keeps 'touch' on the popover.",
xstyle: 'StyleX styles for layout; must be stylex.create() value',
},
};
Loading
Loading