/** * `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 (not the focus a sheet or dialog moves to it as it * opens, layers.js), 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). 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. */ import { hoverPopover, LEAVE_DELAY_MS } from './tooltip.js' import { reopenGuard } from './util.js' document.addEventListener('alpine:init', () => { window.Alpine.data('materialRichTooltip', (persistent = false) => ({ stop: 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) { // A persistent bubble is a `popover="auto"`, and its trigger is outside it: the // press on the trigger light-dismisses the open bubble, and the click that // follows would open it again, so a second press never closed it. const guard = reopenGuard() bubble.addEventListener('beforetoggle', (event) => guard.beforeToggle(event)) wrapper.addEventListener('click', (event) => { if (bubble.contains(event.target)) { return } if (open()) { guard.closing() bubble.hidePopover() } else if (guard.canOpen()) { bubble.showPopover() } }) return } const popover = hoverPopover(wrapper, { open, show: () => bubble.showPopover(), hide: () => bubble.hidePopover(), focusable: (event) => event.target.matches(':focus-visible'), }) wrapper.addEventListener('focusout', (event) => !wrapper.contains(event.relatedTarget) && popover.hide(LEAVE_DELAY_MS), { signal: popover.signal }) this.stop = popover.destroy }, destroy() { this.stop?.() }, })) })