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,266 @@
/**
* External dependencies
*/
import type { CSSProperties } from 'react';
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
/**
* Internal dependencies
*/
import BorderControlStylePicker from '../border-control-style-picker';
import Button from '../../button';
import ColorIndicator from '../../color-indicator';
import ColorPalette from '../../color-palette';
import Dropdown from '../../dropdown';
import { HStack } from '../../h-stack';
import { VStack } from '../../v-stack';
import type { WordPressComponentProps } from '../../context';
import { contextConnect } from '../../context';
import { useBorderControlDropdown } from './hook';
import { StyledLabel } from '../../base-control/styles/base-control-styles';
import DropdownContentWrapper from '../../dropdown/dropdown-content-wrapper';
import type { ColorObject } from '../../color-palette/types';
import { isMultiplePaletteArray } from '../../color-palette/utils';
import type { DropdownProps as DropdownComponentProps } from '../../dropdown/types';
import type { ColorProps, DropdownProps } from '../types';
const getAriaLabelColorValue = ( colorValue: string ) => {
// Leave hex values as-is. Remove the `var()` wrapper from CSS vars.
return colorValue.replace( /^var\((.+)\)$/, '$1' );
};
const getColorObject = (
colorValue: CSSProperties[ 'borderColor' ],
colors: ColorProps[ 'colors' ] | undefined
) => {
if ( ! colorValue || ! colors ) {
return;
}
if ( isMultiplePaletteArray( colors ) ) {
// Multiple origins
let matchedColor;
colors.some( ( origin ) =>
origin.colors.some( ( color ) => {
if ( color.color === colorValue ) {
matchedColor = color;
return true;
}
return false;
} )
);
return matchedColor;
}
// Single origin
return colors.find( ( color ) => color.color === colorValue );
};
const getToggleAriaLabel = (
colorValue: CSSProperties[ 'borderColor' ],
colorObject: ColorObject | undefined,
style: CSSProperties[ 'borderStyle' ],
isStyleEnabled: boolean
) => {
if ( isStyleEnabled ) {
if ( colorObject ) {
const ariaLabelValue = getAriaLabelColorValue( colorObject.color );
return style
? sprintf(
// translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g.: "#f00:". %3$s: The current border style selection e.g. "solid".
'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s". The currently selected style is "%3$s".',
colorObject.name,
ariaLabelValue,
style
)
: sprintf(
// translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g.: "#f00:".
'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s".',
colorObject.name,
ariaLabelValue
);
}
if ( colorValue ) {
const ariaLabelValue = getAriaLabelColorValue( colorValue );
return style
? sprintf(
// translators: %1$s: The color's hex code e.g.: "#f00:". %2$s: The current border style selection e.g. "solid".
'Border color and style picker. The currently selected color has a value of "%1$s". The currently selected style is "%2$s".',
ariaLabelValue,
style
)
: sprintf(
// translators: %1$s: The color's hex code e.g: "#f00".
'Border color and style picker. The currently selected color has a value of "%1$s".',
ariaLabelValue
);
}
return __( 'Border color and style picker.' );
}
if ( colorObject ) {
return sprintf(
// translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g: "#f00".
'Border color picker. The currently selected color is called "%1$s" and has a value of "%2$s".',
colorObject.name,
getAriaLabelColorValue( colorObject.color )
);
}
if ( colorValue ) {
return sprintf(
// translators: %1$s: The color's hex code e.g: "#f00".
'Border color picker. The currently selected color has a value of "%1$s".',
getAriaLabelColorValue( colorValue )
);
}
return __( 'Border color picker.' );
};
const BorderControlDropdown = (
props: WordPressComponentProps< DropdownProps, 'div' >,
forwardedRef: React.ForwardedRef< any >
) => {
const {
__experimentalIsRenderedInSidebar,
border,
colors,
disableCustomColors,
enableAlpha,
enableStyle,
indicatorClassName,
indicatorWrapperClassName,
isStyleSettable,
onReset,
onColorChange,
onStyleChange,
popoverContentClassName,
popoverControlsClassName,
resetButtonClassName,
showDropdownHeader,
size,
__unstablePopoverProps,
...otherProps
} = useBorderControlDropdown( props );
const { color, style } = border || {};
const colorObject = getColorObject( color, colors );
const toggleAriaLabel = getToggleAriaLabel(
color,
colorObject,
style,
enableStyle
);
const showResetButton = color || ( style && style !== 'none' );
const dropdownPosition = __experimentalIsRenderedInSidebar
? 'bottom left'
: undefined;
const renderToggle: DropdownComponentProps[ 'renderToggle' ] = ( {
onToggle,
} ) => (
<Button
onClick={ onToggle }
variant="tertiary"
aria-label={ toggleAriaLabel }
tooltipPosition={ dropdownPosition }
label={ __( 'Border color and style picker' ) }
showTooltip={ true }
__next40pxDefaultSize={ size === '__unstable-large' ? true : false }
>
<span className={ indicatorWrapperClassName }>
<ColorIndicator
className={ indicatorClassName }
colorValue={ color }
/>
</span>
</Button>
);
const renderContent: DropdownComponentProps[ 'renderContent' ] = ( {
onClose,
} ) => (
<>
<DropdownContentWrapper paddingSize="medium">
<VStack className={ popoverControlsClassName } spacing={ 6 }>
{ showDropdownHeader ? (
<HStack>
<StyledLabel>{ __( 'Border color' ) }</StyledLabel>
<Button
size="small"
label={ __( 'Close border color' ) }
icon={ closeSmall }
onClick={ onClose }
/>
</HStack>
) : undefined }
<ColorPalette
className={ popoverContentClassName }
value={ color }
onChange={ onColorChange }
{ ...{ colors, disableCustomColors } }
__experimentalIsRenderedInSidebar={
__experimentalIsRenderedInSidebar
}
clearable={ false }
enableAlpha={ enableAlpha }
/>
{ enableStyle && isStyleSettable && (
<BorderControlStylePicker
label={ __( 'Style' ) }
value={ style }
onChange={ onStyleChange }
/>
) }
</VStack>
</DropdownContentWrapper>
{ showResetButton && (
<DropdownContentWrapper paddingSize="none">
<Button
className={ resetButtonClassName }
variant="tertiary"
onClick={ () => {
onReset();
onClose();
} }
>
{ __( 'Reset' ) }
</Button>
</DropdownContentWrapper>
) }
</>
);
return (
<Dropdown
renderToggle={ renderToggle }
renderContent={ renderContent }
popoverProps={ {
...__unstablePopoverProps,
} }
{ ...otherProps }
ref={ forwardedRef }
/>
);
};
const ConnectedBorderControlDropdown = contextConnect(
BorderControlDropdown,
'BorderControlDropdown'
);
export default ConnectedBorderControlDropdown;

View File

@@ -0,0 +1,101 @@
/**
* WordPress dependencies
*/
import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import * as styles from '../styles';
import { parseQuantityAndUnitFromRawValue } from '../../unit-control/utils';
import type { WordPressComponentProps } from '../../context';
import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import type { DropdownProps } from '../types';
export function useBorderControlDropdown(
props: WordPressComponentProps< DropdownProps, 'div' >
) {
const {
border,
className,
colors = [],
enableAlpha = false,
enableStyle = true,
onChange,
previousStyleSelection,
size = 'default',
__experimentalIsRenderedInSidebar = false,
...otherProps
} = useContextSystem( props, 'BorderControlDropdown' );
const [ widthValue ] = parseQuantityAndUnitFromRawValue( border?.width );
const hasZeroWidth = widthValue === 0;
const onColorChange = ( color?: string ) => {
const style =
border?.style === 'none' ? previousStyleSelection : border?.style;
const width = hasZeroWidth && !! color ? '1px' : border?.width;
onChange( { color, style, width } );
};
const onStyleChange = ( style?: string ) => {
const width = hasZeroWidth && !! style ? '1px' : border?.width;
onChange( { ...border, style, width } );
};
const onReset = () => {
onChange( {
...border,
color: undefined,
style: undefined,
} );
};
// Generate class names.
const cx = useCx();
const classes = useMemo( () => {
return cx( styles.borderControlDropdown, className );
}, [ className, cx ] );
const indicatorClassName = useMemo( () => {
return cx( styles.borderColorIndicator );
}, [ cx ] );
const indicatorWrapperClassName = useMemo( () => {
return cx( styles.colorIndicatorWrapper( border, size ) );
}, [ border, cx, size ] );
const popoverControlsClassName = useMemo( () => {
return cx( styles.borderControlPopoverControls );
}, [ cx ] );
const popoverContentClassName = useMemo( () => {
return cx( styles.borderControlPopoverContent );
}, [ cx ] );
const resetButtonClassName = useMemo( () => {
return cx( styles.resetButton );
}, [ cx ] );
return {
...otherProps,
border,
className: classes,
colors,
enableAlpha,
enableStyle,
indicatorClassName,
indicatorWrapperClassName,
onColorChange,
onStyleChange,
onReset,
popoverContentClassName,
popoverControlsClassName,
resetButtonClassName,
size,
__experimentalIsRenderedInSidebar,
};
}

View File

@@ -0,0 +1 @@
export { default } from './component';

View File

@@ -0,0 +1,55 @@
/**
* WordPress dependencies
*/
import { lineDashed, lineDotted, lineSolid } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { contextConnect } from '../../context';
import type { StylePickerProps } from '../types';
import {
ToggleGroupControl,
ToggleGroupControlOptionIcon,
} from '../../toggle-group-control';
const BORDER_STYLES = [
{ label: __( 'Solid' ), icon: lineSolid, value: 'solid' },
{ label: __( 'Dashed' ), icon: lineDashed, value: 'dashed' },
{ label: __( 'Dotted' ), icon: lineDotted, value: 'dotted' },
];
function UnconnectedBorderControlStylePicker(
{ onChange, ...restProps }: StylePickerProps,
forwardedRef: React.ForwardedRef< any >
) {
return (
<ToggleGroupControl
__nextHasNoMarginBottom
__next40pxDefaultSize
ref={ forwardedRef }
isDeselectable
onChange={ ( value ) => {
onChange?.( value as string | undefined );
} }
{ ...restProps }
>
{ BORDER_STYLES.map( ( borderStyle ) => (
<ToggleGroupControlOptionIcon
key={ borderStyle.value }
value={ borderStyle.value }
icon={ borderStyle.icon }
label={ borderStyle.label }
/>
) ) }
</ToggleGroupControl>
);
}
const BorderControlStylePicker = contextConnect(
UnconnectedBorderControlStylePicker,
'BorderControlStylePicker'
);
export default BorderControlStylePicker;

View File

@@ -0,0 +1 @@
export { default } from './component';

View File

@@ -0,0 +1,175 @@
# BorderControl
<div class="callout callout-alert">
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>
<br />
This component provides control over a border's color, style, and width.
## Development guidelines
The `BorderControl` brings together internal sub-components which allow users to
set the various properties of a border. The first sub-component, a
`BorderDropdown` contains options representing border color and style. The
border width is controlled via a `UnitControl` and an optional `RangeControl`.
Border radius is not covered by this control as it may be desired separate to
color, style, and width. For example, the border radius may be absorbed under
a "shape" abstraction.
## Usage
```jsx
import { useState } from 'react';
import { __experimentalBorderControl as BorderControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
const colors = [
{ name: 'Blue 20', color: '#72aee6' },
// ...
];
const MyBorderControl = () => {
const [ border, setBorder ] = useState();
return (
<BorderControl
colors={ colors }
label={ __( 'Border' ) }
onChange={ setBorder }
value={ border }
/>
);
};
```
If you're using this component outside the editor, you can
[ensure `Tooltip` positioning](/packages/components/README.md#popovers-and-tooltips)
for the `BorderControl`'s color and style options, by rendering your
`BorderControl` with a `Popover.Slot` further up the element tree and within a
`SlotFillProvider` overall.
## Props
### `colors`: `( PaletteObject | ColorObject )[]`
An array of color definitions. This may also be a multi-dimensional array where
colors are organized by multiple origins.
Each color may be an object containing a `name` and `color` value.
- Required: No
- Default: `[]`
### `disableCustomColors`: `boolean`
This toggles the ability to choose custom colors.
- Required: No
### `disableUnits`: `boolean`
This controls whether unit selection should be disabled.
- Required: No
### `enableAlpha`: `boolean`
This controls whether the alpha channel will be offered when selecting
custom colors.
- Required: No
- Default: `false`
### `enableStyle`: `boolean`
This controls whether to support border style selection.
- Required: No
- Default: `true`
### `hideLabelFromVision`: `boolean`
Provides control over whether the label will only be visible to screen readers.
- Required: No
### `isCompact`: `boolean`
This flags the `BorderControl` to render with a more compact appearance. It
restricts the width of the control and prevents it from expanding to take up
additional space.
- Required: No
### `label`: `string`
If provided, a label will be generated using this as the content.
_Whether it is visible only to screen readers is controlled via
`hideLabelFromVision`._
- Required: No
### `onChange`: `( value?: Object ) => void`
A callback function invoked when the border value is changed via an interaction
that selects or clears, border color, style, or width.
_Note: the value may be `undefined` if a user clears all border properties._
- Required: Yes
### `shouldSanitizeBorder`: `boolean`
If opted into, sanitizing the border means that if no width or color have been
selected, the border style is also cleared and `undefined` is returned as the
new border value.
- Required: No
- Default: true
### `showDropdownHeader`: `boolean`
Whether or not to render a header for the border color and style picker
dropdown. The header includes a label for the color picker and a close button.
- Required: No
### `size`: `string`
Size of the control.
- Required: No
- Default: `default`
- Allowed values: `default`, `__unstable-large`
### `value`: `Object`
An object representing a border or `undefined`. Used to set the current border
configuration for this component.
Example:
```js
{
color: '#72aee6',
style: 'solid',
width: '2px,
}
```
- Required: No
### `width`: `CSSProperties[ 'width' ]`
Controls the visual width of the `BorderControl`. It has no effect if the
`isCompact` prop is set to `true`.
- Required: No
### `withSlider`: `boolean`
Flags whether this `BorderControl` should also render a `RangeControl` for
additional control over a border's width.
- Required: No

View File

@@ -0,0 +1,165 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import BorderControlDropdown from '../border-control-dropdown';
import UnitControl from '../../unit-control';
import RangeControl from '../../range-control';
import { HStack } from '../../h-stack';
import { StyledLabel } from '../../base-control/styles/base-control-styles';
import { View } from '../../view';
import { VisuallyHidden } from '../../visually-hidden';
import type { WordPressComponentProps } from '../../context';
import { contextConnect } from '../../context';
import { useBorderControl } from './hook';
import type { BorderControlProps, LabelProps } from '../types';
const BorderLabel = ( props: LabelProps ) => {
const { label, hideLabelFromVision } = props;
if ( ! label ) {
return null;
}
return hideLabelFromVision ? (
<VisuallyHidden as="legend">{ label }</VisuallyHidden>
) : (
<StyledLabel as="legend">{ label }</StyledLabel>
);
};
const UnconnectedBorderControl = (
props: WordPressComponentProps< BorderControlProps, 'div', false >,
forwardedRef: React.ForwardedRef< any >
) => {
const {
__next40pxDefaultSize = false,
colors,
disableCustomColors,
disableUnits,
enableAlpha,
enableStyle,
hideLabelFromVision,
innerWrapperClassName,
inputWidth,
isStyleSettable,
label,
onBorderChange,
onSliderChange,
onWidthChange,
placeholder,
__unstablePopoverProps,
previousStyleSelection,
showDropdownHeader,
size,
sliderClassName,
value: border,
widthUnit,
widthValue,
withSlider,
__experimentalIsRenderedInSidebar,
...otherProps
} = useBorderControl( props );
return (
<View as="fieldset" { ...otherProps } ref={ forwardedRef }>
<BorderLabel
label={ label }
hideLabelFromVision={ hideLabelFromVision }
/>
<HStack spacing={ 4 } className={ innerWrapperClassName }>
<UnitControl
prefix={
<BorderControlDropdown
border={ border }
colors={ colors }
__unstablePopoverProps={ __unstablePopoverProps }
disableCustomColors={ disableCustomColors }
enableAlpha={ enableAlpha }
enableStyle={ enableStyle }
isStyleSettable={ isStyleSettable }
onChange={ onBorderChange }
previousStyleSelection={ previousStyleSelection }
showDropdownHeader={ showDropdownHeader }
__experimentalIsRenderedInSidebar={
__experimentalIsRenderedInSidebar
}
size={ size }
/>
}
label={ __( 'Border width' ) }
hideLabelFromVision
min={ 0 }
onChange={ onWidthChange }
value={ border?.width || '' }
placeholder={ placeholder }
disableUnits={ disableUnits }
__unstableInputWidth={ inputWidth }
size={ size }
/>
{ withSlider && (
<RangeControl
__nextHasNoMarginBottom
label={ __( 'Border width' ) }
hideLabelFromVision
className={ sliderClassName }
initialPosition={ 0 }
max={ 100 }
min={ 0 }
onChange={ onSliderChange }
step={ [ 'px', '%' ].includes( widthUnit ) ? 1 : 0.1 }
value={ widthValue || undefined }
withInputField={ false }
__next40pxDefaultSize={ __next40pxDefaultSize }
/>
) }
</HStack>
</View>
);
};
/**
* The `BorderControl` brings together internal sub-components which allow users to
* set the various properties of a border. The first sub-component, a
* `BorderDropdown` contains options representing border color and style. The
* border width is controlled via a `UnitControl` and an optional `RangeControl`.
*
* Border radius is not covered by this control as it may be desired separate to
* color, style, and width. For example, the border radius may be absorbed under
* a "shape" abstraction.
*
* ```jsx
* import { __experimentalBorderControl as BorderControl } from '@wordpress/components';
* import { __ } from '@wordpress/i18n';
*
* const colors = [
* { name: 'Blue 20', color: '#72aee6' },
* // ...
* ];
*
* const MyBorderControl = () => {
* const [ border, setBorder ] = useState();
* const onChange = ( newBorder ) => setBorder( newBorder );
*
* return (
* <BorderControl
* colors={ colors }
* label={ __( 'Border' ) }
* onChange={ onChange }
* value={ border }
* />
* );
* };
* ```
*/
export const BorderControl = contextConnect(
UnconnectedBorderControl,
'BorderControl'
);
export default BorderControl;

View File

@@ -0,0 +1,167 @@
/**
* WordPress dependencies
*/
import { useCallback, useMemo, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import * as styles from '../styles';
import { parseQuantityAndUnitFromRawValue } from '../../unit-control/utils';
import type { WordPressComponentProps } from '../../context';
import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import type { Border, BorderControlProps } from '../types';
// If either width or color are defined, the border is considered valid
// and a border style can be set as well.
const isValidBorder = ( border?: Border ) => {
const hasWidth = border?.width !== undefined && border.width !== '';
const hasColor = border?.color !== undefined;
return hasWidth || hasColor;
};
export function useBorderControl(
props: WordPressComponentProps< BorderControlProps, 'div' >
) {
const {
className,
colors = [],
isCompact,
onChange,
enableAlpha = true,
enableStyle = true,
shouldSanitizeBorder = true,
size = 'default',
value: border,
width,
__experimentalIsRenderedInSidebar = false,
__next40pxDefaultSize,
...otherProps
} = useContextSystem( props, 'BorderControl' );
const computedSize =
size === 'default' && __next40pxDefaultSize ? '__unstable-large' : size;
const [ widthValue, originalWidthUnit ] = parseQuantityAndUnitFromRawValue(
border?.width
);
const widthUnit = originalWidthUnit || 'px';
const hadPreviousZeroWidth = widthValue === 0;
const [ colorSelection, setColorSelection ] = useState< string >();
const [ styleSelection, setStyleSelection ] = useState< string >();
const isStyleSettable = shouldSanitizeBorder
? isValidBorder( border )
: true;
const onBorderChange = useCallback(
( newBorder?: Border ) => {
if ( shouldSanitizeBorder && ! isValidBorder( newBorder ) ) {
onChange( undefined );
return;
}
onChange( newBorder );
},
[ onChange, shouldSanitizeBorder ]
);
const onWidthChange = useCallback(
( newWidth?: string ) => {
const newWidthValue = newWidth === '' ? undefined : newWidth;
const [ parsedValue ] =
parseQuantityAndUnitFromRawValue( newWidth );
const hasZeroWidth = parsedValue === 0;
const updatedBorder = { ...border, width: newWidthValue };
// Setting the border width explicitly to zero will also set the
// border style to `none` and clear the border color.
if ( hasZeroWidth && ! hadPreviousZeroWidth ) {
// Before clearing the color and style selections, keep track of
// the current selections so they can be restored when the width
// changes to a non-zero value.
setColorSelection( border?.color );
setStyleSelection( border?.style );
// Clear the color and style border properties.
updatedBorder.color = undefined;
updatedBorder.style = 'none';
}
// Selection has changed from zero border width to non-zero width.
if ( ! hasZeroWidth && hadPreviousZeroWidth ) {
// Restore previous border color and style selections if width
// is now not zero.
if ( updatedBorder.color === undefined ) {
updatedBorder.color = colorSelection;
}
if ( updatedBorder.style === 'none' ) {
updatedBorder.style = styleSelection;
}
}
onBorderChange( updatedBorder );
},
[
border,
hadPreviousZeroWidth,
colorSelection,
styleSelection,
onBorderChange,
]
);
const onSliderChange = useCallback(
( value?: number ) => {
onWidthChange( `${ value }${ widthUnit }` );
},
[ onWidthChange, widthUnit ]
);
// Generate class names.
const cx = useCx();
const classes = useMemo( () => {
return cx( styles.borderControl, className );
}, [ className, cx ] );
let wrapperWidth = width;
if ( isCompact ) {
// Widths below represent the minimum usable width for compact controls.
// Taller controls contain greater internal padding, thus greater width.
wrapperWidth = size === '__unstable-large' ? '116px' : '90px';
}
const innerWrapperClassName = useMemo( () => {
const widthStyle = !! wrapperWidth && styles.wrapperWidth;
const heightStyle = styles.wrapperHeight( computedSize );
return cx( styles.innerWrapper(), widthStyle, heightStyle );
}, [ wrapperWidth, cx, computedSize ] );
const sliderClassName = useMemo( () => {
return cx( styles.borderSlider() );
}, [ cx ] );
return {
...otherProps,
className: classes,
colors,
enableAlpha,
enableStyle,
innerWrapperClassName,
inputWidth: wrapperWidth,
isStyleSettable,
onBorderChange,
onSliderChange,
onWidthChange,
previousStyleSelection: styleSelection,
sliderClassName,
value: border,
widthUnit,
widthValue,
size: computedSize,
__experimentalIsRenderedInSidebar,
__next40pxDefaultSize,
};
}

View File

@@ -0,0 +1,2 @@
export { default as BorderControl } from './component';
export { useBorderControl } from './hook';

View File

@@ -0,0 +1,2 @@
export { default as BorderControl } from './border-control/component';
export { useBorderControl } from './border-control/hook';

View File

@@ -0,0 +1,144 @@
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';
import type { ComponentProps } from 'react';
/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { BorderControl } from '..';
import type { Border } from '../types';
const meta: Meta< typeof BorderControl > = {
title: 'Components (Experimental)/BorderControl',
component: BorderControl,
argTypes: {
onChange: {
action: 'onChange',
},
width: { control: { type: 'text' } },
value: { control: { type: null } },
},
parameters: {
controls: { expanded: true },
docs: { canvas: { sourceState: 'shown' } },
},
};
export default meta;
// Available border colors.
const colors = [
{ name: 'Blue 20', color: '#72aee6' },
{ name: 'Blue 40', color: '#3582c4' },
{ name: 'Red 40', color: '#e65054' },
{ name: 'Red 70', color: '#8a2424' },
{ name: 'Yellow 10', color: '#f2d675' },
{ name: 'Yellow 40', color: '#bd8600' },
];
// Multiple origin colors.
const multipleOriginColors = [
{
name: 'Default',
colors: [
{ name: 'Gray 20', color: '#a7aaad' },
{ name: 'Gray 70', color: '#3c434a' },
],
},
{
name: 'Theme',
colors: [
{ name: 'Blue 20', color: '#72aee6' },
{ name: 'Blue 40', color: '#3582c4' },
{ name: 'Blue 70', color: '#0a4b78' },
],
},
{
name: 'User',
colors: [
{ name: 'Green', color: '#00a32a' },
{ name: 'Yellow', color: '#f2d675' },
],
},
];
const Template: StoryFn< typeof BorderControl > = ( {
onChange,
...props
} ) => {
const [ border, setBorder ] = useState< Border >();
const onChangeMerged: ComponentProps<
typeof BorderControl
>[ 'onChange' ] = ( newBorder ) => {
setBorder( newBorder );
onChange( newBorder );
};
return (
<BorderControl
onChange={ onChangeMerged }
value={ border }
{ ...props }
/>
);
};
export const Default = Template.bind( {} );
Default.args = {
colors,
label: 'Border',
};
/**
* Render a slider beside the control.
*/
export const WithSlider = Template.bind( {} );
WithSlider.args = {
...Default.args,
withSlider: true,
};
/**
* When rendering with a slider, the `width` prop is useful to customize the width of the number input.
*/
export const WithSliderCustomWidth = Template.bind( {} );
WithSliderCustomWidth.args = {
...Default.args,
withSlider: true,
width: '150px',
};
WithSliderCustomWidth.storyName = 'With Slider (Custom Width)';
/**
* Restrict the width of the control and prevent it from expanding to take up additional space.
* When `true`, the `width` prop will be ignored.
*/
export const IsCompact = Template.bind( {} );
IsCompact.args = {
...Default.args,
isCompact: true,
};
/**
* The `colors` object can contain multiple origins.
*/
export const WithMultipleOrigins = Template.bind( {} );
WithMultipleOrigins.args = {
...Default.args,
colors: multipleOriginColors,
};
/**
* Allow the alpha channel to be edited on each color.
*/
export const WithAlphaEnabled = Template.bind( {} );
WithAlphaEnabled.args = {
...Default.args,
enableAlpha: true,
};

View File

@@ -0,0 +1,171 @@
/**
* External dependencies
*/
import { css } from '@emotion/react';
/**
* Internal dependencies
*/
import { COLORS, CONFIG, boxSizingReset, rtl } from '../utils';
import { space } from '../utils/space';
import { StyledLabel } from '../base-control/styles/base-control-styles';
import {
ValueInput as UnitControlWrapper,
UnitSelect,
} from '../unit-control/styles/unit-control-styles';
import type { Border } from './types';
const labelStyles = css`
font-weight: 500;
`;
const focusBoxShadow = css`
box-shadow: inset ${ CONFIG.controlBoxShadowFocus };
`;
export const borderControl = css`
border: 0;
padding: 0;
margin: 0;
${ boxSizingReset }
`;
export const innerWrapper = () => css`
${ UnitControlWrapper } {
flex: 1 1 40%;
}
&& ${ UnitSelect } {
/* Prevent unit select forcing min height larger than its UnitControl */
min-height: 0;
}
`;
/*
* This style is only applied to the UnitControl wrapper when the border width
* field should be a set width. Omitting this allows the UnitControl &
* RangeControl to share the available width in a 40/60 split respectively.
*/
export const wrapperWidth = css`
${ UnitControlWrapper } {
/* Force the UnitControl's set width. */
flex: 0 0 auto;
}
`;
export const wrapperHeight = ( size?: 'default' | '__unstable-large' ) => {
return css`
height: ${ size === '__unstable-large' ? '40px' : '30px' };
`;
};
export const borderControlDropdown = css`
background: #fff;
&& > button {
aspect-ratio: 1;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
${ rtl(
{ borderRadius: `2px 0 0 2px` },
{ borderRadius: `0 2px 2px 0` }
)() }
border: ${ CONFIG.borderWidth } solid ${ COLORS.ui.border };
&:focus,
&:hover:not( :disabled ) {
${ focusBoxShadow }
border-color: ${ COLORS.ui.borderFocus };
z-index: 1;
position: relative;
}
}
`;
export const colorIndicatorBorder = ( border?: Border ) => {
const { color, style } = border || {};
const fallbackColor =
!! style && style !== 'none' ? COLORS.gray[ 300 ] : undefined;
return css`
border-style: ${ style === 'none' ? 'solid' : style };
border-color: ${ color || fallbackColor };
`;
};
export const colorIndicatorWrapper = (
border?: Border,
size?: 'default' | '__unstable-large'
) => {
const { style } = border || {};
return css`
border-radius: 9999px;
border: 2px solid transparent;
${ style ? colorIndicatorBorder( border ) : undefined }
width: ${ size === '__unstable-large' ? '24px' : '22px' };
height: ${ size === '__unstable-large' ? '24px' : '22px' };
padding: ${ size === '__unstable-large' ? '2px' : '1px' };
/*
* ColorIndicator
*
* The transparent colors used here ensure visibility of the indicator
* over the active state of the border control dropdown's toggle button.
*/
& > span {
height: ${ space( 4 ) };
width: ${ space( 4 ) };
background: linear-gradient(
-45deg,
transparent 48%,
rgb( 0 0 0 / 20% ) 48%,
rgb( 0 0 0 / 20% ) 52%,
transparent 52%
);
}
`;
};
// Must equal $color-palette-circle-size from:
// @wordpress/components/src/circular-option-picker/style.scss
const swatchSize = 28;
const swatchGap = 12;
export const borderControlPopoverControls = css`
width: ${ swatchSize * 6 + swatchGap * 5 }px;
> div:first-of-type > ${ StyledLabel } {
margin-bottom: 0;
${ labelStyles }
}
&& ${ StyledLabel } + button:not( .has-text ) {
min-width: 24px;
padding: 0;
}
`;
export const borderControlPopoverContent = css``;
export const borderColorIndicator = css``;
export const resetButton = css`
justify-content: center;
width: 100%;
/* Override button component styling */
&& {
border-top: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 400 ] };
border-top-left-radius: 0;
border-top-right-radius: 0;
height: 40px;
}
`;
export const borderSlider = () => css`
flex: 1 1 60%;
${ rtl( { marginRight: space( 3 ) } )() }
`;

View File

@@ -0,0 +1,501 @@
/**
* External dependencies
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { BorderControl } from '../';
const colors = [
{ name: 'Gray', color: '#f6f7f7' },
{ name: 'Blue', color: '#72aee6' },
{ name: 'Red', color: '#e65054' },
{ name: 'Green', color: '#00a32a' },
{ name: 'Yellow', color: '#bd8600' },
];
const defaultBorder = {
color: '#72aee6',
style: 'solid',
width: '1px',
};
function createProps( customProps ) {
const props = {
colors,
label: 'Border',
onChange: jest.fn().mockImplementation( ( newValue ) => {
props.value = newValue;
} ),
value: defaultBorder,
...customProps,
};
return props;
}
const toggleLabelRegex = /Border color( and style)* picker/;
const openPopover = async ( user ) => {
const toggleButton = screen.getByLabelText( toggleLabelRegex );
await user.click( toggleButton );
// Wait for color picker popover to fully appear
const pickerButton = screen.getByRole( 'button', {
name: /^Custom color picker/,
} );
await waitFor( () => expect( pickerButton ).toBePositionedPopover() );
};
const getButton = ( name ) => {
return screen.getByRole( 'button', { name } );
};
const getColorOption = ( color ) => {
return screen.getByRole( 'option', { name: `Color: ${ color }` } );
};
const queryButton = ( name ) => {
return screen.queryByRole( 'button', { name } );
};
const getSliderInput = () => {
return screen.getByRole( 'slider', { name: 'Border width' } );
};
const getWidthInput = () => {
return screen.getByRole( 'spinbutton', { name: 'Border width' } );
};
describe( 'BorderControl', () => {
describe( 'basic rendering', () => {
it( 'should render standard border control', () => {
const props = createProps();
render( <BorderControl { ...props } /> );
const label = screen.getByText( props.label );
const colorButton = screen.getByLabelText( toggleLabelRegex );
const widthInput = getWidthInput();
const unitSelect = screen.getByRole( 'combobox', {
name: 'Select unit',
} );
const slider = screen.queryByRole( 'slider', {
name: 'Border width',
} );
expect( label ).toBeInTheDocument();
expect( colorButton ).toBeInTheDocument();
expect( widthInput ).toBeInTheDocument();
expect( unitSelect ).toBeInTheDocument();
expect( slider ).not.toBeInTheDocument();
} );
it( 'should hide label', () => {
const props = createProps( { hideLabelFromVision: true } );
render( <BorderControl { ...props } /> );
const label = screen.getByText( props.label );
// As visually hidden labels are still included in the document
// and do not have `display: none` styling, we can't rely on
// `.toBeInTheDocument()` or `.toBeVisible()` assertions.
expect( label ).toHaveAttribute(
'data-wp-component',
'VisuallyHidden'
);
} );
it( 'should render with slider', () => {
const props = createProps( { withSlider: true } );
render( <BorderControl { ...props } /> );
const slider = getSliderInput();
expect( slider ).toBeInTheDocument();
} );
it( 'should render placeholder in UnitControl', () => {
const props = createProps( { placeholder: 'Mixed' } );
render( <BorderControl { ...props } /> );
const widthInput = getWidthInput();
expect( widthInput ).toHaveAttribute( 'placeholder', 'Mixed' );
} );
it( 'should render color and style popover', async () => {
const user = userEvent.setup();
const props = createProps();
render( <BorderControl { ...props } /> );
await openPopover( user );
const customColorPicker = getButton( /Custom color picker/ );
const colorSwatchButtons = screen.getAllByRole( 'option', {
name: /^Color:/,
} );
const styleLabel = screen.getByText( 'Style' );
const solidButton = getButton( 'Solid' );
const dashedButton = getButton( 'Dashed' );
const dottedButton = getButton( 'Dotted' );
const resetButton = getButton( 'Reset' );
expect( customColorPicker ).toBeInTheDocument();
expect( colorSwatchButtons.length ).toEqual( colors.length );
expect( styleLabel ).toBeInTheDocument();
expect( solidButton ).toBeInTheDocument();
expect( dashedButton ).toBeInTheDocument();
expect( dottedButton ).toBeInTheDocument();
expect( resetButton ).toBeInTheDocument();
} );
it( 'should render color and style popover header', async () => {
const user = userEvent.setup();
const props = createProps( { showDropdownHeader: true } );
render( <BorderControl { ...props } /> );
await openPopover( user );
const headerLabel = screen.getByText( 'Border color' );
const closeButton = getButton( 'Close border color' );
expect( headerLabel ).toBeInTheDocument();
expect( closeButton ).toBeInTheDocument();
} );
it( 'should not render style options when opted out of', async () => {
const user = userEvent.setup();
const props = createProps( { enableStyle: false } );
render( <BorderControl { ...props } /> );
await openPopover( user );
const styleLabel = screen.queryByText( 'Style' );
const solidButton = queryButton( 'Solid' );
const dashedButton = queryButton( 'Dashed' );
const dottedButton = queryButton( 'Dotted' );
expect( styleLabel ).not.toBeInTheDocument();
expect( solidButton ).not.toBeInTheDocument();
expect( dashedButton ).not.toBeInTheDocument();
expect( dottedButton ).not.toBeInTheDocument();
} );
} );
describe( 'color and style picker aria labels', () => {
describe( 'with style selection enabled', () => {
it( 'should include both color and style in label', () => {
const props = createProps( { value: undefined } );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText( 'Border color and style picker.' )
).toBeInTheDocument();
} );
it( 'should correctly describe named color selection', () => {
const props = createProps( { value: { color: '#72aee6' } } );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText(
'Border color and style picker. The currently selected color is called "Blue" and has a value of "#72aee6".'
)
).toBeInTheDocument();
} );
it( 'should correctly describe custom color selection', () => {
const props = createProps( { value: { color: '#4b1d80' } } );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText(
'Border color and style picker. The currently selected color has a value of "#4b1d80".'
)
).toBeInTheDocument();
} );
it( 'should correctly describe named color and style selections', () => {
const props = createProps( {
value: { color: '#72aee6', style: 'dotted' },
} );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText(
'Border color and style picker. The currently selected color is called "Blue" and has a value of "#72aee6". The currently selected style is "dotted".'
)
).toBeInTheDocument();
} );
it( 'should correctly describe custom color and style selections', () => {
const props = createProps( {
value: { color: '#4b1d80', style: 'dashed' },
} );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText(
'Border color and style picker. The currently selected color has a value of "#4b1d80". The currently selected style is "dashed".'
)
).toBeInTheDocument();
} );
} );
describe( 'with style selection disabled', () => {
it( 'should only include color in the label', () => {
const props = createProps( {
value: undefined,
enableStyle: false,
} );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText( 'Border color picker.' )
).toBeInTheDocument();
} );
it( 'should correctly describe named color selection', () => {
const props = createProps( {
value: { color: '#72aee6' },
enableStyle: false,
} );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText(
'Border color picker. The currently selected color is called "Blue" and has a value of "#72aee6".'
)
).toBeInTheDocument();
} );
it( 'should correctly describe custom color selection', () => {
const props = createProps( {
value: { color: '#4b1d80' },
enableStyle: false,
} );
render( <BorderControl { ...props } /> );
expect(
screen.getByLabelText(
'Border color picker. The currently selected color has a value of "#4b1d80".'
)
).toBeInTheDocument();
} );
} );
} );
describe( 'onChange handling', () => {
it( 'should update width with slider value', () => {
const props = createProps( { withSlider: true } );
const { rerender } = render( <BorderControl { ...props } /> );
const slider = getSliderInput();
// As per [1], it is not currently possible to reasonably
// replicate this interaction using `userEvent`, so leaving
// `fireEvent` in place to cover it.
// [1]: https://github.com/testing-library/user-event/issues/871
fireEvent.change( slider, { target: { value: '5' } } );
expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
...defaultBorder,
width: '5px',
} );
rerender( <BorderControl { ...props } /> );
const widthInput = getWidthInput();
expect( widthInput.value ).toEqual( '5' );
} );
it( 'should update color selection', async () => {
const user = userEvent.setup();
const props = createProps();
render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Green' ) );
expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
...defaultBorder,
color: '#00a32a',
} );
} );
it( 'should clear color selection when toggling swatch off', async () => {
const user = userEvent.setup();
const props = createProps();
render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Blue' ) );
expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
...defaultBorder,
color: undefined,
} );
} );
it( 'should update style selection', async () => {
const user = userEvent.setup();
const props = createProps();
render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getButton( 'Dashed' ) );
expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
...defaultBorder,
style: 'dashed',
} );
} );
it( 'should take no action when color and style popover is closed', async () => {
const user = userEvent.setup();
const props = createProps( { showDropdownHeader: true } );
render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getButton( 'Close border color' ) );
expect( props.onChange ).not.toHaveBeenCalled();
} );
it( 'should reset color and style only when popover reset button clicked', async () => {
const user = userEvent.setup();
const props = createProps();
render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getButton( 'Reset' ) );
expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
color: undefined,
style: undefined,
width: defaultBorder.width,
} );
} );
it( 'should sanitize border when width and color are undefined', async () => {
const user = userEvent.setup();
const props = createProps();
const { rerender } = render( <BorderControl { ...props } /> );
await user.clear( getWidthInput() );
rerender( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Blue' ) );
expect( props.onChange ).toHaveBeenCalledWith( undefined );
} );
it( 'should not sanitize border when requested', async () => {
const user = userEvent.setup();
const props = createProps( {
shouldSanitizeBorder: false,
} );
const { rerender } = render( <BorderControl { ...props } /> );
await user.clear( getWidthInput() );
rerender( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Blue' ) );
expect( props.onChange ).toHaveBeenNthCalledWith( 2, {
color: undefined,
style: defaultBorder.style,
width: undefined,
} );
} );
it( 'should clear color and set style to `none` when setting zero width', async () => {
const user = userEvent.setup();
const props = createProps();
render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Green' ) );
await user.click( getButton( 'Dotted' ) );
await user.type( getWidthInput(), '0', {
initialSelectionStart: 0,
initialSelectionEnd: 1,
} );
expect( props.onChange ).toHaveBeenNthCalledWith( 3, {
color: undefined,
style: 'none',
width: '0px',
} );
} );
it( 'should reselect color and style selections when changing to non-zero width', async () => {
const user = userEvent.setup();
const props = createProps();
const { rerender } = render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Green' ) );
rerender( <BorderControl { ...props } /> );
await user.click( getButton( 'Dotted' ) );
rerender( <BorderControl { ...props } /> );
const widthInput = getWidthInput();
await user.type( widthInput, '0', {
initialSelectionStart: 0,
initialSelectionEnd: 1,
} );
await user.type( widthInput, '5', {
initialSelectionStart: 0,
initialSelectionEnd: 1,
} );
expect( props.onChange ).toHaveBeenNthCalledWith( 4, {
color: '#00a32a',
style: 'dotted',
width: '5px',
} );
} );
it( 'should set a non-zero width when applying color to zero width border', async () => {
const user = userEvent.setup();
const props = createProps( { value: undefined } );
const { rerender } = render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Yellow' ) );
expect( props.onChange ).toHaveBeenCalledWith( {
color: '#bd8600',
style: undefined,
width: undefined,
} );
await user.type( getWidthInput(), '0' );
rerender( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Green' ) );
expect( props.onChange ).toHaveBeenCalledWith( {
color: '#00a32a',
style: undefined,
width: '1px',
} );
} );
it( 'should set a non-zero width when applying style to zero width border', async () => {
const user = userEvent.setup();
const props = createProps( {
value: undefined,
shouldSanitizeBorder: false,
} );
const { rerender } = render( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getButton( 'Dashed' ) );
expect( props.onChange ).toHaveBeenCalledWith( {
color: undefined,
style: 'dashed',
width: undefined,
} );
await user.type( getWidthInput(), '0' );
rerender( <BorderControl { ...props } /> );
await openPopover( user );
await user.click( getButton( 'Dotted' ) );
expect( props.onChange ).toHaveBeenCalledWith( {
color: undefined,
style: 'dotted',
width: '1px',
} );
} );
} );
} );

View File

@@ -0,0 +1,159 @@
/**
* External dependencies
*/
import type { CSSProperties } from 'react';
/**
* Internal dependencies
*/
import type { ColorPaletteProps } from '../color-palette/types';
import type { PopoverProps } from '../popover/types';
import type { ToggleGroupControlProps } from '../toggle-group-control/types';
export type Border = {
color?: CSSProperties[ 'borderColor' ];
style?: CSSProperties[ 'borderStyle' ];
width?: CSSProperties[ 'borderWidth' ];
};
export type ColorProps = Pick<
ColorPaletteProps,
'colors' | 'enableAlpha' | '__experimentalIsRenderedInSidebar'
> & {
/**
* This toggles the ability to choose custom colors.
*/
disableCustomColors?: boolean;
};
export type LabelProps = {
/**
* Provides control over whether the label will only be visible to
* screen readers.
*/
hideLabelFromVision?: boolean;
/**
* If provided, a label will be generated using this as the content.
*/
label?: string;
};
export type BorderControlProps = ColorProps &
LabelProps & {
/**
* This controls whether unit selection should be disabled.
*/
disableUnits?: boolean;
/**
* This controls whether to support border style selection.
*
* @default true
*/
enableStyle?: boolean;
/**
* This flags the `BorderControl` to render with a more compact
* appearance. It restricts the width of the control and prevents it
* from expanding to take up additional space.
*/
isCompact?: boolean;
/**
* A callback function invoked when the border value is changed via an
* interaction that selects or clears, border color, style, or width.
*/
onChange: ( value?: Border ) => void;
/**
* An internal prop used to control the visibility of the dropdown.
*/
__unstablePopoverProps?: Omit< PopoverProps, 'children' >;
/**
* If opted into, sanitizing the border means that if no width or color
* have been selected, the border style is also cleared and `undefined`
* is returned as the new border value.
*
* @default true
*/
shouldSanitizeBorder?: boolean;
/**
* Whether or not to show the header for the border color and style
* picker dropdown. The header includes a label for the color picker
* and a close button.
*/
showDropdownHeader?: boolean;
/**
* Size of the control.
*
* @default 'default'
*/
size?: 'default' | '__unstable-large';
/**
* An object representing a border or `undefined`. Used to set the
* current border configuration for this component.
*/
value?: Border;
/**
* Controls the visual width of the `BorderControl`. It has no effect if
* the `isCompact` prop is set to `true`.
*/
width?: CSSProperties[ 'width' ];
/**
* Flags whether this `BorderControl` should also render a
* `RangeControl` for additional control over a border's width.
*/
withSlider?: boolean;
/**
* Start opting into the larger default height that will become the default size in a future version.
*
* @default false
*/
__next40pxDefaultSize?: boolean;
};
export type DropdownProps = ColorProps &
Pick< BorderControlProps, 'enableStyle' | 'size' > & {
/**
* An object representing a border or `undefined`. This component will
* extract the border color and style selections from this object to use as
* values for its popover controls.
*/
border?: Border;
/**
* Whether a border style can be set, based on the border sanitization settings.
*/
isStyleSettable: boolean;
/**
* An internal prop used to control the visibility of the dropdown.
*/
__unstablePopoverProps?: Omit< PopoverProps, 'children' >;
/**
* A callback invoked when the border color or style selections change.
*/
onChange: ( newBorder?: Border ) => void;
/**
* Any previous style selection made by the user. This can be used to
* reapply that previous selection when, for example, a zero border width is
* to a non-zero value.
*/
previousStyleSelection?: string;
/**
* Whether or not to render a header for the border color and style picker
* dropdown. The header includes a label for the color picker and a
* close button.
*/
showDropdownHeader?: boolean;
};
export type StylePickerProps = Omit<
ToggleGroupControlProps,
'value' | 'onChange' | 'children'
> & {
/**
* A callback function invoked when a border style is selected or cleared.
*/
onChange: ( style?: string ) => void;
/**
* The currently selected border style if there is one. Styles available via
* this control are `solid`, `dashed` & `dotted`, however the possibility
* to store other valid CSS values is maintained e.g. `none`, `inherit` etc.
*/
value?: string;
};