Skip to content

Commit

Permalink
Add radio option to ButtonGroup (#20805)
Browse files Browse the repository at this point in the history
* Add radio mode to ButtonGroup with aria attributes

* Destructure child props

* Add tab index for radio buttons

* Add keyboard handlers

* Remove TODO comment

Current behavior is consistent with RadioControl

* Add storybook example

* Add documentation example

* Add StoryShot snapshot

* Mention ButtonGroup in RadioControl

* Convert to using context instead of cloneElement

* Merge refs so forwardRef is still usable with radio group

* Refactor to move button props to button component

* Refactor for readability

* Add comment about default value

* Consolidate ref and className

* Fix useMemo return value

* Update snapshots

Reordered props and removed value prop

* Partially revert snapshots

* Update prop order to match snapshots

* Update comments for clarity

* Move changes to radio-group and radio

* Remove radio button group storybook snapshot

* Update ButtonGroup extra props to override role

* Implement forwardRef for ButtonGroup

* Update snapshot with forwardRef(ButtonGroup)

* Update RadioControl to mention RadioGroup

* Replace Radio/RadioGroup with Reakit version

* Add storybook stories for Radio/RadioGroup

* Update Radio/RadioGroup READMEs

* Update docs manifest

* Add __experimental prefix for Radio/RadioGroup

* Pass through disabled state to children

* Remove Radio ids from stories

* Update snapshots
  • Loading branch information
ajlende authored Apr 8, 2020
1 parent eedd738 commit c6e67ce
Show file tree
Hide file tree
Showing 12 changed files with 567 additions and 7 deletions.
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,12 @@
"markdown_source": "../packages/components/src/radio-control/README.md",
"parent": "components"
},
{
"title": "RadioGroup",
"slug": "radio-group",
"markdown_source": "../packages/components/src/radio-group/README.md",
"parent": "components"
},
{
"title": "RangeControl",
"slug": "range-control",
Expand Down
11 changes: 8 additions & 3 deletions packages/components/src/button-group/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
*/
import classnames from 'classnames';

function ButtonGroup( { className, ...props } ) {
/**
* WordPress dependencies
*/
import { forwardRef } from '@wordpress/element';

function ButtonGroup( { className, ...props }, ref ) {
const classes = classnames( 'components-button-group', className );

return <div { ...props } className={ classes } role="group" />;
return <div ref={ ref } role="group" className={ classes } { ...props } />;
}

export default ButtonGroup;
export default forwardRef( ButtonGroup );
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is specified 1`]
value="00"
/>
</div>
<ButtonGroup
<ForwardRef(ButtonGroup)
className="components-datetime__time-field components-datetime__time-field-am-pm"
>
<ForwardRef(Button)
Expand All @@ -336,7 +336,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is specified 1`]
>
PM
</ForwardRef(Button)>
</ButtonGroup>
</ForwardRef(ButtonGroup)>
</div>
</fieldset>
</div>
Expand Down Expand Up @@ -498,7 +498,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is true 1`] = `
value="00"
/>
</div>
<ButtonGroup
<ForwardRef(ButtonGroup)
className="components-datetime__time-field components-datetime__time-field-am-pm"
>
<ForwardRef(Button)
Expand All @@ -517,7 +517,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is true 1`] = `
>
PM
</ForwardRef(Button)>
</ButtonGroup>
</ForwardRef(ButtonGroup)>
</div>
</fieldset>
</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export { default as PanelRow } from './panel/row';
export { default as Placeholder } from './placeholder';
export { default as Popover } from './popover';
export { default as QueryControls } from './query-controls';
export { default as __experimentalRadio } from './radio';
export { default as __experimentalRadioGroup } from './radio-group';
export { default as RadioControl } from './radio-control';
export { default as RangeControl } from './range-control';
export { default as ResizableBox } from './resizable-box';
Expand Down
11 changes: 11 additions & 0 deletions packages/components/src/radio-context/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';

const RadioContext = createContext( {
state: null,
setState: () => {},
} );

export default RadioContext;
1 change: 1 addition & 0 deletions packages/components/src/radio-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,4 @@ A function that receives the value of the new option that is being selected as i

* To select one or more items from a set, use the `CheckboxControl` component.
* To toggle a single setting on or off, use the `ToggleControl` component.
* To format as a button group, use the `RadioGroup` component.
87 changes: 87 additions & 0 deletions packages/components/src/radio-group/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# RadioGroup

Use a RadioGroup component when you want users to select one option from a small set of options.

![RadioGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png)

## Table of contents

1. [Design guidelines](#design-guidelines)
2. [Development guidelines](#development-guidelines)
3. [Related components](#related-components)

## Design guidelines

### Usage

#### Selected action

Only one option in a radio group can be selected and active at a time. Selecting one option deselects any other.

### Best practices

Radio groups should:

- **Be clearly and accurately labeled.**
- **Clearly communicate that clicking or tapping will trigger an action.**
- **Use established colors appropriately.** For example, only use red buttons for actions that are difficult or impossible to undo.
- **Have consistent locations in the interface.**
- **Have a default option already selected.**

### States

#### Active and available radio groups

A radio group’s state makes it clear which option is active. Hover and focus states express the available selection options for buttons in a button group.

#### Disabled radio groups

Radio groups that cannot be selected can either be given a disabled state, or be hidden.

## Development guidelines

### Usage

#### Controlled

```jsx
import { Radio, RadioGroup } from '@wordpress/components';
import { useState } from '@wordpress/element';

const MyControlledRadioRadioGroup = () => {
const [ checked, setChecked ] = useState( '25' );
return (
<RadioGroup accessibilityLabel="Width" onChange={ setChecked } checked={ checked }>
<Radio value="25">25%</Radio>
<Radio value="50">50%</Radio>
<Radio value="75">75%</Radio>
<Radio value="100">100%</Radio>
</RadioGroup>
);
};
```

#### Uncontrolled

When using the RadioGroup component as an uncontrolled component, the default value can be set with the `defaultChecked` prop.

```jsx
import { Radio, RadioGroup } from '@wordpress/components';
import { useState } from '@wordpress/element';

const MyUncontrolledRadioRadioGroup = () => {
return (
<RadioGroup accessibilityLabel="Width" defaultChecked="25">
<Radio value="25">25%</Radio>
<Radio value="50">50%</Radio>
<Radio value="75">75%</Radio>
<Radio value="100">100%</Radio>
</RadioGroup>
);
};
```

## Related components

- For simple buttons that are related, use a `ButtonGroup` component.
- For traditional radio options, use a `RadioControl` component.
53 changes: 53 additions & 0 deletions packages/components/src/radio-group/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { useRadioState, RadioGroup as ReakitRadioGroup } from 'reakit/Radio';

/**
* WordPress dependencies
*/
import { forwardRef } from '@wordpress/element';

/**
* Internal dependencies
*/
import ButtonGroup from '../button-group';
import RadioContext from '../radio-context';

function RadioGroup(
{
accessibilityLabel,
checked,
defaultChecked,
disabled,
onChange,
...props
},
ref
) {
const radioState = useRadioState( {
state: defaultChecked,
baseId: props.id,
} );
const radioContext = {
...radioState,
disabled,
// controlled or uncontrolled
state: checked || radioState.state,
setState: onChange || radioState.setState,
};

return (
<RadioContext.Provider value={ radioContext }>
<ReakitRadioGroup
ref={ ref }
as={ ButtonGroup }
aria-label={ accessibilityLabel }
{ ...radioState }
{ ...props }
/>
</RadioContext.Provider>
);
}

export default forwardRef( RadioGroup );
71 changes: 71 additions & 0 deletions packages/components/src/radio-group/stories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import Radio from '../../radio';
import RadioGroup from '../';

export default { title: 'Components/RadioGroup', component: RadioGroup };

export const _default = () => {
/* eslint-disable no-restricted-syntax */
return (
<RadioGroup
// id is required for server side rendering
id="default-radiogroup"
accessibilityLabel="options"
defaultChecked="option2"
>
<Radio value="option1">Option 1</Radio>
<Radio value="option2">Option 2</Radio>
<Radio value="option3">Option 3</Radio>
</RadioGroup>
);
/* eslint-enable no-restricted-syntax */
};

export const disabled = () => {
/* eslint-disable no-restricted-syntax */
return (
<RadioGroup
// id is required for server side rendering
id="disabled-radiogroup"
disabled
accessibilityLabel="options"
defaultChecked="option2"
>
<Radio value="option1">Option 1</Radio>
<Radio value="option2">Option 2</Radio>
<Radio value="option3">Option 3</Radio>
</RadioGroup>
);
/* eslint-enable no-restricted-syntax */
};

const ControlledRadioGroupWithState = () => {
const [ checked, setChecked ] = useState( 'option2' );

/* eslint-disable no-restricted-syntax */
return (
<RadioGroup
// id is required for server side rendering
id="controlled-radiogroup"
accessibilityLabel="options"
checked={ checked }
onChange={ setChecked }
>
<Radio value="option1">Option 1</Radio>
<Radio value="option2">Option 2</Radio>
<Radio value="option3">Option 3</Radio>
</RadioGroup>
);
/* eslint-enable no-restricted-syntax */
};

export const controlled = () => {
return <ControlledRadioGroupWithState />;
};
36 changes: 36 additions & 0 deletions packages/components/src/radio/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { Radio as ReakitRadio } from 'reakit/Radio';

/**
* WordPress dependencies
*/
import { useContext, forwardRef } from '@wordpress/element';

/**
* Internal dependencies
*/
import Button from '../button';
import RadioContext from '../radio-context';

function Radio( { children, value, ...props }, ref ) {
const radioContext = useContext( RadioContext );
const checked = radioContext.state === value;

return (
<ReakitRadio
ref={ ref }
as={ Button }
isPrimary={ checked }
isSecondary={ ! checked }
value={ value }
{ ...radioContext }
{ ...props }
>
{ children || value }
</ReakitRadio>
);
}

export default forwardRef( Radio );
20 changes: 20 additions & 0 deletions packages/components/src/radio/stories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Internal dependencies
*/
import RadioGroup from '../../radio-group';
import Radio from '../';

export default { title: 'Components/Radio', component: Radio };

export const _default = () => {
// Radio components must be a descendent of a RadioGroup component.
/* eslint-disable no-restricted-syntax */
return (
// id is required for server side rendering
<RadioGroup id="default-radiogroup" accessibilityLabel="options">
<Radio value="option1">Option 1</Radio>
<Radio value="option2">Option 2</Radio>
</RadioGroup>
);
/* eslint-enable no-restricted-syntax */
};
Loading

0 comments on commit c6e67ce

Please sign in to comment.