Editable filter chips, uniformly-sized value controls, working field-picker search, and native date-range inputs with relative presets in place of the vendored calendar — native inputs give year-jump, manual entry and keyboard accessibility for free.
76 lines
3 KiB
JavaScript
76 lines
3 KiB
JavaScript
// HoverPopover hook: opens a native Popover-API element on hover/keyboard focus
|
|
// of its trigger, so the popover renders in the browser TOP LAYER and escapes
|
|
// any ancestor `overflow` clipping (the members table forces overflow-y:auto,
|
|
// which clips an absolutely-positioned daisyUI tooltip/dropdown-content). Unlike
|
|
// the older SortTooltip, placement is handed to CSS anchor positioning
|
|
// (`anchor-name` on the trigger, `position-anchor`/`position-area` on the
|
|
// popover) — this hook does no coordinate math, it only toggles visibility.
|
|
//
|
|
// WCAG 2.2 AA / SC 1.4.13 (Content on Hover or Focus): the popover is shown on
|
|
// both hover and focus, is dismissible via Escape, and is hoverable/persistent —
|
|
// a short close delay plus the popover's own hover keep it open while the pointer
|
|
// travels from the trigger onto the popover content.
|
|
//
|
|
// Expected DOM:
|
|
// <a phx-hook="HoverPopover" data-popover-target="tip-id" style="anchor-name:--x">…</a>
|
|
// <div id="tip-id" popover style="position-anchor:--x">…</div>
|
|
const HoverPopover = {
|
|
mounted() {
|
|
this.tip = document.getElementById(this.el.dataset.popoverTarget)
|
|
// Feature-detect: without Popover API support fall back to no visual popover
|
|
// (the trigger's aria-label still conveys the summary to assistive tech).
|
|
if (!this.tip || typeof this.tip.showPopover !== "function") return
|
|
|
|
this.open = () => {
|
|
try {
|
|
if (!this.tip.matches(":popover-open")) this.tip.showPopover()
|
|
} catch (_e) {}
|
|
}
|
|
this.close = () => {
|
|
try {
|
|
if (this.tip.matches(":popover-open")) this.tip.hidePopover()
|
|
} catch (_e) {}
|
|
}
|
|
this.cancelClose = () => {
|
|
if (this.closeTimer) {
|
|
clearTimeout(this.closeTimer)
|
|
this.closeTimer = null
|
|
}
|
|
}
|
|
this.show = () => {
|
|
this.cancelClose()
|
|
this.open()
|
|
}
|
|
this.scheduleClose = () => {
|
|
this.cancelClose()
|
|
this.closeTimer = setTimeout(() => this.close(), 120)
|
|
}
|
|
this.onKey = (e) => {
|
|
if (e.key === "Escape") this.close()
|
|
}
|
|
|
|
this.el.addEventListener("mouseenter", this.show)
|
|
this.el.addEventListener("focus", this.show)
|
|
this.el.addEventListener("mouseleave", this.scheduleClose)
|
|
this.el.addEventListener("blur", this.scheduleClose)
|
|
this.el.addEventListener("keydown", this.onKey)
|
|
// Keep it open while the pointer is over the popover itself (hoverable).
|
|
this.tip.addEventListener("mouseenter", this.show)
|
|
this.tip.addEventListener("mouseleave", this.scheduleClose)
|
|
},
|
|
|
|
destroyed() {
|
|
if (!this.tip) return
|
|
this.cancelClose()
|
|
this.close()
|
|
this.el.removeEventListener("mouseenter", this.show)
|
|
this.el.removeEventListener("focus", this.show)
|
|
this.el.removeEventListener("mouseleave", this.scheduleClose)
|
|
this.el.removeEventListener("blur", this.scheduleClose)
|
|
this.el.removeEventListener("keydown", this.onKey)
|
|
this.tip.removeEventListener("mouseenter", this.show)
|
|
this.tip.removeEventListener("mouseleave", this.scheduleClose)
|
|
}
|
|
}
|
|
|
|
export default HoverPopover
|