/** * `materialRichTooltip`: shows an ``. * * Transient (the default): like a plain tooltip (tooltip.js) โ€” after a short hover on a pointer * that can hover, at once on keyboard focus, and standing for 1.5s once the pointer or the focus * leaves, which M3 gives plain and rich tooltips alike (docs/reference/m3/ * components-actions-communication-containment.md ยง Tooltips, ACT-25). The bubble is inside the * wrapper, so moving onto it to reach its actions, by pointer or by Tab, is no leave at all; Escape * hides it at once. Persistent: a press on the trigger opens it as a light-dismiss popover. * * The trigger is pointed at the bubble with `aria-describedby`, so the explanation โ€” the whole * point of a rich tooltip โ€” is read out with the control rather than never at all; a persistent * one also says `aria-haspopup="dialog"` and whether it is open. The server cannot write any of * it: the trigger comes from a slot, and only script knows which element in it takes the focus. * A Livewire morph strips attributes the server did not render and gives the bubble a new id, so * they are written again whenever the trigger is reached. */ const HOVER_DELAY_MS = 500 const LEAVE_GRACE_MS = 1500 document.addEventListener('alpine:init', () => { window.Alpine.data('materialRichTooltip', (persistent = false) => ({ timer: null, init() { const bubble = this.$refs.bubble const wrapper = this.$el const open = () => bubble.matches(':popover-open') const control = () => [...wrapper.querySelectorAll('button, a[href], [tabindex]')].find((element) => !bubble.contains(element)) const describe = () => { const trigger = control() if (!trigger) { return } trigger.setAttribute('aria-describedby', bubble.id) if (persistent) { trigger.setAttribute('aria-haspopup', 'dialog') trigger.setAttribute('aria-expanded', String(open())) } } describe() bubble.addEventListener('toggle', describe) wrapper.addEventListener('focusin', describe) wrapper.addEventListener('pointerenter', describe) if (persistent) { wrapper.addEventListener('click', (event) => { if (bubble.contains(event.target)) { return } open() ? bubble.hidePopover() : bubble.showPopover() }) return } const show = (delay) => { clearTimeout(this.timer) this.timer = setTimeout(() => !open() && bubble.showPopover(), delay) } const hide = (delay = 0) => { clearTimeout(this.timer) this.timer = setTimeout(() => open() && bubble.hidePopover(), delay) } wrapper.addEventListener('pointerenter', (event) => event.pointerType === 'mouse' && show(HOVER_DELAY_MS)) wrapper.addEventListener('pointerleave', () => hide(LEAVE_GRACE_MS)) wrapper.addEventListener('focusin', (event) => event.target.matches(':focus-visible') && show(0)) wrapper.addEventListener('focusout', (event) => !wrapper.contains(event.relatedTarget) && hide(LEAVE_GRACE_MS)) document.addEventListener('keydown', (event) => event.key === 'Escape' && hide()) }, destroy() { clearTimeout(this.timer) }, })) })