mdc-react
Version:
Material Components for the web implemented in React
196 lines (162 loc) • 6.72 kB
JSX
import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useUpdated } from '../hooks';
import { clone } from '../component';
import Layer from '../layer';
import { numbers, cssClasses, Origin } from './constants';
import { getAnchorOrigin } from './utils';
const MenuSurface = forwardRef(({
anchor,
anchorRef: _anchorRef,
anchorOrigin: _anchorOrigin = Origin.TOP_LEFT,
transformOrigin: _transformOrigin = _anchorOrigin,
open = false,
modal = false,
quick = false,
fixed = false,
persistent = false,
fullWidth = false,
onClose = Function.prototype,
onKeyDown = Function.prototype,
className,
...props
}, ref) => {
const rootRef = useRef();
const anchorRef = useRef(_anchorRef?.current);
useImperativeHandle(ref, () => rootRef.current);
useEffect(() => {
if (!_anchorRef) return;
anchorRef.current = _anchorRef.current;
}, [_anchorRef]);
useUpdated(() => {
if (!open || persistent) return;
function handleBodyClick(event) {
onClose(event);
}
document.body.addEventListener('click', handleBodyClick, true);
return () => {
document.body.removeEventListener('click', handleBodyClick, true);
};
}, [open, persistent, onClose]);
useUpdated(() => {
if (!open || !rootRef.current || !anchorRef.current) return;
const anchor = anchorRef.current;
const { clientWidth: width, clientHeight: height } = rootRef.current;
const anchorClientRect = anchor.getBoundingClientRect();
const anchorDimensions = modal ? anchorClientRect : {
top: anchor.offsetTop,
left: anchor.offsetLeft,
bottom: anchor.offsetTop + anchor.offsetHeight,
right: anchor.offsetLeft + anchor.offsetWidth,
width: anchor.offsetWidth,
height: anchor.offsetHeight
};
const style = {
top: anchorDimensions.top,
left: anchorDimensions.left,
width: fullWidth ? '100%' : undefined,
maxWidth: fullWidth ? `${anchorDimensions.width}px` : undefined,
position: fixed ? 'fixed' : 'absolute',
transformOrigin: _transformOrigin
};
const scrollY = (modal && !fixed) ? window.scrollY : 0;
const scrollX = (modal && !fixed) ? window.scrollX : 0;
const anchorOrigin = getAnchorOrigin(_anchorOrigin);
const transformOrigin = getAnchorOrigin(_transformOrigin);
if (anchorOrigin.top) {
style.top = anchorDimensions.top;
} else if (anchorOrigin.bottom) {
style.top = anchorDimensions.bottom;
}
if (anchorOrigin.left) {
style.left = anchorDimensions.left;
} else if (anchorOrigin.right) {
style.left = anchorDimensions.right;
}
if (transformOrigin.top) {
const top = scrollY + style.top;
const bottomOverflow = scrollY + (anchorOrigin.bottom ? anchorClientRect.bottom : anchorClientRect.top) + height - window.innerHeight;
style.top = bottomOverflow > 0 ? (top - bottomOverflow) : top;
} else if (transformOrigin.bottom) {
const top = scrollY + style.top - height;
const topOverflow = scrollY + (anchorOrigin.bottom ? anchorClientRect.bottom : anchorClientRect.top) - height;
style.top = topOverflow > 0 ? top : 0;
}
if (transformOrigin.left) {
const left = style.left;
const rightOverflow = scrollX + window.innerWidth - (anchorOrigin.left ? anchorClientRect.left : anchorClientRect.right) + width;
style.left = rightOverflow > 0 ? left : left - Math.abs(rightOverflow);
} else if (transformOrigin.right) {
const left = style.left - width;
const leftOverflow = scrollX + (anchorOrigin.right ? anchorClientRect.right : anchorClientRect.left) - width;
style.left = leftOverflow > 0 ? left : 0;
}
rootRef.current.style.top = `${style.top}px`;
rootRef.current.style.left = `${style.left}px`;
rootRef.current.style.position = style.position;
rootRef.current.style.width = style.width;
rootRef.current.style.maxWidth = style.maxWidth;
rootRef.current.style.transformOrigin = style.transformOrigin;
}, [open, modal, fixed, _anchorOrigin, _transformOrigin]);
const handleKeyDown = useCallback(event => {
if (event.key === 'Escape' && !persistent) {
event.stopPropagation();
onClose(event);
}
onKeyDown(event);
}, [persistent, onKeyDown, onClose]);
const isBelowAnchor = (
_anchorOrigin.includes('bottom') &&
_transformOrigin.includes('top')
);
const classNames = classnames(cssClasses.SURFACE, {
[cssClasses.SURFACE_FIXED]: fixed,
[cssClasses.SURFACE_BELOW_ANCHOR]: isBelowAnchor
}, className);
return (<>
{anchor &&
clone(anchor, { ref: anchorRef })
}
<Layer
in={open}
modal={modal}
timeout={quick ? 0 : {
enter: numbers.TRANSITION_OPEN_DURATION,
exit: numbers.TRANSITION_CLOSE_DURATION
}}
classNames={quick ? {
enterDone: cssClasses.SURFACE_OPEN,
} : {
enter: cssClasses.SURFACE_ANIMATING_OPEN,
enterActive: cssClasses.SURFACE_ANIMATING_OPEN,
enterDone: cssClasses.SURFACE_OPEN,
exit: cssClasses.SURFACE_OPEN,
exitActive: cssClasses.SURFACE_ANIMATING_CLOSED
}}
mountOnEnter
unmountOnExit
>
<div
ref={rootRef}
className={classNames}
onKeyDown={handleKeyDown}
{...props}
/>
</Layer>
</>);
});
MenuSurface.displayName = 'MDCMenuSurface';
MenuSurface.propTypes = {
anchor: PropTypes.element,
anchorRef: PropTypes.object,
anchorOrigin: PropTypes.oneOf(Object.values(Origin)),
open: PropTypes.bool,
modal: PropTypes.bool,
quick: PropTypes.bool,
fixed: PropTypes.bool,
persistent: PropTypes.bool,
fullWidth: PropTypes.bool,
onClose: PropTypes.func
};
export default MenuSurface;