@automattic/charts 2.0.0 → 3.0.0
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 +28 -0
- package/TOKENS.md +107 -48
- package/dist/index.cjs +584 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +97 -43
- package/dist/index.d.cts +46 -41
- package/dist/index.d.ts +46 -41
- package/dist/index.js +582 -210
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/area-chart/area-chart.module.scss +8 -3
- package/src/charts/area-chart/area-chart.tsx +3 -5
- package/src/charts/bar-chart/bar-chart.module.scss +14 -6
- 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/conversion-funnel-chart/conversion-funnel-chart.module.scss +17 -12
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
- package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
- package/src/charts/geo-chart/geo-chart.tsx +12 -4
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +13 -9
- package/src/charts/heatmap-chart/heatmap-chart.tsx +12 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
- package/src/charts/leaderboard-chart/types.ts +0 -2
- package/src/charts/line-chart/line-chart.module.scss +10 -5
- package/src/charts/line-chart/line-chart.tsx +3 -5
- package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
- package/src/charts/pie-chart/pie-chart.tsx +4 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -1
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/time-axis.test.ts +247 -2
- package/src/charts/private/time-axis.ts +252 -63
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/components/legend/private/base-legend.tsx +3 -1
- package/src/components/legend/test/legend.test.tsx +2 -1
- package/src/components/tooltip/accessible-tooltip.tsx +4 -2
- package/src/components/tooltip/base-tooltip.tsx +8 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +22 -1
- package/src/components/trend-indicator/trend-indicator.module.scss +3 -6
- package/src/components/trend-indicator/trend-indicator.tsx +3 -0
- package/src/hooks/test/use-chart-margin.test.tsx +60 -0
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +89 -0
- package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
- package/src/hooks/use-chart-margin.tsx +3 -4
- package/src/hooks/use-xychart-theme.ts +19 -11
- package/src/index.ts +9 -2
- package/src/providers/chart-context/global-charts-provider.tsx +35 -12
- package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
- package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +0 -40
- package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
- package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
- package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
- package/src/providers/chart-context/themes.ts +18 -27
- package/src/providers/chart-context/types.ts +0 -3
- package/src/providers/chart-scope/chart-scope-context.ts +8 -0
- package/src/providers/chart-scope/index.ts +3 -0
- package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
- package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
- package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
- package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
- package/src/providers/index.ts +1 -0
- package/src/styles/chart-scope-class.ts +6 -0
- package/src/styles/chart-scope.scss +83 -0
- package/src/styles/test/chart-scope.test.ts +165 -0
- package/src/types.ts +6 -40
- 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
- package/src/charts/private/grid-control/grid-control.module.scss +0 -8
- package/src/charts/private/grid-control/grid-control.tsx +0 -26
- package/src/charts/private/grid-control/index.ts +0 -1
- package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
|
@@ -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,28 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { GlobalChartsProvider } from '../../providers/chart-context/global-charts-provider';
|
|
3
|
+
import { ChartScopeContext } from '../../providers/chart-scope';
|
|
4
|
+
import { useXYChartTheme } from '../use-xychart-theme';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
// A `theme` prop override reaches CSS-painted elements through the inline `--a8c-charts-*` var GlobalChartsProvider writes on its wrapper, and reaches visx (SVG presentation attributes) through `providerTheme`. Before the fix, `providerTheme` carried the override's literal directly, so an element-level `--a8c-charts-color-grid` override set inside the provider tree had no effect on the JS-resolved value — the two paths could disagree. After the fix, `providerTheme` carries the catalog pointer for an overridden role, so both paths resolve through the same CSS cascade and the nearer element wins.
|
|
8
|
+
describe( 'useXYChartTheme resolves a theme prop override through the CSS cascade', () => {
|
|
9
|
+
it( "an element override set inside the provider tree beats the provider's theme prop literal", () => {
|
|
10
|
+
const nestedOverride = document.createElement( 'div' );
|
|
11
|
+
nestedOverride.style.setProperty( '--a8c-charts-color-grid', '#00ff00' );
|
|
12
|
+
document.body.appendChild( nestedOverride );
|
|
13
|
+
|
|
14
|
+
const wrapper = ( { children }: { children: ReactNode } ) => (
|
|
15
|
+
<GlobalChartsProvider theme={ { gridStyles: { stroke: 'rgb(255,0,0)' } } }>
|
|
16
|
+
<ChartScopeContext.Provider value={ nestedOverride }>
|
|
17
|
+
{ children }
|
|
18
|
+
</ChartScopeContext.Provider>
|
|
19
|
+
</GlobalChartsProvider>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const { result } = renderHook( () => useXYChartTheme( [] ), { wrapper } );
|
|
23
|
+
|
|
24
|
+
expect( result.current.gridStyles.stroke ).toBe( '#00ff00' );
|
|
25
|
+
|
|
26
|
+
document.body.removeChild( nestedOverride );
|
|
27
|
+
} );
|
|
28
|
+
} );
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { ChartScopeContext } from '../../providers/chart-scope';
|
|
3
|
+
import { useXYChartTheme } from '../use-xychart-theme';
|
|
4
|
+
import type { SeriesData } from '../../types';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
// 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 only when the scope element attaches or a series colour changes, so a dashboard mounting N charts pays the count below N times over.
|
|
8
|
+
const STABLE_DATA: SeriesData[] = [];
|
|
9
|
+
|
|
10
|
+
describe( 'useXYChartTheme style reads', () => {
|
|
11
|
+
let scope: HTMLElement;
|
|
12
|
+
let getComputedStyleSpy: jest.SpyInstance;
|
|
13
|
+
|
|
14
|
+
beforeEach( () => {
|
|
15
|
+
scope = document.createElement( 'div' );
|
|
16
|
+
scope.style.setProperty( '--a8c-charts-color-grid', '#00ff00' );
|
|
17
|
+
document.body.appendChild( scope );
|
|
18
|
+
getComputedStyleSpy = jest.spyOn( window, 'getComputedStyle' );
|
|
19
|
+
} );
|
|
20
|
+
|
|
21
|
+
afterEach( () => {
|
|
22
|
+
getComputedStyleSpy.mockRestore();
|
|
23
|
+
scope.remove();
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
const renderWithScope = ( data: SeriesData[] = STABLE_DATA ) =>
|
|
27
|
+
renderHook( ( { series }: { series: SeriesData[] } ) => useXYChartTheme( series ), {
|
|
28
|
+
initialProps: { series: data },
|
|
29
|
+
wrapper: ( { children }: { children: ReactNode } ) => (
|
|
30
|
+
<ChartScopeContext.Provider value={ scope }>{ children }</ChartScopeContext.Provider>
|
|
31
|
+
),
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'reads every role from one computed-style snapshot, not one per role', () => {
|
|
35
|
+
scope.style.setProperty( '--a8c-charts-color-background', '#111111' );
|
|
36
|
+
scope.style.setProperty( '--a8c-charts-color-axis', '#222222' );
|
|
37
|
+
|
|
38
|
+
const { result } = renderWithScope();
|
|
39
|
+
|
|
40
|
+
expect( getComputedStyleSpy ).toHaveBeenCalledTimes( 1 );
|
|
41
|
+
expect( result.current.backgroundColor ).toBe( '#111111' );
|
|
42
|
+
expect( result.current.gridStyles.stroke ).toBe( '#00ff00' );
|
|
43
|
+
expect( result.current.axisStyles.x.bottom.axisLine.stroke ).toBe( '#222222' );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
it( 'does not re-read styles when the hook re-renders with the same inputs', () => {
|
|
47
|
+
const { rerender } = renderWithScope();
|
|
48
|
+
|
|
49
|
+
getComputedStyleSpy.mockClear();
|
|
50
|
+
rerender( { series: STABLE_DATA } );
|
|
51
|
+
|
|
52
|
+
expect( getComputedStyleSpy ).not.toHaveBeenCalled();
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
// 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.
|
|
56
|
+
it( 'does not rebuild when the caller passes a fresh data array of the same strokes', () => {
|
|
57
|
+
const { rerender } = renderWithScope( [] );
|
|
58
|
+
|
|
59
|
+
getComputedStyleSpy.mockClear();
|
|
60
|
+
rerender( { series: [] } );
|
|
61
|
+
|
|
62
|
+
expect( getComputedStyleSpy ).not.toHaveBeenCalled();
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'rebuilds when a series stroke actually changes', () => {
|
|
66
|
+
const withStroke = ( stroke: string ) =>
|
|
67
|
+
[ { label: 'A', options: { stroke }, data: [] } ] as unknown as SeriesData[];
|
|
68
|
+
const { result, rerender } = renderWithScope( withStroke( 'rgba(0, 0, 0, 0.5)' ) );
|
|
69
|
+
|
|
70
|
+
getComputedStyleSpy.mockClear();
|
|
71
|
+
rerender( { series: withStroke( 'rebeccapurple' ) } );
|
|
72
|
+
|
|
73
|
+
expect( getComputedStyleSpy ).toHaveBeenCalledTimes( 1 );
|
|
74
|
+
expect( result.current.colors[ 0 ] ).toBe( 'rebeccapurple' );
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
// The memo key serialises the strokes rather than joining them, so a colour containing a comma or a space survives the round-trip intact.
|
|
78
|
+
it( 'keeps a stroke that contains separators intact', () => {
|
|
79
|
+
const { result } = renderWithScope( [
|
|
80
|
+
{ label: 'A', options: { stroke: 'rgba(0, 0, 0, 0.5)' }, data: [] },
|
|
81
|
+
{ label: 'B', options: { stroke: 'var(--brand, #fff)' }, data: [] },
|
|
82
|
+
] as unknown as SeriesData[] );
|
|
83
|
+
|
|
84
|
+
expect( result.current.colors.slice( 0, 2 ) ).toEqual( [
|
|
85
|
+
'rgba(0, 0, 0, 0.5)',
|
|
86
|
+
'var(--brand, #fff)',
|
|
87
|
+
] );
|
|
88
|
+
} );
|
|
89
|
+
} );
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { ChartScopeContext } from '../../providers/chart-scope';
|
|
3
|
+
import { useXYChartTheme } from '../use-xychart-theme';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
describe( 'useXYChartTheme', () => {
|
|
7
|
+
it( 'resolves grid colour against the scope element, not the document root', () => {
|
|
8
|
+
document.documentElement.style.setProperty( '--a8c-charts-color-grid', '#ff0000' );
|
|
9
|
+
|
|
10
|
+
const scope = document.createElement( 'div' );
|
|
11
|
+
scope.style.setProperty( '--a8c-charts-color-grid', '#00ff00' );
|
|
12
|
+
document.body.appendChild( scope );
|
|
13
|
+
|
|
14
|
+
const wrapper = ( { children }: { children: ReactNode } ) => (
|
|
15
|
+
<ChartScopeContext.Provider value={ scope }>{ children }</ChartScopeContext.Provider>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const { result } = renderHook( () => useXYChartTheme( [] ), { wrapper } );
|
|
19
|
+
|
|
20
|
+
expect( result.current.gridStyles.stroke ).toBe( '#00ff00' );
|
|
21
|
+
|
|
22
|
+
document.documentElement.style.removeProperty( '--a8c-charts-color-grid' );
|
|
23
|
+
document.body.removeChild( scope );
|
|
24
|
+
} );
|
|
25
|
+
} );
|
|
@@ -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,30 @@
|
|
|
1
1
|
import { buildChartTheme } from '@visx/xychart';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { useGlobalChartsTheme } from '../providers';
|
|
4
|
-
import {
|
|
4
|
+
import { useChartScopeElement } from '../providers/chart-scope';
|
|
5
|
+
import { createCssVariableResolver } from '../utils';
|
|
5
6
|
import type { SeriesData } from '../types';
|
|
6
7
|
|
|
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
8
|
export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
14
9
|
const theme = useGlobalChartsTheme();
|
|
10
|
+
const scopeElement = useChartScopeElement();
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
// 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.
|
|
13
|
+
const seriesColorKey = JSON.stringify(
|
|
14
|
+
( data ?? [] )
|
|
18
15
|
.map( series => series.options?.stroke )
|
|
19
|
-
.filter( ( color ): color is string => Boolean( color ) )
|
|
16
|
+
.filter( ( color ): color is string => Boolean( color ) )
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return useMemo( () => {
|
|
20
|
+
// visx applies grid, axis, and tick-label colours as SVG presentation attributes, where CSS var() cannot resolve. Resolve the catalog token against the chart's own scope element — never :root — so this reads any override set inside the provider tree, the same one a CSS-painted element would inherit.
|
|
21
|
+
//
|
|
22
|
+
// One resolver per theme build, so the five roles below share a single getComputedStyle call rather than taking one each: this memo re-runs only when the scope element attaches or a series colour changes, which for a dashboard of charts is the difference between two style queries per chart and ten.
|
|
23
|
+
const resolve = createCssVariableResolver( scopeElement );
|
|
24
|
+
const resolveColor = ( value?: string ): string | undefined =>
|
|
25
|
+
value ? resolve( value ) ?? value : value;
|
|
26
|
+
|
|
27
|
+
const seriesColors: string[] = JSON.parse( seriesColorKey );
|
|
20
28
|
|
|
21
29
|
return buildChartTheme( {
|
|
22
30
|
...theme,
|
|
@@ -39,5 +47,5 @@ export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
|
39
47
|
fill: resolveColor( theme.svgLabelSmall.fill ),
|
|
40
48
|
},
|
|
41
49
|
} );
|
|
42
|
-
}, [ theme,
|
|
50
|
+
}, [ theme, seriesColorKey, scopeElement ] );
|
|
43
51
|
};
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,8 @@ export {
|
|
|
32
32
|
useGlobalChartsTheme,
|
|
33
33
|
GlobalChartsContext,
|
|
34
34
|
defaultTheme,
|
|
35
|
+
useChartScopeElement,
|
|
36
|
+
useChartRegistration,
|
|
35
37
|
} from './providers';
|
|
36
38
|
|
|
37
39
|
// Types - explicit exports (excludes internal types like DataPointPercentageCalculated)
|
|
@@ -60,7 +62,6 @@ export type {
|
|
|
60
62
|
ChartLegendConfig,
|
|
61
63
|
SeriesChartLegendConfig,
|
|
62
64
|
BaseChartProps,
|
|
63
|
-
GridProps,
|
|
64
65
|
GoogleDataTableColumn,
|
|
65
66
|
GoogleDataTableColumnRoleType,
|
|
66
67
|
GoogleDataTableRow,
|
|
@@ -122,6 +123,12 @@ export type { ArcData } from './charts/pie-semi-circle-chart';
|
|
|
122
123
|
export type { SparklineProps, GradientConfig, SparklineDataPoint } from './charts/sparkline';
|
|
123
124
|
|
|
124
125
|
// Utilities
|
|
125
|
-
export {
|
|
126
|
+
export {
|
|
127
|
+
parseAsLocalDate,
|
|
128
|
+
formatMetricValue,
|
|
129
|
+
formatPercentage,
|
|
130
|
+
mergeThemes,
|
|
131
|
+
resolveCssVariable,
|
|
132
|
+
} from './utils';
|
|
126
133
|
export * from './utils/color-utils';
|
|
127
134
|
export type { MetricValueType } from './utils';
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
useLayoutEffect,
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react';
|
|
11
|
+
// Side-effect import: emits the `--a8c-charts-*` catalog, scoped to the `a8c-charts-scope` class applied to this component's wrapper below. Every chart either sits under a `GlobalChartsProvider` or auto-mounts its own, so importing it here guarantees the catalog always reaches the bundle.
|
|
12
|
+
//
|
|
13
|
+
// This must be a direct import of the stylesheet, not through a re-export barrel: a barrel `.ts` file doesn't match this package's `sideEffects` glob (only `*.css`/`*.scss` do), so tsdown/Rolldown treats an unused barrel import as side-effect-free and drops it — taking the nested stylesheet import with it. The file also can't be named `*.module.scss`: it declares zero CSS-module class names (only a `:where()`-wrapped selector), and `@tsdown/css` marks a `.module.*` file's generated JS proxy `moduleSideEffects: false` (tree-shakeable unless a class name is read); a plain stylesheet gets `moduleSideEffects: "no-treeshake"` instead, so it always ships. `tools/assert-charts-scope-emitted.ts` fails `pnpm run build` if this regresses — a passing test suite alone does not catch a dropped stylesheet.
|
|
14
|
+
import '../../styles/chart-scope.scss';
|
|
15
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
11
16
|
import {
|
|
12
17
|
getItemShapeStyles,
|
|
13
18
|
getSeriesBarStyles,
|
|
@@ -16,11 +21,15 @@ import {
|
|
|
16
21
|
resolveCssVariable,
|
|
17
22
|
normalizeColorToHex,
|
|
18
23
|
} from '../../utils';
|
|
24
|
+
// Imported from the module rather than the `chart-scope` barrel: the barrel also pulls `use-standalone-scope-class`, which imports `GlobalChartsContext` back from this file. That cycle resolves today only because the binding is read lazily inside the hook body.
|
|
25
|
+
import { ChartScopeContext } from '../chart-scope/chart-scope-context';
|
|
19
26
|
import { getChartColor, type ColorCache } from './private/get-chart-color';
|
|
27
|
+
import { themeOverrideVars } from './private/theme-override-vars';
|
|
28
|
+
import { withCatalogPointers } from './private/with-catalog-pointers';
|
|
20
29
|
import { defaultTheme } from './themes';
|
|
21
30
|
import type { GlobalChartsContextValue, ChartRegistration } from './types';
|
|
22
31
|
import type { ChartTheme, CompleteChartTheme } from '../../types';
|
|
23
|
-
import type { FC, ReactNode } from 'react';
|
|
32
|
+
import type { CSSProperties, FC, ReactNode } from 'react';
|
|
24
33
|
|
|
25
34
|
export const GlobalChartsContext = createContext< GlobalChartsContextValue | null >( null );
|
|
26
35
|
|
|
@@ -38,10 +47,26 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
38
47
|
|
|
39
48
|
// Ref to the wrapper element for resolving scoped CSS variables
|
|
40
49
|
const wrapperRef = useRef< HTMLDivElement >( null );
|
|
50
|
+
const [ scopeNode, setScopeNode ] = useState< HTMLElement | null >( null );
|
|
51
|
+
|
|
52
|
+
const setWrapperNode = useCallback( ( node: HTMLDivElement | null ) => {
|
|
53
|
+
wrapperRef.current = node;
|
|
54
|
+
setScopeNode( node );
|
|
55
|
+
}, [] );
|
|
56
|
+
|
|
57
|
+
// themeOverrideVars reads the raw `theme` prop, never `providerTheme` — feeding it the restored theme below would make an overridden role's pointer look like a self-reference and drop the var (see themeOverrideVars' own doc comment).
|
|
58
|
+
const { vars: overrideVars, roles: overriddenRoles } = useMemo(
|
|
59
|
+
() => themeOverrideVars( theme ),
|
|
60
|
+
[ theme ]
|
|
61
|
+
);
|
|
41
62
|
|
|
42
63
|
const providerTheme: CompleteChartTheme = useMemo( () => {
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
if ( ! theme ) {
|
|
65
|
+
return defaultTheme;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return withCatalogPointers( mergeThemes( defaultTheme, theme ), overriddenRoles );
|
|
69
|
+
}, [ theme, overriddenRoles ] );
|
|
45
70
|
|
|
46
71
|
// Cache expensive color computations that only change when theme colors change
|
|
47
72
|
// Using useState + useLayoutEffect instead of useMemo to ensure CSS variables
|
|
@@ -186,11 +211,6 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
186
211
|
[ colorCache, groupToColorMap ]
|
|
187
212
|
);
|
|
188
213
|
|
|
189
|
-
const resolveThemeColor = useCallback< GlobalChartsContextValue[ 'resolveThemeColor' ] >(
|
|
190
|
-
value => ( value ? normalizeColorToHex( value, wrapperRef.current, resolveCssVariable ) : '' ),
|
|
191
|
-
[]
|
|
192
|
-
);
|
|
193
|
-
|
|
194
214
|
const getElementStyles = useCallback< GlobalChartsContextValue[ 'getElementStyles' ] >(
|
|
195
215
|
( { data, index, overrideColor, legendShape } ) => {
|
|
196
216
|
const isSeriesData = data && typeof data === 'object' && 'data' in data && 'options' in data;
|
|
@@ -269,7 +289,6 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
269
289
|
getChartData,
|
|
270
290
|
theme: providerTheme,
|
|
271
291
|
getElementStyles,
|
|
272
|
-
resolveThemeColor,
|
|
273
292
|
toggleSeriesVisibility,
|
|
274
293
|
isSeriesVisible,
|
|
275
294
|
getHiddenSeries,
|
|
@@ -282,7 +301,6 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
282
301
|
getChartData,
|
|
283
302
|
providerTheme,
|
|
284
303
|
getElementStyles,
|
|
285
|
-
resolveThemeColor,
|
|
286
304
|
toggleSeriesVisibility,
|
|
287
305
|
isSeriesVisible,
|
|
288
306
|
getHiddenSeries,
|
|
@@ -292,8 +310,13 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
292
310
|
|
|
293
311
|
return (
|
|
294
312
|
<GlobalChartsContext.Provider value={ value }>
|
|
295
|
-
<div
|
|
296
|
-
{
|
|
313
|
+
<div
|
|
314
|
+
ref={ setWrapperNode }
|
|
315
|
+
className={ CHART_SCOPE_CLASS }
|
|
316
|
+
data-testid="charts-scope"
|
|
317
|
+
style={ { display: 'contents', ...overrideVars } as CSSProperties }
|
|
318
|
+
>
|
|
319
|
+
<ChartScopeContext.Provider value={ scopeNode }>{ children }</ChartScopeContext.Provider>
|
|
297
320
|
</div>
|
|
298
321
|
</GlobalChartsContext.Provider>
|
|
299
322
|
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ChartTheme } from '../../../types';
|
|
2
|
+
|
|
3
|
+
// The consumer theme fields that correspond to an emitted catalog role, and the role each one overrides. Each role must be read by exactly the elements its field controlled before the role existed: `svgLabelSmall.fill` maps to the narrow `--a8c-charts-color-label-axis` rather than `--a8c-charts-color-label`, because the broad role is also read by legend labels, heatmap cell values, funnel labels and the line-chart tooltip, which that field never moved.
|
|
4
|
+
const ROLE_FOR_FIELD: Array< [ string, ( theme: Partial< ChartTheme > ) => string | undefined ] > =
|
|
5
|
+
[
|
|
6
|
+
[ '--a8c-charts-color-background', theme => theme.backgroundColor ],
|
|
7
|
+
[ '--a8c-charts-color-grid', theme => theme.gridStyles?.stroke ],
|
|
8
|
+
[ '--a8c-charts-color-axis', theme => theme.xAxisLineStyles?.stroke ],
|
|
9
|
+
[ '--a8c-charts-color-tick', theme => theme.xTickLineStyles?.stroke ],
|
|
10
|
+
[ '--a8c-charts-color-label-axis', theme => theme.svgLabelSmall?.fill ],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The variable a `theme` prop override is published as, one layer outside the role itself.
|
|
15
|
+
*
|
|
16
|
+
* `chart-scope.scss` declares each of these roles as `var(<role>-theme, <catalog default>)`, so an override that is invalid at computed-value time — a fallback-less `var()` naming a token the host never set — only invalidates this variable, and the role still resolves its mapped `--wpds-*` token. Published as the role directly, such a value took the role and every bare `var(<role>)` read site down with it.
|
|
17
|
+
*
|
|
18
|
+
* @param role - The catalog role being overridden.
|
|
19
|
+
* @return The custom property carrying the consumer's value.
|
|
20
|
+
*/
|
|
21
|
+
export const themeLayerVar = ( role: string ): string => `${ role }-theme`;
|
|
22
|
+
|
|
23
|
+
/** Every role whose catalog entry must read a theme layer. `src/styles/test/chart-scope.test.ts` checks the stylesheet against this. */
|
|
24
|
+
export const THEME_LAYERED_ROLES: readonly string[] = ROLE_FOR_FIELD.map( ( [ role ] ) => role );
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a value reads the role it is being published for, which would make the catalog entry depend on itself.
|
|
28
|
+
*
|
|
29
|
+
* The dependency runs `<role>: var(<role>-theme, …)`, so a value naming `<role>` closes a cycle through the catalog entry. CSS marks every custom property in a cycle invalid at computed-value time — the role's own fallback is *not* used, so the token resolves to nothing and every chart loses that colour. Verified in Chrome: `--role-theme: var(--role, blue); --role: var(--role-theme, green)` leaves `--role` empty, not `green`.
|
|
30
|
+
*
|
|
31
|
+
* Three things this has to get right, none of which a plain substring test gets:
|
|
32
|
+
*
|
|
33
|
+
* `var()` permits whitespace after the opening paren, so testing for `var(<role>` misses `var( --a8c-charts-color-grid, … )`.
|
|
34
|
+
*
|
|
35
|
+
* Several roles are prefixes of others (`--a8c-charts-color-label` of `--a8c-charts-color-label-secondary`), and every role is a prefix of its own theme layer. A prefix match would read a legitimate cross-role pointer as a self-reference and silently drop the override, so the role must be followed by something that cannot continue an identifier — `-` included. A value naming `<role>-theme` is therefore not treated as a self-reference: that cycle is confined to the theme layer, which leaves the role free to fall back to the catalog default.
|
|
36
|
+
*
|
|
37
|
+
* `resolveCssVariable` also accepts a bare custom-property name with no `var()` wrapper (`--a8c-charts-color-grid`), so that is legal `theme` input too. It forms no cycle — custom properties accept arbitrary token streams — so it survives as a literal string and drops silently at the use site instead of falling back to the catalog default.
|
|
38
|
+
*
|
|
39
|
+
* @param value - The consumer's value for the field.
|
|
40
|
+
* @param role - The catalog role this value would override.
|
|
41
|
+
* @return True when the value reads `role`, so it must not be published.
|
|
42
|
+
*/
|
|
43
|
+
const readsOwnRole = ( value: string, role: string ): boolean =>
|
|
44
|
+
value.trim() === role ||
|
|
45
|
+
new RegExp( `var\\(\\s*${ role.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ) }(?![\\w-])` ).test(
|
|
46
|
+
value
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export type ThemeOverrides = {
|
|
50
|
+
/** The custom properties to write on the provider wrapper. */
|
|
51
|
+
vars: Record< string, string >;
|
|
52
|
+
/** Every mapped role the consumer set a value for, whether or not it was published. */
|
|
53
|
+
roles: readonly string[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Maps a sparse consumer theme onto the theme-layer variables its overridden catalog roles read, so CSS-painted and JS-resolved colours read one source.
|
|
58
|
+
*
|
|
59
|
+
* A value that reads the role it would override is left unpublished — that is the default theme's own pointer surviving `mergeThemes`, and publishing it would invalidate the role. The role is still reported in `roles`: `withCatalogPointers` restores its theme field to the catalog pointer either way, so CSS and visx agree on the catalog default rather than visx painting a literal CSS never sees.
|
|
60
|
+
*
|
|
61
|
+
* @param theme - The consumer's `theme` prop, before merging with the default theme.
|
|
62
|
+
* @return The wrapper's custom properties, and the roles the consumer overrode.
|
|
63
|
+
*/
|
|
64
|
+
export const themeOverrideVars = ( theme?: Partial< ChartTheme > ): ThemeOverrides => {
|
|
65
|
+
if ( ! theme ) {
|
|
66
|
+
return { vars: {}, roles: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const vars: Record< string, string > = {};
|
|
70
|
+
const roles: string[] = [];
|
|
71
|
+
|
|
72
|
+
for ( const [ role, read ] of ROLE_FOR_FIELD ) {
|
|
73
|
+
const value = read( theme );
|
|
74
|
+
|
|
75
|
+
if ( typeof value !== 'string' || value === '' ) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
roles.push( role );
|
|
80
|
+
|
|
81
|
+
if ( ! readsOwnRole( value, role ) ) {
|
|
82
|
+
vars[ themeLayerVar( role ) ] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { vars, roles };
|
|
87
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defaultTheme } from '../themes';
|
|
2
|
+
import type { CompleteChartTheme } from '../../../types';
|
|
3
|
+
|
|
4
|
+
// Restores each overridden role's mapped theme field to the catalog pointer `defaultTheme` already carries for it, so the value comes from `themes.ts` itself rather than a second, hand-copied literal.
|
|
5
|
+
const CATALOG_RESTORE_FOR_ROLE: Record<
|
|
6
|
+
string,
|
|
7
|
+
( theme: CompleteChartTheme ) => Partial< CompleteChartTheme >
|
|
8
|
+
> = {
|
|
9
|
+
'--a8c-charts-color-background': () => ( { backgroundColor: defaultTheme.backgroundColor } ),
|
|
10
|
+
'--a8c-charts-color-grid': theme => ( {
|
|
11
|
+
gridStyles: { ...theme.gridStyles, stroke: defaultTheme.gridStyles.stroke },
|
|
12
|
+
} ),
|
|
13
|
+
'--a8c-charts-color-axis': theme => ( {
|
|
14
|
+
xAxisLineStyles: { ...theme.xAxisLineStyles, stroke: defaultTheme.xAxisLineStyles.stroke },
|
|
15
|
+
} ),
|
|
16
|
+
'--a8c-charts-color-tick': theme => ( {
|
|
17
|
+
xTickLineStyles: { ...theme.xTickLineStyles, stroke: defaultTheme.xTickLineStyles.stroke },
|
|
18
|
+
} ),
|
|
19
|
+
'--a8c-charts-color-label-axis': theme => ( {
|
|
20
|
+
svgLabelSmall: { ...theme.svgLabelSmall, fill: defaultTheme.svgLabelSmall.fill },
|
|
21
|
+
} ),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Restores the mapped theme fields of `overriddenRoles` to their catalog pointer, so the theme-layer variable `themeOverrideVars` writes on the provider wrapper is the only carrier for an overridden role — CSS and the JS bridge (`useXYChartTheme`) then resolve it through the same cascade instead of visx reading a baked literal that can disagree with a closer CSS override.
|
|
26
|
+
*
|
|
27
|
+
* This covers roles `themeOverrideVars` deliberately left unpublished as well as the ones it published. A value that reads its own role is not publishable, but leaving visx the consumer's literal is what the whole mechanism exists to prevent: `theme={ { gridStyles: { stroke: 'var(--brand, var(--a8c-charts-color-grid, red))' } } }` would have CSS paint the catalog default while visx painted `--brand`.
|
|
28
|
+
*
|
|
29
|
+
* Every field outside the five mapped roles is left exactly as `merged` provided it: those aren't published as theme-layer vars, so rewriting them would erase a consumer's override with no replacement carrier.
|
|
30
|
+
*
|
|
31
|
+
* @param merged - The consumer theme merged onto `defaultTheme` (`mergeThemes` output).
|
|
32
|
+
* @param overriddenRoles - The catalog roles the consumer overrode, from `themeOverrideVars`.
|
|
33
|
+
* @return A new theme object; `merged` is not mutated.
|
|
34
|
+
*/
|
|
35
|
+
export const withCatalogPointers = (
|
|
36
|
+
merged: CompleteChartTheme,
|
|
37
|
+
overriddenRoles: readonly string[]
|
|
38
|
+
): CompleteChartTheme => {
|
|
39
|
+
let result = merged;
|
|
40
|
+
|
|
41
|
+
for ( const role of overriddenRoles ) {
|
|
42
|
+
const restore = CATALOG_RESTORE_FOR_ROLE[ role ];
|
|
43
|
+
|
|
44
|
+
if ( restore ) {
|
|
45
|
+
result = { ...result, ...restore( result ) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
@@ -2536,46 +2536,6 @@ describe( 'ChartContext', () => {
|
|
|
2536
2536
|
expect( afterRerenderColor ).toBe( '#ff0000' );
|
|
2537
2537
|
} );
|
|
2538
2538
|
} );
|
|
2539
|
-
|
|
2540
|
-
describe( 'resolveThemeColor', () => {
|
|
2541
|
-
let contextValue: GlobalChartsContextValue;
|
|
2542
|
-
|
|
2543
|
-
const TestComponent = () => {
|
|
2544
|
-
contextValue = useGlobalChartsContext();
|
|
2545
|
-
return <div>Test</div>;
|
|
2546
|
-
};
|
|
2547
|
-
|
|
2548
|
-
const mountProvider = () =>
|
|
2549
|
-
render(
|
|
2550
|
-
<GlobalChartsProvider>
|
|
2551
|
-
<TestComponent />
|
|
2552
|
-
</GlobalChartsProvider>
|
|
2553
|
-
);
|
|
2554
|
-
|
|
2555
|
-
it( 'resolves a CSS variable against the provider scope, not the document root', () => {
|
|
2556
|
-
window.getComputedStyle = jest.fn( ( element: Element ) => ( {
|
|
2557
|
-
getPropertyValue: ( prop: string ) =>
|
|
2558
|
-
prop === '--surface' && element !== document.documentElement ? '#1e1e1e' : '#ffffff',
|
|
2559
|
-
} ) ) as unknown as typeof window.getComputedStyle;
|
|
2560
|
-
|
|
2561
|
-
mountProvider();
|
|
2562
|
-
|
|
2563
|
-
expect( contextValue.resolveThemeColor( 'var(--surface, #fff)' ) ).toBe( '#1e1e1e' );
|
|
2564
|
-
} );
|
|
2565
|
-
|
|
2566
|
-
it( 'passes a resolved color through to hex', () => {
|
|
2567
|
-
mountProvider();
|
|
2568
|
-
|
|
2569
|
-
expect( contextValue.resolveThemeColor( '#abcdef' ) ).toBe( '#abcdef' );
|
|
2570
|
-
expect( contextValue.resolveThemeColor( 'rgb(255, 0, 0)' ) ).toBe( '#ff0000' );
|
|
2571
|
-
} );
|
|
2572
|
-
|
|
2573
|
-
it( 'returns an empty string for an empty value', () => {
|
|
2574
|
-
mountProvider();
|
|
2575
|
-
|
|
2576
|
-
expect( contextValue.resolveThemeColor( '' ) ).toBe( '' );
|
|
2577
|
-
} );
|
|
2578
|
-
} );
|
|
2579
2539
|
} );
|
|
2580
2540
|
|
|
2581
2541
|
describe( 'defaultTheme', () => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { CHART_SCOPE_CLASS } from '../../../styles/chart-scope-class';
|
|
3
|
+
import { GlobalChartsProvider } from '../global-charts-provider';
|
|
4
|
+
|
|
5
|
+
describe( 'GlobalChartsProvider wrapper', () => {
|
|
6
|
+
it( 'carries the a8c-charts-scope class and nothing that affects layout', () => {
|
|
7
|
+
render(
|
|
8
|
+
<GlobalChartsProvider>
|
|
9
|
+
<div>child</div>
|
|
10
|
+
</GlobalChartsProvider>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const wrapper = screen.getByTestId( 'charts-scope' );
|
|
14
|
+
|
|
15
|
+
expect( wrapper ).toHaveClass( CHART_SCOPE_CLASS );
|
|
16
|
+
// The catalog needs an element to sit on; `display: contents` is what keeps that element out of the consumer's layout.
|
|
17
|
+
expect( wrapper.style.cssText.replace( /\s/g, '' ) ).toBe( 'display:contents;' );
|
|
18
|
+
} );
|
|
19
|
+
} );
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defaultTheme } from '../themes';
|
|
2
|
+
|
|
3
|
+
const flatten = ( value: unknown ): string[] => {
|
|
4
|
+
if ( typeof value === 'string' ) {
|
|
5
|
+
return [ value ];
|
|
6
|
+
}
|
|
7
|
+
if ( Array.isArray( value ) ) {
|
|
8
|
+
return value.flatMap( flatten );
|
|
9
|
+
}
|
|
10
|
+
if ( value && typeof value === 'object' ) {
|
|
11
|
+
return Object.values( value ).flatMap( flatten );
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe( 'defaultTheme', () => {
|
|
17
|
+
const strings = flatten( defaultTheme );
|
|
18
|
+
|
|
19
|
+
it( 'holds no --wpds-* mapping — that lives only in the catalog stylesheet', () => {
|
|
20
|
+
expect( strings.filter( value => value.includes( '--wpds-' ) ) ).toEqual( [] );
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
it( 'points every themed colour at an --a8c-charts-* catalog role', () => {
|
|
24
|
+
const varStrings = strings.filter( value => value.startsWith( 'var(' ) );
|
|
25
|
+
|
|
26
|
+
expect( varStrings.length ).toBeGreaterThan( 0 );
|
|
27
|
+
expect( varStrings.every( value => value.startsWith( 'var(--a8c-charts-' ) ) ).toBe( true );
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
it( 'gives every catalog pointer a terminal literal for the SSR and jsdom paths', () => {
|
|
31
|
+
const varStrings = strings.filter( value => value.startsWith( 'var(--a8c-charts-' ) );
|
|
32
|
+
|
|
33
|
+
expect( varStrings.every( value => value.includes( ',' ) ) ).toBe( true );
|
|
34
|
+
} );
|
|
35
|
+
} );
|