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

Add API docs for src-radio #902

Merged
merged 4 commits into from Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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> = {
Copy link
Member

Choose a reason for hiding this comment

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

is this needed now?

(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}</>
);
};