diff --git a/assets/css/app.css b/assets/css/app.css index 37091909..7a828ff3 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -957,3 +957,16 @@ margin-bottom: 0; } +/* + * Sort-header tooltips must render on top of the pinned checkbox column's + * horizontal-scroll fade (::after on .sticky-first-col-th). The hovered/focused + * header th is already lifted above the checkbox th (hover/focus-within:z-50), + * so the whole tooltip wins the stacking; this rule additionally keeps the + * daisyUI tooltip bubble's own pseudo-elements above the fade overlay. Scoped + * to the member overview header so no unrelated tooltip is affected. + */ +#members-keyboard thead .tooltip::before, +#members-keyboard thead .tooltip::after { + z-index: 60; +} + diff --git a/assets/js/app.js b/assets/js/app.js index 85bbc6ba..91172948 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -336,6 +336,72 @@ Hooks.SortableList = { } } +// SortTooltip hook: shows the sort-header hint in the browser TOP LAYER via the +// native Popover API, so it escapes the members table's overflow clipping +// (overflow-x:auto forces overflow-y:auto, which clips a CSS tooltip below the +// header) and paints above the sticky header, neighbor cells, and the pinned +// checkbox column's fade — no z-index fight. The popover is shown on hover AND +// keyboard focus and positioned next to the header with getBoundingClientRect +// (portable; CSS anchor-positioning is not yet supported in Safari/Firefox). +Hooks.SortTooltip = { + mounted() { + this.tip = document.getElementById(this.el.dataset.tooltipId) + // Feature-detect: without Popover API support fall back to no visual tooltip + // (the button's aria-label still conveys the hint to assistive tech). + if (!this.tip || typeof this.tip.showPopover !== "function") return + + this.position = () => { + const btn = this.el.getBoundingClientRect() + const tip = this.tip.getBoundingClientRect() + const gap = 6 + // Prefer just below the header; flip above when there is no room below. + let top = btn.bottom + gap + if (top + tip.height > window.innerHeight && btn.top - gap - tip.height >= 0) { + top = btn.top - gap - tip.height + } + // Left-align to the header, but keep the whole bubble on-screen. + let left = btn.left + const maxLeft = window.innerWidth - tip.width - gap + if (left > maxLeft) left = maxLeft + if (left < gap) left = gap + this.tip.style.top = `${Math.round(top)}px` + this.tip.style.left = `${Math.round(left)}px` + } + + this.show = () => { + try { + if (!this.tip.matches(":popover-open")) this.tip.showPopover() + // showPopover() makes it measurable; positioning in the same tick avoids + // a visible flash (JS runs before the browser paints). + this.position() + } catch (_e) {} + } + + this.hide = () => { + try { + if (this.tip.matches(":popover-open")) this.tip.hidePopover() + } catch (_e) {} + } + + this.onKey = (e) => { if (e.key === "Escape") this.hide() } + + this.el.addEventListener("mouseenter", this.show) + this.el.addEventListener("focus", this.show) + this.el.addEventListener("mouseleave", this.hide) + this.el.addEventListener("blur", this.hide) + this.el.addEventListener("keydown", this.onKey) + }, + + destroyed() { + if (this.hide) this.hide() + this.el.removeEventListener("mouseenter", this.show) + this.el.removeEventListener("focus", this.show) + this.el.removeEventListener("mouseleave", this.hide) + this.el.removeEventListener("blur", this.hide) + this.el.removeEventListener("keydown", this.onKey) + } +} + // SidebarState hook: Manages sidebar expanded/collapsed state Hooks.SidebarState = { mounted() { diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 2b4dcde5..76c44b1c 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -1061,7 +1061,17 @@ defmodule MvWeb.CoreComponents do > {col[:label]} -