@@ -14,8 +14,13 @@ import { alpha } from '@mui/material/styles';
*
* @prop {Array} items - Wheel flips pages.
* @prop {React.ComponentType|Function} renderItem - Row renderer (component or callback).
* @prop {React.ReactNode} [header] - Rendered below pagination controls and above the list items.
* @prop {number} [minPageSize=10] - 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} [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.
*/
@@ -52,20 +57,41 @@ 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 ) ;
const minSize = Math . max ( 1 , Number ( props . minPageSize ) || 10 ) ;
const maxSize = Number ( props . maxPageSize ) > 0 ? Number ( props . maxPageSize ) : Infinity ;
this . state = {
unitHeight : 0 ,
viewportHeight : null ,
pageSize : Number ( props . minPageSize ) || 10 ,
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
? Number ( explicitCount )
: Array . isArray ( props . items )
? props . items . length
: 0 ;
}
componentDidMount ( ) {
@@ -79,47 +105,168 @@ class FillSpace extends Component {
if ( prevProps . renderItem !== this . props . renderItem ) {
this . _probeMeasure ( ) ;
}
if ( prevProps . minPageSize !== this . props . minPageSize ) {
this . setState ( { pageSize : Number ( this . props . minPageSize ) || 10 } ) ;
if (
prevProps . minPageSize !== this . props . minPageSize ||
prevProps . maxPageSize !== this . props . maxPageSize
) {
const minSize = Math . max ( 1 , Number ( this . props . minPageSize ) || 10 ) ;
const maxSize = Number ( this . props . maxPageSize ) > 0 ? Number ( this . props . maxPageSize ) : Infinity ;
this . setState ( { pageSize : Math . min ( maxSize , minSize ) } ) ;
}
if (
prevProps . aboveRef !== this . props . aboveRef ||
prevProps . belowRef !== this . props . belowRef ||
prevProps . containerRef !== this . props . containerRef ||
prevProps . reserveBottom !== this . props . reserveBottom ||
prevProps . items !== this . props . items
prevProps . items !== this . props . items ||
prevProps . header !== this . props . header
) {
this . _observe ( ) ;
this . _viewportMeasure ( ) ;
}
const explicitCount = this . props . unfilteredCount ? ? this . props . totalCount ;
if ( explicitCount != null ) {
this . maxSeenCount = Number ( explicitCount ) ;
} else if ( Array . isArray ( this . props . items ) ) {
this . maxSeenCount = Math . max ( this . maxSeenCount || 0 , this . props . items . length ) ;
}
// Clamp current page when the item list shrinks (e.g. brand deleted).
if (
Array . isArray ( this . props . 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 (
1 ,
Math . ceil ( this . props . items . length / Math . max ( 1 , this . state . pageSize ) ) ,
Math . ceil ( total / Math . max ( 1 , this . state . pageSize ) ) ,
) ;
if ( this . state . currentPage > pageCount ) {
this . setState ( { currentPage : pageCount } ) ;
this . props . onPageChange ? . ( pageCount ) ;
this . props . onPageChange ? . ( pageCount , {
pageCount ,
pageSize : this . state . pageSize ,
total ,
} ) ;
}
}
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 ( ( ) => {
@@ -163,9 +310,25 @@ 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 h = this . probeR ef? . offsetHeight ;
const firstRow = this . r ef? . querySelector ( '[data-fill-row]' ) ;
const h = this . probeRef ? . offsetHeight || firstRow ? . offsetHeight ;
if ( h > 0 && h !== this . state . unitHeight ) {
this . setState ( { unitHeight : h } , ( ) => this . _viewportMeasure ( ) ) ;
}
@@ -197,23 +360,61 @@ class FillSpace extends Component {
available = Math . max ( 0 , window . innerHeight - rect . top - belowPx ) ;
}
const minSize = Math . max ( 1 , Number ( minPageSize ) || 10 ) ;
const maxSize = Number ( this . props . maxPageSize ) > 0 ? Number ( this . props . maxPageSize ) : Infinity ;
const { unitHeight } = this . state ;
const nextPageSize =
const calculated =
unitHeight > 0 ? Math . max ( minSize , Math . floor ( available / unitHeight ) ) : minSize ;
const nextPageSize = Math . min ( maxSize , calculated ) ;
const nextViewportHeight =
Number . isFinite ( maxSize ) && unitHeight > 0
? Math . min ( available , nextPageSize * unitHeight )
: available ;
if (
available !== this . state . viewportHeight ||
nextViewportHeight !== this . state . viewportHeight ||
nextPageSize !== this . state . pageSize
) {
this . setState ( { viewportHeight : available , pageSize : nextPageSize } ) ;
this . setState ( { viewportHeight : nextViewportHeight , pageSize : nextPageSize } ) ;
}
}
_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 } = this . props ;
const { items , totalCount , unfilteredCount } = this . props ;
if ( ! Array . isArray ( items ) ) return ;
if ( this . ref && event . target && ! this . ref . contains ( event . target ) ) return ;
const { pageSize , currentPage } = this . state ;
const pageCount = Math . max ( 1 , Math . ceil ( items . length / Math . max ( 1 , pageSize ) ) ) ;
const explicitTotal = unfilteredCount ? ? totalCount ;
const total = explicitTotal != null ? Number ( explicitTotal ) : items . length ;
const pageCount = Math . max ( 1 , Math . ceil ( total / Math . max ( 1 , pageSize ) ) ) ;
const delta = event . deltaY || 0 ;
if ( pageCount <= 1 ) return ;
if ( delta === 0 ) return ;
@@ -231,13 +432,29 @@ class FillSpace extends Component {
} ;
_goToPage = ( page ) => {
const { items , onPageChange } = this . props ;
const t otal = Array . isArray ( items ) ? items . length : 0 ;
const { items , onPageChange , totalCount , unfilteredCount , virtualScrollbar } = this . props ;
const explicitT otal = 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 , 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 ) ;
onPageChange ? . ( next , { pageCount , pageSize : this . state . pageSize , total } );
} ;
render ( ) {
@@ -250,11 +467,19 @@ class FillSpace extends Component {
minHeight ,
maxHeight ,
minPageSize ,
maxPageSize ,
totalCount ,
unfilteredCount ,
listRef ,
onPageChange ,
onWheel ,
virtualScrollbar = false ,
racewayMarginBottom ,
racewayRight ,
debug = false ,
items ,
renderItem ,
header ,
sx ,
... rest
} = this . props ;
@@ -262,130 +487,262 @@ 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 t otal = list . length ;
const explicitT otal = unfilteredCount ? ? totalCount ;
const total = explicitTotal != null ? Number ( explicitTotal ) : list . length ;
const fullTotal = Math . max ( this . maxSeenCount || 0 , total ) ;
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 , offse t + pageSize ) ;
const showPagination = total > pageSize ;
const currentFirstIndex =
virtualScrollbar && unitHeigh t > 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 ) ;
const racewayRightOffset = racewayRight != null ? racewayRight : - ( RACEWAY _WIDTH - 8 ) ;
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 ) ;
return (
< >
< Box
/ / Fixed single - line bar : the header height must not depend on the
/ / page count. Otherwise the first fit ( e.g. " 1 - 17 " ) pushes the
/ / spacer down , the available height shrinks , and a second pass
/ / flips to " 1 - 15 " . One pass : constant 44px , real values always
/ / rendered ( only visibility is gated ) , no placeholder numbers.
sx = { { mb : 1 , height : 44 , overflow : 'hidden' } }
>
{ needsPaginationContainer ? (
< Box
aria - hidden = { ! showPagination || undefined }
sx = { {
display : 'flex' ,
justifyContent : 'space-between' ,
alignItems : 'center' ,
flexWrap : 'nowrap' ,
gap : 1 ,
width : '100%' ,
height : 44 ,
visibility : showPagination ? 'visible' : 'hidden' ,
pointerEvents : showPagination ? 'auto' : 'none' ,
} }
/ / Fixed single - line bar : the header height must not depend on the
/ / filtered count so the content never shifts when filtering.
sx = { { mb : 1 , height : 44 , overflow : 'hidden' } }
>
< Typography
v ariant = "body2"
color = "text.secondary"
noWrap
sx = { { flexShrink : 1 , minWidth : 0 , overflow : 'hidden' , textOverflow : 'ellipsis' } }
>
{ total === 0 ? '0' : ` ${ offset + 1 } – ${ Math . min ( offset + pageSize , total ) } ` } von { ' ' }
{ total }
< / Typography >
< Pagination
count = { pageCount }
page = { safePage }
onChange = { ( _e , page ) => this . _goToPage ( page ) }
color = "primary"
size = "large"
onMouseDown = { ( e ) => e . preventDefault ( ) }
< Box
aria- hidden = { ! showPaginationControls || undefined }
sx = { {
flexShrink : 0 ,
maxWidth : '100% ',
overflowX : 'auto ' ,
scrollbarWidth : 'none ' ,
'&::-webkit-scrollbar' : { display : 'none' } ,
'& .MuiPagination-ul' : { flexWrap : 'nowrap' } ,
'& .MuiPaginationItem-root' : {
minWidth : 44 ,
height : 44 ,
fontSize : '1rem' ,
} ,
display : 'flex' ,
justifyContent : 'space-between ',
alignItems : 'center ' ,
flexWrap : 'nowrap ' ,
gap : 1 ,
width : '100%' ,
height : 44 ,
visibility : showPaginationControls ? 'visible' : 'hidden' ,
pointerEvents : showPaginationControls ? 'auto' : 'none' ,
} }
/ >
>
< Typography
variant = "body2"
color = "text.secondary"
noWrap
sx = { { flexShrink : 1 , minWidth : 0 , overflow : 'hidden' , textOverflow : 'ellipsis' } }
>
{ total === 0 ? '0' : ` ${ countStart } – ${ countEnd } ` } von { total }
< / Typography >
< Pagination
count = { pageCount }
page = { safePage }
onChange = { ( _e , page ) => this . _goToPage ( page ) }
color = "primary"
size = "large"
onMouseDown = { ( e ) => e . preventDefault ( ) }
sx = { {
flexShrink : 0 ,
maxWidth : '100%' ,
overflowX : 'auto' ,
scrollbarWidth : 'none' ,
'&::-webkit-scrollbar' : { display : 'none' } ,
'& .MuiPagination-ul' : { flexWrap : 'nowrap' } ,
'& .MuiPaginationItem-root' : {
minWidth : 44 ,
height : 44 ,
fontSize : '1rem' ,
} ,
} }
/ >
< / Box >
< / Box >
< / Box >
) : 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 } ` ,
} )
outline : ` 2px dashed ${ theme . palette . primary . main } ` ,
} )
: undefined ,
sx ,
] }
>
< Box sx = { { width : '100%' } } >
< List 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 != nul l ? (
< Box
ref = { ( el ) => {
this . ref = el ;
} }
onScroll = { virtualScrollbar ? this . _handleScroll : undefined }
onWheel = { ( e ) => {
if ( ! virtualScrollbar ) {
this . _handleWheel ( e ) ;
}
onWhee l ? . ( 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 ? ` ${ Math . max ( 0 , RACEWAY _WIDTH + racewayRightOffset + 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 : racewayRightOffset ,
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 ? (