fix(member-live): guard the composite sort event and apply review cleanup
This commit is contained in:
parent
f2312c24a1
commit
2c8a403cc0
6 changed files with 66 additions and 48 deletions
|
|
@ -923,8 +923,7 @@
|
|||
* 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.
|
||||
* room below. The SubfieldMenu hook only toggles it — no coordinate math.
|
||||
*/
|
||||
.popover-menu {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -352,6 +352,20 @@ Hooks.SortableList = {
|
|||
}
|
||||
}
|
||||
|
||||
// Guarded native Popover API toggles: showPopover()/hidePopover() throw on a
|
||||
// detached element or a redundant open/close, so the popover hooks below share
|
||||
// these wrappers instead of repeating the try/catch + :popover-open check.
|
||||
function openPopover(el) {
|
||||
try {
|
||||
if (el && !el.matches(":popover-open")) el.showPopover()
|
||||
} catch (_e) {}
|
||||
}
|
||||
function closePopover(el) {
|
||||
try {
|
||||
if (el && el.matches(":popover-open")) el.hidePopover()
|
||||
} catch (_e) {}
|
||||
}
|
||||
|
||||
// PopoverTooltip hook: shows a hint (referenced by data-tooltip-id) 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
|
||||
|
|
@ -386,19 +400,15 @@ Hooks.PopoverTooltip = {
|
|||
}
|
||||
|
||||
this.show = () => {
|
||||
openPopover(this.tip)
|
||||
// Popover is measurable once open; positioning in the same tick avoids a
|
||||
// visible flash (JS runs before the browser paints).
|
||||
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.hide = () => closePopover(this.tip)
|
||||
|
||||
this.onKey = (e) => { if (e.key === "Escape") this.hide() }
|
||||
|
||||
|
|
@ -431,8 +441,9 @@ Hooks.PopoverTooltip = {
|
|||
// 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).
|
||||
// re-renders on sort_composite anyway). The menu and its hint are placed via CSS
|
||||
// anchor positioning (see .popover-menu / .popover-tooltip in app.css), so this
|
||||
// hook does no coordinate math — it only toggles visibility, focus and keyboard.
|
||||
Hooks.SubfieldMenu = {
|
||||
mounted() {
|
||||
this.menu = document.getElementById(this.el.dataset.menuId)
|
||||
|
|
@ -442,17 +453,11 @@ Hooks.SubfieldMenu = {
|
|||
// .popover-tooltip look as the sort-header tooltips; placement is CSS anchor
|
||||
// positioning (no coordinate math).
|
||||
this.tip = document.getElementById(this.el.dataset.tooltipId)
|
||||
// Only hint while the menu is closed.
|
||||
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) {}
|
||||
if (!this.menu.matches(":popover-open")) openPopover(this.tip)
|
||||
}
|
||||
this.hideTip = () => closePopover(this.tip)
|
||||
|
||||
this.items = () => [...this.menu.querySelectorAll('[role="menuitemradio"]')]
|
||||
|
||||
|
|
@ -591,10 +596,11 @@ Hooks.SidebarState = {
|
|||
// approaches the viewport, rather than only once it is fully scrolled into view.
|
||||
// An IntersectionObserver rooted at the scroll container with a positive bottom
|
||||
// rootMargin treats the sentinel as visible while it is still that many pixels
|
||||
// below the fold, so the next page loads ahead of time. It complements the
|
||||
// built-in phx-viewport-bottom binding (which stays as a reliable backstop);
|
||||
// duplicate loads are harmless because the server guards on "more?" and the
|
||||
// stream de-duplicates rows by id.
|
||||
// below the fold, so the next page loads ahead of time. This is the SOLE load
|
||||
// trigger: the table deliberately carries no phx-viewport-bottom binding, whose
|
||||
// built-in InfiniteScroll would scroll the sentinel row back into view after each
|
||||
// load and cascade into loading every page. Duplicate loads are still harmless —
|
||||
// the server guards on "more?" and the stream de-duplicates rows by id.
|
||||
Hooks.LoadMorePrefetch = {
|
||||
mounted() {
|
||||
const container = document.getElementById(this.el.dataset.scrollContainer)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue