Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 4 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- `DropdownMenu`: Keep the menu closed on ArrowDown when the toggle is disabled ([#84556](https://github.com/WordPress/gutenberg/pull/84556)).
- `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)).

### Internal

Expand Down
3 changes: 1 addition & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"@emotion/styled": "^11.14.1",
"@emotion/utils": "^1.4.2",
"@floating-ui/react-dom": "^2.1.9",
"@types/gradient-parser": "^1.1.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gradient-parser@1.1.1 has no bundled declarations, and the lockfile still installs it, so removing @types/gradient-parser breaks type checking. Could we require gradient-parser@^1.2.0, update the lockfile, and make any changes needed for its types in this PR?

@joshualip-plaudit joshualip-plaudit Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. I missed that the lockfile was important for this in my initial commit (I was trying to minimize the change count)

"@types/highlight-words-core": "^1.2.1",
"@use-gesture/react": "^10.3.1",
"@wordpress/a11y": "file:../a11y",
Expand Down Expand Up @@ -90,7 +89,7 @@
"deepmerge": "^4.3.1",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^11.15.0",
"gradient-parser": "^1.1.1",
"gradient-parser": "^1.2.0",
"highlight-words-core": "^1.2.2",
"is-plain-object": "^5.1.0",
"memize": "^2.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const DEFAULT_LINEAR_GRADIENT_ANGLE = 180;
export const HORIZONTAL_GRADIENT_ORIENTATION = {
type: 'angular',
value: '90',
unit: 'deg',
} as const;

export const GRADIENT_OPTIONS = [
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/custom-gradient-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const GradientAnglePicker = ( {
orientation: {
type: 'angular',
value: `${ newAngle }`,
unit: 'deg',
},
} )
);
Expand Down Expand Up @@ -74,6 +75,7 @@ const GradientTypePicker = ( {
orientation: {
type: 'angular' as const,
value: `${ lastLinearOrientationAngle.current }`,
unit: 'deg',
},
type: 'linear-gradient',
} satisfies LinearGradientNode )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function serializeGradientColor( {
return `hsl(${ hue },${ saturation }%,${ lightness }%)`;
}
if ( type === 'hsla' ) {
const [ hue, saturation, lightness, alpha ] = value;
const [ hue, saturation, lightness, alpha = '1' ] = value;
return `hsla(${ hue },${ saturation }%,${ lightness }%,${ alpha })`;
}
return `${ type }(${ value.join( ',' ) })`;
Expand Down Expand Up @@ -58,7 +58,7 @@ export function serializeGradientOrientation(
) {
return;
}
return `${ orientation.value }deg`;
return `${ orientation.value }${ orientation.unit }`;
}

export function serializeGradient( {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,35 @@ describe( 'It should serialize a gradient', () => {

test( 'serializeGradientOrientation', () => {
expect(
serializeGradientOrientation( { type: 'angular', value: '40' } )
serializeGradientOrientation( {
type: 'angular',
value: '40',
unit: 'deg',
} )
).toBe( '40deg' );

expect(
serializeGradientOrientation( { type: 'angular', value: '0' } )
serializeGradientOrientation( {
type: 'angular',
value: '0',
unit: 'deg',
} )
).toBe( '0deg' );

expect(
serializeGradientOrientation( {
type: 'angular',
value: '0.25',
unit: 'turn',
} )
).toBe( '0.25turn' );
} );

test( 'serializeGradient', () => {
expect(
serializeGradient( {
type: 'linear-gradient',
orientation: { type: 'angular', value: '40' },
orientation: { type: 'angular', value: '40', unit: 'deg' },
colorStops: [
{
type: 'rgba',
Expand Down Expand Up @@ -160,7 +176,7 @@ describe( 'It should serialize a gradient', () => {
expect(
serializeGradient( {
type: 'linear-gradient',
orientation: { type: 'angular', value: '0' },
orientation: { type: 'angular', value: '0', unit: 'deg' },
colorStops: [
{
type: 'rgba',
Expand Down
70 changes: 70 additions & 0 deletions packages/components/src/custom-gradient-picker/test/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, test } from 'vitest';
import { DEFAULT_GRADIENT } from '../constants';
import {
getGradientAstWithDefault,
getGradientAstWithControlPoints,
getStopCssColor,
} from '../utils';
import { serializeGradient } from '../serializer';

describe( 'CustomGradientPicker gradient parsing', () => {
test.each( [
'90deg',
'0.25turn',
'100grad',
`${ Math.PI / 2 }rad`,
'to right',
] )( 'preserves the angle of %s when editing a color stop', ( angle ) => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
`linear-gradient(${ angle },red 0%,blue 100%)`
);

expect( hasGradient ).toBe( true );
expect( gradientAST.orientation ).toEqual( {
type: 'angular',
value: '90',
unit: 'deg',
} );
expect(
serializeGradient(
getGradientAstWithControlPoints( gradientAST, [
{ color: 'red', position: 10 },
{ color: 'blue', position: 100 },
] )
)
).toBe( 'linear-gradient(90deg,rgb(255,0,0) 10%,rgb(0,0,255) 100%)' );
} );

test( 'preserves an opaque HSLA color when editing a stop', () => {
const { gradientAST, hasGradient } = getGradientAstWithDefault(
'linear-gradient(90deg,hsla(0 100% 50%) 0%,blue 100%)'
);
const controlPoints = gradientAST.colorStops.map( ( stop ) => ( {
color: getStopCssColor( stop ),
position: Number( stop.length?.value ),
} ) );
controlPoints[ 0 ].position = 10;

expect( hasGradient ).toBe( true );
expect( serializeGradient( gradientAST ) ).toBe(
'linear-gradient(90deg,hsla(0,100%,50%,1) 0%,blue 100%)'
);
expect(
serializeGradient(
getGradientAstWithControlPoints( gradientAST, controlPoints )
)
).toBe( 'linear-gradient(90deg,rgb(255,0,0) 10%,rgb(0,0,255) 100%)' );
} );

test.each( [
'conic-gradient(from 90deg at 25% 25%,red 0%,blue 100%)',
'repeating-conic-gradient(red 0%,blue 100%)',
'linear-gradient(90deg,red 0% 50%,blue 100%)',
] )( 'uses the unselected fallback for unsupported %s', ( gradient ) => {
const result = getGradientAstWithDefault( gradient );
const fallback = getGradientAstWithDefault( DEFAULT_GRADIENT );

expect( result.hasGradient ).toBe( false );
expect( result.gradientAST ).toEqual( fallback.gradientAST );
} );
} );
4 changes: 1 addition & 3 deletions packages/components/src/custom-gradient-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ export type CustomGradientPickerProps = {
};

export type GradientAnglePickerProps = {
gradientAST:
| gradientParser.LinearGradientNode
| gradientParser.RepeatingLinearGradientNode;
gradientAST: gradientParser.LinearGradientNode;
hasGradient: boolean;
onChange: ( gradient: string ) => void;
};
Expand Down
38 changes: 34 additions & 4 deletions packages/components/src/custom-gradient-picker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import type { ControlPoint } from './types';

extend( [ namesPlugin ] );

const ANGLE_UNIT_TO_DEGREES: Record< gradientParser.AngleUnit, number > = {
deg: 1,
rad: 180 / Math.PI,
grad: 0.9,
turn: 360,
};

export function getLinearGradientRepresentation(
gradientAST: gradientParser.GradientNode
) {
Expand Down Expand Up @@ -41,7 +48,16 @@ export function getGradientAstWithDefault( value?: string | null ) {
'wp.components.CustomGradientPicker failed to parse the gradient with error',
error
);
}

// The picker cannot preserve conic orientations or a second position per stop.
// Keep these newly parsed forms on the existing unselected fallback path.
if (
! gradientAST ||
gradientAST.type === 'conic-gradient' ||
gradientAST.type === 'repeating-conic-gradient' ||
gradientAST.colorStops.some( ( stop ) => stop.length2 !== undefined )
) {
gradientAST = gradientParser.parse( DEFAULT_GRADIENT )[ 0 ];
hasGradient = false;
}
Expand All @@ -50,11 +66,25 @@ export function getGradientAstWithDefault( value?: string | null ) {
! Array.isArray( gradientAST.orientation ) &&
gradientAST.orientation?.type === 'directional'
) {
const direction =
gradientAST.orientation.value.toLowerCase() as keyof typeof DIRECTIONAL_ORIENTATION_ANGLE_MAP;
gradientAST.orientation = {
type: 'angular',
value: DIRECTIONAL_ORIENTATION_ANGLE_MAP[ direction ].toString(),
unit: 'deg',
};
}

if (
! Array.isArray( gradientAST.orientation ) &&
gradientAST.orientation?.type === 'angular' &&
gradientAST.orientation.unit !== 'deg'
) {
const { value: angle, unit } = gradientAST.orientation;
gradientAST.orientation = {
type: 'angular',
value: DIRECTIONAL_ORIENTATION_ANGLE_MAP[
gradientAST.orientation.value
].toString(),
value: `${ Number( angle ) * ANGLE_UNIT_TO_DEGREES[ unit ] }`,
unit: 'deg',
};
}

Expand Down Expand Up @@ -111,7 +141,7 @@ export function getStopCssColor( colorStop: gradientParser.ColorStop ) {
return `hsl(${ hue },${ saturation }%,${ lightness }%)`;
}
case 'hsla': {
const [ hue, saturation, lightness, alpha ] = colorStop.value;
const [ hue, saturation, lightness, alpha = '1' ] = colorStop.value;
return `hsla(${ hue },${ saturation }%,${ lightness }%,${ alpha })`;
}
default:
Expand Down
Loading