Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- `Modal`: Keep an accessible name on the dialog when `title` is combined with `__experimentalHideHeader`. The heading that `aria-labelledby` referenced is not rendered in that case, so the title is now applied as the dialog's `aria-label` instead ([#82645](https://github.com/WordPress/gutenberg/pull/82645)).
- `ColorPicker`: Make the HSL and RGB input labels translatable, and name the Hue and Alpha inputs after their color format ([#70214](https://github.com/WordPress/gutenberg/pull/70214)).
- `CustomGradientPicker`: Use the bundled `gradient-parser` types, preserve gradient angles when editing, and keep unsupported gradient syntax on the existing fallback path ([#84582](https://github.com/WordPress/gutenberg/pull/84582)).
- `CustomGradientPicker`: Keep the color interpolation method of a gradient, such as `in oklch`, when editing it, instead of replacing the gradient with the default one ([#84383](https://github.com/WordPress/gutenberg/pull/84383)).

### Internal

Expand Down
31 changes: 31 additions & 0 deletions packages/components/src/custom-gradient-picker/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,34 @@ export const DIRECTIONAL_ORIENTATION_ANGLE_MAP = {
'top left': 315,
'left top': 315,
};

// The color spaces and hue interpolation methods that a CSS
// `<color-interpolation-method>` accepts, e.g. `in oklch longer hue`.
// See https://www.w3.org/TR/css-color-4/#interpolation-space.
export const RECTANGULAR_INTERPOLATION_COLOR_SPACES = [
'srgb',
'srgb-linear',
'display-p3',
Comment thread
ciampo marked this conversation as resolved.
Resolved
'a98-rgb',
'prophoto-rgb',
'rec2020',
'lab',
'oklab',
'xyz',
'xyz-d50',
'xyz-d65',
];

export const POLAR_INTERPOLATION_COLOR_SPACES = [
'hsl',
'hwb',
'lch',
'oklch',
];

export const HUE_INTERPOLATION_METHODS = [
'shorter',
'longer',
'increasing',
'decreasing',
];
13 changes: 11 additions & 2 deletions packages/components/src/custom-gradient-picker/serializer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type gradientParser from 'gradient-parser';
import type { GradientAST } from './types';

export function serializeGradientColor( {
type,
Expand Down Expand Up @@ -63,10 +64,18 @@ export function serializeGradientOrientation(

export function serializeGradient( {
type,
colorInterpolation,
orientation,
colorStops,
}: gradientParser.GradientNode ) {
const serializedOrientation = serializeGradientOrientation( orientation );
}: GradientAST ) {
// The color interpolation method shares the first argument with the
// orientation, e.g. `linear-gradient(in oklch 160deg, …)`.
const serializedOrientation = [
colorInterpolation,
serializeGradientOrientation( orientation ),
]
.filter( Boolean )
.join( ' ' );
const serializedColorStops = colorStops
.sort( ( colorStop1, colorStop2 ) => {
const getNumericStopValue = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,88 @@ describe( 'CustomGradientPicker', () => {
expect( lastCall ).not.toContain( 'deg' );
} );
} );

describe( 'with a color interpolation method', () => {
const INTERPOLATED_GRADIENT =
'linear-gradient(in oklch 160deg,rgb(0,0,0) 0%,rgb(255,255,255) 100%)';

it( 'shows the color stops of a gradient that has an interpolation method', async () => {
await render(
<CustomGradientPicker
value={ INTERPOLATED_GRADIENT }
onChange={ vi.fn() }
/>
);

expect(
Comment thread
ciampo marked this conversation as resolved.
Resolved
screen.getAllByRole( 'button', {
name: /Gradient control point/,
} )
).toHaveLength( 2 );
} );

it( 'keeps the interpolation method when the angle changes', async () => {
const user = userEvent.setup();
const onChange = vi.fn();

await render(
<CustomGradientPicker
value={ INTERPOLATED_GRADIENT }
onChange={ onChange }
/>
);

screen.getByRole( 'spinbutton', { name: 'Angle' } ).focus();
await user.keyboard( '[ArrowUp]' );

const lastCall =
onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ];
expect( lastCall ).toMatch( /^linear-gradient\(in oklch 161deg,/ );
} );

it( 'keeps the interpolation method when switching to a radial gradient', async () => {
const user = userEvent.setup();
const onChange = vi.fn();

await render(
<CustomGradientPicker
value={ INTERPOLATED_GRADIENT }
onChange={ onChange }
/>
);

await user.selectOptions(
screen.getByRole( 'combobox', { name: /type/i } ),
'radial-gradient'
);

const lastCall =
onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ];
expect( lastCall ).toMatch( /^radial-gradient\(in oklch,/ );
} );

it( 'keeps the interpolation method when a color stop moves', async () => {
const user = userEvent.setup();
const onChange = vi.fn();

await render(
<CustomGradientPicker
value={ INTERPOLATED_GRADIENT }
onChange={ onChange }
/>
);

const [ firstPoint ] = screen.getAllByRole( 'button', {
name: /Gradient control point/,
} );
firstPoint.focus();
await user.keyboard( '[ArrowRight]' );

const lastCall =
onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ];
expect( lastCall ).toMatch( /^linear-gradient\(in oklch 160deg,/ );
} );
} );
} );

describe( 'CustomGradientBar', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,36 @@ describe( 'It should serialize a gradient', () => {
'linear-gradient(0deg,rgba(1,2,3,0.5) 0%,rgba(10,20,30,0.5) 20%,rgba(255,1,1,0.9) 40%,rgba(1,2,3,0.5) 100%)'
);
} );

test( 'serializeGradient writes the interpolation method before the angle', () => {
const colorStops = [
{
type: 'hex' as const,
value: '000',
length: { type: '%' as const, value: '0' },
},
{
type: 'hex' as const,
value: 'fff',
length: { type: '%' as const, value: '100' },
},
];

expect(
serializeGradient( {
type: 'linear-gradient',
colorInterpolation: 'in oklch',
orientation: { type: 'angular', value: '160', unit: 'deg' },
colorStops,
} )
).toBe( 'linear-gradient(in oklch 160deg,#000 0%,#fff 100%)' );

expect(
serializeGradient( {
type: 'radial-gradient',
colorInterpolation: 'in oklab',
colorStops,
} )
).toBe( 'radial-gradient(in oklab,#000 0%,#fff 100%)' );
} );
} );
74 changes: 74 additions & 0 deletions packages/components/src/custom-gradient-picker/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,77 @@ describe( 'CustomGradientPicker gradient parsing', () => {
expect( result.gradientAST ).toEqual( fallback.gradientAST );
} );
} );

describe( 'getGradientAstWithDefault', () => {
describe( 'with a color interpolation method', () => {
test( 'reads an interpolation method placed before the angle', () => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
'linear-gradient(in oklch 160deg, #009ED4 0%, #FFCE00 100%)'
);

expect( hasGradient ).toBe( true );
expect( gradientAST.colorInterpolation ).toBe( 'in oklch' );
expect( gradientAST.orientation ).toEqual( {
type: 'angular',
value: '160',
unit: 'deg',
} );
expect( gradientAST.colorStops ).toHaveLength( 2 );
} );

test( 'reads an interpolation method placed after the direction', () => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
'linear-gradient(to right in oklab, #009ED4 0%, #FFCE00 100%)'
);

expect( hasGradient ).toBe( true );
expect( gradientAST.colorInterpolation ).toBe( 'in oklab' );
expect( gradientAST.orientation ).toEqual( {
type: 'angular',
value: '90',
unit: 'deg',
} );
} );

test( 'keeps the hue interpolation method of a polar color space', () => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
'linear-gradient(in oklch longer hue 160deg, #009ED4 0%, #FFCE00 100%)'
);

expect( hasGradient ).toBe( true );
expect( gradientAST.colorInterpolation ).toBe(
'in oklch longer hue'
);
} );

test( 'falls back to the default gradient for an unknown color space', () => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
'linear-gradient(in not-a-space 160deg, #009ED4 0%, #FFCE00 100%)'
);

expect( console ).toHaveWarned();
expect( hasGradient ).toBe( false );
expect( gradientAST.colorInterpolation ).toBeUndefined();
} );

test( 'reads an interpolation method that is the whole first argument', () => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
'linear-gradient(in oklch, #009ED4 0%, #FFCE00 100%)'
);

expect( hasGradient ).toBe( true );
expect( gradientAST.colorInterpolation ).toBe( 'in oklch' );
expect( gradientAST.colorStops ).toHaveLength( 2 );
} );

test( 'falls back to the default gradient for a hue interpolation method on a non-hue color space', () => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
'linear-gradient(in oklab longer hue 160deg, #009ED4 0%, #FFCE00 100%)'
);

expect( console ).toHaveWarned();
expect( hasGradient ).toBe( false );
expect( gradientAST.colorInterpolation ).toBeUndefined();
} );
} );
} );
12 changes: 10 additions & 2 deletions packages/components/src/custom-gradient-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,22 @@ export type CustomGradientPickerProps = {
__experimentalIsRenderedInSidebar?: boolean;
};

/**
* `gradient-parser` can't read the color interpolation method of a gradient
* (e.g. `in oklch`), so the picker keeps it next to the parsed AST.
*/
type WithColorInterpolation = { colorInterpolation?: string };

export type GradientAST = gradientParser.GradientNode & WithColorInterpolation;

export type GradientAnglePickerProps = {
gradientAST: gradientParser.LinearGradientNode;
gradientAST: gradientParser.LinearGradientNode & WithColorInterpolation;
hasGradient: boolean;
onChange: ( gradient: string ) => void;
};

export type GradientTypePickerProps = {
gradientAST: gradientParser.GradientNode;
gradientAST: GradientAST;
hasGradient: boolean;
onChange: ( gradient: string ) => void;
};
Expand Down
Loading
Loading