Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ccdac38
refactor(date-field): align date inputs with Field composition
IzumiSy Jul 16, 2026
b783da1
fix(date-field): restore semantic props and form validation
IzumiSy Jul 29, 2026
c1345e1
Merge branch 'main' into refactor/date-field-field-composition
IzumiSy Jul 29, 2026
d9bb279
Merge branch 'main' into refactor/date-field-field-composition
interacsean Jul 30, 2026
80dbc54
refactor(date-field): drop Base UI field integration
IzumiSy Jul 31, 2026
77e3677
refactor(date-field): drop proxy validity effects
IzumiSy Jul 31, 2026
c891a58
Merge branch 'main' into refactor/date-field-field-composition
IzumiSy Jul 31, 2026
eb230c6
refactor(date-field): narrow react imports
IzumiSy Jul 31, 2026
e53dcb5
fix(vite-app): tighten date picker demo typing
IzumiSy Jul 31, 2026
a2af364
test(date-field): restore standalone regression coverage
IzumiSy Jul 31, 2026
090599b
test(date-field): restore test comments
IzumiSy Jul 31, 2026
37e33bf
test: restore date-field tests from main baseline
IzumiSy Jul 31, 2026
0ad0835
feat: integrate date controls with Field wiring
IzumiSy Jul 31, 2026
bc8a659
docs(date-field): clarify proxy input bridge
IzumiSy Jul 31, 2026
7833711
Merge branch 'main' into refactor/date-field-field-composition
IzumiSy Jul 31, 2026
344f611
Fix DatePicker popup focus and Field label wiring
IzumiSy Jul 31, 2026
98fbc50
Refine date field docs and typing
IzumiSy Jul 31, 2026
86abe76
Merge branch 'main' into refactor/date-field-field-composition
IzumiSy Aug 3, 2026
62d7e75
Merge branch 'main' into refactor/date-field-field-composition
IzumiSy Aug 12, 2026
14e7a9c
fix(date-field): restore Form and Field wiring
IzumiSy Aug 13, 2026
10e93aa
refactor(date-field): split and document hook responsibilities
IzumiSy Aug 13, 2026
47bb969
docs(date-field): tighten hook comments
IzumiSy Aug 13, 2026
a8f25da
refactor(date-field): reduce ref churn in field bridge
IzumiSy Aug 13, 2026
a86f81a
Merge branch 'main' into refactor/date-field-field-composition
IzumiSy Aug 13, 2026
4eaeb10
docs(changeset): consolidate date-field notes
IzumiSy Aug 13, 2026
27ee89c
docs(changeset): clarify hideTimeZone removal
IzumiSy Aug 13, 2026
665b34c
Use the exact version of base-ui
IzumiSy Aug 13, 2026
bc235a3
Merge branch 'main' into refactor/date-field-field-composition
IzumiSy Aug 13, 2026
cd18196
fix(date-field): address review follow-ups
IzumiSy Aug 14, 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
43 changes: 43 additions & 0 deletions .changeset/odd-maps-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
"@tailor-platform/app-shell": minor
---

Refactor `DateField` / `DatePicker` to follow the same composition model as `Field`, `Select`, `Combobox`, and `Autocomplete`.

The date controls are now **control-first**: field chrome moved out of the control props and into `Field.Root` composition. They also interoperate correctly with `Form` / `Field.Root` label wiring, `onFormSubmit` value collection, and submit-time validation for required and out-of-range default values.

Breaking changes:

- `label`, `description`, and `errorMessage` were removed from `DateField` / `DatePicker`; compose them with `Field.Root`, `Field.Label`, `Field.Description`, and `Field.Error` instead.
- `hideTimeZone` was removed; it was previously accepted by the prop types but had no effect.

`isInvalid` still remains a top-level prop for externally-controlled invalid styling, and the semantic date props (`isRequired`, `isDisabled`, `isReadOnly`, `minValue`, `maxValue`, `isDateUnavailable`) remain top-level and aligned with `Calendar`.

Before:

```tsx
<DatePicker
label="Delivery date"
description="When should we ship your order?"
minValue={today(getLocalTimeZone())}
errorMessage={error}
isInvalid={!!error}
/>
```

After:

```tsx
<Field.Root invalid={!!error}>
<Field.Label>Delivery date</Field.Label>
<DatePicker aria-label="Delivery date" minValue={today(getLocalTimeZone())} />
<Field.Description>When should we ship your order?</Field.Description>
<Field.Error match={!!error}>{error}</Field.Error>
</Field.Root>
```

Standalone usage still works with accessible naming:

```tsx
<DateField aria-label="Invoice date" />
```
193 changes: 103 additions & 90 deletions docs/components/date-picker.md

Large diffs are not rendered by default.

209 changes: 134 additions & 75 deletions examples/vite-app/src/pages/date-picker/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useState, type FormEvent } from "react";
import { cloneElement, useState, type ReactElement } from "react";
import {
Layout,
DateField,
DatePicker,
Calendar,
Form,
Field,
Button,
useTimeZone,
parseDate,
Expand All @@ -14,39 +15,68 @@ import {
} from "@tailor-platform/app-shell";
import { CalendarDays } from "lucide-react";

type DemoFieldControlProps = {
id?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
isInvalid?: boolean;
};

function DemoField({
id,
label,
description,
error,
children,
}: {
id: string;
label: string;
description?: string;
error?: string;
children: ReactElement<DemoFieldControlProps>;
}) {
const describedBy = [description && `${id}-description`, error && `${id}-error`]
.filter(Boolean)
.join(" ");

return (
<div className="flex flex-col gap-1 items-start">
<label id={`${id}-label`} htmlFor={id} className="text-sm font-medium">
{label}
</label>
{cloneElement(children, {
id,
"aria-labelledby": `${id}-label`,
"aria-describedby": describedBy || undefined,
isInvalid: !!error || children.props.isInvalid,
})}
{description && (
<p id={`${id}-description`} className="text-sm text-muted-foreground">
{description}
</p>
)}
{error && (
<p id={`${id}-error`} className="text-sm font-medium text-destructive">
{error}
</p>
)}
</div>
);
}

const DatePickerPage = () => {
const tz = useTimeZone();
const [fieldValue, setFieldValue] = useState<CalendarDate | null>(null);
const [pickerValue, setPickerValue] = useState<CalendarDate | null>(null);
const [calendarValue, setCalendarValue] = useState<DateValue | null>(null);
const [weekendValue, setWeekendValue] = useState<CalendarDate | null>(null);

// Form-validation demo state.
const [deliveryDate, setDeliveryDate] = useState<CalendarDate | null>(null);
const [deliveryError, setDeliveryError] = useState<string | undefined>(undefined);
const [confirmedDate, setConfirmedDate] = useState<string | null>(null);

const tomorrow = tz.today().add({ days: 1 });
const threeMonths = tz.today().add({ months: 3 });

// Validation runs on submit; the DatePicker surfaces the message through its
// own `errorMessage` / `isInvalid` props (it isn't a Base UI Field control).
const handleDeliverySubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!deliveryDate) {
setConfirmedDate(null);
setDeliveryError("Please select a delivery date.");
return;
}
if (deliveryDate.compare(tz.today()) < 0) {
setConfirmedDate(null);
setDeliveryError("Delivery date can't be in the past.");
return;
}
setDeliveryError(undefined);
setConfirmedDate(deliveryDate.toString());
};

return (
<Layout>
<Layout.Header title="DatePicker" />
Expand All @@ -72,19 +102,25 @@ const DatePickerPage = () => {
<h2 className="text-base font-semibold border-b pb-2">DateField</h2>

<div className="flex flex-wrap gap-6 items-start">
<DateField
label="Basic"
value={fieldValue}
onChange={(v) => setFieldValue(v as CalendarDate | null)}
/>
<DateField
<DemoField id="date-field-basic" label="Basic">
<DateField
value={fieldValue}
onChange={(v) => setFieldValue(v as CalendarDate | null)}
/>
</DemoField>
<DemoField
id="date-field-with-description"
label="With description"
description="Select a date within the next 3 months"
minValue={tomorrow}
maxValue={threeMonths}
/>
<DateField label="Disabled" isDisabled defaultValue={parseDate("2025-06-15")} />
<DateField label="Required" isRequired errorMessage="Date is required" />
>
<DateField minValue={tomorrow} maxValue={threeMonths} />
</DemoField>
<DemoField id="date-field-disabled" label="Disabled">
<DateField isDisabled defaultValue={parseDate("2025-06-15")} />
</DemoField>
<DemoField id="date-field-required" label="Required">
<DateField isRequired />
</DemoField>
</div>

{fieldValue && (
Expand All @@ -99,30 +135,38 @@ const DatePickerPage = () => {
<h2 className="text-base font-semibold border-b pb-2">DatePicker</h2>

<div className="flex flex-wrap gap-6 items-start">
<DatePicker
label="Basic"
value={pickerValue}
onChange={(v) => setPickerValue(v as CalendarDate | null)}
/>
<DatePicker
<DemoField id="date-picker-basic" label="Basic">
<DatePicker
value={pickerValue}
onChange={(v) => setPickerValue(v as CalendarDate | null)}
/>
</DemoField>
<DemoField
id="date-picker-future"
label="Future dates only"
description="Minimum: tomorrow"
minValue={tomorrow}
/>
<DatePicker
>
<DatePicker minValue={tomorrow} />
</DemoField>
<DemoField
id="date-picker-weekdays"
label="No weekends"
description="Weekday dates only"
isDateUnavailable={(d) => {
const day = d.toDate(tz.value).getDay();
return day === 0 || day === 6;
}}
/>
<DatePicker
>
<DatePicker
isDateUnavailable={(d) => {
const day = d.toDate(tz.value).getDay();
return day === 0 || day === 6;
}}
/>
</DemoField>
<DemoField
id="date-picker-range"
label="With range"
minValue={tz.today()}
maxValue={threeMonths}
description={`Today → ${threeMonths.toString()}`}
/>
>
<DatePicker minValue={tz.today()} maxValue={threeMonths} />
</DemoField>
</div>

{pickerValue && (
Expand All @@ -137,28 +181,31 @@ const DatePickerPage = () => {
<h2 className="text-base font-semibold border-b pb-2">In a form (submit validation)</h2>
<p className="text-sm text-muted-foreground">
Standard <code className="bg-muted px-1 py-0.5 rounded">Form</code> +{" "}
<code className="bg-muted px-1 py-0.5 rounded">Button</code>. Submitting empty (or
with a past date) triggers validation — the error surfaces through the DatePicker's
own <code className="bg-muted px-1 py-0.5 rounded">errorMessage</code> /{" "}
<code className="bg-muted px-1 py-0.5 rounded">isInvalid</code> props, and clears as
soon as a valid date is picked.
<code className="bg-muted px-1 py-0.5 rounded">Field.Root</code>. Submitting empty (or
with a past date) blocks submit, shows the field error, and clears as soon as a valid
date is picked.
</p>
<Form
onSubmit={handleDeliverySubmit}
<Form<{ deliveryDate: string }>
onFormSubmit={({ deliveryDate: submittedDeliveryDate }) =>
setConfirmedDate(submittedDeliveryDate)
}
className="flex flex-col items-start gap-4 max-w-sm"
>
<DatePicker
label="Delivery date"
description="When should we ship your order?"
isRequired
value={deliveryDate}
onChange={(v) => {
setDeliveryDate(v as CalendarDate | null);
if (v) setDeliveryError(undefined);
}}
errorMessage={deliveryError}
isInvalid={!!deliveryError}
/>
<Field.Root name="deliveryDate">
<Field.Label>Delivery date</Field.Label>
<DatePicker
value={deliveryDate}
onChange={(v) => {
setDeliveryDate(v as CalendarDate | null);
setConfirmedDate(null);
}}
isRequired
minValue={tz.today()}
/>
<Field.Description>When should we ship your order?</Field.Description>
<Field.Error match="valueMissing">Please select a delivery date.</Field.Error>
<Field.Error match="customError" />
</Field.Root>
<Button type="submit">Schedule delivery</Button>
</Form>
{confirmedDate && (
Expand All @@ -177,9 +224,15 @@ const DatePickerPage = () => {
explicitly to force a specific start day regardless of locale.
</p>
<div className="flex flex-wrap gap-6 items-start">
<DatePicker label="Forced Sunday" firstDayOfWeek="sun" />
<DatePicker label="Forced Monday" firstDayOfWeek="mon" />
<DatePicker label="Locale default" />
<DemoField id="date-picker-sun" label="Forced Sunday">
<DatePicker firstDayOfWeek="sun" />
</DemoField>
<DemoField id="date-picker-mon" label="Forced Monday">
<DatePicker firstDayOfWeek="mon" />
</DemoField>
<DemoField id="date-picker-locale" label="Locale default">
<DatePicker />
</DemoField>
</div>
</section>

Expand All @@ -189,9 +242,15 @@ const DatePickerPage = () => {
Locale (segment order + names)
</h2>
<div className="flex flex-wrap gap-6 items-start">
<DatePicker label="en-US (MM/DD/YYYY)" locale="en-US" />
<DatePicker label="en-GB (DD/MM/YYYY, Mon-first)" locale="en-GB" />
<DatePicker label="ja-JP (YYYY/MM/DD)" locale="ja-JP" />
<DemoField id="date-picker-en-us" label="en-US (MM/DD/YYYY)">
<DatePicker locale="en-US" />
</DemoField>
<DemoField id="date-picker-en-gb" label="en-GB (DD/MM/YYYY, Mon-first)">
<DatePicker locale="en-GB" />
</DemoField>
<DemoField id="date-picker-ja-jp" label="ja-JP (YYYY/MM/DD)">
<DatePicker locale="ja-JP" />
</DemoField>
</div>
</section>

Expand Down
Loading
Loading