UNPKG

terra-abstract-modal

Version:

The abstract modal is a structural component that provides the ability to display portal'd content in a layer above the app.

219 lines (205 loc) 6.49 kB
import React, { forwardRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import classNamesBind from 'classnames/bind'; import ThemeContext from 'terra-theme-context'; import VisuallyHiddenText from 'terra-visually-hidden-text'; import { FocusOn } from 'react-focus-on'; import ModalOverlay from './_ModalOverlay'; import { hideModalDomUpdates, showModalDomUpdates } from './inertHelpers'; import styles from './ModalContent.module.scss'; const cx = classNamesBind.bind(styles); const zIndexes = ['6000', '7000', '8000', '9000']; const propTypes = { /** * String that labels the modal for screen readers. */ ariaLabel: PropTypes.string, /** * String that labels the modal for screen readers. */ ariaLabelledBy: PropTypes.string, /** * String that labels the modal for screen readers. */ ariaDescribedBy: PropTypes.string, /** * Content inside the modal dialog. */ children: PropTypes.node.isRequired, /** * CSS classnames that are appended to the modal. */ classNameModal: PropTypes.string, /** * CSS classnames that are appended to the overlay. */ classNameOverlay: PropTypes.string, /** * If set to true, the modal will close when a mouseclick is triggered outside the modal. */ closeOnOutsideClick: PropTypes.bool, /** * Callback function indicating a close condition was met, should be combined with isOpen for state management. */ onRequestClose: PropTypes.func.isRequired, /** * If set to true, the modal will be fullscreen on all breakpoint sizes. */ isFullscreen: PropTypes.bool, /** * If set to true, the modal dialog with have overflow-y set to scroll. */ isScrollable: PropTypes.bool, /** * Role attribute on the modal dialog. */ role: PropTypes.string, /** * Allows assigning of root element custom data attribute for easy selecting of document base component. */ rootSelector: PropTypes.string, /** * Z-Index layer to apply to the ModalContent and ModalOverlay. */ zIndex: PropTypes.oneOf(zIndexes), /** * @private * Callback function to set the reference of the element that will receive focus when the Slide content is visible. */ setModalFocusElementRef: PropTypes.func, /** * @private * If set to true, the AbstractModal is rendered inside a NotificationDialog. */ isCalledFromNotificationDialog: PropTypes.bool, /** * If set to true, then the focus lock will get enabled. */ shouldTrapFocus: PropTypes.bool, }; const defaultProps = { classNameModal: null, classNameOverlay: null, closeOnOutsideClick: true, isFullscreen: false, isScrollable: false, role: 'dialog', rootSelector: '#root', zIndex: '6000', isCalledFromNotificationDialog: false, }; const ModalContent = forwardRef((props, ref) => { const { ariaLabel, ariaLabelledBy, ariaDescribedBy, children, classNameModal, classNameOverlay, closeOnOutsideClick, onRequestClose, role, isFullscreen, isScrollable, rootSelector, zIndex, setModalFocusElementRef, isCalledFromNotificationDialog, shouldTrapFocus, ...customProps } = props; useEffect(() => { // Store element that was last focused prior to modal opening const modalTrigger = document.activeElement; showModalDomUpdates(ref.current, rootSelector); return () => { hideModalDomUpdates(modalTrigger, rootSelector); }; }, [ref, rootSelector]); let zIndexLayer = '6000'; if (zIndexes.indexOf(zIndex) >= 0) { zIndexLayer = zIndex; } const theme = React.useContext(ThemeContext); const modalClassName = classNames(cx( 'abstract-modal', { 'is-fullscreen': isFullscreen }, `layer-${zIndexLayer}`, theme.className, ), classNameModal); const modalContainerClassName = classNames(cx('abstract-modal-container')); // Delete the closePortal prop that comes from react-portal. delete customProps.closePortal; delete customProps.fallbackFocus; const beginLabelId = ariaLabel === 'Modal' ? 'Terra.AbstractModal.BeginModalDialog' : 'Terra.AbstractModal.BeginModalDialogTitle'; const endLabelId = ariaLabel === 'Modal' ? 'Terra.AbstractModal.EndModalDialog' : 'Terra.AbstractModal.EndModalDialogTitle'; const modalContent = ( <div {...customProps} aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} aria-describedby={ariaDescribedBy} className={modalClassName} role={role} ref={ref} > <div className={modalContainerClassName} ref={setModalFocusElementRef} data-terra-abstract-modal-begin tabIndex="-1"> {(!isCalledFromNotificationDialog) && ( <FormattedMessage id={beginLabelId} values={{ title: ariaLabel }}> {text => { // In the latest version of react-intl this param is an array, when previous versions it was a string. let useText = text; if (Array.isArray(text)) { useText = text.join(''); } return ( <VisuallyHiddenText text={useText} /> ); }} </FormattedMessage> )} {children} {(!isCalledFromNotificationDialog) && ( <FormattedMessage id={endLabelId} values={{ title: ariaLabel }}> {text => { // In the latest version of react-intl this param is an array, when previous versions it was a string. let useText = text; if (Array.isArray(text)) { useText = text.join(''); } return ( <VisuallyHiddenText text={useText} /> ); }} </FormattedMessage> )} </div> </div> ); return ( <React.Fragment> <ModalOverlay onClick={closeOnOutsideClick ? onRequestClose : null} className={classNameOverlay} zIndex={zIndexLayer} /> { /* When an aria-label is set and tabIndex is set to 0, VoiceOver will read the aria-label value when the modal is opened */ } { shouldTrapFocus ? <FocusOn onClickOutside={closeOnOutsideClick ? onRequestClose : null}>{modalContent}</FocusOn> : <>{modalContent}</> } </React.Fragment> ); }); ModalContent.propTypes = propTypes; ModalContent.defaultProps = defaultProps; export default ModalContent;