-
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
Making Circular Option Picker a listbox
#52255
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. |
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
.
@@ -91,10 +92,10 @@ function SinglePalette( { | |||
}, [ colors, value, onChange, clearColor ] ); | |||
|
|||
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 group
s 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.
@@ -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 } ); |
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
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
@@ -75,6 +79,10 @@ const clickButton = ( name ) => { | |||
fireEvent.click( getButton( name ) ); | |||
}; | |||
|
|||
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.
@@ -91,10 +92,10 @@ function SinglePalette( { | |||
}, [ colors, value, onChange, clearColor ] ); | |||
|
|||
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
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
CircularOptionPicker
to become alistbox
, but using theComposite
package. Additionally, it updates the various components that useCircularOptionPicker
.Why?
Currently, keyboard interaction with
CircularOptionPicker
s 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
CircularOptionPicker
has been partially rebuilt using the variousComposite
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 alistbox
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 theasButtons
prop to switch back to the legacy behavior.