Skip to content
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

Making Circular Option Picker a listbox #52255

Conversation

andrewhayward
Copy link
Contributor

@andrewhayward andrewhayward commented Jul 3, 2023

What?

As per #35292, This patch modifies CircularOptionPicker to become a listbox, but using the Composite package. Additionally, it updates the various components that use CircularOptionPicker.

Why?

Currently, keyboard interaction with CircularOptionPickers is difficult, as each option presents as an individual tab stop. By changing the component to behave as a listbox, the entire control becomes a single tab stop, with individual colour options accessed using arrow keys.

How?

The CircularOptionPicker has been partially rebuilt using the various Composite elements, which reduces any complex behavioural additions on our part.

Testing Instructions

Nothing should visually change, as the underlying base components are still being used.

Testing Instructions for Keyboard

Every use of the CircularOptionPicker (ColorPalette, for example) should now present as a single tab stop, with options being picked using arrow keys.

Additional Notes

  • Initial values aren't currently picked up correctly.

✍️ Dev note

To improve CircularOptionPicker's semantics and keyboard navigation, the component has been tweaked to render and behave as a listbox by default. This change also causes the component to become a single tab stop, with the individual color options accessed using arrow keys.

In the (few) instances in which it makes sense for CircularOptionPicker to still render as a list of individual buttons, consumers of the component can use the asButtons prop to switch back to the legacy behavior.

Using the `Composite` component to make the Circular Option Picker present as a listbox.
@github-actions
Copy link

github-actions bot commented Jul 3, 2023

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @andrewhayward! In case you missed it, we'd love to have you join us in our Slack community, where we hold regularly weekly meetings open to anyone to coordinate with each other.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Jul 3, 2023
@mirka mirka added the [Package] Components /packages/components label Jul 3, 2023
@mirka mirka self-requested a review July 3, 2023 16:32
Copy link
Member

@mirka mirka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this big enhancement!

Based on my first-pass review, this approach looks viable to me. What do you think? If you agree, let's address the remaining issues and add some unit tests.

We also need to check for RTL support. I think we need to map @wordpress/i18n's isRTL() to the Composite store's rtl.

packages/components/src/circular-option-picker/types.ts Outdated Show resolved Hide resolved
packages/components/src/circular-option-picker/index.tsx Outdated Show resolved Hide resolved
packages/components/src/circular-option-picker/index.tsx Outdated Show resolved Hide resolved
packages/components/src/color-palette/index.tsx Outdated Show resolved Hide resolved
@@ -91,10 +92,10 @@ function SinglePalette( {
}, [ colors, value, onChange, clearColor ] );

return (
<CircularOptionPicker
<CircularOptionPicker.OptionGroup
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure about modeling the multiple palette case as groups in a single listbox. From reading the WAI guidance, I think a listbox is assumed to be one-dimensional (i.e. a list not a grid), with a aria-orientation to dictate which arrow keys to use.

What do you think about dropping the group construct and modeling them as separate listboxes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about dropping the group construct and modeling them as separate listboxes?

After interacting with the ColorPalette storybook demo with multiple origins, I also got the feeling that having each OptionGroup as a separate tab stop feels better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After further discussion, we've decided that we're going to keep the current behaviour, and iterate if/when necessary in a future PR.

@@ -152,28 +195,37 @@ export function ButtonAction( {
*/

function CircularOptionPicker( props: CircularOptionPickerProps ) {
const { actions, className, options, children } = props;
const { actions, className, options, children, loop = true } = props;
const compositeState = useCompositeState( { loop } );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually not up to date on this, but is there a reason why we're using useCompositeState instead of useCompositeStore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're still using reakit under the hood for this, which is where useCompositeState is coming from. As and when we move composite to ariakit we'll have to change that.

packages/components/src/circular-option-picker/index.tsx Outdated Show resolved Hide resolved
...args
} ) => {
const [ color, setColor ] = useState< string | undefined >();
const [ color, setColor ] = useState< string | undefined >( value );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Marking this as TODO so we don't forget to address it)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, what exactly was the TODO item related to this line of code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewhayward would you happen to know what needed to be done around this line?

andrewhayward and others added 3 commits August 1, 2023 23:29
– Adding support for RTL
- Adding support for preselected values
- Adjusting how groups behave
- Addressing some feedback
@mirka mirka added [Type] Enhancement A suggestion for improvement. [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). and removed [Type] Enhancement A suggestion for improvement. labels Aug 18, 2023
@andrewhayward andrewhayward marked this pull request as ready for review August 29, 2023 16:24
Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job on this one so far, @andrewhayward !

I gave a quick look at Storybook and at the code and left some comments.

I'll have a closer look at how the component behaves in the editor in the next review round

@@ -75,6 +79,10 @@ const clickButton = ( name ) => {
fireEvent.click( getButton( name ) );
};

const selectColorOption = ( name ) => {
fireEvent.click( getColorOption( name ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea for a follow-up PR: refactor BorderControl's unit tests from fireEvent to testing-library/user-event

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #54155.

@@ -91,10 +92,10 @@ function SinglePalette( {
}, [ colors, value, onChange, clearColor ] );

return (
<CircularOptionPicker
<CircularOptionPicker.OptionGroup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about dropping the group construct and modeling them as separate listboxes?

After interacting with the ColorPalette storybook demo with multiple origins, I also got the feeling that having each OptionGroup as a separate tab stop feels better.

packages/components/src/color-palette/index.tsx Outdated Show resolved Hide resolved
Comment on lines 91 to 102
/**
* A label to identify the purpose of the control.
*
* @todo Either this or `aria-labelledby` should be required
*/
'aria-label'?: string;
/**
* An ID of an element to provide a label for the control.
*
* @todo Either this or `aria-label` should be required
*/
'aria-labelledby'?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want either prop a or prop b to be required, you should be able to do so by using the never keyword — see this playground example that I created to illustrate how to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the purposes of this PR, I wanted to leave both aria-label and aria-labelledby as optional props here, because of the number of components that use this and don't provide either; fixing all of those would dramatically increase the scope of the ticket. I created a follow-up ticket to track that work.

The desired mutual exclusivity could probably be clearer here though, and it may be worth adopting the never union syntax, even if they both remain optional...

export type ColorPaletteProps = Pick< PaletteProps, 'onChange' > & {
  ...
} & (
  | {
      'aria-label'?: string;
      'aria-labelledby'?: never;
    }
  | {
      'aria-label'?: never;
      'aria-labelledby'?: string;
    }
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, we can do this as a follow-up

packages/components/src/duotone-picker/duotone-picker.tsx Outdated Show resolved Hide resolved
packages/components/src/duotone-picker/types.ts Outdated Show resolved Hide resolved
packages/components/src/gradient-picker/types.ts Outdated Show resolved Hide resolved
packages/components/src/circular-option-picker/types.ts Outdated Show resolved Hide resolved
packages/components/CHANGELOG.md Outdated Show resolved Hide resolved
@andrewhayward andrewhayward requested a review from ciampo September 1, 2023 17:03
Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀

Code changes look good and test well as per instructions.

@ciampo ciampo enabled auto-merge (squash) September 4, 2023 14:15
@ciampo
Copy link
Contributor

ciampo commented Sep 4, 2023

Noticed #54156 while testing, @andrewhayward this is also another good follow-up task

@ciampo ciampo merged commit 3434e37 into WordPress:trunk Sep 4, 2023
@github-actions github-actions bot added this to the Gutenberg 16.7 milestone Sep 4, 2023
@ciampo ciampo added the has dev note when dev note is done (for upcoming WordPress release) label Oct 12, 2023
@ciampo
Copy link
Contributor

ciampo commented Oct 12, 2023

Added a dev note in the PR description

@ciampo ciampo added the [Type] Enhancement A suggestion for improvement. label Oct 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). has dev note when dev note is done (for upcoming WordPress release) [Package] Components /packages/components [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants