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
9 changes: 9 additions & 0 deletions .changeset/show-day-of-week.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@tailor-platform/app-shell": minor
---

Add `showDayOfWeek` to `DateField`, `DatePicker`, and `DateRangePicker`. It appends the locale's short day of the week where the locale places it (`2025/12/19(金)`, `Fri, 12/19/2025`). It is read-only and derived from the entered date. While a date segment is mid-entry it keeps showing the previous day of the week, so typing `25` never flashes the 2nd's. Screen readers hear the full day name with each date segment's value.

```tsx
<DatePicker aria-label="Closing date" showDayOfWeek />
```
8 changes: 4 additions & 4 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,10 +470,10 @@
"DateRangePickerProps"
],
"hashes": {
"typeSurface": "6b07d4169fa991df",
"outline": "caab56e5766bdb72",
"typeSurface": "a0f5522359e8f81d",
"outline": "50e117ad2a0562ba",
"snapshot": null,
"outputMd": "f39002ad79625dc4",
"outputMd": "bc69f14829004f9d",
"examples": null
}
},
Expand Down Expand Up @@ -1694,7 +1694,7 @@
"packages/core/skills/app-shell-patterns/references/components/command-palette.md": "7e5bc675a593791a",
"packages/core/skills/app-shell-patterns/references/components/csv-importer.md": "53d3c795ca21b9dc",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "3e8474e4d1437c91",
"packages/core/skills/app-shell-patterns/references/components/date-picker.md": "f39002ad79625dc4",
"packages/core/skills/app-shell-patterns/references/components/date-picker.md": "bc69f14829004f9d",
"packages/core/skills/app-shell-patterns/references/components/default-header.md": "375e55f4b12fa2be",
"packages/core/skills/app-shell-patterns/references/components/default-sidebar.md": "3422388a0cc78571",
"packages/core/skills/app-shell-patterns/references/components/description-card.md": "a347fd1f4ccd433e",
Expand Down
11 changes: 10 additions & 1 deletion docs-src/components/date-picker.docs.outline.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ Locale and timezone come from AppShell automatically. Override per field with `l
<DatePicker aria-label="Date" locale="ja-JP" />
```

### Day of the week

`showDayOfWeek` adds the locale's short day of the week where the locale places it — `2025/12/19(金)` for ja-JP, `Fri, 12/19/2025` for en-US. It is read-only and derived from the entered date: it shows a placeholder until the date is complete, and while a date segment is mid-entry it keeps the previous day of the week (muted), so typing `25` into the day never flashes the 2nd's. Screen readers hear the full day name with each date segment's value.

```tsx
<DatePicker aria-label="Closing date" showDayOfWeek />
```

## Keyboard

- **Segments:** `↑`/`↓` increment/decrement, digits type-to-fill (auto-advance), `←`/`→` move between segments, `Backspace` clears, `/` commits the current segment and advances.
Expand All @@ -209,6 +217,7 @@ Locale and timezone come from AppShell automatically. Override per field with `l
| `hourCycle` | `12 \| 24` | Force 12- or 24-hour time; defaults to the locale |
| `autoFocus` | `boolean` | Focus the first segment on mount |
| `locale` | `string` | BCP-47 locale override |
| `showDayOfWeek` | `boolean` | Show the locale's short day of the week next to the date |
| `name` | `string` | Emits a form value through the proxy input |
| `id` | `string` | Proxy input id (use with external `<label htmlFor>`). |
| `firstDayOfWeek` | `"sun" \| "mon" \| "tue" \| "wed" \| "thu" \| "fri" \| "sat"` | Override the locale week start used by `w` / `k` shortcuts |
Expand All @@ -231,7 +240,7 @@ See the calendar docs in-code: controlled/uncontrolled value, min/max, unavailab

### DateRangePickerProps

The `DatePickerProps` surface (labeling, `isInvalid`, `min/maxValue`, `isDateUnavailable`, `granularity`, `hourCycle`, `firstDayOfWeek`, `timeZone`, `locale`, `autoFocus`, `isDisabled/ReadOnly/Required`), with the range-specific differences:
The `DatePickerProps` surface (labeling, `isInvalid`, `min/maxValue`, `isDateUnavailable`, `granularity`, `hourCycle`, `firstDayOfWeek`, `timeZone`, `locale`, `showDayOfWeek`, `autoFocus`, `isDisabled/ReadOnly/Required`), with the range-specific differences:

| Prop | Type | Description |
| ------------------------ | -------------------------------- | ------------------------------------------------------------------------------- |
Expand Down
11 changes: 10 additions & 1 deletion docs/components/date-picker.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions examples/vite-app/src/pages/showcase/date-picker/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,26 @@ watch("period")?.start.toString(); // "2025-06-10"
</div>
</section>

{/* ── Day of the week ─────────────────────────────────────── */}
<section className="flex flex-col gap-4">
<h2 className="text-base font-semibold border-b pb-2">Day of the week</h2>
<p className="text-sm text-muted-foreground">
<code className="bg-muted px-1 py-0.5 rounded">showDayOfWeek</code> appends the
locale's short day of the week where the locale places it.
</p>
<div className="flex flex-wrap gap-6 items-start">
<DemoField id="date-picker-ja-dow" label="ja-JP">
<DatePicker locale="ja-JP" showDayOfWeek />
</DemoField>
<DemoField id="date-picker-en-dow" label="en-US">
<DatePicker locale="en-US" showDayOfWeek />
</DemoField>
<DemoField id="date-picker-ja-range-dow" label="ja-JP range">
<DateRangePicker locale="ja-JP" showDayOfWeek />
</DemoField>
</div>
</section>

{/* ── Calendar ───────────────────────────────────────────── */}
<section className="flex flex-col gap-4">
<h2 className="text-base font-semibold border-b pb-2">Calendar (standalone)</h2>
Expand Down
93 changes: 93 additions & 0 deletions packages/core/src/components/date-field/date-field.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
CalendarDate,
CalendarDateTime,
parseDate,
parseZonedDateTime,
today,
Expand Down Expand Up @@ -835,6 +836,98 @@ describe("DateField keyboard shortcuts", () => {
});
});

// ─── Date format + day of the week ────────────────────────────────────────────────────

const groupText = () => screen.getByRole("group").textContent;
const dayOfWeek = () => document.querySelector('[data-slot="date-segment"][data-type="dayOfWeek"]');

describe("showDayOfWeek", () => {
const value = new CalendarDate(2025, 12, 19); // a Friday

it("shows the day of the week where the locale puts it", () => {
render(<DateField aria-label="Date" locale="ja-JP" showDayOfWeek defaultValue={value} />);
expect(groupText()).toBe("2025/12/19(金)");
cleanup();
render(<DateField aria-label="Date" locale="en-US" showDayOfWeek defaultValue={value} />);
expect(groupText()).toBe("Fri, 12/19/2025");
});

it("shows a placeholder day of the week until the date is complete", () => {
render(<DateField aria-label="Date" locale="en-US" showDayOfWeek />);
expect(dayOfWeek()?.textContent).toBe("––");
expect(dayOfWeek()?.hasAttribute("data-placeholder")).toBe(true);
});

it("holds the pre-entry day of the week while a date segment is mid-entry, so '25' never flashes the 2nd", async () => {
const user = userEvent.setup();
render(<DateField aria-label="Date" locale="en-US" showDayOfWeek defaultValue={value} />);

await user.click(screen.getByRole("spinbutton", { name: "day" }));
await user.keyboard("2");
// Still Friday (the pre-entry 19th), muted — never Tuesday (the 2nd).
expect(dayOfWeek()?.textContent).toBe("Fri");
expect(dayOfWeek()?.hasAttribute("data-placeholder")).toBe(true);
await user.keyboard("5");
expect(dayOfWeek()?.textContent).toBe("Thu"); // 25 Dec 2025
expect(dayOfWeek()?.hasAttribute("data-placeholder")).toBe(false);
});

it("never holds the day of the week while editing a time segment", async () => {
const user = userEvent.setup();
render(
<DateField
aria-label="Date"
locale="en-GB"
granularity="minute"
showDayOfWeek
defaultValue={new CalendarDateTime(2025, 12, 19, 10, 30)}
/>,
);

await user.click(screen.getByRole("spinbutton", { name: "hour" }));
await user.keyboard("1");
expect(dayOfWeek()?.textContent).toBe("Fri");
expect(dayOfWeek()?.hasAttribute("data-placeholder")).toBe(false);
});

it("announces the day of the week with the date segments' values", async () => {
const user = userEvent.setup();
render(<DateField aria-label="Date" locale="en-US" showDayOfWeek defaultValue={value} />);
const day = screen.getByRole("spinbutton", { name: "day" });
expect(day.getAttribute("aria-valuetext")).toBe("19, Friday");
expect(dayOfWeek()?.getAttribute("aria-hidden")).toBe("true");

await user.click(day);
await user.keyboard("{ArrowUp}");
expect(day.getAttribute("aria-valuetext")).toBe("20, Saturday");
});

it("settles a single-digit entry when the segment loses focus", async () => {
const user = userEvent.setup();
render(<DateField aria-label="Date" locale="en-US" showDayOfWeek defaultValue={value} />);

await user.click(screen.getByRole("spinbutton", { name: "day" }));
await user.keyboard("2");
await user.tab();
expect(dayOfWeek()?.textContent).toBe("Tue"); // 2 Dec 2025
});

it("updates the day of the week immediately on arrow-key stepping", async () => {
const user = userEvent.setup();
render(<DateField aria-label="Date" locale="en-US" showDayOfWeek defaultValue={value} />);

await user.click(screen.getByRole("spinbutton", { name: "day" }));
await user.keyboard("{ArrowUp}");
expect(dayOfWeek()?.textContent).toBe("Sat");
});

it("keeps the day of the week out of the tab order", () => {
render(<DateField aria-label="Date" locale="en-US" showDayOfWeek defaultValue={value} />);
expect(dayOfWeek()?.hasAttribute("tabindex")).toBe(false);
expect(screen.getAllByRole("spinbutton")).toHaveLength(3);
});
});

// ─── DatePicker ───────────────────────────────────────────────────────────────

describe("DatePicker", () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/components/date-field/date-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ interface DateControlProps<T extends DateValue> {
"aria-describedby"?: string;
/** BCP-47 locale override; defaults to the AppShell formatting locale. */
locale?: string;
/** Show the locale's short day of the week next to the date (e.g. `(金)`, `Fri,`). */
showDayOfWeek?: boolean;
}

export type DateFieldProps<T extends DateValue = DateValue> = DateControlProps<T>;
Expand Down Expand Up @@ -103,6 +105,7 @@ const DateField = forwardRef(function DateField<T extends DateValue = DateValue>
placeholderValue,
autoFocus,
firstDayOfWeek,
showDayOfWeek,
name,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
Expand Down Expand Up @@ -132,6 +135,7 @@ const DateField = forwardRef(function DateField<T extends DateValue = DateValue>
maxValue,
isDateUnavailable,
firstDayOfWeek,
showDayOfWeek,
isReadOnly: resolvedReadOnly,
});

Expand Down Expand Up @@ -181,6 +185,7 @@ const DateField = forwardRef(function DateField<T extends DateValue = DateValue>
setDayPeriod={state.setDayPeriod}
clearSegment={state.clearSegment}
applyShortcut={state.applyShortcut}
settleEntry={state.settleEntry}
commitOnBlur={state.commitOnBlur}
expandShortYear={state.expandShortYear}
isDisabled={bindings.isDisabled}
Expand Down Expand Up @@ -229,6 +234,7 @@ const DatePicker = forwardRef(function DatePicker<T extends DateValue = DateValu
placeholderValue,
autoFocus,
firstDayOfWeek,
showDayOfWeek,
name,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
Expand Down Expand Up @@ -269,6 +275,7 @@ const DatePicker = forwardRef(function DatePicker<T extends DateValue = DateValu
maxValue,
isDateUnavailable,
firstDayOfWeek,
showDayOfWeek,
isReadOnly: resolvedReadOnly,
});

Expand Down Expand Up @@ -384,6 +391,7 @@ const DatePicker = forwardRef(function DatePicker<T extends DateValue = DateValu
setDayPeriod={fieldState.setDayPeriod}
clearSegment={fieldState.clearSegment}
applyShortcut={fieldState.applyShortcut}
settleEntry={fieldState.settleEntry}
commitOnBlur={fieldState.commitOnBlur}
expandShortYear={fieldState.expandShortYear}
onOpenCalendar={() => handleOpenChange(true)}
Expand Down
Loading
Loading