Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: keep the table filter popover inside the window

This commit is contained in:
iris 2026-10-21 21:13:13 +02:00 • committed by mara
commit 16c9e0ae3b
3 changed files with 53 additions and 2 deletions

View file

@ -9,12 +9,19 @@
// a `<table>` doesn't shrink below its content's natural width on its
// own, so without this the page itself would break, not just look
// cramped.
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "preact/hooks";
import { createPortal } from "preact/compat";
import type { ComponentChildren } from "preact";
import { FilterIcon } from "@hive/shared/icons.js";
import { useLocalSetting } from "@hive/shared/settings-storage.js";
import { MultiselectFilterOptions } from "../multiselect-filter/MultiselectFilter.js";
import { clampLeft } from "./popover-position.js";
import "./Table.css";
export interface TableColumn<T> {
@ -299,6 +306,13 @@ export function Table<T>({
// effect below can recompute the *currently open* column's position
// without needing the click that opened it to have happened again.
const thRefs = useRef<Map<string, HTMLTableCellElement>>(new Map());
// The open popover's own width, measured once it is on screen, so its
// left edge can be pulled back inside the window.
const popoverRef = useRef<HTMLDivElement>(null);
const [popoverWidth, setPopoverWidth] = useState(0);
useLayoutEffect(() => {
setPopoverWidth(popoverRef.current?.offsetWidth ?? 0);
}, [openFilterKey, popoverPos]);
function computePopoverPos(key: string) {
const th = thRefs.current.get(key);
@ -579,10 +593,11 @@ export function Table<T>({
class="ui-table-filter-popover"
role="dialog"
aria-label={filterLabel(openColumn)}
ref={popoverRef}
style={{
position: "fixed",
top: `${popoverPos.top}px`,
left: `${popoverPos.left}px`,
left: `${clampLeft(popoverPos.left, popoverWidth, window.innerWidth)}px`,
}}
>
{renderFilterControl(openColumn)}

View file

@ -0,0 +1,19 @@
// Where a table's filter popover sits, without a DOM.
// Space kept between a floating box and the viewport's edge.
export const POPOVER_EDGE_PX = 8;
/**
* The left edge for a popover of `width` that wants to start at `left`:
* pulled in so its right edge stays on screen, but never past the left edge.
* A filterable column near the right of the window would otherwise open
* partly outside it.
*/
export function clampLeft(
left: number,
width: number,
viewportWidth: number,
): number {
const maxLeft = viewportWidth - width - POPOVER_EDGE_PX;
return Math.max(POPOVER_EDGE_PX, Math.min(left, maxLeft));
}

View file

@ -0,0 +1,17 @@
// Where a table's filter popover sits. Run by `npm test`, which Node runs
// with its built-in test runner and type stripping, so the `.ts` module is
// imported as it is.
import assert from "node:assert/strict";
import { test } from "node:test";
import { clampLeft } from "../src/ui/table/popover-position.ts";
test("a popover near the right edge is pulled back on screen", () => {
// 300 wide in a 1000 wide window: the right edge may reach 992.
assert.equal(clampLeft(900, 300, 1000), 692);
assert.equal(clampLeft(100, 300, 1000), 100);
});
test("a popover never goes past the left edge", () => {
assert.equal(clampLeft(5, 300, 1000), 8);
assert.equal(clampLeft(50, 2000, 1000), 8);
});