Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,13 @@ export function AppSearch(props: AppSearchProps) {
},
},
body: {
// DocSearch unconditionally sets `margin-right: <scrollbar width>` on the body to
// compensate for the scrollbar it expects to hide. It only hides the one on <body>,
// while the docs keep the scrollbar on <html> (overflow-y: scroll), so nothing
// disappears and the compensation shifts the whole page to the left instead.
'&.DocSearch--active': {
marginRight: '0 !important',
},
'.DocSearch-Container': {
transition: `opacity ${FADE_DURATION}ms`,
opacity: 0,
Expand Down
4 changes: 1 addition & 3 deletions packages-internal/core-docs/src/branding/brandingTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1555,9 +1555,7 @@ export function getThemedComponents(): ThemeOptions {
styleOverrides: {
html: {
overflowY: 'scroll',
// TODO add support for it,
// https://github.com/mui/material-ui/issues/40748
// scrollbarGutter: 'stable',
scrollbarGutter: 'stable',
},
},
},
Expand Down
26 changes: 26 additions & 0 deletions packages/mui-material/src/Modal/ModalManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, beforeAll, afterAll, it, expect, beforeEach, afterEach } from 'vitest';
import getScrollbarSize from '@mui/utils/getScrollbarSize';
import { isJsdom } from '@mui/internal-test-utils';
import { ModalManager } from './ModalManager';

interface Modal {
Expand Down Expand Up @@ -125,6 +126,7 @@ describe('ModalManager', () => {

afterEach(() => {
document.body.removeChild(fixedNode);
container1.style.removeProperty('scrollbar-gutter');
window.innerWidth -= 1;
});

Expand All @@ -143,6 +145,30 @@ describe('ModalManager', () => {
expect(fixedNode.style.paddingRight).to.equal('14.4px');
});

// A stable scrollbar gutter keeps the scrollbar space reserved while the scroll is locked,
// so compensating for it would shift the content instead of keeping it in place.
it.skipIf(isJsdom())(
'should not compensate the scrollbar when the scroll container has a stable gutter',
() => {
// This test is useless without support.
expect(CSS.supports('scrollbar-gutter', 'stable')).to.equal(true);

fixedNode.style.paddingRight = '14.4px';
container1.style.setProperty('scrollbar-gutter', 'stable');

const modal = getDummyModal();
modalManager.add(modal, container1);
modalManager.mount(modal, {});
expect(container1.style.overflow).to.equal('hidden');
expect(container1.style.paddingRight).to.equal('20px');
expect(fixedNode.style.paddingRight).to.equal('14.4px');
modalManager.remove(modal);
expect(container1.style.overflow).to.equal('');
expect(container1.style.paddingRight).to.equal('20px');
expect(fixedNode.style.paddingRight).to.equal('14.4px');
},
);

it('should disable the scroll even when not overflowing', () => {
// simulate non-overflowing container
const container2 = document.createElement('div');
Expand Down
21 changes: 20 additions & 1 deletion packages/mui-material/src/Modal/ModalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ function ariaHiddenSiblings(
});
}

// A stable scrollbar gutter keeps the scrollbar space reserved while the scroll is locked,
// so there is no layout shift to compensate for.
// Reading the computed value doubles as a feature detection: browsers that don't support
// scrollbar-gutter resolve it to an empty string.
function hasStableScrollbarGutter(scrollContainer: HTMLElement): boolean {
const win = ownerWindow(scrollContainer);
const doc = ownerDocument(scrollContainer);
// scrollbar-gutter isn't inherited, but the one set on the root element propagates to the
// viewport, so check <html> as well when locking the document scroller.
const elements =
scrollContainer === doc.body || scrollContainer === doc.documentElement
? [scrollContainer, doc.documentElement]
: [scrollContainer];

return elements.some((element) =>
win.getComputedStyle(element).getPropertyValue('scrollbar-gutter').includes('stable'),
);
}

function handleContainer(containerInfo: Container, props: ManagedModalProps) {
const restoreStyle: Array<{
/**
Expand Down Expand Up @@ -100,7 +119,7 @@ function handleContainer(containerInfo: Container, props: ManagedModalProps) {
: container;
}

if (isOverflowing(scrollContainer)) {
if (isOverflowing(scrollContainer) && !hasStableScrollbarGutter(scrollContainer)) {
// Compute the size before applying overflow hidden to avoid any scroll jumps.
const scrollbarSize = getScrollbarSize(ownerWindow(scrollContainer));

Expand Down
Loading