@wordpress/components
Version:
UI components for WordPress.
223 lines (202 loc) • 7.44 kB
JavaScript
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