Skip to content

Commit

Permalink
Cover: Fix placeholder color options keyboard accessibility (WordPres…
Browse files Browse the repository at this point in the history
…s#68662)

* Render the color options as buttons.

* Make circular option picker get a group role and optional label when rendered as buttons.

* Adjust tests.

* Adjust more tests.

* Abstract logic to compute the common props.

* Update snapshot.

* Fix OptionGroup ARIA role.

* Rename.

* Adjust tests.

* Restore group ARIA role on OptionGroup.

* Change Background color label to Overlay color.

Co-authored-by: afercia <[email protected]>
Co-authored-by: ciampo <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
Co-authored-by: carolinan <[email protected]>
Co-authored-by: Mayank-Tripathi32 <[email protected]>
  • Loading branch information
6 people authored Feb 18, 2025
1 parent 9d1a2ce commit d3a9586
Show file tree
Hide file tree
Showing 20 changed files with 109 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ exports[`ColorPaletteControl matches the snapshot 1`] = `
class="components-circular-option-picker"
>
<div
aria-label="Custom color picker."
aria-label="Custom color picker"
id="components-circular-option-picker-0"
role="listbox"
>
Expand Down
2 changes: 2 additions & 0 deletions packages/block-library/src/cover/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@ function CoverEdit( {
value={ overlayColor.color }
onChange={ onSetOverlayColor }
clearable={ false }
asButtons
aria-label={ __( 'Overlay color' ) }
/>
</div>
</CoverPlaceholder>
Expand Down
12 changes: 6 additions & 6 deletions packages/block-library/src/cover/test/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async function setup( attributes, useCoreBlocks, customSettings ) {

async function createAndSelectBlock() {
await userEvent.click(
screen.getByRole( 'option', {
screen.getByRole( 'button', {
name: 'Black',
} )
);
Expand All @@ -72,7 +72,7 @@ describe( 'Cover block', () => {

test( 'can set overlay color using color picker on block placeholder', async () => {
const { container } = await setup();
const colorPicker = screen.getByRole( 'option', {
const colorPicker = screen.getByRole( 'button', {
name: 'Black',
} );
await userEvent.click( colorPicker );
Expand All @@ -96,7 +96,7 @@ describe( 'Cover block', () => {
await setup();

await userEvent.click(
screen.getByRole( 'option', {
screen.getByRole( 'button', {
name: 'Black',
} )
);
Expand Down Expand Up @@ -389,7 +389,7 @@ describe( 'Cover block', () => {
describe( 'isDark settings', () => {
test( 'should toggle is-light class if background changed from light to dark', async () => {
await setup();
const colorPicker = screen.getByRole( 'option', {
const colorPicker = screen.getByRole( 'button', {
name: 'White',
} );
await userEvent.click( colorPicker );
Expand All @@ -413,7 +413,7 @@ describe( 'Cover block', () => {
} );
test( 'should remove is-light class if overlay color is removed', async () => {
await setup();
const colorPicker = screen.getByRole( 'option', {
const colorPicker = screen.getByRole( 'button', {
name: 'White',
} );
await userEvent.click( colorPicker );
Expand All @@ -426,7 +426,7 @@ describe( 'Cover block', () => {
} )
);
await userEvent.click( screen.getByText( 'Overlay' ) );
// The default color is black, so clicking the black color option will remove the background color,
// The default color is black, so clicking the black color button will remove the background color,
// which should remove the isDark setting and assign the is-light class.
const popupColorPicker = screen.getByRole( 'option', {
name: 'White',
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/border-box-control/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe( 'BorderBoxControl', () => {
await waitFor( () =>
expect(
screen.getByRole( 'button', {
name: 'Custom color picker.',
name: 'Custom color picker',
} )
).toBeVisible()
);
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/border-control/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe( 'BorderControl', () => {

const customColorPicker = getButton( /Custom color picker/ );
const circularOptionPicker = screen.getByRole( 'listbox', {
name: 'Custom color picker.',
name: 'Custom color picker',
} );
const colorSwatchButtons =
within( circularOptionPicker ).getAllByRole( 'option' );
Expand Down
13 changes: 13 additions & 0 deletions packages/components/src/circular-option-picker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ Prevents keyboard interaction from wrapping around. Only used when `asButtons` i
- Required: No
- Default: `true`

### `aria-labelledby`: `string`

The ID reference list of one or more elements that label the wrapper element.

- Required: No

### `aria-label`: `string`

The label for the wrapper element. Not used if an 'aria-labelledby' is provided.

- Required: No
- Default: `Custom color picker`

## Subcomponents

### `CircularOptionPicker.ButtonAction`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ function ButtonsCircularOptionPicker(
);

return (
<div { ...additionalProps } id={ baseId }>
<div { ...additionalProps } role="group" id={ baseId }>
<CircularOptionPickerContext.Provider value={ contextValue }>
{ options }
{ children }
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/circular-option-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export {
ButtonAction,
DropdownLinkAction,
} from './circular-option-picker-actions';
export { getComputeCircularOptionPickerCommonProps } from './utils';

export default CircularOptionPicker;
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ WithLoopingDisabled.parameters = {
docs: {
source: {
code: `<CircularOptionPicker
aria-label="${ WithLoopingDisabled.args[ 'aria-label' ] }"
'aria-label': 'Circular Option Picker',
loop={false}
options={<DefaultOptions />}
/>`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe( 'CircularOptionPicker', () => {

expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
expect( screen.queryByRole( 'option' ) ).not.toBeInTheDocument();
expect( screen.getByRole( 'group' ) ).toBeInTheDocument();
expect( screen.getByRole( 'button' ) ).toBeInTheDocument();
} );
} );
Expand Down
21 changes: 11 additions & 10 deletions packages/components/src/circular-option-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ type CommonCircularOptionPickerProps = {
* The child elements.
*/
children?: ReactNode;
/**
* The ID reference list of one or more elements that label the wrapper
* element.
*/
'aria-labelledby'?: string;
/**
* The label for the wrapper element. Defaults to 'Custom color picker'. Not
* used if an 'aria-labelledby' is provided.
*/
'aria-label'?: string;
};

type WithBaseId = {
Expand All @@ -59,16 +69,7 @@ type FullListboxCircularOptionPickerProps = CommonCircularOptionPickerProps & {
* @default true
*/
loop?: boolean;
} & (
| {
'aria-label': string;
'aria-labelledby'?: never;
}
| {
'aria-label'?: never;
'aria-labelledby': string;
}
);
};

export type ListboxCircularOptionPickerProps = WithBaseId &
Omit< FullListboxCircularOptionPickerProps, 'asButtons' >;
Expand Down
27 changes: 27 additions & 0 deletions packages/components/src/circular-option-picker/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Computes the common props for the CircularOptionPicker.
*/
export function getComputeCircularOptionPickerCommonProps(
asButtons?: boolean,
loop?: boolean,
ariaLabel?: string,
ariaLabelledby?: string
) {
const metaProps = asButtons
? { asButtons: true }
: { asButtons: false, loop };

const labelProps = {
'aria-labelledby': ariaLabelledby,
'aria-label': ariaLabelledby
? undefined
: ariaLabel || __( 'Custom color picker' ),
};

return { metaProps, labelProps };
}
40 changes: 11 additions & 29 deletions packages/components/src/color-palette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import { useCallback, useMemo, useState, forwardRef } from '@wordpress/element';
*/
import Dropdown from '../dropdown';
import { ColorPicker } from '../color-picker';
import CircularOptionPicker from '../circular-option-picker';
import CircularOptionPicker, {
getComputeCircularOptionPickerCommonProps,
} from '../circular-option-picker';
import { VStack } from '../v-stack';
import { Truncate } from '../truncate';
import { ColorHeading } from './styles';
Expand Down Expand Up @@ -233,7 +235,7 @@ function UnforwardedColorPalette(
buttonLabelName,
displayValue
)
: __( 'Custom color picker.' );
: __( 'Custom color picker' );

const paletteCommonProps = {
clearColor,
Expand All @@ -251,33 +253,12 @@ function UnforwardedColorPalette(
</CircularOptionPicker.ButtonAction>
);

let metaProps:
| { asButtons: false; loop?: boolean; 'aria-label': string }
| { asButtons: false; loop?: boolean; 'aria-labelledby': string }
| { asButtons: true };

if ( asButtons ) {
metaProps = { asButtons: true };
} else {
const _metaProps: { asButtons: false; loop?: boolean } = {
asButtons: false,
loop,
};

if ( ariaLabel ) {
metaProps = { ..._metaProps, 'aria-label': ariaLabel };
} else if ( ariaLabelledby ) {
metaProps = {
..._metaProps,
'aria-labelledby': ariaLabelledby,
};
} else {
metaProps = {
..._metaProps,
'aria-label': __( 'Custom color picker.' ),
};
}
}
const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps(
asButtons,
loop,
ariaLabel,
ariaLabelledby
);

return (
<VStack spacing={ 3 } ref={ forwardedRef } { ...additionalProps }>
Expand Down Expand Up @@ -335,6 +316,7 @@ function UnforwardedColorPalette(
{ ( colors.length > 0 || actions ) && (
<CircularOptionPicker
{ ...metaProps }
{ ...labelProps }
actions={ actions }
options={
hasMultipleColorOrigins ? (
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/color-palette/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ describe( 'ColorPalette', () => {
expect( screen.queryByText( colorCode ) ).not.toBeInTheDocument();
expect(
screen.getByRole( 'button', {
name: /^Custom color picker.$/,
name: /^Custom color picker$/,
} )
).toBeInTheDocument();
} );
Expand Down
38 changes: 10 additions & 28 deletions packages/components/src/duotone-picker/duotone-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { __, sprintf } from '@wordpress/i18n';
* Internal dependencies
*/
import ColorListPicker from './color-list-picker';
import CircularOptionPicker from '../circular-option-picker';
import CircularOptionPicker, {
getComputeCircularOptionPickerCommonProps,
} from '../circular-option-picker';
import { VStack } from '../v-stack';

import CustomDuotoneBar from './custom-duotone-bar';
Expand Down Expand Up @@ -127,33 +129,12 @@ function DuotonePicker( {
);
} );

let metaProps:
| { asButtons: false; loop?: boolean; 'aria-label': string }
| { asButtons: false; loop?: boolean; 'aria-labelledby': string }
| { asButtons: true };

if ( asButtons ) {
metaProps = { asButtons: true };
} else {
const _metaProps: { asButtons: false; loop?: boolean } = {
asButtons: false,
loop,
};

if ( ariaLabel ) {
metaProps = { ..._metaProps, 'aria-label': ariaLabel };
} else if ( ariaLabelledby ) {
metaProps = {
..._metaProps,
'aria-labelledby': ariaLabelledby,
};
} else {
metaProps = {
..._metaProps,
'aria-label': __( 'Custom color picker.' ),
};
}
}
const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps(
asButtons,
loop,
ariaLabel,
ariaLabelledby
);

const options = unsetable
? [ unsetOption, ...duotoneOptions ]
Expand All @@ -163,6 +144,7 @@ function DuotonePicker( {
<CircularOptionPicker
{ ...otherProps }
{ ...metaProps }
{ ...labelProps }
options={ options }
actions={
!! clearable && (
Expand Down
38 changes: 10 additions & 28 deletions packages/components/src/gradient-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { useCallback, useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import CircularOptionPicker from '../circular-option-picker';
import CircularOptionPicker, {
getComputeCircularOptionPickerCommonProps,
} from '../circular-option-picker';
import CustomGradientPicker from '../custom-gradient-picker';
import { VStack } from '../v-stack';
import { ColorHeading } from '../color-palette/styles';
Expand Down Expand Up @@ -128,37 +130,17 @@ function Component( props: PickerProps< any > ) {
<SingleOrigin { ...additionalProps } />
);

let metaProps:
| { asButtons: false; loop?: boolean; 'aria-label': string }
| { asButtons: false; loop?: boolean; 'aria-labelledby': string }
| { asButtons: true };

if ( asButtons ) {
metaProps = { asButtons: true };
} else {
const _metaProps: { asButtons: false; loop?: boolean } = {
asButtons: false,
loop,
};

if ( ariaLabel ) {
metaProps = { ..._metaProps, 'aria-label': ariaLabel };
} else if ( ariaLabelledby ) {
metaProps = {
..._metaProps,
'aria-labelledby': ariaLabelledby,
};
} else {
metaProps = {
..._metaProps,
'aria-label': __( 'Custom color picker.' ),
};
}
}
const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps(
asButtons,
loop,
ariaLabel,
ariaLabelledby
);

return (
<CircularOptionPicker
{ ...metaProps }
{ ...labelProps }
actions={ actions }
options={ options }
/>
Expand Down
Loading

0 comments on commit d3a9586

Please sign in to comment.