From c6fe907cc87b7687d3f8e11eb3a1af00fc5a918c Mon Sep 17 00:00:00 2001 From: Simon Adcock Date: Mon, 16 Aug 2021 12:13:34 +0100 Subject: [PATCH 1/4] extend story stub with default args and arg types --- src/@types/storybook-emotion-10-fixes.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/@types/storybook-emotion-10-fixes.ts b/src/@types/storybook-emotion-10-fixes.ts index d5e6e1f4f..a07e67e30 100644 --- a/src/@types/storybook-emotion-10-fixes.ts +++ b/src/@types/storybook-emotion-10-fixes.ts @@ -13,9 +13,10 @@ export type Parameters = { [key: string]: any; }; -export type Story = { - (arg0: any): JSX.Element; +export type Story = { + (arg0: Args & T): JSX.Element; args?: Args; parameters?: Parameters; + argTypes?: Args; storyName?: string; }; From aaf9f9240dfb8ca3a5607c89a32357415b8d01da Mon Sep 17 00:00:00 2001 From: Simon Adcock Date: Mon, 16 Aug 2021 12:14:15 +0100 Subject: [PATCH 2/4] remove api docs from readme Co-authored-by: Jamie Lynch --- src/core/components/radio/README.md | 96 ++--------------------------- 1 file changed, 4 insertions(+), 92 deletions(-) diff --git a/src/core/components/radio/README.md b/src/core/components/radio/README.md index cc3318524..f4b881e67 100644 --- a/src/core/components/radio/README.md +++ b/src/core/components/radio/README.md @@ -10,101 +10,13 @@ $ yarn add @guardian/src-radio ## Use -```js -import { RadioGroup, Radio } from '@guardian/src-radio'; +### API -const Form = () => { - const [selected, setSelected] = useState(null); +See [storybook](https://guardian.github.io/source/?path=/docs/source-src-radio-radio--demo) - return ( -
- - - , - -
- ); -}; -``` - -## `RadioGroup` Props - -### `name` - -**`string`** - -Gets passed as the name attribute for each radio button - -### `label` - -**`string`** - -Appears as a legend at the top of the radio group - -### `hideLabel` - -**`boolean`** _= "false"_ - -Visually hides the label. - -### `supporting` - -**`string | JSX.Element` ** - -Additional text or component that appears below the label - -### `orientation` - -**`"vertical" | "horizontal"`** _= "vertical"_ - -The direction in which radio buttons are stacked - -### `error` - -**`string`** - -If passed, error styling should applies to this radio group. The string appears as an inline error message. - -## `Radio` Props - -### `label` - -**`ReactNode`** - -Appears to the right of the radio button. If a visible label is undesirable (e.g. for layout reasons) use `aria-label` instead. - -If label is omitted, supporting text will not appear either. - -### `supporting` - -**`ReactNode`** - -Additional text or a component that appears below the label - -### `checked` - -**`boolean`** - -Whether radio button is checked. This is necessary when using the [controlled approach](https://reactjs.org/docs/forms.html#controlled-components) to form state management. +### How to use -**Note:** if you pass the `checked` prop, you **must** also pass an `onChange` handler, or the field will be rendered as read-only. +For context and visual guides relating to usage see the [Source Design System website](https://theguardian.design/2a1e5182b/p/2891dd-radio-button/b/46940d). ## Supported themes From c8087196110c143c471986e8918970a4ccd40f89 Mon Sep 17 00:00:00 2001 From: Simon Adcock Date: Mon, 16 Aug 2021 12:15:35 +0100 Subject: [PATCH 3/4] extract radio components into separate modules Co-authored-by: Jamie Lynch --- src/core/components/radio/Radio.tsx | 132 ++++++++++++++++ src/core/components/radio/RadioGroup.tsx | 106 +++++++++++++ src/core/components/radio/index.tsx | 188 +---------------------- 3 files changed, 240 insertions(+), 186 deletions(-) create mode 100644 src/core/components/radio/Radio.tsx create mode 100644 src/core/components/radio/RadioGroup.tsx diff --git a/src/core/components/radio/Radio.tsx b/src/core/components/radio/Radio.tsx new file mode 100644 index 000000000..244348a5f --- /dev/null +++ b/src/core/components/radio/Radio.tsx @@ -0,0 +1,132 @@ +import React, { ReactNode, InputHTMLAttributes } from 'react'; +import { + label, + labelWithSupportingText, + radio, + labelText, + labelTextWithSupportingText, + supportingText, +} from './styles'; +import { Props } from '@guardian/src-helpers'; + +const LabelText = ({ + hasSupportingText, + children, +}: { + hasSupportingText?: boolean; + children: ReactNode; +}) => { + return ( +
[ + hasSupportingText ? labelTextWithSupportingText : '', + labelText(theme.radio && theme), + ]} + className="src-radio-label-text" + > + {children} +
+ ); +}; + +const SupportingText = ({ children }: { children: ReactNode }) => { + return ( +
supportingText(theme.radio && theme)}> + {children} +
+ ); +}; + +export interface RadioProps + extends InputHTMLAttributes, + Props { + /** + * Whether radio button is checked. This is necessary when using the + * [controlled approach](https://reactjs.org/docs/forms.html#controlled-components) + * (recommended) to form state management. + * + * _Note: if you pass the `checked` prop, you MUST also pass an `onChange` + * handler, or the field will be rendered as read-only._ + */ + checked?: boolean; + /** + * When using the [uncontrolled approach](https://reactjs.org/docs/uncontrolled-components.html), + * use defaultChecked to indicate the initially checked button. + */ + defaultChecked?: boolean; + /** + * Appears to the right of the radio button. If a visible label is + * undesirable (e.g. for layout reasons) use `aria-label` instead. + * + * If label is omitted, supporting text will not appear either. + */ + label?: string | ReactNode; + /** + * Additional text or a component that appears below the label + */ + supporting?: string | ReactNode; +} + +/** + * [Storybook](https://guardian.github.io/source/?path=/docs/source-src-radio-radio--demo) • + * [Design System](https://theguardian.design/2a1e5182b/p/2891dd-radio-button/b/46940d) • + * [GitHub](https://github.com/guardian/source/tree/main/src/core/components/radio) • + * [NPM](https://www.npmjs.com/package/@guardian/src-radio) + * + * Radio buttons allow users to make a single selection from a set of options. + * + * The following themes are supported: `default`, `brand` + */ +export const Radio = ({ + label: labelContent, + value, + supporting, + checked, + defaultChecked, + cssOverrides, + ...props +}: RadioProps) => { + const isChecked = (): boolean => { + if (checked != null) { + return checked; + } + + return !!defaultChecked; + }; + const radioControl = ( + [radio(theme.radio && theme), cssOverrides]} + value={value} + aria-checked={isChecked()} + defaultChecked={defaultChecked != null ? defaultChecked : undefined} + checked={checked != null ? isChecked() : undefined} + {...props} + /> + ); + + const labelledRadioControl = ( + + ); + + return ( + <>{labelContent || supporting ? labelledRadioControl : radioControl} + ); +}; diff --git a/src/core/components/radio/RadioGroup.tsx b/src/core/components/radio/RadioGroup.tsx new file mode 100644 index 000000000..4dad586b3 --- /dev/null +++ b/src/core/components/radio/RadioGroup.tsx @@ -0,0 +1,106 @@ +import React, { FieldsetHTMLAttributes } from 'react'; +import { Legend } from '@guardian/src-label'; +import { InlineError } from '@guardian/src-user-feedback'; +import { + descriptionId, + generateSourceId, +} from '@guardian/src-foundations/accessibility'; +import { fieldset, horizontal, vertical } from './styles'; +import { Props } from '@guardian/src-helpers'; + +type Orientation = 'vertical' | 'horizontal'; + +const orientationStyles = { + vertical: vertical, + horizontal: horizontal, +}; + +export interface RadioGroupProps + extends FieldsetHTMLAttributes, + Props { + id?: string; + /** + * Appears as a legend at the top of the radio group + */ + label?: string; + /** + * Visually hides the label + */ + hideLabel?: boolean; + /** + * Additional text or component that appears below the label + */ + supporting?: string | JSX.Element; + /** + * The direction in which radio buttons are stacked + */ + orientation?: Orientation; + /** + * If passed, error styling should applies to this radio group. The string appears as an inline error message. + */ + error?: string; +} + +/** + * [Storybook](https://guardian.github.io/source/?path=/docs/source-src-radio-radio-group--demo) • + * [Design System](https://theguardian.design/2a1e5182b/p/2891dd-radio-button/b/46940d) • + * [GitHub](https://github.com/guardian/source/tree/main/src/core/components/radio) • + * [NPM](https://www.npmjs.com/package/@guardian/src-radio) + * + * Radio buttons allow users to make a single selection from a set of options. + * + * The following themes are supported: `default`, `brand` + */ +export const RadioGroup = ({ + id, + name, + label, + hideLabel = false, + supporting, + orientation = 'vertical', + error, + cssOverrides, + children, + ...props +}: RadioGroupProps) => { + const groupId = id || generateSourceId(); + const legend = label ? ( + + ) : ( + '' + ); + const message = error && ( + {error} + ); + + return ( +
[ + fieldset(theme.radio && theme), + orientationStyles[orientation], + cssOverrides, + ]} + {...props} + > + {legend} + {message} + {React.Children.map(children, (child) => { + return React.cloneElement( + child as React.ReactElement, + Object.assign( + error + ? { + 'aria-describedby': descriptionId(groupId), + } + : {}, + { + name, + }, + ), + ); + })} +
+ ); +}; diff --git a/src/core/components/radio/index.tsx b/src/core/components/radio/index.tsx index 85b328715..67431b6df 100644 --- a/src/core/components/radio/index.tsx +++ b/src/core/components/radio/index.tsx @@ -1,188 +1,4 @@ -import React, { ReactNode, InputHTMLAttributes } from 'react'; -import { SerializedStyles } from '@emotion/react'; -import { Legend } from '@guardian/src-label'; -import { InlineError } from '@guardian/src-user-feedback'; -import { - descriptionId, - generateSourceId, -} from '@guardian/src-foundations/accessibility'; -import { - fieldset, - label, - labelWithSupportingText, - radio, - labelText, - labelTextWithSupportingText, - supportingText, - horizontal, - vertical, -} from './styles'; -import { Props } from '@guardian/src-helpers'; - export { radioBrand, radioDefault } from '@guardian/src-foundations/themes'; -type Orientation = 'vertical' | 'horizontal'; - -const orientationStyles = { - vertical: vertical, - horizontal: horizontal, -}; - -interface RadioGroupProps extends Props { - id?: string; - name: string; - label?: string; - hideLabel?: boolean; - supporting?: string | JSX.Element; - orientation?: Orientation; - error?: string; - children: JSX.Element | JSX.Element[]; - cssOverrides?: SerializedStyles | SerializedStyles[]; -} - -const RadioGroup = ({ - id, - name, - label, - hideLabel, - supporting, - orientation = 'vertical', - error, - cssOverrides, - children, - ...props -}: RadioGroupProps) => { - const groupId = id || generateSourceId(); - const legend = label ? ( - - ) : ( - '' - ); - const message = error && ( - {error} - ); - - return ( -
[ - fieldset(theme.radio && theme), - orientationStyles[orientation], - cssOverrides, - ]} - {...props} - > - {legend} - {message} - {React.Children.map(children, (child) => { - return React.cloneElement( - child, - Object.assign( - error - ? { - 'aria-describedby': descriptionId(groupId), - } - : {}, - { - name, - }, - ), - ); - })} -
- ); -}; - -const LabelText = ({ - hasSupportingText, - children, -}: { - hasSupportingText?: boolean; - children: ReactNode; -}) => { - return ( -
[ - hasSupportingText ? labelTextWithSupportingText : '', - labelText(theme.radio && theme), - ]} - className="src-radio-label-text" - > - {children} -
- ); -}; - -const SupportingText = ({ children }: { children: ReactNode }) => { - return ( -
supportingText(theme.radio && theme)}> - {children} -
- ); -}; - -interface RadioProps extends InputHTMLAttributes, Props { - value: string; - checked?: boolean; - defaultChecked?: boolean; - label?: ReactNode; - supporting?: ReactNode; - cssOverrides?: SerializedStyles | SerializedStyles[]; -} - -const Radio = ({ - label: labelContent, - value, - supporting, - checked, - defaultChecked, - cssOverrides, - ...props -}: RadioProps) => { - const isChecked = (): boolean => { - if (checked != null) { - return checked; - } - - return !!defaultChecked; - }; - const radioControl = ( - [radio(theme.radio && theme), cssOverrides]} - value={value} - aria-checked={isChecked()} - defaultChecked={defaultChecked != null ? defaultChecked : undefined} - checked={checked != null ? isChecked() : undefined} - {...props} - /> - ); - - const labelledRadioControl = ( - - ); - - return ( - <>{labelContent || supporting ? labelledRadioControl : radioControl} - ); -}; - -export { RadioGroup, Radio }; +export { RadioGroup } from './RadioGroup'; +export { Radio } from './Radio'; From 884c2f97ffc6cad223c0af28134298ad5972efeb Mon Sep 17 00:00:00 2001 From: Simon Adcock Date: Mon, 16 Aug 2021 12:16:48 +0100 Subject: [PATCH 4/4] refactor stories into component-specific stories files Co-authored-by: Jamie Lynch --- src/core/components/radio/Radio.stories.tsx | 124 ++++++++++++++ .../components/radio/RadioGroup.stories.tsx | 156 ++++++++++++++++++ .../components/radio/radio-group.stories.tsx | 15 -- src/core/components/radio/radio.stories.tsx | 11 -- .../radio/stories/radio-group/controlled.tsx | 34 ---- .../radio/stories/radio-group/error.tsx | 54 ------ .../radio/stories/radio-group/horizontal.tsx | 14 -- .../stories/radio-group/legend-error.tsx | 53 ------ .../radio-group/legend-supporting-media.tsx | 51 ------ .../radio-group/legend-supporting-text.tsx | 51 ------ .../radio/stories/radio-group/legend.tsx | 73 -------- .../radio/stories/radio-group/vertical.tsx | 43 ----- .../stories/radio/supporting-text-only.tsx | 76 --------- .../radio/stories/radio/supporting-text.tsx | 79 --------- .../radio/stories/radio/ungrouped.tsx | 25 --- .../radio/stories/radio/unlabelled.tsx | 15 -- 16 files changed, 280 insertions(+), 594 deletions(-) create mode 100644 src/core/components/radio/Radio.stories.tsx create mode 100644 src/core/components/radio/RadioGroup.stories.tsx delete mode 100644 src/core/components/radio/radio-group.stories.tsx delete mode 100644 src/core/components/radio/radio.stories.tsx delete mode 100644 src/core/components/radio/stories/radio-group/controlled.tsx delete mode 100644 src/core/components/radio/stories/radio-group/error.tsx delete mode 100644 src/core/components/radio/stories/radio-group/horizontal.tsx delete mode 100644 src/core/components/radio/stories/radio-group/legend-error.tsx delete mode 100644 src/core/components/radio/stories/radio-group/legend-supporting-media.tsx delete mode 100644 src/core/components/radio/stories/radio-group/legend-supporting-text.tsx delete mode 100644 src/core/components/radio/stories/radio-group/legend.tsx delete mode 100644 src/core/components/radio/stories/radio-group/vertical.tsx delete mode 100644 src/core/components/radio/stories/radio/supporting-text-only.tsx delete mode 100644 src/core/components/radio/stories/radio/supporting-text.tsx delete mode 100644 src/core/components/radio/stories/radio/ungrouped.tsx delete mode 100644 src/core/components/radio/stories/radio/unlabelled.tsx diff --git a/src/core/components/radio/Radio.stories.tsx b/src/core/components/radio/Radio.stories.tsx new file mode 100644 index 000000000..7af4f80a6 --- /dev/null +++ b/src/core/components/radio/Radio.stories.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { Radio } from './Radio'; +import type { RadioProps } from './Radio'; +import { radioBrand } from './index'; +import { ThemeProvider } from '@emotion/react'; +import type { Story } from '../../../@types/storybook-emotion-10-fixes'; +import { asPlayground, asChromaticStory } from '../../../lib/story-intents'; + +// These types are the right types, but don't work with Storybook v6 which uses Emotion v10 +// import type { Args, Story } from '@storybook/react'; + +export default { + title: 'Source/src-radio/Radio', + component: Radio, + argTypes: { + label: { + control: { + type: 'text', + }, + }, + supporting: { + control: { + type: 'text', + }, + }, + cssOverrides: { + control: null, + }, + }, + args: { + label: 'Red', + value: 'red', + supporting: '', + checked: true, + }, +}; + +const Template: Story = (args: RadioProps) => ; + +// ***************************************************************************** + +export const Playground = Template.bind({}); +asPlayground(Playground); + +// ***************************************************************************** + +export const DefaultLightTheme = Template.bind({}); +asChromaticStory(DefaultLightTheme); + +// ***************************************************************************** + +export const DefaultBrandTheme = (args: RadioProps) => ( + +