diff --git a/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.stories.tsx b/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.stories.tsx index 21351b51d..152ab3a75 100644 --- a/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.stories.tsx +++ b/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.stories.tsx @@ -1,24 +1,9 @@ -import { palette } from '@guardian/source-foundations'; -import type { Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; import { SvgCamera } from '../../vendor/icons/SvgCamera'; import { ChoiceCard } from './ChoiceCard'; import type { ChoiceCardProps } from './ChoiceCard'; -const choiceCardThemeDark = { - textUnselected: palette.neutral[86], - textSelected: palette.brand[400], - textHover: palette.brand[800], - textError: palette.error[500], - borderUnselected: palette.neutral[86], - borderSelected: palette.brand[800], - borderHover: palette.brand[800], - borderError: palette.error[500], - backgroundUnselected: palette.neutral[20], - backgroundHover: palette.neutral[20], - backgroundSelected: palette.neutral[100], - backgroundTick: palette.brand[500], -}; -export default { +const meta: Meta = { title: 'ChoiceCard', component: ChoiceCard, args: { @@ -49,31 +34,33 @@ export default { }, }; -const Template: Story = (args: ChoiceCardProps) => ( +export default meta; + +const Template: StoryFn = (args: ChoiceCardProps) => ( ); // ***************************************************************************** -export const DefaultDefaultTheme = Template.bind({}); +export const DefaultDefaultTheme: StoryFn = Template.bind({}); // ***************************************************************************** -export const CheckedDefaultTheme = Template.bind({}); +export const CheckedDefaultTheme: StoryFn = Template.bind({}); CheckedDefaultTheme.args = { checked: true, }; // ***************************************************************************** -export const ErrorDefaultTheme = Template.bind({}); +export const ErrorDefaultTheme: StoryFn = Template.bind({}); ErrorDefaultTheme.args = { error: true, }; // ***************************************************************************** -export const IconDefaultTheme = Template.bind({}); +export const IconDefaultTheme: StoryFn = Template.bind({}); IconDefaultTheme.args = { label: 'Camera', // @ts-expect-error - Storybook maps 'JSX element' to Option 1 @@ -82,7 +69,10 @@ IconDefaultTheme.args = { // ***************************************************************************** -export const DarkTheme = Template.bind({}); -DarkTheme.args = { - theme: choiceCardThemeDark, +export const CustomTheme: StoryFn = Template.bind({}); +CustomTheme.args = { + theme: { + backgroundUnselected: 'black', + backgroundSelected: 'hotpink', + }, }; diff --git a/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.tsx b/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.tsx index 0a1f0bf77..6e857b63a 100644 --- a/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.tsx +++ b/libs/@guardian/source-react-components/src/choice-card/ChoiceCard.tsx @@ -16,12 +16,22 @@ import { tick, tickAnimation, } from './styles'; -import type { - ChoiceCardFullTheme, - ChoiceCardTheme, - choiceCardThemeDefault, -} from './theme'; -import { choiceCardTheme } from './theme'; +import { choiceCardTheme as defaultTheme } from './theme'; + +export type ChoiceCardTheme = { + textUnselected: string; + textSelected: string; + textHover: string; + textError: string; + borderUnselected: string; + borderSelected: string; + borderHover: string; + borderError: string; + backgroundUnselected: string; + backgroundHover: string; + backgroundSelected: string; + backgroundTick: string; +}; export interface ChoiceCardProps extends InputHTMLAttributes, @@ -58,7 +68,7 @@ export interface ChoiceCardProps /** * A component level theme to override the colour palette of the choice card component */ - theme?: ChoiceCardTheme; + theme?: Partial; } /** @@ -80,7 +90,7 @@ export const ChoiceCard = ({ cssOverrides, error, onChange, - theme, + theme = {}, type = 'radio', ...props }: ChoiceCardProps): EmotionJSX.Element => { @@ -92,30 +102,37 @@ export const ChoiceCard = ({ return !!defaultChecked; }; - const transformProvidedTheme = ( - providedTheme: typeof choiceCardThemeDefault.choiceCard | undefined, - ): ChoiceCardTheme => { - if (!providedTheme) { - return {}; + /** Transforms an old shaped `ThemeProvider` theme to ChoiceCardTheme */ + const transformOldProviderTheme = ( + providerTheme: Theme['choiceCard'], + ): Partial => { + const transformedTheme: Partial = {}; + + if (providerTheme?.textLabel) { + transformedTheme.textUnselected = providerTheme.textLabel; } - return { - textUnselected: providedTheme.textLabel, - textSelected: providedTheme.textChecked, - borderUnselected: providedTheme.border, - borderSelected: providedTheme.borderChecked, - backgroundSelected: providedTheme.backgroundChecked, - ...providedTheme, - }; + if (providerTheme?.textChecked) { + transformedTheme.textSelected = providerTheme.textChecked; + } + if (providerTheme?.border) { + transformedTheme.borderUnselected = providerTheme.border; + } + if (providerTheme?.borderChecked) { + transformedTheme.borderSelected = providerTheme.borderChecked; + } + if (providerTheme?.backgroundChecked) { + transformedTheme.backgroundSelected = providerTheme.backgroundChecked; + } + return { ...transformedTheme, ...providerTheme }; }; - const getCombinedTheme = ( - providedTheme: typeof choiceCardThemeDefault.choiceCard | undefined, - ): ChoiceCardFullTheme => { - const transformedProvidedTheme = transformProvidedTheme(providedTheme); + const combineThemes = ( + providerTheme: Theme['choiceCard'], + ): ChoiceCardTheme => { return { - ...choiceCardTheme, - ...transformedProvidedTheme, - ...(theme ?? {}), + ...defaultTheme, + ...transformOldProviderTheme(providerTheme), + ...theme, }; }; // prevent the animation firing if a Choice Card has been checked by default @@ -124,15 +141,15 @@ export const ChoiceCard = ({ return ( <> [ - input(getCombinedTheme(theme.choiceCard)), + css={(providerTheme: Theme) => [ + input(combineThemes(providerTheme.choiceCard)), userChanged ? tickAnimation : '', cssOverrides, ]} id={id} value={value} aria-invalid={!!error} - defaultChecked={defaultChecked != null ? defaultChecked : undefined} + defaultChecked={defaultChecked ?? undefined} checked={checked != null ? isChecked() : undefined} onChange={(event) => { if (onChange) { @@ -144,9 +161,9 @@ export const ChoiceCard = ({ {...props} /> diff --git a/libs/@guardian/source-react-components/src/choice-card/styles.ts b/libs/@guardian/source-react-components/src/choice-card/styles.ts index db52947c2..e37245106 100644 --- a/libs/@guardian/source-react-components/src/choice-card/styles.ts +++ b/libs/@guardian/source-react-components/src/choice-card/styles.ts @@ -11,8 +11,8 @@ import { visuallyHidden, width, } from '@guardian/source-foundations'; +import type { ChoiceCardTheme } from './ChoiceCard'; import type { ChoiceCardColumns } from './ChoiceCardGroup'; -import { choiceCardTheme } from './theme'; export const fieldset = css` ${resets.fieldset}; @@ -61,7 +61,7 @@ export const gridColumns: { [key in ChoiceCardColumns]: SerializedStyles } = { 5: gridColumnsStyle(5), }; -export const input = (choiceCard = choiceCardTheme): SerializedStyles => css` +export const input = (theme: ChoiceCardTheme): SerializedStyles => css` ${visuallyHidden}; &:focus + label { @@ -71,11 +71,11 @@ export const input = (choiceCard = choiceCardTheme): SerializedStyles => css` } &:checked + label { - box-shadow: inset 0 0 0 2px ${choiceCard.borderSelected}; - background-color: ${choiceCard.backgroundSelected}; + box-shadow: inset 0 0 0 2px ${theme.borderSelected}; + background-color: ${theme.backgroundSelected}; & > * { - color: ${choiceCard.textSelected}; + color: ${theme.textSelected}; /* last child is the tick */ &:last-child { @@ -131,20 +131,18 @@ export const tickAnimation = css` } `; -export const choiceCard = ( - choiceCard = choiceCardTheme, -): SerializedStyles => css` +export const choiceCard = (theme: ChoiceCardTheme): SerializedStyles => css` flex: 1; display: flex; justify-content: center; min-height: ${height.inputMedium}px; margin: 0 0 ${space[2]}px 0; - box-shadow: inset 0 0 0 1px ${choiceCard.borderUnselected}; + box-shadow: inset 0 0 0 1px ${theme.borderUnselected}; border-radius: 4px; position: relative; cursor: pointer; - background-color: ${choiceCard.backgroundUnselected}; - color: ${choiceCard.textUnselected}; + background-color: ${theme.backgroundUnselected}; + color: ${theme.textUnselected}; ${from.mobileLandscape} { margin: 0 ${space[2]}px 0 0; @@ -154,9 +152,9 @@ export const choiceCard = ( } &:hover { - box-shadow: inset 0 0 0 2px ${choiceCard.borderHover}; - background-color: ${choiceCard.backgroundHover}; - color: ${choiceCard.textHover}; + box-shadow: inset 0 0 0 2px ${theme.borderHover}; + background-color: ${theme.backgroundHover}; + color: ${theme.textHover}; } `; @@ -205,7 +203,7 @@ export const contentWrapperLabelOnly = css` // TODO: most of this is duplicated in the checkbox component // We should extract it into its own module somewhere -export const tick = (choiceCard = choiceCardTheme): SerializedStyles => css` +export const tick = (theme: ChoiceCardTheme): SerializedStyles => css` /* overall positional properties */ position: absolute; top: 50%; @@ -220,7 +218,7 @@ export const tick = (choiceCard = choiceCardTheme): SerializedStyles => css` &:before { position: absolute; display: block; - background-color: ${choiceCard.backgroundTick}; + background-color: ${theme.backgroundTick}; transition: all ${transitions.short} ease-in-out; content: ''; } @@ -245,11 +243,11 @@ export const tick = (choiceCard = choiceCardTheme): SerializedStyles => css` `; export const errorChoiceCard = ( - choiceCard = choiceCardTheme, + theme: ChoiceCardTheme, ): SerializedStyles => css` - box-shadow: inset 0 0 0 2px ${choiceCard.borderError}; + box-shadow: inset 0 0 0 2px ${theme.borderError}; & > * { - color: ${choiceCard.textError}; + color: ${theme.textError}; } `; diff --git a/libs/@guardian/source-react-components/src/choice-card/theme.ts b/libs/@guardian/source-react-components/src/choice-card/theme.ts index 3b7cfe750..b6317d6b0 100644 --- a/libs/@guardian/source-react-components/src/choice-card/theme.ts +++ b/libs/@guardian/source-react-components/src/choice-card/theme.ts @@ -1,25 +1,8 @@ import { palette } from '@guardian/source-foundations'; import { userFeedbackThemeDefault } from '../user-feedback/theme'; +import type { ChoiceCardTheme } from './ChoiceCard'; -export interface ChoiceCardTheme { - textUnselected?: string; - textSelected?: string; - textHover?: string; - textError?: string; - borderUnselected?: string; - borderSelected?: string; - borderHover?: string; - borderError?: string; - backgroundUnselected?: string; - backgroundHover?: string; - backgroundSelected?: string; - backgroundTick?: string; -} - -export type ChoiceCardFullTheme = { - [P in keyof ChoiceCardTheme]-?: ChoiceCardTheme[P]; -}; -/** @deprecated Use `choiceCardTheme` and component `theme` prop instead of emotion's `ThemeProvider` **/ +/** @deprecated Use `choiceCardTheme` and component `theme` prop instead of emotion's `ThemeProvider` */ export const choiceCardThemeDefault = { choiceCard: { textLabel: palette.neutral[46], @@ -37,9 +20,9 @@ export const choiceCardThemeDefault = { borderError: palette.error[400], }, ...userFeedbackThemeDefault, -}; +} as const; -export const choiceCardTheme: ChoiceCardFullTheme = { +export const choiceCardTheme: ChoiceCardTheme = { textUnselected: palette.neutral[46], textSelected: palette.brand[400], textHover: palette.brand[500], @@ -52,4 +35,4 @@ export const choiceCardTheme: ChoiceCardFullTheme = { backgroundHover: 'transparent', backgroundSelected: '#E3F6FF', backgroundTick: palette.brand[500], -}; +} as const;