feat(member): make overview columns sortable through accessible sort headers

This commit is contained in:
Simon 2026-07-06 10:48:28 +02:00
parent dfc616257d
commit 0c375234c8
9 changed files with 350 additions and 51 deletions

View file

@ -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;
}

View file

@ -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() {