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

Add radio option to ButtonGroup #20805

Merged
merged 38 commits into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5ccd36f
Add radio mode to ButtonGroup with aria attributes
ajlende Mar 11, 2020
c942d56
Destructure child props
ajlende Mar 11, 2020
9c85fc3
Add tab index for radio buttons
ajlende Mar 11, 2020
726ac0e
Add keyboard handlers
ajlende Mar 11, 2020
42e671c
Remove TODO comment
ajlende Mar 11, 2020
4a061ff
Add storybook example
ajlende Mar 11, 2020
4373a44
Add documentation example
ajlende Mar 11, 2020
adf8bab
Add StoryShot snapshot
ajlende Mar 11, 2020
cb97066
Mention ButtonGroup in RadioControl
ajlende Mar 11, 2020
0ca5022
Merge branch 'master' of github.com:WordPress/gutenberg into try/radi…
ajlende Mar 24, 2020
a39023b
Merge branch 'master' of github.com:WordPress/gutenberg into try/radi…
ajlende Mar 25, 2020
f798e69
Convert to using context instead of cloneElement
ajlende Mar 25, 2020
1af2d22
Merge refs so forwardRef is still usable with radio group
ajlende Mar 25, 2020
daca280
Refactor to move button props to button component
ajlende Mar 25, 2020
c946eab
Refactor for readability
ajlende Mar 25, 2020
3083dc6
Add comment about default value
ajlende Mar 25, 2020
4242001
Consolidate ref and className
ajlende Mar 26, 2020
86d1088
Fix useMemo return value
ajlende Mar 26, 2020
282ed66
Update snapshots
ajlende Mar 26, 2020
c0d3d51
Partially revert snapshots
ajlende Mar 26, 2020
bd0f097
Update prop order to match snapshots
ajlende Mar 26, 2020
287c9f0
Update comments for clarity
ajlende Mar 26, 2020
a0b7581
Merge branch 'master' of github.com:WordPress/gutenberg into try/radi…
ajlende Mar 26, 2020
213673e
Move changes to radio-group and radio
ajlende Mar 31, 2020
4b2851c
Merge branch 'master' into try/radio-button-group
ajlende Apr 1, 2020
d724209
Remove radio button group storybook snapshot
ajlende Apr 1, 2020
10d7e26
Update ButtonGroup extra props to override role
ajlende Apr 1, 2020
c01c3bf
Implement forwardRef for ButtonGroup
ajlende Apr 1, 2020
3129af7
Update snapshot with forwardRef(ButtonGroup)
ajlende Apr 1, 2020
37dc2b1
Update RadioControl to mention RadioGroup
ajlende Apr 1, 2020
c07e117
Replace Radio/RadioGroup with Reakit version
ajlende Apr 1, 2020
3def111
Add storybook stories for Radio/RadioGroup
ajlende Apr 1, 2020
4f66910
Update Radio/RadioGroup READMEs
ajlende Apr 1, 2020
c611871
Update docs manifest
ajlende Apr 1, 2020
7a417ac
Add __experimental prefix for Radio/RadioGroup
ajlende Apr 4, 2020
538538f
Pass through disabled state to children
ajlende Apr 4, 2020
da54def
Remove Radio ids from stories
ajlende Apr 4, 2020
f2dad34
Update snapshots
ajlende Apr 4, 2020
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
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",
Copy link
Member

Choose a reason for hiding this comment

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

Interesting, it’s experimental but we expose it in developer documentation. I hope to have a look into it and figure out how we can improve it. The good part is that https://developer.wordpress.org/block-editor/components/ is generated from the branch that targets WP 5.4 😃

"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 );
ajlende marked this conversation as resolved.
Show resolved Hide resolved
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.
ajlende marked this conversation as resolved.
Show resolved Hide resolved

## 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,
Copy link
Member

Choose a reason for hiding this comment

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

Very clever! 👍

// 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"
Copy link
Member

Choose a reason for hiding this comment

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

We might start inlining the comment in id 😂

id=“required-for-server-side-rendering-only”?

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