diff --git a/docs/pages/material-ui/api/popover.json b/docs/pages/material-ui/api/popover.json
index 9e2435ef15a160..6fa28eedfcaa61 100644
--- a/docs/pages/material-ui/api/popover.json
+++ b/docs/pages/material-ui/api/popover.json
@@ -23,6 +23,7 @@
"children": { "type": { "name": "node" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"container": { "type": { "name": "union", "description": "HTML element
| func" } },
+ "disableScrollLock": { "type": { "name": "bool" }, "default": "false" },
"elevation": { "type": { "name": "custom", "description": "integer" }, "default": "8" },
"marginThreshold": { "type": { "name": "number" }, "default": "16" },
"onClose": { "type": { "name": "func" } },
diff --git a/docs/translations/api-docs/popover/popover.json b/docs/translations/api-docs/popover/popover.json
index bfa27376575c7c..9b4e2582257946 100644
--- a/docs/translations/api-docs/popover/popover.json
+++ b/docs/translations/api-docs/popover/popover.json
@@ -21,9 +21,10 @@
"container": {
"description": "An HTML element, component instance, or function that returns either. The container
will passed to the Modal component.
By default, it uses the body of the anchorEl's top-level document object, so it's simply document.body
most of the time."
},
+ "disableScrollLock": { "description": "Disable the scroll lock behavior." },
"elevation": { "description": "The elevation of the popover." },
"marginThreshold": {
- "description": "Specifies how close to the edge of the window the popover can appear."
+ "description": "Specifies how close to the edge of the window the popover can appear. If null, the popover will not be constrained by the window."
},
"onClose": {
"description": "Callback fired when the component requests to be closed. The reason
parameter can optionally be used to control the response to onClose
."
diff --git a/packages/mui-material/src/Popover/Popover.d.ts b/packages/mui-material/src/Popover/Popover.d.ts
index 8735a0dd97b0c8..54b4a7e8bd3527 100644
--- a/packages/mui-material/src/Popover/Popover.d.ts
+++ b/packages/mui-material/src/Popover/Popover.d.ts
@@ -91,9 +91,10 @@ export interface PopoverProps
elevation?: number;
/**
* Specifies how close to the edge of the window the popover can appear.
+ * If null, the popover will not be constrained by the window.
* @default 16
*/
- marginThreshold?: number;
+ marginThreshold?: number | null;
onClose?: ModalProps['onClose'];
/**
* If `true`, the component is shown.
diff --git a/packages/mui-material/src/Popover/Popover.js b/packages/mui-material/src/Popover/Popover.js
index 1d024103f46b2c..5059c4f7fbc229 100644
--- a/packages/mui-material/src/Popover/Popover.js
+++ b/packages/mui-material/src/Popover/Popover.js
@@ -125,6 +125,7 @@ const Popover = React.forwardRef(function Popover(inProps, ref) {
TransitionComponent = Grow,
transitionDuration: transitionDurationProp = 'auto',
TransitionProps: { onEntering, ...TransitionProps } = {},
+ disableScrollLock = false,
...other
} = props;
@@ -244,13 +245,17 @@ const Popover = React.forwardRef(function Popover(inProps, ref) {
const widthThreshold = containerWindow.innerWidth - marginThreshold;
// Check if the vertical axis needs shifting
- if (top < marginThreshold) {
+ if (marginThreshold !== null && top < marginThreshold) {
const diff = top - marginThreshold;
+
top -= diff;
+
elemTransformOrigin.vertical += diff;
- } else if (bottom > heightThreshold) {
+ } else if (marginThreshold !== null && bottom > heightThreshold) {
const diff = bottom - heightThreshold;
+
top -= diff;
+
elemTransformOrigin.vertical += diff;
}
@@ -269,7 +274,7 @@ const Popover = React.forwardRef(function Popover(inProps, ref) {
}
// Check if the horizontal axis needs shifting
- if (left < marginThreshold) {
+ if (marginThreshold !== null && left < marginThreshold) {
const diff = left - marginThreshold;
left -= diff;
elemTransformOrigin.horizontal += diff;
@@ -309,6 +314,13 @@ const Popover = React.forwardRef(function Popover(inProps, ref) {
setIsPositioned(true);
}, [getPositioningStyle]);
+ React.useEffect(() => {
+ if (disableScrollLock) {
+ window.addEventListener('scroll', setPositioningStyles);
+ }
+ return () => window.removeEventListener('scroll', setPositioningStyles);
+ }, [anchorEl, disableScrollLock, setPositioningStyles]);
+
const handleEntering = (element, isAppearing) => {
if (onEntering) {
onEntering(element, isAppearing);
@@ -403,7 +415,10 @@ const Popover = React.forwardRef(function Popover(inProps, ref) {
});
return (
-
+
', () => {
});
});
- [0, 18, 16].forEach((marginThreshold) => {
- describe(`positioning when \`marginThreshold=${marginThreshold}\``, () => {
+ describe('prop: marginThreshold', () => {
+ [0, 18, 16].forEach((marginThreshold) => {
function getElementStyleOfOpenPopover(anchorEl = document.createElement('svg')) {
let style;
render(
@@ -858,7 +858,7 @@ describe('', () => {
},
}}
marginThreshold={marginThreshold}
- PaperProps={{ component: FakePaper }}
+ slotProps={{ paper: { component: FakePaper } }}
>
,
@@ -866,98 +866,132 @@ describe('', () => {
return style;
}
- specify('when no movement is needed', () => {
- const negative = marginThreshold === 0 ? '' : '-';
- const positioningStyle = getElementStyleOfOpenPopover();
+ describe(`positioning when \`marginThreshold=${marginThreshold}\``, () => {
+ specify('when no movement is needed', () => {
+ const negative = marginThreshold === 0 ? '' : '-';
+ const positioningStyle = getElementStyleOfOpenPopover();
- expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
- expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
- expect(positioningStyle.transformOrigin).to.match(
- new RegExp(`${negative}${marginThreshold}px ${negative}${marginThreshold}px( 0px)?`),
- );
- });
-
- specify('top < marginThreshold', () => {
- const mockedAnchor = document.createElement('div');
- stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
- left: marginThreshold,
- top: marginThreshold - 1,
- }));
- const positioningStyle = getElementStyleOfOpenPopover(mockedAnchor);
-
- expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
- expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
- expect(positioningStyle.transformOrigin).to.match(/0px -1px( 0ms)?/);
- });
-
- describe('bottom > heightThreshold', () => {
- let windowInnerHeight;
-
- before(() => {
- windowInnerHeight = window.innerHeight;
- window.innerHeight = marginThreshold * 2;
- });
-
- after(() => {
- window.innerHeight = windowInnerHeight;
+ expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
+ expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
+ expect(positioningStyle.transformOrigin).to.match(
+ new RegExp(`${negative}${marginThreshold}px ${negative}${marginThreshold}px( 0px)?`),
+ );
});
- specify('test', () => {
+ specify('top < marginThreshold', () => {
const mockedAnchor = document.createElement('div');
stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
left: marginThreshold,
- top: marginThreshold + 1,
+ top: marginThreshold - 1,
}));
-
const positioningStyle = getElementStyleOfOpenPopover(mockedAnchor);
expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
- expect(positioningStyle.transformOrigin).to.match(/0px 1px( 0px)?/);
+ expect(positioningStyle.transformOrigin).to.match(/0px -1px( 0px)?/);
});
- });
- specify('left < marginThreshold', () => {
- const mockedAnchor = document.createElement('div');
- stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
- left: marginThreshold - 1,
- top: marginThreshold,
- }));
-
- const positioningStyle = getElementStyleOfOpenPopover(mockedAnchor);
+ describe('bottom > heightThreshold', () => {
+ let windowInnerHeight;
- expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
+ before(() => {
+ windowInnerHeight = window.innerHeight;
+ window.innerHeight = marginThreshold * 2;
+ });
- expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
+ after(() => {
+ window.innerHeight = windowInnerHeight;
+ });
- expect(positioningStyle.transformOrigin).to.match(/-1px 0px( 0px)?/);
- });
+ specify('test', () => {
+ const mockedAnchor = document.createElement('div');
+ stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
+ left: marginThreshold,
+ top: marginThreshold + 1,
+ }));
- describe('right > widthThreshold', () => {
- let innerWidthContainer;
+ const positioningStyle = getElementStyleOfOpenPopover(mockedAnchor);
- before(() => {
- innerWidthContainer = window.innerWidth;
- window.innerWidth = marginThreshold * 2;
+ expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
+ expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
+ expect(positioningStyle.transformOrigin).to.match(/0px 1px( 0px)?/);
+ });
});
- after(() => {
- window.innerWidth = innerWidthContainer;
- });
-
- specify('test', () => {
+ specify('left < marginThreshold', () => {
const mockedAnchor = document.createElement('div');
stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
- left: marginThreshold + 1,
+ left: marginThreshold - 1,
top: marginThreshold,
}));
const positioningStyle = getElementStyleOfOpenPopover(mockedAnchor);
expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
+
expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
- expect(positioningStyle.transformOrigin).to.match(/1px 0px( 0px)?/);
+
+ expect(positioningStyle.transformOrigin).to.match(/-1px 0px( 0px)?/);
});
+
+ describe('right > widthThreshold', () => {
+ let innerWidthContainer;
+
+ before(() => {
+ innerWidthContainer = window.innerWidth;
+ window.innerWidth = marginThreshold * 2;
+ });
+
+ after(() => {
+ window.innerWidth = innerWidthContainer;
+ });
+
+ specify('test', () => {
+ const mockedAnchor = document.createElement('div');
+ stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
+ left: marginThreshold + 1,
+ top: marginThreshold,
+ }));
+
+ const positioningStyle = getElementStyleOfOpenPopover(mockedAnchor);
+
+ expect(positioningStyle.top).to.equal(`${marginThreshold}px`);
+ expect(positioningStyle.left).to.equal(`${marginThreshold}px`);
+ expect(positioningStyle.transformOrigin).to.match(/1px 0px( 0px)?/);
+ });
+ });
+ });
+ });
+
+ describe('positioning when `marginThreshold=null`', () => {
+ it('should not apply the marginThreshold when marginThreshold is null', () => {
+ const mockedAnchor = document.createElement('div');
+ const valueOutsideWindow = -100;
+ stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
+ top: valueOutsideWindow,
+ left: valueOutsideWindow,
+ }));
+
+ let style;
+ render(
+ {
+ style = node.style;
+ },
+ }}
+ marginThreshold={null}
+ slotProps={{ paper: { component: FakePaper } }}
+ >
+
+ ,
+ );
+
+ expect(style.top).to.equal(`${valueOutsideWindow}px`);
+ expect(style.left).to.equal(`${valueOutsideWindow}px`);
+ expect(style.transformOrigin).to.match(/0px 0px( 0px)?/);
});
});
});