Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: portal the filter popover past the table's own clip

argus's review (reproduced, not speculative): .ui-table-scroll sets
overflow-x: auto with overflow-y left unset. Per the CSS overflow
spec, an axis left visible computes to auto too once the other axis
isn't visible -- so this box silently clips vertically as well as
horizontally. The popover was position: absolute; top: 100% under its
header <th>, itself inside .ui-table-scroll -- on a table shorter than
header-plus-popover (a small hive's roster, or any table narrowed by
an existing filter), the popover got cut off at the scroll box's own
bottom edge, with a stray vertical scrollbar as the visible symptom.

Computes the popover's viewport position from the anchor <th>'s own
getBoundingClientRect() and renders it via a portal onto
document.body, position: fixed -- escapes .ui-table-scroll's clip the
same way any position: fixed element escapes an ancestor's overflow
(neither .ui-table-scroll nor .ui-table establishes a new containing
block). Recomputes on scroll (capture-phase window listener, the
standard technique for detecting scroll on a nested scroll container
without binding to every ancestor by hand) and resize while open, so
the popover stays anchored rather than only positioning once at click
time. createPortal comes from preact/compat, already resolvable
through the existing preact dependency -- no new package.json entry.

Verified against argus's own repro shape: a 1-row table, scripted
click on the filter icon (same real-DOM-event technique as this PR's
first round), screenshot shows the popover rendering fully rather than
clipped, no stray scrollbar.
This commit is contained in:
iris 2026-09-08 11:47:31 +02:00 • committed by mara
commit 63f3afae3e
2 changed files with 118 additions and 39 deletions

View file

@ -63,14 +63,14 @@
opacity: 1;
}
/* A filterable header cell anchors its own popover — `position: relative`
is what lets `.ui-table-filter-popover`'s `position: absolute` below
sit under this `<th>` specifically rather than the page. */
.ui-table-th-filterable {
position: relative;
}
/* `.ui-table-th-filterable` (set on a filterable column's `<th>`, no
rule of its own) exists purely as a `:hover`/`:focus-within` selector
target for the fade below — nothing to do with positioning the
popover itself, which is computed in JS and rendered via a portal
instead (see Table.tsx's own comment on `popoverPos` for why it
isn't just `position: absolute` under this `<th>`).
/* The filter-icon trigger — invisible by default, `opacity` transition
The filter-icon trigger — invisible by default, `opacity` transition
rather than a hard show/hide (mara: "fade in out animation"). Shown
on three signals, matching Table.tsx's own comment on `filterValue`:
hovering/focusing the header cell (so it's discoverable without a
@ -112,12 +112,16 @@
color: var(--purple);
}
/* `position`/`top`/`left` are inline styles, not here — Table.tsx
computes them from the anchor `<th>`'s own `getBoundingClientRect()`
and renders this via a portal onto `document.body` (see its own
`popoverPos` comment for the full reasoning: escaping
`.ui-table-scroll`'s clip). `z-index: 10` matches the existing
overlay-menu convention (`LinksMenu`/`UserMenu`), now that this
sits alongside them as a `document.body`-level sibling rather than
nested inside the table. */
.ui-table-filter-popover {
position: absolute;
top: 100%;
left: 0;
z-index: 5;
margin-top: 0.35em;
z-index: 10;
min-width: 12em;
padding: 0.5em;
background: var(--bg-elev);