feat(member-live): sort the composite Member column by first or last name
This commit is contained in:
parent
ca2aaa069c
commit
f7005f395f
11 changed files with 443 additions and 34 deletions
|
|
@ -926,6 +926,21 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sub-field sort menu (composite Member column). A native Popover-API element in
|
||||
* the browser TOP LAYER so it escapes the members table's overflow; placement is
|
||||
* pure CSS anchor positioning (the "⋮" trigger carries anchor-name, this popover
|
||||
* carries position-anchor). `position-area` drops it just below the trigger,
|
||||
* right-aligned, and `position-try-fallbacks` flips it above when there is no
|
||||
* room below. The SubfieldMenu hook only toggles it — no coordinate math. This
|
||||
* mirrors the payment-badge popover approach (§548) for a consistent look/feel.
|
||||
*/
|
||||
.popover-menu {
|
||||
margin: 0;
|
||||
position-area: bottom span-left;
|
||||
position-try-fallbacks: flip-block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Vertically center the row checkbox in the sticky first column using the
|
||||
* table-native vertical-align: middle. This keeps display: table-cell intact so
|
||||
|
|
|
|||
121
assets/js/app.js
121
assets/js/app.js
|
|
@ -407,6 +407,12 @@ Hooks.PopoverTooltip = {
|
|||
this.el.addEventListener("mouseleave", this.hide)
|
||||
this.el.addEventListener("blur", this.hide)
|
||||
this.el.addEventListener("keydown", this.onKey)
|
||||
// Hide on activation: a click (sort/reset) triggers a LiveView re-render that
|
||||
// re-emits the tooltip element without the JS-set inline top/left, which would
|
||||
// otherwise leave the still-open popover stuck at the default (0,0) position,
|
||||
// covering the header. Closing it here keeps it out of the re-render; the next
|
||||
// hover repositions it cleanly.
|
||||
this.el.addEventListener("click", this.hide)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
|
|
@ -416,6 +422,121 @@ Hooks.PopoverTooltip = {
|
|||
this.el.removeEventListener("mouseleave", this.hide)
|
||||
this.el.removeEventListener("blur", this.hide)
|
||||
this.el.removeEventListener("keydown", this.onKey)
|
||||
this.el.removeEventListener("click", this.hide)
|
||||
}
|
||||
}
|
||||
|
||||
// SubfieldMenu hook: toggles a native Popover menu (top layer, so it escapes the
|
||||
// overview table's overflow clipping) for choosing the composite Member column's
|
||||
// sort sub-field. Attached to the "⋮" trigger; data-menu-id names the <ul
|
||||
// popover> menu. Handles click / keyboard open, roving arrow-key focus,
|
||||
// Escape / click-away close, and closes after a selection (the LiveView
|
||||
// re-renders on sort_composite anyway). Positioned with getBoundingClientRect
|
||||
// (portable; no CSS anchor-positioning needed).
|
||||
Hooks.SubfieldMenu = {
|
||||
mounted() {
|
||||
this.menu = document.getElementById(this.el.dataset.menuId)
|
||||
if (!this.menu || typeof this.menu.showPopover !== "function") return
|
||||
|
||||
// Hover/focus hint on the trigger, shown while the menu is closed. Same
|
||||
// .popover-tooltip look as the sort-header tooltips; placement is CSS anchor
|
||||
// positioning (no coordinate math).
|
||||
this.tip = document.getElementById(this.el.dataset.tooltipId)
|
||||
this.showTip = () => {
|
||||
if (!this.tip || this.menu.matches(":popover-open")) return
|
||||
try {
|
||||
if (!this.tip.matches(":popover-open")) this.tip.showPopover()
|
||||
} catch (_e) {}
|
||||
}
|
||||
this.hideTip = () => {
|
||||
try {
|
||||
if (this.tip && this.tip.matches(":popover-open")) this.tip.hidePopover()
|
||||
} catch (_e) {}
|
||||
}
|
||||
|
||||
this.items = () => [...this.menu.querySelectorAll('[role="menuitemradio"]')]
|
||||
|
||||
// Placement is pure CSS anchor positioning (see `.popover-menu` in app.css);
|
||||
// this hook only toggles visibility, focus and keyboard interaction.
|
||||
this.open = ({focusFirst = true} = {}) => {
|
||||
this.hideTip()
|
||||
if (!this.menu.matches(":popover-open")) this.menu.showPopover()
|
||||
this.el.setAttribute("aria-expanded", "true")
|
||||
if (focusFirst) {
|
||||
const active = this.menu.querySelector('[aria-checked="true"]') || this.items()[0]
|
||||
if (active) active.focus()
|
||||
}
|
||||
}
|
||||
|
||||
this.close = ({focusTrigger = false} = {}) => {
|
||||
if (this.menu.matches(":popover-open")) this.menu.hidePopover()
|
||||
this.el.setAttribute("aria-expanded", "false")
|
||||
if (focusTrigger) this.el.focus()
|
||||
}
|
||||
|
||||
this.onTriggerClick = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.menu.matches(":popover-open") ? this.close() : this.open()
|
||||
}
|
||||
this.onTriggerKey = (e) => {
|
||||
if (["ArrowDown", "Enter", " "].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
this.open()
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
this.open({focusFirst: false})
|
||||
const items = this.items()
|
||||
if (items.length) items[items.length - 1].focus()
|
||||
}
|
||||
}
|
||||
this.onMenuKey = (e) => {
|
||||
const items = this.items()
|
||||
const i = items.indexOf(document.activeElement)
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault()
|
||||
;(items[i + 1] || items[0]).focus()
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
;(items[i - 1] || items[items.length - 1]).focus()
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
this.close({focusTrigger: true})
|
||||
} else if (e.key === "Tab") {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
// A selection fires the LiveView sort; drop the menu.
|
||||
this.onMenuClick = () => this.close()
|
||||
this.onDocPointer = (e) => {
|
||||
if (!this.el.contains(e.target) && !this.menu.contains(e.target)) this.close()
|
||||
}
|
||||
|
||||
this.el.addEventListener("click", this.onTriggerClick)
|
||||
this.el.addEventListener("keydown", this.onTriggerKey)
|
||||
this.el.addEventListener("mouseenter", this.showTip)
|
||||
this.el.addEventListener("focus", this.showTip)
|
||||
this.el.addEventListener("mouseleave", this.hideTip)
|
||||
this.el.addEventListener("blur", this.hideTip)
|
||||
this.menu.addEventListener("keydown", this.onMenuKey)
|
||||
this.menu.addEventListener("click", this.onMenuClick)
|
||||
document.addEventListener("pointerdown", this.onDocPointer, true)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.el.removeEventListener("click", this.onTriggerClick)
|
||||
this.el.removeEventListener("keydown", this.onTriggerKey)
|
||||
this.el.removeEventListener("mouseenter", this.showTip)
|
||||
this.el.removeEventListener("focus", this.showTip)
|
||||
this.el.removeEventListener("mouseleave", this.hideTip)
|
||||
this.el.removeEventListener("blur", this.hideTip)
|
||||
this.hideTip()
|
||||
if (this.menu) {
|
||||
this.menu.removeEventListener("keydown", this.onMenuKey)
|
||||
this.menu.removeEventListener("click", this.onMenuClick)
|
||||
if (this.menu.matches(":popover-open")) this.menu.hidePopover()
|
||||
}
|
||||
document.removeEventListener("pointerdown", this.onDocPointer, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue