diff --git a/docs/pages/api-docs/popover.json b/docs/pages/api-docs/popover.json index 4c034015a04250..b4bb01dffceee3 100644 --- a/docs/pages/api-docs/popover.json +++ b/docs/pages/api-docs/popover.json @@ -31,6 +31,7 @@ "type": { "name": "shape", "description": "{ component?: element type }" }, "default": "{}" }, + "sx": { "type": { "name": "object" } }, "transformOrigin": { "type": { "name": "shape", @@ -55,6 +56,6 @@ "filename": "/packages/material-ui/src/Popover/Popover.js", "inheritance": { "component": "Modal", "pathname": "/api/modal/" }, "demos": "", - "styledComponent": false, + "styledComponent": true, "cssComponent": false } diff --git a/docs/translations/api-docs/popover/popover.json b/docs/translations/api-docs/popover/popover.json index 169f18cde52cb6..de0491a8b129e1 100644 --- a/docs/translations/api-docs/popover/popover.json +++ b/docs/translations/api-docs/popover/popover.json @@ -15,6 +15,7 @@ "onClose": "Callback fired when the component requests to be closed. The reason parameter can optionally be used to control the response to onClose.", "open": "If true, the component is shown.", "PaperProps": "Props applied to the Paper element.", + "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "transformOrigin": "This is the point on the popover which will attach to the anchor's origin.
Options: vertical: [top, center, bottom, x(px)]; horizontal: [left, center, right, x(px)].", "TransitionComponent": "The component used for the transition. Follow this guide to learn more about the requirements for this component.", "transitionDuration": "Set to 'auto' to automatically calculate transition time based on height.", diff --git a/packages/material-ui/src/Popover/Popover.d.ts b/packages/material-ui/src/Popover/Popover.d.ts index 7e572794f99f7b..ce77cbf7559c3d 100644 --- a/packages/material-ui/src/Popover/Popover.d.ts +++ b/packages/material-ui/src/Popover/Popover.d.ts @@ -1,7 +1,9 @@ import * as React from 'react'; +import { SxProps } from '@material-ui/system'; import { InternalStandardProps as StandardProps } from '..'; import { PaperProps } from '../Paper'; import { ModalProps } from '../Modal'; +import { Theme } from '../styles'; import { TransitionHandlerProps, TransitionProps } from '../transitions/transition'; export interface PopoverOrigin { @@ -103,6 +105,10 @@ export interface PopoverProps * @default {} */ PaperProps?: Partial; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; /** * This is the point on the popover which * will attach to the anchor's origin. diff --git a/packages/material-ui/src/Popover/Popover.js b/packages/material-ui/src/Popover/Popover.js index d403c77fd0d245..42cbefd237fe1b 100644 --- a/packages/material-ui/src/Popover/Popover.js +++ b/packages/material-ui/src/Popover/Popover.js @@ -1,19 +1,23 @@ import * as React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; import { chainPropTypes, + deepmerge, elementTypeAcceptingRef, refType, HTMLElementType, } from '@material-ui/utils'; -import clsx from 'clsx'; +import experimentalStyled from '../styles/experimentalStyled'; +import useThemeProps from '../styles/useThemeProps'; import debounce from '../utils/debounce'; import ownerDocument from '../utils/ownerDocument'; import ownerWindow from '../utils/ownerWindow'; -import withStyles from '../styles/withStyles'; -import Modal from '../Modal'; import Grow from '../Grow'; +import Modal from '../Modal'; import Paper from '../Paper'; +import popoverClasses, { getPopoverUtilityClass } from './popoverClasses'; export function getOffsetTop(rect, vertical) { let offset = 0; @@ -65,26 +69,56 @@ function getAnchorEl(anchorEl) { return typeof anchorEl === 'function' ? anchorEl() : anchorEl; } -export const styles = { - /* Styles applied to the root element. */ - root: {}, - /* Styles applied to the Paper component. */ - paper: { - position: 'absolute', - overflowY: 'auto', - overflowX: 'hidden', - // So we see the popover when it's empty. - // It's most likely on issue on userland. - minWidth: 16, - minHeight: 16, - maxWidth: 'calc(100% - 32px)', - maxHeight: 'calc(100% - 32px)', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - }, +const overridesResolver = (props, styles) => { + return deepmerge(styles.root || {}, { + [`& .${popoverClasses.paper}`]: styles.paper, + }); }; -const Popover = React.forwardRef(function Popover(props, ref) { +const useUtilityClasses = (styleProps) => { + const { classes } = styleProps; + + const slots = { + root: ['root'], + paper: ['paper'], + }; + + return composeClasses(slots, getPopoverUtilityClass, classes); +}; + +const PopoverRoot = experimentalStyled( + Modal, + {}, + { + name: 'MuiPopover', + slot: 'Root', + overridesResolver, + }, +)({}); + +const PopoverPaper = experimentalStyled( + Paper, + {}, + { + name: 'MuiPopover', + slot: 'Paper', + }, +)({ + position: 'absolute', + overflowY: 'auto', + overflowX: 'hidden', + // So we see the popover when it's empty. + // It's most likely on issue on userland. + minWidth: 16, + minHeight: 16, + maxWidth: 'calc(100% - 32px)', + maxHeight: 'calc(100% - 32px)', + // We disable the focus ring for mouse, touch and keyboard users. + outline: 0, +}); + +const Popover = React.forwardRef(function Popover(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiPopover' }); const { action, anchorEl, @@ -95,7 +129,6 @@ const Popover = React.forwardRef(function Popover(props, ref) { anchorPosition, anchorReference = 'anchorEl', children, - classes, className, container: containerProp, elevation = 8, @@ -114,6 +147,21 @@ const Popover = React.forwardRef(function Popover(props, ref) { } = props; const paperRef = React.useRef(); + const styleProps = { + ...props, + anchorOrigin, + anchorReference, + elevation, + marginThreshold, + PaperProps, + transformOrigin, + TransitionComponent, + transitionDuration: transitionDurationProp, + TransitionProps, + }; + + const classes = useUtilityClasses(styleProps); + // Returns the top/left offset of the position // to attach to on the anchor element (or body if none is provided) const getAnchorOffset = React.useCallback( @@ -379,31 +427,32 @@ const Popover = React.forwardRef(function Popover(props, ref) { containerProp || (anchorEl ? ownerDocument(getAnchorEl(anchorEl)).body : undefined); return ( - - {children} - + - + ); }); @@ -548,6 +597,10 @@ Popover.propTypes = { PaperProps: PropTypes /* @typescript-to-proptypes-ignore */.shape({ component: elementTypeAcceptingRef, }), + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, /** * This is the point on the popover which * will attach to the anchor's origin. @@ -595,4 +648,4 @@ Popover.propTypes = { TransitionProps: PropTypes.object, }; -export default withStyles(styles, { name: 'MuiPopover' })(Popover); +export default Popover; diff --git a/packages/material-ui/src/Popover/Popover.test.js b/packages/material-ui/src/Popover/Popover.test.js index 92ed380077c614..697838a8188445 100644 --- a/packages/material-ui/src/Popover/Popover.test.js +++ b/packages/material-ui/src/Popover/Popover.test.js @@ -1,12 +1,18 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy, stub, useFakeTimers } from 'sinon'; -import { findOutermostIntrinsic, getClasses, createMount, describeConformance } from 'test/utils'; +import { + findOutermostIntrinsic, + createMount, + createClientRender, + describeConformanceV5, +} from 'test/utils'; import PropTypes from 'prop-types'; -import Grow from '../Grow'; -import Modal from '../Modal'; -import Paper from '../Paper'; -import Popover, { getOffsetLeft, getOffsetTop } from './Popover'; +import Grow from '@material-ui/core/Grow'; +import Modal from '@material-ui/core/Modal'; +import Paper from '@material-ui/core/Paper'; +import Popover, { popoverClasses as classes } from '@material-ui/core/Popover'; +import { getOffsetLeft, getOffsetTop } from './Popover'; import useForkRef from '../utils/useForkRef'; const mockedAnchorEl = () => { @@ -49,27 +55,26 @@ const FakePaper = React.forwardRef(function FakeWidthPaper(props, ref) { describe('', () => { // StrictModeViolation: Not using act(), prefer using createClientRender from test/utils const mount = createMount({ strict: false }); - let classes; + const render = createClientRender(); const defaultProps = { open: false, anchorEl: () => document.createElement('svg'), }; - before(() => { - classes = getClasses( - -
- , - ); - }); - - describeConformance(, () => ({ + describeConformanceV5(, () => ({ classes, inheritComponent: Modal, + render, mount, + muiName: 'MuiPopover', refInstanceof: window.HTMLDivElement, + testDeepOverrides: { slotName: 'paper', slotClassName: classes.paper }, skip: [ 'componentProp', + 'componentsProp', + 'themeDefaultProps', + 'themeStyleOverrides', + 'themeVariants', // react-transition-group issue 'reactTestRenderer', ], @@ -83,7 +88,7 @@ describe('', () => { , ); const root = wrapper.find('ForwardRef(Popover) > [data-root-node]').first(); - expect(root.type()).to.equal(Modal); + expect(root.find(Modal).exists()).to.equal(true); expect(root.props().BackdropProps.invisible).to.equal(true); }); @@ -433,7 +438,7 @@ describe('', () => { it('should warn if anchorEl is not valid', () => { expect(() => { PropTypes.checkPropTypes( - Popover.Naked.propTypes, + Popover.propTypes, { classes: {}, open: true }, 'prop', 'MockedPopover', @@ -444,7 +449,7 @@ describe('', () => { it('warns if a component for the Paper is used that cant hold a ref', () => { expect(() => { PropTypes.checkPropTypes( - Popover.Naked.propTypes, + Popover.propTypes, { ...defaultProps, classes: {}, PaperProps: { component: () =>
, elevation: 4 } }, 'prop', 'MockedPopover', diff --git a/packages/material-ui/src/Popover/index.js b/packages/material-ui/src/Popover/index.js index 44c04deafa71ff..210824f52648b8 100644 --- a/packages/material-ui/src/Popover/index.js +++ b/packages/material-ui/src/Popover/index.js @@ -1 +1,4 @@ export { default } from './Popover'; + +export { default as popoverClasses } from './popoverClasses'; +export * from './popoverClasses'; diff --git a/packages/material-ui/src/Popover/popoverClasses.d.ts b/packages/material-ui/src/Popover/popoverClasses.d.ts new file mode 100644 index 00000000000000..dba3bff5ed8ab9 --- /dev/null +++ b/packages/material-ui/src/Popover/popoverClasses.d.ts @@ -0,0 +1,7 @@ +import { PopoverClassKey } from './Popover'; + +declare const popoverClasses: Record; + +export function getPopoverUtilityClass(slot: string): string; + +export default popoverClasses; diff --git a/packages/material-ui/src/Popover/popoverClasses.js b/packages/material-ui/src/Popover/popoverClasses.js new file mode 100644 index 00000000000000..c7d09eada0eff2 --- /dev/null +++ b/packages/material-ui/src/Popover/popoverClasses.js @@ -0,0 +1,9 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export function getPopoverUtilityClass(slot) { + return generateUtilityClass('MuiPopover', slot); +} + +const popoverClasses = generateUtilityClasses('MuiPopover', ['root', 'paper']); + +export default popoverClasses;