-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Components: Improve ToolbarButton #18931
Changes from all commits
badb51d
31dcf58
e5110b1
474aba5
da487d9
b3bf911
7779fa7
0643f3c
482e28f
aeca4d6
b3ef374
83aeceb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,63 +12,55 @@ import { useContext } from '@wordpress/element'; | |
* Internal dependencies | ||
*/ | ||
import Button from '../button'; | ||
import ToolbarItem from '../toolbar-item'; | ||
import ToolbarContext from '../toolbar-context'; | ||
import AccessibleToolbarButtonContainer from './accessible-toolbar-button-container'; | ||
import ToolbarButtonContainer from './toolbar-button-container'; | ||
|
||
function ToolbarButton( { | ||
containerClassName, | ||
icon, | ||
title, | ||
shortcut, | ||
subscript, | ||
onClick, | ||
className, | ||
isActive, | ||
isDisabled, | ||
extraProps, | ||
children, | ||
...props | ||
} ) { | ||
// It'll contain state if `ToolbarButton` is being used within | ||
// `<Toolbar __experimentalAccessibilityLabel="label" />` | ||
const accessibleToolbarState = useContext( ToolbarContext ); | ||
|
||
const button = ( | ||
<Button | ||
icon={ icon } | ||
label={ title } | ||
shortcut={ shortcut } | ||
data-subscript={ subscript } | ||
onClick={ ( event ) => { | ||
event.stopPropagation(); | ||
if ( onClick ) { | ||
onClick( event ); | ||
} | ||
} } | ||
className={ classnames( | ||
'components-toolbar__control', | ||
className, | ||
) } | ||
isPressed={ isActive } | ||
disabled={ isDisabled } | ||
{ ...extraProps } | ||
/> | ||
); | ||
|
||
if ( accessibleToolbarState ) { | ||
if ( ! accessibleToolbarState ) { | ||
// This should be deprecated when <Toolbar __experimentalAccessibilityLabel="label"> | ||
// becomes stable. | ||
return ( | ||
<AccessibleToolbarButtonContainer className={ containerClassName }> | ||
{ button } | ||
</AccessibleToolbarButtonContainer> | ||
<ToolbarButtonContainer className={ containerClassName }> | ||
<Button | ||
icon={ props.icon } | ||
label={ props.title } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it mean that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say that these props should be deprecated:
Still not sure what to do with This PR only changes the behavior of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea, let's go this way in the follow-up PRs. |
||
shortcut={ props.shortcut } | ||
data-subscript={ props.subscript } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will investigate in a follow-up whether we can get rid of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept it there so as to avoid breaking anything, but it's not being used in the new implementation (using |
||
onClick={ ( event ) => { | ||
event.stopPropagation(); | ||
if ( props.onClick ) { | ||
props.onClick( event ); | ||
} | ||
} } | ||
className={ classnames( 'components-toolbar__control', className ) } | ||
isPressed={ props.isActive } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A similar question, should we align API and deprecated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's the current API. This PR only changes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, we will need to align it at some point with the Button component as discussed. |
||
disabled={ props.isDisabled } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this was discussed in other PRs, we should make a final call on whether we use /cc @youknowriad @aduth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense to adopt |
||
{ ...extraProps } | ||
/> | ||
{ children } | ||
</ToolbarButtonContainer> | ||
); | ||
} | ||
|
||
// ToolbarButton is being used outside of the accessible Toolbar | ||
// ToobarItem will pass all props to the render prop child, which will pass | ||
// all props to Button. This means that ToolbarButton has the same API as | ||
// Button. | ||
return ( | ||
<ToolbarButtonContainer className={ containerClassName }> | ||
{ button } | ||
{ children } | ||
</ToolbarButtonContainer> | ||
<ToolbarItem | ||
className={ classnames( 'components-toolbar-button', className ) } | ||
{ ...props } | ||
> | ||
{ ( toolbarItemProps ) => <Button { ...toolbarItemProps }>{ children }</Button> } | ||
</ToolbarItem> | ||
); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { useToolbarItem } from 'reakit/Toolbar'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { forwardRef, useContext } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import ToolbarContext from '../toolbar-context'; | ||
|
||
function ToolbarItem( { children, ...props }, ref ) { | ||
const accessibleToolbarState = useContext( ToolbarContext ); | ||
// https://reakit.io/docs/composition/#props-hooks | ||
const itemProps = useToolbarItem( accessibleToolbarState, { ...props, ref } ); | ||
|
||
if ( typeof children !== 'function' ) { | ||
// eslint-disable-next-line no-console | ||
console.warn( '`ToolbarItem` is a generic headless component that accepts only function children props' ); | ||
diegohaz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return null; | ||
} | ||
|
||
if ( ! accessibleToolbarState ) { | ||
// eslint-disable-next-line no-console | ||
console.warn( '`ToolbarItem` should be rendered within `<Toolbar __experimentalAccessibilityLabel="label">`' ); | ||
return null; | ||
} | ||
|
||
return children( itemProps ); | ||
} | ||
|
||
export default forwardRef( ToolbarItem ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { forwardRef } from '@wordpress/element'; | ||
|
||
function ToolbarItem( { children, ...props }, ref ) { | ||
if ( typeof children !== 'function' ) { | ||
// eslint-disable-next-line no-console | ||
console.warn( '`ToolbarItem` is a generic headless component that accepts only function children props' ); | ||
return null; | ||
} | ||
|
||
return children( { ...props, ref } ); | ||
} | ||
|
||
export default forwardRef( ToolbarItem ); |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,7 +2,14 @@ | |||||||||||||||||
* Internal dependencies | ||||||||||||||||||
*/ | ||||||||||||||||||
import Toolbar from '../'; | ||||||||||||||||||
import { SVG, Path, ToolbarButton, ToolbarGroup } from '../../'; | ||||||||||||||||||
import { | ||||||||||||||||||
SVG, | ||||||||||||||||||
Path, | ||||||||||||||||||
ToolbarButton, | ||||||||||||||||||
ToolbarGroup, | ||||||||||||||||||
__experimentalToolbarItem as ToolbarItem, | ||||||||||||||||||
DropdownMenu, | ||||||||||||||||||
} from '../../'; | ||||||||||||||||||
|
||||||||||||||||||
export default { title: 'Components|Toolbar', component: Toolbar }; | ||||||||||||||||||
|
||||||||||||||||||
|
@@ -20,22 +27,30 @@ export const _default = () => { | |||||||||||||||||
// id is required for server side rendering | ||||||||||||||||||
<Toolbar __experimentalAccessibilityLabel="Options" id="options-toolbar"> | ||||||||||||||||||
<ToolbarGroup> | ||||||||||||||||||
<ToolbarButton icon="editor-paragraph" title="Paragraph" /> | ||||||||||||||||||
<ToolbarButton icon="editor-paragraph" label="Paragraph" /> | ||||||||||||||||||
</ToolbarGroup> | ||||||||||||||||||
<ToolbarGroup> | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are some cases in Gutenberg where we're using gutenberg/packages/block-editor/src/components/block-settings-menu/index.js Lines 59 to 66 in 2c1e3be
I'm not sure how easy is it to refactor those edge cases to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, it all makes sense. I think I mentioned it already, I just wanted to ensure you are aware of the current state of the art :) |
||||||||||||||||||
<ToolbarItem> | ||||||||||||||||||
{ ( toggleProps ) => ( | ||||||||||||||||||
<DropdownMenu | ||||||||||||||||||
hasArrowIndicator | ||||||||||||||||||
icon="editor-alignleft" | ||||||||||||||||||
label="Change text alignment" | ||||||||||||||||||
controls={ [ | ||||||||||||||||||
{ icon: 'editor-alignleft', title: 'Align left', isActive: true }, | ||||||||||||||||||
{ icon: 'editor-aligncenter', title: 'Align center' }, | ||||||||||||||||||
{ icon: 'editor-alignright', title: 'Align right' }, | ||||||||||||||||||
] } | ||||||||||||||||||
toggleProps={ toggleProps } | ||||||||||||||||||
/> | ||||||||||||||||||
) } | ||||||||||||||||||
</ToolbarItem> | ||||||||||||||||||
</ToolbarGroup> | ||||||||||||||||||
<ToolbarGroup | ||||||||||||||||||
icon="editor-alignleft" | ||||||||||||||||||
label="Change text alignment" | ||||||||||||||||||
isCollapsed | ||||||||||||||||||
controls={ [ | ||||||||||||||||||
{ icon: 'editor-alignleft', title: 'Align left', isActive: true }, | ||||||||||||||||||
{ icon: 'editor-aligncenter', title: 'Align center' }, | ||||||||||||||||||
{ icon: 'editor-alignright', title: 'Align right' }, | ||||||||||||||||||
] } | ||||||||||||||||||
/> | ||||||||||||||||||
<ToolbarGroup> | ||||||||||||||||||
<ToolbarButton icon="editor-bold" title="Bold" /> | ||||||||||||||||||
<ToolbarButton icon="editor-italic" title="Italic" /> | ||||||||||||||||||
<ToolbarButton icon="admin-links" title="Link" /> | ||||||||||||||||||
<ToolbarButton>Text</ToolbarButton> | ||||||||||||||||||
<ToolbarButton icon="editor-bold" label="Bold" isPressed /> | ||||||||||||||||||
<ToolbarButton icon="editor-italic" label="Italic" /> | ||||||||||||||||||
<ToolbarButton icon="admin-links" label="Link" /> | ||||||||||||||||||
<ToolbarGroup | ||||||||||||||||||
isCollapsed | ||||||||||||||||||
icon={ false } | ||||||||||||||||||
|
@@ -65,10 +80,13 @@ export const _default = () => { | |||||||||||||||||
export const withoutGroup = () => { | ||||||||||||||||||
return ( | ||||||||||||||||||
// id is required for server side rendering | ||||||||||||||||||
<Toolbar __experimentalAccessibilityLabel="Options" id="options-toolbar-without-group"> | ||||||||||||||||||
<ToolbarButton icon="editor-bold" title="Bold" /> | ||||||||||||||||||
<ToolbarButton icon="editor-italic" title="Italic" /> | ||||||||||||||||||
<ToolbarButton icon="admin-links" title="Link" /> | ||||||||||||||||||
<Toolbar | ||||||||||||||||||
__experimentalAccessibilityLabel="Options" | ||||||||||||||||||
id="options-toolbar-without-group" | ||||||||||||||||||
> | ||||||||||||||||||
<ToolbarButton icon="editor-bold" label="Bold" isPressed /> | ||||||||||||||||||
<ToolbarButton icon="editor-italic" label="Italic" /> | ||||||||||||||||||
<ToolbarButton icon="admin-links" label="Link" /> | ||||||||||||||||||
</Toolbar> | ||||||||||||||||||
); | ||||||||||||||||||
}; | ||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,8 @@ describe( 'Toolbar', () => { | |
it( 'should render a toolbar with toolbar buttons', () => { | ||
const wrapper = mount( | ||
<Toolbar __experimentalAccessibilityLabel="blocks"> | ||
<ToolbarButton title="control1" /> | ||
<ToolbarButton title="control2" /> | ||
<ToolbarButton label="control1" /> | ||
<ToolbarButton label="control2" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When used within There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice 👍 |
||
</Toolbar> | ||
); | ||
const control1 = wrapper.find( 'button[aria-label="control1"]' ); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed most of the props here so we can spread
{ ...props }
directly intoButton
. This way,ToolbarButton
(when used within<Toolbar __experimentalAccessibilityLabel="label">
) andButton
have identical API.