u
This commit is contained in:
@@ -20,6 +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} [unfilteredCount] - Alias for totalCount.
|
||||
* @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} [debug=false] - Dashed outline for layout debugging.
|
||||
* @prop {object} [sx] - Extra styles.
|
||||
*/
|
||||
@@ -56,6 +57,10 @@ function renderProbeElement(renderItem) {
|
||||
return <Row brand={undefined} item={undefined} index={0} disabled />;
|
||||
}
|
||||
|
||||
const MIN_THUMB_HEIGHT = 48;
|
||||
const RACEWAY_WIDTH = 16;
|
||||
const RACEWAY_BOTTOM_MARGIN = 8;
|
||||
|
||||
class FillSpace extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -66,12 +71,20 @@ class FillSpace extends Component {
|
||||
viewportHeight: null,
|
||||
pageSize: Math.min(maxSize, minSize),
|
||||
currentPage: 1,
|
||||
scrollTop: 0,
|
||||
isDragging: false,
|
||||
};
|
||||
this.ref = null;
|
||||
this.trackRef = null;
|
||||
this.thumbRef = null;
|
||||
this.probeRef = null;
|
||||
this.wheelAcc = 0;
|
||||
this.observer = null;
|
||||
this.raf = null;
|
||||
this.scrollRaf = null;
|
||||
this.pendingScrollTop = 0;
|
||||
this.dragStartY = 0;
|
||||
this.dragStartScrollTop = 0;
|
||||
const explicitCount = props.unfilteredCount ?? props.totalCount;
|
||||
this.maxSeenCount =
|
||||
explicitCount != null
|
||||
@@ -138,20 +151,122 @@ 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() {
|
||||
if (this.state.isDragging) {
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
window.removeEventListener('mousemove', this._onThumbDragMove);
|
||||
window.removeEventListener('mouseup', this._onThumbDragEnd);
|
||||
}
|
||||
window.removeEventListener('resize', this._onViewportChange);
|
||||
if (this.raf) {
|
||||
cancelAnimationFrame(this.raf);
|
||||
this.raf = null;
|
||||
}
|
||||
if (this.scrollRaf) {
|
||||
cancelAnimationFrame(this.scrollRaf);
|
||||
this.scrollRaf = null;
|
||||
}
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_onThumbMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dragStartY = e.clientY;
|
||||
this.dragStartScrollTop = this.state.scrollTop;
|
||||
this.setState({ isDragging: true });
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'grabbing';
|
||||
window.addEventListener('mousemove', this._onThumbDragMove);
|
||||
window.addEventListener('mouseup', this._onThumbDragEnd);
|
||||
};
|
||||
|
||||
_onThumbDragMove = (e) => {
|
||||
if (!this.state.isDragging) return;
|
||||
const deltaY = e.clientY - this.dragStartY;
|
||||
const { viewportHeight, unitHeight } = this.state;
|
||||
const { items, totalCount, unfilteredCount } = this.props;
|
||||
const explicitTotal = unfilteredCount ?? totalCount;
|
||||
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
|
||||
const totalHeight = total * unitHeight;
|
||||
const maxScrollTop = Math.max(0, totalHeight - (viewportHeight || 0));
|
||||
if (maxScrollTop <= 0) return;
|
||||
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;
|
||||
const thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, (viewportHeight / totalHeight) * trackHeight));
|
||||
const maxThumbTravel = trackHeight - thumbHeight;
|
||||
if (maxThumbTravel <= 0) return;
|
||||
const scrollDelta = (deltaY / maxThumbTravel) * maxScrollTop;
|
||||
const nextScrollTop = Math.max(0, Math.min(maxScrollTop, this.dragStartScrollTop + scrollDelta));
|
||||
if (this.ref) {
|
||||
this.ref.scrollTop = nextScrollTop;
|
||||
}
|
||||
this.setState({ scrollTop: nextScrollTop });
|
||||
};
|
||||
|
||||
_onThumbDragEnd = () => {
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
window.removeEventListener('mousemove', this._onThumbDragMove);
|
||||
window.removeEventListener('mouseup', this._onThumbDragEnd);
|
||||
this.setState({ isDragging: false });
|
||||
};
|
||||
|
||||
_handleTrackMouseDown = (e) => {
|
||||
if (e.target === this.thumbRef || this.thumbRef?.contains(e.target)) return;
|
||||
if (!this.trackRef) return;
|
||||
const trackRect = this.trackRef.getBoundingClientRect();
|
||||
const clickY = e.clientY - trackRect.top;
|
||||
const { viewportHeight, unitHeight } = this.state;
|
||||
const { items, totalCount, unfilteredCount } = this.props;
|
||||
const explicitTotal = unfilteredCount ?? totalCount;
|
||||
const total = explicitTotal != null ? Number(explicitTotal) : (items?.length || 0);
|
||||
const totalHeight = total * unitHeight;
|
||||
const maxScrollTop = Math.max(0, totalHeight - (viewportHeight || 0));
|
||||
if (maxScrollTop <= 0) return;
|
||||
const trackHeight = trackRect.height;
|
||||
if (trackHeight <= 0) return;
|
||||
const thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, (viewportHeight / totalHeight) * trackHeight));
|
||||
const maxThumbTravel = trackHeight - thumbHeight;
|
||||
const targetThumbTop = Math.max(0, Math.min(maxThumbTravel, clickY - thumbHeight / 2));
|
||||
const targetScrollTop = maxThumbTravel > 0 ? (targetThumbTop / maxThumbTravel) * maxScrollTop : 0;
|
||||
if (this.ref) {
|
||||
this.ref.scrollTop = targetScrollTop;
|
||||
}
|
||||
this.setState({ scrollTop: targetScrollTop });
|
||||
};
|
||||
|
||||
_onViewportChange = () => {
|
||||
if (this.raf) return;
|
||||
this.raf = requestAnimationFrame(() => {
|
||||
@@ -195,6 +310,21 @@ class FillSpace extends Component {
|
||||
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. */
|
||||
_probeMeasure() {
|
||||
const firstRow = this.ref?.querySelector('[data-fill-row]');
|
||||
@@ -247,6 +377,36 @@ 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) => {
|
||||
const { items, totalCount, unfilteredCount } = this.props;
|
||||
if (!Array.isArray(items)) return;
|
||||
@@ -272,7 +432,7 @@ class FillSpace extends Component {
|
||||
};
|
||||
|
||||
_goToPage = (page) => {
|
||||
const { items, onPageChange, totalCount, unfilteredCount } = this.props;
|
||||
const { items, onPageChange, totalCount, unfilteredCount, virtualScrollbar } = this.props;
|
||||
const explicitTotal = unfilteredCount ?? totalCount;
|
||||
const total =
|
||||
explicitTotal != null
|
||||
@@ -282,6 +442,16 @@ class FillSpace extends Component {
|
||||
: 0;
|
||||
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);
|
||||
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;
|
||||
this.setState({ currentPage: next });
|
||||
onPageChange?.(next, { pageCount, pageSize: this.state.pageSize, total });
|
||||
@@ -303,6 +473,8 @@ class FillSpace extends Component {
|
||||
listRef,
|
||||
onPageChange,
|
||||
onWheel,
|
||||
virtualScrollbar = false,
|
||||
racewayMarginBottom,
|
||||
debug = false,
|
||||
items,
|
||||
renderItem,
|
||||
@@ -314,7 +486,7 @@ class FillSpace extends Component {
|
||||
// FillSpace owns current page, wheel-pagination and the pagination header.
|
||||
// Caller feeds `items` plus one row element via `renderItem` (component
|
||||
// type or callback). `items` may be undefined while data is loading.
|
||||
const { pageSize, currentPage, viewportHeight } = this.state;
|
||||
const { pageSize, currentPage, viewportHeight, unitHeight, scrollTop } = this.state;
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
const explicitTotal = unfilteredCount ?? totalCount;
|
||||
const total = explicitTotal != null ? Number(explicitTotal) : list.length;
|
||||
@@ -322,9 +494,62 @@ class FillSpace extends Component {
|
||||
const needsPaginationContainer = fullTotal > pageSize;
|
||||
const showPaginationControls = total > pageSize;
|
||||
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 = list.slice(offset, offset + pageSize);
|
||||
|
||||
const currentFirstIndex =
|
||||
virtualScrollbar && unitHeight > 0
|
||||
? 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 =
|
||||
virtualScrollbar &&
|
||||
viewportHeight > 0 &&
|
||||
unitHeight > 0 &&
|
||||
total * unitHeight > viewportHeight;
|
||||
|
||||
const racewayBottom = Math.max(0, Number(racewayMarginBottom ?? RACEWAY_BOTTOM_MARGIN) || 0);
|
||||
|
||||
let thumbHeight = 0;
|
||||
let thumbTop = 0;
|
||||
|
||||
if (canScroll) {
|
||||
const trackHeight = Math.max(0, (viewportHeight || 0) - racewayBottom);
|
||||
const fullHeight = total * unitHeight;
|
||||
thumbHeight = Math.max(MIN_THUMB_HEIGHT, Math.min(trackHeight, (viewportHeight / fullHeight) * trackHeight));
|
||||
const maxScrollTop = Math.max(0, fullHeight - viewportHeight);
|
||||
const maxThumbTravel = trackHeight - thumbHeight;
|
||||
thumbTop = maxScrollTop > 0 ? (scrollTop / maxScrollTop) * maxThumbTravel : 0;
|
||||
}
|
||||
|
||||
// Hidden data-free probe: measures `renderItem` itself (no data).
|
||||
const probe = renderProbeElement(renderItem);
|
||||
|
||||
@@ -356,8 +581,7 @@ class FillSpace extends Component {
|
||||
noWrap
|
||||
sx={{ flexShrink: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{total === 0 ? '0' : `${offset + 1}–${Math.min(offset + pageSize, total)}`} von{' '}
|
||||
{total}
|
||||
{total === 0 ? '0' : `${countStart}–${countEnd}`} von {total}
|
||||
</Typography>
|
||||
<Pagination
|
||||
count={pageCount}
|
||||
@@ -385,62 +609,138 @@ class FillSpace extends Component {
|
||||
) : null}
|
||||
{header}
|
||||
<Box
|
||||
ref={(el) => {
|
||||
this.ref = el;
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
this._handleWheel(e);
|
||||
onWheel?.(e);
|
||||
}}
|
||||
{...rest}
|
||||
sx={[
|
||||
{
|
||||
position: 'relative',
|
||||
height: viewportHeight ?? 'auto',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
// Keep wheel ticks from scrolling the page behind the list.
|
||||
// (No preventDefault needed — React wheel is passive.)
|
||||
overscrollBehavior: 'contain',
|
||||
},
|
||||
sx,
|
||||
debug
|
||||
? (theme) => ({
|
||||
outline: `2px dashed ${theme.palette.primary.main}`,
|
||||
})
|
||||
: undefined,
|
||||
sx,
|
||||
]}
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<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
|
||||
ref={(el) => {
|
||||
this.ref = el;
|
||||
}}
|
||||
onScroll={virtualScrollbar ? this._handleScroll : undefined}
|
||||
onWheel={(e) => {
|
||||
if (!virtualScrollbar) {
|
||||
this._handleWheel(e);
|
||||
}
|
||||
onWheel?.(e);
|
||||
}}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflowY: virtualScrollbar ? 'auto' : 'hidden',
|
||||
overflowX: 'hidden',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={(el) => {
|
||||
this.probeRef = el;
|
||||
}}
|
||||
aria-hidden
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
pr: canScroll ? `${RACEWAY_WIDTH + 8}px` : 0,
|
||||
boxSizing: 'border-box',
|
||||
...(virtualScrollbar && unitHeight > 0
|
||||
? {
|
||||
paddingTop: `${paddingTop}px`,
|
||||
paddingBottom: `${paddingBottom}px`,
|
||||
minHeight: totalContentHeight,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<List dense disablePadding sx={{ width: '100%' }}>
|
||||
<Box data-fill-row sx={{ width: '100%' }}>
|
||||
{probe}
|
||||
</Box>
|
||||
<List ref={listRef} dense disablePadding sx={{ width: '100%' }}>
|
||||
{renderedItems.map((item, i) => (
|
||||
<Box key={item?.id ?? itemOffset + i} data-fill-row sx={{ width: '100%' }}>
|
||||
{renderRowElement(renderItem, item, itemOffset + i)}
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
{probe != 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 ? (
|
||||
<Box
|
||||
ref={(el) => {
|
||||
this.trackRef = el;
|
||||
}}
|
||||
onMouseDown={this._handleTrackMouseDown}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 3,
|
||||
bottom: `${racewayBottom}px`,
|
||||
width: RACEWAY_WIDTH,
|
||||
zIndex: 5,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 8,
|
||||
bgcolor: (theme) => alpha(theme.palette.text.primary, 0.04),
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
bgcolor: (theme) => alpha(theme.palette.text.primary, 0.09),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={(el) => {
|
||||
this.thumbRef = el;
|
||||
}}
|
||||
onMouseDown={this._onThumbMouseDown}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: thumbTop,
|
||||
height: thumbHeight,
|
||||
left: 2,
|
||||
right: 2,
|
||||
borderRadius: 6,
|
||||
bgcolor: (theme) =>
|
||||
this.state.isDragging
|
||||
? alpha(theme.palette.primary.main, 0.7)
|
||||
: alpha(theme.palette.text.primary, 0.3),
|
||||
cursor: this.state.isDragging ? 'grabbing' : 'grab',
|
||||
transition: this.state.isDragging ? 'none' : 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
bgcolor: (theme) =>
|
||||
this.state.isDragging
|
||||
? alpha(theme.palette.primary.main, 0.7)
|
||||
: alpha(theme.palette.text.primary, 0.55),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
{debug ? (
|
||||
|
||||
@@ -34,6 +34,7 @@ class FillSpaceMockPage extends Component {
|
||||
this.ex1 = { header: createRef(), footer: createRef() };
|
||||
this.ex2 = { header: createRef(), footer: createRef() };
|
||||
this.ex3 = { header: createRef(), footer: createRef() };
|
||||
this.exVirtual = { header: createRef(), footer: createRef() };
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -138,6 +139,37 @@ class FillSpaceMockPage extends Component {
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Example 4: Virtual Scrollbar */}
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Virtual Scrollbar (smooth continuous scroll with scrollbar)
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
ref={this.exVirtual.header}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mb: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Header</Typography>
|
||||
</Box>
|
||||
|
||||
<FillSpace
|
||||
aboveRef={this.exVirtual.header}
|
||||
belowRef={this.exVirtual.footer}
|
||||
items={SAMPLE_ITEMS}
|
||||
renderItem={Row}
|
||||
minPageSize={5}
|
||||
virtualScrollbar
|
||||
/>
|
||||
|
||||
<Box
|
||||
ref={this.exVirtual.footer}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mt: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Footer</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<ComponentDocs name="FillSpace" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user