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,76 @@
# Keyboard Shortcuts
`<KeyboardShortcuts />` is a component which handles keyboard sequences during the lifetime of the rendering element.
When passed children, it will capture key events which occur on or within the children. If no children are passed, events are captured on the document.
It uses the [Mousetrap](https://craig.is/killing/mice) library to implement keyboard sequence bindings.
## Example
Render `<KeyboardShortcuts />` with a `shortcuts` prop object:
```jsx
import { useState } from 'react';
import { KeyboardShortcuts } from '@wordpress/components';
const MyKeyboardShortcuts = () => {
const [ isAllSelected, setIsAllSelected ] = useState( false );
const selectAll = () => {
setIsAllSelected( true );
};
return (
<div>
<KeyboardShortcuts
shortcuts={ {
'mod+a': selectAll,
} }
/>
[cmd/ctrl + A] Combination pressed? { isAllSelected ? 'Yes' : 'No' }
</div>
);
};
```
## Props
The component accepts the following props:
### children
Elements to render, upon whom key events are to be monitored.
- Type: `ReactNode`
- Required: No
### shortcuts
An object of shortcut bindings, where each key is a keyboard combination, the value of which is the callback to be invoked when the key combination is pressed.
- Type: `Object`
- Required: Yes
**Note:** The value of each shortcut should be a consistent function reference, not an anonymous function. Otherwise, the callback will not be correctly unbound when the component unmounts.
**Note:** The `KeyboardShortcuts` component will not update to reflect a changed `shortcuts` prop. If you need to change shortcuts, mount a separate `KeyboardShortcuts` element, which can be achieved by assigning a unique `key` prop.
### bindGlobal
By default, a callback will not be invoked if the key combination occurs in an editable field. Pass `bindGlobal` as `true` if the key events should be observed globally, including within editable fields.
- Type: `Boolean`
- Required: No
_Tip:_ If you need some but not all keyboard events to be observed globally, simply render two distinct `KeyboardShortcuts` elements, one with and one without the `bindGlobal` prop.
### eventName
By default, a callback is invoked in response to the `keydown` event. To override this, pass `eventName` with the name of a specific keyboard event.
- Type: `String`
- Required: No
## References
- [Mousetrap documentation](https://craig.is/killing/mice)

View File

@@ -0,0 +1,2 @@
const KeyboardShortcuts = () => null;
export default KeyboardShortcuts;

View File

@@ -0,0 +1,93 @@
/**
* WordPress dependencies
*/
import { useRef, Children } from '@wordpress/element';
import { useKeyboardShortcut } from '@wordpress/compose';
/**
* Internal dependencies
*/
import type { KeyboardShortcutProps, KeyboardShortcutsProps } from './types';
function KeyboardShortcut( {
target,
callback,
shortcut,
bindGlobal,
eventName,
}: KeyboardShortcutProps ) {
useKeyboardShortcut( shortcut, callback, {
bindGlobal,
target,
eventName,
} );
return null;
}
/**
* `KeyboardShortcuts` is a component which handles keyboard sequences during the lifetime of the rendering element.
*
* When passed children, it will capture key events which occur on or within the children. If no children are passed, events are captured on the document.
*
* It uses the [Mousetrap](https://craig.is/killing/mice) library to implement keyboard sequence bindings.
*
* ```jsx
* import { KeyboardShortcuts } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const MyKeyboardShortcuts = () => {
* const [ isAllSelected, setIsAllSelected ] = useState( false );
* const selectAll = () => {
* setIsAllSelected( true );
* };
*
* return (
* <div>
* <KeyboardShortcuts
* shortcuts={ {
* 'mod+a': selectAll,
* } }
* />
* [cmd/ctrl + A] Combination pressed? { isAllSelected ? 'Yes' : 'No' }
* </div>
* );
* };
* ```
*/
function KeyboardShortcuts( {
children,
shortcuts,
bindGlobal,
eventName,
}: KeyboardShortcutsProps ) {
const target = useRef( null );
const element = Object.entries( shortcuts ?? {} ).map(
( [ shortcut, callback ] ) => (
<KeyboardShortcut
key={ shortcut }
shortcut={ shortcut }
callback={ callback }
bindGlobal={ bindGlobal }
eventName={ eventName }
target={ target }
/>
)
);
// Render as non-visual if there are no children pressed. Keyboard
// events will be bound to the document instead.
if ( ! Children.count( children ) ) {
return <>{ element }</>;
}
return (
<div ref={ target }>
{ element }
{ children }
</div>
);
}
export default KeyboardShortcuts;

View File

@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';
/**
* Internal dependencies
*/
import KeyboardShortcuts from '..';
const meta: Meta< typeof KeyboardShortcuts > = {
component: KeyboardShortcuts,
title: 'Components/KeyboardShortcuts',
parameters: {
controls: { expanded: true },
docs: { canvas: { sourceState: 'shown' } },
},
};
export default meta;
const Template: StoryFn< typeof KeyboardShortcuts > = ( props ) => (
<KeyboardShortcuts { ...props } />
);
export const Default = Template.bind( {} );
Default.args = {
shortcuts: {
// eslint-disable-next-line no-alert
a: () => window.alert( 'You hit "a"!' ),
// eslint-disable-next-line no-alert
b: () => window.alert( 'You hit "b"!' ),
},
children: (
<div>
<p>{ `Hit the "a" or "b" key in this textarea:` }</p>
<textarea />
</div>
),
};
Default.parameters = {
docs: {
source: {
code: `
<KeyboardShortcuts
shortcuts={{
a: () => window.alert('You hit "a"!'),
b: () => window.alert('You hit "b"!'),
}}
>
<div>
<p>
Hit the "a" or "b" key in this textarea:
</p>
<textarea />
</div>
</KeyboardShortcuts>
`,
},
},
};

View File

@@ -0,0 +1,113 @@
/**
* External dependencies
*/
import { createEvent, fireEvent, render, screen } from '@testing-library/react';
/**
* Internal dependencies
*/
import KeyboardShortcuts from '..';
describe( 'KeyboardShortcuts', () => {
function keyPress(
which: KeyboardEvent[ 'which' ],
target: Parameters< typeof fireEvent >[ 0 ]
) {
[ 'keydown', 'keypress', 'keyup' ].forEach( ( eventName ) => {
const event = createEvent(
eventName,
target,
{
bubbles: true,
keyCode: which,
which,
},
{ EventType: 'KeyboardEvent' }
);
fireEvent( target, event );
} );
}
it( 'should capture key events', async () => {
const spy = jest.fn();
render(
<KeyboardShortcuts
shortcuts={ {
d: spy,
} }
/>
);
keyPress( 68, document );
expect( spy ).toHaveBeenCalled();
} );
it( 'should capture key events globally', () => {
const spy = jest.fn();
render(
<div>
<KeyboardShortcuts
bindGlobal
shortcuts={ {
d: spy,
} }
/>
<textarea></textarea>
</div>
);
keyPress( 68, screen.getByRole( 'textbox' ) );
expect( spy ).toHaveBeenCalled();
} );
it( 'should capture key events on specific event', () => {
const spy = jest.fn();
render(
<div>
<KeyboardShortcuts
eventName="keyup"
shortcuts={ {
d: spy,
} }
/>
<textarea></textarea>
</div>
);
keyPress( 68, screen.getByRole( 'textbox' ) );
expect( spy.mock.calls[ 0 ][ 0 ].type ).toBe( 'keyup' );
} );
it( 'should capture key events on children', () => {
const spy = jest.fn();
render(
<div>
<KeyboardShortcuts
shortcuts={ {
d: spy,
} }
>
<textarea></textarea>
</KeyboardShortcuts>
<textarea></textarea>
</div>
);
const textareas = screen.getAllByRole( 'textbox' );
// Outside scope
keyPress( 68, textareas[ 1 ] );
expect( spy ).not.toHaveBeenCalled();
// Inside scope
keyPress( 68, textareas[ 0 ] );
expect( spy ).toHaveBeenCalled();
} );
} );

View File

@@ -0,0 +1,51 @@
/**
* WordPress dependencies
*/
import type { useKeyboardShortcut } from '@wordpress/compose';
// TODO: We wouldn't have to do this if this type was exported from `@wordpress/compose`.
type WPKeyboardShortcutConfig = NonNullable<
Parameters< typeof useKeyboardShortcut >[ 2 ]
>;
export type KeyboardShortcutProps = {
shortcut: string | string[];
/**
* @see {@link https://craig.is/killing/mice Mousetrap documentation}
*/
callback: ( event: Mousetrap.ExtendedKeyboardEvent, combo: string ) => void;
} & Pick< WPKeyboardShortcutConfig, 'bindGlobal' | 'eventName' | 'target' >;
export type KeyboardShortcutsProps = {
/**
* Elements to render, upon whom key events are to be monitored.
*/
children?: React.ReactNode;
/**
* An object of shortcut bindings, where each key is a keyboard combination,
* the value of which is the callback to be invoked when the key combination is pressed.
*
* The value of each shortcut should be a consistent function reference, not an anonymous function.
* Otherwise, the callback will not be correctly unbound when the component unmounts.
*
* The `KeyboardShortcuts` component will not update to reflect a changed `shortcuts` prop.
* If you need to change shortcuts, mount a separate `KeyboardShortcuts` element,
* which can be achieved by assigning a unique `key` prop.
*
* @see {@link https://craig.is/killing/mice Mousetrap documentation}
*/
shortcuts: Record< string, KeyboardShortcutProps[ 'callback' ] >;
/**
* By default, a callback will not be invoked if the key combination occurs in an editable field.
* Pass `bindGlobal` as `true` if the key events should be observed globally, including within editable fields.
*
* Tip: If you need some but not all keyboard events to be observed globally,
* simply render two distinct `KeyboardShortcuts` elements, one with and one without the `bindGlobal` prop.
*/
bindGlobal?: KeyboardShortcutProps[ 'bindGlobal' ];
/**
* By default, a callback is invoked in response to the `keydown` event.
* To override this, pass `eventName` with the name of a specific keyboard event.
*/
eventName?: KeyboardShortcutProps[ 'eventName' ];
};