Skip to content
This repository has been archived by the owner on Dec 6, 2022. It is now read-only.

Commit

Permalink
Add API docs for src-radio (#902)
Browse files Browse the repository at this point in the history
* extend story stub with default args and arg types

* remove api docs from readme

Co-authored-by: Jamie Lynch <jamie.lynch@theguardian.com>

* extract radio components into separate modules

Co-authored-by: Jamie Lynch <jamie.lynch@theguardian.com>

* refactor stories into component-specific stories files

Co-authored-by: Jamie Lynch <jamie.lynch@theguardian.com>

Co-authored-by: Simon Adcock <simonadcock2@gmail.com>
Co-authored-by: Jamie Lynch <jamie.lynch@theguardian.com>
  • Loading branch information
3 people committed Aug 16, 2021
1 parent 36a6c9f commit 8f60b7c
Show file tree
Hide file tree
Showing 21 changed files with 527 additions and 874 deletions.
5 changes: 3 additions & 2 deletions src/@types/storybook-emotion-10-fixes.ts
Expand Up @@ -13,9 +13,10 @@ export type Parameters = {
[key: string]: any;
};

export type Story = {
(arg0: any): JSX.Element;
export type Story<T = Args> = {
(arg0: Args & T): JSX.Element;
args?: Args;
parameters?: Parameters;
argTypes?: Args;
storyName?: string;
};
96 changes: 4 additions & 92 deletions src/core/components/radio/README.md
Expand Up @@ -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 (
<form>
<RadioGroup
name="consent"
orientation="vertical"
label="Do you accept the terms and conditions?"
supporting="The full terms are available on our website"
>
<Radio
value="no"
label="No"
supporting="I do not accept the terms"
checked={selected === 'no'}
onChange={setSelected('no')}
/>
<Radio
value="yes"
label="Yes"
supporting="I accept the terms"
checked={selected === 'yes'}
onChange={setSelected('yes')}
/>,
</RadioGroup>
</form>
);
};
```

## `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

Expand Down
124 changes: 124 additions & 0 deletions 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) => <Radio {...args} />;

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

export const Playground = Template.bind({});
asPlayground(Playground);

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

export const DefaultLightTheme = Template.bind({});
asChromaticStory(DefaultLightTheme);

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

export const DefaultBrandTheme = (args: RadioProps) => (
<ThemeProvider theme={radioBrand}>
<Template {...args} />
</ThemeProvider>
);
DefaultBrandTheme.parameters = {
backgrounds: {
default: 'brandBackground.primary',
},
};
asChromaticStory(DefaultBrandTheme);

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

export const SupportingTextLightTheme = Template.bind({});
SupportingTextLightTheme.args = {
supporting: '#ff0000',
};
asChromaticStory(SupportingTextLightTheme);

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

export const SupportingTextBrandTheme: Story = (args: RadioProps) => (
<ThemeProvider theme={radioBrand}>
<Template {...args} />
</ThemeProvider>
);
SupportingTextBrandTheme.parameters = {
backgrounds: {
default: 'brandBackground.primary',
},
};
SupportingTextBrandTheme.args = {
supporting: '#ff0000',
};
asChromaticStory(SupportingTextBrandTheme);

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

export const SupportingTextOnlyLightTheme = Template.bind({});
SupportingTextOnlyLightTheme.args = {
supporting: '#ff0000',
label: null,
};
asChromaticStory(SupportingTextOnlyLightTheme);

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

export const SupportingTextOnlyBrandTheme = (args: RadioProps) => (
<ThemeProvider theme={radioBrand}>
<Template {...args} />
</ThemeProvider>
);
SupportingTextOnlyBrandTheme.story = {
parameters: {
backgrounds: {
default: 'brandBackground.primary',
},
},
};
SupportingTextOnlyBrandTheme.args = {
supporting: '#ff0000',
label: null,
};
asChromaticStory(SupportingTextOnlyBrandTheme);

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

export const UnlabelledLightTheme = Template.bind({});
UnlabelledLightTheme.args = {
label: undefined,
};
asChromaticStory(UnlabelledLightTheme);
132 changes: 132 additions & 0 deletions 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 (
<div
css={(theme) => [
hasSupportingText ? labelTextWithSupportingText : '',
labelText(theme.radio && theme),
]}
className="src-radio-label-text"
>
{children}
</div>
);
};

const SupportingText = ({ children }: { children: ReactNode }) => {
return (
<div css={(theme) => supportingText(theme.radio && theme)}>
{children}
</div>
);
};

export interface RadioProps
extends InputHTMLAttributes<HTMLInputElement>,
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 = (
<input
type="radio"
css={(theme) => [radio(theme.radio && theme), cssOverrides]}
value={value}
aria-checked={isChecked()}
defaultChecked={defaultChecked != null ? defaultChecked : undefined}
checked={checked != null ? isChecked() : undefined}
{...props}
/>
);

const labelledRadioControl = (
<label
css={(theme) => [
label(theme.radio && theme),
supporting ? labelWithSupportingText : '',
]}
>
{radioControl}
{supporting ? (
<div>
<LabelText hasSupportingText={true}>
{labelContent}
</LabelText>
<SupportingText>{supporting}</SupportingText>
</div>
) : (
<LabelText>{labelContent}</LabelText>
)}
</label>
);

return (
<>{labelContent || supporting ? labelledRadioControl : radioControl}</>
);
};

0 comments on commit 8f60b7c

Please sign in to comment.