package.src.vaadin-menu-bar-interactions-mixin.js Maven / Gradle / Ivy
/**
* @license
* Copyright (c) 2019 - 2022 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { FocusMixin } from '@vaadin/component-base/src/focus-mixin.js';
import { isKeyboardActive } from '@vaadin/component-base/src/focus-utils.js';
import { KeyboardDirectionMixin } from '@vaadin/component-base/src/keyboard-direction-mixin.js';
/**
* @polymerMixin
* @mixes FocusMixin
* @mixes KeyboardDirectionMixinClass
*/
export const InteractionsMixin = (superClass) =>
class InteractionsMixin extends KeyboardDirectionMixin(FocusMixin(superClass)) {
static get properties() {
return {
/**
* If true, the submenu will open on hover (mouseover) instead of click.
* @attr {boolean} open-on-hover
*/
openOnHover: {
type: Boolean,
},
};
}
constructor() {
super();
this.__boundOnContextMenuKeydown = this.__onContextMenuKeydown.bind(this);
}
static get observers() {
return ['_itemsChanged(items, items.splices)'];
}
/** @protected */
ready() {
super.ready();
this.addEventListener('mousedown', () => this._hideTooltip());
this.addEventListener('mouseleave', () => this._hideTooltip());
this._subMenu.addEventListener('item-selected', this.__onItemSelected.bind(this));
this._subMenu.addEventListener('close-all-menus', this.__onEscapeClose.bind(this));
const overlay = this._subMenu.$.overlay;
overlay.addEventListener('keydown', this.__boundOnContextMenuKeydown);
const container = this._container;
container.addEventListener('click', this.__onButtonClick.bind(this));
container.addEventListener('mouseover', (e) => this._onMouseOver(e));
}
/**
* Override getter from `KeyboardDirectionMixin`
* to look for activeElement in shadow root, or
* use the expanded button as a fallback.
*
* @return {Element | null}
* @protected
* @override
*/
get focused() {
return this.shadowRoot.activeElement || this._expandedButton;
}
/**
* Override getter from `KeyboardDirectionMixin`.
*
* @return {boolean}
* @protected
* @override
*/
get _vertical() {
return false;
}
/**
* Override method inherited from `KeyboardDirectionMixin`
* to use the list of menu-bar buttons as items.
*
* @return {Element[]}
* @protected
* @override
*/
_getItems() {
return this._buttons;
}
/** @private */
get __isRTL() {
return this.getAttribute('dir') === 'rtl';
}
/** @protected */
disconnectedCallback() {
super.disconnectedCallback();
this._hideTooltip(true);
}
/**
* @param {HTMLElement} button
* @protected
*/
_showTooltip(button, isHover) {
// Check if there is a slotted vaadin-tooltip element.
const tooltip = this._tooltipController.node;
if (tooltip && tooltip.isConnected) {
// If the tooltip element doesn't have a generator assigned, use a default one
// that reads the `tooltip` property of an item.
if (tooltip.generator === undefined) {
tooltip.generator = ({ item }) => item && item.tooltip;
}
if (!this._subMenu.opened) {
this._tooltipController.setTarget(button);
this._tooltipController.setContext({ item: button.item });
// Trigger opening using the corresponding delay.
tooltip._stateController.open({
hover: isHover,
focus: !isHover,
});
}
}
}
/** @protected */
_hideTooltip(immediate) {
const tooltip = this._tooltipController.node;
if (tooltip) {
tooltip._stateController.close(immediate);
}
}
/** @protected */
_setExpanded(button, expanded) {
button.toggleAttribute('expanded', expanded);
button.toggleAttribute('active', expanded);
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}
/** @protected */
_setTabindex(button, focused) {
button.setAttribute('tabindex', focused ? '0' : '-1');
}
/**
* Override method inherited from `KeyboardDirectionMixin`
* to close the submenu for the previously focused button
* and open another one for the newly focused button.
*
* @param {Element} item
* @param {boolean} navigating
* @protected
* @override
*/
_focusItem(item, navigating) {
const wasExpanded = navigating && this.focused === this._expandedButton;
if (wasExpanded) {
this._close();
}
super._focusItem(item, navigating);
this._buttons.forEach((btn) => {
this._setTabindex(btn, btn === item);
});
if (wasExpanded && item.item && item.item.children) {
this.__openSubMenu(item, true, { keepFocus: true });
} else if (item === this._overflow) {
this._hideTooltip();
} else {
this._showTooltip(item);
}
}
/** @private */
_getButtonFromEvent(e) {
return Array.from(e.composedPath()).find((el) => el.localName === 'vaadin-menu-bar-button');
}
/**
* Override method inherited from `FocusMixin`
*
* @param {boolean} focused
* @override
* @protected
*/
_setFocused(focused) {
if (focused) {
const target = this.shadowRoot.querySelector('[part$="button"][tabindex="0"]');
if (target) {
this._buttons.forEach((btn) => {
this._setTabindex(btn, btn === target);
if (btn === target && btn !== this._overflow && isKeyboardActive()) {
this._showTooltip(btn);
}
});
}
} else {
this._hideTooltip();
}
}
/**
* @param {!KeyboardEvent} event
* @private
*/
_onArrowDown(event) {
// Prevent page scroll.
event.preventDefault();
const button = this._getButtonFromEvent(event);
if (button === this._expandedButton) {
// Menu opened previously, focus first item
this._focusFirstItem();
} else {
this.__openSubMenu(button, true);
}
}
/**
* @param {!KeyboardEvent} event
* @private
*/
_onArrowUp(event) {
// Prevent page scroll.
event.preventDefault();
const button = this._getButtonFromEvent(event);
if (button === this._expandedButton) {
// Menu opened previously, focus last item
this._focusLastItem();
} else {
this.__openSubMenu(button, true, { focusLast: true });
}
}
/**
* Override an event listener from `KeyboardMixin`:
* - to close the sub-menu for expanded button,
* - to close a tooltip for collapsed button.
*
* @param {!KeyboardEvent} event
* @protected
* @override
*/
_onEscape(event) {
if (event.composedPath().includes(this._expandedButton)) {
this._close(true);
}
this._hideTooltip(true);
}
/**
* Override an event listener from `KeyboardMixin`.
*
* @param {!KeyboardEvent} event
* @protected
* @override
*/
_onKeyDown(event) {
switch (event.key) {
case 'ArrowDown':
this._onArrowDown(event);
break;
case 'ArrowUp':
this._onArrowUp(event);
break;
default:
super._onKeyDown(event);
break;
}
}
/** @private */
get _subMenu() {
return this.shadowRoot.querySelector('vaadin-menu-bar-submenu');
}
/** @private */
_itemsChanged() {
const subMenu = this._subMenu;
if (subMenu && subMenu.opened) {
subMenu.close();
}
}
/**
* @param {!MouseEvent} e
* @protected
*/
_onMouseOver(e) {
const button = this._getButtonFromEvent(e);
if (!button) {
// Hide tooltip on mouseover to disabled button
this._hideTooltip();
} else if (button !== this._expandedButton) {
const isOpened = this._subMenu.opened;
if (button.item.children && (this.openOnHover || isOpened)) {
this.__openSubMenu(button, false);
} else if (isOpened) {
this._close();
}
if (button === this._overflow || (this.openOnHover && button.item.children)) {
this._hideTooltip();
} else {
this._showTooltip(button, true);
}
}
}
/** @private */
__onContextMenuKeydown(e) {
const item = Array.from(e.composedPath()).find((el) => el._item);
if (item) {
const list = item.parentNode;
if (e.keyCode === 38 && item === list.items[0]) {
this._close(true);
}
// ArrowLeft, or ArrowRight on non-parent submenu item
if (e.keyCode === 37 || (e.keyCode === 39 && !item._item.children)) {
// Prevent ArrowLeft from being handled in context-menu
e.stopImmediatePropagation();
this._onKeyDown(e);
}
}
}
/** @private */
__fireItemSelected(value) {
this.dispatchEvent(new CustomEvent('item-selected', { detail: { value } }));
}
/** @private */
__onButtonClick(e) {
e.stopPropagation();
const button = this._getButtonFromEvent(e);
if (button) {
this.__openSubMenu(button, false);
}
}
/** @private */
__openSubMenu(button, keydown, options = {}) {
const subMenu = this._subMenu;
const item = button.item;
if (subMenu.opened) {
this._close();
if (subMenu.listenOn === button) {
return;
}
}
const items = item && item.children;
if (!items || items.length === 0) {
this.__fireItemSelected(item);
return;
}
subMenu.items = items;
subMenu.listenOn = button;
const overlay = subMenu.$.overlay;
overlay.positionTarget = button;
overlay.noVerticalOverlap = true;
this._expandedButton = button;
requestAnimationFrame(() => {
button.dispatchEvent(
new CustomEvent('opensubmenu', {
detail: {
children: items,
},
}),
);
this._hideTooltip(true);
this._setExpanded(button, true);
});
this.style.pointerEvents = 'auto';
overlay.addEventListener(
'vaadin-overlay-open',
() => {
if (options.focusLast) {
this._focusLastItem();
}
if (options.keepFocus) {
this._focusItem(this._expandedButton, false);
}
// Do not focus item when open not from keyboard
if (!keydown) {
overlay.$.overlay.focus();
}
overlay._updatePosition();
},
{ once: true },
);
}
/** @private */
_focusFirstItem() {
const list = this._subMenu.$.overlay.firstElementChild;
list.focus();
}
/** @private */
_focusLastItem() {
const list = this._subMenu.$.overlay.firstElementChild;
const item = list.items[list.items.length - 1];
if (item) {
item.focus();
}
}
/** @private */
__onItemSelected(e) {
e.stopPropagation();
this._close();
this.__fireItemSelected(e.detail.value);
}
/** @private */
__onEscapeClose() {
this.__deactivateButton(true);
}
/** @private */
__deactivateButton(restoreFocus) {
const button = this._expandedButton;
if (button && button.hasAttribute('expanded')) {
this._setExpanded(button, false);
if (restoreFocus) {
this._focusItem(button, false);
}
this._expandedButton = null;
}
}
/**
* @param {boolean} restoreFocus
* @protected
*/
_close(restoreFocus) {
this.style.pointerEvents = '';
this.__deactivateButton(restoreFocus);
if (this._subMenu.opened) {
this._subMenu.close();
}
}
};
© 2015 - 2025 Weber Informatics LLC | Privacy Policy