fix(date-picker): improve calendar accessibility

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-24 18:00:20 +08:00
parent 397ef7ace9
commit 3c172c411e
3 changed files with 25 additions and 15 deletions
@@ -3,6 +3,12 @@ import { describe, expect, it, vi } from "vitest";
import { DatePicker } from "./date-picker";
function getCalendarDayButtons(dialogName: string) {
return within(screen.getByRole("dialog", { name: dialogName }))
.getAllByRole("button")
.filter((button) => button.getAttribute("data-slot") === "day");
}
describe("DatePicker", () => {
it("renders a placeholder and selects a date in uncontrolled mode", async () => {
render(
@@ -16,8 +22,7 @@ describe("DatePicker", () => {
const field = screen.getByRole("combobox", { name: "Launch date" });
expect(field.closest('[data-slot="root"]')).toHaveAttribute("data-placeholder", "");
const calendar = screen.getByRole("grid");
const dayButton = within(calendar).getAllByRole("gridcell")[10];
const dayButton = getCalendarDayButtons("Launch date calendar")[10];
fireEvent.click(dayButton);
@@ -38,7 +43,7 @@ describe("DatePicker", () => {
);
fireEvent.click(
screen.getByRole("gridcell", {
screen.getByRole("button", {
name: /Apr 20, 2026|20 Apr 2026|Apr 20 2026/i
})
);
@@ -96,7 +101,7 @@ describe("DatePicker", () => {
/>
);
const firstDay = within(screen.getByRole("grid")).getAllByRole("gridcell")[0];
const firstDay = getCalendarDayButtons("Monday-first date calendar")[0];
expect(firstDay).toHaveAccessibleName("Feb 23, 2026");
expect(screen.getByText("Mon")).toBeInTheDocument();
@@ -113,7 +118,7 @@ describe("DatePicker", () => {
/>
);
const firstDay = within(screen.getByRole("grid")).getAllByRole("gridcell")[0];
const firstDay = getCalendarDayButtons("Sunday-first date calendar")[0];
expect(firstDay).toHaveAccessibleName("Mar 1, 2026");
expect(screen.getByText("Sun")).toBeInTheDocument();
@@ -130,9 +135,9 @@ describe("DatePicker", () => {
/>
);
const disabledDays = screen
.getAllByRole("gridcell")
.filter((cell) => cell.hasAttribute("data-disabled"));
const disabledDays = getCalendarDayButtons("Guardrailed date calendar").filter((cell) =>
cell.hasAttribute("data-disabled")
);
expect(disabledDays.length).toBeGreaterThan(0);
});
+9 -4
View File
@@ -423,7 +423,13 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
</div>
</PopoverAnchor>
<PopoverContent className={datePickerContentVariants()} id={popupId} padding="sm" size="xl">
<PopoverContent
aria-label={`${props["aria-label"] ?? "Date picker"} calendar`}
className={datePickerContentVariants()}
id={popupId}
padding="sm"
size="xl"
>
<div className="grid gap-3">
<div {...createSlot("header")} className={datePickerHeaderVariants()}>
<div className={datePickerNavigationVariants()}>
@@ -510,7 +516,7 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
))}
</div>
<div className={datePickerGridVariants()} role="grid">
<div className={datePickerGridVariants()}>
{days.map((day, index) => {
const outside = day.getMonth() !== visibleMonth.getMonth();
const selected = sameDay(day, selectedDate);
@@ -528,7 +534,6 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
today: isToday
})}
aria-label={formatValue(day, locale)}
aria-pressed={selected}
className={datePickerDayVariants()}
disabled={dayDisabled}
onClick={() => {
@@ -539,7 +544,7 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
ref={(node) => {
dayRefs.current[index] = node;
}}
role="gridcell"
style={selected ? undefined : { color: "var(--color-foreground)" }}
type="button"
>
{day.getDate()}
@@ -37,12 +37,12 @@ export const datePickerGridVariants = cva("grid grid-cols-7 gap-1");
export const datePickerDayVariants = cva(
[
"inline-flex h-9 items-center justify-center rounded-[var(--ui-control-radius)] text-sm font-medium outline-none",
"inline-flex h-9 items-center justify-center rounded-[var(--ui-control-radius)] text-sm font-medium text-[var(--color-foreground)] outline-none",
"transition-[background-color,color,box-shadow,transform] duration-[var(--dur-fast)] ease-[var(--ease-standard)]",
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ui-panel-bg)]",
"data-[outside=true]:text-[color-mix(in_oklch,var(--color-muted-foreground)_78%,transparent)]",
"data-[outside=true]:text-[var(--color-foreground)]",
"data-[today=true]:shadow-[inset_0_0_0_1px_color-mix(in_oklch,var(--color-primary)_26%,transparent)]",
"data-[disabled=true]:pointer-events-none opacity-35",
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-35",
"data-[selected=true]:bg-[var(--color-primary)] data-[selected=true]:text-[var(--color-primary-foreground)] data-[selected=true]:shadow-[var(--ui-control-shadow)]",
"hover:bg-[var(--ui-control-bg)] hover:text-[var(--color-foreground)]",
getMotionRecipeClassNames("ring")