Skip to content

[DataGrid] Update vertical scrollbar position to cover pinned rows #16476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/x-data-grid/src/components/GridRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
gridEditRowsStateSelector,
gridRowIsEditingSelector,
} from '../hooks/features/editing/gridEditingSelectors';
import { GridScrollbarFillerCell as ScrollbarFiller } from './GridScrollbarFillerCell';
import { getPinnedCellOffset } from '../internals/utils/getPinnedCellOffset';
import { useGridConfiguration } from '../hooks/utils/useGridConfiguration';
import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext';
Expand Down Expand Up @@ -474,9 +473,6 @@ const GridRow = forwardRef<HTMLDivElement, GridRowProps>(function GridRow(props,
{cells}
<div role="presentation" className={clsx(gridClasses.cell, gridClasses.cellEmpty)} />
{rightCells}
{scrollbarWidth !== 0 && (
<ScrollbarFiller pinnedRight={pinnedColumns.right.length > 0} borderTop={!isFirstVisible} />
)}
</div>
);
});
Expand Down
26 changes: 2 additions & 24 deletions packages/x-data-grid/src/components/GridScrollbarFillerCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,12 @@ import { gridClasses } from '../constants';

const classes = {
root: gridClasses.scrollbarFiller,
header: gridClasses['scrollbarFiller--header'],
borderTop: gridClasses['scrollbarFiller--borderTop'],
borderBottom: gridClasses['scrollbarFiller--borderBottom'],
pinnedRight: gridClasses['scrollbarFiller--pinnedRight'],
};

function GridScrollbarFillerCell({
header,
borderTop = true,
borderBottom,
pinnedRight,
}: {
header?: boolean;
borderTop?: boolean;
borderBottom?: boolean;
pinnedRight?: boolean;
}) {
function GridScrollbarFillerCell({ pinnedRight }: { pinnedRight?: boolean }) {
return (
<div
role="presentation"
className={clsx(
classes.root,
header && classes.header,
borderTop && classes.borderTop,
borderBottom && classes.borderBottom,
pinnedRight && classes.pinnedRight,
)}
/>
<div role="presentation" className={clsx(classes.root, pinnedRight && classes.pinnedRight)} />
);
}

Expand Down
11 changes: 0 additions & 11 deletions packages/x-data-grid/src/components/GridSkeletonLoadingOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { getDataGridUtilityClass, gridClasses } from '../constants/gridClasses';
import { getPinnedCellOffset } from '../internals/utils/getPinnedCellOffset';
import { shouldCellShowLeftBorder, shouldCellShowRightBorder } from '../utils/cellBorderUtils';
import { escapeOperandAttributeSelector } from '../utils/domUtils';
import { GridScrollbarFillerCell } from './GridScrollbarFillerCell';
import { rtlFlipSide } from '../utils/rtlFlipSide';
import { attachPinnedStyle } from '../internals/utils';

Expand Down Expand Up @@ -146,7 +145,6 @@ export const GridSkeletonLoadingOverlayInner = forwardRef<
const emptyCell = (
<slots.skeletonCell key={`skeleton-filler-column-${i}`} width={emptyCellWidth} empty />
);
const hasScrollbarFiller = isLastColumn && scrollbarWidth !== 0;

if (hasFillerBefore) {
rowCells.push(emptyCell);
Expand Down Expand Up @@ -177,15 +175,6 @@ export const GridSkeletonLoadingOverlayInner = forwardRef<
if (hasFillerAfter) {
rowCells.push(emptyCell);
}

if (hasScrollbarFiller) {
rowCells.push(
<GridScrollbarFillerCell
key={`skeleton-scrollbar-filler-${i}`}
pinnedRight={pinnedColumns.right.length > 0}
/>,
);
}
}

array.push(
Expand Down
27 changes: 9 additions & 18 deletions packages/x-data-grid/src/components/containers/GridRootStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,6 @@ export const GridRootStyles = styled('div', {
{ [`& .${c['scrollbar--horizontal']}`]: styles['scrollbar--horizontal'] },
{ [`& .${c['scrollbar--vertical']}`]: styles['scrollbar--vertical'] },
{ [`& .${c.scrollbarFiller}`]: styles.scrollbarFiller },
{ [`& .${c['scrollbarFiller--borderBottom']}`]: styles['scrollbarFiller--borderBottom'] },
{ [`& .${c['scrollbarFiller--borderTop']}`]: styles['scrollbarFiller--borderTop'] },
{ [`& .${c['scrollbarFiller--header']}`]: styles['scrollbarFiller--header'] },
{ [`& .${c['scrollbarFiller--pinnedRight']}`]: styles['scrollbarFiller--pinnedRight'] },
{ [`& .${c.sortIcon}`]: styles.sortIcon },
{ [`& .${c.treeDataGroupingCell}`]: styles.treeDataGroupingCell },
Expand Down Expand Up @@ -762,44 +759,38 @@ export const GridRootStyles = styled('div', {
alignSelf: 'stretch',
marginRight: vars.spacing(2),
},

/* ScrollbarFiller styles */
[`.${c.scrollbarFiller}`]: {
minWidth: 'calc(var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize))',
alignSelf: 'stretch',
[`&.${c['scrollbarFiller--borderTop']}`]: {
borderTop: '1px solid var(--DataGrid-rowBorderColor)',
},
[`&.${c['scrollbarFiller--borderBottom']}`]: {
borderBottom: '1px solid var(--DataGrid-rowBorderColor)',
},
backgroundColor: headerBackground,
[`&.${c['scrollbarFiller--pinnedRight']}`]: {
backgroundColor: vars.cell.background.pinned,
position: 'sticky',
right: 0,
zIndex: 30,
},
},

[`& .${c.filler}`]: {
flex: '1 0 auto',
},
[`& .${c['filler--borderBottom']}`]: {
borderBottom: '1px solid var(--DataGrid-rowBorderColor)',
},

/* Hide grid rows, row filler, and vertical scrollbar. Used when skeleton/no columns overlay is visible */
/* Used when skeleton/no columns overlay is visible */
[`& .${c['main--hiddenContent']}`]: {
// Position vertical scrollbar and fillers out of grid viewport
marginRight: 'calc(var(--DataGrid-scrollbarSize) * -1)',
Copy link
Member Author

@KenanYusuf KenanYusuf May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the easiest way to ensure the horizontal scrollbar is the correct width and the vertical and corner scrollbar are hidden. Also removes the need for fillers in the skeleton loader rows.

[`& .${c.virtualScrollerContent}`]: {
// We use visibility hidden so that the virtual scroller content retains its height.
// Position fixed is used to remove the virtual scroller content from the flow.
// https://github.com/mui/mui-x/issues/14061
position: 'fixed',
visibility: 'hidden',
},
[`& .${c['scrollbar--vertical']}, & .${c.pinnedRows}, & .${c.virtualScroller} > .${c.filler}`]:
{
display: 'none',
},
// Hide grid content
[`& .${c.pinnedRows}`]: {
display: 'none',
},
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,21 @@ const Scrollbar = styled('div')({
const ScrollbarVertical = styled(Scrollbar)({
width: 'var(--size)',
height:
'calc(var(--DataGrid-hasScrollY) * (100% - var(--DataGrid-topContainerHeight) - var(--DataGrid-bottomContainerHeight) - var(--DataGrid-hasScrollX) * var(--DataGrid-scrollbarSize)))',
'calc(var(--DataGrid-hasScrollY) * (100% - var(--DataGrid-headerHeight) - var(--DataGrid-hasScrollX) * var(--DataGrid-scrollbarSize)))',
overflowY: 'auto',
overflowX: 'hidden',
// Disable focus-visible style, it's a scrollbar.
outline: 0,
'& > div': {
width: 'var(--size)',
},
top: 'var(--DataGrid-topContainerHeight)',
right: '0px',
top: 'var(--DataGrid-headerHeight)',
right: 0,
});

const ScrollbarHorizontal = styled(Scrollbar)({
width: '100%',
width:
'calc(var(--DataGrid-hasScrollX) * (100% - var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize)))',
height: 'var(--size)',
overflowY: 'hidden',
overflowX: 'auto',
Expand All @@ -71,7 +72,15 @@ const ScrollbarHorizontal = styled(Scrollbar)({
'& > div': {
height: 'var(--size)',
},
bottom: '0px',
bottom: 0,
});

export const ScrollbarCorner = styled(Scrollbar)({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This corner piece covers up the dead zone between the two scrollbars.

Without With
Screenshot 2025-05-21 at 17 01 47 Screenshot 2025-05-21 at 17 01 16

width: 'var(--size)',
height: 'var(--size)',
right: 0,
bottom: 0,
overflow: 'scroll',
});

const GridVirtualScrollbar = forwardRef<HTMLDivElement, GridVirtualScrollbarProps>(
Expand All @@ -81,25 +90,17 @@ const GridVirtualScrollbar = forwardRef<HTMLDivElement, GridVirtualScrollbarProp
const isLocked = React.useRef(false);
const lastPosition = React.useRef(0);
const scrollbarRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const classes = useUtilityClasses(rootProps, props.position);
const dimensions = useGridSelector(apiRef, gridDimensionsSelector);

const propertyDimension = props.position === 'vertical' ? 'height' : 'width';
const propertyScroll = props.position === 'vertical' ? 'scrollTop' : 'scrollLeft';
const propertyScrollPosition = props.position === 'vertical' ? 'top' : 'left';
const hasScroll = props.position === 'vertical' ? dimensions.hasScrollX : dimensions.hasScrollY;

const contentSize =
dimensions.minimumSize[propertyDimension] + (hasScroll ? dimensions.scrollbarSize : 0);

const scrollbarSize =
props.position === 'vertical'
? dimensions.viewportInnerSize.height
: dimensions.viewportOuterSize.width;

const scrollbarInnerSize =
scrollbarSize * (contentSize / dimensions.viewportOuterSize[propertyDimension]);
props.position === 'horizontal'
? dimensions.minimumSize.width
: dimensions.minimumSize.height - dimensions.headersTotalHeight;

const onScrollerScroll = useEventCallback(() => {
const scrollbar = scrollbarRef.current;
Expand All @@ -121,8 +122,7 @@ const GridVirtualScrollbar = forwardRef<HTMLDivElement, GridVirtualScrollbarProp
}
isLocked.current = true;

const value = scrollPosition[propertyScrollPosition] / contentSize;
scrollbar[propertyScroll] = value * scrollbarInnerSize;
scrollbar[propertyScroll] = scrollPosition[propertyScrollPosition];
});

const onScrollbarScroll = useEventCallback(() => {
Expand All @@ -139,8 +139,7 @@ const GridVirtualScrollbar = forwardRef<HTMLDivElement, GridVirtualScrollbarProp
}
isLocked.current = true;

const value = scrollbar[propertyScroll] / scrollbarInnerSize;
scroller[propertyScroll] = value * contentSize;
scroller[propertyScroll] = scrollbar[propertyScroll];
});

useOnMount(() => {
Expand All @@ -155,11 +154,6 @@ const GridVirtualScrollbar = forwardRef<HTMLDivElement, GridVirtualScrollbarProp
};
});

React.useEffect(() => {
const content = contentRef.current!;
content.style.setProperty(propertyDimension, `${scrollbarInnerSize}px`);
}, [scrollbarInnerSize, propertyDimension]);

const Container = props.position === 'vertical' ? ScrollbarVertical : ScrollbarHorizontal;

return (
Expand All @@ -179,7 +173,10 @@ const GridVirtualScrollbar = forwardRef<HTMLDivElement, GridVirtualScrollbarProp
event.target.blur();
}}
>
<div ref={contentRef} className={classes.content} />
<div
className={classes.content}
style={{ [propertyDimension]: `${scrollbarInnerSize}px` }}
/>
</Container>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { GridTopContainer as TopContainer } from './GridTopContainer';
import { GridVirtualScrollerContent as Content } from './GridVirtualScrollerContent';
import { GridVirtualScrollerFiller as SpaceFiller } from './GridVirtualScrollerFiller';
import { GridVirtualScrollerRenderZone as RenderZone } from './GridVirtualScrollerRenderZone';
import { GridVirtualScrollbar as Scrollbar } from './GridVirtualScrollbar';
import { GridVirtualScrollbar as Scrollbar, ScrollbarCorner } from './GridVirtualScrollbar';
import { GridLoadingOverlayVariant } from '../GridLoadingOverlay';
import { GridOverlayType } from '../base/GridOverlays';
import { GridApiCommunity } from '../../models/api/gridApiCommunity';
Expand Down Expand Up @@ -136,6 +136,7 @@ function GridVirtualScroller(props: GridVirtualScrollerProps) {
<Scrollbar position="horizontal" {...getScrollbarHorizontalProps()} />
)}
{hasScrollY && <Scrollbar position="vertical" {...getScrollbarVerticalProps()} />}
{hasScrollX && hasScrollY && <ScrollbarCorner aria-hidden="true" />}
{props.children}
</Container>
);
Expand Down
18 changes: 0 additions & 18 deletions packages/x-data-grid/src/constants/gridClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,21 +661,6 @@ export interface GridClasses {
* Styles applied to the scrollbar filler cell.
*/
scrollbarFiller: string;
/**
* @ignore - do not document.
* Styles applied to the scrollbar filler cell, in header position.
*/
'scrollbarFiller--header': string;
/**
* @ignore - do not document.
* Styles applied to the scrollbar filler cell, with a border top.
*/
'scrollbarFiller--borderTop': string;
/**
* @ignore - do not document.
* Styles applied to the scrollbar filler cell, with a border bottom.
*/
'scrollbarFiller--borderBottom': string;
/**
* @ignore - do not document.
* Styles applied to the scrollbar filler cell.
Expand Down Expand Up @@ -1079,9 +1064,6 @@ export const gridClasses = generateUtilityClasses<GridClassKey>('MuiDataGrid', [
'scrollbar--vertical',
'scrollbar--horizontal',
'scrollbarFiller',
'scrollbarFiller--header',
'scrollbarFiller--borderTop',
'scrollbarFiller--borderBottom',
'scrollbarFiller--pinnedRight',
'selectedRowCount',
'sortButton',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => {
)}
/>
)}
{hasScrollbarFiller && (
<ScrollbarFiller
header
pinnedRight={isPinnedRight}
borderBottom={borderBottom}
borderTop={false}
/>
)}
{hasScrollbarFiller && <ScrollbarFiller pinnedRight={isPinnedRight} />}
</React.Fragment>
);
};
Expand Down