@automattic/charts 2.0.1 → 3.1.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 +29 -0
- package/TOKENS.md +109 -50
- package/dist/index.cjs +529 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +118 -55
- package/dist/index.d.cts +90 -48
- package/dist/index.d.ts +90 -48
- package/dist/index.js +527 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.module.scss +8 -3
- package/src/charts/area-chart/area-chart.tsx +25 -25
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +301 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.module.scss +14 -6
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/test/bar-chart.test.tsx +255 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +23 -18
- 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.module.scss +8 -0
- package/src/charts/geo-chart/geo-chart.tsx +12 -4
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +22 -9
- package/src/charts/heatmap-chart/heatmap-chart.tsx +43 -13
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
- package/src/charts/heatmap-chart/types.ts +8 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/leaderboard-chart/types.ts +0 -2
- package/src/charts/line-chart/line-chart.module.scss +12 -7
- package/src/charts/line-chart/line-chart.tsx +32 -28
- package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +262 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +12 -14
- package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +12 -14
- package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
- package/src/charts/private/chart-instance-context/index.ts +3 -0
- package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
- package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- 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/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
- package/src/components/legend/legend.tsx +4 -4
- package/src/components/legend/private/base-legend.module.scss +1 -1
- package/src/components/legend/private/base-legend.tsx +70 -19
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +17 -4
- package/src/components/tooltip/base-tooltip.tsx +8 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +20 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +4 -7
- package/src/components/trend-indicator/trend-indicator.tsx +3 -0
- package/src/hooks/index.ts +1 -1
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +19 -13
- package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/hooks/use-xychart-theme.ts +6 -4
- package/src/index.ts +12 -3
- package/src/providers/chart-context/global-charts-provider.tsx +127 -30
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- 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 +142 -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/use-default-hidden-series-paint.test.tsx +56 -0
- package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -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 +7 -4
- 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/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -34
- 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
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -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
|
|
|
@@ -35,13 +44,33 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
35
44
|
const [ hiddenSeries, setHiddenSeries ] = useState< Map< string, Set< string > > >(
|
|
36
45
|
() => new Map()
|
|
37
46
|
);
|
|
47
|
+
// Which charts have had their defaults seeded. Kept apart from `hiddenSeries`
|
|
48
|
+
// because that map drops a chart's entry once its hidden set empties, so an
|
|
49
|
+
// absent entry cannot tell "never seeded" from "the reader revealed the lot".
|
|
50
|
+
const seededCharts = useRef< Set< string > >( new Set() );
|
|
38
51
|
|
|
39
52
|
// Ref to the wrapper element for resolving scoped CSS variables
|
|
40
53
|
const wrapperRef = useRef< HTMLDivElement >( null );
|
|
54
|
+
const [ scopeNode, setScopeNode ] = useState< HTMLElement | null >( null );
|
|
55
|
+
|
|
56
|
+
const setWrapperNode = useCallback( ( node: HTMLDivElement | null ) => {
|
|
57
|
+
wrapperRef.current = node;
|
|
58
|
+
setScopeNode( node );
|
|
59
|
+
}, [] );
|
|
60
|
+
|
|
61
|
+
// 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).
|
|
62
|
+
const { vars: overrideVars, roles: overriddenRoles } = useMemo(
|
|
63
|
+
() => themeOverrideVars( theme ),
|
|
64
|
+
[ theme ]
|
|
65
|
+
);
|
|
41
66
|
|
|
42
67
|
const providerTheme: CompleteChartTheme = useMemo( () => {
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
if ( ! theme ) {
|
|
69
|
+
return defaultTheme;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return withCatalogPointers( mergeThemes( defaultTheme, theme ), overriddenRoles );
|
|
73
|
+
}, [ theme, overriddenRoles ] );
|
|
45
74
|
|
|
46
75
|
// Cache expensive color computations that only change when theme colors change
|
|
47
76
|
// Using useState + useLayoutEffect instead of useMemo to ensure CSS variables
|
|
@@ -186,11 +215,6 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
186
215
|
[ colorCache, groupToColorMap ]
|
|
187
216
|
);
|
|
188
217
|
|
|
189
|
-
const resolveThemeColor = useCallback< GlobalChartsContextValue[ 'resolveThemeColor' ] >(
|
|
190
|
-
value => ( value ? normalizeColorToHex( value, wrapperRef.current, resolveCssVariable ) : '' ),
|
|
191
|
-
[]
|
|
192
|
-
);
|
|
193
|
-
|
|
194
218
|
const getElementStyles = useCallback< GlobalChartsContextValue[ 'getElementStyles' ] >(
|
|
195
219
|
( { data, index, overrideColor, legendShape } ) => {
|
|
196
220
|
const isSeriesData = data && typeof data === 'object' && 'data' in data && 'options' in data;
|
|
@@ -222,28 +246,90 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
222
246
|
[ providerTheme, resolveColor ]
|
|
223
247
|
);
|
|
224
248
|
|
|
225
|
-
//
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
// Keep hidden-series updates and no-op detection consistent across setters.
|
|
250
|
+
const updateHiddenSeries = useCallback(
|
|
251
|
+
( chartId: string, update: ( current: Set< string > ) => Set< string > ) => {
|
|
252
|
+
setHiddenSeries( prev => {
|
|
253
|
+
const current = prev.get( chartId );
|
|
254
|
+
const next = update( new Set( current ?? [] ) );
|
|
231
255
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
256
|
+
if ( ! current && next.size === 0 ) {
|
|
257
|
+
return prev;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Preserve identity for no-op updates.
|
|
261
|
+
if (
|
|
262
|
+
current &&
|
|
263
|
+
current.size === next.size &&
|
|
264
|
+
[ ...next ].every( label => current.has( label ) )
|
|
265
|
+
) {
|
|
266
|
+
return prev;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const newMap = new Map( prev );
|
|
270
|
+
if ( next.size === 0 ) {
|
|
271
|
+
newMap.delete( chartId );
|
|
272
|
+
} else {
|
|
273
|
+
newMap.set( chartId, next );
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return newMap;
|
|
277
|
+
} );
|
|
278
|
+
},
|
|
279
|
+
[]
|
|
280
|
+
);
|
|
237
281
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
282
|
+
const toggleSeriesVisibility = useCallback(
|
|
283
|
+
( chartId: string, seriesLabel: string ) => {
|
|
284
|
+
updateHiddenSeries( chartId, current => {
|
|
285
|
+
if ( current.has( seriesLabel ) ) {
|
|
286
|
+
current.delete( seriesLabel );
|
|
287
|
+
} else {
|
|
288
|
+
current.add( seriesLabel );
|
|
289
|
+
}
|
|
290
|
+
return current;
|
|
291
|
+
} );
|
|
292
|
+
},
|
|
293
|
+
[ updateHiddenSeries ]
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const setSeriesVisibility = useCallback(
|
|
297
|
+
( chartId: string, seriesLabel: string, visible: boolean ) => {
|
|
298
|
+
updateHiddenSeries( chartId, current => {
|
|
299
|
+
if ( visible ) {
|
|
300
|
+
current.delete( seriesLabel );
|
|
301
|
+
} else {
|
|
302
|
+
current.add( seriesLabel );
|
|
303
|
+
}
|
|
304
|
+
return current;
|
|
305
|
+
} );
|
|
306
|
+
},
|
|
307
|
+
[ updateHiddenSeries ]
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const setChartHiddenSeries = useCallback(
|
|
311
|
+
( chartId: string, seriesLabels: readonly string[] ) => {
|
|
312
|
+
updateHiddenSeries( chartId, () => new Set( seriesLabels ) );
|
|
313
|
+
},
|
|
314
|
+
[ updateHiddenSeries ]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const hasSeededChart = useCallback(
|
|
318
|
+
( chartId: string ) => seededCharts.current.has( chartId ),
|
|
319
|
+
[]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const seedChartHiddenSeries = useCallback(
|
|
323
|
+
( chartId: string, seriesLabels: readonly string[] ) => {
|
|
324
|
+
if ( seededCharts.current.has( chartId ) ) {
|
|
325
|
+
return;
|
|
242
326
|
}
|
|
243
327
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
328
|
+
seededCharts.current.add( chartId );
|
|
329
|
+
setChartHiddenSeries( chartId, seriesLabels );
|
|
330
|
+
},
|
|
331
|
+
[ setChartHiddenSeries ]
|
|
332
|
+
);
|
|
247
333
|
|
|
248
334
|
const isSeriesVisible = useCallback(
|
|
249
335
|
( chartId: string, seriesLabel: string ) => {
|
|
@@ -269,8 +355,11 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
269
355
|
getChartData,
|
|
270
356
|
theme: providerTheme,
|
|
271
357
|
getElementStyles,
|
|
272
|
-
resolveThemeColor,
|
|
273
358
|
toggleSeriesVisibility,
|
|
359
|
+
setSeriesVisibility,
|
|
360
|
+
setChartHiddenSeries,
|
|
361
|
+
seedChartHiddenSeries,
|
|
362
|
+
hasSeededChart,
|
|
274
363
|
isSeriesVisible,
|
|
275
364
|
getHiddenSeries,
|
|
276
365
|
isColorPaletteResolved,
|
|
@@ -282,8 +371,11 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
282
371
|
getChartData,
|
|
283
372
|
providerTheme,
|
|
284
373
|
getElementStyles,
|
|
285
|
-
resolveThemeColor,
|
|
286
374
|
toggleSeriesVisibility,
|
|
375
|
+
setSeriesVisibility,
|
|
376
|
+
setChartHiddenSeries,
|
|
377
|
+
seedChartHiddenSeries,
|
|
378
|
+
hasSeededChart,
|
|
287
379
|
isSeriesVisible,
|
|
288
380
|
getHiddenSeries,
|
|
289
381
|
isColorPaletteResolved,
|
|
@@ -292,8 +384,13 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
292
384
|
|
|
293
385
|
return (
|
|
294
386
|
<GlobalChartsContext.Provider value={ value }>
|
|
295
|
-
<div
|
|
296
|
-
{
|
|
387
|
+
<div
|
|
388
|
+
ref={ setWrapperNode }
|
|
389
|
+
className={ CHART_SCOPE_CLASS }
|
|
390
|
+
data-testid="charts-scope"
|
|
391
|
+
style={ { display: 'contents', ...overrideVars } as CSSProperties }
|
|
392
|
+
>
|
|
393
|
+
<ChartScopeContext.Provider value={ scopeNode }>{ children }</ChartScopeContext.Provider>
|
|
297
394
|
</div>
|
|
298
395
|
</GlobalChartsContext.Provider>
|
|
299
396
|
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { useGlobalChartsContext } from './use-global-charts-context';
|
|
3
|
+
|
|
4
|
+
// Hide seeded series before paint without triggering React's SSR warning.
|
|
5
|
+
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Seeds a chart's hidden series once per chart ID, the first time the provider
|
|
9
|
+
* sees that ID.
|
|
10
|
+
*
|
|
11
|
+
* The provider owns the record of what has been seeded, not this hook: a
|
|
12
|
+
* mount-scoped one would re-apply the defaults on every remount, and a chart
|
|
13
|
+
* remounts for reasons the reader never thinks of as a new chart — switching
|
|
14
|
+
* between line and bar, tabbing away and back, a resize flipping tabs into a
|
|
15
|
+
* dropdown. Each of those would throw away the series they had just revealed.
|
|
16
|
+
*
|
|
17
|
+
* @param chartId - The chart ID passed to the provider's visibility methods.
|
|
18
|
+
* @param seriesLabels - Labels to hide when this chart ID is first seen. Later values are ignored.
|
|
19
|
+
* @return The resolved hidden-series set.
|
|
20
|
+
*/
|
|
21
|
+
export const useDefaultHiddenSeries = (
|
|
22
|
+
chartId: string,
|
|
23
|
+
seriesLabels?: readonly string[]
|
|
24
|
+
): Set< string > => {
|
|
25
|
+
const { getHiddenSeries, seedChartHiddenSeries, hasSeededChart } = useGlobalChartsContext();
|
|
26
|
+
// Read through a ref so the effect never depends on a new array identity.
|
|
27
|
+
const labelsRef = useRef( seriesLabels );
|
|
28
|
+
labelsRef.current = seriesLabels;
|
|
29
|
+
const hasSeriesLabels = seriesLabels !== undefined;
|
|
30
|
+
const shouldUseDefaults = hasSeriesLabels && ! hasSeededChart( chartId );
|
|
31
|
+
|
|
32
|
+
useIsomorphicLayoutEffect( () => {
|
|
33
|
+
if ( labelsRef.current === undefined ) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
seedChartHiddenSeries( chartId, labelsRef.current );
|
|
37
|
+
}, [ chartId, hasSeriesLabels, seedChartHiddenSeries ] );
|
|
38
|
+
|
|
39
|
+
// getHiddenSeries hands out a fresh copy per call, so hold the set still between
|
|
40
|
+
// visibility changes. Charts derive their rendered series from it, and a new
|
|
41
|
+
// identity every render invalidates every memo downstream — including the one
|
|
42
|
+
// the accessible tooltip watches, which closes a tooltip mid-navigation.
|
|
43
|
+
return useMemo(
|
|
44
|
+
() => ( shouldUseDefaults ? new Set( labelsRef.current ) : getHiddenSeries( chartId ) ),
|
|
45
|
+
[ shouldUseDefaults, getHiddenSeries, chartId ]
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -1728,6 +1728,148 @@ describe( 'ChartContext', () => {
|
|
|
1728
1728
|
// Should have empty set after all series are visible
|
|
1729
1729
|
expect( hidden.size ).toBe( 0 );
|
|
1730
1730
|
} );
|
|
1731
|
+
|
|
1732
|
+
it( 'setSeriesVisibility sets state absolutely rather than toggling', () => {
|
|
1733
|
+
let contextValue: GlobalChartsContextValue;
|
|
1734
|
+
const TestComponent = () => {
|
|
1735
|
+
contextValue = useGlobalChartsContext();
|
|
1736
|
+
return <div>Test</div>;
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
render(
|
|
1740
|
+
<GlobalChartsProvider>
|
|
1741
|
+
<TestComponent />
|
|
1742
|
+
</GlobalChartsProvider>
|
|
1743
|
+
);
|
|
1744
|
+
|
|
1745
|
+
act( () => {
|
|
1746
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1747
|
+
} );
|
|
1748
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( false );
|
|
1749
|
+
|
|
1750
|
+
// Calling it again with the same value must not flip it back.
|
|
1751
|
+
act( () => {
|
|
1752
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1753
|
+
} );
|
|
1754
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( false );
|
|
1755
|
+
|
|
1756
|
+
act( () => {
|
|
1757
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', true );
|
|
1758
|
+
} );
|
|
1759
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( true );
|
|
1760
|
+
} );
|
|
1761
|
+
|
|
1762
|
+
it( 'setChartHiddenSeries replaces the hidden set rather than merging into it', () => {
|
|
1763
|
+
let contextValue: GlobalChartsContextValue;
|
|
1764
|
+
const TestComponent = () => {
|
|
1765
|
+
contextValue = useGlobalChartsContext();
|
|
1766
|
+
return <div>Test</div>;
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
render(
|
|
1770
|
+
<GlobalChartsProvider>
|
|
1771
|
+
<TestComponent />
|
|
1772
|
+
</GlobalChartsProvider>
|
|
1773
|
+
);
|
|
1774
|
+
|
|
1775
|
+
act( () => {
|
|
1776
|
+
contextValue.setChartHiddenSeries( 'test-chart', [ 'Series 1', 'Series 2' ] );
|
|
1777
|
+
} );
|
|
1778
|
+
expect( contextValue.getHiddenSeries( 'test-chart' ) ).toEqual(
|
|
1779
|
+
new Set( [ 'Series 1', 'Series 2' ] )
|
|
1780
|
+
);
|
|
1781
|
+
|
|
1782
|
+
act( () => {
|
|
1783
|
+
contextValue.setChartHiddenSeries( 'test-chart', [ 'Series 2' ] );
|
|
1784
|
+
} );
|
|
1785
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( true );
|
|
1786
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 2' ) ).toBe( false );
|
|
1787
|
+
|
|
1788
|
+
act( () => {
|
|
1789
|
+
contextValue.setChartHiddenSeries( 'test-chart', [] );
|
|
1790
|
+
} );
|
|
1791
|
+
expect( contextValue.getHiddenSeries( 'test-chart' ) ).toEqual( new Set() );
|
|
1792
|
+
} );
|
|
1793
|
+
|
|
1794
|
+
it( 'setChartHiddenSeries leaves other charts alone', () => {
|
|
1795
|
+
let contextValue: GlobalChartsContextValue;
|
|
1796
|
+
const TestComponent = () => {
|
|
1797
|
+
contextValue = useGlobalChartsContext();
|
|
1798
|
+
return <div>Test</div>;
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
render(
|
|
1802
|
+
<GlobalChartsProvider>
|
|
1803
|
+
<TestComponent />
|
|
1804
|
+
</GlobalChartsProvider>
|
|
1805
|
+
);
|
|
1806
|
+
|
|
1807
|
+
act( () => {
|
|
1808
|
+
contextValue.setChartHiddenSeries( 'chart-a', [ 'Series 1' ] );
|
|
1809
|
+
contextValue.setChartHiddenSeries( 'chart-b', [ 'Series 2' ] );
|
|
1810
|
+
} );
|
|
1811
|
+
|
|
1812
|
+
expect( contextValue.isSeriesVisible( 'chart-a', 'Series 1' ) ).toBe( false );
|
|
1813
|
+
expect( contextValue.isSeriesVisible( 'chart-a', 'Series 2' ) ).toBe( true );
|
|
1814
|
+
expect( contextValue.isSeriesVisible( 'chart-b', 'Series 2' ) ).toBe( false );
|
|
1815
|
+
} );
|
|
1816
|
+
|
|
1817
|
+
it( 'a redundant setSeriesVisibility write does not change context identity or re-render', () => {
|
|
1818
|
+
let contextValue: GlobalChartsContextValue;
|
|
1819
|
+
let childUpdateTally = 0;
|
|
1820
|
+
const TestComponent = () => {
|
|
1821
|
+
contextValue = useGlobalChartsContext();
|
|
1822
|
+
childUpdateTally++;
|
|
1823
|
+
return <div>Test</div>;
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
render(
|
|
1827
|
+
<GlobalChartsProvider>
|
|
1828
|
+
<TestComponent />
|
|
1829
|
+
</GlobalChartsProvider>
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
act( () => {
|
|
1833
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1834
|
+
} );
|
|
1835
|
+
const isSeriesVisibleAfterFirstWrite = contextValue.isSeriesVisible;
|
|
1836
|
+
const tallyAfterFirstWrite = childUpdateTally;
|
|
1837
|
+
|
|
1838
|
+
// Redundant write: the series is already hidden, so this must be a no-op.
|
|
1839
|
+
act( () => {
|
|
1840
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1841
|
+
} );
|
|
1842
|
+
|
|
1843
|
+
expect( contextValue.isSeriesVisible ).toBe( isSeriesVisibleAfterFirstWrite );
|
|
1844
|
+
expect( childUpdateTally ).toBe( tallyAfterFirstWrite );
|
|
1845
|
+
} );
|
|
1846
|
+
|
|
1847
|
+
it( 'setting an already-empty hidden set does not create then delete a Map entry', () => {
|
|
1848
|
+
let contextValue: GlobalChartsContextValue;
|
|
1849
|
+
let childUpdateTally = 0;
|
|
1850
|
+
const TestComponent = () => {
|
|
1851
|
+
contextValue = useGlobalChartsContext();
|
|
1852
|
+
childUpdateTally++;
|
|
1853
|
+
return <div>Test</div>;
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
render(
|
|
1857
|
+
<GlobalChartsProvider>
|
|
1858
|
+
<TestComponent />
|
|
1859
|
+
</GlobalChartsProvider>
|
|
1860
|
+
);
|
|
1861
|
+
|
|
1862
|
+
const tallyBeforeWrite = childUpdateTally;
|
|
1863
|
+
const isSeriesVisibleBefore = contextValue.isSeriesVisible;
|
|
1864
|
+
|
|
1865
|
+
// This chart has no hidden-series entry at all yet.
|
|
1866
|
+
act( () => {
|
|
1867
|
+
contextValue.setChartHiddenSeries( 'test-chart', [] );
|
|
1868
|
+
} );
|
|
1869
|
+
|
|
1870
|
+
expect( childUpdateTally ).toBe( tallyBeforeWrite );
|
|
1871
|
+
expect( contextValue.isSeriesVisible ).toBe( isSeriesVisibleBefore );
|
|
1872
|
+
} );
|
|
1731
1873
|
} );
|
|
1732
1874
|
|
|
1733
1875
|
describe( 'GlobalChartsProvider - CSS Variable Support', () => {
|
|
@@ -2536,46 +2678,6 @@ describe( 'ChartContext', () => {
|
|
|
2536
2678
|
expect( afterRerenderColor ).toBe( '#ff0000' );
|
|
2537
2679
|
} );
|
|
2538
2680
|
} );
|
|
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
2681
|
} );
|
|
2580
2682
|
|
|
2581
2683
|
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
|
+
} );
|