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

Add non-listbox functionality back to CircularOptionPicker #54290

Conversation

andrewhayward
Copy link
Contributor

@andrewhayward andrewhayward commented Sep 8, 2023

What?

This PR puts back the ability to use CircularOptionPicker as buttons, which was removed in #52255, while maintaining the new (now default) listbox behaviour. (Closes #54239.)

Why?

In #35292, CircularOptionPicker was refactored to present better semantics and keyboard navigation for picking options. However, as per #54239, it was discovered that CircularOptionPicker is not just used as an "option picker", but also as an access method for editing options. As such, we need to put the original functionality back in place, while maintaining the new functionality, so that the control can be used in either way.

How?

A new asButtons flag has been added to CircularOptionPicker, allowing component consumers to decide how it should be rendered, depending on context. This is passed on by ColorPalette, DuotonePicker and GradientPicker, the higher-order components that expose it.

Testing Instructions

Superficially, nothing should change, and CircularOptionPicker should continue to behave as before. Clicking each option should fire an onClick event, and in Storybook, the selected option should show as checked. Toggling asButtons shouldn't impact this behaviour.

Testing Instructions for Keyboard

With a keyboard, the default behaviour should also continue as before. The CircularOptionPicker control should still present as a single tab stop, and arrow keys should navigate between the different options. Semantically, it should still present as a listbox with multiple options, marked aria-selected as appropriate.

Enabling asButtons should change this, so that each option becomes its own tab stop. The listbox semantics should be removed, and each option should be a button, marked aria-pressed where needed.

Andrew Hayward added 2 commits September 8, 2023 09:34
Added some tests and a new story to go with the changes.
They all need to pass through the new `asButtons`/`disableLooping` prop following the `CircularOptionPicker` refactor.
@ciampo ciampo added [Type] Enhancement A suggestion for improvement. [Package] Components /packages/components labels Sep 8, 2023
@alexstine
Copy link
Contributor

That's annoying that we have to maintain two versions.

@andrewhayward
Copy link
Contributor Author

That's annoying that we have to maintain two versions.

Unfortunately, despite what CircularOptionPicker might be called, it was built in such a way that it could be used for purposes other than picking things. As such, it was used for purposes other than picking things. And here we are.

The joys of component libraries!

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.

Thank you for working on this (and adding unit tests too).

Left a few comments, let me know in case anything isn't clear.

packages/components/src/circular-option-picker/types.ts Outdated Show resolved Hide resolved
{ ...compositeState }
as={ Button }
className={ classnames( className, {
'is-pressed': isSelected,
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason why we're not passing isPressed as a prop here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doing so also sets aria-pressed on the control, which we don't want for role="option". I thought I'd tried to override that and failed, but clearly not hard enough. Definitely better to let Button worry about which classes to apply.

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. Maybe let's add an inline comment above the aria-pressed override explaining the reason for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out I possibly had tried hard enough, because aria-pressed={ null } doesn't "work"...

Types of property ''aria-pressed'' are incompatible.
Type 'null' is not assignable to type 'boolean | "true" | "false" | "mixed" | undefined'.

Setting it to null does properly override the value, and prevent aria-pressed from showing up as an attribute in the HTML, but TypeScript doesn't like it.

Copy link
Contributor

Choose a reason for hiding this comment

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

From what you say, it sounds like passing undefined would correctly remove the aria-pressed attribute from the button.

Maybe something like this would work?

{ ...( isSelected ? { 'aria-pressed': undefined } : {} ) }

Let's still add an inline comment in case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately passing undefined as a value for aria-pressed gets it ignored entirely. I've added a note to explain the class addition.

Note

Follow-up task: Update Button to check for role before setting aria-pressed, and set a more appropriate attribute (e.g. aria-selected) as needed.

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 — could you open a new issue about it? You could work on it as a follow-up in the upcoming weeks.

@ciampo
Copy link
Contributor

ciampo commented Sep 12, 2023

One more question:

This is passed on by ColorPalette, DuotonePicker and GradientPicker, the higher-order components that expose it.

Currently, do ColorPalette, DuotonePicker and GradientPicker all need to expose this prop? Do they all render in scenarios which required them to sometimes be a listbox, sometimes a list of separate buttons?

My instinct is usually to add the least amount of public APIs necessary.

@andrewhayward
Copy link
Contributor Author

One more question:

This is passed on by ColorPalette, DuotonePicker and GradientPicker, the higher-order components that expose it.

Currently, do ColorPalette, DuotonePicker and GradientPicker all need to expose this prop? Do they all render in scenarios which required them to sometimes be a listbox, sometimes a list of separate buttons?

My instinct is usually to add the least amount of public APIs necessary.

ColorPalette and GradientPicker definitely do, in the theme style editor. I think DuotonePicker does too, in the same way, but that actually seems broken to me, having looked.

Copy link
Contributor

@chad1008 chad1008 left a comment

Choose a reason for hiding this comment

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

I definitely feel like more of an observer on this PR but it looks good and tests well to me. Nothing new to add, but I do agree with @ciampo's comment on those two Omits.

This was fun to go through, thanks for all the work you've done on it!

@andrewhayward
Copy link
Contributor Author

Definitely agreed with the Omit feedback. It's been a bit of a slog to refactor them out, but I think everything is happy now.

@andrewhayward
Copy link
Contributor Author

I think DuotonePicker does too, in the same way, but that actually seems broken to me, having looked.

For whatever reason, DuotonePicker here is seemingly intentionally useless, with onChange={ noop }... not sure why!

<DuotonePicker
duotonePalette={ duotonePalette }
disableCustomDuotone={ true }
disableCustomColors={ true }
clearable={ false }
onChange={ noop }
/>

@andrewhayward
Copy link
Contributor Author

Overrode the code sample for WithLoopingDisabled to make sure that loop={false} shows up. Would prefer to be able to override how loop is handled at the Story level so that the live sample works correctly too, but for now this will have to do.

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.

Almost there! Just a few minor comments left, but things are looking good.

{ ...compositeState }
as={ Button }
className={ classnames( className, {
'is-pressed': isSelected,
Copy link
Contributor

Choose a reason for hiding this comment

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

From what you say, it sounds like passing undefined would correctly remove the aria-pressed attribute from the button.

Maybe something like this would work?

{ ...( isSelected ? { 'aria-pressed': undefined } : {} ) }

Let's still add an inline comment in case.

packages/components/src/circular-option-picker/types.ts Outdated Show resolved Hide resolved
packages/components/src/circular-option-picker/types.ts Outdated Show resolved Hide resolved
packages/components/src/composite/index.ts Show resolved Hide resolved
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.

I believe something went wrong with the latest trunk merge.

Also, the last commit broke unit tests

@andrewhayward andrewhayward force-pushed the 54239/allow-circularoptionpicker-non-listbox branch from 3b1e00e to 87a3b2b Compare September 15, 2023 15:02
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.

🚀

@ciampo ciampo enabled auto-merge (squash) September 15, 2023 15:14
@ciampo ciampo disabled auto-merge September 15, 2023 15:14
@andrewhayward andrewhayward merged commit d2e3e29 into WordPress:trunk Sep 15, 2023
@github-actions github-actions bot added this to the Gutenberg 16.7 milestone Sep 15, 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

See #52255 for the dev note

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has dev note when dev note is done (for upcoming WordPress release) [Package] Components /packages/components [Type] Enhancement A suggestion for improvement.
Projects
Status: Done 🎉
Development

Successfully merging this pull request may close these issues.

Allow CircularOptionPicker to work as a non-listbox
4 participants