From 5ccd36f2da592f99b746806815ef2a708371d557 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 17:15:20 +0000 Subject: [PATCH 01/34] Add radio mode to ButtonGroup with aria attributes --- packages/components/src/button-group/index.js | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 6f4fb672c8c6e..c315d721047b9 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -3,10 +3,41 @@ */ import classnames from 'classnames'; -function ButtonGroup( { className, ...props } ) { +/** + * WordPress dependencies + */ +import { Children, cloneElement } from '@wordpress/element'; + +function ButtonGroup( { + mode, + checked, + onChange, + className, + children, + ...props +} ) { const classes = classnames( 'components-button-group', className ); + const role = mode === 'radio' ? 'radiogroup' : 'group'; - return
; + return ( +
+ { mode === 'radio' + ? Children.map( children, ( child ) => { + // TODO: Handle children witout value props + const isChecked = checked === child.props.value; + return cloneElement( child, { + role: mode, + 'aria-checked': isChecked, + isPrimary: isChecked, + isSecondary: ! isChecked, + onClick() { + onChange( child.props.value ); + }, + } ); + } ) + : children } +
+ ); } export default ButtonGroup; From c942d5682ffc487a8cd2bfec9a9bc012e5f4fe66 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 18:05:01 +0000 Subject: [PATCH 02/34] Destructure child props --- packages/components/src/button-group/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index c315d721047b9..acfe7a0dc4d9f 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -24,14 +24,15 @@ function ButtonGroup( { { mode === 'radio' ? Children.map( children, ( child ) => { // TODO: Handle children witout value props - const isChecked = checked === child.props.value; + const { value } = child.props; + const isChecked = checked === value; return cloneElement( child, { role: mode, 'aria-checked': isChecked, isPrimary: isChecked, isSecondary: ! isChecked, onClick() { - onChange( child.props.value ); + onChange( value ); }, } ); } ) From 9c85fc367d60e4567634e83fd6299e8df620b138 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 18:07:07 +0000 Subject: [PATCH 03/34] Add tab index for radio buttons --- packages/components/src/button-group/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index acfe7a0dc4d9f..1da5d8e780dee 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -22,7 +22,7 @@ function ButtonGroup( { return (
{ mode === 'radio' - ? Children.map( children, ( child ) => { + ? Children.map( children, ( child, index ) => { // TODO: Handle children witout value props const { value } = child.props; const isChecked = checked === value; @@ -31,6 +31,10 @@ function ButtonGroup( { 'aria-checked': isChecked, isPrimary: isChecked, isSecondary: ! isChecked, + tabIndex: + isChecked || ( ! checked && index === 0 ) + ? 0 + : -1, onClick() { onChange( value ); }, From 726ac0e1b8fac5b5e438771d12887b6d09e94e0f Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 19:00:51 +0000 Subject: [PATCH 04/34] Add keyboard handlers --- packages/components/src/button-group/index.js | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 1da5d8e780dee..acc8c02477eb5 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -6,23 +6,25 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Children, cloneElement } from '@wordpress/element'; +import { Children, cloneElement, useRef } from '@wordpress/element'; function ButtonGroup( { mode, checked, onChange, className, - children, + children: reactChildren, ...props } ) { const classes = classnames( 'components-button-group', className ); const role = mode === 'radio' ? 'radiogroup' : 'group'; + const children = Children.toArray( reactChildren ); + const childRefs = useRef( new Map() ); return (
{ mode === 'radio' - ? Children.map( children, ( child, index ) => { + ? children.map( ( child, index ) => { // TODO: Handle children witout value props const { value } = child.props; const isChecked = checked === value; @@ -35,12 +37,47 @@ function ButtonGroup( { isChecked || ( ! checked && index === 0 ) ? 0 : -1, + ref: ( ref ) => + ref === null + ? childRefs.current.delete( index ) + : childRefs.current.set( index, ref ), + onKeyDown( e ) { + if ( + e.key === 'ArrowUp' || + e.key === 'ArrowLeft' + ) { + e.preventDefault(); + + const prevIndex = + ( index - 1 + children.length ) % + children.length; + const prevValue = + children[ prevIndex ].props.value; + + childRefs.current.get( prevIndex ).focus(); + onChange( prevValue ); + } + if ( + e.key === 'ArrowDown' || + e.key === 'ArrowRight' + ) { + e.preventDefault(); + + const nextIndex = + ( index + 1 ) % children.length; + const nextValue = + children[ nextIndex ].props.value; + + childRefs.current.get( nextIndex ).focus(); + onChange( nextValue ); + } + }, onClick() { onChange( value ); }, } ); } ) - : children } + : reactChildren }
); } From 42e671cd7ff871ba84df15fe8d8beefa33ae2d19 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 20:42:17 +0000 Subject: [PATCH 05/34] Remove TODO comment Current behavior is consistent with RadioControl --- packages/components/src/button-group/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index acc8c02477eb5..0701eb4c50ef3 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -25,7 +25,6 @@ function ButtonGroup( {
{ mode === 'radio' ? children.map( ( child, index ) => { - // TODO: Handle children witout value props const { value } = child.props; const isChecked = checked === value; return cloneElement( child, { From 4a061ff5fa7ae5ba454ec12cc8b01dd6bda668c6 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 20:56:48 +0000 Subject: [PATCH 06/34] Add storybook example --- .../src/button-group/stories/index.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/components/src/button-group/stories/index.js b/packages/components/src/button-group/stories/index.js index 50332eede746a..5d947d14e11f4 100644 --- a/packages/components/src/button-group/stories/index.js +++ b/packages/components/src/button-group/stories/index.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + /** * Internal dependencies */ @@ -19,3 +24,18 @@ export const _default = () => { ); }; + +const ButtonGroupWithState = () => { + const [ checked, setChecked ] = useState( 'medium' ); + return ( + + + + + + ); +}; + +export const radioButtonGroup = () => { + return ; +}; From 4373a44f6aa53f634002cfd8ad0bf890f2c78c08 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 21:02:35 +0000 Subject: [PATCH 07/34] Add documentation example --- .../components/src/button-group/README.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/components/src/button-group/README.md b/packages/components/src/button-group/README.md index 49ac6b0619c8f..d51034c72e7e3 100644 --- a/packages/components/src/button-group/README.md +++ b/packages/components/src/button-group/README.md @@ -46,6 +46,8 @@ Button groups that cannot be selected can either be given a disabled state, or b ### Usage +**As a simple group** + ```jsx import { Button, ButtonGroup } from '@wordpress/components'; @@ -57,6 +59,25 @@ const MyButtonGroup = () => ( ); ``` +**As a radio group** + +```jsx +import { Button, ButtonGroup } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +const MyRadioButtonGroup = () => { + const [ checked, setChecked ] = useState( 'medium' ); + return ( + + + + + + ); +}; +``` + ## Related components - For individual buttons, use a `Button` component. +- For a traditional radio group, use a `RadioControl` component. From adf8bab9a4c7ab61f5efb1552fb237fe06390d7b Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 22:31:35 +0000 Subject: [PATCH 08/34] Add StoryShot snapshot --- storybook/test/__snapshots__/index.js.snap | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/storybook/test/__snapshots__/index.js.snap b/storybook/test/__snapshots__/index.js.snap index 603152a58439e..84787a2b106ff 100644 --- a/storybook/test/__snapshots__/index.js.snap +++ b/storybook/test/__snapshots__/index.js.snap @@ -801,6 +801,50 @@ exports[`Storyshots Components/ButtonGroup Default 1`] = `
`; +exports[`Storyshots Components/ButtonGroup Radio Button Group 1`] = ` +
+ + + +
+`; + exports[`Storyshots Components/Card Default 1`] = ` .emotion-6 { background: #fff; From cb97066f2f9668d151c772559384f3f3c477ebc7 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 11 Mar 2020 22:53:20 +0000 Subject: [PATCH 09/34] Mention ButtonGroup in RadioControl --- packages/components/src/radio-control/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/radio-control/README.md b/packages/components/src/radio-control/README.md index 2a55a81718fb1..52369d0aca971 100644 --- a/packages/components/src/radio-control/README.md +++ b/packages/components/src/radio-control/README.md @@ -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 `ButtonGroup` component with `role="radio"`. From f798e69e13d38633790e03f43df805ca2ce82736 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 15:28:04 -0500 Subject: [PATCH 10/34] Convert to using context instead of cloneElement --- packages/components/src/button-group/index.js | 117 ++++++++++-------- packages/components/src/button/index.js | 15 ++- 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 0701eb4c50ef3..2d605fe589b8a 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -6,7 +6,15 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Children, cloneElement, useRef } from '@wordpress/element'; +import { + Children, + useRef, + createContext, + useEffect, + useState, +} from '@wordpress/element'; + +export const RadioContext = createContext( {} ); function ButtonGroup( { mode, @@ -18,65 +26,64 @@ function ButtonGroup( { } ) { const classes = classnames( 'components-button-group', className ); const role = mode === 'radio' ? 'radiogroup' : 'group'; - const children = Children.toArray( reactChildren ); - const childRefs = useRef( new Map() ); + const childRefs = useRef( [] ); + const [ context, setContext ] = useState( {} ); - return ( -
- { mode === 'radio' - ? children.map( ( child, index ) => { - const { value } = child.props; - const isChecked = checked === value; - return cloneElement( child, { - role: mode, - 'aria-checked': isChecked, - isPrimary: isChecked, - isSecondary: ! isChecked, - tabIndex: - isChecked || ( ! checked && index === 0 ) - ? 0 - : -1, - ref: ( ref ) => - ref === null - ? childRefs.current.delete( index ) - : childRefs.current.set( index, ref ), - onKeyDown( e ) { - if ( - e.key === 'ArrowUp' || - e.key === 'ArrowLeft' - ) { - e.preventDefault(); + useEffect( () => { + const children = Children.toArray( reactChildren ); + const current = {}; + children.forEach( ( child, index ) => { + const { value } = child.props; + const isChecked = checked === value; + current[ value ] = { + role: mode, + 'aria-checked': isChecked, + tabIndex: isChecked || ( ! checked && index === 0 ) ? 0 : -1, + ref: ( ref ) => { + if ( ref === null ) { + delete childRefs.current[ index ]; + } else { + childRefs.current[ index ] = ref; + } + }, + onKeyDown( e ) { + if ( e.key === 'ArrowUp' || e.key === 'ArrowLeft' ) { + e.preventDefault(); - const prevIndex = - ( index - 1 + children.length ) % - children.length; - const prevValue = - children[ prevIndex ].props.value; + const prevIndex = + ( index - 1 + children.length ) % children.length; + const prevValue = children[ prevIndex ].props.value; - childRefs.current.get( prevIndex ).focus(); - onChange( prevValue ); - } - if ( - e.key === 'ArrowDown' || - e.key === 'ArrowRight' - ) { - e.preventDefault(); + childRefs.current[ prevIndex ].focus(); + onChange( prevValue ); + } + if ( e.key === 'ArrowDown' || e.key === 'ArrowRight' ) { + e.preventDefault(); - const nextIndex = - ( index + 1 ) % children.length; - const nextValue = - children[ nextIndex ].props.value; + const nextIndex = ( index + 1 ) % children.length; + const nextValue = children[ nextIndex ].props.value; - childRefs.current.get( nextIndex ).focus(); - onChange( nextValue ); - } - }, - onClick() { - onChange( value ); - }, - } ); - } ) - : reactChildren } + childRefs.current[ nextIndex ].focus(); + onChange( nextValue ); + } + }, + onClick() { + onChange( value ); + }, + }; + } ); + setContext( { current } ); + }, [ reactChildren ] ); + + return ( +
+ { mode === 'radio' ? ( + + { reactChildren } + + ) : ( + reactChildren + ) }
); } diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 7e5acfb6f2ccf..6b0f64dfc126b 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -8,11 +8,12 @@ import { isArray } from 'lodash'; * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; -import { forwardRef } from '@wordpress/element'; +import { forwardRef, useContext } from '@wordpress/element'; /** * Internal dependencies */ +import { RadioContext } from '../button-group'; import Tooltip from '../tooltip'; import Icon from '../icon'; @@ -41,6 +42,7 @@ export function Button( props, ref ) { shortcut, label, children, + value, __experimentalIsFocusable: isFocusable, ...additionalProps } = props; @@ -51,9 +53,13 @@ export function Button( props, ref ) { } ); } + const context = useContext( RadioContext ); + const radioProps = context[ value ] || {}; + const classes = classnames( 'components-button', className, { - 'is-secondary': isDefault || isSecondary, - 'is-primary': isPrimary, + 'is-secondary': + isDefault || isSecondary || radioProps[ 'aria-checked' ] === false, + 'is-primary': isPrimary || radioProps[ 'aria-checked' ] === true, 'is-large': isLarge, 'is-small': isSmall, 'is-tertiary': isTertiary, @@ -106,11 +112,12 @@ export function Button( props, ref ) { const element = ( { icon && } { children } From 1af2d222af9ea1bd97d46a13944f7328408a2152 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 15:29:04 -0500 Subject: [PATCH 11/34] Merge refs so forwardRef is still usable with radio group --- packages/components/src/button/index.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 6b0f64dfc126b..481a5fc2c1396 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -56,6 +56,18 @@ export function Button( props, ref ) { const context = useContext( RadioContext ); const radioProps = context[ value ] || {}; + const refCallback = ( current ) => { + // Merge the refs so you can still use the forwarded ref of the button + // alongside the radio button group ref + [ radioProps.ref, ref ].forEach( ( r ) => { + if ( typeof r === 'function' ) { + r( current ); + } else if ( r ) { + r.current = current; + } + } ); + }; + const classes = classnames( 'components-button', className, { 'is-secondary': isDefault || isSecondary || radioProps[ 'aria-checked' ] === false, @@ -112,12 +124,12 @@ export function Button( props, ref ) { const element = ( { icon && } { children } From daca2802d3a55ad3c975ebbeb0f96932682175f5 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 16:24:48 -0500 Subject: [PATCH 12/34] Refactor to move button props to button component --- packages/components/src/button-group/index.js | 63 +++++++++---------- packages/components/src/button/index.js | 29 +++++++-- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 2d605fe589b8a..790746b8cceab 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -27,58 +27,51 @@ function ButtonGroup( { const classes = classnames( 'components-button-group', className ); const role = mode === 'radio' ? 'radiogroup' : 'group'; const childRefs = useRef( [] ); - const [ context, setContext ] = useState( {} ); + const [ buttons, setButtons ] = useState( {} ); useEffect( () => { const children = Children.toArray( reactChildren ); - const current = {}; + const allButtons = {}; children.forEach( ( child, index ) => { - const { value } = child.props; - const isChecked = checked === value; - current[ value ] = { - role: mode, - 'aria-checked': isChecked, - tabIndex: isChecked || ( ! checked && index === 0 ) ? 0 : -1, - ref: ( ref ) => { + const prevIndex = ( index - 1 + children.length ) % children.length; + const nextIndex = ( index + 1 ) % children.length; + allButtons[ child.props.value ] = { + isChecked: checked === child.props.value, + isFirst: ! checked && index === 0, + onPrev: () => { + childRefs.current[ prevIndex ].focus(); + onChange( children[ prevIndex ].props.value ); + }, + onNext: () => { + childRefs.current[ nextIndex ].focus(); + onChange( children[ nextIndex ].props.value ); + }, + onSelect: () => { + onChange( child.props.value ); + }, + refCallback: ( ref ) => { if ( ref === null ) { delete childRefs.current[ index ]; } else { childRefs.current[ index ] = ref; } }, - onKeyDown( e ) { - if ( e.key === 'ArrowUp' || e.key === 'ArrowLeft' ) { - e.preventDefault(); - - const prevIndex = - ( index - 1 + children.length ) % children.length; - const prevValue = children[ prevIndex ].props.value; - - childRefs.current[ prevIndex ].focus(); - onChange( prevValue ); - } - if ( e.key === 'ArrowDown' || e.key === 'ArrowRight' ) { - e.preventDefault(); - - const nextIndex = ( index + 1 ) % children.length; - const nextValue = children[ nextIndex ].props.value; - - childRefs.current[ nextIndex ].focus(); - onChange( nextValue ); - } - }, - onClick() { - onChange( value ); - }, }; } ); - setContext( { current } ); + setButtons( allButtons ); }, [ reactChildren ] ); return (
{ mode === 'radio' ? ( - + { reactChildren } ) : ( diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 481a5fc2c1396..504c8166efd89 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -53,8 +53,28 @@ export function Button( props, ref ) { } ); } - const context = useContext( RadioContext ); - const radioProps = context[ value ] || {}; + const radioContext = useContext( RadioContext ); + const buttonContext = radioContext.buttons[ value ] || {}; + + const radioProps = { + role: radioContext.mode, + 'aria-checked': buttonContext.isChecked, + tabIndex: buttonContext.isChecked || buttonContext.isFirst ? 0 : -1, + ref: buttonContext.refCallback, + onKeyDown( e ) { + if ( e.key === 'ArrowUp' || e.key === 'ArrowLeft' ) { + e.preventDefault(); + buttonContext.onPrev(); + } + if ( e.key === 'ArrowDown' || e.key === 'ArrowRight' ) { + e.preventDefault(); + buttonContext.onNext(); + } + }, + onClick() { + buttonContext.onSelect(); + }, + }; const refCallback = ( current ) => { // Merge the refs so you can still use the forwarded ref of the button @@ -69,9 +89,10 @@ export function Button( props, ref ) { }; const classes = classnames( 'components-button', className, { + // isChecked strict equality so undefined will return false 'is-secondary': - isDefault || isSecondary || radioProps[ 'aria-checked' ] === false, - 'is-primary': isPrimary || radioProps[ 'aria-checked' ] === true, + isDefault || isSecondary || buttonContext.isChecked === false, + 'is-primary': isPrimary || buttonContext.isChecked === true, 'is-large': isLarge, 'is-small': isSmall, 'is-tertiary': isTertiary, From c946eab4a951c2a69f9e65da072fddc9d4a910c7 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 18:23:31 -0500 Subject: [PATCH 13/34] Refactor for readability --- packages/components/src/button-group/index.js | 99 +++++++--------- packages/components/src/button/index.js | 110 +++++++++++------- 2 files changed, 109 insertions(+), 100 deletions(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 790746b8cceab..d358bbc408cc8 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -6,77 +6,66 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { - Children, - useRef, - createContext, - useEffect, - useState, -} from '@wordpress/element'; +import { Children, useRef, createContext, useMemo } from '@wordpress/element'; -export const RadioContext = createContext( {} ); +export const ButtonGroupContext = createContext( { + mode: null, + buttons: {}, +} ); function ButtonGroup( { mode, checked, onChange, className, - children: reactChildren, + children, ...props } ) { const classes = classnames( 'components-button-group', className ); const role = mode === 'radio' ? 'radiogroup' : 'group'; const childRefs = useRef( [] ); - const [ buttons, setButtons ] = useState( {} ); - useEffect( () => { - const children = Children.toArray( reactChildren ); - const allButtons = {}; - children.forEach( ( child, index ) => { - const prevIndex = ( index - 1 + children.length ) % children.length; - const nextIndex = ( index + 1 ) % children.length; - allButtons[ child.props.value ] = { - isChecked: checked === child.props.value, - isFirst: ! checked && index === 0, - onPrev: () => { - childRefs.current[ prevIndex ].focus(); - onChange( children[ prevIndex ].props.value ); - }, - onNext: () => { - childRefs.current[ nextIndex ].focus(); - onChange( children[ nextIndex ].props.value ); - }, - onSelect: () => { - onChange( child.props.value ); - }, - refCallback: ( ref ) => { - if ( ref === null ) { - delete childRefs.current[ index ]; - } else { - childRefs.current[ index ] = ref; - } - }, - }; - } ); - setButtons( allButtons ); - }, [ reactChildren ] ); + const buttons = useMemo( () => { + const buttonsContext = {}; + if ( mode === 'radio' ) { + const childrenArray = Children.toArray( children ); + childrenArray.forEach( ( child, index ) => { + buttonsContext[ child.props.value ] = { + isChecked: checked === child.props.value, + isFirst: ! checked && index === 0, + onPrev: () => { + const prevIndex = + ( index - 1 + childrenArray.length ) % + childrenArray.length; + childRefs.current[ prevIndex ].focus(); + onChange( childrenArray[ prevIndex ].props.value ); + }, + onNext: () => { + const nextIndex = ( index + 1 ) % childrenArray.length; + childRefs.current[ nextIndex ].focus(); + onChange( childrenArray[ nextIndex ].props.value ); + }, + onSelect: () => { + onChange( child.props.value ); + }, + refCallback: ( ref ) => { + if ( ref === null ) { + delete childRefs.current[ index ]; + } else { + childRefs.current[ index ] = ref; + } + }, + }; + } ); + return buttonsContext; + } + }, [ children, mode, onChange, checked, childRefs ] ); return (
- { mode === 'radio' ? ( - - { reactChildren } - - ) : ( - reactChildren - ) } + + { children } +
); } diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 504c8166efd89..97fd1840d26a2 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -13,7 +13,7 @@ import { forwardRef, useContext } from '@wordpress/element'; /** * Internal dependencies */ -import { RadioContext } from '../button-group'; +import { ButtonGroupContext } from '../button-group'; import Tooltip from '../tooltip'; import Icon from '../icon'; @@ -43,6 +43,8 @@ export function Button( props, ref ) { label, children, value, + onKeyDown, + onClick, __experimentalIsFocusable: isFocusable, ...additionalProps } = props; @@ -53,46 +55,9 @@ export function Button( props, ref ) { } ); } - const radioContext = useContext( RadioContext ); - const buttonContext = radioContext.buttons[ value ] || {}; - - const radioProps = { - role: radioContext.mode, - 'aria-checked': buttonContext.isChecked, - tabIndex: buttonContext.isChecked || buttonContext.isFirst ? 0 : -1, - ref: buttonContext.refCallback, - onKeyDown( e ) { - if ( e.key === 'ArrowUp' || e.key === 'ArrowLeft' ) { - e.preventDefault(); - buttonContext.onPrev(); - } - if ( e.key === 'ArrowDown' || e.key === 'ArrowRight' ) { - e.preventDefault(); - buttonContext.onNext(); - } - }, - onClick() { - buttonContext.onSelect(); - }, - }; - - const refCallback = ( current ) => { - // Merge the refs so you can still use the forwarded ref of the button - // alongside the radio button group ref - [ radioProps.ref, ref ].forEach( ( r ) => { - if ( typeof r === 'function' ) { - r( current ); - } else if ( r ) { - r.current = current; - } - } ); - }; - - const classes = classnames( 'components-button', className, { - // isChecked strict equality so undefined will return false - 'is-secondary': - isDefault || isSecondary || buttonContext.isChecked === false, - 'is-primary': isPrimary || buttonContext.isChecked === true, + let classes = classnames( 'components-button', className, { + 'is-secondary': isDefault || isSecondary, + 'is-primary': isPrimary, 'is-large': isLarge, 'is-small': isSmall, 'is-tertiary': isTertiary, @@ -128,6 +93,60 @@ export function Button( props, ref ) { } } + const groupContext = useContext( ButtonGroupContext ); + const buttonContext = groupContext.buttons[ value ]; + + let refs = ref; + + if ( groupContext.mode === 'radio' && buttonContext ) { + const { + isChecked, + isFirst, + onPrev, + onNext, + onSelect, + refCallback, + } = buttonContext; + + Object.assign( tagProps, { + role: groupContext.mode, + 'aria-checked': isChecked, + tabIndex: isChecked || isFirst ? 0 : -1, + onKeyDown( e ) { + if ( typeof onKeyDown === 'function' ) onKeyDown( e ); + if ( e.key === 'ArrowUp' || e.key === 'ArrowLeft' ) { + e.preventDefault(); + onPrev(); + } + if ( e.key === 'ArrowDown' || e.key === 'ArrowRight' ) { + e.preventDefault(); + onNext(); + } + }, + onClick( e ) { + if ( typeof onClick === 'function' ) onClick( e ); + onSelect(); + }, + } ); + + // Automatically update the visual style + classes = classnames( classes, { + 'is-secondary': ! isChecked, + 'is-primary': isChecked, + } ); + + // Also handle both the forwardRef ref and the button group ref + refs = ( current ) => { + refCallback( current ); + + if ( typeof ref === 'function' ) { + ref( current ); + } else if ( ref ) { + ref.current = current; + } + }; + } + // Should show the tooltip if... const shouldShowTooltip = ! trulyDisabled && @@ -145,12 +164,13 @@ export function Button( props, ref ) { const element = ( { icon && } { children } From 3083dc6291a523d25bfdb96385dccd1041766cdc Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 18:27:13 -0500 Subject: [PATCH 14/34] Add comment about default value --- packages/components/src/button-group/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index d358bbc408cc8..3a228ece9c79e 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { Children, useRef, createContext, useMemo } from '@wordpress/element'; +// Default values for when a button isn't a child of a group export const ButtonGroupContext = createContext( { mode: null, buttons: {}, From 4242001d0828a56e72ab65a5fc986a57a85cb94d Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 19:19:18 -0500 Subject: [PATCH 15/34] Consolidate ref and className --- packages/components/src/button/index.js | 34 ++++++++++--------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 97fd1840d26a2..961679c93520a 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -55,7 +55,7 @@ export function Button( props, ref ) { } ); } - let classes = classnames( 'components-button', className, { + const classes = classnames( 'components-button', className, { 'is-secondary': isDefault || isSecondary, 'is-primary': isPrimary, 'is-large': isLarge, @@ -96,8 +96,6 @@ export function Button( props, ref ) { const groupContext = useContext( ButtonGroupContext ); const buttonContext = groupContext.buttons[ value ]; - let refs = ref; - if ( groupContext.mode === 'radio' && buttonContext ) { const { isChecked, @@ -127,24 +125,20 @@ export function Button( props, ref ) { if ( typeof onClick === 'function' ) onClick( e ); onSelect(); }, - } ); + ref: ( current ) => { + refCallback( current ); - // Automatically update the visual style - classes = classnames( classes, { - 'is-secondary': ! isChecked, - 'is-primary': isChecked, + if ( typeof ref === 'function' ) { + ref( current ); + } else if ( ref ) { + ref.current = current; + } + }, + className: classnames( classes, { + 'is-secondary': ! isChecked, + 'is-primary': isChecked, + } ), } ); - - // Also handle both the forwardRef ref and the button group ref - refs = ( current ) => { - refCallback( current ); - - if ( typeof ref === 'function' ) { - ref( current ); - } else if ( ref ) { - ref.current = current; - } - }; } // Should show the tooltip if... @@ -164,7 +158,7 @@ export function Button( props, ref ) { const element = ( Date: Wed, 25 Mar 2020 19:23:05 -0500 Subject: [PATCH 16/34] Fix useMemo return value --- packages/components/src/button-group/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 3a228ece9c79e..022f2b27f0107 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -58,8 +58,8 @@ function ButtonGroup( { }, }; } ); - return buttonsContext; } + return buttonsContext; }, [ children, mode, onChange, checked, childRefs ] ); return ( From 282ed6699b40a5d36856f16c8293849b2336a83f Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 19:29:12 -0500 Subject: [PATCH 17/34] Update snapshots Reordered props and removed value prop --- .../components/link-control/test/__snapshots__/index.js.snap | 2 +- .../plugin-post-publish-panel/test/__snapshots__/index.js.snap | 2 +- .../plugin-pre-publish-panel/test/__snapshots__/index.js.snap | 2 +- storybook/test/__snapshots__/index.js.snap | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap index 857f93071f8b0..387abde2be789 100644 --- a/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Basic rendering should render 1`] = `""`; +exports[`Basic rendering should render 1`] = `""`; diff --git a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap index aafa7c918f179..56d5662877900 100644 --- a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginPostPublishPanel renders fill properly 1`] = `"

My panel content
"`; +exports[`PluginPostPublishPanel renders fill properly 1`] = `"

My panel content
"`; diff --git a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap index 6087ec62497e5..a4521197c18a8 100644 --- a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginPrePublishPanel renders fill properly 1`] = `"

My panel content
"`; +exports[`PluginPrePublishPanel renders fill properly 1`] = `"

My panel content
"`; diff --git a/storybook/test/__snapshots__/index.js.snap b/storybook/test/__snapshots__/index.js.snap index 6796250dc4fe9..ae1230c9a1a20 100644 --- a/storybook/test/__snapshots__/index.js.snap +++ b/storybook/test/__snapshots__/index.js.snap @@ -814,7 +814,6 @@ exports[`Storyshots Components/ButtonGroup Radio Button Group 1`] = ` role="radio" tabIndex={-1} type="button" - value="small" > Small @@ -826,7 +825,6 @@ exports[`Storyshots Components/ButtonGroup Radio Button Group 1`] = ` role="radio" tabIndex={0} type="button" - value="medium" > Medium @@ -838,7 +836,6 @@ exports[`Storyshots Components/ButtonGroup Radio Button Group 1`] = ` role="radio" tabIndex={-1} type="button" - value="large" > Large From c0d3d51751f7b84aa088abb2e8c35e4534b9f657 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 20:54:14 -0500 Subject: [PATCH 18/34] Partially revert snapshots --- .../components/link-control/test/__snapshots__/index.js.snap | 2 +- .../plugin-post-publish-panel/test/__snapshots__/index.js.snap | 2 +- .../plugin-pre-publish-panel/test/__snapshots__/index.js.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap index 387abde2be789..857f93071f8b0 100644 --- a/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Basic rendering should render 1`] = `""`; +exports[`Basic rendering should render 1`] = `""`; diff --git a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap index 56d5662877900..aafa7c918f179 100644 --- a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginPostPublishPanel renders fill properly 1`] = `"

My panel content
"`; +exports[`PluginPostPublishPanel renders fill properly 1`] = `"

My panel content
"`; diff --git a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap index a4521197c18a8..6087ec62497e5 100644 --- a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginPrePublishPanel renders fill properly 1`] = `"

My panel content
"`; +exports[`PluginPrePublishPanel renders fill properly 1`] = `"

My panel content
"`; From bd0f0970d2b066c9233743ccd16cf50bbda0fdc6 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 20:54:48 -0500 Subject: [PATCH 19/34] Update prop order to match snapshots --- packages/components/src/button/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 961679c93520a..53bbfeca93289 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -80,13 +80,14 @@ export function Button( props, ref ) { 'aria-pressed': isPressed, }; + const disabledEventProps = {}; if ( disabled && isFocusable ) { // In this case, the button will be disabled, but still focusable and // perceivable by screen reader users. tagProps[ 'aria-disabled' ] = true; for ( const disabledEvent of disabledEventsOnDisabledButton ) { - additionalProps[ disabledEvent ] = ( event ) => { + disabledEventProps[ disabledEvent ] = ( event ) => { event.stopPropagation(); event.preventDefault(); }; @@ -95,6 +96,7 @@ export function Button( props, ref ) { const groupContext = useContext( ButtonGroupContext ); const buttonContext = groupContext.buttons[ value ]; + const groupProps = {}; if ( groupContext.mode === 'radio' && buttonContext ) { const { @@ -106,7 +108,7 @@ export function Button( props, ref ) { refCallback, } = buttonContext; - Object.assign( tagProps, { + Object.assign( groupProps, { role: groupContext.mode, 'aria-checked': isChecked, tabIndex: isChecked || isFirst ? 0 : -1, @@ -159,12 +161,14 @@ export function Button( props, ref ) { const element = ( { icon && } { children } From 287c9f0cdd7df6588fed2ecddca029ab8fe98002 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 25 Mar 2020 21:09:54 -0500 Subject: [PATCH 20/34] Update comments for clarity --- packages/components/src/button/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 53bbfeca93289..e9074702c3988 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -81,6 +81,7 @@ export function Button( props, ref ) { }; const disabledEventProps = {}; + if ( disabled && isFocusable ) { // In this case, the button will be disabled, but still focusable and // perceivable by screen reader users. @@ -112,6 +113,7 @@ export function Button( props, ref ) { role: groupContext.mode, 'aria-checked': isChecked, tabIndex: isChecked || isFirst ? 0 : -1, + // Pass through events and also handle the keyboard controls onKeyDown( e ) { if ( typeof onKeyDown === 'function' ) onKeyDown( e ); if ( e.key === 'ArrowUp' || e.key === 'ArrowLeft' ) { @@ -123,10 +125,13 @@ export function Button( props, ref ) { onNext(); } }, + // May get overridden when disabled && isFocusable onClick( e ) { if ( typeof onClick === 'function' ) onClick( e ); onSelect(); }, + // Grab a ref for handling onPrev and onNext in the group, but also + // pass through the ref from forwardRef ref: ( current ) => { refCallback( current ); @@ -136,6 +141,7 @@ export function Button( props, ref ) { ref.current = current; } }, + // Automatically handle the styling for the selected button className: classnames( classes, { 'is-secondary': ! isChecked, 'is-primary': isChecked, @@ -167,8 +173,8 @@ export function Button( props, ref ) { aria-label={ additionalProps[ 'aria-label' ] || label } onKeyDown={ onKeyDown } onClick={ onClick } - { ...groupProps } // overrides className, onKeyDown, and onClick - { ...disabledEventProps } // overrides onMouseDown and onClick + { ...groupProps } // Overrides className, onKeyDown, and onClick + { ...disabledEventProps } // May override onMouseDown and/or onClick > { icon && } { children } From 213673e8b83f4f38ef9719147c9c79cda207243f Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 31 Mar 2020 15:18:28 -0500 Subject: [PATCH 21/34] Move changes to radio-group and radio --- .../components/src/button-group/README.md | 21 -- packages/components/src/button-group/index.js | 66 +---- .../src/button-group/stories/index.js | 20 -- packages/components/src/button/index.js | 70 +---- packages/components/src/index.js | 2 + packages/components/src/radio-group/README.md | 83 ++++++ packages/components/src/radio-group/index.js | 74 +++++ .../src/radio-group/stories/index.js | 41 +++ .../components/src/radio-group/style.scss | 35 +++ packages/components/src/radio/README.md | 145 ++++++++++ packages/components/src/radio/deprecated.js | 29 ++ packages/components/src/radio/index.js | 199 +++++++++++++ packages/components/src/radio/index.native.js | 198 +++++++++++++ .../components/src/radio/stories/index.js | 174 ++++++++++++ .../components/src/radio/stories/style.css | 8 + packages/components/src/radio/style.scss | 268 ++++++++++++++++++ packages/components/src/radio/test/index.js | 205 ++++++++++++++ packages/components/src/style.scss | 2 + 18 files changed, 1468 insertions(+), 172 deletions(-) create mode 100644 packages/components/src/radio-group/README.md create mode 100644 packages/components/src/radio-group/index.js create mode 100644 packages/components/src/radio-group/stories/index.js create mode 100644 packages/components/src/radio-group/style.scss create mode 100644 packages/components/src/radio/README.md create mode 100644 packages/components/src/radio/deprecated.js create mode 100644 packages/components/src/radio/index.js create mode 100644 packages/components/src/radio/index.native.js create mode 100644 packages/components/src/radio/stories/index.js create mode 100644 packages/components/src/radio/stories/style.css create mode 100644 packages/components/src/radio/style.scss create mode 100644 packages/components/src/radio/test/index.js diff --git a/packages/components/src/button-group/README.md b/packages/components/src/button-group/README.md index d51034c72e7e3..49ac6b0619c8f 100644 --- a/packages/components/src/button-group/README.md +++ b/packages/components/src/button-group/README.md @@ -46,8 +46,6 @@ Button groups that cannot be selected can either be given a disabled state, or b ### Usage -**As a simple group** - ```jsx import { Button, ButtonGroup } from '@wordpress/components'; @@ -59,25 +57,6 @@ const MyButtonGroup = () => ( ); ``` -**As a radio group** - -```jsx -import { Button, ButtonGroup } from '@wordpress/components'; -import { useState } from '@wordpress/element'; - -const MyRadioButtonGroup = () => { - const [ checked, setChecked ] = useState( 'medium' ); - return ( - - - - - - ); -}; -``` - ## Related components - For individual buttons, use a `Button` component. -- For a traditional radio group, use a `RadioControl` component. diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 022f2b27f0107..6f4fb672c8c6e 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -3,72 +3,10 @@ */ import classnames from 'classnames'; -/** - * WordPress dependencies - */ -import { Children, useRef, createContext, useMemo } from '@wordpress/element'; - -// Default values for when a button isn't a child of a group -export const ButtonGroupContext = createContext( { - mode: null, - buttons: {}, -} ); - -function ButtonGroup( { - mode, - checked, - onChange, - className, - children, - ...props -} ) { +function ButtonGroup( { className, ...props } ) { const classes = classnames( 'components-button-group', className ); - const role = mode === 'radio' ? 'radiogroup' : 'group'; - const childRefs = useRef( [] ); - - const buttons = useMemo( () => { - const buttonsContext = {}; - if ( mode === 'radio' ) { - const childrenArray = Children.toArray( children ); - childrenArray.forEach( ( child, index ) => { - buttonsContext[ child.props.value ] = { - isChecked: checked === child.props.value, - isFirst: ! checked && index === 0, - onPrev: () => { - const prevIndex = - ( index - 1 + childrenArray.length ) % - childrenArray.length; - childRefs.current[ prevIndex ].focus(); - onChange( childrenArray[ prevIndex ].props.value ); - }, - onNext: () => { - const nextIndex = ( index + 1 ) % childrenArray.length; - childRefs.current[ nextIndex ].focus(); - onChange( childrenArray[ nextIndex ].props.value ); - }, - onSelect: () => { - onChange( child.props.value ); - }, - refCallback: ( ref ) => { - if ( ref === null ) { - delete childRefs.current[ index ]; - } else { - childRefs.current[ index ] = ref; - } - }, - }; - } ); - } - return buttonsContext; - }, [ children, mode, onChange, checked, childRefs ] ); - return ( -
- - { children } - -
- ); + return
; } export default ButtonGroup; diff --git a/packages/components/src/button-group/stories/index.js b/packages/components/src/button-group/stories/index.js index 5d947d14e11f4..50332eede746a 100644 --- a/packages/components/src/button-group/stories/index.js +++ b/packages/components/src/button-group/stories/index.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - /** * Internal dependencies */ @@ -24,18 +19,3 @@ export const _default = () => { ); }; - -const ButtonGroupWithState = () => { - const [ checked, setChecked ] = useState( 'medium' ); - return ( - - - - - - ); -}; - -export const radioButtonGroup = () => { - return ; -}; diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index e9074702c3988..7e5acfb6f2ccf 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -8,12 +8,11 @@ import { isArray } from 'lodash'; * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; -import { forwardRef, useContext } from '@wordpress/element'; +import { forwardRef } from '@wordpress/element'; /** * Internal dependencies */ -import { ButtonGroupContext } from '../button-group'; import Tooltip from '../tooltip'; import Icon from '../icon'; @@ -42,9 +41,6 @@ export function Button( props, ref ) { shortcut, label, children, - value, - onKeyDown, - onClick, __experimentalIsFocusable: isFocusable, ...additionalProps } = props; @@ -80,75 +76,19 @@ export function Button( props, ref ) { 'aria-pressed': isPressed, }; - const disabledEventProps = {}; - if ( disabled && isFocusable ) { // In this case, the button will be disabled, but still focusable and // perceivable by screen reader users. tagProps[ 'aria-disabled' ] = true; for ( const disabledEvent of disabledEventsOnDisabledButton ) { - disabledEventProps[ disabledEvent ] = ( event ) => { + additionalProps[ disabledEvent ] = ( event ) => { event.stopPropagation(); event.preventDefault(); }; } } - const groupContext = useContext( ButtonGroupContext ); - const buttonContext = groupContext.buttons[ value ]; - const groupProps = {}; - - if ( groupContext.mode === 'radio' && buttonContext ) { - const { - isChecked, - isFirst, - onPrev, - onNext, - onSelect, - refCallback, - } = buttonContext; - - Object.assign( groupProps, { - role: groupContext.mode, - 'aria-checked': isChecked, - tabIndex: isChecked || isFirst ? 0 : -1, - // Pass through events and also handle the keyboard controls - onKeyDown( e ) { - if ( typeof onKeyDown === 'function' ) onKeyDown( e ); - if ( e.key === 'ArrowUp' || e.key === 'ArrowLeft' ) { - e.preventDefault(); - onPrev(); - } - if ( e.key === 'ArrowDown' || e.key === 'ArrowRight' ) { - e.preventDefault(); - onNext(); - } - }, - // May get overridden when disabled && isFocusable - onClick( e ) { - if ( typeof onClick === 'function' ) onClick( e ); - onSelect(); - }, - // Grab a ref for handling onPrev and onNext in the group, but also - // pass through the ref from forwardRef - ref: ( current ) => { - refCallback( current ); - - if ( typeof ref === 'function' ) { - ref( current ); - } else if ( ref ) { - ref.current = current; - } - }, - // Automatically handle the styling for the selected button - className: classnames( classes, { - 'is-secondary': ! isChecked, - 'is-primary': isChecked, - } ), - } ); - } - // Should show the tooltip if... const shouldShowTooltip = ! trulyDisabled && @@ -166,15 +106,11 @@ export function Button( props, ref ) { const element = ( { icon && } { children } diff --git a/packages/components/src/index.js b/packages/components/src/index.js index e02dde7f7217d..4fdb930635f67 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -70,6 +70,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 Radio } from './radio'; +export { default as RadioGroup } from './radio-group'; export { default as RadioControl } from './radio-control'; export { default as RangeControl } from './range-control'; export { default as ResizableBox } from './resizable-box'; diff --git a/packages/components/src/radio-group/README.md b/packages/components/src/radio-group/README.md new file mode 100644 index 0000000000000..d51034c72e7e3 --- /dev/null +++ b/packages/components/src/radio-group/README.md @@ -0,0 +1,83 @@ +# ButtonGroup + +ButtonGroup can be used to group any related buttons together. To emphasize related buttons, a group should share a common container. + +![ButtonGroup 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 + +![ButtonGroup selection](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1544127594329_ButtonGroup-Do.png) + +**Do** +Only one option in a button group can be selected and active at a time. Selecting one option deselects any other. + +### Best practices + +Button 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.** + +### States + +![ButtonGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png) + +**Active and available button groups** + +A button group’s state makes it clear which button is active. Hover and focus states express the available selection options for buttons in a button group. + +**Disabled button groups** + +Button groups that cannot be selected can either be given a disabled state, or be hidden. + +## Development guidelines + +### Usage + +**As a simple group** + +```jsx +import { Button, ButtonGroup } from '@wordpress/components'; + +const MyButtonGroup = () => ( + + + + +); +``` + +**As a radio group** + +```jsx +import { Button, ButtonGroup } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +const MyRadioButtonGroup = () => { + const [ checked, setChecked ] = useState( 'medium' ); + return ( + + + + + + ); +}; +``` + +## Related components + +- For individual buttons, use a `Button` component. +- For a traditional radio group, use a `RadioControl` component. diff --git a/packages/components/src/radio-group/index.js b/packages/components/src/radio-group/index.js new file mode 100644 index 0000000000000..022f2b27f0107 --- /dev/null +++ b/packages/components/src/radio-group/index.js @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Children, useRef, createContext, useMemo } from '@wordpress/element'; + +// Default values for when a button isn't a child of a group +export const ButtonGroupContext = createContext( { + mode: null, + buttons: {}, +} ); + +function ButtonGroup( { + mode, + checked, + onChange, + className, + children, + ...props +} ) { + const classes = classnames( 'components-button-group', className ); + const role = mode === 'radio' ? 'radiogroup' : 'group'; + const childRefs = useRef( [] ); + + const buttons = useMemo( () => { + const buttonsContext = {}; + if ( mode === 'radio' ) { + const childrenArray = Children.toArray( children ); + childrenArray.forEach( ( child, index ) => { + buttonsContext[ child.props.value ] = { + isChecked: checked === child.props.value, + isFirst: ! checked && index === 0, + onPrev: () => { + const prevIndex = + ( index - 1 + childrenArray.length ) % + childrenArray.length; + childRefs.current[ prevIndex ].focus(); + onChange( childrenArray[ prevIndex ].props.value ); + }, + onNext: () => { + const nextIndex = ( index + 1 ) % childrenArray.length; + childRefs.current[ nextIndex ].focus(); + onChange( childrenArray[ nextIndex ].props.value ); + }, + onSelect: () => { + onChange( child.props.value ); + }, + refCallback: ( ref ) => { + if ( ref === null ) { + delete childRefs.current[ index ]; + } else { + childRefs.current[ index ] = ref; + } + }, + }; + } ); + } + return buttonsContext; + }, [ children, mode, onChange, checked, childRefs ] ); + + return ( +
+ + { children } + +
+ ); +} + +export default ButtonGroup; diff --git a/packages/components/src/radio-group/stories/index.js b/packages/components/src/radio-group/stories/index.js new file mode 100644 index 0000000000000..5d947d14e11f4 --- /dev/null +++ b/packages/components/src/radio-group/stories/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Button from '../../button'; +import ButtonGroup from '../'; + +export default { title: 'Components/ButtonGroup', component: ButtonGroup }; + +export const _default = () => { + const style = { margin: '0 4px' }; + return ( + + + + + ); +}; + +const ButtonGroupWithState = () => { + const [ checked, setChecked ] = useState( 'medium' ); + return ( + + + + + + ); +}; + +export const radioButtonGroup = () => { + return ; +}; diff --git a/packages/components/src/radio-group/style.scss b/packages/components/src/radio-group/style.scss new file mode 100644 index 0000000000000..f1dace4f64797 --- /dev/null +++ b/packages/components/src/radio-group/style.scss @@ -0,0 +1,35 @@ +.components-button-group { + display: inline-block; + + .components-button { + border-radius: 0; + display: inline-flex; + color: $theme-color; + box-shadow: inset 0 0 0 $border-width $theme-color; + + & + .components-button { + margin-left: -1px; + } + + &:first-child { + border-radius: $radius-block-ui 0 0 $radius-block-ui; + } + + &:last-child { + border-radius: 0 $radius-block-ui $radius-block-ui 0; + } + + // The focused button should be elevated so the focus ring isn't cropped, + // as should the active button, because it has a different border color. + &:focus, + &.is-primary { + position: relative; + z-index: z-index(".components-button {:focus or .is-primary}"); + } + + // The active button should look pressed. + &.is-primary { + box-shadow: inset 0 0 0 $border-width $theme-color; + } + } +} diff --git a/packages/components/src/radio/README.md b/packages/components/src/radio/README.md new file mode 100644 index 0000000000000..bf71236c408ae --- /dev/null +++ b/packages/components/src/radio/README.md @@ -0,0 +1,145 @@ +# Button +Buttons let users take actions and make choices with a single click or tap. + +![Button components](https://make.wordpress.org/design/files/2019/03/button.png) + +## Table of contents + +1. [Design guidelines](#design-guidelines) +2. [Development guidelines](#development-guidelines) +3. [Related components](#related-components) + +## Design guidelines + +### Usage + +Buttons tell users what actions they can take and give them a way to interact with the interface. You’ll find them throughout a UI, particularly in places like: + +- Modals +- Forms +- Toolbars + +### Best practices + +Buttons 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. +- **Prioritize the most important actions.** This helps users focus. Too many calls to action on one screen can be confusing, making users unsure what to do next. +- **Have consistent locations in the interface.** + +### Content guidelines + +Buttons should be clear and predictable—users should be able to anticipate what will happen when they click a button. Never deceive a user by mislabeling a button. + +Buttons text should lead with a strong verb that encourages action, and add a noun that clarifies what will actually change. The only exceptions are common actions like Save, Close, Cancel, or OK. Otherwise, use the {verb}+{noun} format to ensure that your button gives the user enough information. + +Button text should also be quickly scannable — avoid unnecessary words and articles like the, an, or a. + +### Types + +#### Link button + +Link buttons have low emphasis. They don’t stand out much on the page, so they’re used for less-important actions. What’s less important can vary based on context, but it’s usually a supplementary action to the main action we want someone to take. Link buttons are also useful when you don’t want to distract from the content. + +![Link button](https://make.wordpress.org/design/files/2019/03/link-button.png) + +#### Default button + +Default buttons have medium emphasis. The button appearance helps differentiate them from the page background, so they’re useful when you want more emphasis than a link button offers. + +![Default button](https://make.wordpress.org/design/files/2019/03/default-button.png) + +#### Primary button + +Primary buttons have high emphasis. Their color fill and shadow means they pop off the background. + +Since a high-emphasis button commands the most attention, a layout should contain a single primary button. This makes it clear that other buttons have less importance and helps users understand when an action requires their attention. + +![Primary button](https://make.wordpress.org/design/files/2019/03/primary-button.png) + +#### Text label + +All button types use text labels to describe the action that happens when a user taps a button. If there’s no text label, there should be an icon to signify what the button does. + +![](https://make.wordpress.org/design/files/2019/03/do-link-button.png) + +**Do** +Use color to distinguish link button labels from other text. + +![](https://make.wordpress.org/design/files/2019/03/dont-wrap-button-text.png) + +**Don’t** +Don’t wrap button text. For maximum legibility, keep text labels on a single line. + +### Hierarchy + +![A layout with a single prominent button](https://make.wordpress.org/design/files/2019/03/button.png) + +A layout should contain a single prominently-located button. If multiple buttons are required, a single high-emphasis button can be joined by medium- and low-emphasis buttons mapped to less-important actions. When using multiple buttons, make sure the available state of one button doesn’t look like the disabled state of another. + +![A diagram showing high emphasis at the top, medium emphasis in the middle, and low emphasis at the bottom](https://make.wordpress.org/design/files/2019/03/button-hierarchy.png) + +A button’s level of emphasis helps determine its appearance, typography, and placement. + +#### Placement + +Use button types to express different emphasis levels for all the actions a user can perform. + +![A link, default, and primary button](https://make.wordpress.org/design/files/2019/03/button-layout.png) + +This screen layout uses: + +1. A primary button for high emphasis. +2. A default button for medium emphasis. +3. A link button for low emphasis. + +Placement best practices: + +- **Do**: When using multiple buttons in a row, show users which action is more important by placing it next to a button with a lower emphasis (e.g. a primary button next to a default button, or a default button next to a link button). +- **Don’t**: Don’t place two primary buttons next to one another — they compete for focus. Only use one primary button per view. +- **Don’t**: Don’t place a button below another button if there is space to place them side by side. +- **Caution**: Avoid using too many buttons on a single page. When designing pages in the app or website, think about the most important actions for users to take. Too many calls to action can cause confusion and make users unsure what to do next — we always want users to feel confident and capable. + +## Development guidelines + +### Usage + +Renders a button with default style. + +```jsx +import { Button } from "@wordpress/components"; + +const MyButton = () => ( + +); +``` + +### Props + +The presence of a `href` prop determines whether an `anchor` element is rendered instead of a `button`. + +Props not included in this set will be applied to the `a` or `button` element. + +Name | Type | Default | Description +--- | --- | --- | --- +`disabled` | `bool` | `false` | Whether the button is disabled. If `true`, this will force a `button` element to be rendered. +`href` | `string` | `undefined` | If provided, renders `a` instead of `button`. +`isSecondary` | `bool` | `false` | Renders a default button style. +`isPrimary` | `bool` | `false` | Renders a primary button style. +`isTertiary` | `bool` | `false` | Renders a text-based button style. +`isDestructive` | `bool` | `false` | Renders a red text-based button style to indicate destructive behavior. +`isLarge` | `bool` | `false` | Increases the size of the button. +`isSmall` | `bool` | `false` | Decreases the size of the button. +`isPressed` | `bool` | `false` | Renders a pressed button style. +`isBusy` | `bool` | `false` | Indicates activity while a action is being performed. +`isLink` | `bool` | `false` | Renders a button with an anchor style. +`focus` | `bool` | `false` | Whether the button is focused. + +## Related components + +- To group buttons together, use the `ButtonGroup` component. + diff --git a/packages/components/src/radio/deprecated.js b/packages/components/src/radio/deprecated.js new file mode 100644 index 0000000000000..6c676d0021581 --- /dev/null +++ b/packages/components/src/radio/deprecated.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Button from '../button'; + +function IconButton( { labelPosition, size, tooltip, label, ...props }, ref ) { + deprecated( 'wp.components.IconButton', { + alternative: 'wp.components.Button', + } ); + + return ( + ; +}; + +export const primary = () => { + const label = text( 'Label', 'Primary Button' ); + + return ; +}; + +export const secondary = () => { + const label = text( 'Label', 'Secondary Button' ); + + return ; +}; + +export const tertiary = () => { + const label = text( 'Label', 'Tertiary Button' ); + + return ; +}; + +export const small = () => { + const label = text( 'Label', 'Small Button' ); + + return ; +}; + +export const pressed = () => { + const label = text( 'Label', 'Pressed Button' ); + + return ; +}; + +export const disabled = () => { + const label = text( 'Label', 'Disabled Button' ); + + return ; +}; + +export const disabledFocusable = () => { + const label = text( 'Label', 'Disabled Button' ); + + return ( + + ); +}; + +export const link = () => { + const label = text( 'Label', 'Link Button' ); + + return ( + + ); +}; + +export const disabledLink = () => { + const label = text( 'Label', 'Disabled Link Button' ); + + return ( + + ); +}; + +export const icon = () => { + const usedIcon = text( 'Icon', 'ellipsis' ); + const label = text( 'Label', 'More' ); + const size = number( 'Size' ); + + return + + + + +
+ +

Regular Buttons

+
+ + + + + +
+
+ ); +}; diff --git a/packages/components/src/radio/stories/style.css b/packages/components/src/radio/stories/style.css new file mode 100644 index 0000000000000..239018d6a298a --- /dev/null +++ b/packages/components/src/radio/stories/style.css @@ -0,0 +1,8 @@ +.story-buttons-container { + display: flex; +} + +.story-buttons-container > * { + margin-right: 10px; + margin-bottom: 10px; +} diff --git a/packages/components/src/radio/style.scss b/packages/components/src/radio/style.scss new file mode 100644 index 0000000000000..2d0d325d6209f --- /dev/null +++ b/packages/components/src/radio/style.scss @@ -0,0 +1,268 @@ +.components-button { + display: inline-flex; + text-decoration: none; + font-size: $default-font-size; + margin: 0; + border: 0; + cursor: pointer; + -webkit-appearance: none; + background: none; + transition: box-shadow 0.1s linear; + @include reduce-motion("transition"); + height: $button-size; + align-items: center; + box-sizing: border-box; + padding: 6px 12px; + overflow: hidden; + border-radius: $radius-block-ui; + color: $dark-gray-primary; + + &[aria-expanded="true"], + &:hover { + color: $theme-color; + } + + // Unset some hovers, instead of adding :not specificity. + &[aria-disabled="true"]:hover { + color: initial; + } + + // Focus. + // See https://github.com/WordPress/gutenberg/issues/13267 for more context on these selectors. + &:focus:not(:disabled) { + box-shadow: 0 0 0 2px color($theme-color); + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 1px solid transparent; + } + + /** + * Primary button style. + */ + + &.is-primary { + white-space: nowrap; + background: color($theme-color); + color: $white; + text-decoration: none; + text-shadow: none; + + &:hover:not(:disabled) { + background: color($theme-color shade(10%)); + color: $white; + } + + &:active:not(:disabled) { + background: color($theme-color shade(20%)); + border-color: color($theme-color shade(20%)); + color: $white; + } + + &:focus:not(:disabled) { + box-shadow: inset 0 0 0 1px $white, 0 0 0 2px color($theme-color); + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 1px solid transparent; + } + + &:disabled, + &:disabled:active:enabled, + &[aria-disabled="true"], + &[aria-disabled="true"]:enabled, // This catches a situation where a Button is aria-disabled, but not disabled. + &[aria-disabled="true"]:active:enabled { + color: color($theme-color tint(40%)); + background: color($theme-color tint(10%)); + border-color: color($theme-color tint(10%)); + opacity: 1; + + &:focus:enabled { + box-shadow: + 0 0 0 $border-width $white, + 0 0 0 3px color($theme-color); + } + } + + &.is-busy, + &.is-busy:disabled, + &.is-busy[aria-disabled="true"] { + color: $white; + background-size: 100px 100%; + // Disable reason: This function call looks nicer when each argument is on its own line. + /* stylelint-disable */ + background-image: linear-gradient( + -45deg, + $theme-color 28%, + color($theme-color shade(20%)) 28%, + color($theme-color shade(20%)) 72%, + $theme-color 72% + ); + /* stylelint-enable */ + border-color: color($theme-color); + } + } + + /** + * Secondary and tertiary buttons. + */ + + &.is-secondary, + &.is-tertiary { + &:active:not(:disabled) { + background: $light-gray-tertiary; + color: color($theme-color shade(10%)); + box-shadow: none; + } + + &:hover:not(:disabled) { + color: color($theme-color shade(10%)); + box-shadow: inset 0 0 0 $border-width color($theme-color shade(10%)); + } + + &:disabled, + &[aria-disabled="true"], + &[aria-disabled="true"]:hover { + color: lighten($medium-gray-text, 5%); + background: lighten($light-gray-tertiary, 5%); + transform: none; + opacity: 1; + box-shadow: none; + } + } + + /** + * Secondary button style. + */ + + &.is-secondary { + box-shadow: inset 0 0 0 $border-width $theme-color; + outline: 1px solid transparent; // Shown in high contrast mode. + white-space: nowrap; + color: $theme-color; + background: transparent; + } + + /** + * Tertiary buttons. + */ + + &.is-tertiary { + white-space: nowrap; + color: $theme-color; + background: transparent; + padding: 6px; // This reduces the horizontal padding on tertiary/text buttons, so as to space them optically. + + .dashicon { + display: inline-block; + flex: 0 0 auto; + } + } + + /** + * Link buttons. + */ + + &.is-link { + margin: 0; + padding: 0; + box-shadow: none; + border: 0; + border-radius: 0; + background: none; + outline: none; + text-align: left; + /* Mimics the default link style in common.css */ + color: #0073aa; + text-decoration: underline; + transition-property: border, background, color; + transition-duration: 0.05s; + transition-timing-function: ease-in-out; + @include reduce-motion("transition"); + height: auto; + + &:hover:not(:disabled), + &:active:not(:disabled) { + color: #00a0d2; + } + + &:focus { + color: #124964; + box-shadow: + 0 0 0 $border-width #5b9dd9, + 0 0 2px $border-width rgba(30, 140, 190, 0.8); + } + } + + // Link buttons that are red to indicate destructive behavior. + &.is-link.is-destructive { + color: $alert-red; + } + + &:not([aria-disabled="true"]):active { + color: inherit; + } + + &:disabled, + &[aria-disabled="true"] { + cursor: default; + opacity: 0.3; + } + + &.is-busy, + &.is-secondary.is-busy, + &.is-secondary.is-busy:disabled, + &.is-secondary.is-busy[aria-disabled="true"] { + animation: components-button__busy-animation 2500ms infinite linear; + background-size: 100px 100%; + background-image: repeating-linear-gradient(-45deg, $light-gray-500, $white 11px, $white 10px, $light-gray-500 20px); + opacity: 1; + } + + &.is-small { + height: 24px; + line-height: 22px; + padding: 0 8px; + font-size: 11px; + + &.has-icon:not(.has-text) { + width: 24px; + } + } + + &.has-icon { + padding: 6px; // Works for 24px icons. Smaller icons are vertically centered by flex alignments. + + // Icon buttons are square. + min-width: $button-size; + justify-content: center; + + .dashicon { + display: inline-block; + flex: 0 0 auto; + } + + &.has-text { + justify-content: left; + } + + &.has-text svg { + margin-right: 8px; + } + } + + svg { + fill: currentColor; + outline: none; + } + + // Fixes a Safari+VoiceOver bug, where the screen reader text is announced not respecting the source order. + // See https://core.trac.wordpress.org/ticket/42006 and https://github.com/h5bp/html5-boilerplate/issues/1985 + .components-visually-hidden { + height: auto; + } +} + +@keyframes components-button__busy-animation { + 0% { + background-position: 200px 0; + } +} diff --git a/packages/components/src/radio/test/index.js b/packages/components/src/radio/test/index.js new file mode 100644 index 0000000000000..8aa9928a9ef62 --- /dev/null +++ b/packages/components/src/radio/test/index.js @@ -0,0 +1,205 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; +import TestUtils from 'react-dom/test-utils'; + +/** + * WordPress dependencies + */ +import { createRef } from '@wordpress/element'; +import { plusCircle } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import ButtonWithForwardedRef, { Button } from '../'; + +describe( 'Button', () => { + describe( 'basic rendering', () => { + it( 'should render a button element with only one class', () => { + const button = shallow( + ); + expect( iconButton.name() ).toBe( 'button' ); + } ); + + it( 'should force showing the tooltip even if icon and children defined', () => { + const iconButton = shallow( + + ); + expect( iconButton.name() ).toBe( 'Tooltip' ); + } ); + } ); + + describe( 'with href property', () => { + it( 'should render a link instead of a button with href prop', () => { + const button = shallow(
`; -exports[`Storyshots Components/ButtonGroup Radio Button Group 1`] = ` -
- - - -
-`; - exports[`Storyshots Components/Card Default 1`] = ` .emotion-6 { background: #fff; From 10d7e261b12594c7eace8d6059b7f45449e15c63 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 1 Apr 2020 10:16:37 -0500 Subject: [PATCH 23/34] Update ButtonGroup extra props to override role --- packages/components/src/button-group/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index 6f4fb672c8c6e..c2a2ccd1ebe75 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; function ButtonGroup( { className, ...props } ) { const classes = classnames( 'components-button-group', className ); - return
; + return
; } export default ButtonGroup; From c01c3bf6aa79ef09cd7b285d8207ac419fd0772f Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 1 Apr 2020 10:18:13 -0500 Subject: [PATCH 24/34] Implement forwardRef for ButtonGroup --- packages/components/src/button-group/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js index c2a2ccd1ebe75..2ef2b5e94c601 100644 --- a/packages/components/src/button-group/index.js +++ b/packages/components/src/button-group/index.js @@ -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
; + return
; } -export default ButtonGroup; +export default forwardRef( ButtonGroup ); From 3129af7c1b7c04b1087dd5dc576e63c94320cf09 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 1 Apr 2020 10:22:36 -0500 Subject: [PATCH 25/34] Update snapshot with forwardRef(ButtonGroup) --- .../src/date-time/test/__snapshots__/time.js.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/date-time/test/__snapshots__/time.js.snap b/packages/components/src/date-time/test/__snapshots__/time.js.snap index e27e600f089f2..b65d4fddd151c 100644 --- a/packages/components/src/date-time/test/__snapshots__/time.js.snap +++ b/packages/components/src/date-time/test/__snapshots__/time.js.snap @@ -317,7 +317,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is specified 1`] value="00" />
- PM - +
@@ -498,7 +498,7 @@ exports[`TimePicker matches the snapshot when the is12hour prop is true 1`] = ` value="00" />
- PM - +
From 37dc2b104db2a3bc2cf5f829cc21db25943b1676 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 1 Apr 2020 12:01:41 -0500 Subject: [PATCH 26/34] Update RadioControl to mention RadioGroup --- packages/components/src/radio-control/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/radio-control/README.md b/packages/components/src/radio-control/README.md index 52369d0aca971..f08f7da87b495 100644 --- a/packages/components/src/radio-control/README.md +++ b/packages/components/src/radio-control/README.md @@ -121,4 +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 `ButtonGroup` component with `role="radio"`. +* To format as a button group, use the `RadioGroup` component. From c07e117d4383ee5f95fbcc303713b8996ec50ab2 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 1 Apr 2020 12:37:26 -0500 Subject: [PATCH 27/34] Replace Radio/RadioGroup with Reakit version --- .../components/src/radio-context/index.js | 11 + packages/components/src/radio-group/index.js | 91 ++---- .../src/radio-group/stories/index.js | 41 --- .../components/src/radio-group/style.scss | 35 --- packages/components/src/radio/deprecated.js | 29 -- packages/components/src/radio/index.js | 199 ++----------- packages/components/src/radio/index.native.js | 198 ------------- .../components/src/radio/stories/index.js | 174 ------------ .../components/src/radio/stories/style.css | 8 - packages/components/src/radio/style.scss | 268 ------------------ packages/components/src/radio/test/index.js | 205 -------------- packages/components/src/style.scss | 2 - 12 files changed, 60 insertions(+), 1201 deletions(-) create mode 100644 packages/components/src/radio-context/index.js delete mode 100644 packages/components/src/radio-group/stories/index.js delete mode 100644 packages/components/src/radio-group/style.scss delete mode 100644 packages/components/src/radio/deprecated.js delete mode 100644 packages/components/src/radio/index.native.js delete mode 100644 packages/components/src/radio/stories/index.js delete mode 100644 packages/components/src/radio/stories/style.css delete mode 100644 packages/components/src/radio/style.scss delete mode 100644 packages/components/src/radio/test/index.js diff --git a/packages/components/src/radio-context/index.js b/packages/components/src/radio-context/index.js new file mode 100644 index 0000000000000..58a7783dcb84e --- /dev/null +++ b/packages/components/src/radio-context/index.js @@ -0,0 +1,11 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +const RadioContext = createContext( { + state: null, + setState: () => {}, +} ); + +export default RadioContext; diff --git a/packages/components/src/radio-group/index.js b/packages/components/src/radio-group/index.js index 022f2b27f0107..cbf36e0ebfdf5 100644 --- a/packages/components/src/radio-group/index.js +++ b/packages/components/src/radio-group/index.js @@ -1,74 +1,45 @@ /** * External dependencies */ -import classnames from 'classnames'; +import { useRadioState, RadioGroup as ReakitRadioGroup } from 'reakit/Radio'; /** * WordPress dependencies */ -import { Children, useRef, createContext, useMemo } from '@wordpress/element'; +import { forwardRef } from '@wordpress/element'; -// Default values for when a button isn't a child of a group -export const ButtonGroupContext = createContext( { - mode: null, - buttons: {}, -} ); - -function ButtonGroup( { - mode, - checked, - onChange, - className, - children, - ...props -} ) { - const classes = classnames( 'components-button-group', className ); - const role = mode === 'radio' ? 'radiogroup' : 'group'; - const childRefs = useRef( [] ); +/** + * Internal dependencies + */ +import ButtonGroup from '../button-group'; +import RadioContext from '../radio-context'; - const buttons = useMemo( () => { - const buttonsContext = {}; - if ( mode === 'radio' ) { - const childrenArray = Children.toArray( children ); - childrenArray.forEach( ( child, index ) => { - buttonsContext[ child.props.value ] = { - isChecked: checked === child.props.value, - isFirst: ! checked && index === 0, - onPrev: () => { - const prevIndex = - ( index - 1 + childrenArray.length ) % - childrenArray.length; - childRefs.current[ prevIndex ].focus(); - onChange( childrenArray[ prevIndex ].props.value ); - }, - onNext: () => { - const nextIndex = ( index + 1 ) % childrenArray.length; - childRefs.current[ nextIndex ].focus(); - onChange( childrenArray[ nextIndex ].props.value ); - }, - onSelect: () => { - onChange( child.props.value ); - }, - refCallback: ( ref ) => { - if ( ref === null ) { - delete childRefs.current[ index ]; - } else { - childRefs.current[ index ] = ref; - } - }, - }; - } ); - } - return buttonsContext; - }, [ children, mode, onChange, checked, childRefs ] ); +function RadioGroup( + { accessibilityLabel, defaultChecked, checked, onChange, ...props }, + ref +) { + const radioState = useRadioState( { + state: defaultChecked, + baseId: props.id, + } ); + const radioContext = { + ...radioState, + // controlled or uncontrolled + state: checked || radioState.state, + setState: onChange || radioState.setState, + }; return ( -
- - { children } - -
+ + + ); } -export default ButtonGroup; +export default forwardRef( RadioGroup ); diff --git a/packages/components/src/radio-group/stories/index.js b/packages/components/src/radio-group/stories/index.js deleted file mode 100644 index 5d947d14e11f4..0000000000000 --- a/packages/components/src/radio-group/stories/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import Button from '../../button'; -import ButtonGroup from '../'; - -export default { title: 'Components/ButtonGroup', component: ButtonGroup }; - -export const _default = () => { - const style = { margin: '0 4px' }; - return ( - - - - - ); -}; - -const ButtonGroupWithState = () => { - const [ checked, setChecked ] = useState( 'medium' ); - return ( - - - - - - ); -}; - -export const radioButtonGroup = () => { - return ; -}; diff --git a/packages/components/src/radio-group/style.scss b/packages/components/src/radio-group/style.scss deleted file mode 100644 index f1dace4f64797..0000000000000 --- a/packages/components/src/radio-group/style.scss +++ /dev/null @@ -1,35 +0,0 @@ -.components-button-group { - display: inline-block; - - .components-button { - border-radius: 0; - display: inline-flex; - color: $theme-color; - box-shadow: inset 0 0 0 $border-width $theme-color; - - & + .components-button { - margin-left: -1px; - } - - &:first-child { - border-radius: $radius-block-ui 0 0 $radius-block-ui; - } - - &:last-child { - border-radius: 0 $radius-block-ui $radius-block-ui 0; - } - - // The focused button should be elevated so the focus ring isn't cropped, - // as should the active button, because it has a different border color. - &:focus, - &.is-primary { - position: relative; - z-index: z-index(".components-button {:focus or .is-primary}"); - } - - // The active button should look pressed. - &.is-primary { - box-shadow: inset 0 0 0 $border-width $theme-color; - } - } -} diff --git a/packages/components/src/radio/deprecated.js b/packages/components/src/radio/deprecated.js deleted file mode 100644 index 6c676d0021581..0000000000000 --- a/packages/components/src/radio/deprecated.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * WordPress dependencies - */ -import deprecated from '@wordpress/deprecated'; -import { forwardRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import Button from '../button'; - -function IconButton( { labelPosition, size, tooltip, label, ...props }, ref ) { - deprecated( 'wp.components.IconButton', { - alternative: 'wp.components.Button', - } ); - - return ( - ; -}; - -export const primary = () => { - const label = text( 'Label', 'Primary Button' ); - - return ; -}; - -export const secondary = () => { - const label = text( 'Label', 'Secondary Button' ); - - return ; -}; - -export const tertiary = () => { - const label = text( 'Label', 'Tertiary Button' ); - - return ; -}; - -export const small = () => { - const label = text( 'Label', 'Small Button' ); - - return ; -}; - -export const pressed = () => { - const label = text( 'Label', 'Pressed Button' ); - - return ; -}; - -export const disabled = () => { - const label = text( 'Label', 'Disabled Button' ); - - return ; -}; - -export const disabledFocusable = () => { - const label = text( 'Label', 'Disabled Button' ); - - return ( - - ); -}; - -export const link = () => { - const label = text( 'Label', 'Link Button' ); - - return ( - - ); -}; - -export const disabledLink = () => { - const label = text( 'Label', 'Disabled Link Button' ); - - return ( - - ); -}; - -export const icon = () => { - const usedIcon = text( 'Icon', 'ellipsis' ); - const label = text( 'Label', 'More' ); - const size = number( 'Size' ); - - return - - - - - - -

Regular Buttons

-
- - - - - -
- - ); -}; diff --git a/packages/components/src/radio/stories/style.css b/packages/components/src/radio/stories/style.css deleted file mode 100644 index 239018d6a298a..0000000000000 --- a/packages/components/src/radio/stories/style.css +++ /dev/null @@ -1,8 +0,0 @@ -.story-buttons-container { - display: flex; -} - -.story-buttons-container > * { - margin-right: 10px; - margin-bottom: 10px; -} diff --git a/packages/components/src/radio/style.scss b/packages/components/src/radio/style.scss deleted file mode 100644 index 2d0d325d6209f..0000000000000 --- a/packages/components/src/radio/style.scss +++ /dev/null @@ -1,268 +0,0 @@ -.components-button { - display: inline-flex; - text-decoration: none; - font-size: $default-font-size; - margin: 0; - border: 0; - cursor: pointer; - -webkit-appearance: none; - background: none; - transition: box-shadow 0.1s linear; - @include reduce-motion("transition"); - height: $button-size; - align-items: center; - box-sizing: border-box; - padding: 6px 12px; - overflow: hidden; - border-radius: $radius-block-ui; - color: $dark-gray-primary; - - &[aria-expanded="true"], - &:hover { - color: $theme-color; - } - - // Unset some hovers, instead of adding :not specificity. - &[aria-disabled="true"]:hover { - color: initial; - } - - // Focus. - // See https://github.com/WordPress/gutenberg/issues/13267 for more context on these selectors. - &:focus:not(:disabled) { - box-shadow: 0 0 0 2px color($theme-color); - - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 1px solid transparent; - } - - /** - * Primary button style. - */ - - &.is-primary { - white-space: nowrap; - background: color($theme-color); - color: $white; - text-decoration: none; - text-shadow: none; - - &:hover:not(:disabled) { - background: color($theme-color shade(10%)); - color: $white; - } - - &:active:not(:disabled) { - background: color($theme-color shade(20%)); - border-color: color($theme-color shade(20%)); - color: $white; - } - - &:focus:not(:disabled) { - box-shadow: inset 0 0 0 1px $white, 0 0 0 2px color($theme-color); - - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 1px solid transparent; - } - - &:disabled, - &:disabled:active:enabled, - &[aria-disabled="true"], - &[aria-disabled="true"]:enabled, // This catches a situation where a Button is aria-disabled, but not disabled. - &[aria-disabled="true"]:active:enabled { - color: color($theme-color tint(40%)); - background: color($theme-color tint(10%)); - border-color: color($theme-color tint(10%)); - opacity: 1; - - &:focus:enabled { - box-shadow: - 0 0 0 $border-width $white, - 0 0 0 3px color($theme-color); - } - } - - &.is-busy, - &.is-busy:disabled, - &.is-busy[aria-disabled="true"] { - color: $white; - background-size: 100px 100%; - // Disable reason: This function call looks nicer when each argument is on its own line. - /* stylelint-disable */ - background-image: linear-gradient( - -45deg, - $theme-color 28%, - color($theme-color shade(20%)) 28%, - color($theme-color shade(20%)) 72%, - $theme-color 72% - ); - /* stylelint-enable */ - border-color: color($theme-color); - } - } - - /** - * Secondary and tertiary buttons. - */ - - &.is-secondary, - &.is-tertiary { - &:active:not(:disabled) { - background: $light-gray-tertiary; - color: color($theme-color shade(10%)); - box-shadow: none; - } - - &:hover:not(:disabled) { - color: color($theme-color shade(10%)); - box-shadow: inset 0 0 0 $border-width color($theme-color shade(10%)); - } - - &:disabled, - &[aria-disabled="true"], - &[aria-disabled="true"]:hover { - color: lighten($medium-gray-text, 5%); - background: lighten($light-gray-tertiary, 5%); - transform: none; - opacity: 1; - box-shadow: none; - } - } - - /** - * Secondary button style. - */ - - &.is-secondary { - box-shadow: inset 0 0 0 $border-width $theme-color; - outline: 1px solid transparent; // Shown in high contrast mode. - white-space: nowrap; - color: $theme-color; - background: transparent; - } - - /** - * Tertiary buttons. - */ - - &.is-tertiary { - white-space: nowrap; - color: $theme-color; - background: transparent; - padding: 6px; // This reduces the horizontal padding on tertiary/text buttons, so as to space them optically. - - .dashicon { - display: inline-block; - flex: 0 0 auto; - } - } - - /** - * Link buttons. - */ - - &.is-link { - margin: 0; - padding: 0; - box-shadow: none; - border: 0; - border-radius: 0; - background: none; - outline: none; - text-align: left; - /* Mimics the default link style in common.css */ - color: #0073aa; - text-decoration: underline; - transition-property: border, background, color; - transition-duration: 0.05s; - transition-timing-function: ease-in-out; - @include reduce-motion("transition"); - height: auto; - - &:hover:not(:disabled), - &:active:not(:disabled) { - color: #00a0d2; - } - - &:focus { - color: #124964; - box-shadow: - 0 0 0 $border-width #5b9dd9, - 0 0 2px $border-width rgba(30, 140, 190, 0.8); - } - } - - // Link buttons that are red to indicate destructive behavior. - &.is-link.is-destructive { - color: $alert-red; - } - - &:not([aria-disabled="true"]):active { - color: inherit; - } - - &:disabled, - &[aria-disabled="true"] { - cursor: default; - opacity: 0.3; - } - - &.is-busy, - &.is-secondary.is-busy, - &.is-secondary.is-busy:disabled, - &.is-secondary.is-busy[aria-disabled="true"] { - animation: components-button__busy-animation 2500ms infinite linear; - background-size: 100px 100%; - background-image: repeating-linear-gradient(-45deg, $light-gray-500, $white 11px, $white 10px, $light-gray-500 20px); - opacity: 1; - } - - &.is-small { - height: 24px; - line-height: 22px; - padding: 0 8px; - font-size: 11px; - - &.has-icon:not(.has-text) { - width: 24px; - } - } - - &.has-icon { - padding: 6px; // Works for 24px icons. Smaller icons are vertically centered by flex alignments. - - // Icon buttons are square. - min-width: $button-size; - justify-content: center; - - .dashicon { - display: inline-block; - flex: 0 0 auto; - } - - &.has-text { - justify-content: left; - } - - &.has-text svg { - margin-right: 8px; - } - } - - svg { - fill: currentColor; - outline: none; - } - - // Fixes a Safari+VoiceOver bug, where the screen reader text is announced not respecting the source order. - // See https://core.trac.wordpress.org/ticket/42006 and https://github.com/h5bp/html5-boilerplate/issues/1985 - .components-visually-hidden { - height: auto; - } -} - -@keyframes components-button__busy-animation { - 0% { - background-position: 200px 0; - } -} diff --git a/packages/components/src/radio/test/index.js b/packages/components/src/radio/test/index.js deleted file mode 100644 index 8aa9928a9ef62..0000000000000 --- a/packages/components/src/radio/test/index.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; -import TestUtils from 'react-dom/test-utils'; - -/** - * WordPress dependencies - */ -import { createRef } from '@wordpress/element'; -import { plusCircle } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import ButtonWithForwardedRef, { Button } from '../'; - -describe( 'Button', () => { - describe( 'basic rendering', () => { - it( 'should render a button element with only one class', () => { - const button = shallow( - ); - expect( iconButton.name() ).toBe( 'button' ); - } ); - - it( 'should force showing the tooltip even if icon and children defined', () => { - const iconButton = shallow( - - ); - expect( iconButton.name() ).toBe( 'Tooltip' ); - } ); - } ); - - describe( 'with href property', () => { - it( 'should render a link instead of a button with href prop', () => { - const button = shallow( + + +`; + exports[`Storyshots Components/RadioControl Default 1`] = `
`; +exports[`Storyshots Components/RadioGroup Controlled 1`] = ` +
+ + + +
+`; + +exports[`Storyshots Components/RadioGroup Default 1`] = ` +
+ + + +
+`; + exports[`Storyshots Components/RangeControl Custom Marks 1`] = ` .emotion-36 { -webkit-tap-highlight-color: transparent; From 4f669109cc4017745d7b2290d8611263a327e8da Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 1 Apr 2020 14:51:42 -0500 Subject: [PATCH 29/34] Update Radio/RadioGroup READMEs --- packages/components/src/radio-group/README.md | 72 +++++---- packages/components/src/radio/README.md | 145 ------------------ 2 files changed, 38 insertions(+), 179 deletions(-) delete mode 100644 packages/components/src/radio/README.md diff --git a/packages/components/src/radio-group/README.md b/packages/components/src/radio-group/README.md index d51034c72e7e3..7c6eab29b0bf1 100644 --- a/packages/components/src/radio-group/README.md +++ b/packages/components/src/radio-group/README.md @@ -1,8 +1,8 @@ -# ButtonGroup +# RadioGroup -ButtonGroup can be used to group any related buttons together. To emphasize related buttons, a group should share a common container. +Use a RadioGroup component when you want users to select one option from a small set of options. -![ButtonGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png) +![RadioGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png) ## Table of contents @@ -16,68 +16,72 @@ ButtonGroup can be used to group any related buttons together. To emphasize rela #### Selected action -![ButtonGroup selection](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1544127594329_ButtonGroup-Do.png) - -**Do** -Only one option in a button group can be selected and active at a time. Selecting one option deselects any other. +Only one option in a radio group can be selected and active at a time. Selecting one option deselects any other. ### Best practices -Button groups should: +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 -![ButtonGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png) - -**Active and available button groups** +#### Active and available radio groups -A button group’s state makes it clear which button is active. Hover and focus states express the available selection options for buttons in a button group. +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 button groups** +#### Disabled radio groups -Button groups that cannot be selected can either be given a disabled state, or be hidden. +Radio groups that cannot be selected can either be given a disabled state, or be hidden. ## Development guidelines ### Usage -**As a simple group** +#### Controlled ```jsx -import { Button, ButtonGroup } from '@wordpress/components'; - -const MyButtonGroup = () => ( - - - - -); +import { Radio, RadioGroup } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +const MyControlledRadioRadioGroup = () => { + const [ checked, setChecked ] = useState( '25' ); + return ( + + 25% + 50% + 75% + 100% + + ); +}; ``` -**As a radio group** +#### Uncontrolled + +When using the RadioGroup component as an uncontrolled component, the default value can be set with the `defaultChecked` prop. ```jsx -import { Button, ButtonGroup } from '@wordpress/components'; +import { Radio, RadioGroup } from '@wordpress/components'; import { useState } from '@wordpress/element'; -const MyRadioButtonGroup = () => { - const [ checked, setChecked ] = useState( 'medium' ); +const MyUncontrolledRadioRadioGroup = () => { return ( - - - - - + + 25% + 50% + 75% + 100% + ); }; ``` ## Related components -- For individual buttons, use a `Button` component. -- For a traditional radio group, use a `RadioControl` component. +- For simple buttons that are related, use a `ButtonGroup` component. +- For traditional radio options, use a `RadioControl` component. diff --git a/packages/components/src/radio/README.md b/packages/components/src/radio/README.md deleted file mode 100644 index bf71236c408ae..0000000000000 --- a/packages/components/src/radio/README.md +++ /dev/null @@ -1,145 +0,0 @@ -# Button -Buttons let users take actions and make choices with a single click or tap. - -![Button components](https://make.wordpress.org/design/files/2019/03/button.png) - -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - -## Design guidelines - -### Usage - -Buttons tell users what actions they can take and give them a way to interact with the interface. You’ll find them throughout a UI, particularly in places like: - -- Modals -- Forms -- Toolbars - -### Best practices - -Buttons 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. -- **Prioritize the most important actions.** This helps users focus. Too many calls to action on one screen can be confusing, making users unsure what to do next. -- **Have consistent locations in the interface.** - -### Content guidelines - -Buttons should be clear and predictable—users should be able to anticipate what will happen when they click a button. Never deceive a user by mislabeling a button. - -Buttons text should lead with a strong verb that encourages action, and add a noun that clarifies what will actually change. The only exceptions are common actions like Save, Close, Cancel, or OK. Otherwise, use the {verb}+{noun} format to ensure that your button gives the user enough information. - -Button text should also be quickly scannable — avoid unnecessary words and articles like the, an, or a. - -### Types - -#### Link button - -Link buttons have low emphasis. They don’t stand out much on the page, so they’re used for less-important actions. What’s less important can vary based on context, but it’s usually a supplementary action to the main action we want someone to take. Link buttons are also useful when you don’t want to distract from the content. - -![Link button](https://make.wordpress.org/design/files/2019/03/link-button.png) - -#### Default button - -Default buttons have medium emphasis. The button appearance helps differentiate them from the page background, so they’re useful when you want more emphasis than a link button offers. - -![Default button](https://make.wordpress.org/design/files/2019/03/default-button.png) - -#### Primary button - -Primary buttons have high emphasis. Their color fill and shadow means they pop off the background. - -Since a high-emphasis button commands the most attention, a layout should contain a single primary button. This makes it clear that other buttons have less importance and helps users understand when an action requires their attention. - -![Primary button](https://make.wordpress.org/design/files/2019/03/primary-button.png) - -#### Text label - -All button types use text labels to describe the action that happens when a user taps a button. If there’s no text label, there should be an icon to signify what the button does. - -![](https://make.wordpress.org/design/files/2019/03/do-link-button.png) - -**Do** -Use color to distinguish link button labels from other text. - -![](https://make.wordpress.org/design/files/2019/03/dont-wrap-button-text.png) - -**Don’t** -Don’t wrap button text. For maximum legibility, keep text labels on a single line. - -### Hierarchy - -![A layout with a single prominent button](https://make.wordpress.org/design/files/2019/03/button.png) - -A layout should contain a single prominently-located button. If multiple buttons are required, a single high-emphasis button can be joined by medium- and low-emphasis buttons mapped to less-important actions. When using multiple buttons, make sure the available state of one button doesn’t look like the disabled state of another. - -![A diagram showing high emphasis at the top, medium emphasis in the middle, and low emphasis at the bottom](https://make.wordpress.org/design/files/2019/03/button-hierarchy.png) - -A button’s level of emphasis helps determine its appearance, typography, and placement. - -#### Placement - -Use button types to express different emphasis levels for all the actions a user can perform. - -![A link, default, and primary button](https://make.wordpress.org/design/files/2019/03/button-layout.png) - -This screen layout uses: - -1. A primary button for high emphasis. -2. A default button for medium emphasis. -3. A link button for low emphasis. - -Placement best practices: - -- **Do**: When using multiple buttons in a row, show users which action is more important by placing it next to a button with a lower emphasis (e.g. a primary button next to a default button, or a default button next to a link button). -- **Don’t**: Don’t place two primary buttons next to one another — they compete for focus. Only use one primary button per view. -- **Don’t**: Don’t place a button below another button if there is space to place them side by side. -- **Caution**: Avoid using too many buttons on a single page. When designing pages in the app or website, think about the most important actions for users to take. Too many calls to action can cause confusion and make users unsure what to do next — we always want users to feel confident and capable. - -## Development guidelines - -### Usage - -Renders a button with default style. - -```jsx -import { Button } from "@wordpress/components"; - -const MyButton = () => ( - -); -``` - -### Props - -The presence of a `href` prop determines whether an `anchor` element is rendered instead of a `button`. - -Props not included in this set will be applied to the `a` or `button` element. - -Name | Type | Default | Description ---- | --- | --- | --- -`disabled` | `bool` | `false` | Whether the button is disabled. If `true`, this will force a `button` element to be rendered. -`href` | `string` | `undefined` | If provided, renders `a` instead of `button`. -`isSecondary` | `bool` | `false` | Renders a default button style. -`isPrimary` | `bool` | `false` | Renders a primary button style. -`isTertiary` | `bool` | `false` | Renders a text-based button style. -`isDestructive` | `bool` | `false` | Renders a red text-based button style to indicate destructive behavior. -`isLarge` | `bool` | `false` | Increases the size of the button. -`isSmall` | `bool` | `false` | Decreases the size of the button. -`isPressed` | `bool` | `false` | Renders a pressed button style. -`isBusy` | `bool` | `false` | Indicates activity while a action is being performed. -`isLink` | `bool` | `false` | Renders a button with an anchor style. -`focus` | `bool` | `false` | Whether the button is focused. - -## Related components - -- To group buttons together, use the `ButtonGroup` component. - From c611871ac2587bdc6d542caa15f03425667421ad Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 1 Apr 2020 15:46:23 -0500 Subject: [PATCH 30/34] Update docs manifest --- docs/manifest.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/manifest.json b/docs/manifest.json index 75b5514dca1e9..537f6ea671988 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", From 7a417acd4d9bb60675eee28318b0085d7d2829d5 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Sat, 4 Apr 2020 09:25:32 -0500 Subject: [PATCH 31/34] Add __experimental prefix for Radio/RadioGroup --- packages/components/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 776139fb4364d..f5142afb2f86e 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -71,8 +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 Radio } from './radio'; -export { default as RadioGroup } from './radio-group'; +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'; From 538538f894494f5c7e4c477c91c05326ba64e7be Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Sat, 4 Apr 2020 09:51:52 -0500 Subject: [PATCH 32/34] Pass through disabled state to children --- packages/components/src/radio-group/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/components/src/radio-group/index.js b/packages/components/src/radio-group/index.js index cbf36e0ebfdf5..11f2460529792 100644 --- a/packages/components/src/radio-group/index.js +++ b/packages/components/src/radio-group/index.js @@ -15,7 +15,14 @@ import ButtonGroup from '../button-group'; import RadioContext from '../radio-context'; function RadioGroup( - { accessibilityLabel, defaultChecked, checked, onChange, ...props }, + { + accessibilityLabel, + checked, + defaultChecked, + disabled, + onChange, + ...props + }, ref ) { const radioState = useRadioState( { @@ -24,6 +31,7 @@ function RadioGroup( } ); const radioContext = { ...radioState, + disabled, // controlled or uncontrolled state: checked || radioState.state, setState: onChange || radioState.setState, From da54defbc2e58521e17674ff5a12dd1d2221bfd7 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Sat, 4 Apr 2020 09:58:12 -0500 Subject: [PATCH 33/34] Remove Radio ids from stories --- .../src/radio-group/stories/index.js | 46 +++++++++++-------- .../components/src/radio/stories/index.js | 24 ++-------- 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/packages/components/src/radio-group/stories/index.js b/packages/components/src/radio-group/stories/index.js index decb321b20715..5844cd82016b5 100644 --- a/packages/components/src/radio-group/stories/index.js +++ b/packages/components/src/radio-group/stories/index.js @@ -13,22 +13,34 @@ export default { title: 'Components/RadioGroup', component: RadioGroup }; export const _default = () => { /* eslint-disable no-restricted-syntax */ - // id is required for server side rendering return ( - - Option 1 - - - Option 2 - - - Option 3 - + Option 1 + Option 2 + Option 3 + + ); + /* eslint-enable no-restricted-syntax */ +}; + +export const disabled = () => { + /* eslint-disable no-restricted-syntax */ + return ( + + Option 1 + Option 2 + Option 3 ); /* eslint-enable no-restricted-syntax */ @@ -38,23 +50,17 @@ const ControlledRadioGroupWithState = () => { const [ checked, setChecked ] = useState( 'option2' ); /* eslint-disable no-restricted-syntax */ - // id is required for server side rendering return ( - - Option 1 - - - Option 2 - - - Option 3 - + Option 1 + Option 2 + Option 3 ); /* eslint-enable no-restricted-syntax */ diff --git a/packages/components/src/radio/stories/index.js b/packages/components/src/radio/stories/index.js index 1384122227e44..b64c35ed7ba17 100644 --- a/packages/components/src/radio/stories/index.js +++ b/packages/components/src/radio/stories/index.js @@ -6,30 +6,14 @@ import Radio from '../'; export default { title: 'Components/Radio', component: Radio }; -// Radio components can be in their own component, but must be a descendent of -// a RadioGroup component at some point. -const RadioOptions = () => { - /* eslint-disable no-restricted-syntax */ - // id is required for server side rendering - return ( - <> - - Option 1 - - - Option 2 - - - ); - /* eslint-enable no-restricted-syntax */ -}; - export const _default = () => { + // Radio components must be a descendent of a RadioGroup component. /* eslint-disable no-restricted-syntax */ - // id is required for server side rendering return ( + // id is required for server side rendering - + Option 1 + Option 2 ); /* eslint-enable no-restricted-syntax */ From f2dad34593f62f3a3166200b03212b67e94d161f Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Sat, 4 Apr 2020 09:58:22 -0500 Subject: [PATCH 34/34] Update snapshots --- storybook/test/__snapshots__/index.js.snap | 101 +++++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/storybook/test/__snapshots__/index.js.snap b/storybook/test/__snapshots__/index.js.snap index d880e5fc0985c..321b96d00f17a 100644 --- a/storybook/test/__snapshots__/index.js.snap +++ b/storybook/test/__snapshots__/index.js.snap @@ -4383,7 +4383,7 @@ exports[`Storyshots Components/Radio Default 1`] = ` aria-checked={false} checked={false} className="components-button is-secondary" - id="option1-radio" + id="default-radiogroup-1" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4401,7 +4401,7 @@ exports[`Storyshots Components/Radio Default 1`] = ` aria-checked={false} checked={false} className="components-button is-secondary" - id="option2-radio" + id="default-radiogroup-2" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4583,7 +4583,7 @@ exports[`Storyshots Components/RadioGroup Controlled 1`] = ` aria-checked={false} checked={false} className="components-button is-secondary" - id="option1-radio" + id="controlled-radiogroup-1" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4601,7 +4601,7 @@ exports[`Storyshots Components/RadioGroup Controlled 1`] = ` aria-checked={true} checked={true} className="components-button is-primary" - id="option2-radio" + id="controlled-radiogroup-2" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4619,7 +4619,7 @@ exports[`Storyshots Components/RadioGroup Controlled 1`] = ` aria-checked={false} checked={false} className="components-button is-secondary" - id="option3-radio" + id="controlled-radiogroup-3" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4650,7 +4650,7 @@ exports[`Storyshots Components/RadioGroup Default 1`] = ` aria-checked={false} checked={false} className="components-button is-secondary" - id="option1-radio" + id="default-radiogroup-1" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4668,7 +4668,7 @@ exports[`Storyshots Components/RadioGroup Default 1`] = ` aria-checked={true} checked={true} className="components-button is-primary" - id="option2-radio" + id="default-radiogroup-2" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4686,7 +4686,7 @@ exports[`Storyshots Components/RadioGroup Default 1`] = ` aria-checked={false} checked={false} className="components-button is-secondary" - id="option3-radio" + id="default-radiogroup-3" onChange={[Function]} onClick={[Function]} onFocus={[Function]} @@ -4703,6 +4703,91 @@ exports[`Storyshots Components/RadioGroup Default 1`] = `
`; +exports[`Storyshots Components/RadioGroup Disabled 1`] = ` +
+ + + +
+`; + exports[`Storyshots Components/RangeControl Custom Marks 1`] = ` .emotion-36 { -webkit-tap-highlight-color: transparent;