swarm-ui: keep the table filter popover inside the window
This commit is contained in:
parent
c2f17799fc
commit
16c9e0ae3b
3 changed files with 53 additions and 2 deletions
|
|
@ -9,12 +9,19 @@
|
||||||
// a `<table>` doesn't shrink below its content's natural width on its
|
// 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
|
// own, so without this the page itself would break, not just look
|
||||||
// cramped.
|
// cramped.
|
||||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "preact/hooks";
|
||||||
import { createPortal } from "preact/compat";
|
import { createPortal } from "preact/compat";
|
||||||
import type { ComponentChildren } from "preact";
|
import type { ComponentChildren } from "preact";
|
||||||
import { FilterIcon } from "@hive/shared/icons.js";
|
import { FilterIcon } from "@hive/shared/icons.js";
|
||||||
import { useLocalSetting } from "@hive/shared/settings-storage.js";
|
import { useLocalSetting } from "@hive/shared/settings-storage.js";
|
||||||
import { MultiselectFilterOptions } from "../multiselect-filter/MultiselectFilter.js";
|
import { MultiselectFilterOptions } from "../multiselect-filter/MultiselectFilter.js";
|
||||||
|
import { clampLeft } from "./popover-position.js";
|
||||||
import "./Table.css";
|
import "./Table.css";
|
||||||
|
|
||||||
export interface TableColumn<T> {
|
export interface TableColumn<T> {
|
||||||
|
|
@ -299,6 +306,13 @@ export function Table<T>({
|
||||||
// effect below can recompute the *currently open* column's position
|
// effect below can recompute the *currently open* column's position
|
||||||
// without needing the click that opened it to have happened again.
|
// without needing the click that opened it to have happened again.
|
||||||
const thRefs = useRef<Map<string, HTMLTableCellElement>>(new Map());
|
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) {
|
function computePopoverPos(key: string) {
|
||||||
const th = thRefs.current.get(key);
|
const th = thRefs.current.get(key);
|
||||||
|
|
@ -579,10 +593,11 @@ export function Table<T>({
|
||||||
class="ui-table-filter-popover"
|
class="ui-table-filter-popover"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label={filterLabel(openColumn)}
|
aria-label={filterLabel(openColumn)}
|
||||||
|
ref={popoverRef}
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
top: `${popoverPos.top}px`,
|
top: `${popoverPos.top}px`,
|
||||||
left: `${popoverPos.left}px`,
|
left: `${clampLeft(popoverPos.left, popoverWidth, window.innerWidth)}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderFilterControl(openColumn)}
|
{renderFilterControl(openColumn)}
|
||||||
|
|
|
||||||
19
frontend/packages/swarm-ui/src/ui/table/popover-position.ts
Normal file
19
frontend/packages/swarm-ui/src/ui/table/popover-position.ts
Normal 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));
|
||||||
|
}
|
||||||
17
frontend/packages/swarm-ui/test/popover-position.test.mjs
Normal file
17
frontend/packages/swarm-ui/test/popover-position.test.mjs
Normal 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);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue