fix: prevent asset conflicts between React and Grid.js versions

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>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,49 @@
const LIVE_REGION_ARIA_ROLES = new Set(['alert', 'status', 'log', 'marquee', 'timer']);
const hiddenElementsByDepth = [];
/**
* Hides all elements in the body element from screen-readers except
* the provided element and elements that should not be hidden from
* screen-readers.
*
* The reason we do this is because `aria-modal="true"` currently is bugged
* in Safari, and support is spotty in other browsers overall. In the future
* we should consider removing these helper functions in favor of
* `aria-modal="true"`.
*
* @param modalElement The element that should not be hidden.
*/
export function modalize(modalElement) {
const elements = Array.from(document.body.children);
const hiddenElements = [];
hiddenElementsByDepth.push(hiddenElements);
for (const element of elements) {
if (element === modalElement) continue;
if (elementShouldBeHidden(element)) {
element.setAttribute('aria-hidden', 'true');
hiddenElements.push(element);
}
}
}
/**
* Determines if the passed element should not be hidden from screen readers.
*
* @param element The element that should be checked.
*
* @return Whether the element should not be hidden from screen-readers.
*/
export function elementShouldBeHidden(element) {
const role = element.getAttribute('role');
return !(element.tagName === 'SCRIPT' || element.hasAttribute('aria-hidden') || element.hasAttribute('aria-live') || role && LIVE_REGION_ARIA_ROLES.has(role));
}
/**
* Accessibly reveals the elements hidden by the latest modal.
*/
export function unmodalize() {
const hiddenElements = hiddenElementsByDepth.pop();
if (!hiddenElements) return;
for (const element of hiddenElements) element.removeAttribute('aria-hidden');
}
//# sourceMappingURL=aria-helper.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["LIVE_REGION_ARIA_ROLES","Set","hiddenElementsByDepth","modalize","modalElement","elements","Array","from","document","body","children","hiddenElements","push","element","elementShouldBeHidden","setAttribute","role","getAttribute","tagName","hasAttribute","has","unmodalize","pop","removeAttribute"],"sources":["@wordpress/components/src/modal/aria-helper.ts"],"sourcesContent":["const LIVE_REGION_ARIA_ROLES = new Set( [\n\t'alert',\n\t'status',\n\t'log',\n\t'marquee',\n\t'timer',\n] );\n\nconst hiddenElementsByDepth: Element[][] = [];\n\n/**\n * Hides all elements in the body element from screen-readers except\n * the provided element and elements that should not be hidden from\n * screen-readers.\n *\n * The reason we do this is because `aria-modal=\"true\"` currently is bugged\n * in Safari, and support is spotty in other browsers overall. In the future\n * we should consider removing these helper functions in favor of\n * `aria-modal=\"true\"`.\n *\n * @param modalElement The element that should not be hidden.\n */\nexport function modalize( modalElement?: HTMLDivElement ) {\n\tconst elements = Array.from( document.body.children );\n\tconst hiddenElements: Element[] = [];\n\thiddenElementsByDepth.push( hiddenElements );\n\tfor ( const element of elements ) {\n\t\tif ( element === modalElement ) continue;\n\n\t\tif ( elementShouldBeHidden( element ) ) {\n\t\t\telement.setAttribute( 'aria-hidden', 'true' );\n\t\t\thiddenElements.push( element );\n\t\t}\n\t}\n}\n\n/**\n * Determines if the passed element should not be hidden from screen readers.\n *\n * @param element The element that should be checked.\n *\n * @return Whether the element should not be hidden from screen-readers.\n */\nexport function elementShouldBeHidden( element: Element ) {\n\tconst role = element.getAttribute( 'role' );\n\treturn ! (\n\t\telement.tagName === 'SCRIPT' ||\n\t\telement.hasAttribute( 'aria-hidden' ) ||\n\t\telement.hasAttribute( 'aria-live' ) ||\n\t\t( role && LIVE_REGION_ARIA_ROLES.has( role ) )\n\t);\n}\n\n/**\n * Accessibly reveals the elements hidden by the latest modal.\n */\nexport function unmodalize() {\n\tconst hiddenElements = hiddenElementsByDepth.pop();\n\tif ( ! hiddenElements ) return;\n\n\tfor ( const element of hiddenElements )\n\t\telement.removeAttribute( 'aria-hidden' );\n}\n"],"mappings":"AAAA,MAAMA,sBAAsB,GAAG,IAAIC,GAAG,CAAE,CACvC,OAAO,EACP,QAAQ,EACR,KAAK,EACL,SAAS,EACT,OAAO,CACN,CAAC;AAEH,MAAMC,qBAAkC,GAAG,EAAE;;AAE7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,QAAQA,CAAEC,YAA6B,EAAG;EACzD,MAAMC,QAAQ,GAAGC,KAAK,CAACC,IAAI,CAAEC,QAAQ,CAACC,IAAI,CAACC,QAAS,CAAC;EACrD,MAAMC,cAAyB,GAAG,EAAE;EACpCT,qBAAqB,CAACU,IAAI,CAAED,cAAe,CAAC;EAC5C,KAAM,MAAME,OAAO,IAAIR,QAAQ,EAAG;IACjC,IAAKQ,OAAO,KAAKT,YAAY,EAAG;IAEhC,IAAKU,qBAAqB,CAAED,OAAQ,CAAC,EAAG;MACvCA,OAAO,CAACE,YAAY,CAAE,aAAa,EAAE,MAAO,CAAC;MAC7CJ,cAAc,CAACC,IAAI,CAAEC,OAAQ,CAAC;IAC/B;EACD;AACD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,qBAAqBA,CAAED,OAAgB,EAAG;EACzD,MAAMG,IAAI,GAAGH,OAAO,CAACI,YAAY,CAAE,MAAO,CAAC;EAC3C,OAAO,EACNJ,OAAO,CAACK,OAAO,KAAK,QAAQ,IAC5BL,OAAO,CAACM,YAAY,CAAE,aAAc,CAAC,IACrCN,OAAO,CAACM,YAAY,CAAE,WAAY,CAAC,IACjCH,IAAI,IAAIhB,sBAAsB,CAACoB,GAAG,CAAEJ,IAAK,CAAG,CAC9C;AACF;;AAEA;AACA;AACA;AACA,OAAO,SAASK,UAAUA,CAAA,EAAG;EAC5B,MAAMV,cAAc,GAAGT,qBAAqB,CAACoB,GAAG,CAAC,CAAC;EAClD,IAAK,CAAEX,cAAc,EAAG;EAExB,KAAM,MAAME,OAAO,IAAIF,cAAc,EACpCE,OAAO,CAACU,eAAe,CAAE,aAAc,CAAC;AAC1C"}

View File

@@ -0,0 +1,289 @@
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 theyre 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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":[],"sources":["@wordpress/components/src/modal/types.ts"],"sourcesContent":["/**\n * External dependencies\n */\nimport type {\n\tAriaRole,\n\tCSSProperties,\n\tReactNode,\n\tKeyboardEventHandler,\n\tKeyboardEvent,\n\tSyntheticEvent,\n} from 'react';\n\n/**\n * WordPress dependencies\n */\nimport type { useFocusOnMount } from '@wordpress/compose';\n\nexport type ModalProps = {\n\taria?: {\n\t\t/**\n\t\t * If this property is added, it will be added to the modal content\n\t\t * `div` as `aria-describedby`.\n\t\t */\n\t\tdescribedby?: string;\n\t\t/**\n\t\t * If this property is added, it will be added to the modal content\n\t\t * `div` as `aria-labelledby`. Use this when you are rendering the title\n\t\t * yourself within the modal's content area instead of using the `title`\n\t\t * prop. This ensures the title is usable by assistive technology.\n\t\t *\n\t\t * Titles are required for accessibility reasons, see `contentLabel` and\n\t\t * `title` for other ways to provide a title.\n\t\t */\n\t\tlabelledby?: string;\n\t};\n\t/**\n\t * Class name added to the body element when the modal is open.\n\t *\n\t * @default 'modal-open'\n\t */\n\tbodyOpenClassName?: string;\n\t/**\n\t * The children elements.\n\t */\n\tchildren: ReactNode;\n\t/**\n\t * If this property is added, it will an additional class name to the modal\n\t * content `div`.\n\t */\n\tclassName?: string;\n\t/**\n\t * Label on the close button.\n\t *\n\t * @default `__( 'Close' )`\n\t */\n\tcloseButtonLabel?: string;\n\t/**\n\t * If this property is added, it will be added to the modal content `div` as\n\t * `aria-label`.\n\t *\n\t * Titles are required for accessibility reasons, see `aria.labelledby` and\n\t * `title` for other ways to provide a title.\n\t */\n\tcontentLabel?: string;\n\t/**\n\t * If this property is true, it will focus the first tabbable element\n\t * rendered in the modal.\n\t *\n\t * @default true\n\t */\n\tfocusOnMount?:\n\t\t| Parameters< typeof useFocusOnMount >[ 0 ]\n\t\t| 'firstContentElement';\n\t/**\n\t * Elements that are injected into the modal header to the left of the close button (if rendered).\n\t * Hidden if `__experimentalHideHeader` is `true`.\n\t *\n\t * @default null\n\t */\n\theaderActions?: ReactNode;\n\n\t/**\n\t * If this property is added, an icon will be added before the title.\n\t */\n\ticon?: JSX.Element;\n\t/**\n\t * If this property is set to false, the modal will not display a close icon\n\t * and cannot be dismissed.\n\t *\n\t * @default true\n\t */\n\tisDismissible?: boolean;\n\t/**\n\t * This property when set to `true` will render a full screen modal.\n\t *\n\t * @default false\n\t */\n\tisFullScreen?: boolean;\n\t/**\n\t * If this property is added it will cause the modal to render at a preset\n\t * width, or expand to fill the screen. This prop will be ignored if\n\t * `isFullScreen` is set to `true`.\n\t *\n\t * Note: `Modal`'s width can also be controlled by adjusting the width of the\n\t * modal's contents, or via CSS using the `style` prop.\n\t */\n\tsize?: 'small' | 'medium' | 'large' | 'fill';\n\t/**\n\t * Handle the key down on the modal frame `div`.\n\t */\n\tonKeyDown?: KeyboardEventHandler< HTMLDivElement >;\n\t/**\n\t * This function is called to indicate that the modal should be closed.\n\t */\n\tonRequestClose: (\n\t\tevent?: KeyboardEvent< HTMLDivElement > | SyntheticEvent\n\t) => void;\n\t/**\n\t * If this property is added, it will an additional class name to the modal\n\t * overlay `div`.\n\t */\n\toverlayClassName?: string;\n\t/**\n\t * If this property is added, it will override the default role of the\n\t * modal.\n\t *\n\t * @default 'dialog'\n\t */\n\trole?: AriaRole;\n\t/**\n\t * If this property is added, it will determine whether the modal requests\n\t * to close when a mouse click occurs outside of the modal content.\n\t *\n\t * @default true\n\t */\n\tshouldCloseOnClickOutside?: boolean;\n\t/**\n\t * If this property is added, it will determine whether the modal requests\n\t * to close when the escape key is pressed.\n\t *\n\t * @default true\n\t */\n\tshouldCloseOnEsc?: boolean;\n\t/**\n\t * If this property is added, it will be added to the modal frame `div`.\n\t */\n\tstyle?: CSSProperties;\n\t/**\n\t * This property is used as the modal header's title.\n\t *\n\t * Titles are required for accessibility reasons, see `aria.labelledby` and\n\t * `contentLabel` for other ways to provide a title.\n\t */\n\ttitle?: string;\n\t/**\n\t * When set to `true`, the Modal's header (including the icon, title and\n\t * close button) will not be rendered.\n\t *\n\t * _Warning_: This property is still experimental. “Experimental” means this\n\t * is an early implementation subject to drastic and breaking changes.\n\t *\n\t * @default false\n\t */\n\t__experimentalHideHeader?: boolean;\n};\n"],"mappings":""}