-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Making Circular Option Picker a listbox
#52255
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
Using the `Composite` component to make the Circular Option Picker present as a listbox.
|
👋 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. |
mirka
left a comment
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.
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.
|
|
||
| return ( | ||
| <CircularOptionPicker | ||
| <CircularOptionPicker.OptionGroup |
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.
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?
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.
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.
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.
After further discussion, we've decided that we're going to keep the current behaviour, and iterate if/when necessary in a future PR.
| function CircularOptionPicker( props: CircularOptionPickerProps ) { | ||
| const { actions, className, options, children } = props; | ||
| const { actions, className, options, children, loop = true } = props; | ||
| const compositeState = useCompositeState( { loop } ); |
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.
I'm actually not up to date on this, but is there a reason why we're using useCompositeState instead of useCompositeStore?
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.
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.
| ...args | ||
| } ) => { | ||
| const [ color, setColor ] = useState< string | undefined >(); | ||
| const [ color, setColor ] = useState< string | undefined >( value ); |
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.
(Marking this as TODO so we don't forget to address it)
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.
Out of curiosity, what exactly was the TODO item related to this line of code?
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.
@andrewhayward would you happen to know what needed to be done around this line?
…ard_accessiblity--listbox
– Adding support for RTL - Adding support for preselected values - Adjusting how groups behave - Addressing some feedback
Using `useEffect` to fix updates during render cycle
ciampo
left a comment
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.
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
| }; | ||
|
|
||
| const selectColorOption = ( name ) => { | ||
| fireEvent.click( getColorOption( name ) ); |
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.
Another idea for a follow-up PR: refactor BorderControl's unit tests from fireEvent to testing-library/user-event
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.
See #54155.
|
|
||
| return ( | ||
| <CircularOptionPicker | ||
| <CircularOptionPicker.OptionGroup |
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.
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.
| /** | ||
| * 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; |
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.
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.
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.
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;
}
);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.
Sounds good, we can do this as a follow-up
ciampo
left a comment
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.
LGTM 🚀
Code changes look good and test well as per instructions.
|
Noticed #54156 while testing, @andrewhayward this is also another good follow-up task |
|
Added a dev note in the PR description |
What?
As per #35292, This patch modifies
CircularOptionPickerto become alistbox, but using theCompositepackage. Additionally, it updates the various components that useCircularOptionPicker.Why?
Currently, keyboard interaction with
CircularOptionPickers is difficult, as each option presents as an individual tab stop. By changing the component to behave as alistbox, the entire control becomes a single tab stop, with individual colour options accessed using arrow keys.How?
The
CircularOptionPickerhas been partially rebuilt using the variousCompositeelements, 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 alistboxby 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
CircularOptionPickerto still render as a list of individual buttons, consumers of the component can use theasButtonsprop to switch back to the legacy behavior.