From 5f676ca43f76d798aff8f975539c53e918aa745f Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 12 Aug 2026 02:42:38 +0200 Subject: [PATCH] feat: add FillSpace viewport-height spacer with repeating measureElement Adds a reusable FillSpace component to dashgui that fills the remaining viewport height between an aboveRef and optional belowRef, with a scrollable middle section. Supports an optional measureElement that is repeated top-to-bottom to fill the leftover space below children (rows are rendered first, units fill height - contentHeight). Includes mock page, docs wiring, and screenshot references. --- README.md | 1 + package.json | 1 + src/FillSpace.jsx | 268 ++++++++++++++++++ src/dev/DevApp.jsx | 2 + src/dev/mock-pages/FillSpaceMockPage.jsx | 171 +++++++++++ src/dev/screenshots/ScreenshotHarness.jsx | 125 +++++++- src/index.js | 1 + tests/screenshot.spec.js | 2 + .../reference/fillspace-measure.png | Bin 0 -> 6321 bytes tests/screenshots/reference/fillspace.png | Bin 0 -> 10545 bytes vite-plugin-component-docs.js | 1 + 11 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 src/FillSpace.jsx create mode 100644 src/dev/mock-pages/FillSpaceMockPage.jsx create mode 100644 tests/screenshots/reference/fillspace-measure.png create mode 100644 tests/screenshots/reference/fillspace.png diff --git a/README.md b/README.md index f442e0a..c71f728 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This package is source-only: consumers import directly from `src/` via the | `dashgui/tiles` | `src/tiles/` | `Tile`, `TILE_SIZE`, `tileBaseSx` | | `dashgui/ElevatedPill` | `src/ElevatedPill.jsx` | `ElevatedPill`, `ToggleElevatedPill`, `ActionElevatedPill` | | `dashgui/SingleLineTextEdit` | `src/SingleLineTextEdit.jsx` | Centralized single-line text field | +| `dashgui/FillSpace` | `src/FillSpace.jsx` | `FillSpace` — viewport-height spacer between header/footer refs; optional repeating `measureElement` | ## Design rules diff --git a/package.json b/package.json index 642bab9..c09889e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "./tiles": "./src/tiles/index.js", "./ElevatedPill": "./src/ElevatedPill.jsx", "./SingleLineTextEdit": "./src/SingleLineTextEdit.jsx", + "./FillSpace": "./src/FillSpace.jsx", "./package.json": "./package.json" }, "files": [ diff --git a/src/FillSpace.jsx b/src/FillSpace.jsx new file mode 100644 index 0000000..7001b1a --- /dev/null +++ b/src/FillSpace.jsx @@ -0,0 +1,268 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import { alpha } from '@mui/material/styles'; + +/** + * Vertical spacer that fills the remaining viewport height between the content + * above it and (optionally) a footer below it — useful for creating scrollable + * middle sections in apps with fixed headers/footers. + * + * @component FillSpace + * @import dashgui/FillSpace + * @defaultExport + * + * @prop {React.Ref} [aboveRef] - Ref to the element immediately above the spacer. It is observed (not strictly required) so the spacer re-measures when that element's height changes. + * @prop {React.Ref} [belowRef] - Ref to the footer element below. Its height (plus top margin) is reserved so the footer stays visible without page scrolling. + * @prop {ReactNode} [measureElement] - When given, this element is used as the measurement unit of the spacer: its height is read from a hidden copy and it is repeated top-to-bottom to fill only the leftover space below `children` (like a repeating ruler/row pattern). When omitted, only `children` are rendered. + * @prop {number} [measureGap=0] - Vertical gap (px) between repeated `measureElement` units. + * @prop {number} [minHeight=0] - Minimum spacer height in px. + * @prop {number} [maxHeight] - Optional maximum spacer height in px. + * @prop {boolean} [debug=false] - When true, draws a dashed outline around the spacer and a translucent overlay over the whole viewport so the measured bounds are visible. + * @prop {ReactNode} [children] - Real content rendered at the top of the spacer. The repeated `measureElement` units (if any) fill the remaining space below it. + * @prop {object} [sx] - Extra styles merged into the Box. + * + * @behaviour Fills to the bottom of the initial viewport: height = viewport height − spacer's distance from the document top − reserved below height. + * @behaviour The distance-from-top is read from the spacer's own position (document-relative), so every element above (including their margins and the parent's padding) is accounted for, and scrolling the page does not change the computed height. + * @behaviour When `measureElement` is set, `children` are rendered on top and the units fill only `height − contentHeight`, so no units are shown once the content already fills the space. + * @behaviour Re-measures on window resize and whenever the observed refs or the content change size (ResizeObserver), throttled with requestAnimationFrame. + * @behaviour `debug` draws a dashed primary outline around the spacer plus a translucent full-viewport overlay. + */ +class FillSpace extends Component { + constructor(props) { + super(props); + this.state = { height: 0, unitHeight: 0, contentHeight: 0 }; + this.ref = null; + this.contentRef = null; + this.unitRef = null; + this.observer = null; + this.raf = null; + } + + componentDidMount() { + requestAnimationFrame(() => { + this._measure(); + this._observe(); + }); + window.addEventListener('resize', this._onViewportChange); + } + + componentDidUpdate(prevProps) { + if ( + prevProps.aboveRef !== this.props.aboveRef || + prevProps.belowRef !== this.props.belowRef || + prevProps.minHeight !== this.props.minHeight || + prevProps.maxHeight !== this.props.maxHeight || + prevProps.measureElement !== this.props.measureElement || + prevProps.measureGap !== this.props.measureGap + ) { + this._observe(); + this._measure(); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this._onViewportChange); + if (this.raf) { + cancelAnimationFrame(this.raf); + this.raf = null; + } + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + } + + _onViewportChange = () => { + if (this.raf) return; + this.raf = requestAnimationFrame(() => { + this.raf = null; + this._measure(); + }); + }; + + _observe() { + if (this.observer) { + this.observer.disconnect(); + } + + this.observer = new ResizeObserver(() => this._measure()); + + // Only observe the referenced elements + the rendered content — never the + // spacer's own parent, since resizing the spacer would resize the parent + // and create a loop. + const { aboveRef, belowRef } = this.props; + const targets = [aboveRef?.current, belowRef?.current, this.contentRef]; + for (const el of targets) { + if (el && el.offsetParent !== null) { + this.observer.observe(el); + } + } + } + + _measure() { + if (!this.ref) return; + + const { belowRef, measureElement, minHeight = 0, maxHeight } = this.props; + + // Distance from the document top to the spacer top — includes every + // element above the spacer plus their margins and the parent's padding. + // Document-relative (rect.top + scrollY), so scrolling the page does not + // change the computed height (fixes "grows with scrolling"). + const rect = this.ref.getBoundingClientRect(); + const abovePx = rect.top + window.scrollY; + + // Height to reserve for the footer below (its box + top margin). + const belowEl = belowRef?.current; + let belowPx = 0; + if (belowEl && belowEl.offsetParent !== null) { + belowPx = belowEl.offsetHeight; + const marginTop = parseFloat(window.getComputedStyle(belowEl).marginTop); + belowPx += Number.isFinite(marginTop) ? marginTop : 0; + } + + const available = Math.max(0, window.innerHeight - abovePx - belowPx); + const clamped = Math.min( + Math.max(available, minHeight), + maxHeight ?? Infinity, + ); + + // Height of the real rendered children (the top part of the spacer). + let contentHeight = 0; + if (this.contentRef) { + contentHeight = this.contentRef.offsetHeight; + } + + // Measure the height of one measurement unit from the hidden copy so the + // repeats match the real rendered size. + let unitHeight = this.state.unitHeight; + if (measureElement != null && this.unitRef) { + const h = this.unitRef.offsetHeight; + if (h > 0) unitHeight = h; + } + + if ( + Math.abs(clamped - this.state.height) > 0.5 || + Math.abs(unitHeight - this.state.unitHeight) > 0.5 || + Math.abs(contentHeight - this.state.contentHeight) > 0.5 + ) { + this.setState({ height: clamped, unitHeight, contentHeight }); + } + } + + render() { + const { + minHeight = 0, + maxHeight, + debug = false, + children, + measureElement, + measureGap = 0, + sx, + ...rest + } = this.props; + + const { height, unitHeight, contentHeight } = this.state; + + // Repeated measurement units fill only the leftover space below the real + // children. Count starts at 0: if the content already fills the space, + // no units are rendered. + let fill = null; + if (measureElement != null) { + const unit = unitHeight > 0 ? unitHeight : 0; + const fillHeight = Math.max(0, height - contentHeight); + const count = + unit > 0 + ? Math.floor((fillHeight + measureGap) / (unit + measureGap)) + : 0; + const items = []; + for (let i = 0; i < count; i++) { + items.push( + + {measureElement} + , + ); + } + fill = ( + + {items} + + ); + } + + return ( + <> + { + this.ref = el; + }} + {...rest} + sx={[ + { + position: 'relative', + height, + minHeight, + ...(maxHeight != null ? { maxHeight } : {}), + }, + debug + ? (theme) => ({ + outline: `2px dashed ${theme.palette.primary.main}`, + }) + : undefined, + sx, + ]} + > + {children != null ? ( + { + this.contentRef = el; + }} + sx={{ width: '100%' }} + > + {children} + + ) : null} + {fill} + {measureElement != null ? ( + { + this.unitRef = el; + }} + aria-hidden + sx={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + visibility: 'hidden', + pointerEvents: 'none', + }} + > + {measureElement} + + ) : null} + + {debug ? ( + ({ + position: 'fixed', + inset: 0, + pointerEvents: 'none', + background: alpha(theme.palette.primary.main, 0.08), + zIndex: 9999, + })} + /> + ) : null} + + ); + } +} + +export default FillSpace; diff --git a/src/dev/DevApp.jsx b/src/dev/DevApp.jsx index 8275b51..198ef1c 100644 --- a/src/dev/DevApp.jsx +++ b/src/dev/DevApp.jsx @@ -8,6 +8,7 @@ import TilesMockPage from './mock-pages/TilesMockPage'; import PillsMockPage from './mock-pages/PillsMockPage'; import TextFieldsMockPage from './mock-pages/TextFieldsMockPage'; import ListsMockPage from './mock-pages/ListsMockPage'; +import FillSpaceMockPage from './mock-pages/FillSpaceMockPage'; const PAGES = [ { id: 'panels', label: 'Panels', node: }, @@ -15,6 +16,7 @@ const PAGES = [ { id: 'tiles', label: 'Tiles', node: }, { id: 'pills', label: 'Pills', node: }, { id: 'textfields', label: 'Text fields', node: }, + { id: 'fillspace', label: 'FillSpace', node: }, ]; /** diff --git a/src/dev/mock-pages/FillSpaceMockPage.jsx b/src/dev/mock-pages/FillSpaceMockPage.jsx new file mode 100644 index 0000000..50f21be --- /dev/null +++ b/src/dev/mock-pages/FillSpaceMockPage.jsx @@ -0,0 +1,171 @@ +import { Component, createRef } from 'react'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import FillSpace from '../../FillSpace'; +import { ComponentDocs } from '../ComponentDocs'; + +/** + * Mock page exercising FillSpace: real content on top, and a repeating + * `measureElement` filling only the leftover space down to the end of the + * viewport. + */ +class FillSpaceMockPage extends Component { + constructor(props) { + super(props); + this.ex1 = { header: createRef(), footer: createRef() }; + this.ex2 = { header: createRef(), footer: createRef() }; + this.ex3 = { header: createRef(), footer: createRef() }; + } + + render() { + return ( + + + FillSpace renders real content on top and repeats the measurement + element until the end of the page. + + + {/* Main example: content + repeating fill to end of page */} + + + Content + repeating element to end of page + + + + Header (measured) + + + + Einheit + + } + > + + + Echte Inhalte (children) + + + Diese Zeilen stehen oben, darunter wiederholt sich die + Einheit bis zum Seitenende. + + + + + + Footer (measured) + + + + + {/* Example 2: With minHeight */} + + + With minHeight constraint + + + + Header + + + + + + Minimum height of 200px enforced. + + + + + + Footer + + + + + {/* Example 3: Debug mode */} + + + Debug mode overlay + + + + Header + + + + + + Green background shows actual spacer bounds; purple overlay + shows full viewport coverage. + + + + + + Footer + + + + + + + ); + } +} + +export default FillSpaceMockPage; diff --git a/src/dev/screenshots/ScreenshotHarness.jsx b/src/dev/screenshots/ScreenshotHarness.jsx index fbc5cdc..4636e0a 100644 --- a/src/dev/screenshots/ScreenshotHarness.jsx +++ b/src/dev/screenshots/ScreenshotHarness.jsx @@ -1,4 +1,4 @@ -import { Component } from 'react'; +import { Component, createRef } from 'react'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Chip from '@mui/material/Chip'; @@ -15,6 +15,7 @@ import ElevatedPill, { ToggleElevatedPill, } from '../../ElevatedPill'; import SingleLineTextEdit from '../../SingleLineTextEdit'; +import FillSpace from '../../FillSpace'; const MOCK_ROWS = [ { @@ -292,6 +293,128 @@ class ScreenshotHarness extends Component { color="#e2e8f0" /> + +
+ +
+ +
+ +
+ + ); + } +} + +/** + * Deterministic FillSpace example for the screenshot harness. Bound with a + * `maxHeight` so the reference is stable regardless of the viewport height. + */ +class FillSpaceExample extends Component { + constructor(props) { + super(props); + this.headerRef = createRef(); + this.footerRef = createRef(); + } + + render() { + return ( + + + Header (gemessen) + + + + + + FillSpace füllt den Platz zwischen Header und Footer. + + + + + + Footer (gemessen) + + + ); + } +} + +/** + * FillSpace with a repeating measurement element plus real children on top, + * for the screenshot harness. Bound with maxHeight so the reference is stable. + */ +class FillSpaceMeasureExample extends Component { + constructor(props) { + super(props); + this.headerRef = createRef(); + this.footerRef = createRef(); + } + + render() { + return ( + + + Header + + + + Einheit + + } + measureGap={8} + minHeight={60} + maxHeight={260} + > + + Echte Inhalte + + + + + Footer + ); } diff --git a/src/index.js b/src/index.js index 1f01774..2a019e6 100644 --- a/src/index.js +++ b/src/index.js @@ -14,3 +14,4 @@ export * from './lists'; export * from './tiles'; export { default as ElevatedPill } from './ElevatedPill'; export { default as SingleLineTextEdit } from './SingleLineTextEdit'; +export { default as FillSpace } from './FillSpace'; diff --git a/tests/screenshot.spec.js b/tests/screenshot.spec.js index 9039368..85d8746 100644 --- a/tests/screenshot.spec.js +++ b/tests/screenshot.spec.js @@ -21,6 +21,8 @@ const STATIC_SECTIONS = [ 'lists-loading', 'lists-failed', 'lists-no-actions', + 'fillspace', + 'fillspace-measure', ]; test.beforeEach(async ({ page }) => { diff --git a/tests/screenshots/reference/fillspace-measure.png b/tests/screenshots/reference/fillspace-measure.png new file mode 100644 index 0000000000000000000000000000000000000000..d308506d8b7ae4dc9719923be77f252f19de3584 GIT binary patch literal 6321 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 PAW)Q5dk2&G81TOU^aZV> literal 0 HcmV?d00001 diff --git a/vite-plugin-component-docs.js b/vite-plugin-component-docs.js index 18ee22b..5311771 100644 --- a/vite-plugin-component-docs.js +++ b/vite-plugin-component-docs.js @@ -32,6 +32,7 @@ const SOURCES = [ { file: 'src/panels/EditablePanelTile.jsx', importPath: 'dashgui/panels' }, { file: 'src/ElevatedPill.jsx', importPath: 'dashgui/ElevatedPill' }, { file: 'src/SingleLineTextEdit.jsx', importPath: 'dashgui/SingleLineTextEdit' }, + { file: 'src/FillSpace.jsx', importPath: 'dashgui/FillSpace' }, { file: 'src/tiles/Tile.jsx', importPath: 'dashgui/tiles' }, { file: 'src/lists/RecordList.jsx', importPath: 'dashgui/lists' }, ];