@automattic/charts 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.cjs +292 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +292 -101
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/area-chart/area-chart.tsx +3 -5
- package/src/charts/bar-chart/bar-chart.tsx +24 -15
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +197 -30
- package/src/charts/bar-chart/test/bar-chart.test.tsx +685 -1
- package/src/charts/line-chart/line-chart.tsx +3 -5
- package/src/charts/private/test/time-axis.test.ts +247 -2
- package/src/charts/private/time-axis.ts +252 -63
- package/src/components/legend/test/legend.test.tsx +2 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +2 -1
- package/src/hooks/test/use-chart-margin.test.tsx +60 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +83 -0
- package/src/hooks/use-chart-margin.tsx +3 -4
- package/src/hooks/use-xychart-theme.ts +17 -11
- package/src/types.ts +6 -6
- package/src/utils/index.ts +1 -1
- package/src/utils/resolve-css-var.ts +69 -28
- package/src/utils/test/create-css-variable-resolver.test.ts +83 -0
|
@@ -74,8 +74,9 @@ describe( 'TrendIndicator', () => {
|
|
|
74
74
|
<TrendIndicator direction="up" value="+10%" style={ { fontSize: '2rem' } } />
|
|
75
75
|
);
|
|
76
76
|
|
|
77
|
+
// Note getComputedStyle converts lengths to px.
|
|
77
78
|
// eslint-disable-next-line testing-library/no-node-access
|
|
78
|
-
expect( container.firstChild ).toHaveStyle( { fontSize: '
|
|
79
|
+
expect( container.firstChild ).toHaveStyle( { fontSize: '32px' } );
|
|
79
80
|
} );
|
|
80
81
|
|
|
81
82
|
describe( 'accessibility', () => {
|
|
@@ -184,4 +184,64 @@ describe( 'useChartMargin', () => {
|
|
|
184
184
|
// This is larger than the 20px default bottom margin, so it should be used.
|
|
185
185
|
expect( result.current.bottom ).toBe( 25 );
|
|
186
186
|
} );
|
|
187
|
+
|
|
188
|
+
describe( 'horizontal y ticks', () => {
|
|
189
|
+
const horizontalOptions = ( tickFormat: ( value: string | number ) => string ) => ( {
|
|
190
|
+
...optionsBase,
|
|
191
|
+
axis: {
|
|
192
|
+
...optionsBase.axis,
|
|
193
|
+
y: { ...optionsBase.axis.y, orientation: Orientation.left, tickFormat },
|
|
194
|
+
},
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
it( 'measures dated ticks by formatting the raw timestamps once', () => {
|
|
198
|
+
const formatHour = ( timestamp: string | number ) =>
|
|
199
|
+
new Date( timestamp ).toLocaleTimeString( undefined, { hour: 'numeric', hour12: true } );
|
|
200
|
+
const hourlyData = [
|
|
201
|
+
{
|
|
202
|
+
label: 'Series 1',
|
|
203
|
+
data: [
|
|
204
|
+
{ date: new Date( 2024, 0, 1, 6 ), value: 10 },
|
|
205
|
+
{ date: new Date( 2024, 0, 1, 7 ), value: 20 },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
renderHook( () =>
|
|
211
|
+
useChartMargin( 300, horizontalOptions( formatHour ), hourlyData, baseTheme, true )
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const [ ticks, measureFormatter ] = mockGetLongestTickWidth.mock.calls[ 0 ];
|
|
215
|
+
// Raw timestamps, so the formatter runs exactly once — formatting here
|
|
216
|
+
// and again while measuring would date-parse "6 AM" to "Invalid Date".
|
|
217
|
+
expect( ticks ).toEqual( [
|
|
218
|
+
new Date( 2024, 0, 1, 6 ).getTime(),
|
|
219
|
+
new Date( 2024, 0, 1, 7 ).getTime(),
|
|
220
|
+
] );
|
|
221
|
+
expect( measureFormatter ).toBe( formatHour );
|
|
222
|
+
} );
|
|
223
|
+
|
|
224
|
+
it( "measures labelled ticks through the caller's formatter", () => {
|
|
225
|
+
const withSuffix = ( label: string | number ) => `${ label } (total)`;
|
|
226
|
+
const labelledData = [
|
|
227
|
+
{
|
|
228
|
+
label: 'Series 1',
|
|
229
|
+
data: [
|
|
230
|
+
{ label: 'Mon', value: 10 },
|
|
231
|
+
{ label: 'Tuesday', value: 20 },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
renderHook( () =>
|
|
237
|
+
useChartMargin( 300, horizontalOptions( withSuffix ), labelledData, baseTheme, true )
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const [ ticks, measureFormatter ] = mockGetLongestTickWidth.mock.calls[ 0 ];
|
|
241
|
+
// A formatter that lengthens labels has to reach the measurement, or the
|
|
242
|
+
// margin under-measures and the widest label clips at the SVG edge.
|
|
243
|
+
expect( ticks ).toEqual( [ 'Mon', 'Tuesday' ] );
|
|
244
|
+
expect( measureFormatter ).toBe( withSuffix );
|
|
245
|
+
} );
|
|
246
|
+
} );
|
|
187
247
|
} );
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { useXYChartTheme } from '../use-xychart-theme';
|
|
3
|
+
import type { SeriesData } from '../../types';
|
|
4
|
+
|
|
5
|
+
// The theme resolves five catalog roles — background, grid, axis, tick and axis label. Each `getComputedStyle` call can force the browser to flush pending style, and this memo re-runs whenever `data` changes identity, so a dashboard mounting N charts pays the count below N times over.
|
|
6
|
+
const STABLE_DATA: SeriesData[] = [];
|
|
7
|
+
|
|
8
|
+
describe( 'useXYChartTheme style reads', () => {
|
|
9
|
+
let getComputedStyleSpy: jest.SpyInstance;
|
|
10
|
+
|
|
11
|
+
beforeEach( () => {
|
|
12
|
+
document.documentElement.style.setProperty( '--a8c-charts-color-grid', '#00ff00' );
|
|
13
|
+
getComputedStyleSpy = jest.spyOn( window, 'getComputedStyle' );
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
afterEach( () => {
|
|
17
|
+
getComputedStyleSpy.mockRestore();
|
|
18
|
+
document.documentElement.style.removeProperty( '--a8c-charts-color-grid' );
|
|
19
|
+
document.documentElement.style.removeProperty( '--a8c-charts-color-background' );
|
|
20
|
+
document.documentElement.style.removeProperty( '--a8c-charts-color-axis' );
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
const renderTheme = ( data: SeriesData[] = STABLE_DATA ) =>
|
|
24
|
+
renderHook( ( { series }: { series: SeriesData[] } ) => useXYChartTheme( series ), {
|
|
25
|
+
initialProps: { series: data },
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'reads every role from one computed-style snapshot, not one per role', () => {
|
|
29
|
+
document.documentElement.style.setProperty( '--a8c-charts-color-background', '#111111' );
|
|
30
|
+
document.documentElement.style.setProperty( '--a8c-charts-color-axis', '#222222' );
|
|
31
|
+
|
|
32
|
+
const { result } = renderTheme();
|
|
33
|
+
|
|
34
|
+
expect( getComputedStyleSpy ).toHaveBeenCalledTimes( 1 );
|
|
35
|
+
expect( result.current.backgroundColor ).toBe( '#111111' );
|
|
36
|
+
expect( result.current.gridStyles.stroke ).toBe( '#00ff00' );
|
|
37
|
+
expect( result.current.axisStyles.x.bottom.axisLine.stroke ).toBe( '#222222' );
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
it( 'does not re-read styles when the hook re-renders with the same inputs', () => {
|
|
41
|
+
const { rerender } = renderTheme();
|
|
42
|
+
|
|
43
|
+
getComputedStyleSpy.mockClear();
|
|
44
|
+
rerender( { series: STABLE_DATA } );
|
|
45
|
+
|
|
46
|
+
expect( getComputedStyleSpy ).not.toHaveBeenCalled();
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
// A caller passing an inline array literal — `<LineChart data={ [ … ] } />` — hands the hook a new array identity on every render. The memo keys on the series strokes instead, so the theme is not rebuilt and no style is read. Keyed on `data` this cost one query per render, and before the roles shared a snapshot it cost five.
|
|
50
|
+
it( 'does not rebuild when the caller passes a fresh data array of the same strokes', () => {
|
|
51
|
+
const { rerender } = renderTheme( [] );
|
|
52
|
+
|
|
53
|
+
getComputedStyleSpy.mockClear();
|
|
54
|
+
rerender( { series: [] } );
|
|
55
|
+
|
|
56
|
+
expect( getComputedStyleSpy ).not.toHaveBeenCalled();
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
it( 'rebuilds when a series stroke actually changes', () => {
|
|
60
|
+
const withStroke = ( stroke: string ) =>
|
|
61
|
+
[ { label: 'A', options: { stroke }, data: [] } ] as unknown as SeriesData[];
|
|
62
|
+
const { result, rerender } = renderTheme( withStroke( 'rgba(0, 0, 0, 0.5)' ) );
|
|
63
|
+
|
|
64
|
+
getComputedStyleSpy.mockClear();
|
|
65
|
+
rerender( { series: withStroke( 'rebeccapurple' ) } );
|
|
66
|
+
|
|
67
|
+
expect( getComputedStyleSpy ).toHaveBeenCalledTimes( 1 );
|
|
68
|
+
expect( result.current.colors[ 0 ] ).toBe( 'rebeccapurple' );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
// The memo key serialises the strokes rather than joining them, so a colour containing a comma or a space survives the round-trip intact.
|
|
72
|
+
it( 'keeps a stroke that contains separators intact', () => {
|
|
73
|
+
const { result } = renderTheme( [
|
|
74
|
+
{ label: 'A', options: { stroke: 'rgba(0, 0, 0, 0.5)' }, data: [] },
|
|
75
|
+
{ label: 'B', options: { stroke: 'var(--brand, #fff)' }, data: [] },
|
|
76
|
+
] as unknown as SeriesData[] );
|
|
77
|
+
|
|
78
|
+
expect( result.current.colors.slice( 0, 2 ) ).toEqual( [
|
|
79
|
+
'rgba(0, 0, 0, 0.5)',
|
|
80
|
+
'var(--brand, #fff)',
|
|
81
|
+
] );
|
|
82
|
+
} );
|
|
83
|
+
} );
|
|
@@ -75,10 +75,9 @@ export const useChartMargin = (
|
|
|
75
75
|
const allDataPoints = data.flatMap( series => series.data as DataPointDate[] );
|
|
76
76
|
|
|
77
77
|
if ( horizontal ) {
|
|
78
|
-
// When horizontal, y ticks
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
78
|
+
// When horizontal, y ticks render the category values; leave them raw so
|
|
79
|
+
// the axis tick formatter is applied exactly once, when measuring below.
|
|
80
|
+
return allDataPoints.map( d => d.label || d.date?.getTime() );
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
if ( options.axis?.y?.tickValues?.length ) {
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { buildChartTheme } from '@visx/xychart';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { useGlobalChartsTheme } from '../providers';
|
|
4
|
-
import {
|
|
4
|
+
import { createCssVariableResolver } from '../utils';
|
|
5
5
|
import type { SeriesData } from '../types';
|
|
6
6
|
|
|
7
|
-
// visx applies grid, axis, and tick-label colors as SVG presentation attributes,
|
|
8
|
-
// where CSS var() cannot resolve. Resolve WPDS tokens to concrete values (or their
|
|
9
|
-
// fallbacks) before handing the theme to buildChartTheme.
|
|
10
|
-
const resolveColor = ( value?: string ): string | undefined =>
|
|
11
|
-
value ? resolveCssVariable( value ) ?? value : value;
|
|
12
|
-
|
|
13
7
|
export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
14
8
|
const theme = useGlobalChartsTheme();
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
// The only thing the theme takes from `data` is the series strokes, so key the memo on those rather than on the array's identity. A caller passing an inline literal — `<LineChart data={ [ … ] } />`, which the stories and several consumers do — otherwise rebuilds the whole theme on every render. Serialised rather than joined: a stroke can be `rgba(0, 0, 0, 0.5)` or `var(--brand, #fff)`, and any separator that reads naturally inside a colour cannot round-trip.
|
|
11
|
+
const seriesColorKey = JSON.stringify(
|
|
12
|
+
( data ?? [] )
|
|
18
13
|
.map( series => series.options?.stroke )
|
|
19
|
-
.filter( ( color ): color is string => Boolean( color ) )
|
|
14
|
+
.filter( ( color ): color is string => Boolean( color ) )
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return useMemo( () => {
|
|
18
|
+
// visx applies grid, axis, and tick-label colors as SVG presentation attributes, where CSS var() cannot resolve. Resolve them to concrete values before handing the theme to buildChartTheme.
|
|
19
|
+
//
|
|
20
|
+
// One resolver per theme build, so the five roles below share a single getComputedStyle call rather than taking one each.
|
|
21
|
+
const resolve = createCssVariableResolver();
|
|
22
|
+
const resolveColor = ( value?: string ): string | undefined =>
|
|
23
|
+
value ? resolve( value ) ?? value : value;
|
|
24
|
+
|
|
25
|
+
const seriesColors: string[] = JSON.parse( seriesColorKey );
|
|
20
26
|
|
|
21
27
|
return buildChartTheme( {
|
|
22
28
|
...theme,
|
|
@@ -39,5 +45,5 @@ export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
|
39
45
|
fill: resolveColor( theme.svgLabelSmall.fill ),
|
|
40
46
|
},
|
|
41
47
|
} );
|
|
42
|
-
}, [ theme,
|
|
48
|
+
}, [ theme, seriesColorKey ] );
|
|
43
49
|
};
|
package/src/types.ts
CHANGED
|
@@ -482,12 +482,12 @@ export type AxisOptions = {
|
|
|
482
482
|
tickClassName?: string;
|
|
483
483
|
tickFormat?: TickFormatter< ScaleInput< AxisScale > >;
|
|
484
484
|
/**
|
|
485
|
-
* Bucket resolution of the data on
|
|
486
|
-
* tick formatter derives tick formats from it
|
|
487
|
-
* inferring the resolution from point spacing.
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
* Ignored when `tickFormat` is set.
|
|
485
|
+
* Bucket resolution of the data, set on whichever axis carries the dates.
|
|
486
|
+
* When set, the automatic tick formatter derives tick formats from it
|
|
487
|
+
* directly instead of inferring the resolution from point spacing. For
|
|
488
|
+
* daily-or-finer buckets the overall time span still constrains the choice —
|
|
489
|
+
* e.g. hourly buckets spanning more than a week get date ticks, since hour
|
|
490
|
+
* ticks would be unreadable at that span. Ignored when `tickFormat` is set.
|
|
491
491
|
*/
|
|
492
492
|
tickResolution?: TickResolution;
|
|
493
493
|
/**
|
package/src/utils/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { mergeThemes } from './merge-themes';
|
|
|
30
30
|
export * from './color-utils';
|
|
31
31
|
|
|
32
32
|
// CSS utilities
|
|
33
|
-
export { resolveCssVariable } from './resolve-css-var';
|
|
33
|
+
export { resolveCssVariable, createCssVariableResolver } from './resolve-css-var';
|
|
34
34
|
|
|
35
35
|
// Font sizing utilities
|
|
36
36
|
export { resolveFontSize } from './resolve-font-size';
|
|
@@ -15,34 +15,57 @@ const CSS_VAR_NAME_PATTERN = /^--[\w-]+$/;
|
|
|
15
15
|
* @param element - Optional DOM element to resolve the variable from (defaults to document.documentElement)
|
|
16
16
|
* @return The resolved value, fallback value, or null if unresolvable
|
|
17
17
|
*/
|
|
18
|
-
export const resolveCssVariable = (
|
|
19
|
-
value
|
|
18
|
+
export const resolveCssVariable = ( value: string, element?: HTMLElement | null ): string | null =>
|
|
19
|
+
createCssVariableResolver( element )( value );
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Builds a resolver that reads every value from one `getComputedStyle` snapshot.
|
|
23
|
+
*
|
|
24
|
+
* `getComputedStyle` is the expensive half of resolving a token: each call can force the browser to flush pending style, and a chart theme resolves five roles at once. Taking the snapshot once per build turns that into a single query. The reads that share a snapshot happen synchronously inside one memo body, so no style change can land between them — the resolver is single-pass by design and is not meant to be held across renders.
|
|
25
|
+
*
|
|
26
|
+
* @param element - The element to resolve against, or null/undefined for the document root.
|
|
27
|
+
* @return A resolver that resolves each value exactly as `resolveCssVariable` does.
|
|
28
|
+
*/
|
|
29
|
+
export function createCssVariableResolver(
|
|
20
30
|
element?: HTMLElement | null
|
|
21
|
-
): string | null
|
|
22
|
-
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
31
|
+
): ( value: string ) => string | null {
|
|
32
|
+
let styles: CSSStyleDeclaration | null = null;
|
|
25
33
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
// Deferred so a resolver for a theme that turns out to hold only literals costs nothing. Only a successful read is kept: caching the null would leave a resolver built before its element is in the document dead for the rest of its life.
|
|
35
|
+
const getStyles = () => {
|
|
36
|
+
if ( ! styles ) {
|
|
37
|
+
styles = computedStyleFor( element );
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
return styles;
|
|
41
|
+
};
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
return ( value: string ): string | null => {
|
|
44
|
+
if ( ! value ) {
|
|
45
|
+
return null;
|
|
35
46
|
}
|
|
36
|
-
}
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
// Check if it's a var() expression: var(--name) or var(--name, fallback)
|
|
49
|
+
// Parse manually to avoid regex backtracking vulnerabilities
|
|
50
|
+
if ( value.startsWith( 'var(' ) && value.endsWith( ')' ) ) {
|
|
51
|
+
const parsed = parseVarExpression( value );
|
|
52
|
+
|
|
53
|
+
if ( parsed ) {
|
|
54
|
+
const resolved = readCustomProperty( parsed.varName, getStyles() );
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
56
|
+
return resolved || parsed.fallback;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if it's a plain variable name (starts with --)
|
|
61
|
+
if ( value.startsWith( '--' ) ) {
|
|
62
|
+
return readCustomProperty( value, getStyles() );
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Return regular values as-is (e.g., '#ffffff', 'red')
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
46
69
|
|
|
47
70
|
/**
|
|
48
71
|
* Parses a var() expression into its variable name and optional fallback.
|
|
@@ -87,24 +110,42 @@ function parseVarExpression( expr: string ): { varName: string; fallback: string
|
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
/**
|
|
90
|
-
*
|
|
113
|
+
* Takes one computed-style snapshot, or null where there is nothing to read (SSR, or a detached element).
|
|
114
|
+
*
|
|
115
|
+
* @param element - The element to read from, or null/undefined for the document root.
|
|
116
|
+
* @return The computed styles, or null.
|
|
117
|
+
*/
|
|
118
|
+
function computedStyleFor( element?: HTMLElement | null ): CSSStyleDeclaration | null {
|
|
119
|
+
if ( typeof window === 'undefined' || typeof document === 'undefined' ) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
return getComputedStyle( element || document.documentElement );
|
|
125
|
+
} catch {
|
|
126
|
+
// getComputedStyle throws on a detached element.
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Reads one custom property from a computed-style snapshot.
|
|
91
133
|
*
|
|
92
134
|
* @param varName - A CSS variable name like '--my-color'
|
|
93
|
-
* @param
|
|
135
|
+
* @param styles - The snapshot to read from, or null
|
|
94
136
|
* @return The computed value or null
|
|
95
137
|
*/
|
|
96
|
-
function
|
|
97
|
-
if (
|
|
138
|
+
function readCustomProperty( varName: string, styles: CSSStyleDeclaration | null ): string | null {
|
|
139
|
+
if ( ! styles ) {
|
|
98
140
|
return null;
|
|
99
141
|
}
|
|
100
142
|
|
|
101
143
|
try {
|
|
102
|
-
const
|
|
103
|
-
const computedValue = getComputedStyle( targetElement ).getPropertyValue( varName ).trim();
|
|
144
|
+
const computedValue = styles.getPropertyValue( varName ).trim();
|
|
104
145
|
|
|
105
146
|
return computedValue || null;
|
|
106
147
|
} catch {
|
|
107
|
-
//
|
|
148
|
+
// Belt and braces — no engine is known to throw here. Kept because a caller may hand in a stand-in for CSSStyleDeclaration; `resolve-css-var.test.ts` covers exactly that.
|
|
108
149
|
return null;
|
|
109
150
|
}
|
|
110
151
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createCssVariableResolver } from '../resolve-css-var';
|
|
2
|
+
|
|
3
|
+
describe( 'createCssVariableResolver', () => {
|
|
4
|
+
let originalGetComputedStyle: typeof window.getComputedStyle;
|
|
5
|
+
|
|
6
|
+
beforeEach( () => {
|
|
7
|
+
originalGetComputedStyle = window.getComputedStyle;
|
|
8
|
+
} );
|
|
9
|
+
|
|
10
|
+
afterEach( () => {
|
|
11
|
+
window.getComputedStyle = originalGetComputedStyle;
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
const mockStyles = ( values: Record< string, string > ) => {
|
|
15
|
+
const spy = jest.fn( () => ( {
|
|
16
|
+
getPropertyValue: ( prop: string ) => values[ prop ] ?? '',
|
|
17
|
+
} ) );
|
|
18
|
+
window.getComputedStyle = spy as unknown as typeof window.getComputedStyle;
|
|
19
|
+
|
|
20
|
+
return spy;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
it( 'reads many values from a single snapshot', () => {
|
|
24
|
+
const spy = mockStyles( { '--a': '#111', '--b': '#222' } );
|
|
25
|
+
const resolve = createCssVariableResolver();
|
|
26
|
+
|
|
27
|
+
expect( resolve( '--a' ) ).toBe( '#111' );
|
|
28
|
+
expect( resolve( 'var(--b)' ) ).toBe( '#222' );
|
|
29
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
it( 'takes no snapshot at all for values that need no lookup', () => {
|
|
33
|
+
const spy = mockStyles( {} );
|
|
34
|
+
const resolve = createCssVariableResolver();
|
|
35
|
+
|
|
36
|
+
expect( resolve( '#ffffff' ) ).toBe( '#ffffff' );
|
|
37
|
+
expect( resolve( '' ) ).toBeNull();
|
|
38
|
+
expect( spy ).not.toHaveBeenCalled();
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'resolves against the element it was built for', () => {
|
|
42
|
+
const element = document.createElement( 'div' );
|
|
43
|
+
const spy = jest.fn( ( target: Element ) => ( {
|
|
44
|
+
getPropertyValue: () => ( target === element ? '#c029dc' : '#000000' ),
|
|
45
|
+
} ) );
|
|
46
|
+
window.getComputedStyle = spy as unknown as typeof window.getComputedStyle;
|
|
47
|
+
|
|
48
|
+
expect( createCssVariableResolver( element )( '--accent' ) ).toBe( '#c029dc' );
|
|
49
|
+
expect( spy ).toHaveBeenCalledWith( element );
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'falls back to the var() fallback when the property is unset', () => {
|
|
53
|
+
mockStyles( {} );
|
|
54
|
+
|
|
55
|
+
expect( createCssVariableResolver()( 'var(--missing, #dbdbdb)' ) ).toBe( '#dbdbdb' );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
// A failed read is not cached: a resolver built before its element is in the document would otherwise stay dead for the rest of its life, which is the shape CHARTS-255 introduces.
|
|
59
|
+
it( 'retries after a snapshot that could not be taken', () => {
|
|
60
|
+
const failing = jest.fn( () => {
|
|
61
|
+
throw new Error( 'not attached' );
|
|
62
|
+
} );
|
|
63
|
+
window.getComputedStyle = failing as unknown as typeof window.getComputedStyle;
|
|
64
|
+
|
|
65
|
+
const resolve = createCssVariableResolver();
|
|
66
|
+
expect( resolve( '--a' ) ).toBeNull();
|
|
67
|
+
|
|
68
|
+
mockStyles( { '--a': '#111' } );
|
|
69
|
+
expect( resolve( '--a' ) ).toBe( '#111' );
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
it( 'matches resolveCssVariable for the values it is given', () => {
|
|
73
|
+
mockStyles( { '--a': '#111' } );
|
|
74
|
+
const resolve = createCssVariableResolver();
|
|
75
|
+
|
|
76
|
+
expect( [
|
|
77
|
+
resolve( '--a' ),
|
|
78
|
+
resolve( 'var(--a)' ),
|
|
79
|
+
resolve( 'var(--z, red)' ),
|
|
80
|
+
resolve( 'red' ),
|
|
81
|
+
] ).toEqual( [ '#111', '#111', 'red', 'red' ] );
|
|
82
|
+
} );
|
|
83
|
+
} );
|