From 829ddc0ce8e608932d75634db9c0ce9e09f6fcfd Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 1 Oct 2026 10:43:23 -0700 Subject: [PATCH 1/2] fix(react-ui): disabled fields use the disabled-field tokens, like the CMS TextInput, TextareaInput and SelectInput now use the CMS's `--color--disabled-field--*` tokens when disabled, with fallbacks to `--color--disabled--*` for hosts that don't send them. This fixes the ~1.9:1 value contrast in dark mode and dims the placeholder of an empty disabled field. Disabled text fields no longer change border on hover. SelectInput's theme now maps every react-select palette slot the way the CMS's useThemeProps does, so option hover uses surface-raised-hover and nothing falls back to react-select's light-only greys. The theme and styles move to SelectInput/theme.ts so unit tests can cover them. Co-Authored-By: Claude Opus 5.5 --- .changeset/disabled-field-colors.md | 5 + .../react-ui/__tests__/fieldColors.test.ts | 222 ++++++++++++++++++ packages/react-ui/src/Canvas/index.tsx | 21 +- packages/react-ui/src/SelectInput/index.tsx | 137 +---------- packages/react-ui/src/SelectInput/theme.ts | 166 +++++++++++++ .../react-ui/src/TextInput/styles.module.css | 20 +- .../src/TextareaInput/styles.module.css | 20 +- 7 files changed, 452 insertions(+), 139 deletions(-) create mode 100644 .changeset/disabled-field-colors.md create mode 100644 packages/react-ui/__tests__/fieldColors.test.ts create mode 100644 packages/react-ui/src/SelectInput/theme.ts diff --git a/.changeset/disabled-field-colors.md b/.changeset/disabled-field-colors.md new file mode 100644 index 0000000..15cafc2 --- /dev/null +++ b/.changeset/disabled-field-colors.md @@ -0,0 +1,5 @@ +--- +"datocms-react-ui": patch +--- + +Disabled `TextInput`, `TextareaInput` and `SelectInput` fields now match native DatoCMS fields. They use the new `--color--disabled-field--*` tokens, which fixes the low-contrast value text in dark mode. An empty disabled field shows a dimmed placeholder, and a disabled text field no longer reacts to hover. `SelectInput` also maps every react-select palette slot to a DatoCMS token, so menus no longer fall back to light-only greys. On older DatoCMS versions that don't send the new tokens, fields fall back to the previous `--color--disabled--*` colors. diff --git a/packages/react-ui/__tests__/fieldColors.test.ts b/packages/react-ui/__tests__/fieldColors.test.ts new file mode 100644 index 0000000..4149426 --- /dev/null +++ b/packages/react-ui/__tests__/fieldColors.test.ts @@ -0,0 +1,222 @@ +// The repo's @types/node predates the `node:` specifiers +// biome-ignore lint/style/useNodejsImportProtocol: see above +import { readFileSync } from 'fs'; +// biome-ignore lint/style/useNodejsImportProtocol: see above +import { fileURLToPath } from 'url'; +import postcss, { type Rule } from 'postcss'; +import postcssNested from 'postcss-nested'; +import type { CSSObjectWithLabel, Theme } from 'react-select'; +import { buildStyles, themeConfig } from '../src/SelectInput/theme'; + +const disabledFieldSurface = + 'var(--color--disabled-field--surface, var(--color--disabled--surface))'; +const disabledFieldInk = + 'var(--color--disabled-field--ink, var(--color--disabled--ink))'; +const disabledFieldInkPlaceholder = + 'var(--color--disabled-field--ink-placeholder, var(--color--ink-placeholder))'; + +/** react-select's own default theme, as passed to a `ThemeConfig` function. */ +const reactSelectDefaultTheme: Theme = { + borderRadius: 4, + colors: { + primary: '#2684FF', + primary75: '#4C9AFF', + primary50: '#B2D4FF', + primary25: '#DEEBFF', + danger: '#DE350B', + dangerLight: '#FFBDAD', + neutral0: 'hsl(0, 0%, 100%)', + neutral5: 'hsl(0, 0%, 95%)', + neutral10: 'hsl(0, 0%, 90%)', + neutral20: 'hsl(0, 0%, 80%)', + neutral30: 'hsl(0, 0%, 70%)', + neutral40: 'hsl(0, 0%, 60%)', + neutral50: 'hsl(0, 0%, 50%)', + neutral60: 'hsl(0, 0%, 40%)', + neutral70: 'hsl(0, 0%, 30%)', + neutral80: 'hsl(0, 0%, 20%)', + neutral90: 'hsl(0, 0%, 10%)', + }, + spacing: { baseUnit: 4, controlHeight: 38, menuGutter: 8 }, +}; + +// The style functions only read the state flags below; the rest of +// react-select's state object is irrelevant to the colors under test. +const callStyle = (key: string, state: Record, styles: any) => + styles[key]({} as CSSObjectWithLabel, state) as Record; + +describe('SelectInput themeConfig', () => { + const { colors, borderRadius } = themeConfig(reactSelectDefaultTheme); + + it('maps every palette slot react-select reads to a DatoCMS token, like the CMS', () => { + expect(borderRadius).toBe(0); + expect(colors).toMatchObject({ + neutral0: 'var(--color--surface-raised)', + neutral10: disabledFieldSurface, + neutral20: 'var(--color--border)', + neutral30: 'var(--color--border-hover)', + neutral40: 'var(--color--ink-disabled)', + neutral80: 'var(--color--ink)', + primary: 'var(--color--focus--border)', + primary25: 'var(--color--surface-raised-hover)', + }); + }); + + it('leaves no slot it maps on a hardcoded react-select grey', () => { + for (const slot of [ + 'neutral0', + 'neutral10', + 'neutral40', + 'neutral80', + ] as const) { + expect(colors[slot]).toMatch(/^var\(--color--/); + } + }); +}); + +describe('SelectInput styles', () => { + const permutations = [false, true].flatMap((isDisabled) => + [false, true].flatMap((error) => + [false, true].map((isFocused) => ({ isDisabled, error, isFocused })), + ), + ); + + it.each(permutations)( + 'control: disabled=$isDisabled error=$error focused=$isFocused', + ({ isDisabled, error, isFocused }) => { + const control = callStyle( + 'control', + { isFocused, isDisabled }, + buildStyles(isDisabled, error), + ); + + expect(control.backgroundColor).toBe( + isDisabled ? disabledFieldSurface : 'var(--color--surface-raised)', + ); + expect(control.borderColor).toBe( + error + ? 'var(--color--danger-soft--border)' + : isFocused + ? 'var(--color--focus--border)' + : 'var(--color--border)', + ); + expect(control.boxShadow).toBe( + isFocused + ? `0 0 0 4px var(--color--${ + error ? 'danger-soft' : 'focus' + }--outline)` + : undefined, + ); + expect(control.minHeight).toBe(40); + }, + ); + + it.each([false, true])('placeholder: disabled=%s', (isDisabled) => { + const placeholder = callStyle('placeholder', { isDisabled }, buildStyles()); + + expect(placeholder.color).toBe( + isDisabled + ? disabledFieldInkPlaceholder + : 'var(--color--ink-placeholder)', + ); + }); + + it.each([false, true])('singleValue: disabled=%s', (isDisabled) => { + const singleValue = callStyle('singleValue', { isDisabled }, buildStyles()); + + expect(singleValue.color).toBe( + isDisabled ? disabledFieldInk : 'var(--color--ink)', + ); + }); + + it.each([ + { isSelected: false, isFocused: false, expected: undefined }, + { + isSelected: false, + isFocused: true, + expected: 'var(--color--surface-raised-hover)', + }, + { + isSelected: true, + isFocused: false, + expected: 'var(--color--selected--surface)', + }, + { + isSelected: true, + isFocused: true, + expected: 'var(--color--selected--surface)', + }, + ])( + 'option: selected=$isSelected focused=$isFocused', + ({ isSelected, isFocused, expected }) => { + const option = callStyle( + 'option', + { isSelected, isFocused }, + buildStyles(), + ); + + expect(option.backgroundColor).toBe(expected); + expect(option.color).toBe('var(--color--ink)'); + }, + ); +}); + +/** Flattens a CSS module's nesting and returns its declarations by selector. */ +const declarationsBySelector = (component: string) => { + const source = readFileSync( + fileURLToPath( + new URL(`../src/${component}/styles.module.css`, import.meta.url).href, + ), + 'utf8', + ); + const root = postcss([postcssNested]).process(source, { + from: undefined, + }).root; + const result: Record> = {}; + + root.walkRules((rule: Rule) => { + result[rule.selector] ??= {}; + rule.walkDecls((decl) => { + result[rule.selector][decl.prop] = decl.value.replace(/\s+/g, ' '); + }); + }); + + return result; +}; + +/** Collapses `var( a, b )` whitespace the formatter adds across lines. */ +const normalize = (value: string) => + value.replace(/\(\s+/g, '(').replace(/\s+\)/g, ')'); + +describe.each(['TextInput', 'TextareaInput'])('%s disabled colors', (name) => { + const rules = declarationsBySelector(name); + const disabled = rules[`.${name}--disabled`]; + + it('uses the disabled-field surface and ink, with fallbacks', () => { + expect(normalize(disabled.background)).toBe(disabledFieldSurface); + expect(normalize(disabled.color)).toBe(disabledFieldInk); + expect(disabled['border-color']).toBe('var(--color--border)'); + }); + + it('dims the placeholder of an empty disabled field', () => { + expect(normalize(rules[`.${name}--disabled::placeholder`].color)).toBe( + disabledFieldInkPlaceholder, + ); + }); + + it('keeps the border static on hover, like the CMS', () => { + expect(rules[`.${name}--disabled:hover`]['border-color']).toBe( + 'var(--color--border)', + ); + }); + + it('declares disabled overrides after the base rules, so they win the tie', () => { + const selectors = Object.keys(rules); + + for (const pseudo of ['::placeholder', ':hover']) { + expect(selectors.indexOf(`.${name}--disabled${pseudo}`)).toBeGreaterThan( + selectors.indexOf(`.${name}${pseudo}`), + ); + } + }); +}); diff --git a/packages/react-ui/src/Canvas/index.tsx b/packages/react-ui/src/Canvas/index.tsx index 3849afe..7e4568e 100644 --- a/packages/react-ui/src/Canvas/index.tsx +++ b/packages/react-ui/src/Canvas/index.tsx @@ -207,7 +207,7 @@ export type CanvasProps = { * collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }} * > *

- * The flat, low-contrast pair applied to non-interactive controls: disabled buttons, disabled selects and disabled toggles. + * The flat, low-contrast pair applied to non-interactive controls: disabled buttons, switches and checkboxes. *

* * {(isOpen, setOpen) => ( *
setOpen((v) => !v) }} + * > + *

+ * Disabled and read-only text fields and selects. The value must stay readable, and an empty field's placeholder must still read as "empty". Older DatoCMS versions don't send these tokens, so fall back to the disabled context, e.g. var(--color--disabled-field--surface, var(--color--disabled--surface)). + *

+ * + *
+ * )} + * + * + * + * {(isOpen, setOpen) => ( + *
setOpen((v) => !v) }} * > diff --git a/packages/react-ui/src/SelectInput/index.tsx b/packages/react-ui/src/SelectInput/index.tsx index 069f69c..ad393ed 100644 --- a/packages/react-ui/src/SelectInput/index.tsx +++ b/packages/react-ui/src/SelectInput/index.tsx @@ -2,8 +2,6 @@ import React, { useMemo } from 'react'; import RawSelect, { type Props as RawSelectProps, type GroupBase, - type StylesConfig, - type ThemeConfig, } from 'react-select'; import RawAsyncSelect, { type AsyncProps } from 'react-select/async'; import RawAsyncCreatableSelect, { @@ -12,139 +10,10 @@ import RawAsyncCreatableSelect, { import RawCreatableSelect, { type CreatableProps, } from 'react-select/creatable'; +import { buildStyles, themeConfig } from './theme'; -const themeConfig: ThemeConfig = (existing) => ({ - ...existing, - borderRadius: 0, - colors: { - ...existing.colors, - primary25: 'var(--color--surface-hover)', - // disabled - neutral10: 'var(--color--border)', - // normal - neutral20: 'var(--color--border)', - // focused - primary: 'var(--color--focus--border)', - // hover - neutral30: 'var(--color--border-hover)', - }, -}); - -const useStyles = (isDisabled?: boolean, error?: boolean) => { - return useMemo(() => { - return { - placeholder: (provided) => ({ - ...provided, - color: 'var(--color--ink-placeholder)', - }), - container: (provided) => { - return { - ...provided, - fontSize: 'inherit', - }; - }, - - control: (provided, { isFocused }) => { - let result = provided; - - result = { - ...result, - minHeight: 40, - }; - - if (isFocused) { - return { - ...result, - borderColor: error - ? 'var(--color--danger-soft--border)' - : 'var(--color--focus--border)', - backgroundColor: isDisabled - ? 'var(--color--disabled--surface)' - : 'var(--color--surface-raised)', - boxShadow: `0 0 0 4px ${ - error - ? 'var(--color--danger-soft--outline)' - : 'var(--color--focus--outline)' - }`, - '&:hover': { - borderColor: error - ? 'var(--color--danger-soft--border)' - : 'var(--color--focus--border)', - }, - }; - } - - return { - ...result, - borderColor: error - ? 'var(--color--danger-soft--border)' - : 'var(--color--border)', - backgroundColor: isDisabled - ? 'var(--color--disabled--surface)' - : 'var(--color--surface-raised)', - '&:hover': { - borderColor: error - ? 'var(--color--danger-soft--border)' - : 'var(--color--border-hover)', - }, - }; - }, - multiValueRemove: (provided) => ({ - ...provided, - cursor: 'pointer', - color: 'var(--color--primary-soft--ink)', - ':hover': { - backgroundColor: 'var(--color--primary-soft--surface-hover)', - color: 'var(--color--primary-soft--ink)', - }, - }), - menu: (provided) => { - return { - ...provided, - zIndex: 1000, - minWidth: 250, - backgroundColor: 'var(--color--surface-raised)', - boxShadow: 'var(--shadow--floating)', - }; - }, - singleValue: (provided) => ({ - ...provided, - color: 'var(--color--ink)', - }), - input: (provided) => ({ - ...provided, - color: 'var(--color--ink)', - boxShadow: 'none', - 'input:focus': { - boxShadow: 'none', - }, - }), - option: (provided, { isFocused, isSelected }) => ({ - ...provided, - backgroundColor: isSelected - ? 'var(--color--selected--surface)' - : isFocused - ? 'var(--color--surface-hover)' - : undefined, - color: 'var(--color--ink)', - }), - multiValue: (provided) => { - return { - ...provided, - zIndex: 100, - backgroundColor: 'var(--color--primary-soft--surface)', - userSelect: 'none', - }; - }, - multiValueLabel: (provided) => ({ - ...provided, - fontSize: 'inherit', - padding: 3, - color: 'var(--color--primary-soft--ink)', - }), - }; - }, [isDisabled, error]); -}; +const useStyles = (isDisabled?: boolean, error?: boolean) => + useMemo(() => buildStyles(isDisabled, error), [isDisabled, error]); type ErrorProp = { error?: boolean }; diff --git a/packages/react-ui/src/SelectInput/theme.ts b/packages/react-ui/src/SelectInput/theme.ts new file mode 100644 index 0000000..4e89bd7 --- /dev/null +++ b/packages/react-ui/src/SelectInput/theme.ts @@ -0,0 +1,166 @@ +import type { StylesConfig, Theme } from 'react-select'; + +// Hosts older than the `disabled-field` context don't send its tokens, and +// the SDK defines no colors of its own, so each one falls back to the token +// disabled fields used before. +const disabledFieldSurface = + 'var(--color--disabled-field--surface, var(--color--disabled--surface))'; +const disabledFieldInk = + 'var(--color--disabled-field--ink, var(--color--disabled--ink))'; +const disabledFieldInkPlaceholder = + 'var(--color--disabled-field--ink-placeholder, var(--color--ink-placeholder))'; + +/** + * Maps react-select's palette slots to DatoCMS tokens, mirroring the CMS's own + * `SelectInput/useThemeProps.ts`. Every slot react-select reads is mapped, so + * none falls back to its hardcoded (light-only) greys. + */ +export const themeConfig = (existing: Theme): Theme => ({ + ...existing, + borderRadius: 0, + colors: { + ...existing.colors, + // menu & option background + neutral0: 'var(--color--surface-raised)', + // disabled background + neutral10: disabledFieldSurface, + // default border + neutral20: 'var(--color--border)', + // hover border + neutral30: 'var(--color--border-hover)', + // disabled indicator/text + neutral40: 'var(--color--ink-disabled)', + // value text + neutral80: 'var(--color--ink)', + // focused border + primary: 'var(--color--focus--border)', + // option hover background + primary25: 'var(--color--surface-raised-hover)', + }, +}); + +/** + * Builds the react-select `styles` for a SelectInput. + * + * @param isDisabled - Whether the control is disabled + * @param error - Whether the field is in an error state + */ +export const buildStyles = ( + isDisabled?: boolean, + error?: boolean, +): StylesConfig => ({ + placeholder: (provided, state) => ({ + ...provided, + color: state.isDisabled + ? disabledFieldInkPlaceholder + : 'var(--color--ink-placeholder)', + }), + container: (provided) => { + return { + ...provided, + fontSize: 'inherit', + }; + }, + + control: (provided, { isFocused }) => { + let result = provided; + + result = { + ...result, + minHeight: 40, + }; + + if (isFocused) { + return { + ...result, + borderColor: error + ? 'var(--color--danger-soft--border)' + : 'var(--color--focus--border)', + backgroundColor: isDisabled + ? disabledFieldSurface + : 'var(--color--surface-raised)', + boxShadow: `0 0 0 4px ${ + error + ? 'var(--color--danger-soft--outline)' + : 'var(--color--focus--outline)' + }`, + '&:hover': { + borderColor: error + ? 'var(--color--danger-soft--border)' + : 'var(--color--focus--border)', + }, + }; + } + + return { + ...result, + borderColor: error + ? 'var(--color--danger-soft--border)' + : 'var(--color--border)', + backgroundColor: isDisabled + ? disabledFieldSurface + : 'var(--color--surface-raised)', + '&:hover': { + borderColor: error + ? 'var(--color--danger-soft--border)' + : 'var(--color--border-hover)', + }, + }; + }, + multiValueRemove: (provided) => ({ + ...provided, + cursor: 'pointer', + color: 'var(--color--primary-soft--ink)', + ':hover': { + backgroundColor: 'var(--color--primary-soft--surface-hover)', + color: 'var(--color--primary-soft--ink)', + }, + }), + menu: (provided) => { + return { + ...provided, + zIndex: 1000, + minWidth: 250, + backgroundColor: 'var(--color--surface-raised)', + boxShadow: 'var(--shadow--floating)', + }; + }, + singleValue: (provided, state) => ({ + ...provided, + // the disabled control sits on `disabled-field--surface`, so its text must + // use the paired context ink: the standalone `ink-disabled` used by + // react-select's neutral40 slot can coincide with that surface + color: state.isDisabled ? disabledFieldInk : 'var(--color--ink)', + }), + input: (provided) => ({ + ...provided, + color: 'var(--color--ink)', + boxShadow: 'none', + 'input:focus': { + boxShadow: 'none', + }, + }), + option: (provided, { isFocused, isSelected }) => ({ + ...provided, + backgroundColor: isSelected + ? 'var(--color--selected--surface)' + : isFocused + ? 'var(--color--surface-raised-hover)' + : undefined, + color: 'var(--color--ink)', + }), + multiValue: (provided) => { + return { + ...provided, + zIndex: 100, + backgroundColor: 'var(--color--primary-soft--surface)', + userSelect: 'none', + }; + }, + multiValueLabel: (provided) => ({ + ...provided, + fontSize: 'inherit', + padding: 3, + color: 'var(--color--primary-soft--ink)', + }), +}); diff --git a/packages/react-ui/src/TextInput/styles.module.css b/packages/react-ui/src/TextInput/styles.module.css index 4c90a2b..f325ac3 100644 --- a/packages/react-ui/src/TextInput/styles.module.css +++ b/packages/react-ui/src/TextInput/styles.module.css @@ -35,9 +35,25 @@ } .TextInput--disabled { - color: var(--color--disabled--ink); + /* `disabled-field--*` fall back to the pre-context tokens on older hosts */ + color: var(--color--disabled-field--ink, var(--color--disabled--ink)); border-color: var(--color--border); - background: var(--color--disabled--surface); + background: var( + --color--disabled-field--surface, + var(--color--disabled--surface) + ); + + /* like the CMS, a disabled field doesn't react to hover */ + &:hover { + border-color: var(--color--border); + } + + &::placeholder { + color: var( + --color--disabled-field--ink-placeholder, + var(--color--ink-placeholder) + ); + } } .TextInput--error { diff --git a/packages/react-ui/src/TextareaInput/styles.module.css b/packages/react-ui/src/TextareaInput/styles.module.css index ca66fe0..38feea8 100644 --- a/packages/react-ui/src/TextareaInput/styles.module.css +++ b/packages/react-ui/src/TextareaInput/styles.module.css @@ -35,9 +35,25 @@ } .TextareaInput--disabled { - color: var(--color--disabled--ink); + /* `disabled-field--*` fall back to the pre-context tokens on older hosts */ + color: var(--color--disabled-field--ink, var(--color--disabled--ink)); border-color: var(--color--border); - background: var(--color--disabled--surface); + background: var( + --color--disabled-field--surface, + var(--color--disabled--surface) + ); + + /* like the CMS, a disabled field doesn't react to hover */ + &:hover { + border-color: var(--color--border); + } + + &::placeholder { + color: var( + --color--disabled-field--ink-placeholder, + var(--color--ink-placeholder) + ); + } } .TextareaInput--error { From 6d078f3a469b1e4219587753f723a0637a954e2c Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 1 Oct 2026 12:00:42 -0700 Subject: [PATCH 2/2] fix(react-ui): address review of the disabled-field PR - Map react-select's `primary` slot to `selected--surface`, like the CMS, so pressing the selected option no longer flashes the focus color. - Say "slot for slot like the CMS" instead of "every slot": like the CMS, `neutral60` and `primary50` keep react-select's defaults. - Drop a theme test that could never fail. - Correct the Canvas docs: the placeholder falls back to `ink-placeholder`, not to the disabled context. - Write each fallback `var()` chain on one line so the built CSS doesn't keep line breaks. Co-Authored-By: Claude Opus 5.5 --- .changeset/disabled-field-colors.md | 2 +- packages/react-ui/__tests__/fieldColors.test.ts | 15 ++------------- packages/react-ui/src/Canvas/index.tsx | 2 +- packages/react-ui/src/SelectInput/theme.ts | 11 ++++++----- packages/react-ui/src/TextInput/styles.module.css | 10 ++-------- .../react-ui/src/TextareaInput/styles.module.css | 10 ++-------- 6 files changed, 14 insertions(+), 36 deletions(-) diff --git a/.changeset/disabled-field-colors.md b/.changeset/disabled-field-colors.md index 15cafc2..e3192c8 100644 --- a/.changeset/disabled-field-colors.md +++ b/.changeset/disabled-field-colors.md @@ -2,4 +2,4 @@ "datocms-react-ui": patch --- -Disabled `TextInput`, `TextareaInput` and `SelectInput` fields now match native DatoCMS fields. They use the new `--color--disabled-field--*` tokens, which fixes the low-contrast value text in dark mode. An empty disabled field shows a dimmed placeholder, and a disabled text field no longer reacts to hover. `SelectInput` also maps every react-select palette slot to a DatoCMS token, so menus no longer fall back to light-only greys. On older DatoCMS versions that don't send the new tokens, fields fall back to the previous `--color--disabled--*` colors. +Disabled `TextInput`, `TextareaInput` and `SelectInput` fields now match native DatoCMS fields. They use the new `--color--disabled-field--*` tokens, which fixes the low-contrast value text in dark mode. An empty disabled field shows a dimmed placeholder, and a disabled text field no longer reacts to hover. `SelectInput` also maps react-select's palette slot for slot like the CMS does, so menus no longer fall back to light-only greys in the slots the CMS maps. On older DatoCMS versions that don't send the new tokens, fields fall back to the previous `--color--disabled--*` colors. diff --git a/packages/react-ui/__tests__/fieldColors.test.ts b/packages/react-ui/__tests__/fieldColors.test.ts index 4149426..4f8072d 100644 --- a/packages/react-ui/__tests__/fieldColors.test.ts +++ b/packages/react-ui/__tests__/fieldColors.test.ts @@ -48,7 +48,7 @@ const callStyle = (key: string, state: Record, styles: any) => describe('SelectInput themeConfig', () => { const { colors, borderRadius } = themeConfig(reactSelectDefaultTheme); - it('maps every palette slot react-select reads to a DatoCMS token, like the CMS', () => { + it('maps the palette slots the CMS maps, to the same tokens', () => { expect(borderRadius).toBe(0); expect(colors).toMatchObject({ neutral0: 'var(--color--surface-raised)', @@ -57,21 +57,10 @@ describe('SelectInput themeConfig', () => { neutral30: 'var(--color--border-hover)', neutral40: 'var(--color--ink-disabled)', neutral80: 'var(--color--ink)', - primary: 'var(--color--focus--border)', + primary: 'var(--color--selected--surface)', primary25: 'var(--color--surface-raised-hover)', }); }); - - it('leaves no slot it maps on a hardcoded react-select grey', () => { - for (const slot of [ - 'neutral0', - 'neutral10', - 'neutral40', - 'neutral80', - ] as const) { - expect(colors[slot]).toMatch(/^var\(--color--/); - } - }); }); describe('SelectInput styles', () => { diff --git a/packages/react-ui/src/Canvas/index.tsx b/packages/react-ui/src/Canvas/index.tsx index 7e4568e..8928de0 100644 --- a/packages/react-ui/src/Canvas/index.tsx +++ b/packages/react-ui/src/Canvas/index.tsx @@ -225,7 +225,7 @@ export type CanvasProps = { * collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }} * > *

- * Disabled and read-only text fields and selects. The value must stay readable, and an empty field's placeholder must still read as "empty". Older DatoCMS versions don't send these tokens, so fall back to the disabled context, e.g. var(--color--disabled-field--surface, var(--color--disabled--surface)). + * Disabled and read-only text fields and selects. The value must stay readable, and an empty field's placeholder must still read as "empty". Older DatoCMS versions don't send these tokens, so give each one a fallback: disabled--surface and disabled--ink for the surface and ink, and ink-placeholder for the placeholder, e.g. var(--color--disabled-field--surface, var(--color--disabled--surface)). *

* ({ ...existing, @@ -32,8 +33,8 @@ export const themeConfig = (existing: Theme): Theme => ({ neutral40: 'var(--color--ink-disabled)', // value text neutral80: 'var(--color--ink)', - // focused border - primary: 'var(--color--focus--border)', + // selected option background + primary: 'var(--color--selected--surface)', // option hover background primary25: 'var(--color--surface-raised-hover)', }, diff --git a/packages/react-ui/src/TextInput/styles.module.css b/packages/react-ui/src/TextInput/styles.module.css index f325ac3..c2171f9 100644 --- a/packages/react-ui/src/TextInput/styles.module.css +++ b/packages/react-ui/src/TextInput/styles.module.css @@ -38,10 +38,7 @@ /* `disabled-field--*` fall back to the pre-context tokens on older hosts */ color: var(--color--disabled-field--ink, var(--color--disabled--ink)); border-color: var(--color--border); - background: var( - --color--disabled-field--surface, - var(--color--disabled--surface) - ); + background: var(--color--disabled-field--surface, var(--color--disabled--surface)); /* like the CMS, a disabled field doesn't react to hover */ &:hover { @@ -49,10 +46,7 @@ } &::placeholder { - color: var( - --color--disabled-field--ink-placeholder, - var(--color--ink-placeholder) - ); + color: var(--color--disabled-field--ink-placeholder, var(--color--ink-placeholder)); } } diff --git a/packages/react-ui/src/TextareaInput/styles.module.css b/packages/react-ui/src/TextareaInput/styles.module.css index 38feea8..59009a3 100644 --- a/packages/react-ui/src/TextareaInput/styles.module.css +++ b/packages/react-ui/src/TextareaInput/styles.module.css @@ -38,10 +38,7 @@ /* `disabled-field--*` fall back to the pre-context tokens on older hosts */ color: var(--color--disabled-field--ink, var(--color--disabled--ink)); border-color: var(--color--border); - background: var( - --color--disabled-field--surface, - var(--color--disabled--surface) - ); + background: var(--color--disabled-field--surface, var(--color--disabled--surface)); /* like the CMS, a disabled field doesn't react to hover */ &:hover { @@ -49,10 +46,7 @@ } &::placeholder { - color: var( - --color--disabled-field--ink-placeholder, - var(--color--ink-placeholder) - ); + color: var(--color--disabled-field--ink-placeholder, var(--color--ink-placeholder)); } }