Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to theme prop proposal #1085

Merged
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
@@ -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<typeof ChoiceCard> = {
title: 'ChoiceCard',
component: ChoiceCard,
args: {
Expand Down Expand Up @@ -49,31 +34,33 @@ export default {
},
};

const Template: Story<ChoiceCardProps> = (args: ChoiceCardProps) => (
export default meta;

const Template: StoryFn<ChoiceCardProps> = (args: ChoiceCardProps) => (
<ChoiceCard {...args} />
);

// *****************************************************************************

export const DefaultDefaultTheme = Template.bind({});
export const DefaultDefaultTheme: StoryFn<ChoiceCardProps> = Template.bind({});

// *****************************************************************************

export const CheckedDefaultTheme = Template.bind({});
export const CheckedDefaultTheme: StoryFn<ChoiceCardProps> = Template.bind({});
CheckedDefaultTheme.args = {
checked: true,
};

// *****************************************************************************

export const ErrorDefaultTheme = Template.bind({});
export const ErrorDefaultTheme: StoryFn<ChoiceCardProps> = Template.bind({});
ErrorDefaultTheme.args = {
error: true,
};

// *****************************************************************************

export const IconDefaultTheme = Template.bind({});
export const IconDefaultTheme: StoryFn<ChoiceCardProps> = Template.bind({});
IconDefaultTheme.args = {
label: 'Camera',
// @ts-expect-error - Storybook maps 'JSX element' to <em>Option 1</em>
Expand All @@ -82,7 +69,10 @@ IconDefaultTheme.args = {

// *****************************************************************************

export const DarkTheme = Template.bind({});
DarkTheme.args = {
theme: choiceCardThemeDark,
export const CustomTheme: StoryFn<ChoiceCardProps> = Template.bind({});
CustomTheme.args = {
theme: {
backgroundUnselected: 'black',
backgroundSelected: 'hotpink',
},
Comment on lines +73 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the theme: choiceCardDark better if we consider this as part of the documentation for this component. It is the "recommended" way of theming

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i think it would be if we had a dark theme for this component. in that case we'd want to explicit provide a story for that (and any other built-in themes).

the reason i removed it here is because we don't (yet?) have that, and so hopefully this makes clear it's a made up example of one way of using the prop, rather than implying there was a dark theme you can use

Copy link
Contributor

@oliverabrahams oliverabrahams Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Happy to go with that, we could change the colours to come from the palette because that may be more how we want the prop used. I am happy to do that once this is merged tho.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i think we do want that, but part of me felt that because right now you can do the 'wrong' thing, we should have a story that does that, then have it break when we fix it. maybe that's the wrong approach.

but maybe we shoudl fix the types in foundations so you can't?

};
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>,
Expand Down Expand Up @@ -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<ChoiceCardTheme>;
}

/**
Expand All @@ -80,7 +90,7 @@ export const ChoiceCard = ({
cssOverrides,
error,
onChange,
theme,
theme = {},
type = 'radio',
...props
}: ChoiceCardProps): EmotionJSX.Element => {
Expand All @@ -92,30 +102,25 @@ export const ChoiceCard = ({
return !!defaultChecked;
};

const transformProvidedTheme = (
providedTheme: typeof choiceCardThemeDefault.choiceCard | undefined,
): ChoiceCardTheme => {
if (!providedTheme) {
return {};
}
return {
textUnselected: providedTheme.textLabel,
textSelected: providedTheme.textChecked,
borderUnselected: providedTheme.border,
borderSelected: providedTheme.borderChecked,
backgroundSelected: providedTheme.backgroundChecked,
...providedTheme,
};
};
/** Transforms an old shaped `ThemeProvider` theme to ChoiceCardTheme */
const transformOldProviderTheme = (
providerTheme: Theme['choiceCard'],
): Partial<ChoiceCardTheme> => ({
textUnselected: providerTheme?.textLabel,
textSelected: providerTheme?.textChecked,
borderUnselected: providerTheme?.border,
borderSelected: providerTheme?.borderChecked,
backgroundSelected: providerTheme?.backgroundChecked,
...providerTheme,
});
sndrs marked this conversation as resolved.
Show resolved Hide resolved

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
Expand All @@ -124,15 +129,15 @@ export const ChoiceCard = ({
return (
<>
<input
css={(theme: Theme) => [
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) {
Expand All @@ -144,9 +149,9 @@ export const ChoiceCard = ({
{...props}
/>
<label
css={(theme: Theme) => [
choiceCard(getCombinedTheme(theme.choiceCard)),
error ? errorChoiceCard(getCombinedTheme(theme.choiceCard)) : '',
css={(providerTheme: Theme) => [
choiceCard(combineThemes(providerTheme.choiceCard)),
error ? errorChoiceCard(combineThemes(providerTheme.choiceCard)) : '',
]}
htmlFor={id}
>
Expand All @@ -155,7 +160,9 @@ export const ChoiceCard = ({
<div>{labelContent}</div>
</div>
<span
css={(theme: Theme) => [tick(getCombinedTheme(theme.choiceCard))]}
css={(providerTheme: Theme) => [
tick(combineThemes(providerTheme.choiceCard)),
]}
/>
</label>
</>
Expand Down
36 changes: 17 additions & 19 deletions libs/@guardian/source-react-components/src/choice-card/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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};
}
`;

Expand Down Expand Up @@ -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%;
Expand All @@ -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: '';
}
Expand All @@ -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};
}
`;
27 changes: 5 additions & 22 deletions libs/@guardian/source-react-components/src/choice-card/theme.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand All @@ -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],
Expand All @@ -52,4 +35,4 @@ export const choiceCardTheme: ChoiceCardFullTheme = {
backgroundHover: 'transparent',
backgroundSelected: '#E3F6FF',
backgroundTick: palette.brand[500],
};
} as const;