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 ? (