Skip to content

[DataGrid] Add scroll shadows and fix scrollbar overlap #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 44 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7816345
[data grid] Update vertical position to cover pinned rows
KenanYusuf Feb 5, 2025
0737cae
fix build
KenanYusuf Feb 5, 2025
80ae0f9
fix
KenanYusuf Feb 6, 2025
673393e
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Feb 6, 2025
672d1f9
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Feb 17, 2025
5a87515
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Apr 15, 2025
432e5c8
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf May 21, 2025
b610de5
fix vertical scrollbar
KenanYusuf May 22, 2025
82ae8d6
add aria-hidden to presentational scrollbar
KenanYusuf May 22, 2025
57a2d0b
fix scrollbar filler for pinned right columns
KenanYusuf May 22, 2025
9707912
fix skeleton loader styles
KenanYusuf May 22, 2025
4eec5f3
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf May 22, 2025
4b6fda9
revert changes
KenanYusuf May 22, 2025
de7bfb5
fix scrollbarInnerHeight
KenanYusuf May 23, 2025
e89a6a9
fix scrolling vertically when there is horizontal scroll
KenanYusuf May 27, 2025
888f749
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf May 27, 2025
5d87e4a
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf May 27, 2025
04b6311
add scroll shadows
KenanYusuf May 28, 2025
d1dba84
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf May 28, 2025
83f916c
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Jun 2, 2025
62603e3
remove border from filler cells
KenanYusuf Jun 2, 2025
8a42578
round scroll values
KenanYusuf Jun 2, 2025
2b7881e
improve shadow styles
KenanYusuf Jun 2, 2025
35fafe2
improve var naming
KenanYusuf Jun 2, 2025
a9a2668
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Jun 2, 2025
beeb89b
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Jun 23, 2025
1642174
add null check
KenanYusuf Jun 24, 2025
bf0fc47
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Jun 24, 2025
f44d1b3
fix vertical scrollbar size and position
KenanYusuf Jun 24, 2025
0ec921a
alternate approach to hiding vertical scrollbar on overlays
KenanYusuf Jun 25, 2025
6dd3eed
export type
KenanYusuf Jun 25, 2025
fbc9c83
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Jun 25, 2025
aeb9ff4
adjust shadow
KenanYusuf Jun 25, 2025
872530a
fix space for horizontal scrollbar in list view
KenanYusuf Jun 25, 2025
fe55f40
add missing dependency
KenanYusuf Jun 25, 2025
2386c9c
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Jun 25, 2025
67c3a42
update background color in dark mode
KenanYusuf Jun 26, 2025
825be6d
update light mode background color
KenanYusuf Jun 26, 2025
bedd100
refine shadows
KenanYusuf Jun 26, 2025
24ea113
add border to scrollbar filler on header filter rows
KenanYusuf Jun 26, 2025
684f386
adjust shadows _again_
KenanYusuf Jun 26, 2025
81b3a90
Merge branch 'master' into vertical-scrollbar-include-pinned-rows
KenanYusuf Jun 26, 2025
392d9a0
fix scroll shadows in RTL
KenanYusuf Jun 26, 2025
7d5734e
lint
KenanYusuf Jun 27, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => {
const indexInSection = i;
const sectionLength = renderedColumns.length;

const showLeftBorder = shouldCellShowLeftBorder(pinnedPosition, indexInSection);
const showLeftBorder = shouldCellShowLeftBorder(
pinnedPosition,
indexInSection,
rootProps.showColumnVerticalBorder,
);
const showRightBorder = shouldCellShowRightBorder(
pinnedPosition,
indexInSection,
Expand Down
3 changes: 1 addition & 2 deletions packages/x-data-grid/src/components/GridLoadingOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { GridOverlay, GridOverlayProps } from './containers/GridOverlay';
import { GridSkeletonLoadingOverlay } from './GridSkeletonLoadingOverlay';
import { useGridApiContext } from '../hooks/utils/useGridApiContext';
import { gridRowCountSelector, useGridSelector } from '../hooks';

export type GridLoadingOverlayVariant = 'circular-progress' | 'linear-progress' | 'skeleton';
import type { GridLoadingOverlayVariant } from '../hooks/features/overlays/gridOverlaysInterfaces';

export interface GridLoadingOverlayProps extends GridOverlayProps {
/**
Expand Down
10 changes: 5 additions & 5 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 @@ -350,7 +349,11 @@ const GridRow = forwardRef<HTMLDivElement, GridRowProps>(function GridRow(props,

const cellIsNotVisible = pinnedPosition === PinnedColumnPosition.VIRTUAL;

const showLeftBorder = shouldCellShowLeftBorder(pinnedPosition, indexInSection);
const showLeftBorder = shouldCellShowLeftBorder(
pinnedPosition,
indexInSection,
rootProps.showCellVerticalBorder,
);
const showRightBorder = shouldCellShowRightBorder(
pinnedPosition,
indexInSection,
Expand Down Expand Up @@ -474,9 +477,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} />
Copy link
Member

Choose a reason for hiding this comment

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

So now the empty cell above fills the space ScrollbarFiller was filling, correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

My understanding is that scrollbar fillers on rows is currently only necessary to cover the area where the scrollbar is not present on pinned rows:
Screenshot 2025-06-26 at 14 29 16

Now that the scrollbar covers this area, I don't think it is necessary 🤔

)}
</div>
);
});
Expand Down
132 changes: 132 additions & 0 deletions packages/x-data-grid/src/components/GridScrollShadows.tsx
Copy link
Member

Choose a reason for hiding this comment

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

Looks like scroll shadows do not consider RTL and don't work properly

Copy link
Member Author

Choose a reason for hiding this comment

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

Good spot. Updated 392d9a0

Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as React from 'react';
import { styled } from '@mui/system';
import { useRtl } from '@mui/system/RtlProvider';
import {
gridDimensionsSelector,
gridPinnedColumnsSelector,
useGridEvent,
useGridSelector,
} from '../hooks';
import { gridPinnedRowsSelector } from '../hooks/features/rows/gridRowsSelector';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
import { DataGridProcessedProps } from '../models/props/DataGridProps';
import { GridEventListener } from '../models/events';
import { vars } from '../constants/cssVariables';
import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext';

interface GridScrollShadowsProps {
position: 'vertical' | 'horizontal';
}

type OwnerState = Pick<DataGridProcessedProps, 'classes'> & {
position: 'vertical' | 'horizontal';
};

const ScrollShadow = styled('div')<{ ownerState: OwnerState }>(({ theme }) => ({
Copy link
Member

Choose a reason for hiding this comment

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

The shadows look quite dim in dark mode:

I can't tell that there are pinned rows and columns just looking at the screenshot 😅
WDYT?

It looks great in light mode for comparison:

Copy link
Member Author

@KenanYusuf KenanYusuf Jun 26, 2025

Choose a reason for hiding this comment

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

Shadows on dark mode are hard 🫠

We need to have a lighter background on the grid for shadows to be effective. Investigated a little and found that on the Material UI Table component, it has the paper overlay making it a dark grey as opposed to the black we currently have:

Screenshot 2025-06-26 at 14 40 30

I've updated the background color on dark mode for the data grid to match and refined the shadow size/opacity:

Screenshot 2025-06-26 at 14 37 02

Feels like an improvement — WDYT?

position: 'absolute',
inset: 0,
pointerEvents: 'none',
transition: vars.transition(['box-shadow'], { duration: vars.transitions.duration.short }),
'--length': theme.palette.mode === 'dark' ? '8px' : '6px',
'--length-inverse': 'calc(var(--length) * -1)',
'--opacity': theme.palette.mode === 'dark' ? '0.7' : '0.18',
'--blur': 'var(--length)',
'--spread': 'calc(var(--length) * -1)',
'--color-start': 'rgba(0, 0, 0, calc(var(--hasScrollStart) * var(--opacity)))',
'--color-end': 'rgba(0, 0, 0, calc(var(--hasScrollEnd) * var(--opacity)))',
variants: [
{
props: { position: 'vertical' },
style: {
top: 'var(--DataGrid-topContainerHeight)',
bottom:
'calc(var(--DataGrid-bottomContainerHeight) + var(--DataGrid-hasScrollX) * var(--DataGrid-scrollbarSize))',
boxShadow:
'inset 0 var(--length) var(--blur) var(--spread) var(--color-start), inset 0 var(--length-inverse) var(--blur) var(--spread) var(--color-end)',
},
},
{
props: { position: 'horizontal' },
style: {
left: 'var(--DataGrid-leftPinnedWidth)',
right:
'calc(var(--DataGrid-rightPinnedWidth) + var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize))',
boxShadow:
'inset var(--length) 0 var(--blur) var(--spread) var(--color-start), inset var(--length-inverse) 0 var(--blur) var(--spread) var(--color-end)',
},
},
],
}));

function GridScrollShadows(props: GridScrollShadowsProps) {
const { position } = props;
const rootProps = useGridRootProps();
const ownerState = { classes: rootProps.classes, position };
const ref = React.useRef<HTMLDivElement>(null);
const apiRef = useGridPrivateApiContext();
const dimensions = useGridSelector(apiRef, gridDimensionsSelector);
const pinnedRows = useGridSelector(apiRef, gridPinnedRowsSelector);
const pinnedColumns = useGridSelector(apiRef, gridPinnedColumnsSelector);
const initialScrollable =
position === 'vertical'
? dimensions.hasScrollY && pinnedRows?.bottom?.length > 0
: dimensions.hasScrollX &&
pinnedColumns?.right?.length !== undefined &&
pinnedColumns?.right?.length > 0;
const isRtl = useRtl();

const updateScrollShadowVisibility = (scrollPosition: number) => {
if (!ref.current) {
return;
}
// Math.abs to convert negative scroll position (RTL) to positive
const scroll = Math.abs(Math.round(scrollPosition));
const maxScroll = Math.round(
dimensions.contentSize[position === 'vertical' ? 'height' : 'width'] -
dimensions.viewportInnerSize[position === 'vertical' ? 'height' : 'width'],
);
const hasPinnedStart =
position === 'vertical'
? pinnedRows?.top?.length > 0
: pinnedColumns?.left?.length !== undefined && pinnedColumns?.left?.length > 0;
const hasPinnedEnd =
position === 'vertical'
? pinnedRows?.bottom?.length > 0
: pinnedColumns?.right?.length !== undefined && pinnedColumns?.right?.length > 0;
const scrollIsNotAtStart = isRtl ? scroll < maxScroll : scroll > 0;
const scrollIsNotAtEnd = isRtl ? scroll > 0 : scroll < maxScroll;
ref.current.style.setProperty(
'--hasScrollStart',
hasPinnedStart && scrollIsNotAtStart ? '1' : '0',
);
ref.current.style.setProperty('--hasScrollEnd', hasPinnedEnd && scrollIsNotAtEnd ? '1' : '0');
};

const handleScrolling: GridEventListener<'scrollPositionChange'> = (scrollParams) => {
updateScrollShadowVisibility(scrollParams[position === 'vertical' ? 'top' : 'left']);
};

const handleColumnResizeStop: GridEventListener<'columnResizeStop'> = () => {
if (position === 'horizontal') {
updateScrollShadowVisibility(apiRef.current.virtualScrollerRef?.current?.scrollLeft || 0);
}
};

useGridEvent(apiRef, 'scrollPositionChange', handleScrolling);
useGridEvent(apiRef, 'columnResizeStop', handleColumnResizeStop);

return (
<ScrollShadow
ownerState={ownerState}
ref={ref}
style={
{
'--hasScrollStart': 0,
'--hasScrollEnd': initialScrollable ? '1' : '0',
} as React.CSSProperties
}
/>
);
}

export { GridScrollShadows };
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
17 changes: 5 additions & 12 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 @@ -136,7 +135,11 @@ export const GridSkeletonLoadingOverlayInner = forwardRef<
rootProps.showCellVerticalBorder,
gridHasFiller,
);
const showLeftBorder = shouldCellShowLeftBorder(pinnedPosition, sectionIndex);
const showLeftBorder = shouldCellShowLeftBorder(
pinnedPosition,
sectionIndex,
rootProps.showCellVerticalBorder,
);
const isLastColumn = colIndex === columns.length - 1;
const isFirstPinnedRight = isPinnedRight && sectionIndex === 0;
const hasFillerBefore = isFirstPinnedRight && gridHasFiller;
Expand All @@ -146,7 +149,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 +179,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
13 changes: 4 additions & 9 deletions packages/x-data-grid/src/components/base/GridOverlays.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { DataGridProcessedProps } from '../../models/props/DataGridProps';
import { getDataGridUtilityClass } from '../../constants/gridClasses';
import { GridLoadingOverlayVariant } from '../GridLoadingOverlay';
import { GridSlotsComponent } from '../../models';

export type GridOverlayType =
| keyof Pick<
GridSlotsComponent,
'noColumnsOverlay' | 'noRowsOverlay' | 'noResultsOverlay' | 'loadingOverlay'
>
| null;
import type {
GridOverlayType,
GridLoadingOverlayVariant,
} from '../../hooks/features/overlays/gridOverlaysInterfaces';

interface GridOverlaysProps {
overlayType: GridOverlayType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ interface GridColumnHeaderItemProps {
pinnedPosition?: PinnedColumnPosition;
pinnedOffset?: number;
style?: React.CSSProperties;
isLastUnpinned: boolean;
isSiblingFocused: boolean;
showLeftBorder: boolean;
showRightBorder: boolean;
Expand All @@ -60,7 +59,6 @@ const useUtilityClasses = (ownerState: OwnerState) => {
showLeftBorder,
filterItemsCounter,
pinnedPosition,
isLastUnpinned,
isSiblingFocused,
} = ownerState;

Expand All @@ -87,7 +85,6 @@ const useUtilityClasses = (ownerState: OwnerState) => {
pinnedPosition === PinnedColumnPosition.RIGHT && 'columnHeader--pinnedRight',
// TODO: Remove classes below and restore `:has` selectors when they are supported in jsdom
// See https://github.com/mui/mui-x/pull/14559
isLastUnpinned && 'columnHeader--lastUnpinned',
isSiblingFocused && 'columnHeader--siblingFocused',
],
draggableContainer: ['columnHeaderDraggableContainer'],
Expand Down Expand Up @@ -331,7 +328,6 @@ GridColumnHeaderItem.propTypes = {
headerHeight: PropTypes.number.isRequired,
isDragging: PropTypes.bool.isRequired,
isLast: PropTypes.bool.isRequired,
isLastUnpinned: PropTypes.bool.isRequired,
isResizing: PropTypes.bool.isRequired,
isSiblingFocused: PropTypes.bool.isRequired,
pinnedOffset: PropTypes.number,
Expand Down
Loading