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 menu bar buttons focusable with feature flag #8518

Open
wants to merge 14 commits into
base: feat/focusable-disable-components
Choose a base branch
from
6 changes: 6 additions & 0 deletions packages/a11y-base/src/keyboard-direction-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ export declare class KeyboardDirectionMixinClass {
* Focus the given item. Override this method to add custom logic.
*/
protected _focusItem(item: Element, navigating: boolean): void;

/**
* Returns whether the item is focusable. By default,
* returns true if the item is not disabled.
*/
protected _isItemFocusable(item: Element): boolean;
}
14 changes: 13 additions & 1 deletion packages/a11y-base/src/keyboard-direction-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const KeyboardDirectionMixin = (superclass) =>

const item = items[idx];

if (!item.hasAttribute('disabled') && this.__isMatchingItem(item, condition)) {
if (this._isItemFocusable(item) && this.__isMatchingItem(item, condition)) {
return idx;
}
}
Expand All @@ -189,4 +189,16 @@ export const KeyboardDirectionMixin = (superclass) =>
__isMatchingItem(item, condition) {
return typeof condition === 'function' ? condition(item) : true;
}

/**
* Returns whether the item is focusable. By default,
* returns true if the item is not disabled.
*
* @param {Element} item
* @return {boolean}
* @protected
*/
_isItemFocusable(item) {
return !item.hasAttribute('disabled');
}
};
16 changes: 16 additions & 0 deletions packages/menu-bar/src/vaadin-lit-menu-bar-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ class MenuBarButton extends Button {
super._onKeyDown(event);
this.__triggeredWithActiveKeys = null;
}

/**
* Override method inherited from `ButtonMixin` to allow keyboard navigation with
* arrow keys in the menu bar when the button is focusable in the disabled state.
*
* @param {Event} event
* @override
* @protected
*/
_shouldSuppressInteractionEvent(event) {
if (event.type === 'keydown' && ['ArrowLeft', 'ArrowRight'].includes(event.key)) {
return false;
}

return super._shouldSuppressInteractionEvent(event);
}
}

defineCustomElement(MenuBarButton);
16 changes: 16 additions & 0 deletions packages/menu-bar/src/vaadin-menu-bar-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ class MenuBarButton extends Button {
super._onKeyDown(event);
this.__triggeredWithActiveKeys = null;
}

/**
* Override method inherited from `ButtonMixin` to allow keyboard navigation with
* arrow keys in the menu bar when the button is focusable in the disabled state.
*
* @param {Event} event
* @override
* @protected
*/
_shouldSuppressInteractionEvent(event) {
if (event.type === 'keydown' && ['ArrowLeft', 'ArrowRight'].includes(event.key)) {
return false;
}

return super._shouldSuppressInteractionEvent(event);
}
}

defineCustomElement(MenuBarButton);
16 changes: 16 additions & 0 deletions packages/menu-bar/src/vaadin-menu-bar-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ export declare class MenuBarMixinClass {
* {text: 'Help'}
* ];
* ```
*
* #### Disabled items
*
* When an item is disabled, it prevents any user interaction with it, such as
* focusing, clicking, opening a sub-menu, etc. The item is also removed from
* tab order, which makes it inaccessible to screen readers.
*
* To improve accessibility, disabled root-level items (menu bar buttons) can be
* made focusable so that screen readers can still reach and properly announce
* them, while still preventing clicks. This is currently available as an
* experimental enhancement that can be enabled with the following feature flag:
*
* ```
* // Set before any menu bar is attached to the DOM.
* window.Vaadin.featureFlags.accessibleDisabledButtons = true;
* ```
*/
items: MenuBarItem[];

Expand Down
38 changes: 37 additions & 1 deletion packages/menu-bar/src/vaadin-menu-bar-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ export const MenuBarMixin = (superClass) =>
* ];
* ```
*
* #### Disabled items
*
* When an item is disabled, it prevents any user interaction with it, such as
* focusing, clicking, opening a sub-menu, etc. The item is also removed from
* tab order, which makes it inaccessible to screen readers.
*
* To improve accessibility, disabled root-level items (menu bar buttons) can be
* made focusable so that screen readers can still reach and properly announce
* them, while still preventing clicks. This is currently available as an
* experimental enhancement that can be enabled with the following feature flag:
*
* ```
* // Set before any menu bar is attached to the DOM.
* window.Vaadin.featureFlags.accessibleDisabledButtons = true;
* ```
*
* @type {!Array<!MenuBarItem>}
*/
items: {
Expand Down Expand Up @@ -688,7 +704,7 @@ export const MenuBarMixin = (superClass) =>

/** @protected */
_setTabindex(button, focused) {
if (this.tabNavigation && !button.disabled) {
if (this.tabNavigation && this._isItemFocusable(button)) {
button.setAttribute('tabindex', '0');
} else {
button.setAttribute('tabindex', focused ? '0' : '-1');
Expand Down Expand Up @@ -907,6 +923,10 @@ export const MenuBarMixin = (superClass) =>

/** @private */
__openSubMenu(button, keydown, options = {}) {
if (button.disabled) {
return;
}

const subMenu = this._subMenu;
const item = button.item;

Expand Down Expand Up @@ -1030,4 +1050,20 @@ export const MenuBarMixin = (superClass) =>
close() {
this._close();
}

/**
* Override method inherited from `KeyboardDirectionMixin` to allow
* focusing disabled buttons that are configured so.
*
* @param {Element} button
* @protected
* @override
*/
_isItemFocusable(button) {
if (button.disabled && button._shouldAllowFocusWhenDisabled) {
return button._shouldAllowFocusWhenDisabled();
}

return super._isItemFocusable(button);
}
};
3 changes: 3 additions & 0 deletions packages/menu-bar/test/focusable-disabled-buttons-lit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './not-animated-styles.js';
import '../vaadin-lit-menu-bar.js';
import './focusable-disabled-buttons.common.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './not-animated-styles.js';
import '../vaadin-menu-bar.js';
import './focusable-disabled-buttons.common.js';
84 changes: 84 additions & 0 deletions packages/menu-bar/test/focusable-disabled-buttons.common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { expect } from '@vaadin/chai-plugins';
import { fixtureSync, middleOfNode, nextRender } from '@vaadin/testing-helpers';
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';

describe('focusable disabled buttons', () => {
let menuBar, buttons;

before(() => {
window.Vaadin.featureFlags ??= {};
window.Vaadin.featureFlags.accessibleDisabledButtons = true;
});

after(() => {
window.Vaadin.featureFlags.accessibleDisabledButtons = false;
});

beforeEach(async () => {
menuBar = fixtureSync('<vaadin-menu-bar></vaadin-menu-bar>');
menuBar.items = [
{ text: 'Item 0' },
{ text: 'Item 1', disabled: true, children: [{ text: 'SubItem 0' }] },
{ text: 'Item 2' },
];
await nextRender(menuBar);
buttons = menuBar._buttons;
});

afterEach(async () => {
await resetMouse();
});

it('should not open sub-menu on disabled button click', async () => {
const { x, y } = middleOfNode(buttons[1]);
await sendMouse({ type: 'click', position: [Math.floor(x), Math.floor(y)] });
expect(buttons[1].hasAttribute('expanded')).to.be.false;
});

it('should not open sub-menu on disabled button hover', async () => {
menuBar.openOnHover = true;
const { x, y } = middleOfNode(buttons[1]);
await sendMouse({ type: 'move', position: [Math.floor(x), Math.floor(y)] });
expect(buttons[1].hasAttribute('expanded')).to.be.false;
});

it('should include disabled buttons in arrow key navigation', async () => {
await sendKeys({ press: 'Tab' });
expect(document.activeElement).to.equal(buttons[0]);

await sendKeys({ press: 'ArrowRight' });
expect(document.activeElement).to.equal(buttons[1]);

await sendKeys({ press: 'ArrowRight' });
expect(document.activeElement).to.equal(buttons[2]);

await sendKeys({ press: 'ArrowLeft' });
expect(document.activeElement).to.equal(buttons[1]);

await sendKeys({ press: 'ArrowLeft' });
expect(document.activeElement).to.equal(buttons[0]);
});

it('should include disabled buttons in Tab navigation', async () => {
menuBar.tabNavigation = true;

await sendKeys({ press: 'Tab' });
expect(document.activeElement).to.equal(buttons[0]);

await sendKeys({ press: 'Tab' });
expect(document.activeElement).to.equal(buttons[1]);

await sendKeys({ press: 'Tab' });
expect(document.activeElement).to.equal(buttons[2]);

await sendKeys({ down: 'Shift' });
await sendKeys({ press: 'Tab' });
await sendKeys({ up: 'Shift' });
expect(document.activeElement).to.equal(buttons[1]);

await sendKeys({ down: 'Shift' });
await sendKeys({ press: 'Tab' });
await sendKeys({ up: 'Shift' });
expect(document.activeElement).to.equal(buttons[0]);
});
});
Loading
Loading