Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Support HSL, RGB, and RGBA theme colors in addition to hex format
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,24 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
if ( Array.isArray( colors ) ) {
for ( const color of colors ) {
if ( color && typeof color === 'string' ) {
let colorValue = color;

// Handle CSS custom properties - resolve them to actual values
// Supports both '--var-name' and 'var(--var-name)' formats
// Use wrapper element to resolve scoped CSS variables
if ( color.startsWith( '--' ) || color.startsWith( 'var(' ) ) {
const resolved = resolveCssVariable( color, wrapperRef.current );

if ( resolved === null || resolved === '' ) {
continue;
}

colorValue = resolved;
// Normalize color to hex format, handling CSS variables, RGB, HSL, etc.
// This uses normalizeColorToHex which resolves CSS variables and converts
// rgb(), rgba(), hsl() formats to hex
const normalizedColor = normalizeColorToHex(
color,
wrapperRef.current,
resolveCssVariable
);

// Skip if normalization failed or returned empty string
if ( ! normalizedColor ) {
continue;
}

// Process hex colors
if ( colorValue.startsWith( '#' ) ) {
resolvedColors.push( colorValue );
const hslColor = d3Hsl( colorValue );
// Only process valid hex colors (normalizeColorToHex ensures this)
if ( normalizedColor.startsWith( '#' ) ) {
resolvedColors.push( normalizedColor );
const hslColor = d3Hsl( normalizedColor );
// d3Hsl returns NaN values for invalid colors
if ( ! isNaN( hslColor.h ) ) {
const hslTuple: [ number, number, number ] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1901,7 +1901,7 @@ describe( 'ChartContext', () => {
};

const cssVarTheme: ChartTheme = {
colors: [ '--rgb-color', '#ff0000' ],
colors: [ '--rgb-color', '#00ff00' ],
} as ChartTheme;

render(
Expand All @@ -1910,14 +1910,135 @@ describe( 'ChartContext', () => {
</GlobalChartsProvider>
);

// Non-hex colors are currently skipped, should use second color
// RGB colors should now be converted to hex and used
const color = contextValue.getElementStyles( {
data: undefined,
index: 0,
} ).color;

expect( color ).toBe( '#ff0000' );
} );

it( 'handles CSS variables resolving to HSL colors', () => {
window.getComputedStyle = jest.fn( () => ( {
getPropertyValue: ( prop: string ) => {
if ( prop === '--hsl-color' ) {
return 'hsl(120, 100%, 50%)'; // HSL format (green)
}
return '';
},
} ) ) as unknown as typeof window.getComputedStyle;

let contextValue: GlobalChartsContextValue;

const TestComponent = () => {
contextValue = useGlobalChartsContext();
return <div>Test</div>;
};

const cssVarTheme: ChartTheme = {
colors: [ '--hsl-color', '#ff0000' ],
} as ChartTheme;

render(
<GlobalChartsProvider theme={ cssVarTheme }>
<TestComponent />
</GlobalChartsProvider>
);

// HSL colors should be converted to hex and used
const color = contextValue.getElementStyles( {
data: undefined,
index: 0,
} ).color;

expect( color ).toBe( '#00ff00' );
} );

it( 'handles CSS variables resolving to RGBA colors', () => {
window.getComputedStyle = jest.fn( () => ( {
getPropertyValue: ( prop: string ) => {
if ( prop === '--rgba-color' ) {
return 'rgba(0, 0, 255, 0.5)'; // RGBA format (blue with transparency)
}
return '';
},
} ) ) as unknown as typeof window.getComputedStyle;

let contextValue: GlobalChartsContextValue;

const TestComponent = () => {
contextValue = useGlobalChartsContext();
return <div>Test</div>;
};

const cssVarTheme: ChartTheme = {
colors: [ '--rgba-color', '#ff0000' ],
} as ChartTheme;

render(
<GlobalChartsProvider theme={ cssVarTheme }>
<TestComponent />
</GlobalChartsProvider>
);

// RGBA colors should be converted to hex (transparency info is lost)
const color = contextValue.getElementStyles( {
data: undefined,
index: 0,
} ).color;

expect( color ).toBe( '#0000ff' );
} );

it( 'handles mix of RGB, HSL, and hex in theme colors', () => {
window.getComputedStyle = jest.fn( () => ( {
getPropertyValue: ( prop: string ) => {
if ( prop === '--rgb-red' ) {
return 'rgb(255, 0, 0)';
}
if ( prop === '--hsl-green' ) {
return 'hsl(120, 100%, 50%)';
}
return '';
},
} ) ) as unknown as typeof window.getComputedStyle;

let contextValue: GlobalChartsContextValue;

const TestComponent = () => {
contextValue = useGlobalChartsContext();
return <div>Test</div>;
};

const cssVarTheme: ChartTheme = {
colors: [ '--rgb-red', '--hsl-green', '#0000ff' ],
} as ChartTheme;

render(
<GlobalChartsProvider theme={ cssVarTheme }>
<TestComponent />
</GlobalChartsProvider>
);

// All color formats should be properly converted
const color1 = contextValue.getElementStyles( {
data: undefined,
index: 0,
} ).color;
const color2 = contextValue.getElementStyles( {
data: undefined,
index: 1,
} ).color;
const color3 = contextValue.getElementStyles( {
data: undefined,
index: 2,
} ).color;

expect( color1 ).toBe( '#ff0000' ); // RGB red
expect( color2 ).toBe( '#00ff00' ); // HSL green
expect( color3 ).toBe( '#0000ff' ); // Hex blue
} );
} );

describe( 'Error Handling', () => {
Expand Down Expand Up @@ -2052,7 +2173,7 @@ describe( 'ChartContext', () => {
} ).color;

expect( color1 ).toBe( '#ff0000' );
expect( color2 ).toBe( '#bad' ); // Invalid color is still in palette
expect( color2 ).toBe( '#bbaadd' ); // #bad is expanded to #bbaadd by normalizeColorToHex
expect( color3 ).toBe( '#0000ff' );
} );
} );
Expand Down
17 changes: 10 additions & 7 deletions projects/js-packages/charts/src/utils/color-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,17 @@ export const parseHslString = ( hslString: string ): [ number, number, number ]

/**
* Parse an RGB string like 'rgb(255, 0, 0)' into a hex color.
* Note: This function specifically handles rgb() format only, not rgba().
* For general color normalization including rgba(), use normalizeColorToHex() instead.
*
* @param rgbString - RGB color string
* @param rgbString - RGB color string (not RGBA)
* @return hex color string or null if invalid
*/
export const parseRgbString = ( rgbString: string ): string | null => {
const lower = rgbString.toLowerCase().trim();

// Check prefix - only handle rgb(), not rgba()
// This is intentional - use normalizeColorToHex for rgba() support
if ( ! lower.startsWith( 'rgb(' ) || lower.startsWith( 'rgba(' ) ) {
return null;
}
Expand Down Expand Up @@ -160,12 +163,12 @@ export const normalizeColorToHex = (
return color;
}

// Handle HSL and RGB strings using d3-color
if ( trimmed.startsWith( 'hsl(' ) || trimmed.startsWith( 'rgb(' ) ) {
// Reject rgba() - we only handle rgb()
if ( trimmed.startsWith( 'rgba(' ) ) {
return color;
}
// Handle HSL, RGB, and RGBA strings using d3-color
if (
trimmed.startsWith( 'hsl(' ) ||
trimmed.startsWith( 'rgb(' ) ||
trimmed.startsWith( 'rgba(' )
) {
const parsed = d3Color( trimmed );
if ( parsed ) {
return parsed.formatHex();
Expand Down
16 changes: 16 additions & 0 deletions projects/js-packages/charts/src/utils/test/color-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,22 @@ describe( 'normalizeColorToHex', () => {
} );
} );

describe( 'RGBA strings', () => {
it( 'converts rgba(255, 0, 0, 1) to #ff0000', () => {
expect( normalizeColorToHex( 'rgba(255, 0, 0, 1)' ) ).toBe( '#ff0000' );
} );

it( 'converts rgba(0, 0, 255, 0.5) to #0000ff', () => {
// Alpha channel is lost in hex conversion
expect( normalizeColorToHex( 'rgba(0, 0, 255, 0.5)' ) ).toBe( '#0000ff' );
} );

it( 'converts rgba(128, 128, 128, 0) to #000000', () => {
// d3-color converts fully transparent colors (alpha=0) to black
expect( normalizeColorToHex( 'rgba(128, 128, 128, 0)' ) ).toBe( '#000000' );
} );
} );

describe( 'CSS variables', () => {
it( 'returns original if no resolveCss function provided', () => {
expect( normalizeColorToHex( '--my-color' ) ).toBe( '--my-color' );
Expand Down
Loading