UNPKG

19.9 kBJavaScriptView Raw
1'use client';
2
3import * as React from 'react';
4import PropTypes from 'prop-types';
5import clsx from 'clsx';
6import composeClasses from '@mui/utils/composeClasses';
7import HTMLElementType from '@mui/utils/HTMLElementType';
8import refType from '@mui/utils/refType';
9import elementTypeAcceptingRef from '@mui/utils/elementTypeAcceptingRef';
10import integerPropType from '@mui/utils/integerPropType';
11import chainPropTypes from '@mui/utils/chainPropTypes';
12import isHostComponent from "../utils/isHostComponent.js";
13import { styled } from "../zero-styled/index.js";
14import { useDefaultProps } from "../DefaultPropsProvider/index.js";
15import debounce from "../utils/debounce.js";
16import ownerDocument from "../utils/ownerDocument.js";
17import ownerWindow from "../utils/ownerWindow.js";
18import useForkRef from "../utils/useForkRef.js";
19import Grow from "../Grow/index.js";
20import Modal from "../Modal/index.js";
21import PaperBase from "../Paper/index.js";
22import { getPopoverUtilityClass } from "./popoverClasses.js";
23import useSlot from "../utils/useSlot.js";
24import { jsx as _jsx } from "react/jsx-runtime";
25export function getOffsetTop(rect, vertical) {
26 let offset = 0;
27 if (typeof vertical === 'number') {
28 offset = vertical;
29 } else if (vertical === 'center') {
30 offset = rect.height / 2;
31 } else if (vertical === 'bottom') {
32 offset = rect.height;
33 }
34 return offset;
35}
36export function getOffsetLeft(rect, horizontal) {
37 let offset = 0;
38 if (typeof horizontal === 'number') {
39 offset = horizontal;
40 } else if (horizontal === 'center') {
41 offset = rect.width / 2;
42 } else if (horizontal === 'right') {
43 offset = rect.width;
44 }
45 return offset;
46}
47function getTransformOriginValue(transformOrigin) {
48 return [transformOrigin.horizontal, transformOrigin.vertical].map(n => typeof n === 'number' ? `${n}px` : n).join(' ');
49}
50function resolveAnchorEl(anchorEl) {
51 return typeof anchorEl === 'function' ? anchorEl() : anchorEl;
52}
53const useUtilityClasses = ownerState => {
54 const {
55 classes
56 } = ownerState;
57 const slots = {
58 root: ['root'],
59 paper: ['paper']
60 };
61 return composeClasses(slots, getPopoverUtilityClass, classes);
62};
63export const PopoverRoot = styled(Modal, {
64 name: 'MuiPopover',
65 slot: 'Root',
66 overridesResolver: (props, styles) => styles.root
67})({});
68export const PopoverPaper = styled(PaperBase, {
69 name: 'MuiPopover',
70 slot: 'Paper',
71 overridesResolver: (props, styles) => styles.paper
72})({
73 position: 'absolute',
74 overflowY: 'auto',
75 overflowX: 'hidden',
76 // So we see the popover when it's empty.
77 // It's most likely on issue on userland.
78 minWidth: 16,
79 minHeight: 16,
80 maxWidth: 'calc(100% - 32px)',
81 maxHeight: 'calc(100% - 32px)',
82 // We disable the focus ring for mouse, touch and keyboard users.
83 outline: 0
84});
85const Popover = /*#__PURE__*/React.forwardRef(function Popover(inProps, ref) {
86 const props = useDefaultProps({
87 props: inProps,
88 name: 'MuiPopover'
89 });
90 const {
91 action,
92 anchorEl,
93 anchorOrigin = {
94 vertical: 'top',
95 horizontal: 'left'
96 },
97 anchorPosition,
98 anchorReference = 'anchorEl',
99 children,
100 className,
101 container: containerProp,
102 elevation = 8,
103 marginThreshold = 16,
104 open,
105 PaperProps: PaperPropsProp = {},
106 slots = {},
107 slotProps = {},
108 transformOrigin = {
109 vertical: 'top',
110 horizontal: 'left'
111 },
112 TransitionComponent = Grow,
113 transitionDuration: transitionDurationProp = 'auto',
114 TransitionProps: {
115 onEntering,
116 ...TransitionProps
117 } = {},
118 disableScrollLock = false,
119 ...other
120 } = props;
121 const externalPaperSlotProps = slotProps?.paper ?? PaperPropsProp;
122 const paperRef = React.useRef();
123 const ownerState = {
124 ...props,
125 anchorOrigin,
126 anchorReference,
127 elevation,
128 marginThreshold,
129 externalPaperSlotProps,
130 transformOrigin,
131 TransitionComponent,
132 transitionDuration: transitionDurationProp,
133 TransitionProps
134 };
135 const classes = useUtilityClasses(ownerState);
136
137 // Returns the top/left offset of the position
138 // to attach to on the anchor element (or body if none is provided)
139 const getAnchorOffset = React.useCallback(() => {
140 if (anchorReference === 'anchorPosition') {
141 if (process.env.NODE_ENV !== 'production') {
142 if (!anchorPosition) {
143 console.error('MUI: You need to provide a `anchorPosition` prop when using ' + '<Popover anchorReference="anchorPosition" />.');
144 }
145 }
146 return anchorPosition;
147 }
148 const resolvedAnchorEl = resolveAnchorEl(anchorEl);
149
150 // If an anchor element wasn't provided, just use the parent body element of this Popover
151 const anchorElement = resolvedAnchorEl && resolvedAnchorEl.nodeType === 1 ? resolvedAnchorEl : ownerDocument(paperRef.current).body;
152 const anchorRect = anchorElement.getBoundingClientRect();
153 if (process.env.NODE_ENV !== 'production') {
154 const box = anchorElement.getBoundingClientRect();
155 if (process.env.NODE_ENV !== 'test' && box.top === 0 && box.left === 0 && box.right === 0 && box.bottom === 0) {
156 console.warn(['MUI: The `anchorEl` prop provided to the component is invalid.', 'The anchor element should be part of the document layout.', "Make sure the element is present in the document or that it's not display none."].join('\n'));
157 }
158 }
159 return {
160 top: anchorRect.top + getOffsetTop(anchorRect, anchorOrigin.vertical),
161 left: anchorRect.left + getOffsetLeft(anchorRect, anchorOrigin.horizontal)
162 };
163 }, [anchorEl, anchorOrigin.horizontal, anchorOrigin.vertical, anchorPosition, anchorReference]);
164
165 // Returns the base transform origin using the element
166 const getTransformOrigin = React.useCallback(elemRect => {
167 return {
168 vertical: getOffsetTop(elemRect, transformOrigin.vertical),
169 horizontal: getOffsetLeft(elemRect, transformOrigin.horizontal)
170 };
171 }, [transformOrigin.horizontal, transformOrigin.vertical]);
172 const getPositioningStyle = React.useCallback(element => {
173 const elemRect = {
174 width: element.offsetWidth,
175 height: element.offsetHeight
176 };
177
178 // Get the transform origin point on the element itself
179 const elemTransformOrigin = getTransformOrigin(elemRect);
180 if (anchorReference === 'none') {
181 return {
182 top: null,
183 left: null,
184 transformOrigin: getTransformOriginValue(elemTransformOrigin)
185 };
186 }
187
188 // Get the offset of the anchoring element
189 const anchorOffset = getAnchorOffset();
190
191 // Calculate element positioning
192 let top = anchorOffset.top - elemTransformOrigin.vertical;
193 let left = anchorOffset.left - elemTransformOrigin.horizontal;
194 const bottom = top + elemRect.height;
195 const right = left + elemRect.width;
196
197 // Use the parent window of the anchorEl if provided
198 const containerWindow = ownerWindow(resolveAnchorEl(anchorEl));
199
200 // Window thresholds taking required margin into account
201 const heightThreshold = containerWindow.innerHeight - marginThreshold;
202 const widthThreshold = containerWindow.innerWidth - marginThreshold;
203
204 // Check if the vertical axis needs shifting
205 if (marginThreshold !== null && top < marginThreshold) {
206 const diff = top - marginThreshold;
207 top -= diff;
208 elemTransformOrigin.vertical += diff;
209 } else if (marginThreshold !== null && bottom > heightThreshold) {
210 const diff = bottom - heightThreshold;
211 top -= diff;
212 elemTransformOrigin.vertical += diff;
213 }
214 if (process.env.NODE_ENV !== 'production') {
215 if (elemRect.height > heightThreshold && elemRect.height && heightThreshold) {
216 console.error(['MUI: The popover component is too tall.', `Some part of it can not be seen on the screen (${elemRect.height - heightThreshold}px).`, 'Please consider adding a `max-height` to improve the user-experience.'].join('\n'));
217 }
218 }
219
220 // Check if the horizontal axis needs shifting
221 if (marginThreshold !== null && left < marginThreshold) {
222 const diff = left - marginThreshold;
223 left -= diff;
224 elemTransformOrigin.horizontal += diff;
225 } else if (right > widthThreshold) {
226 const diff = right - widthThreshold;
227 left -= diff;
228 elemTransformOrigin.horizontal += diff;
229 }
230 return {
231 top: `${Math.round(top)}px`,
232 left: `${Math.round(left)}px`,
233 transformOrigin: getTransformOriginValue(elemTransformOrigin)
234 };
235 }, [anchorEl, anchorReference, getAnchorOffset, getTransformOrigin, marginThreshold]);
236 const [isPositioned, setIsPositioned] = React.useState(open);
237 const setPositioningStyles = React.useCallback(() => {
238 const element = paperRef.current;
239 if (!element) {
240 return;
241 }
242 const positioning = getPositioningStyle(element);
243 if (positioning.top !== null) {
244 element.style.setProperty('top', positioning.top);
245 }
246 if (positioning.left !== null) {
247 element.style.left = positioning.left;
248 }
249 element.style.transformOrigin = positioning.transformOrigin;
250 setIsPositioned(true);
251 }, [getPositioningStyle]);
252 React.useEffect(() => {
253 if (disableScrollLock) {
254 window.addEventListener('scroll', setPositioningStyles);
255 }
256 return () => window.removeEventListener('scroll', setPositioningStyles);
257 }, [anchorEl, disableScrollLock, setPositioningStyles]);
258 const handleEntering = (element, isAppearing) => {
259 if (onEntering) {
260 onEntering(element, isAppearing);
261 }
262 setPositioningStyles();
263 };
264 const handleExited = () => {
265 setIsPositioned(false);
266 };
267 React.useEffect(() => {
268 if (open) {
269 setPositioningStyles();
270 }
271 });
272 React.useImperativeHandle(action, () => open ? {
273 updatePosition: () => {
274 setPositioningStyles();
275 }
276 } : null, [open, setPositioningStyles]);
277 React.useEffect(() => {
278 if (!open) {
279 return undefined;
280 }
281 const handleResize = debounce(() => {
282 setPositioningStyles();
283 });
284 const containerWindow = ownerWindow(anchorEl);
285 containerWindow.addEventListener('resize', handleResize);
286 return () => {
287 handleResize.clear();
288 containerWindow.removeEventListener('resize', handleResize);
289 };
290 }, [anchorEl, open, setPositioningStyles]);
291 let transitionDuration = transitionDurationProp;
292 if (transitionDurationProp === 'auto' && !TransitionComponent.muiSupportAuto) {
293 transitionDuration = undefined;
294 }
295
296 // If the container prop is provided, use that
297 // If the anchorEl prop is provided, use its parent body element as the container
298 // If neither are provided let the Modal take care of choosing the container
299 const container = containerProp || (anchorEl ? ownerDocument(resolveAnchorEl(anchorEl)).body : undefined);
300 const externalForwardedProps = {
301 slots,
302 slotProps: {
303 ...slotProps,
304 paper: externalPaperSlotProps
305 }
306 };
307 const [PaperSlot, paperProps] = useSlot('paper', {
308 elementType: PopoverPaper,
309 externalForwardedProps,
310 additionalProps: {
311 elevation,
312 className: clsx(classes.paper, externalPaperSlotProps?.className),
313 style: isPositioned ? externalPaperSlotProps.style : {
314 ...externalPaperSlotProps.style,
315 opacity: 0
316 }
317 },
318 ownerState
319 });
320 const [RootSlot, {
321 slotProps: rootSlotPropsProp,
322 ...rootProps
323 }] = useSlot('root', {
324 elementType: PopoverRoot,
325 externalForwardedProps,
326 additionalProps: {
327 slotProps: {
328 backdrop: {
329 invisible: true
330 }
331 },
332 container,
333 open
334 },
335 ownerState,
336 className: clsx(classes.root, className)
337 });
338 const handlePaperRef = useForkRef(paperRef, paperProps.ref);
339 return /*#__PURE__*/_jsx(RootSlot, {
340 ...rootProps,
341 ...(!isHostComponent(RootSlot) && {
342 slotProps: rootSlotPropsProp,
343 disableScrollLock
344 }),
345 ...other,
346 ref: ref,
347 children: /*#__PURE__*/_jsx(TransitionComponent, {
348 appear: true,
349 in: open,
350 onEntering: handleEntering,
351 onExited: handleExited,
352 timeout: transitionDuration,
353 ...TransitionProps,
354 children: /*#__PURE__*/_jsx(PaperSlot, {
355 ...paperProps,
356 ref: handlePaperRef,
357 children: children
358 })
359 })
360 });
361});
362process.env.NODE_ENV !== "production" ? Popover.propTypes /* remove-proptypes */ = {
363 // ┌────────────────────────────── Warning ──────────────────────────────┐
364 // │ These PropTypes are generated from the TypeScript type definitions. │
365 // │ To update them, edit the d.ts file and run `pnpm proptypes`. │
366 // └─────────────────────────────────────────────────────────────────────┘
367 /**
368 * A ref for imperative actions.
369 * It currently only supports updatePosition() action.
370 */
371 action: refType,
372 /**
373 * An HTML element, [PopoverVirtualElement](https://mui.com/material-ui/react-popover/#virtual-element),
374 * or a function that returns either.
375 * It's used to set the position of the popover.
376 */
377 anchorEl: chainPropTypes(PropTypes.oneOfType([HTMLElementType, PropTypes.func]), props => {
378 if (props.open && (!props.anchorReference || props.anchorReference === 'anchorEl')) {
379 const resolvedAnchorEl = resolveAnchorEl(props.anchorEl);
380 if (resolvedAnchorEl && resolvedAnchorEl.nodeType === 1) {
381 const box = resolvedAnchorEl.getBoundingClientRect();
382 if (process.env.NODE_ENV !== 'test' && box.top === 0 && box.left === 0 && box.right === 0 && box.bottom === 0) {
383 return new Error(['MUI: The `anchorEl` prop provided to the component is invalid.', 'The anchor element should be part of the document layout.', "Make sure the element is present in the document or that it's not display none."].join('\n'));
384 }
385 } else {
386 return new Error(['MUI: The `anchorEl` prop provided to the component is invalid.', `It should be an Element or PopoverVirtualElement instance but it's \`${resolvedAnchorEl}\` instead.`].join('\n'));
387 }
388 }
389 return null;
390 }),
391 /**
392 * This is the point on the anchor where the popover's
393 * `anchorEl` will attach to. This is not used when the
394 * anchorReference is 'anchorPosition'.
395 *
396 * Options:
397 * vertical: [top, center, bottom];
398 * horizontal: [left, center, right].
399 * @default {
400 * vertical: 'top',
401 * horizontal: 'left',
402 * }
403 */
404 anchorOrigin: PropTypes.shape({
405 horizontal: PropTypes.oneOfType([PropTypes.oneOf(['center', 'left', 'right']), PropTypes.number]).isRequired,
406 vertical: PropTypes.oneOfType([PropTypes.oneOf(['bottom', 'center', 'top']), PropTypes.number]).isRequired
407 }),
408 /**
409 * This is the position that may be used to set the position of the popover.
410 * The coordinates are relative to the application's client area.
411 */
412 anchorPosition: PropTypes.shape({
413 left: PropTypes.number.isRequired,
414 top: PropTypes.number.isRequired
415 }),
416 /**
417 * This determines which anchor prop to refer to when setting
418 * the position of the popover.
419 * @default 'anchorEl'
420 */
421 anchorReference: PropTypes.oneOf(['anchorEl', 'anchorPosition', 'none']),
422 /**
423 * A backdrop component. This prop enables custom backdrop rendering.
424 * @deprecated Use `slotProps.root.slots.backdrop` instead. While this prop currently works, it will be removed in the next major version.
425 * Use the `slotProps.root.slots.backdrop` prop to make your application ready for the next version of Material UI.
426 * @default styled(Backdrop, {
427 * name: 'MuiModal',
428 * slot: 'Backdrop',
429 * overridesResolver: (props, styles) => {
430 * return styles.backdrop;
431 * },
432 * })({
433 * zIndex: -1,
434 * })
435 */
436 BackdropComponent: PropTypes.elementType,
437 /**
438 * Props applied to the [`Backdrop`](/material-ui/api/backdrop/) element.
439 * @deprecated Use `slotProps.root.slotProps.backdrop` instead.
440 */
441 BackdropProps: PropTypes.object,
442 /**
443 * The content of the component.
444 */
445 children: PropTypes.node,
446 /**
447 * Override or extend the styles applied to the component.
448 */
449 classes: PropTypes.object,
450 /**
451 * @ignore
452 */
453 className: PropTypes.string,
454 /**
455 * An HTML element, component instance, or function that returns either.
456 * The `container` will passed to the Modal component.
457 *
458 * By default, it uses the body of the anchorEl's top-level document object,
459 * so it's simply `document.body` most of the time.
460 */
461 container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, PropTypes.func]),
462 /**
463 * Disable the scroll lock behavior.
464 * @default false
465 */
466 disableScrollLock: PropTypes.bool,
467 /**
468 * The elevation of the popover.
469 * @default 8
470 */
471 elevation: integerPropType,
472 /**
473 * Specifies how close to the edge of the window the popover can appear.
474 * If null, the popover will not be constrained by the window.
475 * @default 16
476 */
477 marginThreshold: PropTypes.number,
478 /**
479 * Callback fired when the component requests to be closed.
480 * The `reason` parameter can optionally be used to control the response to `onClose`.
481 */
482 onClose: PropTypes.func,
483 /**
484 * If `true`, the component is shown.
485 */
486 open: PropTypes.bool.isRequired,
487 /**
488 * Props applied to the [`Paper`](https://mui.com/material-ui/api/paper/) element.
489 *
490 * This prop is an alias for `slotProps.paper` and will be overriden by it if both are used.
491 * @deprecated Use `slotProps.paper` instead.
492 *
493 * @default {}
494 */
495 PaperProps: PropTypes /* @typescript-to-proptypes-ignore */.shape({
496 component: elementTypeAcceptingRef
497 }),
498 /**
499 * The props used for each slot inside.
500 * @default {}
501 */
502 slotProps: PropTypes.shape({
503 paper: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
504 root: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
505 }),
506 /**
507 * The components used for each slot inside.
508 * @default {}
509 */
510 slots: PropTypes.shape({
511 paper: PropTypes.elementType,
512 root: PropTypes.elementType
513 }),
514 /**
515 * The system prop that allows defining system overrides as well as additional CSS styles.
516 */
517 sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object]),
518 /**
519 * This is the point on the popover which
520 * will attach to the anchor's origin.
521 *
522 * Options:
523 * vertical: [top, center, bottom, x(px)];
524 * horizontal: [left, center, right, x(px)].
525 * @default {
526 * vertical: 'top',
527 * horizontal: 'left',
528 * }
529 */
530 transformOrigin: PropTypes.shape({
531 horizontal: PropTypes.oneOfType([PropTypes.oneOf(['center', 'left', 'right']), PropTypes.number]).isRequired,
532 vertical: PropTypes.oneOfType([PropTypes.oneOf(['bottom', 'center', 'top']), PropTypes.number]).isRequired
533 }),
534 /**
535 * The component used for the transition.
536 * [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
537 * @default Grow
538 */
539 TransitionComponent: PropTypes.elementType,
540 /**
541 * Set to 'auto' to automatically calculate transition time based on height.
542 * @default 'auto'
543 */
544 transitionDuration: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number, PropTypes.shape({
545 appear: PropTypes.number,
546 enter: PropTypes.number,
547 exit: PropTypes.number
548 })]),
549 /**
550 * Props applied to the transition element.
551 * By default, the element is based on this [`Transition`](https://reactcommunity.org/react-transition-group/transition/) component.
552 * @default {}
553 */
554 TransitionProps: PropTypes.object
555} : void 0;
556export default Popover;
\No newline at end of file