From eda3b193c61cf0a816c38789fd2e343ab6d81466 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 9 Oct 2026 20:55:28 +0200 Subject: [PATCH] u --- src/FillSpace.jsx | 83 +++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/FillSpace.jsx b/src/FillSpace.jsx index 526d6b6..b3485eb 100644 --- a/src/FillSpace.jsx +++ b/src/FillSpace.jsx @@ -1,5 +1,6 @@ import { Component } from 'react'; import Box from '@mui/material/Box'; +import List from '@mui/material/List'; import Pagination from '@mui/material/Pagination'; import Typography from '@mui/material/Typography'; import { alpha } from '@mui/material/styles'; @@ -21,8 +22,8 @@ import { alpha } from '@mui/material/styles'; * @prop {(count: number) => void} [onMeasurement] - Called whenever the number of `measureElement` units that would fit in the whole spacer height changes, as if there were no `children` (i.e. the total row count the space could hold). Fires once after mount with the initial count. * @prop {number} [minHeight=0] - Minimum spacer height in px. * @prop {number} [minCount=0] - Minimum number of `measureElement` units the spacer must fit. When the viewport is too small, the spacer grows beyond the viewport so the page scrolls instead of clipping. - * @prop {Array} [items] - When provided, FillSpace paginates: it measures the page size, slices `items`, renders the current page via `renderPage`, and owns wheel-pagination plus the pagination header. Omit for legacy children mode. - * @prop {(pageItems: Array, page: {offset:number, pageSize:number, currentPage:number, pageCount:number, total:number}) => ReactNode} [renderPage] - Renders the current page slice (paginated mode). + * @prop {Array} [items] - When provided with `renderItem`, FillSpace paginates: it measures the page size from the rendered row height, slices `items`, renders the current page as a list, and owns wheel-pagination plus the pagination header. Omit for legacy children mode. + * @prop {(item: any, index: number) => ReactNode} [renderItem] - Renders one row element (e.g. a brand row). The first rendered row doubles as the measure element, so no separate `measureElement` is needed in paginated mode. * @prop {number} [minPageSize=10] - Minimum rows per page in paginated mode. * @prop {(page: number) => void} [onPageChange] - Called when the current page changes (wheel or header). * @prop {number} [maxHeight] - Optional maximum spacer height in px. @@ -91,7 +92,8 @@ class FillSpace extends Component { prevProps.minPageSize !== this.props.minPageSize || prevProps.maxHeight !== this.props.maxHeight || prevProps.measureElement !== this.props.measureElement || - prevProps.measureGap !== this.props.measureGap + prevProps.measureGap !== this.props.measureGap || + prevProps.items !== this.props.items ) { this._observe(); this._measure(); @@ -171,6 +173,7 @@ class FillSpace extends Component { containerRef, reserveBottom = 0, measureElement, + renderItem, measureGap = 0, minHeight = 0, minCount = 0, @@ -223,18 +226,23 @@ class FillSpace extends Component { contentHeight = this.contentRef.offsetHeight; } - // Measure the height of one measurement unit from the hidden copy so the - // repeats match the real rendered size. + // Unit height: in paginated mode the first rendered row is the measure + // element — no separate hidden sample needed. Legacy children mode + // still uses the hidden `measureElement` copy. + const paginated = Array.isArray(items) && typeof renderItem === 'function'; let unitHeight = this.state.unitHeight; - if (measureElement != null && this.unitRef) { + if (paginated) { + const firstRow = this.contentRef?.querySelector?.('[data-fill-row]') ?? this.contentRef?.firstElementChild; + const h = firstRow?.offsetHeight; + if (h > 0) unitHeight = h; + if (!(unitHeight > 0) && measureElement != null && this.unitRef) { + const fallback = this.unitRef.offsetHeight; + if (fallback > 0) unitHeight = fallback; + } + } else if (measureElement != null && this.unitRef) { const h = this.unitRef.offsetHeight; if (h > 0) unitHeight = h; } - - // Minimum rows: grow past the viewport so the page scrolls instead - // of clipping when the window is too small. Paginated mode uses - // minPageSize (fallback minCount). - const paginated = Array.isArray(items); const minSize = Math.max(1, Number(minPageSize ?? minCount) || 10); const effectiveMinCount = paginated ? Math.max(Number(minCount) || 0, minSize) : Number(minCount) || 0; const minHeightForCount = minHeightFromCount(effectiveMinCount, unitHeight, measureGap); @@ -243,14 +251,14 @@ class FillSpace extends Component { maxHeight ?? Infinity, ); - // How many units would fit in the WHOLE spacer height, as if there were - // no children — this is what onMeasurement reports. + // How many units fit in the spacer height — drives the page size in + // paginated mode and the `onMeasurement` callback in legacy mode. let totalCount = 0; // How many units actually fit in the leftover space below the children — // this is what gets rendered. Starts at 0: if the content already fills // the space, none fit. let fillCount = 0; - if (measureElement != null && unitHeight > 0) { + if (unitHeight > 0 && (measureElement != null || paginated)) { totalCount = Math.floor((finalHeight + measureGap) / (unitHeight + measureGap)); const fillHeight = Math.max(0, finalHeight - contentHeight); fillCount = Math.floor((fillHeight + measureGap) / (unitHeight + measureGap)); @@ -269,6 +277,7 @@ class FillSpace extends Component { } // Paginated mode: fit count drives the page size (min minPageSize). + // Legacy mode only forwards the count via onMeasurement. let nextPageSize = this.state.pageSize; let measured = this.state.measured; if (paginated && totalCount > 0) { @@ -349,22 +358,21 @@ class FillSpace extends Component { debug = false, children, items, - renderPage, + renderItem, measureElement, measureGap = 0, sx, ...rest } = this.props; - // Paginated mode: FillSpace owns page size (from fit), current page, - // wheel-pagination and the pagination header. Caller just feeds `items` - // and renders the slice via `renderPage`. - if (Array.isArray(items) && typeof renderPage === 'function') { + // Paginated mode: 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`. + if (Array.isArray(items) && typeof renderItem === 'function') { return this.renderPaginated({ minHeight, maxHeight, debug, - measureElement, measureGap, sx, rest, @@ -473,11 +481,12 @@ class FillSpace extends Component { /** * Paginated mode: pagination header (hidden until measured) + exactly one - * page of `items` rendered via `renderPage`. Wheel over the list flips - * pages; effort scales with page count (slow for few pages). + * page of `items`, each row rendered via `renderItem`. The first row + * doubles as the measure element. Wheel over the list flips pages; + * effort scales with page count (slow for few pages). */ - renderPaginated({ minHeight, maxHeight, debug, measureElement, measureGap, sx, rest }) { - const { items, renderPage, onWheel } = this.props; + renderPaginated({ minHeight, maxHeight, debug, measureGap, sx, rest }) { + const { items, renderItem, onWheel } = this.props; const { height, pageSize, currentPage, measured } = this.state; const total = items.length; const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); @@ -562,26 +571,14 @@ class FillSpace extends Component { }} sx={{ width: '100%' }} > - {renderPage(pageItems, { offset, pageSize, currentPage: safePage, pageCount, total })} + + {pageItems.map((item, i) => ( + + {renderItem(item, offset + i)} + + ))} + - {measureElement != null ? ( - { - this.unitRef = el; - }} - aria-hidden - sx={{ - position: 'absolute', - left: 0, - top: 0, - width: '100%', - visibility: 'hidden', - pointerEvents: 'none', - }} - > - {measureElement} - - ) : null} {debug ? (