This commit is contained in:
seb
2026-10-13 23:20:59 +02:00
parent 1f25cf2dc6
commit c7b6af0b43

View File

@@ -17,8 +17,9 @@ import { alpha } from '@mui/material/styles';
* @prop {React.ReactNode} [header] - Rendered below pagination controls and above the list items. * @prop {React.ReactNode} [header] - Rendered below pagination controls and above the list items.
* @prop {number} [minPageSize=10] - Rows per page. * @prop {number} [minPageSize=10] - Rows per page.
* @prop {number} [maxPageSize] - Maximum rows per page. * @prop {number} [maxPageSize] - Maximum rows per page.
* @prop {number} [totalCount] - Total items in unfiltered dataset. When unfiltered count <= pageSize, pagination header is removed entirely. When filtering, the header height is preserved to prevent layout shift. * @prop {number} [totalCount] - Total items in current dataset (or filtered dataset when paginating).
* @prop {number} [unfilteredCount] - Alias for totalCount. * @prop {number} [unfilteredTotal] - Total items in unfiltered dataset. When unfilteredTotal > pageSize, pagination space is reserved even if current filtered count <= pageSize, preventing layout shift.
* @prop {number} [unfilteredCount] - Alias for unfilteredTotal.
* @prop {(page: number) => void} [onPageChange] - Called on page change. * @prop {(page: number) => void} [onPageChange] - Called on page change.
* @prop {boolean} [virtualScrollbar=false] - Shows a synchronized scrollbar alongside pagination controls. * @prop {boolean} [virtualScrollbar=false] - Shows a synchronized scrollbar alongside pagination controls.
* @prop {boolean} [reserveScrollbar] - Reserves raceway space even when items do not overflow (defaults to true when virtualScrollbar is true). * @prop {boolean} [reserveScrollbar] - Reserves raceway space even when items do not overflow (defaults to true when virtualScrollbar is true).
@@ -84,13 +85,15 @@ class FillSpace extends Component {
this.raf = null; this.raf = null;
this.dragStartY = 0; this.dragStartY = 0;
this.dragStartThumbTop = 0; this.dragStartThumbTop = 0;
const explicitCount = props.unfilteredCount ?? props.totalCount; const rawUnfiltered = props.unfilteredTotal ?? props.unfilteredCount;
this.maxSeenCount = const unfilteredTotal = rawUnfiltered != null ? Number(rawUnfiltered) : null;
explicitCount != null const total =
? Number(explicitCount) props.totalCount != null
? Number(props.totalCount)
: Array.isArray(props.items) : Array.isArray(props.items)
? props.items.length ? props.items.length
: 0; : 0;
this.maxSeenCount = unfilteredTotal != null ? unfilteredTotal : total;
} }
componentDidMount() { componentDidMount() {
@@ -123,20 +126,24 @@ class FillSpace extends Component {
this._observe(); this._observe();
this._viewportMeasure(); this._viewportMeasure();
} }
const explicitCount = this.props.unfilteredCount ?? this.props.totalCount; const rawUnfiltered = this.props.unfilteredTotal ?? this.props.unfilteredCount;
if (explicitCount != null) { const unfilteredTotal = rawUnfiltered != null ? Number(rawUnfiltered) : null;
this.maxSeenCount = Number(explicitCount); const total =
} else if (Array.isArray(this.props.items)) { this.props.totalCount != null
this.maxSeenCount = Math.max(this.maxSeenCount || 0, this.props.items.length); ? Number(this.props.totalCount)
: Array.isArray(this.props.items)
? this.props.items.length
: 0;
if (unfilteredTotal != null) {
this.maxSeenCount = unfilteredTotal;
} else {
this.maxSeenCount = Math.max(this.maxSeenCount || 0, total);
} }
// Clamp current page when the item list shrinks (e.g. brand deleted). // Clamp current page when the item list shrinks (e.g. brand deleted).
if ( if (
Array.isArray(this.props.items) && Array.isArray(this.props.items) &&
this.props.items !== prevProps.items this.props.items !== prevProps.items
) { ) {
const explicitTotal = this.props.unfilteredCount ?? this.props.totalCount;
const total =
explicitTotal != null ? Number(explicitTotal) : this.props.items.length;
const pageCount = Math.max( const pageCount = Math.max(
1, 1,
Math.ceil(total / Math.max(1, this.state.pageSize)), Math.ceil(total / Math.max(1, this.state.pageSize)),
@@ -173,10 +180,9 @@ class FillSpace extends Component {
_onThumbMouseDown = (e) => { _onThumbMouseDown = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const { totalCount, unfilteredCount, items } = this.props; const { totalCount, items } = this.props;
const { viewportHeight, pageSize, currentPage } = this.state; const { viewportHeight, pageSize, currentPage } = this.state;
const explicitTotal = unfilteredCount ?? totalCount; const total = totalCount != null ? Number(totalCount) : (items?.length || 0);
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
const racewayBottom = Math.max(0, Number(this.props.racewayMarginBottom ?? RACEWAY_BOTTOM_MARGIN) || 0); const racewayBottom = Math.max(0, Number(this.props.racewayMarginBottom ?? RACEWAY_BOTTOM_MARGIN) || 0);
const trackHeight = Math.max(0, (viewportHeight || 0) - racewayBottom); const trackHeight = Math.max(0, (viewportHeight || 0) - racewayBottom);
@@ -195,10 +201,9 @@ class FillSpace extends Component {
_onThumbDragMove = (e) => { _onThumbDragMove = (e) => {
if (!this.state.isDragging) return; if (!this.state.isDragging) return;
const { totalCount, unfilteredCount, items } = this.props; const { totalCount, items } = this.props;
const { viewportHeight, pageSize } = this.state; const { viewportHeight, pageSize } = this.state;
const explicitTotal = unfilteredCount ?? totalCount; const total = totalCount != null ? Number(totalCount) : (items?.length || 0);
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
if (pageCount <= 1) return; if (pageCount <= 1) return;
@@ -232,9 +237,8 @@ class FillSpace extends Component {
const trackRect = this.trackRef.getBoundingClientRect(); const trackRect = this.trackRef.getBoundingClientRect();
const clickY = e.clientY - trackRect.top; const clickY = e.clientY - trackRect.top;
const { viewportHeight, pageSize } = this.state; const { viewportHeight, pageSize } = this.state;
const { items, totalCount, unfilteredCount } = this.props; const { items, totalCount } = this.props;
const explicitTotal = unfilteredCount ?? totalCount; const total = totalCount != null ? Number(totalCount) : (items?.length || 0);
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
if (pageCount <= 1) return; if (pageCount <= 1) return;
@@ -356,12 +360,11 @@ class FillSpace extends Component {
} }
_handleWheel = (event) => { _handleWheel = (event) => {
const { items, totalCount, unfilteredCount } = this.props; const { items, totalCount } = this.props;
if (!Array.isArray(items)) return; if (!Array.isArray(items)) return;
if (this.ref && event.target && !this.ref.contains(event.target)) return; if (this.ref && event.target && !this.ref.contains(event.target)) return;
const { pageSize, currentPage } = this.state; const { pageSize, currentPage } = this.state;
const explicitTotal = unfilteredCount ?? totalCount; const total = totalCount != null ? Number(totalCount) : items.length;
const total = explicitTotal != null ? Number(explicitTotal) : items.length;
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
const delta = event.deltaY || 0; const delta = event.deltaY || 0;
if (pageCount <= 1) return; if (pageCount <= 1) return;
@@ -380,11 +383,10 @@ class FillSpace extends Component {
}; };
_goToPage = (page) => { _goToPage = (page) => {
const { items, onPageChange, totalCount, unfilteredCount } = this.props; const { items, onPageChange, totalCount } = this.props;
const explicitTotal = unfilteredCount ?? totalCount;
const total = const total =
explicitTotal != null totalCount != null
? Number(explicitTotal) ? Number(totalCount)
: Array.isArray(items) : Array.isArray(items)
? items.length ? items.length
: 0; : 0;
@@ -408,6 +410,7 @@ class FillSpace extends Component {
maxPageSize, maxPageSize,
totalCount, totalCount,
unfilteredCount, unfilteredCount,
unfilteredTotal: propUnfilteredTotal,
listRef, listRef,
onPageChange, onPageChange,
onWheel, onWheel,
@@ -428,9 +431,10 @@ class FillSpace extends Component {
// type or callback). `items` may be undefined while data is loading. // type or callback). `items` may be undefined while data is loading.
const { pageSize, currentPage, viewportHeight } = this.state; const { pageSize, currentPage, viewportHeight } = this.state;
const list = Array.isArray(items) ? items : []; const list = Array.isArray(items) ? items : [];
const explicitTotal = unfilteredCount ?? totalCount; const total = totalCount != null ? Number(totalCount) : list.length;
const total = explicitTotal != null ? Number(explicitTotal) : list.length; const rawUnfiltered = propUnfilteredTotal ?? unfilteredCount;
const fullTotal = Math.max(this.maxSeenCount || 0, total); const unfilteredTotal = rawUnfiltered != null ? Number(rawUnfiltered) : null;
const fullTotal = unfilteredTotal != null ? unfilteredTotal : Math.max(this.maxSeenCount || 0, total);
const needsPaginationContainer = fullTotal > pageSize; const needsPaginationContainer = fullTotal > pageSize;
const showPaginationControls = total > pageSize; const showPaginationControls = total > pageSize;
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));