Skip to content

Commit

Permalink
Components: Add accessible Toolbar (#18534)
Browse files Browse the repository at this point in the history
* Add accessible Toolbar

* Remove ToolbarButton from Button

* Remove ref for now

* Pass className to ToolbarGroup on Toolbar

* Update Toolbar stories

* Remove withInstanceId from Toolbar stories
  • Loading branch information
diegohaz authored and gziolo committed Nov 19, 2019
1 parent b911e1d commit a861f0e
Show file tree
Hide file tree
Showing 26 changed files with 697 additions and 224 deletions.
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"re-resizable": "^6.0.0",
"react-dates": "^17.1.1",
"react-spring": "^8.0.20",
"reakit": "^1.0.0-beta.12",
"rememo": "^3.0.0",
"tinycolor2": "^1.4.1",
"uuid": "^3.3.2"
Expand Down
14 changes: 12 additions & 2 deletions packages/components/src/dropdown-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,18 @@ function DropdownMenu( {
<IconButton
{ ...mergedToggleProps }
icon={ icon }
onClick={ onToggle }
onKeyDown={ openOnArrowDown }
onClick={ ( event ) => {
onToggle( event );
if ( mergedToggleProps.onClick ) {
mergedToggleProps.onClick( event );
}
} }
onKeyDown={ ( event ) => {
openOnArrowDown( event );
if ( mergedToggleProps.onKeyDown ) {
mergedToggleProps.onKeyDown( event );
}
} }
aria-haspopup="true"
aria-expanded={ isOpen }
label={ label }
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export { default as Tip } from './tip';
export { default as ToggleControl } from './toggle-control';
export { default as Toolbar } from './toolbar';
export { default as ToolbarButton } from './toolbar-button';
export { default as ToolbarGroup } from './toolbar-group';
export { default as Tooltip } from './tooltip';
export { default as TreeSelect } from './tree-select';
export { default as VisuallyHidden } from './visually-hidden';
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as Dashicon } from './dashicon';
export { default as Dropdown } from './dropdown';
export { default as Toolbar } from './toolbar';
export { default as ToolbarButton } from './toolbar-button';
export { default as ToolbarGroup } from './toolbar-group';
export { default as Icon } from './icon';
export { default as IconButton } from './icon-button';
export { default as Spinner } from './spinner';
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@
@import "./toggle-control/style.scss";
@import "./toolbar/style.scss";
@import "./toolbar-button/style.scss";
@import "./toolbar-group/style.scss";
@import "./tooltip/style.scss";
@import "./visually-hidden/style.scss";
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { useToolbarItem } from 'reakit/Toolbar';

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

/**
* Internal dependencies
*/
import ToolbarContext from '../toolbar-context';

function AccessibleToolbarButtonContainer( props ) {
const accessibleToolbarState = useContext( ToolbarContext );
const childButton = Children.only( props.children );

// https://reakit.io/docs/composition/#props-hooks
const itemHTMLProps = useToolbarItem( accessibleToolbarState, childButton.props );

return <div { ...props }>{ cloneElement( childButton, itemHTMLProps ) }</div>;
}

export default AccessibleToolbarButtonContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Internal dependencies
*/
import ToolbarButtonContainer from './toolbar-button-container';

export default ToolbarButtonContainer;
62 changes: 44 additions & 18 deletions packages/components/src/toolbar-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
*/
import classnames from 'classnames';

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

/**
* Internal dependencies
*/
import IconButton from '../icon-button';
import ToolbarContext from '../toolbar-context';
import AccessibleToolbarButtonContainer from './accessible-toolbar-button-container';
import ToolbarButtonContainer from './toolbar-button-container';

function ToolbarButton( {
Expand All @@ -22,26 +29,45 @@ function ToolbarButton( {
extraProps,
children,
} ) {
// It'll contain state if `ToolbarButton` is being used within
// `<Toolbar __experimentalAccessibilityLabel="label" />`
const accessibleToolbarState = useContext( ToolbarContext );

const button = (
<IconButton
icon={ icon }
label={ title }
shortcut={ shortcut }
data-subscript={ subscript }
onClick={ ( event ) => {
event.stopPropagation();
if ( onClick ) {
onClick( event );
}
} }
className={ classnames(
'components-toolbar__control',
className,
{ 'is-active': isActive }
) }
aria-pressed={ isActive }
disabled={ isDisabled }
{ ...extraProps }
/>
);

if ( accessibleToolbarState ) {
return (
<AccessibleToolbarButtonContainer className={ containerClassName }>
{ button }
</AccessibleToolbarButtonContainer>
);
}

// ToolbarButton is being used outside of the accessible Toolbar
return (
<ToolbarButtonContainer className={ containerClassName }>
<IconButton
icon={ icon }
label={ title }
shortcut={ shortcut }
data-subscript={ subscript }
onClick={ ( event ) => {
event.stopPropagation();
onClick();
} }
className={ classnames(
'components-toolbar__control',
className,
{ 'is-active': isActive }
) }
aria-pressed={ isActive }
disabled={ isDisabled }
{ ...extraProps }
/>
{ button }
{ children }
</ToolbarButtonContainer>
);
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/toolbar-button/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
border-radius: $radius-round-rectangle;
height: 30px;
width: 30px;
box-sizing: border-box;
}

// Subscript for numbered icon buttons, like headings
Expand Down
8 changes: 8 additions & 0 deletions packages/components/src/toolbar-context/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';

const ToolbarContext = createContext();

export default ToolbarContext;
112 changes: 112 additions & 0 deletions packages/components/src/toolbar-group/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { flatMap } from 'lodash';

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

/**
* Internal dependencies
*/
import ToolbarButton from '../toolbar-button';
import ToolbarGroupContainer from './toolbar-group-container';
import ToolbarGroupCollapsed from './toolbar-group-collapsed';
import ToolbarContext from '../toolbar-context';

/**
* Renders a collapsible group of controls
*
* The `controls` prop accepts an array of sets. A set is an array of controls.
* Controls have the following shape:
*
* ```
* {
* icon: string,
* title: string,
* subscript: string,
* onClick: Function,
* isActive: boolean,
* isDisabled: boolean
* }
* ```
*
* For convenience it is also possible to pass only an array of controls. It is
* then assumed this is the only set.
*
* Either `controls` or `children` is required, otherwise this components
* renders nothing.
*
* @param {Object} props Component props.
* @param {Array} [props.controls] The controls to render in this toolbar.
* @param {WPElement} [props.children] Any other things to render inside the toolbar besides the controls.
* @param {string} [props.className] Class to set on the container div.
* @param {boolean} [props.isCollapsed] Turns ToolbarGroup into a dropdown menu.
* @param {WPBlockTypeIconRender} [props.icon] The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element.
* @param {string} [props.label] The menu item text.
*/
function ToolbarGroup( {
controls = [],
children,
className,
isCollapsed,
icon,
title,
...otherProps
} ) {
// It'll contain state if `ToolbarGroup` is being used within
// `<Toolbar accessibilityLabel="label" />`
const accessibleToolbarState = useContext( ToolbarContext );

if ( ( ! controls || ! controls.length ) && ! children ) {
return null;
}

const finalClassName = classnames(
// Unfortunately, there's legacy code referencing to `.components-toolbar`
// So we can't get rid of it
accessibleToolbarState ? 'components-toolbar-group' : 'components-toolbar',
className
);

// Normalize controls to nested array of objects (sets of controls)
let controlSets = controls;
if ( ! Array.isArray( controlSets[ 0 ] ) ) {
controlSets = [ controlSets ];
}

if ( isCollapsed ) {
return (
<ToolbarGroupCollapsed
icon={ icon }
label={ title }
controls={ controlSets }
className={ finalClassName }
children={ children }
{ ...otherProps }
/>
);
}

return (
<ToolbarGroupContainer className={ finalClassName } { ...otherProps }>
{ flatMap( controlSets, ( controlSet, indexOfSet ) =>
controlSet.map( ( control, indexOfControl ) => (
<ToolbarButton
key={ [ indexOfSet, indexOfControl ].join() }
containerClassName={
indexOfSet > 0 && indexOfControl === 0 ? 'has-left-divider' : null
}
{ ...control }
/>
) )
) }
{ children }
</ToolbarGroupContainer>
);
}

export default ToolbarGroup;
28 changes: 28 additions & 0 deletions packages/components/src/toolbar-group/stories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Internal dependencies
*/
import { ToolbarButton, ToolbarGroup } from '../../';

export default { title: 'Components|ToolbarGroup', component: ToolbarGroup };

export const _default = () => {
return (
<ToolbarGroup>
<ToolbarButton icon="editor-bold" title="Bold" isActive />
<ToolbarButton icon="editor-italic" title="Italic" />
<ToolbarButton icon="admin-links" title="Link" />
</ToolbarGroup>
);
};

export const withControlsProp = () => {
return (
<ToolbarGroup
controls={ [
{ icon: 'editor-bold', title: 'Bold', isActive: true },
{ icon: 'editor-italic', title: 'Italic' },
{ icon: 'admin-links', title: 'Link' },
] }
/>
);
};
11 changes: 11 additions & 0 deletions packages/components/src/toolbar-group/style.native.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.container {
flex-direction: row;
border-left-width: 1px;
border-color: #e9eff3;
padding-left: 5px;
padding-right: 5px;
}

.containerDark {
border-color: #525354;
}
Loading

0 comments on commit a861f0e

Please sign in to comment.