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

399
node_modules/@wordpress/components/src/modal/index.tsx generated vendored Normal file
View File

@@ -0,0 +1,399 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import type {
ForwardedRef,
KeyboardEvent,
MutableRefObject,
UIEvent,
} from 'react';
/**
* 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';
import type { ModalProps } from './types';
// Used to track and dismiss the prior modal when another opens unless nested.
const ModalContext = createContext<
MutableRefObject< ModalProps[ 'onRequestClose' ] | undefined >[]
>( [] );
// Used to track body class names applied while modals are open.
const bodyOpenClasses = new Map< string, number >();
function UnforwardedModal(
props: ModalProps,
forwardedRef: ForwardedRef< HTMLDivElement >
) {
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< HTMLDivElement >();
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< HTMLDivElement >( null );
const childrenContainerRef = useRef< HTMLDivElement >( 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< ModalProps[ 'onRequestClose' ] >();
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< typeof dismissers >( [] );
// 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( () => {
const theClass = bodyOpenClassName;
const oneMore = 1 + ( bodyOpenClasses.get( theClass ) ?? 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: KeyboardEvent< HTMLDivElement > ) {
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: UIEvent< HTMLDivElement > ) => {
const scrollY = e?.currentTarget?.scrollTop ?? -1;
if ( ! hasScrolledContent && scrollY > 0 ) {
setHasScrolledContent( true );
} else if ( hasScrolledContent && scrollY <= 0 ) {
setHasScrolledContent( false );
}
},
[ hasScrolledContent ]
);
let pressTarget: EventTarget | null = null;
const overlayPressHandlers: {
onPointerDown: React.PointerEventHandler< HTMLDivElement >;
onPointerUp: React.PointerEventHandler< HTMLDivElement >;
} = {
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
<div
ref={ useMergeRefs( [ ref, forwardedRef ] ) }
className={ classnames(
'components-modal__screen-overlay',
overlayClassName
) }
onKeyDown={ handleEscapeKeyDown }
{ ...( shouldCloseOnClickOutside ? overlayPressHandlers : {} ) }
>
<StyleProvider document={ document }>
<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 }
>
<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 && (
<div className="components-modal__header">
<div className="components-modal__header-heading-container">
{ icon && (
<span
className="components-modal__icon-container"
aria-hidden
>
{ icon }
</span>
) }
{ title && (
<h1
id={ headingId }
className="components-modal__header-heading"
>
{ title }
</h1>
) }
</div>
{ headerActions }
{ isDismissible && (
<Button
onClick={ onRequestClose }
icon={ close }
label={
closeButtonLabel || __( 'Close' )
}
/>
) }
</div>
) }
<div
ref={ useMergeRefs( [
childrenContainerRef,
focusOnMount === 'firstContentElement'
? focusOnMountRef
: null,
] ) }
>
{ children }
</div>
</div>
</div>
</StyleProvider>
</div>
);
return createPortal(
<ModalContext.Provider value={ nestedDismissers.current }>
{ modal }
</ModalContext.Provider>,
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;