From 6e23c34bd32649212950f83ca1f484eb43a4c382 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 9 Oct 2026 23:59:02 +0200 Subject: [PATCH] u --- src/FillSpace.jsx | 188 +++++++++++++++++----------------------------- 1 file changed, 68 insertions(+), 120 deletions(-) diff --git a/src/FillSpace.jsx b/src/FillSpace.jsx index c7e4633..5891f6f 100644 --- a/src/FillSpace.jsx +++ b/src/FillSpace.jsx @@ -6,32 +6,20 @@ import Typography from '@mui/material/Typography'; import { alpha } from '@mui/material/styles'; /** - * FillSpace — fills remaining viewport height; paginates `items`. + * FillSpace — paginates `items`. * * @component FillSpace * @import dashgui/FillSpace * @defaultExport * - * @prop {React.Ref} [aboveRef] - Element(s) above; re-measure when they resize. - * @prop {React.Ref|React.Ref[]} [belowRef] - Footer element(s) below; their height is reserved. - * @prop {number} [reserveBottom=0] - Extra px to reserve below the spacer. - * @prop {number} [minHeight=0] - Minimum height in px. - * @prop {Array} items - Page size is measured from row height, wheel flips pages. + * @prop {Array} items - Wheel flips pages. * @prop {React.ComponentType|Function} renderItem - Row renderer (component or callback). - * @prop {number} [minPageSize=10] - Minimum rows per page. + * @prop {number} [minPageSize=10] - Rows per page. * @prop {(page: number) => void} [onPageChange] - Called on page change. - * @prop {number} [maxHeight] - Maximum height in px. * @prop {boolean} [debug=false] - Dashed outline for layout debugging. * @prop {object} [sx] - Extra styles. */ -/** Minimum spacer height so at least `count` units fit (lets the page scroll on small windows). */ -function minHeightFromCount(count, unitHeight) { - const n = Number(count) || 0; - if (n <= 0 || !(unitHeight > 0)) return 0; - return n * unitHeight; -} - /** True for class components (e.g. `renderItem={ArtikelMarke}`). Rendered as element, not called. */ function isRowComponent(renderItem) { return ( @@ -68,46 +56,41 @@ class FillSpace extends Component { constructor(props) { super(props); this.state = { - // null = not measured yet: render at natural content height so the - // first visible frame is already full-size, no 0 → expanded jump. - height: null, unitHeight: 0, - // Page size comes from the fit count. + viewportHeight: null, pageSize: Number(props.minPageSize) || 10, currentPage: 1, - measured: false, }; this.ref = null; this.probeRef = null; - this.paginationRef = null; this.wheelAcc = 0; this.observer = null; this.raf = null; } componentDidMount() { - // Measure synchronously (no rAF): setState here is batched before the - // browser paints, so the first visible frame already has the full - // viewport height — no small-box → expanded-box layout shift. - this._measure(); + this._probeMeasure(); + this._viewportMeasure(); this._observe(); window.addEventListener('resize', this._onViewportChange); } componentDidUpdate(prevProps) { + if (prevProps.renderItem !== this.props.renderItem) { + this._probeMeasure(); + } + if (prevProps.minPageSize !== this.props.minPageSize) { + this.setState({ pageSize: Number(this.props.minPageSize) || 10 }); + } if ( prevProps.aboveRef !== this.props.aboveRef || prevProps.belowRef !== this.props.belowRef || prevProps.containerRef !== this.props.containerRef || prevProps.reserveBottom !== this.props.reserveBottom || - prevProps.minHeight !== this.props.minHeight || - prevProps.minPageSize !== this.props.minPageSize || - prevProps.maxHeight !== this.props.maxHeight || - prevProps.items !== this.props.items || - prevProps.renderItem !== this.props.renderItem + prevProps.items !== this.props.items ) { this._observe(); - this._measure(); + this._viewportMeasure(); } // Clamp current page when the item list shrinks (e.g. brand deleted). if ( @@ -141,8 +124,7 @@ class FillSpace extends Component { if (this.raf) return; this.raf = requestAnimationFrame(() => { this.raf = null; - console.log('vp') - this._measure(); + this._viewportMeasure(); }); }; @@ -150,18 +132,16 @@ class FillSpace extends Component { if (this.observer) { this.observer.disconnect(); } - this.observer = new ResizeObserver(() => this._onViewportChange()); - const { aboveRef, belowRef, containerRef } = this.props; const aboveList = Array.isArray(aboveRef) ? aboveRef : [aboveRef]; const belowList = Array.isArray(belowRef) ? belowRef : [belowRef]; - const containerTarget = containerRef && 'current' in containerRef ? containerRef.current : containerRef; + const containerTarget = + containerRef && 'current' in containerRef ? containerRef.current : containerRef; const targets = [ ...aboveList.map((r) => (r && 'current' in r ? r.current : r)), ...belowList.map((r) => (r && 'current' in r ? r.current : r)), containerTarget, - this.paginationRef, ]; for (const el of targets) { if (el && el.offsetParent !== null) { @@ -170,87 +150,61 @@ class FillSpace extends Component { } } - _measure() { + probeMeasure = () => { + this._probeMeasure(); + }; + + viewportMeasure = () => { + this._viewportMeasure(); + }; + + measure = () => { + this._probeMeasure(); + this._viewportMeasure(); + }; + + /** Only measures the height of the `renderItem` probe. Nothing else. */ + _probeMeasure() { + const h = this.probeRef?.offsetHeight; + if (h > 0 && h !== this.state.unitHeight) { + this.setState({ unitHeight: h }, () => this._viewportMeasure()); + } + } + + /** Viewport height minus above/below refs: free space for the list. */ + _viewportMeasure() { if (!this.ref) return; - - const { - belowRef, - containerRef, - reserveBottom = 0, - minHeight = 0, - minPageSize, - maxHeight, - } = this.props; - - // Spacer top, document-relative: scrolling the page does not change the height. + const { belowRef, containerRef, reserveBottom = 0, minPageSize } = this.props; const rect = this.ref.getBoundingClientRect(); - const abovePx = rect.top + window.scrollY; - - // Footer below (box + top margin) plus extra trailing space. + let belowPx = Number(reserveBottom) || 0; const belowList = Array.isArray(belowRef) ? belowRef : [belowRef]; - let belowPx = 0; for (const ref of belowList) { - const belowEl = ref?.current; - if (belowEl && belowEl.offsetParent !== null) { - belowPx += belowEl.offsetHeight; - const marginTop = parseFloat(window.getComputedStyle(belowEl).marginTop); - belowPx += Number.isFinite(marginTop) ? marginTop : 0; + const el = ref && 'current' in ref ? ref.current : ref; + if (el && el.offsetParent !== null) { + belowPx += el.offsetHeight; + const mt = parseFloat(window.getComputedStyle(el).marginTop); + if (Number.isFinite(mt)) belowPx += mt; } } - // Extra trailing space (app-shell padding, tile padding) that must stay visible. - belowPx += Number(reserveBottom) || 0; - - const containerEl = containerRef && 'current' in containerRef ? containerRef.current : containerRef; + const containerEl = + containerRef && 'current' in containerRef ? containerRef.current : containerRef; let available; if (containerEl && typeof containerEl.getBoundingClientRect === 'function') { const cRect = containerEl.getBoundingClientRect(); const paddingBottom = parseFloat(window.getComputedStyle(containerEl).paddingBottom) || 0; available = Math.max(0, cRect.bottom - rect.top - belowPx - paddingBottom); } else { - available = Math.max(0, window.innerHeight - abovePx - belowPx); + available = Math.max(0, window.innerHeight - rect.top - belowPx); } - - const clamped = Math.min( - Math.max(available, minHeight), - maxHeight ?? Infinity, - ); - - let unitHeight = this.state.unitHeight; - const probeH = this.probeRef?.offsetHeight; - if (probeH > 0) unitHeight = probeH; const minSize = Math.max(1, Number(minPageSize) || 10); - const minHeightForCount = minHeightFromCount(minSize, unitHeight); - const finalHeight = Math.min( - Math.max(clamped, minHeightForCount), - maxHeight ?? Infinity, - ); - - // Fitting unit count drives page size. - let totalCount = 0; - if (unitHeight > 0) { - totalCount = Math.floor(finalHeight / unitHeight); - } - - // Fit count drives page size (min minPageSize). - let nextPageSize = this.state.pageSize; - let measured = this.state.measured; - if (totalCount > 0) { - nextPageSize = Math.max(minSize, totalCount); - measured = true; - } - + const { unitHeight } = this.state; + const nextPageSize = + unitHeight > 0 ? Math.max(minSize, Math.floor(available / unitHeight)) : minSize; if ( - Math.abs(finalHeight - this.state.height) > 0.5 || - Math.abs(unitHeight - this.state.unitHeight) > 0.5 || - nextPageSize !== this.state.pageSize || - measured !== this.state.measured + available !== this.state.viewportHeight || + nextPageSize !== this.state.pageSize ) { - this.setState({ - height: finalHeight, - unitHeight, - pageSize: nextPageSize, - measured, - }); + this.setState({ viewportHeight: available, pageSize: nextPageSize }); } } @@ -287,15 +241,16 @@ class FillSpace extends Component { render() { const { + // Discarded legacy layout props (kept here so they don't leak to the DOM). aboveRef, belowRef, containerRef, reserveBottom, - onWheel, - onPageChange, - minHeight = 0, - minPageSize, + minHeight, maxHeight, + minPageSize, + onPageChange, + onWheel, debug = false, items, renderItem, @@ -303,27 +258,22 @@ class FillSpace extends Component { ...rest } = this.props; - // FillSpace owns page size (from the rendered row height), current page, - // wheel-pagination and the pagination header. Caller feeds `items` plus - // one row element via `renderItem` (component type or callback). - const { height, pageSize, currentPage, measured } = this.state; + // FillSpace owns current page, wheel-pagination and the pagination header. + // Caller feeds `items` plus one row element via `renderItem` (component + // type or callback). + const { pageSize, currentPage, viewportHeight } = this.state; const total = items.length; const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const safePage = Math.min(Math.max(1, currentPage), pageCount); const offset = (safePage - 1) * pageSize; const pageItems = items.slice(offset, offset + pageSize); - // Hidden until the fit is measured — no "1–10" flash on load. - const showPagination = measured && total > pageSize; - // Hidden data-free probe: measures `renderItem` itself (no data), so - // the first pass already uses the final row height — one pass, no flip. + const showPagination = total > pageSize; + // Hidden data-free probe: measures `renderItem` itself (no data). const probe = renderProbeElement(renderItem); return ( <> { - this.paginationRef = el; - }} // Fixed single-line bar: the header height must not depend on the // page count. Otherwise the first fit (e.g. "1-17") pushes the // spacer down, the available height shrinks, and a second pass @@ -389,9 +339,7 @@ class FillSpace extends Component { sx={[ { position: 'relative', - height: height ?? 'auto', - minHeight, - ...(maxHeight != null ? { maxHeight } : {}), + height: viewportHeight ?? 'auto', overflow: 'hidden', width: '100%', // Keep wheel ticks from scrolling the page behind the list.