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)?/); }); }); });