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

feat: make disabled buttons focusable with feature flag #8513

Merged
merged 22 commits into from
Jan 21, 2025

Conversation

vursen
Copy link
Contributor

@vursen vursen commented Jan 15, 2025

Description

To improve accessibility, the PR introduces an option to make disabled buttons focusable so that screen readers can reach and announce them to users, including any attached tooltips and popovers. This behavior is currently experimental and can be enabled with the feature flag:

// Set before any button is added to the DOM.
window.Vaadin.featureFlags.accessibleDisabledButtons = true;

Depends on

Part of #4585

Type of change

  • Feature

@vursen vursen changed the title feat: make disabled buttons focusable behind feature flag feat: make disabled buttons focusable with feature flag Jan 15, 2025
@vursen vursen force-pushed the feat/focusable-disable-components branch 2 times, most recently from 6446b00 to 911d0b4 Compare January 16, 2025 05:37
@@ -119,7 +119,8 @@ describe('crud buttons', () => {
expect(spy.firstCall.args[0].detail.value).to.deep.eql({ foo: 'bar' });
});

it('should save a new pre-filled item', async () => {
// FIXME: Why is this test failing in Webkit with Lit?
it.skip('should save a new pre-filled item', async () => {
Copy link
Contributor Author

@vursen vursen Jan 16, 2025

Choose a reason for hiding this comment

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

note: This test started failing in Webkit with Lit after interaction events were blocked for disabled buttons. It seems that the test was previously passing by mistake. When running this test in Webkit or Safari, the save button remains disabled for some reason when the click is performed. Blocking interaction events just exposed this issue. I tried adding various timeouts, but without success. The attributeChangedCallback simply doesn't get triggered when isDirty becomes true:

saveButton.toggleAttribute('disabled', this.__isSaveBtnDisabled(isDirty));

Since it seems to be an issue with the test itself and is only reproducible in Webkit and Safari, I decided to skip this test for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change was extracted into #8541

@vursen vursen force-pushed the feat/focusable-disable-components branch from 911d0b4 to 35f06ee Compare January 16, 2025 09:12
@vursen vursen marked this pull request as ready for review January 17, 2025 15:06
Comment on lines 10 to 21
const INTERACTION_EVENTS = [
'mousedown',
'mouseup',
'touchstart',
'touchend',
'click',
'dblclick',
'keydown',
'keyup',
'pointerstart',
'pointerend',
];
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the purpose of disabling these events? It doesn't look like it's needed for preventing click events. Otherwise I wonder if keypress is missing here, which is still being fired.

Copy link
Contributor Author

@vursen vursen Jan 20, 2025

Choose a reason for hiding this comment

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

All these events were disabled by the browser with pointer-events: none, and by not disabling them now, we risk breaking apps that didn't check for disabled but did subscribe to events like mousedown. That said, events like touchmove or mouseenter need to be allowed since they are used by tooltips. So, in other words, I thought it would be safer to keep the behavior as close to the original as possible. Good catch on keypress – I'll add it to the list.

Copy link
Contributor Author

@vursen vursen Jan 20, 2025

Choose a reason for hiding this comment

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

It turns out that adding some events to this list was incorrect. Native buttons don't suppress pointer and touch events when disabled. I've removed those and added keypress.

Copy link
Member

Choose a reason for hiding this comment

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

Checked this and found that pointer events are intentionally not suppressed for native disabled controls.

Regarding mouse events, as described here, Firefox also blocks contextmenu event but other browsers don't.
BTW there's also auxclick event mentioned there but it's mentioned it is "not about activation".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've decided to extract the suppression logic into a separate PR: #8541

packages/button/src/vaadin-button.js Show resolved Hide resolved
packages/button/src/vaadin-button.js Outdated Show resolved Hide resolved
packages/a11y-base/src/tabindex-mixin.js Outdated Show resolved Hide resolved
packages/button/theme/material/vaadin-button-styles.js Outdated Show resolved Hide resolved
Comment on lines 77 to 79
if (this._shouldAllowFocusWhenDisabled()) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

Could test new tabindex-mixin logic in a11y-base/test/tabindex-mixin.test.js

Copy link
Contributor Author

@vursen vursen Jan 20, 2025

Choose a reason for hiding this comment

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

Considering that this is now a private method, does it still make sense to test it?

Comment on lines 87 to 98
/**
* Overrides the default element `focus` method in order to prevent
* focusing the element when it is disabled.
*
* @protected
* @override
*/
focus() {
if (!this.disabled || this._shouldAllowFocusWhenDisabled()) {
super.focus();
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Previously it was possible to programmatically move focus to a disabled button. This change affects the behaviour even with the feature flag off. It is intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, because native buttons don't allow programmatic focus when disabled.

Copy link
Contributor Author

@vursen vursen Jan 20, 2025

Choose a reason for hiding this comment

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

I can extract this change into a separate PR by the way, as it's more like a fix.

Copy link
Member

Choose a reason for hiding this comment

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

Should the change also go to focus-mixin rather than tabindex-mixin?

Copy link
Contributor Author

@vursen vursen Jan 20, 2025

Choose a reason for hiding this comment

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

I also had this dilemma. However, my reasoning for placing this change to TabindexMixin was that focus() has to be prevented only for components that are made focusable with tabindex=0, because otherwise, it already works out of the box.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created a separate PR with this change: #8540

@vursen vursen merged commit 6c77025 into main Jan 21, 2025
9 checks passed
@vursen vursen deleted the feat/focusable-disable-components branch January 21, 2025 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants