Skip to content

Commit 9c518bb

Browse files
committed
fix: ensure dashboard card text has accessible contrast
1 parent f1cffb2 commit 9c518bb

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

‎lib/theme.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const FONT_FAMILY_VARIABLE_BY_THEME_FONT = {
2828

2929
const LIGHT_BRAND_FOREGROUND = "#FFFFFF";
3030
const DARK_BRAND_FOREGROUND = "#000000";
31+
const FIXED_CARD_SURFACE = "#FFFFFF";
3132

3233
export const DEFAULT_APP_THEME = {
3334
primaryColor: "#FC5B43",
@@ -156,6 +157,17 @@ export function getAccessibleBrandForeground(brandColor: string): string {
156157
: DARK_BRAND_FOREGROUND;
157158
}
158159

160+
function getAccessibleSurfaceText(
161+
preferredTextColor: string,
162+
surfaceColor: string,
163+
): string {
164+
if (getContrastRatio(preferredTextColor, surfaceColor) >= 4.5) {
165+
return preferredTextColor;
166+
}
167+
168+
return getAccessibleBrandForeground(surfaceColor);
169+
}
170+
159171
function resolveColor(value: string | undefined, fallback: string): string {
160172
return value && isHexColor(value) ? value : fallback;
161173
}
@@ -246,7 +258,13 @@ export function getOrganizationThemeCssVariables(
246258
// organization's page colors without changing an unconfigured org's
247259
// current appearance.
248260
variables["--color-app-shell-bg"] = theme.backgroundColor;
249-
variables["--color-app-strong-text"] = theme.textColor;
261+
// Dashboard cards keep their existing white surface. Use the configured
262+
// text color when it is readable there, otherwise choose an AA-safe
263+
// foreground so light-on-dark page themes do not become white-on-white.
264+
variables["--color-app-strong-text"] = getAccessibleSurfaceText(
265+
theme.textColor,
266+
FIXED_CARD_SURFACE,
267+
);
250268
variables["--color-navbar-bg"] = theme.backgroundColor;
251269
variables["--color-mobile-navbar-bg"] = theme.backgroundColor;
252270
variables["--color-media-page-bg"] = theme.backgroundColor;

‎tests/e2e/organization-theme.spec.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,40 @@ test.describe("Organization theme", () => {
161161
await expect(saveButton).toHaveCSS("background-color", "rgb(252, 91, 67)");
162162
await expect(saveButton).toHaveCSS("border-radius", "2.5px");
163163
});
164+
165+
test("keeps fixed white dashboard cards readable with a dark page theme", async ({
166+
page,
167+
}) => {
168+
const slug = `dark-theme-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
169+
const { org, baseUrl } = await createThemedAdminSession(page, slug);
170+
171+
await OrganizationConfigService.setConfig(
172+
org.id,
173+
OrganizationConfigKey.BackgroundColor,
174+
"#111111",
175+
);
176+
await OrganizationConfigService.setConfig(
177+
org.id,
178+
OrganizationConfigKey.TextColor,
179+
"#FFFFFF",
180+
);
181+
182+
await page.goto(baseUrl);
183+
184+
const pageHeading = page.getByRole("heading", {
185+
name: "Admin Dashboard",
186+
level: 1,
187+
});
188+
await expect(pageHeading).toHaveCSS("color", "rgb(255, 255, 255)");
189+
190+
const cardHeading = page.getByRole("heading", {
191+
name: "Events",
192+
level: 3,
193+
});
194+
await expect(cardHeading).toHaveCSS("color", "rgb(0, 0, 0)");
195+
await expect(cardHeading.locator("..")).toHaveCSS(
196+
"background-color",
197+
"rgb(255, 255, 255)",
198+
);
199+
});
164200
});

‎tests/unit/lib/theme.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,17 @@ describe("getOrganizationThemeCssVariables", () => {
134134
"color-mix(in oklch, #5C218C 40%, #FFFEF1)",
135135
);
136136
});
137+
138+
it("keeps fixed white dashboard cards readable for a dark page theme", () => {
139+
const variables = getOrganizationThemeCssVariables({
140+
[OrganizationConfigKey.BackgroundColor]: "#111111",
141+
[OrganizationConfigKey.TextColor]: "#FFFFFF",
142+
});
143+
144+
expect(variables["--color-page-text"]).toBe("#FFFFFF");
145+
expect(variables["--color-app-strong-text"]).toBe("#000000");
146+
expect(
147+
getContrastRatio(variables["--color-app-strong-text"], "#FFFFFF"),
148+
).toBeGreaterThanOrEqual(4.5);
149+
});
137150
});

0 commit comments

Comments
 (0)