-
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
Add lint rule for inaccessible disabled Button
#62080
Conversation
When you run Is anyone available to do a quick sweep of these and fix or ignore as appropriate? If not, I can go in and ignore them with some kind of TODO comment to fix when possible. |
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Size Change: +3.06 kB (+0.18%) Total Size: 1.74 MB
ℹ️ View Unchanged
|
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.
Nice. Haven't approved yet as we still need to deal with the failures.
'error', | ||
{ | ||
selector: | ||
'JSXOpeningElement[name.name="Button"]:not(:has(JSXAttribute[name.name="__experimentalIsFocusable"])) JSXAttribute[name.name="disabled"]', |
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.
Confirmed that the rule works well 👍
FWIW, Ariakit handles a lot of this out of the box. I feel like before doing something like this we should just consider re-writing the internals to use Ariakit, keeping the same API. Then, it should be easier to figure out a strategy to nudge users to keep things accessible. This includes some ideas that I have explored in the past like making buttons with tooltips or binded as popover disclosure triggers automatically |
I have suggested this type of automatic mitigation as a preferred first step as well, but apparently there have already been a couple cases where this would not have been sufficient. Your idea of targeting all buttons bound as popover triggers sounds good, and would work in certain integrated components, but probably not all possible instances of a popover trigger. My feeling now is that these linting and automating strategies are not mutually exclusive, and would complement each other. We should continue to look into automating strategies, since that would benefit all consumers and not just Gutenberg where this lint rule would be in effect. People are already growing weary of the repeated accessibility issues that this introduces, so I'm hoping we can at least prevent new problematic instances and improve developer awareness, while working on some automated strategies in parallel. If they work well enough, maybe we can remove the lint rule. Does that sound reasonable? |
Empty action menu should still be perceivable to aleviate confusion, and does not clutter tab order due to Composite use.
disabled and aria-disabled are set with an identical condition, which doesn't make sense but signals the intent to keep it focusable.
…ption is always there)
… clutter tab order)
…doesn't clutter tab order)
…al that input is invalid)
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 went ahead and looked through all of the errors myself, since I realized it would help me get a better overview of our problems.
For most of them I made a decision based on looking at the code only. Some of them, I looked into the original PR, blames, or actual app UI to gather more context. None of them are tested in app. I believe this is sufficient because technically this shouldn't be introducing "regressions" — there is little damage that adding a __experimentalIsFocusable
could do. The only potentially regressive change is #62080 (comment), but the logic looks clear enough to me.
I wrote brief reasoning comments for each change to leave a trail in case somebody needs to blame
.
expect( screen.getByRole( 'button' ) ).toHaveAttribute( | ||
'aria-disabled', | ||
'true' | ||
); |
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.
This is the recommended way to test for aria-disabled
(see testing-library/jest-dom#144 (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.
Or maybe we can keep both assertions as they check for different things?
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.
That makes a lot of sense, actually. Addressed in bca37a2.
@@ -42,6 +42,7 @@ export default function InstallButton( { attributes, block, clientId } ) { | |||
} | |||
} ) | |||
} | |||
__experimentalIsFocusable |
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.
Clearly a busy state, which can cause focus loss.
// TODO: Investigate whether this button should be accessible when disabled. | ||
// eslint-disable-next-line no-restricted-syntax |
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.
This was the only instance I didn't feel confident enough to make a decision without deeper investigation. This is a publicly exported component with both a tabIndex
and disabled
prop exposed. (But no usages of these props within the Gutenberg repo.)
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 I did the archeology correctly, it was added here: #9815
It can now be seen when inserting buttons in the Buttons
block:
Judging by the functionality and the initial intent, my expectation is that this disabled
was left there unintentionally and there's no reason for it to be there in the first place. I expect that the inserter appender will not be visible at all, rather than stay visible and disabled. With that in mind, I don't think it needs to be accessible if it ends up disabled.
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 so much for investigating! I added a disable reason in d16d2b4.
@@ -149,6 +149,7 @@ export default function LinkPreview( { | |||
isEmptyURL || showIconLabels ? '' : ': ' + value.url | |||
) } | |||
ref={ ref } | |||
__experimentalIsFocusable |
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.
Alleviates confusion, since this is a "standard" button usually present in the view.
aria-disabled={ isFirstItem } | ||
// Disable reason: Truly disable when image is not selected. | ||
// eslint-disable-next-line no-restricted-syntax | ||
disabled={ ! isSelected } | ||
/> | ||
<Button | ||
icon={ chevronRight } | ||
onClick={ isLastItem ? undefined : onMoveForward } | ||
label={ __( 'Move image forward' ) } | ||
aria-disabled={ isLastItem } | ||
// Disable reason: Truly disable when image is not selected. | ||
// eslint-disable-next-line no-restricted-syntax | ||
disabled={ ! isSelected } |
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 was wondering whether a component having both disabled
and aria-disabled
props was something to flag with another lint, but this I think is a legitimate use case. (Though the same logic can also be expressed with a combination of the disabled
and the __experimentalIsFocusable
prop.)
@@ -43,6 +43,7 @@ export default function RenameModal( { menuTitle, onClose, onSave } ) { | |||
|
|||
<Button | |||
__next40pxDefaultSize | |||
__experimentalIsFocusable |
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.
Should be perceivable to signal that the user input is invalid.
@@ -168,6 +168,7 @@ export default function PostPreviewButton( { | |||
className={ className || 'editor-post-preview' } | |||
href={ href } | |||
target={ targetId } | |||
__experimentalIsFocusable |
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.
Should be perceivable, because the button is expected to be there.
@@ -93,6 +93,7 @@ export class PostPublishPanel extends Component { | |||
</div> | |||
<div className="editor-post-publish-panel__header-cancel-button"> | |||
<Button | |||
__experimentalIsFocusable |
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.
Can cause focus loss due to a dynamic disable.
@@ -86,6 +86,7 @@ function ImportForm( { instanceId, onUpload } ) { | |||
<Button | |||
type="submit" | |||
isBusy={ isLoading } | |||
__experimentalIsFocusable |
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.
Should be perceivable, and can cause focus loss.
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.
Accessible disabled matches the behavior of the undo/redo buttons in the actual WP block editor.
After going through all these errors manually, I am now more convinced that we should be approaching this with a combination of automated mitigation and human decision-making (prompted by lints). Automated mitigation can prevent a baseline number of accessibility issues, but a human making context-aware design decisions for each instance can further enhance the UX for screen reader users. |
@@ -219,7 +220,6 @@ function RevisionsButtons( { | |||
</p> | |||
) : ( | |||
<Button | |||
disabled={ areStylesEqual } |
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.
Confirmed that this can and should be removed because areStylesEqual
will never be true since it's already false
based on the ternary logic this is nested in.
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.
Thanks for taking the time to audit and fix them one by one! 👍
I spent some time going through them all with your comments and I think they all make sense. I've left a few comments where there was doubt or uncertainty.
I think it can be shipped - do you think we should ask folks from #59518 for some feedback just in case?
Thanks for the review, I appreciate it 🙏 Since this touches many files, I'll go ahead and merge today. It should be easy enough to revert or tweak, whether in full or in part. |
Thanks everyone for all your effort ❤️ A linting rule seems the best approach to prevent such a11y issues and, more importantly, to improve knowledge and education. |
* Add lint rule for inaccessible disabled `Button` * Exclude react native files * Include files in root `storybook` folder * Fix in Storybook editor playground (matches actual behavior) * Fix in install block button (is clearly a busy state) * Ignore in gallery image reordering buttons * Fix in LinkControl copy link button (aleviates confusion) * Ignore in edit-site pagination buttons (not confusing, and useful) * Fix in enable custom fields (is clearly a busy state) * Fix in DataViews list view Empty action menu should still be perceivable to aleviate confusion, and does not clutter tab order due to Composite use. * Fix in DataViews CompactItemActions (is dropdown trigger) * Fix in template part title modal disabled and aria-disabled are set with an identical condition, which doesn't make sense but signals the intent to keep it focusable. * Fix in PageList block (should be perceivable, esp. because the description is always there) * Fix in ConvertToLinksModal button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site "Apply globally" button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site nav menu rename modal (should be perceivable to signal that input is invalid) * Fix in RevisionsButtons (button is never visible when `areStylesEqual`) * Fix in RevisionsButtons (contains important info and should be perceivable) * Fix in GlobalStylesSidebar (should be perceivable) * Fix in PostPublishPanel cancel button (can cause focus loss) * Fix in PostPreviewButton (should be perceivable) * Defer decision in ButtonBlockAppender * Fix in reusable blocks import form (should be perceivable, can cause focus loss) * Adapt test for PostPreviewButton * Improve rigidity of accessible disabled detection in test * Add disable reason for ButtonBlockAppender Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla <tyxla@git.wordpress.org> Co-authored-by: DaniGuardiola <daniguardiola@git.wordpress.org>
This is a bug fix no? Looks like a bunch of buttons were made more accessible. |
* Add lint rule for inaccessible disabled `Button` * Exclude react native files * Include files in root `storybook` folder * Fix in Storybook editor playground (matches actual behavior) * Fix in install block button (is clearly a busy state) * Ignore in gallery image reordering buttons * Fix in LinkControl copy link button (aleviates confusion) * Ignore in edit-site pagination buttons (not confusing, and useful) * Fix in enable custom fields (is clearly a busy state) * Fix in DataViews list view Empty action menu should still be perceivable to aleviate confusion, and does not clutter tab order due to Composite use. * Fix in DataViews CompactItemActions (is dropdown trigger) * Fix in template part title modal disabled and aria-disabled are set with an identical condition, which doesn't make sense but signals the intent to keep it focusable. * Fix in PageList block (should be perceivable, esp. because the description is always there) * Fix in ConvertToLinksModal button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site "Apply globally" button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site nav menu rename modal (should be perceivable to signal that input is invalid) * Fix in RevisionsButtons (button is never visible when `areStylesEqual`) * Fix in RevisionsButtons (contains important info and should be perceivable) * Fix in GlobalStylesSidebar (should be perceivable) * Fix in PostPublishPanel cancel button (can cause focus loss) * Fix in PostPreviewButton (should be perceivable) * Defer decision in ButtonBlockAppender * Fix in reusable blocks import form (should be perceivable, can cause focus loss) * Adapt test for PostPreviewButton * Improve rigidity of accessible disabled detection in test * Add disable reason for ButtonBlockAppender Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla <tyxla@git.wordpress.org> Co-authored-by: DaniGuardiola <daniguardiola@git.wordpress.org>
* Add lint rule for inaccessible disabled `Button` * Exclude react native files * Include files in root `storybook` folder * Fix in Storybook editor playground (matches actual behavior) * Fix in install block button (is clearly a busy state) * Ignore in gallery image reordering buttons * Fix in LinkControl copy link button (aleviates confusion) * Ignore in edit-site pagination buttons (not confusing, and useful) * Fix in enable custom fields (is clearly a busy state) * Fix in DataViews list view Empty action menu should still be perceivable to aleviate confusion, and does not clutter tab order due to Composite use. * Fix in DataViews CompactItemActions (is dropdown trigger) * Fix in template part title modal disabled and aria-disabled are set with an identical condition, which doesn't make sense but signals the intent to keep it focusable. * Fix in PageList block (should be perceivable, esp. because the description is always there) * Fix in ConvertToLinksModal button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site "Apply globally" button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site nav menu rename modal (should be perceivable to signal that input is invalid) * Fix in RevisionsButtons (button is never visible when `areStylesEqual`) * Fix in RevisionsButtons (contains important info and should be perceivable) * Fix in GlobalStylesSidebar (should be perceivable) * Fix in PostPublishPanel cancel button (can cause focus loss) * Fix in PostPreviewButton (should be perceivable) * Defer decision in ButtonBlockAppender * Fix in reusable blocks import form (should be perceivable, can cause focus loss) * Adapt test for PostPreviewButton * Improve rigidity of accessible disabled detection in test * Add disable reason for ButtonBlockAppender Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla <tyxla@git.wordpress.org> Co-authored-by: DaniGuardiola <daniguardiola@git.wordpress.org>
* Add lint rule for inaccessible disabled `Button` * Exclude react native files * Include files in root `storybook` folder * Fix in Storybook editor playground (matches actual behavior) * Fix in install block button (is clearly a busy state) * Ignore in gallery image reordering buttons * Fix in LinkControl copy link button (aleviates confusion) * Ignore in edit-site pagination buttons (not confusing, and useful) * Fix in enable custom fields (is clearly a busy state) * Fix in DataViews list view Empty action menu should still be perceivable to aleviate confusion, and does not clutter tab order due to Composite use. * Fix in DataViews CompactItemActions (is dropdown trigger) * Fix in template part title modal disabled and aria-disabled are set with an identical condition, which doesn't make sense but signals the intent to keep it focusable. * Fix in PageList block (should be perceivable, esp. because the description is always there) * Fix in ConvertToLinksModal button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site "Apply globally" button (should be perceivable, and doesn't clutter tab order) * Fix in edit-site nav menu rename modal (should be perceivable to signal that input is invalid) * Fix in RevisionsButtons (button is never visible when `areStylesEqual`) * Fix in RevisionsButtons (contains important info and should be perceivable) * Fix in GlobalStylesSidebar (should be perceivable) * Fix in PostPublishPanel cancel button (can cause focus loss) * Fix in PostPreviewButton (should be perceivable) * Defer decision in ButtonBlockAppender * Fix in reusable blocks import form (should be perceivable, can cause focus loss) * Adapt test for PostPreviewButton * Improve rigidity of accessible disabled detection in test * Add disable reason for ButtonBlockAppender Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla <tyxla@git.wordpress.org> Co-authored-by: DaniGuardiola <daniguardiola@git.wordpress.org>
This was cherry-picked to the wp/6.6 branch. |
Alternative to #59518
Part of #56547
What?
Adds an eslint error for when the
disabled
prop is used without the__experimentalIsFocusable
prop in theButton
component.Why?
We have long been discussing how to prevent accessibility issues stemming from
disabled
controls (#56547). Although there are situations where you do want to completely disable a control, we more often find cases where this was not a conscious decision but simply not considered at all. In these cases, we often end up with inaccessible disabled controls, which are not perceivable by screen readers, or cause focus loss after closing a popover.Next steps
__experimentalIsFocusable
prop. We should consider renaming it toaccessibleWhenDisabled
, which is clear and matches the naming in Ariakit. (Button: Stabilize__experimentalIsFocusable
prop #62282)accessibleWhenDisabled
prop to other relevant components, and add those components to the eslint rule.How?
Other strategies considered include:
disabled
prop to be accessible disabled by default. This is not ideal because it goes against the standard behavior of thedisabled
HTML attribute.disabled
prop toisDisabled
, and then make that accessible disabled by default. This is not ideal because it will require a lot of API changes, and the behavioral difference between the two props would be unclear.disabled
, and nudge people toward usingaria-disabled
instead. This is not ideal because usingaria-disabled
is not necessarily going to address all issues, depending on the element/component.Testing Instructions
✅ The linter should error in places where a
Button
has adisabled
prop without a__experimentalIsFocusable
prop.✅ The error message should be clear and actionable.