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

ToggleGroupControl: support disabled options #63450

Merged
merged 11 commits into from
Jul 16, 2024

Conversation

ciampo
Copy link
Contributor

@ciampo ciampo commented Jul 11, 2024

What?

Related to #57862
Closes #57862
New take on #34945

  • Adds support for disabled option for the ToggleGroupControl component
  • Adds styles for disabled options

Why?

This was a missing feature in the component, a gap that should be filled.

How?

By forwarding the disabled prop to the correct internal components in the ToggleGroupControlOptionBase component.

Testing Instructions

  • Review new unit tests and make sure they pass reliably;
  • Edit the Storybook examples, by passing the disabled prop to some options, and interact with the component. Make sure that disabled option are correctly skipped and impossible to select
Here's a quick patch to test the storybook examples
diff --git a/packages/components/src/toggle-group-control/stories/index.story.tsx b/packages/components/src/toggle-group-control/stories/index.story.tsx
index 92f1e60762..3b9b9a400f 100644
--- a/packages/components/src/toggle-group-control/stories/index.story.tsx
+++ b/packages/components/src/toggle-group-control/stories/index.story.tsx
@@ -81,7 +81,7 @@ Default.args = {
 	children: [
 		{ value: 'left', label: 'Left' },
 		{ value: 'center', label: 'Center' },
-		{ value: 'right', label: 'Right' },
+		{ value: 'right', label: 'Right', disabled: true },
 		{ value: 'justify', label: 'Justify' },
 	].map( mapPropsToOptionComponent ),
 	isBlock: true,
@@ -125,7 +125,12 @@ WithIcons.args = {
 	...Default.args,
 	children: [
 		{ value: 'uppercase', label: 'Uppercase', icon: formatUppercase },
-		{ value: 'lowercase', label: 'Lowercase', icon: formatLowercase },
+		{
+			value: 'lowercase',
+			label: 'Lowercase',
+			icon: formatLowercase,
+			disabled: true,
+		},
 	].map( mapPropsToOptionIconComponent ),
 	isBlock: false,
 };

Screenshots or screencast

Before (trunk) After (this PR)
tgc.before.mp4
tgc.after.mp4

✍️ Dev note

ToggleGroupControlOption components can now be marked as disabled via the newly added disabled prop. While disabled, an option cannot be selected by the end user.

@ciampo ciampo requested a review from ajitbohra as a code owner July 11, 2024 17:06
Copy link

github-actions bot commented Jul 11, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ciampo <[email protected]>
Co-authored-by: mirka <[email protected]>
Co-authored-by: tyxla <[email protected]>
Co-authored-by: stokesman <[email protected]>
Co-authored-by: jasmussen <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@ciampo ciampo self-assigned this Jul 11, 2024
@ciampo ciampo added [Type] Bug An existing feature does not function as intended [Package] Components /packages/components labels Jul 11, 2024
@ciampo ciampo requested review from jasmussen, stokesman and a team July 11, 2024 17:11
Comment on lines +85 to +87
<ToggleGroupControlOption value="pizza" label="Pizza" />
<ToggleGroupControlOption value="rice" label="Rice" disabled />
<ToggleGroupControlOption value="pasta" label="Pasta" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ramonjd just making sure you're ok with those food options

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I was away last week.

I would have preferred risotto instead of just "rice", but I appreciate you asking. 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good point. Feel free to open a PR

Comment on lines +56 to +57
// due to how the component works, only the `disabled` prop should be used
| 'aria-disabled'
Copy link
Contributor Author

@ciampo ciampo Jul 11, 2024

Choose a reason for hiding this comment

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

I don't think it makes sense to have the options accessibleWhenDisabled given how the component works cc @mirka

Copy link
Member

Choose a reason for hiding this comment

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

I actually thought this was one of the cases where we always want the option to be focusable when disabled, similar to ToolbarButton (#63102) or tabs in a tablist. It's also a composite widget where there will be a limited number of options, and the number of tab stops it potentially adds to the tab sequence is either zero or negligible.

Implementing that in the radio case for ToggleGroupControl would require some additional design work though 🤔 But at least Ariakit supports the behavior nicely (StackBlitz demo).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implementing that in the radio case for ToggleGroupControl would require some additional design work though

Yep. Currently, since disabled options are not really supported, the backdrop indicator also works as a focus indicator.
If we make the options accessibleWhenDisabled, then we'll need to add separate focus rings.

But at least Ariakit supports the behavior nicely (StackBlitz demo).

Yup. We'll probably need to add the custom logic when rendering individual buttons

@mirka mirka linked an issue Jul 11, 2024 that may be closed by this pull request
@stokesman
Copy link
Contributor

stokesman commented Jul 11, 2024

Just going off the screen recording, it’s easy for me to see that an option is disabled 👍. Still, I wonder if the disabled opacity makes the text too light for a11y guidelines. Unless that’s not required of a disabled state but isn’t it?

It’s a thread-the-needle thing because even the enabled text is not that high contrast so lesser opacity will make it harder to see that an option is disabled. If the contrast is an issue perhaps we can come up with something else but given the current lack of usage for this feature it’s probably not too grave a concern. EDIT: I saw the existing use case Lena noted.

@mirka
Copy link
Member

mirka commented Jul 11, 2024

@stokesman FWIW, inactive controls are exempt from the contrast guidelines.

Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

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

Looks good and tests well 👍

Do we need a design ✅ just in case?

Thanks!

packages/components/CHANGELOG.md Outdated Show resolved Hide resolved
Comment on lines +68 to +69
opacity: 0.4;
cursor: default;
Copy link
Member

Choose a reason for hiding this comment

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

We have more and more of these disabled styles that do opacity: 0.4. Perhaps this is a good opportunity to generalize at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, although this PR is not the right place for it. We should probably assess the current state of shared config in the package, and decide next steps. For example, some variables come from the base-styles package, some are internal to the components package. Some variables are defined in Sass, some as CSS custom properties, and others in a JS object.

Copy link
Member

Choose a reason for hiding this comment

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

Definitely should be a separate PR 👍

@jasmussen
Copy link
Contributor

Seems good to me, 0.4 opacity is also what appears to be used elsewhere.

@ciampo
Copy link
Contributor Author

ciampo commented Jul 12, 2024

Thank you for all the comments!

I'll look into making disable options focusable and using Tabs-like focus styles.

I'll ask for a new round of review once that's completed

@ciampo ciampo force-pushed the feat/toggle-group-control-disabled-styles branch from 3655931 to 98d566f Compare July 15, 2024 10:05
@ciampo ciampo requested review from tyxla and mirka July 15, 2024 10:07
@ciampo ciampo force-pushed the feat/toggle-group-control-disabled-styles branch from a2be8b8 to adf4750 Compare July 15, 2024 10:09
@ciampo
Copy link
Contributor Author

ciampo commented Jul 15, 2024

I applied changes to make disabled options keyboard-accessible (ie. focusable).

The main design change is adding a focus ring for focused, disabled radio options, since the active indicator (backdrop) should not follow focus in that case.

I've also updated tests to reflect the new changes.

tgc.disabled.but.accessible.mp4

* indicator won't follow keyboard focus. This is not an issue for non-radio
* items, since the backdrop is already decoupled from keyboard focus.
*/
&[role='radio']:focus-visible::after {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The focus ring is applied to the after pseudo-element so that we can control its size via the inset prop, so that we can leave a little gap between the focus ring and the backdrop indicator.

Comment on lines 70 to 72
${ ButtonContentView } {
opacity: 0.4;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opacity is applied to the button contents, so that the focus ring can be at full opacity

Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

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

Nothing further from my perspective. The code looks good, and everything tests well, just as demonstrated on the video.

Thanks @ciampo 🚀

@ciampo
Copy link
Contributor Author

ciampo commented Jul 15, 2024

Thank you @tyxla ! I'll also wait for @jasmussen 's approval specifically for the disabled+focused styles before merging.

@jasmussen
Copy link
Contributor

I'm not the best to ask here, as this particular combo of focusing a component with roving focus and what looks like a classic tab-focus style is peculiar to me, specifically because the arrow-key to focus the disabled option still appears to have the previous item selected. It looks like you have two items focused at once. Is there any prior art to look at? What do other segmented controls with disabled options do with their roving focus?

My instinct would go towards the backdrop still animating to the disabled item.

But if this is the best practice, by all means. I wouldn't think this is a pattern that should be used in the block editor, even if the component supports it.

@ciampo
Copy link
Contributor Author

ciampo commented Jul 15, 2024

@jasmussen it's a tricky one, indeed!

because the arrow-key to focus the disabled option still appears to have the previous item selected.

That's exactly what is going on: focus is on a disabled option, which can't be selected (because disabled) — and therefore the previous item is still selected.

It looks like you have two items focused at once.

Yeah, that's a downside of the current approach. Here is what it looks like when showing the "focus ring" for every focused option (not just disabled):

Kapture.2024-07-15.at.17.24.05.mp4

Is there any prior art to look at? What do other segmented controls with disabled options do with their roving focus?

Other libraries

  • NextUI's tabs shows both a focus ring and a visual indicator, but the disabled tab behavior seems buggy (the tab is still selected even when disabled)
  • Material UI, Radix, Tamagui, Ant Design don't seem to allow disabled options to be focusable in the first place
  • Ariakit and React Aria Components don't seem to have a similar example

Similarities with Tabs

Tabs is quite similar:

  • it uses an indicator to show the currently selected item
  • its tabs are keyboard-focusable even when disabled
  • when a tab is disabled but focused, we keep the "active tab" indicator on the active item, and use a focus ring to show keyboard focus
  • this visually works because the focus ring doesn't overlap with the indicator

Screenshot 2024-07-15 at 17 04 59

My instinct would go towards the backdrop still animating to the disabled item.

I hacked together a preview, by having the backdrop animate to the disabled item, but with a faded opacity to convey the sense that the option is disabled. It looks promising:

Kapture.2024-07-15.at.17.59.25.mp4

We'd need to tweak the logic of the component, since currently the backdrop indicator follows the active item, instead of the focused item.

We will also need to refine some aspects of the design and animation, to make sure that the text opacity and color changes are well synced with the backdrop animation.

@ciampo
Copy link
Contributor Author

ciampo commented Jul 16, 2024

I reflected on this a bit more:

  1. The "backdrop still animating to the disabled item" solution visually looks the slickest, but it has one big flaw: the indicator becomes a focus indicator (instead of a "selected item" indicator), and the user doesn't have a way of knowing what is the selected value when focus is moved to a disabled item;
Kapture.2024-07-15.at.17.59.25.mp4
  1. The current implementation is confusing because it shows a focus ring only on disabled items, and thus the user may think that there are two focused items at once, and/or that they lose the keyboard focus when moving to a non-disabled option (plus, it's also incoherent visually)

  2. Always showing a focus ring (like we do in Tabs) feels like the most correct and less confusing option, but it conflicts with the design of the component.

Kapture.2024-07-15.at.17.24.05.mp4

In conclusion, I think that the better choice, given the circumstances, is not to allow disabled option items to be keyboard focusable in ToggleGroupControl.

What do y'all think ? @mirka @tyxla @jasmussen

@jasmussen
Copy link
Contributor

The specific behavior of this one is that I set focus on the segmented control itself, and now I can use arrow keys to choose an option. Tab or shift tab moves me to the next or previous focusable item:

segmented

This behavior is important to retain.

@tyxla
Copy link
Member

tyxla commented Jul 16, 2024

This behavior is important to retain.

@jasmussen if I understand correctly, the focus between components shouldn't be affected. This is about moving between tabs, which is performed with the arrow keys.

In conclusion, I think that the better choice, given the circumstances, is not to allow disabled option items to be keyboard focusable in ToggleGroupControl.

From https://www.w3.org/TR/wai-aria-1.0/states_and_properties#aria-disabled:

irrelevant options in a radio group may be disabled. Disabled elements might not receive focus from the tab order.

@ciampo in theory, yes, skipping focus for disabled tabs is a possible path forward. However, I'm a bit concerned that if disabled tabs are not focusable, users with visual impairment will miss that the button even exists. Instead, if we keep the button focusable, the assistive technology will inform them that they're working with a disabled tab.

As a potential solution, I wonder if we could perhaps make the focus of disabled tabs lighter than the regular one?

@jasmussen
Copy link
Contributor

We'll have the same challenge with the TabPanel, it's also designed to be a single focusable, with arrow-key navigation.

I'm not against capturing edgecases that are theoretically used by 3rd party consumers, even if not by the block editor itself yet, but it's important we keep the key flows the components were designed for.

@ciampo
Copy link
Contributor Author

ciampo commented Jul 16, 2024

The specific behavior of this one is that I set focus on the segmented control itself, and now I can use arrow keys to choose an option. Tab or shift tab moves me to the next or previous focusable item [...] This behavior is important to retain.

@jasmussen this behavior is not changing. What needs to be decided is, with regards to ToggleGroupControl:

  • should disabled options be keyboard focusable?
  • if so, how do we visually let the user know "where is the keyboard focus" vs "what item is currently selected" ?

in theory, yes, skipping focus for disabled tabs is a possible path forward. However, I'm a bit concerned that if disabled tabs are not focusable, users with visual impairment will miss that the button even exists. Instead, if we keep the button focusable, the assistive technology will inform them that they're working with a disabled tab.

I agree, but (as in many other situations) we need to pick the best compromise for a better overall UX.

As a potential solution, I wonder if we could perhaps make the focus of disabled tabs lighter than the regular one?

@tyxla I'm not sure I understand what you're proposing?

We'll have the same challenge with the TabPanel, it's also designed to be a single focusable, with arrow-key navigation.

@jasmussen we already solved this challenge in TabPanel (which will soon be deprecated in favour of Tabs):

  • we show a focus ring to indicate where keyboard focus is (even on disabled tabs)
  • we use an underline to indicate the currently selected tab
Kapture.2024-07-16.at.11.50.48.mp4

It's tricky to apply the same treatment to ToggleGroupControl because the indicator is a full-height backdrop and would visually clash / overlap with the focus ring. I tried this approach in a previous message:

Kapture.2024-07-15.at.17.24.05.mp4

@jasmussen
Copy link
Contributor

Understood, and I'm not the right person to ask there. As I understand, it appears to be a best practice for disabled buttons to still be focusable. I understand this especially to be the case where, for example, you press "Reset" on a button, where this same action also disables the button as there are no more filters to reset. In that case, it needs to be focusable so as to not cause a focus loss. But in the case of a segmented control, I'm struggling to come up with cases where we'd want to show a segmented control that has disabled options.

@tyxla
Copy link
Member

tyxla commented Jul 16, 2024

in theory, yes, skipping focus for disabled tabs is a possible path forward. However, I'm a bit concerned that if disabled tabs are not focusable, users with visual impairment will miss that the button even exists. Instead, if we keep the button focusable, the assistive technology will inform them that they're working with a disabled tab.

I agree, but (as in many other situations) we need to pick the best compromise for a better overall UX.

Okay, I think we can take this step-by-step. This PR aims to add support for disabled options, which it does. I suggest that we open an issue for discussing the disabled option focus issue, and move forward with keeping it non-focused for now. After all, as @jasmussen also said, disabled options will likely be a rare use case.

@ciampo ciampo force-pushed the feat/toggle-group-control-disabled-styles branch from adf4750 to fe854cf Compare July 16, 2024 11:43
@ciampo ciampo enabled auto-merge (squash) July 16, 2024 11:45
@ciampo ciampo merged commit 2171d82 into trunk Jul 16, 2024
61 checks passed
@ciampo ciampo deleted the feat/toggle-group-control-disabled-styles branch July 16, 2024 12:20
@github-actions github-actions bot added this to the Gutenberg 18.9 milestone Jul 16, 2024
@ciampo
Copy link
Contributor Author

ciampo commented Jul 16, 2024

I merged this PR while keeping disabled option items non-accessible, and opened a new issue to continue the conversation.

carstingaxion pushed a commit to carstingaxion/gutenberg that referenced this pull request Jul 18, 2024
* Add disabled option tests

* Fix handling of disabled prop on ToggleGroupControl options

* Add basic disabled styles

* Update snapshots

* CHANGELOG

* Support accessibleWhenDisabled for ratio option items

* Support accessibleWhenDisabled for button option items

* Update snapshots

* Update tests to reflect that options are keyboard focusable while disabled

* Move CHANGELOG entry to enhancements section

* Revert "Support accessibleWhenDisabled for ratio option items"

This reverts commit 62959f0.

--- 

Co-authored-by: ciampo <[email protected]>
Co-authored-by: mirka <[email protected]>
Co-authored-by: tyxla <[email protected]>
Co-authored-by: stokesman <[email protected]>
Co-authored-by: jasmussen <[email protected]>
@fabiankaegy fabiankaegy mentioned this pull request Oct 1, 2024
97 tasks
@ciampo ciampo added the has dev note when dev note is done (for upcoming WordPress release) label Oct 9, 2024
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] Bug An existing feature does not function as intended
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ToggleGroupControl: Add disabled state
6 participants