@zendeskgarden/react-modals
Version:
Components relating to modals in the Garden Design System
192 lines (189 loc) • 6.45 kB
JavaScript
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/
import React__default, { forwardRef, useContext, useRef, useState, useEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { ThemeContext } from 'styled-components';
import PropTypes from 'prop-types';
import { useDocument, useText } from '@zendeskgarden/react-theming';
import { useModal } from '@zendeskgarden/container-modal';
import { mergeRefs } from 'react-merge-refs';
import isWindow from 'dom-helpers/isWindow';
import ownerDocument from 'dom-helpers/ownerDocument';
import ownerWindow from 'dom-helpers/ownerWindow';
import css from 'dom-helpers/css';
import getScrollbarSize from 'dom-helpers/scrollbarSize';
import { StyledBackdrop } from '../styled/StyledBackdrop.js';
import '../styled/StyledBody.js';
import '../styled/StyledClose.js';
import '../styled/StyledFooter.js';
import '../styled/StyledFooterItem.js';
import '../styled/StyledHeader.js';
import '../styled/StyledDangerIcon.js';
import { StyledModal } from '../styled/StyledModal.js';
import '../styled/StyledTooltipDialogBackdrop.js';
import '../styled/StyledTooltipWrapper.js';
import '../styled/StyledTooltipDialog.js';
import '../styled/StyledTooltipDialogTitle.js';
import '../styled/StyledTooltipDialogBody.js';
import '../styled/StyledTooltipDialogFooter.js';
import '../styled/StyledTooltipDialogFooterItem.js';
import '../styled/StyledTooltipDialogClose.js';
import '../styled/StyledDrawer.js';
import '../styled/StyledDrawerHeader.js';
import '../styled/StyledDrawerClose.js';
import '../styled/StyledDrawerBody.js';
import '../styled/StyledDrawerFooter.js';
import '../styled/StyledDrawerFooterItem.js';
import { ModalsContext } from '../utils/useModalContext.js';
import { Body } from './Body.js';
import { Close } from './Close.js';
import { Footer } from './Footer.js';
import { FooterItem } from './FooterItem.js';
import { Header } from './Header.js';
const isOverflowing = element => {
const doc = ownerDocument(element);
const win = ownerWindow(doc);
const isBody = element && element.tagName.toLowerCase() === 'body';
if (!isWindow(doc) && !isBody) {
return element.scrollHeight > element.clientHeight;
}
const style = win.getComputedStyle(doc.body);
const marginLeft = parseInt(style.getPropertyValue('margin-left'), 10);
const marginRight = parseInt(style.getPropertyValue('margin-right'), 10);
return marginLeft + doc.body.clientWidth + marginRight < win.innerWidth;
};
const ModalComponent = forwardRef((_ref, ref) => {
let {
backdropProps,
children,
onClose,
isLarge,
isCentered,
isAnimated,
id,
appendToNode,
focusOnMount,
restoreFocus,
...modalProps
} = _ref;
const theme = useContext(ThemeContext);
const modalRef = useRef(null);
const environment = useDocument(theme);
const [isCloseButtonPresent, setIsCloseButtonPresent] = useState(false);
const [hasHeader, setHasHeader] = useState(false);
const {
getBackdropProps,
getModalProps,
getTitleProps,
getContentProps,
getCloseProps
} = useModal({
idPrefix: id,
onClose,
modalRef,
focusOnMount,
restoreFocus
});
useEffect(() => {
if (!environment) {
return undefined;
}
const htmlElement = environment.querySelector('html');
const bodyElement = environment.querySelector('body');
let previousHtmlOverflow;
let previousBodyPaddingRight;
if (bodyElement) {
if (isOverflowing(bodyElement)) {
const scrollbarSize = getScrollbarSize();
const bodyPaddingRight = parseInt(css(bodyElement, 'paddingRight') || '0', 10);
previousBodyPaddingRight = bodyElement.style.paddingRight;
bodyElement.style.paddingRight = `${bodyPaddingRight + scrollbarSize}px`;
}
if (htmlElement) {
previousHtmlOverflow = htmlElement.style.overflow;
htmlElement.style.overflow = 'hidden';
}
return () => {
if (htmlElement) {
htmlElement.style.overflow = previousHtmlOverflow;
}
bodyElement.style.paddingRight = previousBodyPaddingRight;
};
}
return undefined;
}, [environment]);
const rootNode = useMemo(() => {
if (appendToNode) {
return appendToNode;
}
if (environment) {
return environment.body;
}
return undefined;
}, [appendToNode, environment]);
const value = useMemo(() => ({
isLarge,
isCloseButtonPresent,
hasHeader,
setHasHeader,
getTitleProps,
getContentProps,
getCloseProps,
setIsCloseButtonPresent
}), [isLarge, hasHeader, isCloseButtonPresent, getTitleProps, getContentProps, getCloseProps]);
const modalContainerProps = getModalProps({
'aria-describedby': undefined,
...(hasHeader ? {} : {
'aria-labelledby': undefined
})
});
const attribute = hasHeader ? 'aria-labelledby' : 'aria-label';
const defaultValue = hasHeader ? modalContainerProps['aria-labelledby'] : 'Modal dialog';
const labelValue = hasHeader ? modalContainerProps['aria-labelledby'] : modalProps['aria-label'];
const ariaProps = {
[attribute]: useText(ModalComponent, {
[attribute]: labelValue
}, attribute, defaultValue)
};
if (!rootNode) {
return null;
}
return createPortal(React__default.createElement(ModalsContext.Provider, {
value: value
}, React__default.createElement(StyledBackdrop, Object.assign({
$isCentered: isCentered,
$isAnimated: isAnimated
}, getBackdropProps(backdropProps)), React__default.createElement(StyledModal, Object.assign({
$isCentered: isCentered,
$isAnimated: isAnimated,
$isLarge: isLarge
}, modalContainerProps, ariaProps, modalProps, {
ref: mergeRefs([ref, modalRef])
}), children))), rootNode);
});
ModalComponent.displayName = 'Modal';
ModalComponent.propTypes = {
backdropProps: PropTypes.object,
isLarge: PropTypes.bool,
isAnimated: PropTypes.bool,
isCentered: PropTypes.bool,
focusOnMount: PropTypes.bool,
restoreFocus: PropTypes.bool,
onClose: PropTypes.func,
appendToNode: PropTypes.any
};
ModalComponent.defaultProps = {
isAnimated: true,
isCentered: true
};
const Modal = ModalComponent;
Modal.Body = Body;
Modal.Close = Close;
Modal.Footer = Footer;
Modal.FooterItem = FooterItem;
Modal.Header = Header;
export { Modal, ModalComponent };