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
67 changes: 43 additions & 24 deletions e2e/accessibility/datepicker-a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,51 @@ import {

const MIN_NORMAL_TEXT_CONTRAST = 4.5;

test("sidebar datepicker meets baseline accessibility and contrast checks", async ({
page,
}) => {
await prepareCalendarPage(page);
await ensureSidebarOpen(page);

const sidebar = page.locator("#sidebar");
const monthPicker = sidebar.getByRole("group", { name: "Date navigation" });
await expect(monthPicker).toBeVisible();

// Scoped to #sidebar (not a whole-page scan): this test owns the
// datepicker's targeted contrast regression below, and scoping keeps that
// pairing - the axe pass and the per-date contrast check - about the same
// element on every run.
await expectNoAxeViolations(page, {
include: "#sidebar",
checkpoint: "sidebar datepicker",
});
const THEMES = [
{ name: "light", theme: "light-beach", stored: null },
{ name: "dark", theme: "dark-abyss", stored: "dark-abyss" },
] as const;

test.describe("sidebar datepicker", () => {
for (const { name, theme, stored } of THEMES) {
test(`${name} theme meets baseline accessibility and contrast checks`, async ({
page,
}) => {
if (stored) {
await page.addInitScript((value) => {
localStorage.setItem("compass.theme", value);
}, stored);
}

await prepareCalendarPage(page);
await ensureSidebarOpen(page);

await expect(page.locator("html")).toHaveAttribute("data-theme", theme);

const days = monthPicker.locator(
".react-datepicker__day:not(.react-datepicker__day--disabled)",
);
await expect(days.first()).toBeVisible();
const sidebar = page.locator("#sidebar");
const monthPicker = sidebar.getByRole("group", {
name: "Date navigation",
});
await expect(monthPicker).toBeVisible();

await expectDateContrast(days, "default");
await expectDateContrast(days, "hover");
// Scoped to #sidebar (not a whole-page scan): this test owns the
// datepicker's targeted contrast regression below, and scoping keeps that
// pairing - the axe pass and the per-date contrast check - about the same
// element on every run.
await expectNoAxeViolations(page, {
include: "#sidebar",
checkpoint: `sidebar datepicker ${name}`,
});

const days = monthPicker.locator(
".react-datepicker__day:not(.react-datepicker__day--disabled)",
);
await expect(days.first()).toBeVisible();

await expectDateContrast(days, "default");
await expectDateContrast(days, "hover");
});
}
});

const expectDateContrast = async (
Expand Down
12 changes: 12 additions & 0 deletions packages/web/src/common/styles/theme-css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,16 @@ describe("Tailwind theme CSS", () => {
expect(match?.[1]?.toLowerCase()).toBe(hex.toLowerCase());
}
});

it("paints sidebar month-picker days with theme text, not inherited black", () => {
// react-datepicker sets color: #000 on .react-datepicker. A sidebar
// `color: inherit` picked that up and failed WCAG on Dark Abyss.
expect(indexCss).toContain(".c-month-picker .c-date-picker");
expect(indexCss).toMatch(
/\.c-month-picker \.c-date-picker[\s\S]*& \.react-datepicker__day \{\s*color: var\(--text\);/,
);
expect(indexCss).not.toMatch(
/data-view="sidebar"\] \.react-datepicker__day[\s\S]{0,120}color:\s*inherit/,
);
});
});
4 changes: 2 additions & 2 deletions packages/web/src/components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const DatePicker: React.FC<Props> = (datePickerProps) => {
// When the picker bg has the same polarity as the theme's surfaces, the
// theme's standard --text already contrasts with it; a mismatched bg (e.g.
// the light event-fill picker on the dark theme) needs --on-accent, the
// token that flips polarity. The CSS keys off this via [data-dark].
// token that flips polarity. The CSS keys off this via data-dark="true"|"false".
const usesThemeText = isDarkBackground === isDarkTheme;
const headerColor =
view === "sidebar"
Expand All @@ -143,7 +143,7 @@ export const DatePicker: React.FC<Props> = (datePickerProps) => {
<div
ref={calendarRef}
className={classNames("c-date-picker", className)}
data-dark={usesThemeText}
data-dark={usesThemeText ? "true" : "false"}
data-view={view}
style={datePickerStyle}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ describe("MonthPicker", () => {
).toHaveAttribute("data-pointer-action", POINTER_ACTIONS.datePick);
});

it("marks the calendar as using theme text so day numbers follow --text", () => {
renderPicker(<MonthPicker onSelectDate={mock()} {...pickerProps} />);

expect(document.querySelector(".c-date-picker")).toHaveAttribute(
"data-dark",
"true",
);
});

it("moves a week at a time with every arrow key and opens the week with Enter", async () => {
const user = userEvent.setup({ skipHover: true });
const onSelectDate = mock();
Expand Down
38 changes: 29 additions & 9 deletions packages/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -767,9 +767,15 @@
/*
* Keep this unlayered so it can override react-datepicker's unlayered CSS.
* Rules inside a Tailwind @utility lose to unlayered library defaults.
* @apply lives in its own rule: Tailwind can layer apply-expanded
* declarations, and nesting day colors in that same rule would lose to
* react-datepicker's unlayered `color: #000`.
*/
.c-date-picker {
@apply select-none rounded-xs border-0 shadow-[0_4px_4px_var(--color-shadow-default)];
}

.c-date-picker {
background-color: var(--date-picker-bg);
font-size: 11px;
--date-picker-hover-bg: color-mix(
Expand Down Expand Up @@ -842,13 +848,13 @@

& .react-datepicker__day-name {
margin: 0;
color: var(--on-accent);
color: var(--text-muted);
font-size: 11px;
opacity: 0.8;
}

&[data-dark="true"] .react-datepicker__day-name {
color: var(--text);
&[data-dark="false"] .react-datepicker__day-name {
color: var(--on-accent);
opacity: 0.8;
}

&[data-view="sidebar"] .react-datepicker__day-name {
Expand All @@ -861,12 +867,12 @@
margin: 0;
border: 0;
border-radius: 50%;
color: var(--on-accent);
color: var(--text);
line-height: 1.5rem;
}

&[data-dark="true"] .react-datepicker__day {
color: var(--text);
&[data-dark="false"] .react-datepicker__day {
color: var(--on-accent);
}

&[data-view="sidebar"] .react-datepicker__day {
Expand All @@ -885,11 +891,13 @@
outline-offset: 1px;
}

/* Sidebar days are keyboard targets, not click targets: no hover affordance. */
/* Sidebar days are keyboard targets, not click targets: no hover
affordance. Use --text (not inherit): react-datepicker sets `color: #000`
on .react-datepicker, so inherit painted black numbers on Dark Abyss. */
&[data-view="sidebar"] .react-datepicker__day,
&[data-view="sidebar"] .react-datepicker__day:hover {
background-color: transparent;
color: inherit;
color: var(--text);
cursor: default;
}

Expand Down Expand Up @@ -1213,6 +1221,18 @@
& .react-datepicker__month {
overflow: visible;
}
& .react-datepicker__day-name {
color: var(--text-muted);
}
& .react-datepicker__day {
color: var(--text);
}
& .react-datepicker__day--today:not(.react-datepicker__day--selected) {
color: var(--accent);
}
& .react-datepicker__day--outside-month {
color: var(--text-muted);
}
& .react-datepicker__day--selected {
position: relative;
isolation: isolate;
Expand Down