Files
drupal11-ddev/web/core/modules/navigation/js/toolbar-popover.js

154 lines
4.6 KiB
JavaScript
Raw Normal View History

2025-10-08 11:39:17 -04:00
/**
*
* Toolbar popover.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
*/
const POPOVER_OPEN_DELAY = 150;
const POPOVER_CLOSE_DELAY = 400;
const POPOVER_NO_CLICK_DELAY = 500;
((Drupal, once) => {
Drupal.behaviors.navigationProcessPopovers = {
/**
* Attaches the behavior to the context element.
*
* @param {HTMLElement} context The context element to attach the behavior to.
*/
attach: (context) => {
once(
'toolbar-popover',
context.querySelectorAll('[data-toolbar-popover]'),
).forEach((popover) => {
// This is trigger of popover. Currently only first level button.
const button = popover.querySelector('[data-toolbar-popover-control]');
// This is tooltip content. Currently child menus only.
const tooltip = popover.querySelector('[data-toolbar-popover-wrapper]');
if (!button || !tooltip) return;
const expandPopover = () => {
popover.classList.add('toolbar-popover--expanded');
button.dataset.drupalNoClick = 'true';
tooltip.removeAttribute('inert');
setTimeout(() => {
delete button.dataset.drupalNoClick;
}, POPOVER_NO_CLICK_DELAY);
};
const collapsePopover = () => {
popover.classList.remove('toolbar-popover--expanded');
tooltip.setAttribute('inert', true);
delete button.dataset.drupalNoClick;
};
/**
* We need to change state of trigger and popover.
*
* @param {boolean} state The popover state.
*
* @param {boolean} initialLoad Happens on page loads.
*/
const toggleState = (state, initialLoad = false) => {
/* eslint-disable-next-line no-unused-expressions */
state && !initialLoad ? expandPopover() : collapsePopover();
button.setAttribute('aria-expanded', state && !initialLoad);
const text = button.querySelector('[data-toolbar-action]');
if (text) {
text.textContent = state
? Drupal.t('Collapse')
: Drupal.t('Extend');
}
// Dispatch event to sidebar.js
popover.dispatchEvent(
new CustomEvent('toolbar-popover-toggled', {
bubbles: true,
detail: {
state,
},
}),
);
};
const isPopoverHoverOrFocus = () =>
popover.contains(document.activeElement) || popover.matches(':hover');
const delayedClose = () => {
setTimeout(() => {
if (isPopoverHoverOrFocus()) return;
// eslint-disable-next-line no-use-before-define
close();
}, POPOVER_CLOSE_DELAY);
};
const open = () => {
['mouseleave', 'focusout'].forEach((e) => {
button.addEventListener(e, delayedClose, false);
tooltip.addEventListener(e, delayedClose, false);
});
};
const close = () => {
toggleState(false);
['mouseleave', 'focusout'].forEach((e) => {
button.removeEventListener(e, delayedClose);
tooltip.removeEventListener(e, delayedClose);
});
};
button.addEventListener('mouseover', () => {
// This is not needed because no hover on mobile.
// @todo test is after.
if (window.matchMedia('(max-width: 1023px)').matches) {
return;
}
setTimeout(() => {
// If it is accident hover ignore it.
// If in this timeout popover already opened by click.
if (
!button.matches(':hover') ||
!button.getAttribute('aria-expanded') === 'false'
) {
return;
}
toggleState(true);
open();
}, POPOVER_OPEN_DELAY);
});
button.addEventListener('click', (e) => {
const state =
e.currentTarget.getAttribute('aria-expanded') === 'false';
if (!e.currentTarget.dataset.drupalNoClick) {
toggleState(state);
}
});
// Listens events from sidebar.js.
popover.addEventListener('toolbar-popover-close', () => {
close();
});
// TODO: Add toggle with state.
popover.addEventListener('toolbar-popover-open', () => {
toggleState(true);
});
// Listens events from toolbar-menu.js
popover.addEventListener('toolbar-active-url', () => {
toggleState(true, true);
});
});
},
};
})(Drupal, once);