/** * `materialTooltip`: shows an `` popover for the control it belongs to. * * The trigger is the popover's parent element (the button, or the standalone wrapper). Hover * counts only for a pointer that can hover, after a short delay so a pointer passing over a * toolbar does not flash every label; keyboard focus shows it at once — but not the focus a sheet, * a dialog or the modal rail moves to its first control as it opens (layers.js), which would put * a tooltip over a sheet's close button the moment the sheet appears. A press and Escape hide it * at once; leaving and blur let it stand for M3's 1.5s, so the words can still be read while the * pointer moves on. Only one tooltip is up at a time: showing one takes the last one down, which * the browser will not do for us because these are `popover="manual"`. */ import { openingFocus } from './layers.js' const HOVER_DELAY_MS = 500 export const LEAVE_DELAY_MS = 1500 let visible = null /** * The hover/focus/Escape timing a tooltip and a transient rich tooltip (rich-tooltip.js) share: * shows after HOVER_DELAY_MS on a pointer that can hover, at once on a focus `focusable` accepts * (never the focus layers.js moves as a sheet or dialog opens), hides after LEAVE_DELAY_MS once * the pointer leaves, and at once on Escape. `open`, `show` and `hide` do the actual popover call; * a caller adds its own extra listeners (a press, a blur) on the returned `signal`, and calls * `hide` from them. */ export function hoverPopover(el, { open, show, hide, focusable }) { const controller = new AbortController() const { signal } = controller let timer = null const delayed = (action, delay) => { clearTimeout(timer) timer = setTimeout(action, delay) } const showLater = (delay) => delayed(() => !open() && show(), delay) const hideLater = (delay = 0) => delayed(() => open() && hide(), delay) el.addEventListener('pointerenter', (event) => event.pointerType === 'mouse' && showLater(HOVER_DELAY_MS), { signal }) el.addEventListener('pointerleave', () => hideLater(LEAVE_DELAY_MS), { signal }) el.addEventListener('focusin', (event) => !openingFocus(event) && focusable(event) && showLater(0), { signal }) document.addEventListener('keydown', (event) => event.key === 'Escape' && hideLater(), { signal }) return { hide: hideLater, signal, destroy() { clearTimeout(timer) controller.abort() }, } } document.addEventListener('alpine:init', () => { window.Alpine.data('materialTooltip', () => ({ popover: null, init() { const tip = this.$el const trigger = tip.parentElement const popover = (this.popover = hoverPopover(trigger, { open: () => tip.matches(':popover-open'), show: () => { if (!tip.isConnected) { return } if (visible && visible !== tip && visible.matches(':popover-open')) { visible.hidePopover() } visible = tip tip.showPopover() }, hide: () => { tip.hidePopover() if (visible === tip) { visible = null } }, focusable: () => trigger.matches(':focus-within:has(:focus-visible), :focus-visible'), })) trigger.addEventListener('pointerdown', () => popover.hide(), { signal: popover.signal }) trigger.addEventListener('focusout', () => popover.hide(LEAVE_DELAY_MS), { signal: popover.signal }) }, destroy() { this.popover.destroy() if (visible === this.$el) { visible = null } }, })) })