UNPKG

@wordpress/components

Version:
249 lines (248 loc) 9.56 kB
// packages/components/src/modal/index.tsx import clsx from "clsx"; import { createPortal, useCallback, useEffect, useRef, useState, forwardRef, useLayoutEffect, createContext, useContext } from "@wordpress/element"; import { useInstanceId, useFocusReturn, useFocusOnMount, useConstrainedTabbing, useMergeRefs } from "@wordpress/compose"; import { __ } from "@wordpress/i18n"; import { close } from "@wordpress/icons"; import { getScrollContainer } from "@wordpress/dom"; import * as ariaHelper from "./aria-helper"; import Button from "../button"; import StyleProvider from "../style-provider"; import { withIgnoreIMEEvents } from "../utils/with-ignore-ime-events"; import { Spacer } from "../spacer"; import { useModalExitAnimation } from "./use-modal-exit-animation"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; var ModalContext = createContext(/* @__PURE__ */ new Set()); ModalContext.displayName = "ModalContext"; var bodyOpenClasses = /* @__PURE__ */ new Map(); function UnforwardedModal(props, forwardedRef) { const { bodyOpenClassName = "modal-open", role = "dialog", title = null, focusOnMount = true, shouldCloseOnEsc = true, shouldCloseOnClickOutside = true, isDismissible = true, /* Accessibility. */ aria = { labelledby: void 0, describedby: void 0 }, onRequestClose, icon, closeButtonLabel, children, style, overlayClassName: overlayClassnameProp, className, contentLabel, onKeyDown, isFullScreen = false, size, headerActions = null, __experimentalHideHeader = false } = props; const ref = useRef(); const instanceId = useInstanceId(Modal); const headingId = title ? `components-modal-header-${instanceId}` : aria.labelledby; const focusOnMountRef = useFocusOnMount(focusOnMount === "firstContentElement" ? "firstElement" : focusOnMount); const constrainedTabbingRef = useConstrainedTabbing(); const focusReturnRef = useFocusReturn(); const contentRef = useRef(null); const childrenContainerRef = useRef(null); const [hasScrolledContent, setHasScrolledContent] = useState(false); const [hasScrollableContent, setHasScrollableContent] = useState(false); let sizeClass; if (isFullScreen || size === "fill") { sizeClass = "is-full-screen"; } else if (size) { sizeClass = `has-size-${size}`; } const isContentScrollable = useCallback(() => { if (!contentRef.current) { return; } const closestScrollContainer = getScrollContainer(contentRef.current); if (contentRef.current === closestScrollContainer) { setHasScrollableContent(true); } else { setHasScrollableContent(false); } }, [contentRef]); useEffect(() => { ariaHelper.modalize(ref.current); return () => ariaHelper.unmodalize(); }, []); const onRequestCloseRef = useRef(); useEffect(() => { onRequestCloseRef.current = onRequestClose; }, [onRequestClose]); const dismissers = useContext(ModalContext); const [nestedDismissers] = useState(() => /* @__PURE__ */ new Set()); useEffect(() => { dismissers.add(onRequestCloseRef); for (const dismisser of dismissers) { if (dismisser !== onRequestCloseRef) { dismisser.current?.(); } } return () => { for (const dismisser of nestedDismissers) { dismisser.current?.(); } dismissers.delete(onRequestCloseRef); }; }, [dismissers, nestedDismissers]); useEffect(() => { var _bodyOpenClasses$get; const theClass = bodyOpenClassName; const oneMore = 1 + ((_bodyOpenClasses$get = bodyOpenClasses.get(theClass)) !== null && _bodyOpenClasses$get !== void 0 ? _bodyOpenClasses$get : 0); bodyOpenClasses.set(theClass, oneMore); document.body.classList.add(bodyOpenClassName); return () => { const oneLess = bodyOpenClasses.get(theClass) - 1; if (oneLess === 0) { document.body.classList.remove(theClass); bodyOpenClasses.delete(theClass); } else { bodyOpenClasses.set(theClass, oneLess); } }; }, [bodyOpenClassName]); const { closeModal, frameRef, frameStyle, overlayClassname } = useModalExitAnimation(); 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 (shouldCloseOnEsc && (event.code === "Escape" || event.key === "Escape") && !event.defaultPrevented) { event.preventDefault(); closeModal().then(() => onRequestClose(event)); } } const onContentContainerScroll = useCallback((e) => { var _e$currentTarget$scro; const scrollY = (_e$currentTarget$scro = 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]); let pressTarget = null; const overlayPressHandlers = { onPointerDown: (event) => { if (event.target === event.currentTarget) { pressTarget = event.target; event.preventDefault(); } }, // Closes the modal with two exceptions. 1. Opening the context menu on // the overlay. 2. Pressing on the overlay then dragging the pointer // over the modal and releasing. Due to the modal being a child of the // overlay, such a gesture is a `click` on the overlay and cannot be // excepted by a `click` handler. Thus the tactic of handling // `pointerup` and comparing its target to that of the `pointerdown`. onPointerUp: ({ target, button }) => { const isSameTarget = target === pressTarget; pressTarget = null; if (button === 0 && isSameTarget) { closeModal().then(() => onRequestClose()); } } }; const modal = ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions /* @__PURE__ */ _jsx("div", { ref: useMergeRefs([ref, forwardedRef]), className: clsx("components-modal__screen-overlay", overlayClassname, overlayClassnameProp), onKeyDown: withIgnoreIMEEvents(handleEscapeKeyDown), ...shouldCloseOnClickOutside ? overlayPressHandlers : {}, children: /* @__PURE__ */ _jsx(StyleProvider, { document, children: /* @__PURE__ */ _jsx("div", { className: clsx("components-modal__frame", sizeClass, className), style: { ...frameStyle, ...style }, ref: useMergeRefs([frameRef, constrainedTabbingRef, focusReturnRef, focusOnMount !== "firstContentElement" ? focusOnMountRef : null]), role, "aria-label": contentLabel, "aria-labelledby": contentLabel ? void 0 : headingId, "aria-describedby": aria.describedby, tabIndex: -1, onKeyDown, children: /* @__PURE__ */ _jsxs("div", { className: clsx("components-modal__content", { "hide-header": __experimentalHideHeader, "is-scrollable": hasScrollableContent, "has-scrolled-content": hasScrolledContent }), role: "document", onScroll: onContentContainerScroll, ref: contentRef, "aria-label": hasScrollableContent ? __("Scrollable section") : void 0, tabIndex: hasScrollableContent ? 0 : void 0, children: [!__experimentalHideHeader && /* @__PURE__ */ _jsxs("div", { className: "components-modal__header", children: [/* @__PURE__ */ _jsxs("div", { className: "components-modal__header-heading-container", children: [icon && /* @__PURE__ */ _jsx("span", { className: "components-modal__icon-container", "aria-hidden": true, children: icon }), title && /* @__PURE__ */ _jsx("h1", { id: headingId, className: "components-modal__header-heading", children: title })] }), headerActions, isDismissible && /* @__PURE__ */ _jsxs(_Fragment, { children: [/* @__PURE__ */ _jsx(Spacer, { marginBottom: 0, marginLeft: 2 }), /* @__PURE__ */ _jsx(Button, { size: "compact", onClick: (event) => closeModal().then(() => onRequestClose(event)), icon: close, label: closeButtonLabel || __("Close") })] })] }), /* @__PURE__ */ _jsx("div", { ref: useMergeRefs([childrenContainerRef, focusOnMount === "firstContentElement" ? focusOnMountRef : null]), className: "components-modal__children-container", children })] }) }) }) }) ); return createPortal(/* @__PURE__ */ _jsx(ModalContext.Provider, { value: nestedDismissers, children: modal }), document.body); } var Modal = forwardRef(UnforwardedModal); var modal_default = Modal; export { Modal, modal_default as default }; //# sourceMappingURL=index.js.map