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,118 @@
# GradientPicker
GradientPicker is a React component that renders a color gradient picker to define a multi step gradient. There's either a _linear_ or a _radial_ type available.
![gradient-picker](https://user-images.githubusercontent.com/881729/147505438-3818c4c7-65b5-4394-b97b-af903c62adce.png)
## Usage
Render a GradientPicker.
```jsx
import { useState } from 'react';
import { GradientPicker } from '@wordpress/components';
const myGradientPicker = () => {
const [ gradient, setGradient ] = useState( null );
return (
<GradientPicker
__nextHasNoMargin
value={ gradient }
onChange={ ( currentGradient ) => setGradient( currentGradient ) }
gradients={ [
{
name: 'JShine',
gradient:
'linear-gradient(135deg,#12c2e9 0%,#c471ed 50%,#f64f59 100%)',
slug: 'jshine',
},
{
name: 'Moonlit Asteroid',
gradient:
'linear-gradient(135deg,#0F2027 0%, #203A43 0%, #2c5364 100%)',
slug: 'moonlit-asteroid',
},
{
name: 'Rastafarie',
gradient:
'linear-gradient(135deg,#1E9600 0%, #FFF200 0%, #FF0000 100%)',
slug: 'rastafari',
},
] }
/>
);
};
```
## Props
The component accepts the following props:
### `className`: `string`
The class name added to the wrapper.
- Required: No
### `value`: `string`
The current value of the gradient. Pass a css gradient like `linear-gradient(90deg, rgb(6, 147, 227) 0%, rgb(155, 81, 224) 100%)`. Optionally pass in a `null` value to specify no gradient is currently selected.
- Required: No
- Default: `linear-gradient(90deg, rgb(6, 147, 227) 0%, rgb(155, 81, 224) 100%)`
### `onChange`: `( currentGradient: string | undefined ) => void`
The function called when a new gradient has been defined. It is passed the `currentGradient` as an argument.
- Required: Yes
### `gradients`: `GradientsProp[]`
An array of objects of predefined gradients displayed above the gradient selector.
- Required: No
- Default: `[]`
### `clearable`: `boolean`
Whether the palette should have a clearing button or not.
- Required: No
- Default: true
### `disableCustomGradients`: `boolean`
If true, the gradient picker will not be displayed and only defined gradients from `gradients` are available.
- Required: No
- Default: false
### `__nextHasNoMargin`: `boolean`
Start opting into the new margin-free styles that will become the default in a future version, currently scheduled to be WordPress 6.4. (The prop can be safely removed once this happens.)
- Required: No
- Default: `false`
### `headingLevel`: `1 | 2 | 3 | 4 | 5 | 6 | '1' | '2' | '3' | '4' | '5' | '6'`
The heading level. Only applies in cases where gradients are provided from multiple origins (ie. when the array passed as the `gradients` prop contains two or more items).
- Required: No
- Default: `2`
### `asButtons`: `boolean`
Whether the control should present as a set of buttons, each with its own tab stop.
- Required: No
- Default: `false`
### `loop`: `boolean`
Prevents keyboard interaction from wrapping around. Only used when `asButtons` is not true.
- Required: No
- Default: `true`

View File

@@ -0,0 +1,286 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useInstanceId } from '@wordpress/compose';
import { useCallback, useMemo } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import CircularOptionPicker from '../circular-option-picker';
import CustomGradientPicker from '../custom-gradient-picker';
import { VStack } from '../v-stack';
import { ColorHeading } from '../color-palette/styles';
import { Spacer } from '../spacer';
import type {
GradientPickerComponentProps,
PickerProps,
OriginObject,
GradientObject,
} from './types';
// The Multiple Origin Gradients have a `gradients` property (an array of
// gradient objects), while Single Origin ones have a `gradient` property.
const isMultipleOriginObject = (
obj: Record< string, any >
): obj is OriginObject =>
Array.isArray( obj.gradients ) && ! ( 'gradient' in obj );
const isMultipleOriginArray = ( arr: any[] ): arr is OriginObject[] => {
return (
arr.length > 0 &&
arr.every( ( gradientObj ) => isMultipleOriginObject( gradientObj ) )
);
};
function SingleOrigin( {
className,
clearGradient,
gradients,
onChange,
value,
...additionalProps
}: PickerProps< GradientObject > ) {
const gradientOptions = useMemo( () => {
return gradients.map( ( { gradient, name, slug }, index ) => (
<CircularOptionPicker.Option
key={ slug }
value={ gradient }
isSelected={ value === gradient }
tooltipText={
name ||
// translators: %s: gradient code e.g: "linear-gradient(90deg, rgba(98,16,153,1) 0%, rgba(172,110,22,1) 100%);".
sprintf( __( 'Gradient code: %s' ), gradient )
}
style={ { color: 'rgba( 0,0,0,0 )', background: gradient } }
onClick={
value === gradient
? clearGradient
: () => onChange( gradient, index )
}
aria-label={
name
? // translators: %s: The name of the gradient e.g: "Angular red to blue".
sprintf( __( 'Gradient: %s' ), name )
: // translators: %s: gradient code e.g: "linear-gradient(90deg, rgba(98,16,153,1) 0%, rgba(172,110,22,1) 100%);".
sprintf( __( 'Gradient code: %s' ), gradient )
}
/>
) );
}, [ gradients, value, onChange, clearGradient ] );
return (
<CircularOptionPicker.OptionGroup
className={ className }
options={ gradientOptions }
{ ...additionalProps }
/>
);
}
function MultipleOrigin( {
className,
clearGradient,
gradients,
onChange,
value,
headingLevel,
}: PickerProps< OriginObject > ) {
const instanceId = useInstanceId( MultipleOrigin );
return (
<VStack spacing={ 3 } className={ className }>
{ gradients.map( ( { name, gradients: gradientSet }, index ) => {
const id = `color-palette-${ instanceId }-${ index }`;
return (
<VStack spacing={ 2 } key={ index }>
<ColorHeading level={ headingLevel } id={ id }>
{ name }
</ColorHeading>
<SingleOrigin
clearGradient={ clearGradient }
gradients={ gradientSet }
onChange={ ( gradient ) =>
onChange( gradient, index )
}
value={ value }
aria-labelledby={ id }
/>
</VStack>
);
} ) }
</VStack>
);
}
function Component( props: PickerProps< any > ) {
const {
asButtons,
loop,
actions,
headingLevel,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
...additionalProps
} = props;
const options = isMultipleOriginArray( props.gradients ) ? (
<MultipleOrigin headingLevel={ headingLevel } { ...additionalProps } />
) : (
<SingleOrigin { ...additionalProps } />
);
let metaProps:
| { asButtons: false; loop?: boolean; 'aria-label': string }
| { asButtons: false; loop?: boolean; 'aria-labelledby': string }
| { asButtons: true };
if ( asButtons ) {
metaProps = { asButtons: true };
} else {
const _metaProps: { asButtons: false; loop?: boolean } = {
asButtons: false,
loop,
};
if ( ariaLabel ) {
metaProps = { ..._metaProps, 'aria-label': ariaLabel };
} else if ( ariaLabelledby ) {
metaProps = {
..._metaProps,
'aria-labelledby': ariaLabelledby,
};
} else {
metaProps = {
..._metaProps,
'aria-label': __( 'Custom color picker.' ),
};
}
}
return (
<CircularOptionPicker
{ ...metaProps }
actions={ actions }
options={ options }
/>
);
}
/**
* GradientPicker is a React component that renders a color gradient picker to
* define a multi step gradient. There's either a _linear_ or a _radial_ type
* available.
*
* ```jsx
*import { GradientPicker } from '@wordpress/components';
*import { useState } from '@wordpress/element';
*
*const myGradientPicker = () => {
* const [ gradient, setGradient ] = useState( null );
*
* return (
* <GradientPicker
* __nextHasNoMargin
* value={ gradient }
* onChange={ ( currentGradient ) => setGradient( currentGradient ) }
* gradients={ [
* {
* name: 'JShine',
* gradient:
* 'linear-gradient(135deg,#12c2e9 0%,#c471ed 50%,#f64f59 100%)',
* slug: 'jshine',
* },
* {
* name: 'Moonlit Asteroid',
* gradient:
* 'linear-gradient(135deg,#0F2027 0%, #203A43 0%, #2c5364 100%)',
* slug: 'moonlit-asteroid',
* },
* {
* name: 'Rastafarie',
* gradient:
* 'linear-gradient(135deg,#1E9600 0%, #FFF200 0%, #FF0000 100%)',
* slug: 'rastafari',
* },
* ] }
* />
* );
*};
*```
*
*/
export function GradientPicker( {
/** Start opting into the new margin-free styles that will become the default in a future version. */
__nextHasNoMargin = false,
className,
gradients = [],
onChange,
value,
clearable = true,
disableCustomGradients = false,
__experimentalIsRenderedInSidebar,
headingLevel = 2,
...additionalProps
}: GradientPickerComponentProps ) {
const clearGradient = useCallback(
() => onChange( undefined ),
[ onChange ]
);
if ( ! __nextHasNoMargin ) {
deprecated( 'Outer margin styles for wp.components.GradientPicker', {
since: '6.1',
version: '6.4',
hint: 'Set the `__nextHasNoMargin` prop to true to start opting into the new styles, which will become the default in a future version',
} );
}
const deprecatedMarginSpacerProps = ! __nextHasNoMargin
? {
marginTop: ! gradients.length ? 3 : undefined,
marginBottom: ! clearable ? 6 : 0,
}
: {};
return (
// Outmost Spacer wrapper can be removed when deprecation period is over
<Spacer marginBottom={ 0 } { ...deprecatedMarginSpacerProps }>
<VStack spacing={ gradients.length ? 4 : 0 }>
{ ! disableCustomGradients && (
<CustomGradientPicker
__nextHasNoMargin
__experimentalIsRenderedInSidebar={
__experimentalIsRenderedInSidebar
}
value={ value }
onChange={ onChange }
/>
) }
{ ( gradients.length > 0 || clearable ) && (
<Component
{ ...additionalProps }
className={ className }
clearGradient={ clearGradient }
gradients={ gradients }
onChange={ onChange }
value={ value }
actions={
clearable &&
! disableCustomGradients && (
<CircularOptionPicker.ButtonAction
onClick={ clearGradient }
>
{ __( 'Clear' ) }
</CircularOptionPicker.ButtonAction>
)
}
headingLevel={ headingLevel }
/>
) }
</VStack>
</Spacer>
);
}
export default GradientPicker;

View File

@@ -0,0 +1,105 @@
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';
/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import GradientPicker from '..';
const meta: Meta< typeof GradientPicker > = {
title: 'Components/GradientPicker',
component: GradientPicker,
parameters: {
controls: { expanded: true },
docs: { canvas: { sourceState: 'shown' } },
actions: { argTypesRegex: '^on.*' },
},
argTypes: {
value: { control: { type: null } },
},
};
export default meta;
const GRADIENTS = [
{
name: 'Vivid cyan blue to vivid purple',
gradient:
'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
slug: 'vivid-cyan-blue-to-vivid-purple',
},
{
name: 'Light green cyan to vivid green cyan',
gradient:
'linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%)',
slug: 'light-green-cyan-to-vivid-green-cyan',
},
{
name: 'Luminous vivid amber to luminous vivid orange',
gradient:
'linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%)',
slug: 'luminous-vivid-amber-to-luminous-vivid-orange',
},
{
name: 'Luminous vivid orange to vivid red',
gradient:
'linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%)',
slug: 'luminous-vivid-orange-to-vivid-red',
},
{
name: 'Very light gray to cyan bluish gray',
gradient:
'linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%)',
slug: 'very-light-gray-to-cyan-bluish-gray',
},
{
name: 'Cool to warm spectrum',
gradient:
'linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%)',
slug: 'cool-to-warm-spectrum',
},
];
const Template: StoryFn< typeof GradientPicker > = ( {
onChange,
...props
} ) => {
const [ gradient, setGradient ] =
useState< ( typeof props )[ 'value' ] >( null );
return (
<GradientPicker
{ ...props }
value={ gradient }
onChange={ ( ...changeArgs ) => {
setGradient( ...changeArgs );
onChange?.( ...changeArgs );
} }
/>
);
};
export const Default = Template.bind( {} );
Default.args = {
__nextHasNoMargin: true,
gradients: GRADIENTS,
};
export const WithNoExistingGradients = Template.bind( {} );
WithNoExistingGradients.args = {
...Default.args,
gradients: [],
};
export const MultipleOrigins = Template.bind( {} );
MultipleOrigins.args = {
...Default.args,
gradients: [
{ name: 'Origin 1', gradients: GRADIENTS },
{ name: 'Origin 2', gradients: GRADIENTS },
],
};

View File

@@ -0,0 +1,122 @@
/**
* Internal dependencies
*/
import type { HeadingSize } from '../heading/types';
export type GradientObject = {
gradient: string;
name: string;
slug: string;
};
export type OriginObject = { name: string; gradients: GradientObject[] };
export type GradientsProp = GradientObject[] | OriginObject[];
type GradientPickerBaseProps = {
/**
* The class name added to the wrapper.
*/
className?: string;
/**
* The function called when a new gradient has been defined. It is passed to
* the `currentGradient` as an argument.
*/
onChange: ( currentGradient: string | undefined ) => void;
/**
* The current value of the gradient. Pass a css gradient string (See default value for example).
* Optionally pass in a `null` value to specify no gradient is currently selected.
*
* @default 'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)'
*/
value?: GradientObject[ 'gradient' ] | null;
/**
* Whether the palette should have a clearing button or not.
*
* @default true
*/
clearable?: boolean;
/**
* The heading level. Only applies in cases where gradients are provided
* from multiple origins (ie. when the array passed as the `gradients` prop
* contains two or more items).
*
* @default 2
*/
headingLevel?: HeadingSize;
/**
* Whether the control should present as a set of buttons,
* each with its own tab stop.
*
* @default false
*/
asButtons?: boolean;
/**
* Prevents keyboard interaction from wrapping around.
* Only used when `asButtons` is not true.
*
* @default true
*/
loop?: boolean;
} & (
| {
/**
* A label to identify the purpose of the control.
*
* @todo [#54055] Either this or `aria-labelledby` should be required
*/
'aria-label'?: string;
'aria-labelledby'?: never;
}
| {
/**
* An ID of an element to provide a label for the control.
*
* @todo [#54055] Either this or `aria-label` should be required
*/
'aria-labelledby'?: string;
'aria-label'?: never;
}
);
export type GradientPickerComponentProps = GradientPickerBaseProps & {
/**
* An array of objects as predefined gradients displayed above the gradient
* selector. Alternatively, if there are multiple sets (or 'origins') of
* gradients, you can pass an array of objects each with a `name` and a
* `gradients` array which will in turn contain the predefined gradient objects.
*
* @default []
*/
gradients?: GradientsProp;
/**
* Start opting in to the new margin-free styles that will become the default
* in a future version, currently scheduled to be WordPress 6.4. (The prop
* can be safely removed once this happens.)
*
* @default false
*/
__nextHasNoMargin?: boolean;
/**
* If true, the gradient picker will not be displayed and only defined
* gradients from `gradients` will be shown.
*
* @default false
*/
disableCustomGradients?: boolean;
/**
* Whether this is rendered in the sidebar.
*
* @default false
*/
__experimentalIsRenderedInSidebar?: boolean;
};
export type PickerProps< TOriginType extends GradientObject | OriginObject > =
GradientPickerBaseProps & {
clearGradient: () => void;
onChange: (
currentGradient: string | undefined,
index: number
) => void;
actions?: React.ReactNode;
gradients: TOriginType[];
};