-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Patterns: alternative grid layout to improve keyboard accessibility #52357
Changes from all commits
68ea704
837129d
32cefb4
799a58e
e872d85
e649c9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,6 @@ import { | |
MenuGroup, | ||
MenuItem, | ||
__experimentalHStack as HStack, | ||
__unstableCompositeItem as CompositeItem, | ||
Tooltip, | ||
Flex, | ||
} from '@wordpress/components'; | ||
|
@@ -31,7 +30,6 @@ import { | |
} from '@wordpress/icons'; | ||
import { store as noticesStore } from '@wordpress/notices'; | ||
import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; | ||
import { DELETE, BACKSPACE } from '@wordpress/keycodes'; | ||
|
||
/** | ||
* Internal dependencies | ||
|
@@ -65,12 +63,6 @@ function GridItem( { categoryId, item, ...props } ) { | |
categoryType: item.type, | ||
} ); | ||
|
||
const onKeyDown = ( event ) => { | ||
if ( DELETE === event.keyCode || BACKSPACE === event.keyCode ) { | ||
setIsDeleteDialogOpen( true ); | ||
} | ||
}; | ||
|
||
const isEmpty = ! item.blocks?.length; | ||
const patternClassNames = classnames( 'edit-site-patterns__pattern', { | ||
'is-placeholder': isEmpty, | ||
|
@@ -137,129 +129,120 @@ function GridItem( { categoryId, item, ...props } ) { | |
); | ||
|
||
return ( | ||
<> | ||
<div className={ patternClassNames }> | ||
<CompositeItem | ||
className={ previewClassNames } | ||
role="option" | ||
as="div" | ||
// Even though still incomplete, passing ids helps performance. | ||
// @see https://reakit.io/docs/composite/#performance. | ||
id={ `edit-site-patterns-${ item.name }` } | ||
{ ...props } | ||
onClick={ item.type !== PATTERNS ? onClick : undefined } | ||
onKeyDown={ isCustomPattern ? onKeyDown : undefined } | ||
aria-label={ item.title } | ||
aria-describedby={ | ||
ariaDescriptions.length | ||
? ariaDescriptions | ||
.map( | ||
( _, index ) => | ||
`${ descriptionId }-${ index }` | ||
) | ||
.join( ' ' ) | ||
: undefined | ||
} | ||
<li className={ patternClassNames }> | ||
<button | ||
className={ previewClassNames } | ||
// Even though still incomplete, passing ids helps performance. | ||
// @see https://reakit.io/docs/composite/#performance. | ||
id={ `edit-site-patterns-${ item.name }` } | ||
{ ...props } | ||
onClick={ item.type !== PATTERNS ? onClick : undefined } | ||
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 a pattern is not editable, it is good to keep the button focusable for discoverability. Btw, the button doesn't do anything so we should communicate semantically that, with an |
||
aria-disabled={ item.type !== PATTERNS ? 'false' : 'true' } | ||
aria-label={ item.title } | ||
aria-describedby={ | ||
ariaDescriptions.length | ||
? ariaDescriptions | ||
.map( | ||
( _, index ) => | ||
`${ descriptionId }-${ index }` | ||
) | ||
.join( ' ' ) | ||
: undefined | ||
} | ||
> | ||
{ isEmpty && __( 'Empty pattern' ) } | ||
{ ! isEmpty && <BlockPreview blocks={ item.blocks } /> } | ||
</button> | ||
{ ariaDescriptions.map( ( ariaDescription, index ) => ( | ||
<div | ||
key={ index } | ||
hidden | ||
id={ `${ descriptionId }-${ index }` } | ||
> | ||
{ isEmpty && __( 'Empty pattern' ) } | ||
{ ! isEmpty && <BlockPreview blocks={ item.blocks } /> } | ||
</CompositeItem> | ||
{ ariaDescriptions.map( ( ariaDescription, index ) => ( | ||
<div | ||
key={ index } | ||
hidden | ||
id={ `${ descriptionId }-${ index }` } | ||
> | ||
{ ariaDescription } | ||
</div> | ||
) ) } | ||
{ ariaDescription } | ||
</div> | ||
) ) } | ||
<HStack | ||
className="edit-site-patterns__footer" | ||
justify="space-between" | ||
> | ||
<HStack | ||
aria-hidden="true" | ||
className="edit-site-patterns__footer" | ||
justify="space-between" | ||
alignment="center" | ||
justify="left" | ||
spacing={ 3 } | ||
className="edit-site-patterns__pattern-title" | ||
> | ||
<HStack | ||
alignment="center" | ||
justify="left" | ||
spacing={ 3 } | ||
className="edit-site-patterns__pattern-title" | ||
> | ||
{ itemIcon && ( | ||
<Icon | ||
className="edit-site-patterns__pattern-icon" | ||
icon={ itemIcon } | ||
/> | ||
) } | ||
<Flex as="span" gap={ 0 } justify="left"> | ||
{ item.title } | ||
{ item.type === PATTERNS && ( | ||
<Tooltip | ||
position="top center" | ||
text={ __( | ||
'Theme patterns cannot be edited.' | ||
) } | ||
> | ||
<span className="edit-site-patterns__pattern-lock-icon"> | ||
<Icon icon={ lockSmall } size={ 24 } /> | ||
</span> | ||
</Tooltip> | ||
) } | ||
</Flex> | ||
</HStack> | ||
<DropdownMenu | ||
icon={ moreHorizontal } | ||
label={ __( 'Actions' ) } | ||
className="edit-site-patterns__dropdown" | ||
popoverProps={ { placement: 'bottom-end' } } | ||
toggleProps={ { | ||
className: 'edit-site-patterns__button', | ||
isSmall: true, | ||
describedBy: sprintf( | ||
/* translators: %s: pattern name */ | ||
__( 'Action menu for %s pattern' ), | ||
item.title | ||
), | ||
// The dropdown menu is not focusable using the | ||
// keyboard as this would interfere with the grid's | ||
// roving tab index system. Instead, keyboard users | ||
// use keyboard shortcuts to trigger actions. | ||
tabIndex: -1, | ||
} } | ||
> | ||
{ ( { onClose } ) => ( | ||
<MenuGroup> | ||
{ isCustomPattern && ! hasThemeFile && ( | ||
<RenameMenuItem | ||
item={ item } | ||
onClose={ onClose } | ||
/> | ||
{ itemIcon && ( | ||
<Icon | ||
className="edit-site-patterns__pattern-icon" | ||
icon={ itemIcon } | ||
/> | ||
) } | ||
<Flex as="span" gap={ 0 } justify="left"> | ||
{ item.title } | ||
{ item.type === PATTERNS && ( | ||
<Tooltip | ||
position="top center" | ||
text={ __( | ||
'Theme patterns cannot be edited.' | ||
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'm not sure the terminology used here is meaningful for users. What a |
||
) } | ||
<DuplicateMenuItem | ||
categoryId={ categoryId } | ||
> | ||
<span className="edit-site-patterns__pattern-lock-icon"> | ||
<Icon icon={ lockSmall } size={ 24 } /> | ||
</span> | ||
</Tooltip> | ||
) } | ||
</Flex> | ||
</HStack> | ||
<DropdownMenu | ||
icon={ moreHorizontal } | ||
label={ __( 'Actions' ) } | ||
className="edit-site-patterns__dropdown" | ||
popoverProps={ { placement: 'bottom-end' } } | ||
toggleProps={ { | ||
className: 'edit-site-patterns__button', | ||
isSmall: true, | ||
describedBy: sprintf( | ||
/* translators: %s: pattern name */ | ||
__( 'Action menu for %s pattern' ), | ||
item.title | ||
), | ||
} } | ||
> | ||
{ ( { onClose } ) => ( | ||
<MenuGroup> | ||
{ isCustomPattern && ! hasThemeFile && ( | ||
<RenameMenuItem | ||
item={ item } | ||
onClose={ onClose } | ||
label={ | ||
isNonUserPattern | ||
? __( 'Copy to My patterns' ) | ||
: __( 'Duplicate' ) | ||
} | ||
/> | ||
{ isCustomPattern && ( | ||
<MenuItem | ||
onClick={ () => | ||
setIsDeleteDialogOpen( true ) | ||
} | ||
> | ||
{ hasThemeFile | ||
? __( 'Clear customizations' ) | ||
: __( 'Delete' ) } | ||
</MenuItem> | ||
) } | ||
</MenuGroup> | ||
) } | ||
</DropdownMenu> | ||
</HStack> | ||
</div> | ||
) } | ||
<DuplicateMenuItem | ||
categoryId={ categoryId } | ||
item={ item } | ||
onClose={ onClose } | ||
label={ | ||
isNonUserPattern | ||
? __( 'Copy to My patterns' ) | ||
: __( 'Duplicate' ) | ||
} | ||
/> | ||
{ isCustomPattern && ( | ||
<MenuItem | ||
onClick={ () => | ||
setIsDeleteDialogOpen( true ) | ||
} | ||
> | ||
{ hasThemeFile | ||
? __( 'Clear customizations' ) | ||
: __( 'Delete' ) } | ||
</MenuItem> | ||
) } | ||
</MenuGroup> | ||
) } | ||
</DropdownMenu> | ||
</HStack> | ||
|
||
{ isDeleteDialogOpen && ( | ||
<ConfirmDialog | ||
confirmButtonText={ confirmButtonText } | ||
|
@@ -269,7 +252,7 @@ function GridItem( { categoryId, item, ...props } ) { | |
{ confirmPrompt } | ||
</ConfirmDialog> | ||
) } | ||
</> | ||
</li> | ||
); | ||
} | ||
|
||
|
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.
It seems a little odd using a button here, but all the accessibility guidelines I looked at suggested that this is better than trying to make a div behave like a button
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.
Theoretically, it would be possible to use 'clickable div' elements but to make them fully accessible we should replicate all the native semantics and interaction of a button element, including Enter / Spacebar behaviors, which are different (keydown vs. keyup). That would require an unnecessary amount of ARIA and scripting. Historically, developers started using clickable div elements because a few years ago button elements couldn't be fully styled. Now that modern browsers allow to fully style buttons, it's best to just use a button.
Also, a button element with
type="button"
had no default action, which allows to not useevent.preventDefault()
.