diff --git a/packages/a11y-base/src/keyboard-direction-mixin.d.ts b/packages/a11y-base/src/keyboard-direction-mixin.d.ts
index 5a5ab71c4c..bcbf6ef00c 100644
--- a/packages/a11y-base/src/keyboard-direction-mixin.d.ts
+++ b/packages/a11y-base/src/keyboard-direction-mixin.d.ts
@@ -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;
}
diff --git a/packages/a11y-base/src/keyboard-direction-mixin.js b/packages/a11y-base/src/keyboard-direction-mixin.js
index 5b36382da5..4acbecb52a 100644
--- a/packages/a11y-base/src/keyboard-direction-mixin.js
+++ b/packages/a11y-base/src/keyboard-direction-mixin.js
@@ -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;
}
}
@@ -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');
+ }
};
diff --git a/packages/button/src/vaadin-button-mixin.js b/packages/button/src/vaadin-button-mixin.js
index f4ce0b9a3b..068616064e 100644
--- a/packages/button/src/vaadin-button-mixin.js
+++ b/packages/button/src/vaadin-button-mixin.js
@@ -92,8 +92,18 @@ export const ButtonMixin = (superClass) =>
/** @private */
__onInteractionEvent(event) {
- if (this.disabled) {
+ if (this.__shouldSuppressInteractionEvent(event)) {
event.stopImmediatePropagation();
}
}
+
+ /**
+ * Returns whether to suppress interaction events like `click`, `keydown`, etc.
+ * By default suppresses all interaction events when the button is disabled.
+ *
+ * @private
+ */
+ __shouldSuppressInteractionEvent(_event) {
+ return this.disabled;
+ }
};
diff --git a/packages/menu-bar/src/vaadin-lit-menu-bar-button.js b/packages/menu-bar/src/vaadin-lit-menu-bar-button.js
index c892838743..035760fd1a 100644
--- a/packages/menu-bar/src/vaadin-lit-menu-bar-button.js
+++ b/packages/menu-bar/src/vaadin-lit-menu-bar-button.js
@@ -48,6 +48,20 @@ 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.
+ *
+ * @override
+ */
+ __shouldSuppressInteractionEvent(event) {
+ if (event.type === 'keydown' && ['ArrowLeft', 'ArrowRight'].includes(event.key)) {
+ return false;
+ }
+
+ return super.__shouldSuppressInteractionEvent(event);
+ }
}
defineCustomElement(MenuBarButton);
diff --git a/packages/menu-bar/src/vaadin-menu-bar-button.js b/packages/menu-bar/src/vaadin-menu-bar-button.js
index 452ce932da..38f89b36f6 100644
--- a/packages/menu-bar/src/vaadin-menu-bar-button.js
+++ b/packages/menu-bar/src/vaadin-menu-bar-button.js
@@ -45,6 +45,20 @@ 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.
+ *
+ * @override
+ */
+ __shouldSuppressInteractionEvent(event) {
+ if (event.type === 'keydown' && ['ArrowLeft', 'ArrowRight'].includes(event.key)) {
+ return false;
+ }
+
+ return super.__shouldSuppressInteractionEvent(event);
+ }
}
defineCustomElement(MenuBarButton);
diff --git a/packages/menu-bar/src/vaadin-menu-bar-mixin.d.ts b/packages/menu-bar/src/vaadin-menu-bar-mixin.d.ts
index f3b7733ba9..2b878936aa 100644
--- a/packages/menu-bar/src/vaadin-menu-bar-mixin.d.ts
+++ b/packages/menu-bar/src/vaadin-menu-bar-mixin.d.ts
@@ -99,6 +99,26 @@ export declare class MenuBarMixinClass {
* {text: 'Help'}
* ];
* ```
+ *
+ * #### Disabled buttons
+ *
+ * When a root-level item (menu bar button) is disabled, it prevents all user
+ * interactions with it, such as focusing, clicking, opening a sub-menu, etc.
+ * The button is also removed from tab order, which makes it unreachable via
+ * the keyboard navigation.
+ *
+ * While the default behavior effectively prevents accidental interactions,
+ * it has an accessibility drawback: screen readers skip disabled buttons
+ * entirely, and users can't see tooltips that might explain why the button
+ * is disabled. To address this, an experimental enhancement allows disabled
+ * menu bar buttons to receive focus and show tooltips, while still preventing
+ * other interactions. This feature 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[];
diff --git a/packages/menu-bar/src/vaadin-menu-bar-mixin.js b/packages/menu-bar/src/vaadin-menu-bar-mixin.js
index d147dd0fe8..652fee7339 100644
--- a/packages/menu-bar/src/vaadin-menu-bar-mixin.js
+++ b/packages/menu-bar/src/vaadin-menu-bar-mixin.js
@@ -78,6 +78,26 @@ export const MenuBarMixin = (superClass) =>
* ];
* ```
*
+ * #### Disabled buttons
+ *
+ * When a root-level item (menu bar button) is disabled, it prevents all user
+ * interactions with it, such as focusing, clicking, opening a sub-menu, etc.
+ * The button is also removed from tab order, which makes it unreachable via
+ * the keyboard navigation.
+ *
+ * While the default behavior effectively prevents accidental interactions,
+ * it has an accessibility drawback: screen readers skip disabled buttons
+ * entirely, and users can't see tooltips that might explain why the button
+ * is disabled. To address this, an experimental enhancement allows disabled
+ * menu bar buttons to receive focus and show tooltips, while still preventing
+ * other interactions. This feature 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}
*/
items: {
@@ -688,7 +708,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');
@@ -907,6 +927,10 @@ export const MenuBarMixin = (superClass) =>
/** @private */
__openSubMenu(button, keydown, options = {}) {
+ if (button.disabled) {
+ return;
+ }
+
const subMenu = this._subMenu;
const item = button.item;
@@ -1030,4 +1054,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);
+ }
};
diff --git a/packages/menu-bar/test/accessible-disabled-buttons-lit.test.js b/packages/menu-bar/test/accessible-disabled-buttons-lit.test.js
new file mode 100644
index 0000000000..099addfb4b
--- /dev/null
+++ b/packages/menu-bar/test/accessible-disabled-buttons-lit.test.js
@@ -0,0 +1,3 @@
+import './not-animated-styles.js';
+import '../vaadin-lit-menu-bar.js';
+import './accessible-disabled-buttons.common.js';
diff --git a/packages/menu-bar/test/accessible-disabled-buttons-polymer.test.js b/packages/menu-bar/test/accessible-disabled-buttons-polymer.test.js
new file mode 100644
index 0000000000..1c7e3648bc
--- /dev/null
+++ b/packages/menu-bar/test/accessible-disabled-buttons-polymer.test.js
@@ -0,0 +1,3 @@
+import './not-animated-styles.js';
+import '../vaadin-menu-bar.js';
+import './accessible-disabled-buttons.common.js';
diff --git a/packages/menu-bar/test/accessible-disabled-buttons.common.js b/packages/menu-bar/test/accessible-disabled-buttons.common.js
new file mode 100644
index 0000000000..e264d464b2
--- /dev/null
+++ b/packages/menu-bar/test/accessible-disabled-buttons.common.js
@@ -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('accessible disabled buttons', () => {
+ let menuBar, buttons;
+
+ before(() => {
+ window.Vaadin.featureFlags ??= {};
+ window.Vaadin.featureFlags.accessibleDisabledButtons = true;
+ });
+
+ after(() => {
+ window.Vaadin.featureFlags.accessibleDisabledButtons = false;
+ });
+
+ beforeEach(async () => {
+ menuBar = fixtureSync('');
+ 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]);
+ });
+});
diff --git a/test/integration/menu-bar-tooltip.test.js b/test/integration/menu-bar-tooltip.test.js
index 86ead0c7e0..929b16c808 100644
--- a/test/integration/menu-bar-tooltip.test.js
+++ b/test/integration/menu-bar-tooltip.test.js
@@ -7,11 +7,14 @@ import {
fixtureSync,
focusin,
focusout,
+ middleOfNode,
mousedown,
nextRender,
tabKeyDown,
} from '@vaadin/testing-helpers';
+import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
import sinon from 'sinon';
+import './not-animated-styles.js';
import '@vaadin/menu-bar';
import { Tooltip } from '@vaadin/tooltip';
import { mouseenter, mouseleave } from '@vaadin/tooltip/test/helpers.js';
@@ -34,316 +37,393 @@ describe('menu-bar with tooltip', () => {
Tooltip.setDefaultHideDelay(0);
});
- beforeEach(async () => {
- menuBar = fixtureSync(`
-
-
-
- `);
- menuBar.items = [
- { text: 'Edit', tooltip: 'Edit tooltip' },
- {
- text: 'Share',
- children: [{ text: 'By email' }],
- },
- {
- text: 'Move',
- tooltip: 'Move tooltip',
- children: [{ text: 'To folder' }],
- },
- ];
-
- await nextRender();
- buttons = menuBar._buttons;
-
- tooltip = menuBar.querySelector('vaadin-tooltip');
- });
-
- it('should set manual on the tooltip to true', () => {
- expect(tooltip.manual).to.be.true;
- });
-
- it('should show tooltip on menu button mouseover', () => {
- mouseover(buttons[0]);
- expect(tooltip.opened).to.be.true;
- });
-
- it('should use the tooltip property of an item as tooltip', () => {
- mouseover(buttons[0]);
- expect(getTooltipText()).to.equal('Edit tooltip');
- });
-
- it('should not show tooltip for an item which has no tooltip', () => {
- mouseover(buttons[1]);
- expect(getTooltipText()).not.to.be.ok;
- });
-
- it('should not show tooltip on another parent menu button mouseover when open', async () => {
- mouseover(buttons[1]);
- buttons[1].click();
- await nextRender();
- mouseover(buttons[2]);
- await nextRender();
- expect(tooltip.opened).to.be.false;
- });
-
- it('should hide tooltip on menu bar mouseleave', () => {
- mouseover(buttons[0]);
- mouseleave(menuBar);
- expect(tooltip.opened).to.be.false;
- });
-
- it('should hide tooltip on menu bar container mouseover', () => {
- mouseover(buttons[0]);
- mouseover(menuBar._container);
- expect(tooltip.opened).to.be.false;
- });
-
- it('should show tooltip again on menu bar button mouseover', () => {
- mouseover(buttons[0]);
- mouseover(menuBar._container);
- mouseover(buttons[1]);
- expect(tooltip.opened).to.be.true;
- });
-
- it('should set tooltip target on menu button mouseover', () => {
- mouseover(buttons[0]);
- expect(tooltip.target).to.be.equal(buttons[0]);
- });
-
- it('should set tooltip context on menu button mouseover', () => {
- mouseover(buttons[0]);
- expect(tooltip.context).to.be.instanceOf(Object);
- expect(tooltip.context.item.text).to.equal('Edit');
- });
-
- it('should change tooltip context on another menu button mouseover', () => {
- mouseover(buttons[0]);
- mouseover(buttons[1]);
- expect(tooltip.context.item.text).to.equal('Share');
- });
-
- it('should hide tooltip on menu button mousedown', () => {
- mouseover(buttons[0]);
- mousedown(buttons[0]);
- expect(tooltip.opened).to.be.false;
- });
-
- it('should hide tooltip on mouseleave from overlay to outside', () => {
- const overlay = tooltip._overlayElement;
- mouseover(buttons[0]);
- mouseenter(overlay);
- mouseleave(overlay);
- expect(overlay.opened).to.be.false;
- });
-
- it('should not show tooltip on focus without keyboard interaction', async () => {
- buttons[0].focus();
- await nextRender();
- expect(tooltip.opened).to.be.false;
- });
-
- it('should show tooltip on menu button keyboard focus', () => {
- tabKeyDown(document.body);
- focusin(buttons[0]);
- expect(tooltip.opened).to.be.true;
- });
-
- it('should not show tooltip on another parent menu button focus when open', async () => {
- buttons[0].focus();
- arrowRight(buttons[0]);
- arrowDown(buttons[1]);
- arrowRight(buttons[1]);
- await nextRender();
- expect(tooltip.opened).to.be.false;
- });
+ describe('default', () => {
+ beforeEach(async () => {
+ menuBar = fixtureSync(`
+
+
+
+ `);
+ menuBar.items = [
+ { text: 'Edit', tooltip: 'Edit tooltip' },
+ {
+ text: 'Share',
+ children: [{ text: 'By email' }],
+ },
+ {
+ text: 'Move',
+ tooltip: 'Move tooltip',
+ children: [{ text: 'To folder' }],
+ },
+ ];
- it('should set tooltip target on menu button keyboard focus', () => {
- tabKeyDown(document.body);
- focusin(buttons[0]);
- expect(tooltip.target).to.be.equal(buttons[0]);
- });
+ await nextRender();
+ buttons = menuBar._buttons;
- it('should set tooltip context on menu button keyboard focus', () => {
- tabKeyDown(document.body);
- focusin(buttons[0]);
- expect(tooltip.context).to.be.instanceOf(Object);
- expect(tooltip.context.item.text).to.equal('Edit');
- });
+ tooltip = menuBar.querySelector('vaadin-tooltip');
+ });
- it('should hide tooltip on menu-bar focusout', () => {
- tabKeyDown(document.body);
- focusin(buttons[0]);
- focusout(menuBar);
- expect(tooltip.opened).to.be.false;
- });
+ it('should set manual on the tooltip to true', () => {
+ expect(tooltip.manual).to.be.true;
+ });
- it('should hide tooltip on menuBar menu button content Esc', () => {
- tabKeyDown(document.body);
- focusin(buttons[0]);
- escKeyDown(buttons[0]);
- expect(tooltip.opened).to.be.false;
- });
+ it('should show tooltip on menu button mouseover', () => {
+ mouseover(buttons[0]);
+ expect(tooltip.opened).to.be.true;
+ });
- it('should set tooltip opened to false when the menuBar is removed', () => {
- mouseover(buttons[0]);
+ it('should use the tooltip property of an item as tooltip', () => {
+ mouseover(buttons[0]);
+ expect(getTooltipText()).to.equal('Edit tooltip');
+ });
- menuBar.remove();
+ it('should not show tooltip for an item which has no tooltip', () => {
+ mouseover(buttons[1]);
+ expect(getTooltipText()).not.to.be.ok;
+ });
- expect(tooltip.opened).to.be.false;
- });
+ it('should not show tooltip on another parent menu button mouseover when open', async () => {
+ mouseover(buttons[1]);
+ buttons[1].click();
+ await nextRender();
+ mouseover(buttons[2]);
+ await nextRender();
+ expect(tooltip.opened).to.be.false;
+ });
- it('should not set tooltip properties if there is no tooltip', async () => {
- const spyTarget = sinon.spy(menuBar._tooltipController, 'setTarget');
- const spyContent = sinon.spy(menuBar._tooltipController, 'setContext');
- const spyOpened = sinon.spy(menuBar._tooltipController, 'setOpened');
+ it('should hide tooltip on menu bar mouseleave', () => {
+ mouseover(buttons[0]);
+ mouseleave(menuBar);
+ expect(tooltip.opened).to.be.false;
+ });
- tooltip.remove();
- await nextRender();
+ it('should hide tooltip on menu bar container mouseover', () => {
+ mouseover(buttons[0]);
+ mouseover(menuBar._container);
+ expect(tooltip.opened).to.be.false;
+ });
- mouseover(buttons[0]);
+ it('should show tooltip again on menu bar button mouseover', () => {
+ mouseover(buttons[0]);
+ mouseover(menuBar._container);
+ mouseover(buttons[1]);
+ expect(tooltip.opened).to.be.true;
+ });
- expect(spyTarget.called).to.be.false;
- expect(spyContent.called).to.be.false;
- expect(spyOpened.called).to.be.false;
- });
+ it('should set tooltip target on menu button mouseover', () => {
+ mouseover(buttons[0]);
+ expect(tooltip.target).to.be.equal(buttons[0]);
+ });
- it('should not override a custom generator', () => {
- tooltip.generator = () => 'Custom tooltip';
- mouseover(buttons[0]);
- expect(getTooltipText()).to.equal('Custom tooltip');
- });
+ it('should set tooltip context on menu button mouseover', () => {
+ mouseover(buttons[0]);
+ expect(tooltip.context).to.be.instanceOf(Object);
+ expect(tooltip.context.item.text).to.equal('Edit');
+ });
- describe('overflow button', () => {
- beforeEach(async () => {
- // Reduce the width by the width of the last visible button
- menuBar.style.display = 'inline-block';
- menuBar.style.width = `${menuBar.offsetWidth - buttons[2].offsetWidth}px`;
- await nextRender();
- await nextRender();
- // Increase the width by the width of the overflow button
- menuBar.style.width = `${menuBar.offsetWidth + menuBar._overflow.offsetWidth}px`;
- await nextRender();
- await nextRender();
+ it('should change tooltip context on another menu button mouseover', () => {
+ mouseover(buttons[0]);
+ mouseover(buttons[1]);
+ expect(tooltip.context.item.text).to.equal('Share');
});
- it('should not show tooltip on overflow button mouseover', () => {
- mouseover(buttons[buttons.length - 1]);
+ it('should hide tooltip on menu button mousedown', () => {
+ mouseover(buttons[0]);
+ mousedown(buttons[0]);
expect(tooltip.opened).to.be.false;
});
- it('should close tooltip on overflow button mouseover', () => {
+ it('should hide tooltip on mouseleave from overlay to outside', () => {
+ const overlay = tooltip._overlayElement;
mouseover(buttons[0]);
- mouseover(buttons[buttons.length - 1]);
- expect(tooltip.opened).to.be.false;
+ mouseenter(overlay);
+ mouseleave(overlay);
+ expect(overlay.opened).to.be.false;
});
- it('should close tooltip on overflow button keyboard navigation', () => {
+ it('should not show tooltip on focus without keyboard interaction', async () => {
buttons[0].focus();
- arrowRight(buttons[0]);
- expect(tooltip.opened).to.be.true;
- arrowRight(buttons[1]);
+ await nextRender();
expect(tooltip.opened).to.be.false;
});
- it('should not show tooltip on overflow button keyboard focus', () => {
+ it('should show tooltip on menu button keyboard focus', () => {
+ tabKeyDown(document.body);
+ focusin(buttons[0]);
+ expect(tooltip.opened).to.be.true;
+ });
+
+ it('should not show tooltip on another parent menu button focus when open', async () => {
buttons[0].focus();
arrowRight(buttons[0]);
+ arrowDown(buttons[1]);
arrowRight(buttons[1]);
+ await nextRender();
+ expect(tooltip.opened).to.be.false;
+ });
+
+ it('should set tooltip target on menu button keyboard focus', () => {
+ tabKeyDown(document.body);
+ focusin(buttons[0]);
+ expect(tooltip.target).to.be.equal(buttons[0]);
+ });
+
+ it('should set tooltip context on menu button keyboard focus', () => {
tabKeyDown(document.body);
- focusin(buttons[buttons.length - 1]);
+ focusin(buttons[0]);
+ expect(tooltip.context).to.be.instanceOf(Object);
+ expect(tooltip.context.item.text).to.equal('Edit');
+ });
+
+ it('should hide tooltip on menu-bar focusout', () => {
+ tabKeyDown(document.body);
+ focusin(buttons[0]);
+ focusout(menuBar);
expect(tooltip.opened).to.be.false;
});
- });
- describe('open on hover', () => {
- beforeEach(() => {
- menuBar.openOnHover = true;
+ it('should hide tooltip on menuBar menu button content Esc', () => {
+ tabKeyDown(document.body);
+ focusin(buttons[0]);
+ escKeyDown(buttons[0]);
+ expect(tooltip.opened).to.be.false;
});
- it('should show tooltip on mouseover for button without children', () => {
- menuBar.openOnHover = true;
+ it('should set tooltip opened to false when the menuBar is removed', () => {
mouseover(buttons[0]);
- expect(tooltip.opened).to.be.true;
- });
- it('should not show tooltip on mouseover for button with children', () => {
- menuBar.openOnHover = true;
- mouseover(buttons[1]);
+ menuBar.remove();
+
expect(tooltip.opened).to.be.false;
});
- });
- describe('delay', () => {
- const DEFAULT_DELAY = 500;
+ it('should not set tooltip properties if there is no tooltip', async () => {
+ const spyTarget = sinon.spy(menuBar._tooltipController, 'setTarget');
+ const spyContent = sinon.spy(menuBar._tooltipController, 'setContext');
+ const spyOpened = sinon.spy(menuBar._tooltipController, 'setOpened');
+
+ tooltip.remove();
+ await nextRender();
+
+ mouseover(buttons[0]);
+
+ expect(spyTarget.called).to.be.false;
+ expect(spyContent.called).to.be.false;
+ expect(spyOpened.called).to.be.false;
+ });
+
+ it('should not override a custom generator', () => {
+ tooltip.generator = () => 'Custom tooltip';
+ mouseover(buttons[0]);
+ expect(getTooltipText()).to.equal('Custom tooltip');
+ });
+
+ describe('overflow button', () => {
+ beforeEach(async () => {
+ // Reduce the width by the width of the last visible button
+ menuBar.style.display = 'inline-block';
+ menuBar.style.width = `${menuBar.offsetWidth - buttons[2].offsetWidth}px`;
+ await nextRender();
+ await nextRender();
+ // Increase the width by the width of the overflow button
+ menuBar.style.width = `${menuBar.offsetWidth + menuBar._overflow.offsetWidth}px`;
+ await nextRender();
+ await nextRender();
+ });
+
+ it('should not show tooltip on overflow button mouseover', () => {
+ mouseover(buttons[buttons.length - 1]);
+ expect(tooltip.opened).to.be.false;
+ });
- let clock;
+ it('should close tooltip on overflow button mouseover', () => {
+ mouseover(buttons[0]);
+ mouseover(buttons[buttons.length - 1]);
+ expect(tooltip.opened).to.be.false;
+ });
- beforeEach(() => {
- clock = sinon.useFakeTimers({
- shouldClearNativeTimers: true,
+ it('should close tooltip on overflow button keyboard navigation', () => {
+ buttons[0].focus();
+ arrowRight(buttons[0]);
+ expect(tooltip.opened).to.be.true;
+ arrowRight(buttons[1]);
+ expect(tooltip.opened).to.be.false;
+ });
+
+ it('should not show tooltip on overflow button keyboard focus', () => {
+ buttons[0].focus();
+ arrowRight(buttons[0]);
+ arrowRight(buttons[1]);
+ tabKeyDown(document.body);
+ focusin(buttons[buttons.length - 1]);
+ expect(tooltip.opened).to.be.false;
});
});
- afterEach(() => {
- // Wait for cooldown
- clock.tick(DEFAULT_DELAY);
- clock.restore();
+ describe('open on hover', () => {
+ beforeEach(() => {
+ menuBar.openOnHover = true;
+ });
+
+ it('should show tooltip on mouseover for button without children', () => {
+ menuBar.openOnHover = true;
+ mouseover(buttons[0]);
+ expect(tooltip.opened).to.be.true;
+ });
+
+ it('should not show tooltip on mouseover for button with children', () => {
+ menuBar.openOnHover = true;
+ mouseover(buttons[1]);
+ expect(tooltip.opened).to.be.false;
+ });
});
- it('should use hover delay on menu button mouseover', () => {
- tooltip.hoverDelay = 100;
+ describe('delay', () => {
+ const DEFAULT_DELAY = 500;
- mouseover(buttons[0]);
- expect(tooltip.opened).to.be.false;
+ let clock;
- clock.tick(100);
- expect(tooltip.opened).to.be.true;
+ beforeEach(() => {
+ clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ });
+ });
+
+ afterEach(() => {
+ // Wait for cooldown
+ clock.tick(DEFAULT_DELAY);
+ clock.restore();
+ });
+
+ it('should use hover delay on menu button mouseover', () => {
+ tooltip.hoverDelay = 100;
+
+ mouseover(buttons[0]);
+ expect(tooltip.opened).to.be.false;
+
+ clock.tick(100);
+ expect(tooltip.opened).to.be.true;
+ });
+
+ it('should use hide delay on menu button mouseleave', () => {
+ tooltip.hideDelay = 100;
+
+ mouseover(buttons[0]);
+ clock.tick(DEFAULT_DELAY);
+
+ mouseleave(menuBar);
+ expect(tooltip.opened).to.be.true;
+
+ clock.tick(100);
+ expect(tooltip.opened).to.be.false;
+ });
+
+ it('should not use hide delay on menu button mousedown', () => {
+ tooltip.hideDelay = 100;
+
+ mouseover(buttons[0]);
+ clock.tick(DEFAULT_DELAY);
+
+ mousedown(buttons[0]);
+ expect(tooltip.opened).to.be.false;
+ });
+
+ it('should use focus delay on menu button keyboard focus', () => {
+ tooltip.focusDelay = 100;
+
+ tabKeyDown(document.body);
+ focusin(buttons[0]);
+ expect(tooltip.opened).to.be.false;
+
+ clock.tick(100);
+ expect(tooltip.opened).to.be.true;
+ });
+
+ it('should not use hide delay on menu button Esc', () => {
+ tooltip.hideDelay = 100;
+
+ tabKeyDown(document.body);
+ focusin(buttons[0]);
+ clock.tick(DEFAULT_DELAY);
+
+ escKeyDown(buttons[0]);
+ expect(tooltip.opened).to.be.false;
+ });
});
+ });
- it('should use hide delay on menu button mouseleave', () => {
- tooltip.hideDelay = 100;
+ describe('accessible disabled button', () => {
+ before(() => {
+ window.Vaadin.featureFlags ??= {};
+ window.Vaadin.featureFlags.accessibleDisabledButtons = true;
+ });
- mouseover(buttons[0]);
- clock.tick(DEFAULT_DELAY);
+ after(() => {
+ window.Vaadin.featureFlags.accessibleDisabledButtons = false;
+ });
- mouseleave(menuBar);
- expect(tooltip.opened).to.be.true;
+ beforeEach(async () => {
+ menuBar = fixtureSync(
+ `
+
+
+
+
+
`,
+ ).firstElementChild;
+
+ menuBar.items = [
+ { text: 'Edit' },
+ {
+ text: 'Copy',
+ tooltip: 'Copy tooltip',
+ disabled: true,
+ },
+ ];
- clock.tick(100);
- expect(tooltip.opened).to.be.false;
+ await nextRender();
+ buttons = menuBar._buttons;
+
+ tooltip = menuBar.querySelector('vaadin-tooltip');
});
- it('should not use hide delay on menu button mousedown', () => {
- tooltip.hideDelay = 100;
+ afterEach(async () => {
+ await resetMouse();
+ });
- mouseover(buttons[0]);
- clock.tick(DEFAULT_DELAY);
+ it('should toggle tooltip on disabled button hover', async () => {
+ const { x, y } = middleOfNode(buttons[1]);
+ await sendMouse({ type: 'move', position: [Math.floor(x), Math.floor(y)] });
+ expect(getTooltipText()).to.equal('Copy tooltip');
- mousedown(buttons[0]);
- expect(tooltip.opened).to.be.false;
+ await sendMouse({ type: 'move', position: [0, 0] });
+ expect(getTooltipText()).to.be.null;
});
- it('should use focus delay on menu button keyboard focus', () => {
- tooltip.focusDelay = 100;
+ it('should toggle tooltip on disabled button focus when navigating with arrow keys', async () => {
+ await sendKeys({ press: 'Tab' });
+ expect(getTooltipText()).to.be.null;
- tabKeyDown(document.body);
- focusin(buttons[0]);
- expect(tooltip.opened).to.be.false;
+ await sendKeys({ press: 'ArrowRight' });
+ expect(getTooltipText()).to.equal('Copy tooltip');
- clock.tick(100);
- expect(tooltip.opened).to.be.true;
+ await sendKeys({ press: 'ArrowLeft' });
+ expect(getTooltipText()).to.be.null;
});
- it('should not use hide delay on menu button Esc', () => {
- tooltip.hideDelay = 100;
+ it('should toggle tooltip on disabled button focus when navigating with Tab', async () => {
+ menuBar.tabNavigation = true;
- tabKeyDown(document.body);
- focusin(buttons[0]);
- clock.tick(DEFAULT_DELAY);
+ await sendKeys({ press: 'Tab' });
+ expect(getTooltipText()).to.be.null;
- escKeyDown(buttons[0]);
- expect(tooltip.opened).to.be.false;
+ await sendKeys({ press: 'Tab' });
+ expect(getTooltipText()).to.equal('Copy tooltip');
+
+ await sendKeys({ down: 'Shift' });
+ await sendKeys({ press: 'Tab' });
+ await sendKeys({ up: 'Shift' });
+ expect(getTooltipText()).to.be.null;
});
});
});