UNPKG

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