UNPKG

@wordpress/components

Version:
223 lines (202 loc) 7.44 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import { createElement } from "@wordpress/element"; /** * External dependencies */ import classnames from 'classnames'; /** * WordPress dependencies */ import { createPortal, useCallback, useEffect, useRef, useState, forwardRef, useLayoutEffect } from '@wordpress/element'; import { useInstanceId, useFocusReturn, useFocusOnMount, __experimentalUseFocusOutside as useFocusOutside, useConstrainedTabbing, useMergeRefs } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { close } from '@wordpress/icons'; import { getScrollContainer } from '@wordpress/dom'; /** * Internal dependencies */ import * as ariaHelper from './aria-helper'; import Button from '../button'; import StyleProvider from '../style-provider'; // Used to count the number of open modals. let openModalCount = 0; function UnforwardedModal(props, forwardedRef) { const { bodyOpenClassName = 'modal-open', role = 'dialog', title = null, focusOnMount = true, shouldCloseOnEsc = true, shouldCloseOnClickOutside = true, isDismissible = true, /* Accessibility. */ aria = { labelledby: undefined, describedby: undefined }, onRequestClose, icon, closeButtonLabel, children, style, overlayClassName, className, contentLabel, onKeyDown, isFullScreen = false, __experimentalHideHeader = false } = props; const ref = useRef(); const instanceId = useInstanceId(Modal); const headingId = title ? `components-modal-header-${instanceId}` : aria.labelledby; const focusOnMountRef = useFocusOnMount(focusOnMount); const constrainedTabbingRef = useConstrainedTabbing(); const focusReturnRef = useFocusReturn(); const focusOutsideProps = useFocusOutside(onRequestClose); const contentRef = useRef(null); const childrenContainerRef = useRef(null); const [hasScrolledContent, setHasScrolledContent] = useState(false); const [hasScrollableContent, setHasScrollableContent] = useState(false); // Determines whether the Modal content is scrollable and updates the state. const isContentScrollable = useCallback(() => { if (!contentRef.current) { return; } const closestScrollContainer = getScrollContainer(contentRef.current); if (contentRef.current === closestScrollContainer) { setHasScrollableContent(true); } else { setHasScrollableContent(false); } }, [contentRef]); useEffect(() => { openModalCount++; if (openModalCount === 1) { ariaHelper.hideApp(ref.current); document.body.classList.add(bodyOpenClassName); } return () => { openModalCount--; if (openModalCount === 0) { document.body.classList.remove(bodyOpenClassName); ariaHelper.showApp(); } }; }, [bodyOpenClassName]); // Calls the isContentScrollable callback when the Modal children container resizes. useLayoutEffect(() => { if (!window.ResizeObserver || !childrenContainerRef.current) { return; } const resizeObserver = new ResizeObserver(isContentScrollable); resizeObserver.observe(childrenContainerRef.current); isContentScrollable(); return () => { resizeObserver.disconnect(); }; }, [isContentScrollable, childrenContainerRef]); function handleEscapeKeyDown(event) { if ( // Ignore keydowns from IMEs event.nativeEvent.isComposing || // Workaround for Mac Safari where the final Enter/Backspace of an IME composition // is `isComposing=false`, even though it's technically still part of the composition. // These can only be detected by keyCode. event.keyCode === 229) { return; } if (shouldCloseOnEsc && event.code === 'Escape' && !event.defaultPrevented) { event.preventDefault(); if (onRequestClose) { onRequestClose(event); } } } const onContentContainerScroll = useCallback(e => { var _e$currentTarget$scro, _e$currentTarget; const scrollY = (_e$currentTarget$scro = e === null || e === void 0 ? void 0 : (_e$currentTarget = e.currentTarget) === null || _e$currentTarget === void 0 ? void 0 : _e$currentTarget.scrollTop) !== null && _e$currentTarget$scro !== void 0 ? _e$currentTarget$scro : -1; if (!hasScrolledContent && scrollY > 0) { setHasScrolledContent(true); } else if (hasScrolledContent && scrollY <= 0) { setHasScrolledContent(false); } }, [hasScrolledContent]); return createPortal( // eslint-disable-next-line jsx-a11y/no-static-element-interactions createElement("div", { ref: useMergeRefs([ref, forwardedRef]), className: classnames('components-modal__screen-overlay', overlayClassName), onKeyDown: handleEscapeKeyDown }, createElement(StyleProvider, { document: document }, createElement("div", _extends({ className: classnames('components-modal__frame', className, { 'is-full-screen': isFullScreen }), style: style, ref: useMergeRefs([constrainedTabbingRef, focusReturnRef, focusOnMountRef]), role: role, "aria-label": contentLabel, "aria-labelledby": contentLabel ? undefined : headingId, "aria-describedby": aria.describedby, tabIndex: -1 }, shouldCloseOnClickOutside ? focusOutsideProps : {}, { onKeyDown: onKeyDown }), createElement("div", { className: classnames('components-modal__content', { 'hide-header': __experimentalHideHeader, 'is-scrollable': hasScrollableContent, 'has-scrolled-content': hasScrolledContent }), role: "document", onScroll: onContentContainerScroll, ref: contentRef, "aria-label": hasScrollableContent ? __('Scrollable section') : undefined, tabIndex: hasScrollableContent ? 0 : undefined }, !__experimentalHideHeader && createElement("div", { className: "components-modal__header" }, createElement("div", { className: "components-modal__header-heading-container" }, icon && createElement("span", { className: "components-modal__icon-container", "aria-hidden": true }, icon), title && createElement("h1", { id: headingId, className: "components-modal__header-heading" }, title)), isDismissible && createElement(Button, { onClick: onRequestClose, icon: close, label: closeButtonLabel || __('Close') })), createElement("div", { ref: childrenContainerRef }, children))))), document.body); } /** * Modals give users information and choices related to a task they’re trying to * accomplish. They can contain critical information, require decisions, or * involve multiple tasks. * * ```jsx * import { Button, Modal } from '@wordpress/components'; * import { useState } from '@wordpress/element'; * * const MyModal = () => { * const [ isOpen, setOpen ] = useState( false ); * const openModal = () => setOpen( true ); * const closeModal = () => setOpen( false ); * * return ( * <> * <Button variant="secondary" onClick={ openModal }> * Open Modal * </Button> * { isOpen && ( * <Modal title="This is my modal" onRequestClose={ closeModal }> * <Button variant="secondary" onClick={ closeModal }> * My custom close button * </Button> * </Modal> * ) } * </> * ); * }; * ``` */ export const Modal = forwardRef(UnforwardedModal); export default Modal; //# sourceMappingURL=index.js.map