Compare commits

...

3 Commits

Author SHA1 Message Date
seb
c735ba9c65 u 2026-10-12 02:13:39 +02:00
seb
f49dd4b00f u 2026-10-12 01:52:43 +02:00
seb
c3fd05b424 u 2026-10-12 01:51:08 +02:00
3 changed files with 107 additions and 234 deletions

View File

@@ -20,7 +20,7 @@ import { alpha } from '@mui/material/styles';
* @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 unfiltered dataset. When unfiltered count <= pageSize, pagination header is removed entirely. When filtering, the header height is preserved to prevent layout shift.
* @prop {number} [unfilteredCount] - Alias for totalCount. * @prop {number} [unfilteredCount] - Alias for totalCount.
* @prop {(page: number) => void} [onPageChange] - Called on page change. * @prop {(page: number) => void} [onPageChange] - Called on page change.
* @prop {boolean} [virtualScrollbar=false] - Enables smooth continuous virtual scrolling with a scrollbar synchronized with the pagination controls. * @prop {boolean} [virtualScrollbar=false] - Shows a synchronized scrollbar alongside pagination controls.
* @prop {boolean} [debug=false] - Dashed outline for layout debugging. * @prop {boolean} [debug=false] - Dashed outline for layout debugging.
* @prop {object} [sx] - Extra styles. * @prop {object} [sx] - Extra styles.
*/ */
@@ -71,8 +71,8 @@ class FillSpace extends Component {
viewportHeight: null, viewportHeight: null,
pageSize: Math.min(maxSize, minSize), pageSize: Math.min(maxSize, minSize),
currentPage: 1, currentPage: 1,
scrollTop: 0,
isDragging: false, isDragging: false,
dragThumbTop: 0,
}; };
this.ref = null; this.ref = null;
this.trackRef = null; this.trackRef = null;
@@ -81,10 +81,8 @@ class FillSpace extends Component {
this.wheelAcc = 0; this.wheelAcc = 0;
this.observer = null; this.observer = null;
this.raf = null; this.raf = null;
this.scrollRaf = null;
this.pendingScrollTop = 0;
this.dragStartY = 0; this.dragStartY = 0;
this.dragStartScrollTop = 0; this.dragStartThumbTop = 0;
const explicitCount = props.unfilteredCount ?? props.totalCount; const explicitCount = props.unfilteredCount ?? props.totalCount;
this.maxSeenCount = this.maxSeenCount =
explicitCount != null explicitCount != null
@@ -151,30 +149,6 @@ class FillSpace extends Component {
}); });
} }
} }
if (
this.props.virtualScrollbar &&
(prevProps.items !== this.props.items ||
prevProps.totalCount !== this.props.totalCount ||
prevProps.unfilteredCount !== this.props.unfilteredCount)
) {
const explicitTotal = this.props.unfilteredCount ?? this.props.totalCount;
const total =
explicitTotal != null
? Number(explicitTotal)
: Array.isArray(this.props.items)
? this.props.items.length
: 0;
const maxScrollTop = Math.max(
0,
total * this.state.unitHeight - (this.state.viewportHeight || 0),
);
if (this.state.scrollTop > maxScrollTop) {
if (this.ref && this.ref.scrollTop !== maxScrollTop) {
this.ref.scrollTop = maxScrollTop;
}
this.setState({ scrollTop: maxScrollTop });
}
}
} }
componentWillUnmount() { componentWillUnmount() {
@@ -189,10 +163,6 @@ class FillSpace extends Component {
cancelAnimationFrame(this.raf); cancelAnimationFrame(this.raf);
this.raf = null; this.raf = null;
} }
if (this.scrollRaf) {
cancelAnimationFrame(this.scrollRaf);
this.scrollRaf = null;
}
if (this.observer) { if (this.observer) {
this.observer.disconnect(); this.observer.disconnect();
this.observer = null; this.observer = null;
@@ -202,9 +172,20 @@ class FillSpace extends Component {
_onThumbMouseDown = (e) => { _onThumbMouseDown = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const { totalCount, unfilteredCount, items } = this.props;
const { viewportHeight, pageSize, currentPage } = this.state;
const explicitTotal = unfilteredCount ?? totalCount;
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
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 trackHeight = Math.max(0, (viewportHeight || 0) - racewayBottom);
const thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, total > 0 ? (pageSize / total) * trackHeight : trackHeight));
const maxThumbTravel = Math.max(0, trackHeight - thumbHeight);
const currentThumbTop = pageCount > 1 && maxThumbTravel > 0 ? ((currentPage - 1) / (pageCount - 1)) * maxThumbTravel : 0;
this.dragStartY = e.clientY; this.dragStartY = e.clientY;
this.dragStartScrollTop = this.state.scrollTop; this.dragStartThumbTop = currentThumbTop;
this.setState({ isDragging: true }); this.setState({ isDragging: true, dragThumbTop: currentThumbTop });
document.body.style.userSelect = 'none'; document.body.style.userSelect = 'none';
document.body.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing';
window.addEventListener('mousemove', this._onThumbDragMove); window.addEventListener('mousemove', this._onThumbDragMove);
@@ -213,26 +194,27 @@ class FillSpace extends Component {
_onThumbDragMove = (e) => { _onThumbDragMove = (e) => {
if (!this.state.isDragging) return; if (!this.state.isDragging) return;
const deltaY = e.clientY - this.dragStartY; const { totalCount, unfilteredCount, items } = this.props;
const { viewportHeight, unitHeight } = this.state; const { viewportHeight, pageSize } = this.state;
const { items, totalCount, unfilteredCount } = this.props;
const explicitTotal = unfilteredCount ?? totalCount; const explicitTotal = unfilteredCount ?? totalCount;
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0); const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
const totalHeight = total * unitHeight; const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
const maxScrollTop = Math.max(0, totalHeight - (viewportHeight || 0)); if (pageCount <= 1) return;
if (maxScrollTop <= 0) return;
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);
if (trackHeight <= 0) return; const thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, total > 0 ? (pageSize / total) * trackHeight : trackHeight));
const thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, (viewportHeight / totalHeight) * trackHeight)); const maxThumbTravel = Math.max(0, trackHeight - thumbHeight);
const maxThumbTravel = trackHeight - thumbHeight;
if (maxThumbTravel <= 0) return; if (maxThumbTravel <= 0) return;
const scrollDelta = (deltaY / maxThumbTravel) * maxScrollTop;
const nextScrollTop = Math.max(0, Math.min(maxScrollTop, this.dragStartScrollTop + scrollDelta)); const deltaY = e.clientY - this.dragStartY;
if (this.ref) { const nextThumbTop = Math.max(0, Math.min(maxThumbTravel, this.dragStartThumbTop + deltaY));
this.ref.scrollTop = nextScrollTop; const targetPage = Math.min(pageCount, Math.max(1, Math.round(1 + (nextThumbTop / maxThumbTravel) * (pageCount - 1))));
this.setState({ dragThumbTop: nextThumbTop });
if (targetPage !== this.state.currentPage) {
this._goToPage(targetPage);
} }
this.setState({ scrollTop: nextScrollTop });
}; };
_onThumbDragEnd = () => { _onThumbDragEnd = () => {
@@ -248,23 +230,33 @@ class FillSpace extends Component {
if (!this.trackRef) return; if (!this.trackRef) return;
const trackRect = this.trackRef.getBoundingClientRect(); const trackRect = this.trackRef.getBoundingClientRect();
const clickY = e.clientY - trackRect.top; const clickY = e.clientY - trackRect.top;
const { viewportHeight, unitHeight } = this.state; const { viewportHeight, pageSize } = this.state;
const { items, totalCount, unfilteredCount } = this.props; const { items, totalCount, unfilteredCount } = this.props;
const explicitTotal = unfilteredCount ?? totalCount; const explicitTotal = unfilteredCount ?? totalCount;
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0); const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
const totalHeight = total * unitHeight; const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
const maxScrollTop = Math.max(0, totalHeight - (viewportHeight || 0)); if (pageCount <= 1) return;
if (maxScrollTop <= 0) return;
const trackHeight = trackRect.height; const racewayBottom = Math.max(0, Number(this.props.racewayMarginBottom ?? RACEWAY_BOTTOM_MARGIN) || 0);
const trackHeight = Math.max(0, (viewportHeight || 0) - racewayBottom);
if (trackHeight <= 0) return; if (trackHeight <= 0) return;
const thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, (viewportHeight / totalHeight) * trackHeight)); const thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, total > 0 ? (pageSize / total) * trackHeight : trackHeight));
const maxThumbTravel = trackHeight - thumbHeight; const maxThumbTravel = Math.max(0, trackHeight - thumbHeight);
const targetThumbTop = Math.max(0, Math.min(maxThumbTravel, clickY - thumbHeight / 2)); const targetThumbTop = Math.max(0, Math.min(maxThumbTravel, clickY - thumbHeight / 2));
const targetScrollTop = maxThumbTravel > 0 ? (targetThumbTop / maxThumbTravel) * maxScrollTop : 0; const targetPage = maxThumbTravel > 0 ? Math.min(pageCount, Math.max(1, Math.round(1 + (targetThumbTop / maxThumbTravel) * (pageCount - 1)))) : 1;
if (this.ref) { this._goToPage(targetPage);
this.ref.scrollTop = targetScrollTop; };
scrollToTop = () => {
this._goToPage(1);
};
scrollToIndex = (index) => {
const { pageSize } = this.state;
if (pageSize > 0) {
const targetPage = Math.floor(index / pageSize) + 1;
this._goToPage(targetPage);
} }
this.setState({ scrollTop: targetScrollTop });
}; };
_onViewportChange = () => { _onViewportChange = () => {
@@ -310,21 +302,6 @@ class FillSpace extends Component {
this._viewportMeasure(); this._viewportMeasure();
}; };
scrollToTop = () => {
if (this.ref) {
this.ref.scrollTop = 0;
}
this.setState({ scrollTop: 0, currentPage: 1 });
};
scrollToIndex = (index) => {
if (this.ref && this.state.unitHeight > 0) {
const top = Math.max(0, index * this.state.unitHeight);
this.ref.scrollTop = top;
this.setState({ scrollTop: top });
}
};
/** Only measures the height of the `renderItem` probe. Nothing else. */ /** Only measures the height of the `renderItem` probe. Nothing else. */
_probeMeasure() { _probeMeasure() {
const firstRow = this.ref?.querySelector('[data-fill-row]'); const firstRow = this.ref?.querySelector('[data-fill-row]');
@@ -377,36 +354,6 @@ class FillSpace extends Component {
} }
} }
_handleScroll = (event) => {
const st = event.currentTarget.scrollTop;
this.pendingScrollTop = st;
if (this.scrollRaf) return;
this.scrollRaf = requestAnimationFrame(() => {
this.scrollRaf = null;
const nextSt = this.pendingScrollTop;
if (nextSt != null && nextSt !== this.state.scrollTop) {
const { unitHeight, pageSize } = this.state;
const currentFirst = unitHeight > 0 ? Math.floor(nextSt / unitHeight) : 0;
const newPage = pageSize > 0 ? Math.floor(currentFirst / pageSize) + 1 : 1;
const updates = { scrollTop: nextSt };
if (newPage !== this.state.currentPage) {
updates.currentPage = newPage;
const { items, onPageChange, totalCount, unfilteredCount } = this.props;
const explicitTotal = unfilteredCount ?? totalCount;
const total =
explicitTotal != null
? Number(explicitTotal)
: Array.isArray(items)
? items.length
: 0;
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
onPageChange?.(newPage, { pageCount, pageSize, total });
}
this.setState(updates);
}
});
};
_handleWheel = (event) => { _handleWheel = (event) => {
const { items, totalCount, unfilteredCount } = this.props; const { items, totalCount, unfilteredCount } = this.props;
if (!Array.isArray(items)) return; if (!Array.isArray(items)) return;
@@ -432,7 +379,7 @@ class FillSpace extends Component {
}; };
_goToPage = (page) => { _goToPage = (page) => {
const { items, onPageChange, totalCount, unfilteredCount, virtualScrollbar } = this.props; const { items, onPageChange, totalCount, unfilteredCount } = this.props;
const explicitTotal = unfilteredCount ?? totalCount; const explicitTotal = unfilteredCount ?? totalCount;
const total = const total =
explicitTotal != null explicitTotal != null
@@ -442,16 +389,6 @@ class FillSpace extends Component {
: 0; : 0;
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, this.state.pageSize))); const pageCount = Math.max(1, Math.ceil(total / Math.max(1, this.state.pageSize)));
const next = Math.min(Math.max(1, Number(page) || 1), pageCount); const next = Math.min(Math.max(1, Number(page) || 1), pageCount);
if (virtualScrollbar) {
const targetScrollTop = (next - 1) * this.state.pageSize * this.state.unitHeight;
if (this.ref) {
this.ref.scrollTop = targetScrollTop;
}
if (next === this.state.currentPage && targetScrollTop === this.state.scrollTop) return;
this.setState({ currentPage: next, scrollTop: targetScrollTop });
onPageChange?.(next, { pageCount, pageSize: this.state.pageSize, total });
return;
}
if (next === this.state.currentPage) return; if (next === this.state.currentPage) return;
this.setState({ currentPage: next }); this.setState({ currentPage: next });
onPageChange?.(next, { pageCount, pageSize: this.state.pageSize, total }); onPageChange?.(next, { pageCount, pageSize: this.state.pageSize, total });
@@ -487,7 +424,7 @@ class FillSpace extends Component {
// FillSpace owns current page, wheel-pagination and the pagination header. // FillSpace owns current page, wheel-pagination and the pagination header.
// Caller feeds `items` plus one row element via `renderItem` (component // Caller feeds `items` plus one row element via `renderItem` (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, unitHeight, scrollTop } = 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 explicitTotal = unfilteredCount ?? totalCount;
const total = explicitTotal != null ? Number(explicitTotal) : list.length; const total = explicitTotal != null ? Number(explicitTotal) : list.length;
@@ -495,47 +432,14 @@ class FillSpace extends Component {
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)));
const safePage = Math.min(Math.max(1, currentPage), pageCount);
const currentFirstIndex = const offset = (safePage - 1) * pageSize;
virtualScrollbar && unitHeight > 0 const pageItems = list.slice(offset, offset + pageSize);
? Math.min(Math.max(0, total - 1), Math.floor(scrollTop / unitHeight))
: (Math.min(Math.max(1, currentPage), pageCount) - 1) * pageSize;
const safePage =
virtualScrollbar && pageSize > 0
? Math.min(pageCount, Math.floor(currentFirstIndex / pageSize) + 1)
: Math.min(Math.max(1, currentPage), pageCount);
const countStart = total === 0 ? 0 : currentFirstIndex + 1;
const countEnd = Math.min(total, currentFirstIndex + pageSize);
let renderedItems;
let itemOffset;
let paddingTop = 0;
let paddingBottom = 0;
let totalContentHeight;
if (virtualScrollbar && unitHeight > 0) {
const overscan = 3;
const rawStartIndex = Math.floor(scrollTop / unitHeight);
const startIndex = Math.max(0, rawStartIndex - overscan);
const visibleCount = Math.ceil((viewportHeight || 0) / unitHeight);
const endIndex = Math.min(total, rawStartIndex + visibleCount + overscan);
renderedItems = list.slice(startIndex, endIndex);
itemOffset = startIndex;
paddingTop = startIndex * unitHeight;
paddingBottom = Math.max(0, (total - endIndex) * unitHeight);
totalContentHeight = total * unitHeight;
} else {
const offset = (safePage - 1) * pageSize;
renderedItems = list.slice(offset, offset + pageSize);
itemOffset = offset;
}
const canScroll = const canScroll =
virtualScrollbar && virtualScrollbar &&
viewportHeight > 0 && total > pageSize &&
unitHeight > 0 && (viewportHeight || 0) > 0;
total * unitHeight > viewportHeight;
const racewayBottom = Math.max(0, Number(racewayMarginBottom ?? RACEWAY_BOTTOM_MARGIN) || 0); const racewayBottom = Math.max(0, Number(racewayMarginBottom ?? RACEWAY_BOTTOM_MARGIN) || 0);
const racewayRightOffset = racewayRight != null ? racewayRight : -(RACEWAY_WIDTH - 8); const racewayRightOffset = racewayRight != null ? racewayRight : -(RACEWAY_WIDTH - 8);
@@ -545,11 +449,11 @@ class FillSpace extends Component {
if (canScroll) { if (canScroll) {
const trackHeight = Math.max(0, (viewportHeight || 0) - racewayBottom); const trackHeight = Math.max(0, (viewportHeight || 0) - racewayBottom);
const fullHeight = total * unitHeight; thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, total > 0 ? (pageSize / total) * trackHeight : trackHeight));
thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, (viewportHeight / fullHeight) * trackHeight)); const maxThumbTravel = Math.max(0, trackHeight - thumbHeight);
const maxScrollTop = Math.max(0, fullHeight - viewportHeight); thumbTop = this.state.isDragging
const maxThumbTravel = trackHeight - thumbHeight; ? this.state.dragThumbTop
thumbTop = maxScrollTop > 0 ? (scrollTop / maxScrollTop) * maxThumbTravel : 0; : (pageCount > 1 && maxThumbTravel > 0 ? ((safePage - 1) / (pageCount - 1)) * maxThumbTravel : 0);
} }
// Hidden data-free probe: measures `renderItem` itself (no data). // Hidden data-free probe: measures `renderItem` itself (no data).
@@ -583,7 +487,8 @@ class FillSpace extends Component {
noWrap noWrap
sx={{ flexShrink: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }} sx={{ flexShrink: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}
> >
{total === 0 ? '0' : `${countStart}–${countEnd}`} von {total} {total === 0 ? '0' : `${offset + 1}–${Math.min(offset + pageSize, total)}`} von{' '}
{total}
</Typography> </Typography>
<Pagination <Pagination
count={pageCount} count={pageCount}
@@ -611,88 +516,69 @@ class FillSpace extends Component {
) : null} ) : null}
{header} {header}
<Box <Box
ref={(el) => {
this.ref = el;
}}
onWheel={(e) => {
this._handleWheel(e);
onWheel?.(e);
}}
{...rest} {...rest}
sx={[ sx={[
{ {
position: 'relative', position: 'relative',
height: viewportHeight ?? 'auto', height: viewportHeight ?? 'auto',
overflow: 'hidden',
width: '100%', width: '100%',
// Keep wheel ticks from scrolling the page behind the list.
// (No preventDefault needed — React wheel is passive.)
overscrollBehavior: 'contain', overscrollBehavior: 'contain',
}, },
sx,
debug debug
? (theme) => ({ ? (theme) => ({
outline: `2px dashed ${theme.palette.primary.main}`, outline: `2px dashed ${theme.palette.primary.main}`,
}) })
: undefined, : undefined,
sx,
]} ]}
> >
<Box <Box
ref={(el) => {
this.ref = el;
}}
onScroll={virtualScrollbar ? this._handleScroll : undefined}
onWheel={(e) => {
if (!virtualScrollbar) {
this._handleWheel(e);
}
onWheel?.(e);
}}
sx={{ sx={{
position: 'relative',
height: '100%',
width: '100%', width: '100%',
overflowY: virtualScrollbar ? 'auto' : 'hidden', pr: canScroll ? `${Math.max(0, RACEWAY_WIDTH + racewayRightOffset + 8)}px` : 0,
overflowX: 'hidden', boxSizing: 'border-box',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
}} }}
> >
<List ref={listRef} dense disablePadding sx={{ width: '100%' }}>
{pageItems.map((item, i) => (
<Box key={item?.id ?? offset + i} data-fill-row sx={{ width: '100%' }}>
{renderRowElement(renderItem, item, offset + i)}
</Box>
))}
</List>
</Box>
{probe != null ? (
<Box <Box
ref={(el) => {
this.probeRef = el;
}}
aria-hidden
sx={{ sx={{
position: 'absolute',
left: 0,
top: 0,
width: '100%', width: '100%',
pr: canScroll ? `${Math.max(0, RACEWAY_WIDTH + racewayRightOffset + 8)}px` : 0, visibility: 'hidden',
boxSizing: 'border-box', pointerEvents: 'none',
...(virtualScrollbar && unitHeight > 0
? {
paddingTop: `${paddingTop}px`,
paddingBottom: `${paddingBottom}px`,
minHeight: totalContentHeight,
}
: {}),
}} }}
> >
<List ref={listRef} dense disablePadding sx={{ width: '100%' }}> <List dense disablePadding sx={{ width: '100%' }}>
{renderedItems.map((item, i) => ( <Box data-fill-row sx={{ width: '100%' }}>
<Box key={item?.id ?? itemOffset + i} data-fill-row sx={{ width: '100%' }}> {probe}
{renderRowElement(renderItem, item, itemOffset + i)} </Box>
</Box>
))}
</List> </List>
</Box> </Box>
{probe != null ? ( ) : null}
<Box
ref={(el) => {
this.probeRef = el;
}}
aria-hidden
sx={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
visibility: 'hidden',
pointerEvents: 'none',
}}
>
<List dense disablePadding sx={{ width: '100%' }}>
<Box data-fill-row sx={{ width: '100%' }}>
{probe}
</Box>
</List>
</Box>
) : null}
</Box>
{canScroll ? ( {canScroll ? (
<Box <Box

View File

@@ -142,7 +142,7 @@ class FillSpaceMockPage extends Component {
{/* Example 4: Virtual Scrollbar */} {/* Example 4: Virtual Scrollbar */}
<Paper variant="outlined" sx={{ p: 2 }}> <Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="subtitle2" gutterBottom> <Typography variant="subtitle2" gutterBottom>
Virtual Scrollbar (smooth continuous scroll with scrollbar) Virtual Scrollbar (scrollbar synchronized with pagination)
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: 'column' }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box <Box

View File

@@ -35,7 +35,6 @@ import Tabs from '@mui/material/Tabs';
* @prop {any} [defaultValue] - Initial active tab value/index for uncontrolled mode. * @prop {any} [defaultValue] - Initial active tab value/index for uncontrolled mode.
* @prop {any} [defaultTab] - Alias for defaultValue. * @prop {any} [defaultTab] - Alias for defaultValue.
* @prop {(nextValue: any, event: React.SyntheticEvent) => void} [onChange] - Called on tab switch. * @prop {(nextValue: any, event: React.SyntheticEvent) => void} [onChange] - Called on tab switch.
* @prop {boolean} [keepMounted=true] - When true, inactive grids remain mounted in DOM with `display: none` to preserve scroll, refs, and sortable states.
* @prop {object} [sx] - Extra styles merged into root container Box. * @prop {object} [sx] - Extra styles merged into root container Box.
* @prop {object} [tabsWrapperSx] - Extra styles for the container around Tabs. * @prop {object} [tabsWrapperSx] - Extra styles for the container around Tabs.
* @prop {object} [tabsSx] - Extra styles merged into MUI Tabs. * @prop {object} [tabsSx] - Extra styles merged into MUI Tabs.
@@ -99,7 +98,6 @@ export class TabbedPanelTileGrid extends Component {
render() { render() {
const { const {
keepMounted = true,
sx, sx,
tabsWrapperSx, tabsWrapperSx,
tabsSx, tabsSx,
@@ -110,6 +108,7 @@ export class TabbedPanelTileGrid extends Component {
if (tabs.length === 0) return null; if (tabs.length === 0) return null;
const activeValue = this.getActiveValue(tabs); const activeValue = this.getActiveValue(tabs);
const activeTab = tabs.find((t) => t.value === activeValue) ?? tabs[0];
return ( return (
<Box sx={sx}> <Box sx={sx}>
@@ -183,19 +182,7 @@ export class TabbedPanelTileGrid extends Component {
</Tabs> </Tabs>
</Box> </Box>
{tabs.map((tab) => { {activeTab ? React.cloneElement(activeTab.child, { key: String(activeTab.value) }) : null}
const isSelected = tab.value === activeValue;
if (!isSelected && !keepMounted) {
return null;
}
return React.cloneElement(tab.child, {
key: String(tab.value),
sx: {
...tab.child.props.sx,
...(isSelected ? {} : { display: 'none !important' }),
},
});
})}
</Box> </Box>
); );
} }