fix(a11y): make the form error color meet WCAG AA in both schemes
Mantine's `--mantine-color-error` defaults to `red-6` in light and `red-8` in dark. Both fail the 4.5:1 AA threshold that applies at the 12px size it renders input error text: 4.48:1 on white, and 1.93:1 on our charcoal body. Override to `red-7` (5.72:1) and `red-4` (6.44:1) — the latter matching the one-to-two-step lift App.css already applies to the semantic status/log reds for dark. Surfaced by the JsonObjectInput stories, which are the first in the repo to render an input error state and so the first to put this in front of the Storybook a11y gate. It is app-wide, not specific to that component — every form error in the Inspector was affected. It has to go through `cssVariablesResolver`: MantineProvider injects these variables into a `<style>` block appended after the stylesheet imports, so a `:root` rule in App.css loses to it on source order at equal specificity. That resolver was duplicated in main.tsx and .storybook/preview.tsx and missing from renderWithMantine, so the three could drift on any token they set. Extract one `theme/cssVariables.ts` and use it at all three sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
3a64ce7f4e
commit
04b5e70a53
5 changed files with 48 additions and 27 deletions
|
|
@ -1,14 +1,11 @@
|
|||
import type { Preview } from "@storybook/react-vite";
|
||||
import {
|
||||
MantineProvider,
|
||||
useMantineColorScheme,
|
||||
type CSSVariablesResolver,
|
||||
} from "@mantine/core";
|
||||
import { MantineProvider, useMantineColorScheme } from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
import "../src/App.css";
|
||||
import { theme } from "../src/theme/theme";
|
||||
import { cssVariablesResolver } from "../src/theme/cssVariables";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
|
|
@ -28,14 +25,6 @@ function ColorSchemeWrapper({
|
|||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const resolver: CSSVariablesResolver = () => ({
|
||||
variables: {},
|
||||
light: {},
|
||||
dark: {
|
||||
"--mantine-color-body": "var(--mantine-color-dark-9)",
|
||||
},
|
||||
});
|
||||
|
||||
const preview: Preview = {
|
||||
globalTypes: {
|
||||
colorScheme: {
|
||||
|
|
@ -61,7 +50,7 @@ const preview: Preview = {
|
|||
<MantineProvider
|
||||
theme={theme}
|
||||
defaultColorScheme="light"
|
||||
cssVariablesResolver={resolver}
|
||||
cssVariablesResolver={cssVariablesResolver}
|
||||
>
|
||||
<ColorSchemeWrapper
|
||||
colorScheme={context.globals.colorScheme ?? "light"}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,20 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { MantineProvider, type CSSVariablesResolver } from "@mantine/core";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
import "./App.css";
|
||||
import { theme } from "./theme/theme";
|
||||
import { cssVariablesResolver } from "./theme/cssVariables";
|
||||
import App from "./App";
|
||||
|
||||
const resolver: CSSVariablesResolver = () => ({
|
||||
variables: {},
|
||||
light: {},
|
||||
dark: {
|
||||
// Matches `.storybook/preview.tsx` so the dev app and the story preview
|
||||
// share the same dark-mode page background.
|
||||
"--mantine-color-body": "var(--mantine-color-dark-9)",
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<MantineProvider
|
||||
theme={theme}
|
||||
defaultColorScheme="auto"
|
||||
cssVariablesResolver={resolver}
|
||||
cssVariablesResolver={cssVariablesResolver}
|
||||
>
|
||||
<Notifications position="bottom-right" />
|
||||
<App />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { act, render, type RenderOptions } from "@testing-library/react";
|
|||
import { MantineProvider, type MantineColorScheme } from "@mantine/core";
|
||||
import { afterEach, expect, vi } from "vitest";
|
||||
import { theme } from "../theme/theme";
|
||||
import { cssVariablesResolver } from "../theme/cssVariables";
|
||||
|
||||
// Options accepted by both render helpers: the standard RTL options (minus
|
||||
// `wrapper`, which we own) plus an optional forced `colorScheme`. The default
|
||||
|
|
@ -30,7 +31,12 @@ export type MantineTransitionsRenderOptions = MantineRenderOptions & {
|
|||
function makeWrapper(env: "test" | "default", colorScheme: MantineColorScheme) {
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<MantineProvider theme={theme} defaultColorScheme={colorScheme} env={env}>
|
||||
<MantineProvider
|
||||
theme={theme}
|
||||
defaultColorScheme={colorScheme}
|
||||
env={env}
|
||||
cssVariablesResolver={cssVariablesResolver}
|
||||
>
|
||||
{children}
|
||||
</MantineProvider>
|
||||
);
|
||||
|
|
|
|||
34
clients/web/src/theme/cssVariables.ts
Normal file
34
clients/web/src/theme/cssVariables.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { CSSVariablesResolver } from "@mantine/core";
|
||||
|
||||
/**
|
||||
* Overrides for the Mantine CSS variables that `MantineProvider` injects at
|
||||
* runtime.
|
||||
*
|
||||
* These cannot be set from `App.css`: the provider appends its generated
|
||||
* `<style>` block after the stylesheet imports, so a `:root` rule there loses
|
||||
* to it on source order at equal specificity. `cssVariablesResolver` is the
|
||||
* supported seam, which is why this lives in the theme layer beside the
|
||||
* `Component.extend()` variants rather than with the `--inspector-*` tokens.
|
||||
*
|
||||
* Shared by all three `MantineProvider` sites — the app (`main.tsx`), the
|
||||
* Storybook preview, and `renderWithMantine` — so the dev app, the story
|
||||
* preview, and the tests cannot disagree about a token's value. It used to be
|
||||
* duplicated in the first two and absent from the third.
|
||||
*/
|
||||
export const cssVariablesResolver: CSSVariablesResolver = () => ({
|
||||
variables: {},
|
||||
light: {
|
||||
// Mantine defaults `--mantine-color-error` to `red-6`, which is 4.48:1 on
|
||||
// white — under the 4.5:1 WCAG AA threshold that applies at the 12px size
|
||||
// it renders input error text. `red-7` is 5.72:1.
|
||||
"--mantine-color-error": "var(--mantine-color-red-7)",
|
||||
},
|
||||
dark: {
|
||||
"--mantine-color-body": "var(--mantine-color-dark-9)",
|
||||
// The dark default is `red-8`, which is 1.93:1 on this body color — far
|
||||
// worse than the light-mode miss. `red-4` is 6.44:1, and is the same
|
||||
// one-to-two-step lift `App.css` already applies to the semantic
|
||||
// status/log reds for dark.
|
||||
"--mantine-color-error": "var(--mantine-color-red-4)",
|
||||
},
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { cssVariablesResolver } from "./cssVariables";
|
||||
export { ThemeAccordion } from "./Accordion";
|
||||
export { ThemeActionIcon } from "./ActionIcon";
|
||||
export { ThemeAlert } from "./Alert";
|
||||
|
|
|
|||
Loading…
Reference in a new issue