Add coexistence checks to all enqueue methods to prevent loading both React and Grid.js assets simultaneously. Changes: - ReactAdmin.php: Only enqueue React assets when ?react=1 - Init.php: Skip Grid.js when React active on admin pages - Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0 - Customer.php, Product.php, License.php: Add coexistence checks Now the toggle between Classic and React versions works correctly. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
289 lines
10 KiB
JavaScript
289 lines
10 KiB
JavaScript
import { createElement } from "react";
|
||
/**
|
||
* External dependencies
|
||
*/
|
||
import classnames from 'classnames';
|
||
/**
|
||
* WordPress dependencies
|
||
*/
|
||
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';
|
||
|
||
/**
|
||
* Internal dependencies
|
||
*/
|
||
import * as ariaHelper from './aria-helper';
|
||
import Button from '../button';
|
||
import StyleProvider from '../style-provider';
|
||
// Used to track and dismiss the prior modal when another opens unless nested.
|
||
const ModalContext = createContext([]);
|
||
|
||
// Used to track body class names applied while modals are open.
|
||
const bodyOpenClasses = 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: undefined,
|
||
describedby: undefined
|
||
},
|
||
onRequestClose,
|
||
icon,
|
||
closeButtonLabel,
|
||
children,
|
||
style,
|
||
overlayClassName,
|
||
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;
|
||
|
||
// The focus hook does not support 'firstContentElement' but this is a valid
|
||
// value for the Modal's focusOnMount prop. The following code ensures the focus
|
||
// hook will focus the first focusable node within the element to which it is applied.
|
||
// When `firstContentElement` is passed as the value of the focusOnMount prop,
|
||
// the focus hook is applied to the Modal's content element.
|
||
// Otherwise, the focus hook is applied to the Modal's ref. This ensures that the
|
||
// focus hook will focus the first element in the Modal's **content** when
|
||
// `firstContentElement` is passed.
|
||
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}`;
|
||
}
|
||
|
||
// 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]);
|
||
|
||
// Accessibly isolates/unisolates the modal.
|
||
useEffect(() => {
|
||
ariaHelper.modalize(ref.current);
|
||
return () => ariaHelper.unmodalize();
|
||
}, []);
|
||
|
||
// Keeps a fresh ref for the subsequent effect.
|
||
const refOnRequestClose = useRef();
|
||
useEffect(() => {
|
||
refOnRequestClose.current = onRequestClose;
|
||
}, [onRequestClose]);
|
||
|
||
// The list of `onRequestClose` callbacks of open (non-nested) Modals. Only
|
||
// one should remain open at a time and the list enables closing prior ones.
|
||
const dismissers = useContext(ModalContext);
|
||
// Used for the tracking and dismissing any nested modals.
|
||
const nestedDismissers = useRef([]);
|
||
|
||
// Updates the stack tracking open modals at this level and calls
|
||
// onRequestClose for any prior and/or nested modals as applicable.
|
||
useEffect(() => {
|
||
dismissers.push(refOnRequestClose);
|
||
const [first, second] = dismissers;
|
||
if (second) first?.current?.();
|
||
const nested = nestedDismissers.current;
|
||
return () => {
|
||
nested[0]?.current?.();
|
||
dismissers.shift();
|
||
};
|
||
}, [dismissers]);
|
||
|
||
// Adds/removes the value of bodyOpenClassName to body element.
|
||
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]);
|
||
|
||
// 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.key === 'Escape') && !event.defaultPrevented) {
|
||
event.preventDefault();
|
||
if (onRequestClose) {
|
||
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;
|
||
// Avoids focus changing so that focus return works as expected.
|
||
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) onRequestClose();
|
||
}
|
||
};
|
||
const modal =
|
||
// 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,
|
||
...(shouldCloseOnClickOutside ? overlayPressHandlers : {})
|
||
}, createElement(StyleProvider, {
|
||
document: document
|
||
}, createElement("div", {
|
||
className: classnames('components-modal__frame', sizeClass, className),
|
||
style: style,
|
||
ref: useMergeRefs([constrainedTabbingRef, focusReturnRef, focusOnMount !== 'firstContentElement' ? focusOnMountRef : null]),
|
||
role: role,
|
||
"aria-label": contentLabel,
|
||
"aria-labelledby": contentLabel ? undefined : headingId,
|
||
"aria-describedby": aria.describedby,
|
||
tabIndex: -1,
|
||
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)), headerActions, isDismissible && createElement(Button, {
|
||
onClick: onRequestClose,
|
||
icon: close,
|
||
label: closeButtonLabel || __('Close')
|
||
})), createElement("div", {
|
||
ref: useMergeRefs([childrenContainerRef, focusOnMount === 'firstContentElement' ? focusOnMountRef : null])
|
||
}, children)))));
|
||
return createPortal(createElement(ModalContext.Provider, {
|
||
value: nestedDismissers.current
|
||
}, modal), 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
|