@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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { GlobalChartsProvider } from '../global-charts-provider';
|
|
3
|
+
|
|
4
|
+
// The stock defaultTheme's mapped fields are themselves self-referential pointers (e.g. `gridStyles.stroke: 'var(--a8c-charts-color-grid, #dbdbdb)'`), so themeOverrideVars' own self-reference guard already filters them out no matter which theme object the provider passes in. That makes "reads theme, not providerTheme" unfalsifiable against the real default theme. Mocking the default here to a plain, non-pointer value removes that overlap, so this test only passes when the provider reads the raw consumer `theme` prop rather than the merged `providerTheme`.
|
|
5
|
+
jest.mock( '../themes', () => {
|
|
6
|
+
const actual = jest.requireActual( '../themes' );
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
defaultTheme: {
|
|
10
|
+
...actual.defaultTheme,
|
|
11
|
+
gridStyles: { ...actual.defaultTheme.gridStyles, stroke: '#dbdbdb' },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
describe( 'GlobalChartsProvider wrapper style (discriminating guard test)', () => {
|
|
17
|
+
it( 'does not emit an instance override for a role the consumer left unset, even though the merged theme carries a concrete default value', () => {
|
|
18
|
+
render(
|
|
19
|
+
<GlobalChartsProvider theme={ { tickLength: 8 } }>
|
|
20
|
+
<div>child</div>
|
|
21
|
+
</GlobalChartsProvider>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const wrapper = screen.getByTestId( 'charts-scope' );
|
|
25
|
+
|
|
26
|
+
expect( wrapper.style.getPropertyValue( '--a8c-charts-color-grid-theme' ) ).toBe( '' );
|
|
27
|
+
expect( wrapper.style.getPropertyValue( '--a8c-charts-color-grid' ) ).toBe( '' );
|
|
28
|
+
} );
|
|
29
|
+
} );
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
THEME_LAYERED_ROLES,
|
|
3
|
+
themeLayerVar,
|
|
4
|
+
themeOverrideVars,
|
|
5
|
+
} from '../private/theme-override-vars';
|
|
6
|
+
|
|
7
|
+
describe( 'themeOverrideVars', () => {
|
|
8
|
+
it( 'returns nothing for an absent theme', () => {
|
|
9
|
+
expect( themeOverrideVars( undefined ) ).toEqual( { vars: {}, roles: [] } );
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
it( 'returns nothing for a theme that overrides no mapped role', () => {
|
|
13
|
+
expect( themeOverrideVars( { tickLength: 8 } ) ).toEqual( { vars: {}, roles: [] } );
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
// The value is published one layer outside the role — `chart-scope.scss` declares the role as `var(<role>-theme, <catalog default>)`. Published as the role itself, a value that is invalid at computed-value time (`var(--wpds-token)` with the token unset) would make the role invalid too, and every bare `var(--a8c-charts-color-grid)` read site would paint `unset` instead of falling back to the catalog default.
|
|
17
|
+
it( 'maps a grid stroke override onto the grid role’s theme layer', () => {
|
|
18
|
+
expect( themeOverrideVars( { gridStyles: { stroke: 'red' } } ) ).toEqual( {
|
|
19
|
+
vars: { '--a8c-charts-color-grid-theme': 'red' },
|
|
20
|
+
roles: [ '--a8c-charts-color-grid' ],
|
|
21
|
+
} );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'maps every supported role at once', () => {
|
|
25
|
+
expect(
|
|
26
|
+
themeOverrideVars( {
|
|
27
|
+
backgroundColor: '#111',
|
|
28
|
+
gridStyles: { stroke: '#222' },
|
|
29
|
+
xAxisLineStyles: { stroke: '#333' },
|
|
30
|
+
xTickLineStyles: { stroke: '#444' },
|
|
31
|
+
svgLabelSmall: { fill: '#555' },
|
|
32
|
+
} )
|
|
33
|
+
).toEqual( {
|
|
34
|
+
vars: {
|
|
35
|
+
'--a8c-charts-color-background-theme': '#111',
|
|
36
|
+
'--a8c-charts-color-grid-theme': '#222',
|
|
37
|
+
'--a8c-charts-color-axis-theme': '#333',
|
|
38
|
+
'--a8c-charts-color-tick-theme': '#444',
|
|
39
|
+
'--a8c-charts-color-label-axis-theme': '#555',
|
|
40
|
+
},
|
|
41
|
+
roles: [
|
|
42
|
+
'--a8c-charts-color-background',
|
|
43
|
+
'--a8c-charts-color-grid',
|
|
44
|
+
'--a8c-charts-color-axis',
|
|
45
|
+
'--a8c-charts-color-tick',
|
|
46
|
+
'--a8c-charts-color-label-axis',
|
|
47
|
+
],
|
|
48
|
+
} );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'publishes only theme-layer variables, never a catalog role directly', () => {
|
|
52
|
+
const { vars } = themeOverrideVars( {
|
|
53
|
+
backgroundColor: '#111',
|
|
54
|
+
gridStyles: { stroke: '#222' },
|
|
55
|
+
xAxisLineStyles: { stroke: '#333' },
|
|
56
|
+
xTickLineStyles: { stroke: '#444' },
|
|
57
|
+
svgLabelSmall: { fill: '#555' },
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
expect( Object.keys( vars ) ).toEqual( THEME_LAYERED_ROLES.map( themeLayerVar ) );
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
// The narrow role is the whole point: the broad `--a8c-charts-color-label` is also read by legend labels, heatmap cell values, funnel labels and the line-chart tooltip, none of which `svgLabelSmall.fill` moved before the role was published as a custom property.
|
|
64
|
+
it( 'publishes an svg label override as the narrow axis-label role, not the broad one', () => {
|
|
65
|
+
expect( themeOverrideVars( { svgLabelSmall: { fill: 'red' } } ) ).toEqual( {
|
|
66
|
+
vars: { '--a8c-charts-color-label-axis-theme': 'red' },
|
|
67
|
+
roles: [ '--a8c-charts-color-label-axis' ],
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
it( 'ignores a value that is itself a pointer at the role it would override', () => {
|
|
72
|
+
expect(
|
|
73
|
+
themeOverrideVars( { gridStyles: { stroke: 'var(--a8c-charts-color-grid, #dbdbdb)' } } ).vars
|
|
74
|
+
).toEqual( {} );
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
// A skipped value still reports its role, so `withCatalogPointers` restores the theme field. Without that, CSS would paint the catalog default while visx painted the consumer's literal — the divergence this whole mechanism exists to remove.
|
|
78
|
+
it( 'still reports the role of a value it refuses to publish', () => {
|
|
79
|
+
expect(
|
|
80
|
+
themeOverrideVars( {
|
|
81
|
+
gridStyles: { stroke: 'var(--brand, var(--a8c-charts-color-grid, red))' },
|
|
82
|
+
} )
|
|
83
|
+
).toEqual( { vars: {}, roles: [ '--a8c-charts-color-grid' ] } );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
// `var()` allows whitespace after the opening paren. The role reads its theme layer, so a value naming the role closes a cycle through the catalog entry: CSS marks every custom property in a cycle invalid at computed-value time, and the role's own fallback is not used, so the token resolves to nothing at all.
|
|
87
|
+
it( 'ignores a self-referential pointer padded with whitespace', () => {
|
|
88
|
+
expect(
|
|
89
|
+
themeOverrideVars( { gridStyles: { stroke: 'var( --a8c-charts-color-grid, #dbdbdb )' } } )
|
|
90
|
+
.vars
|
|
91
|
+
).toEqual( {} );
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'still emits a value that merely mentions a different role', () => {
|
|
95
|
+
expect(
|
|
96
|
+
themeOverrideVars( { gridStyles: { stroke: 'var( --a8c-charts-color-axis, #dbdbdb )' } } )
|
|
97
|
+
.vars
|
|
98
|
+
).toEqual( {
|
|
99
|
+
'--a8c-charts-color-grid-theme': 'var( --a8c-charts-color-axis, #dbdbdb )',
|
|
100
|
+
} );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
// A mapped role can be a strict prefix of another custom property — a catalog role such as `--a8c-charts-color-label` of `--a8c-charts-color-label-axis`, the role's own theme layer, or a name the consumer defined themselves. A prefix match would read a legitimate cross-property pointer as a self-reference and silently drop the override, so the role must be followed by something that cannot continue an identifier.
|
|
104
|
+
it( 'does not mistake a longer property name for the role it guards', () => {
|
|
105
|
+
expect(
|
|
106
|
+
themeOverrideVars( {
|
|
107
|
+
xAxisLineStyles: { stroke: 'var(--a8c-charts-color-axis-emphasis, #000)' },
|
|
108
|
+
} ).vars
|
|
109
|
+
).toEqual( { '--a8c-charts-color-axis-theme': 'var(--a8c-charts-color-axis-emphasis, #000)' } );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
// `resolveCssVariable` also accepts a bare custom-property name with no `var()` wrapper, so `theme={ { gridStyles: { stroke: '--a8c-charts-color-grid' } } }` is legal input. It forms no cycle, so it must be caught explicitly or it survives as a literal string value and drops silently at the use site.
|
|
113
|
+
it( 'ignores a self-referential pointer written as a bare custom-property name', () => {
|
|
114
|
+
expect(
|
|
115
|
+
themeOverrideVars( { gridStyles: { stroke: '--a8c-charts-color-grid' } } ).vars
|
|
116
|
+
).toEqual( {} );
|
|
117
|
+
} );
|
|
118
|
+
} );
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mergeThemes } from '../../../utils';
|
|
2
|
+
import { withCatalogPointers } from '../private/with-catalog-pointers';
|
|
3
|
+
import { defaultTheme } from '../themes';
|
|
4
|
+
import type { ChartTheme } from '../../../types';
|
|
5
|
+
|
|
6
|
+
describe( 'withCatalogPointers', () => {
|
|
7
|
+
it( 'restores every overridden role to its catalog pointer, without mutating the input theme', () => {
|
|
8
|
+
const merged = mergeThemes( defaultTheme, {
|
|
9
|
+
backgroundColor: '#111',
|
|
10
|
+
gridStyles: { stroke: '#222' },
|
|
11
|
+
xAxisLineStyles: { stroke: '#333' },
|
|
12
|
+
xTickLineStyles: { stroke: '#444' },
|
|
13
|
+
svgLabelSmall: { fill: '#555' },
|
|
14
|
+
} as Partial< ChartTheme > );
|
|
15
|
+
|
|
16
|
+
const result = withCatalogPointers( merged, [
|
|
17
|
+
'--a8c-charts-color-background',
|
|
18
|
+
'--a8c-charts-color-grid',
|
|
19
|
+
'--a8c-charts-color-axis',
|
|
20
|
+
'--a8c-charts-color-tick',
|
|
21
|
+
'--a8c-charts-color-label-axis',
|
|
22
|
+
] );
|
|
23
|
+
|
|
24
|
+
expect( result.backgroundColor ).toBe( defaultTheme.backgroundColor );
|
|
25
|
+
expect( result.gridStyles.stroke ).toBe( defaultTheme.gridStyles.stroke );
|
|
26
|
+
expect( result.xAxisLineStyles.stroke ).toBe( defaultTheme.xAxisLineStyles.stroke );
|
|
27
|
+
expect( result.xTickLineStyles.stroke ).toBe( defaultTheme.xTickLineStyles.stroke );
|
|
28
|
+
expect( result.svgLabelSmall.fill ).toBe( defaultTheme.svgLabelSmall.fill );
|
|
29
|
+
|
|
30
|
+
expect( merged.backgroundColor ).toBe( '#111' );
|
|
31
|
+
expect( merged.gridStyles.stroke ).toBe( '#222' );
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'leaves fields alone when no role was overridden', () => {
|
|
35
|
+
const merged = mergeThemes( defaultTheme, {
|
|
36
|
+
tickLength: 8,
|
|
37
|
+
leaderboardChart: { deltaColors: [ 'red', 'grey', 'green' ] },
|
|
38
|
+
} as Partial< ChartTheme > );
|
|
39
|
+
|
|
40
|
+
const result = withCatalogPointers( merged, [] );
|
|
41
|
+
|
|
42
|
+
expect( result.tickLength ).toBe( 8 );
|
|
43
|
+
expect( result.gridStyles.stroke ).toBe( defaultTheme.gridStyles.stroke );
|
|
44
|
+
// Not a mapped role, so its literal survives even when other roles are being restored.
|
|
45
|
+
expect( result.leaderboardChart.deltaColors ).toEqual( [ 'red', 'grey', 'green' ] );
|
|
46
|
+
} );
|
|
47
|
+
} );
|
|
@@ -4,31 +4,29 @@ import type { CompleteChartTheme } from '../../types';
|
|
|
4
4
|
* Default theme configuration
|
|
5
5
|
*/
|
|
6
6
|
const defaultTheme: CompleteChartTheme = {
|
|
7
|
-
backgroundColor:
|
|
8
|
-
'var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))',
|
|
7
|
+
backgroundColor: 'var(--a8c-charts-color-background, #fff)',
|
|
9
8
|
labelBackgroundColor: 'transparent', // label background color (transparent by default)
|
|
10
|
-
// White label text sits on top of arbitrary series colors, so it has no WPDS
|
|
11
|
-
// content-foreground equivalent; the role carries the hardcoded value as its fallback.
|
|
9
|
+
// White label text sits on top of arbitrary series colors, so it has no WPDS content-foreground equivalent. Every other colour here is a bare pointer at the catalog emitted by `chart-scope.scss`; the terminal literal is the last resort for the SSR and jsdom paths, where getComputedStyle resolves nothing.
|
|
12
10
|
labelTextColor: 'var(--a8c-charts-color-label-on-fill, #FFFFFF)',
|
|
13
11
|
colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ],
|
|
14
12
|
gridStyles: {
|
|
15
|
-
stroke: 'var(--a8c-charts-color-grid,
|
|
13
|
+
stroke: 'var(--a8c-charts-color-grid, #dbdbdb)',
|
|
16
14
|
strokeWidth: 1,
|
|
17
15
|
},
|
|
18
16
|
tickLength: 4,
|
|
19
17
|
gridColor: '',
|
|
20
18
|
gridColorDark: '',
|
|
21
19
|
xTickLineStyles: {
|
|
22
|
-
stroke: 'var(--a8c-charts-color-tick,
|
|
20
|
+
stroke: 'var(--a8c-charts-color-tick, #dbdbdb)',
|
|
23
21
|
strokeWidth: 1,
|
|
24
22
|
},
|
|
25
23
|
xAxisLineStyles: {
|
|
26
|
-
stroke: 'var(--a8c-charts-color-axis,
|
|
24
|
+
stroke: 'var(--a8c-charts-color-axis, #dbdbdb)',
|
|
27
25
|
strokeWidth: 1,
|
|
28
26
|
},
|
|
29
27
|
legend: {
|
|
30
28
|
labelStyles: {
|
|
31
|
-
color: 'var(--a8c-charts-color-label,
|
|
29
|
+
color: 'var(--a8c-charts-color-label, #1e1e1e)',
|
|
32
30
|
},
|
|
33
31
|
containerStyles: {},
|
|
34
32
|
shapeStyles: [],
|
|
@@ -41,30 +39,26 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
41
39
|
// elements for axis labels and ticks. Setting `inherit` lets SVG text
|
|
42
40
|
// pick up the host application's font-family via normal CSS inheritance.
|
|
43
41
|
svgLabelSmall: {
|
|
44
|
-
fill: 'var(--a8c-charts-color-label
|
|
42
|
+
fill: 'var(--a8c-charts-color-label-axis, #1e1e1e)',
|
|
45
43
|
fontFamily: 'inherit',
|
|
46
44
|
},
|
|
47
45
|
svgLabelBig: { fontFamily: 'inherit' },
|
|
48
46
|
annotationStyles: {
|
|
49
47
|
label: {
|
|
50
|
-
anchorLineStroke:
|
|
51
|
-
|
|
52
|
-
backgroundFill:
|
|
53
|
-
'var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))',
|
|
48
|
+
anchorLineStroke: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
49
|
+
backgroundFill: 'var(--a8c-charts-color-surface, #fff)',
|
|
54
50
|
},
|
|
55
51
|
connector: {
|
|
56
|
-
stroke:
|
|
57
|
-
'var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))',
|
|
52
|
+
stroke: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
58
53
|
},
|
|
59
54
|
circleSubject: {
|
|
60
55
|
stroke: 'transparent',
|
|
61
|
-
fill: 'var(--a8c-charts-color-annotation,
|
|
56
|
+
fill: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
62
57
|
radius: 5,
|
|
63
58
|
},
|
|
64
59
|
},
|
|
65
60
|
geoChart: {
|
|
66
|
-
featureFillColor:
|
|
67
|
-
'var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))',
|
|
61
|
+
featureFillColor: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
|
|
68
62
|
},
|
|
69
63
|
leaderboardChart: {
|
|
70
64
|
rowGap: 12,
|
|
@@ -72,18 +66,15 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
72
66
|
labelSpacing: 'xs',
|
|
73
67
|
// [negative, neutral, positive]
|
|
74
68
|
deltaColors: [
|
|
75
|
-
'var(--a8c-charts-color-trend-down,
|
|
76
|
-
'var(--a8c-charts-color-trend-neutral,
|
|
77
|
-
'var(--a8c-charts-color-trend-up,
|
|
69
|
+
'var(--a8c-charts-color-trend-down, #cc1818)',
|
|
70
|
+
'var(--a8c-charts-color-trend-neutral, #707070)',
|
|
71
|
+
'var(--a8c-charts-color-trend-up, #008030)',
|
|
78
72
|
],
|
|
79
73
|
},
|
|
80
74
|
conversionFunnelChart: {
|
|
81
|
-
backgroundColor:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))',
|
|
85
|
-
negativeChangeColor:
|
|
86
|
-
'var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))',
|
|
75
|
+
backgroundColor: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
|
|
76
|
+
positiveChangeColor: 'var(--a8c-charts-color-trend-up, #008030)',
|
|
77
|
+
negativeChangeColor: 'var(--a8c-charts-color-trend-down, #cc1818)',
|
|
87
78
|
},
|
|
88
79
|
lineChart: {
|
|
89
80
|
lineStyles: {
|
|
@@ -38,9 +38,6 @@ export interface GlobalChartsContextValue {
|
|
|
38
38
|
getChartData: ( id: string ) => ChartRegistration | undefined;
|
|
39
39
|
theme: CompleteChartTheme;
|
|
40
40
|
getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
|
|
41
|
-
// Resolve a theme color or CSS variable to a hex string in the provider's scope.
|
|
42
|
-
// Resolves at call time so it tracks ambient theme changes (e.g. a light/dark switch).
|
|
43
|
-
resolveThemeColor: ( value: string ) => string;
|
|
44
41
|
// Series visibility management for interactive legends
|
|
45
42
|
toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
|
|
46
43
|
isSeriesVisible: ( chartId: string, seriesLabel: string ) => boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The nearest chart element, published so JS token resolution reads the same CSS cascade that paints the chart's own colours.
|
|
5
|
+
*
|
|
6
|
+
* All JS token resolution reads this element rather than `document.documentElement`, so a `getComputedStyle` call sees the same `--a8c-charts-*` values — including any override set inside the provider tree — that a CSS-painted sibling element does.
|
|
7
|
+
*/
|
|
8
|
+
export const ChartScopeContext = createContext< HTMLElement | null >( null );
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { ChartScopeContext, useChartScopeElement } from '../index';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
describe( 'useChartScopeElement', () => {
|
|
6
|
+
// The default has to stay null rather than `document.documentElement`: a chart with no scope above it must resolve nothing and fall back to its theme literal, not silently read the document root again.
|
|
7
|
+
it( 'returns null with no scope above it', () => {
|
|
8
|
+
const { result } = renderHook( () => useChartScopeElement() );
|
|
9
|
+
|
|
10
|
+
expect( result.current ).toBeNull();
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
it( 'returns the enclosing scope element', () => {
|
|
14
|
+
const scope = document.createElement( 'div' );
|
|
15
|
+
|
|
16
|
+
const { result } = renderHook( () => useChartScopeElement(), {
|
|
17
|
+
wrapper: ( { children }: { children: ReactNode } ) => (
|
|
18
|
+
<ChartScopeContext.Provider value={ scope }>{ children }</ChartScopeContext.Provider>
|
|
19
|
+
),
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
expect( result.current ).toBe( scope );
|
|
23
|
+
} );
|
|
24
|
+
} );
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { CHART_SCOPE_CLASS } from '../../../styles/chart-scope-class';
|
|
3
|
+
import { GlobalChartsProvider } from '../../chart-context/global-charts-provider';
|
|
4
|
+
import { useStandaloneScopeClass } from '../use-standalone-scope-class';
|
|
5
|
+
|
|
6
|
+
describe( 'useStandaloneScopeClass', () => {
|
|
7
|
+
it( 'returns the scope class with no provider above it', () => {
|
|
8
|
+
const { result } = renderHook( () => useStandaloneScopeClass() );
|
|
9
|
+
|
|
10
|
+
expect( result.current ).toBe( CHART_SCOPE_CLASS );
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
it( 'returns undefined inside a GlobalChartsProvider', () => {
|
|
14
|
+
const { result } = renderHook( () => useStandaloneScopeClass(), {
|
|
15
|
+
wrapper: ( { children } ) => <GlobalChartsProvider>{ children }</GlobalChartsProvider>,
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
expect( result.current ).toBeUndefined();
|
|
19
|
+
} );
|
|
20
|
+
} );
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ChartScopeContext } from './chart-scope-context';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the nearest chart scope element, or `null` when rendered outside any scope (and during SSR, before the ref is attached).
|
|
6
|
+
*
|
|
7
|
+
* @return {HTMLElement | null} The nearest chart scope element, or `null`.
|
|
8
|
+
*/
|
|
9
|
+
export const useChartScopeElement = (): HTMLElement | null => useContext( ChartScopeContext );
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
3
|
+
import { GlobalChartsContext } from '../chart-context/global-charts-provider';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the catalog class when there is no `GlobalChartsProvider` above this component, and `undefined` when there is.
|
|
7
|
+
*
|
|
8
|
+
* Standalone components need the catalog to resolve at all, but inside a provider the tokens already inherit — re-declaring them there would shadow a consumer override set anywhere in the tree.
|
|
9
|
+
*
|
|
10
|
+
* @return The scope class name, or undefined when a provider is present.
|
|
11
|
+
*/
|
|
12
|
+
export const useStandaloneScopeClass = (): string | undefined =>
|
|
13
|
+
useContext( GlobalChartsContext ) ? undefined : CHART_SCOPE_CLASS;
|
package/src/providers/index.ts
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable class that carries the `--a8c-charts-*` catalog. Applied to the `GlobalChartsProvider` wrapper, to portal-rendered surfaces (which sit outside the provider tree), and to standalone components when no provider is above them.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately unhashed: consumers are documented as being able to target it to override a token on the provider element.
|
|
5
|
+
*/
|
|
6
|
+
export const CHART_SCOPE_CLASS = 'a8c-charts-scope';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The single mapping point between the --a8c-charts-* catalog and WordPress
|
|
2
|
+
// design-system tokens.
|
|
3
|
+
|
|
4
|
+
// Declared on the GlobalChartsProvider wrapper — not at :root — because
|
|
5
|
+
// @wordpress/theme's ThemeProvider emits its generated --wpds-* ramp as inline
|
|
6
|
+
// styles on its own wrapper div, not on the document root. Substituting at the
|
|
7
|
+
// provider means a ThemeProvider above it is picked up; substituting at :root
|
|
8
|
+
// would miss it entirely.
|
|
9
|
+
|
|
10
|
+
// :where() keeps the rule at zero specificity so any consumer rule
|
|
11
|
+
// targeting the same element wins. Chart roots deliberately do NOT
|
|
12
|
+
// re-declare these: an element's own declaration beats an inherited one,
|
|
13
|
+
// so re-declaring lower down would shadow a consumer override set inside
|
|
14
|
+
// the provider tree.
|
|
15
|
+
|
|
16
|
+
// The five roles a `theme` prop field can override read a `*-theme` variable
|
|
17
|
+
// first, which GlobalChartsProvider writes inline from that field. Publishing
|
|
18
|
+
// the consumer's value one layer out, rather than as the role itself, is what
|
|
19
|
+
// keeps the catalog default reachable: a value that is invalid at
|
|
20
|
+
// computed-value time (a fallback-less `var()` naming a token nothing set,
|
|
21
|
+
// which is how premium-analytics passes its grid stroke) invalidates only the
|
|
22
|
+
// `*-theme` variable, and the role's own fallback still resolves the mapped
|
|
23
|
+
// --wpds-* token. Written as the role directly, that same value made the role
|
|
24
|
+
// guaranteed-invalid and took every bare `var(--a8c-charts-color-grid)` read
|
|
25
|
+
// site with it — gridlines painted `unset` instead of the spec grey.
|
|
26
|
+
// `src/styles/test/chart-scope.test.ts` pins which roles carry the layer.
|
|
27
|
+
|
|
28
|
+
// Declares custom properties only. This stylesheet must never change layout.
|
|
29
|
+
:where(.a8c-charts-scope) {
|
|
30
|
+
--a8c-charts-color-grid: var(--a8c-charts-color-grid-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
31
|
+
--a8c-charts-color-axis: var(--a8c-charts-color-axis-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
32
|
+
--a8c-charts-color-tick: var(--a8c-charts-color-tick-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
33
|
+
--a8c-charts-color-label: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
34
|
+
--a8c-charts-color-label-secondary: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
35
|
+
--a8c-charts-color-label-inverse: var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0);
|
|
36
|
+
// White label text sits on arbitrary series colours, so it has no WPDS
|
|
37
|
+
// content-foreground equivalent.
|
|
38
|
+
--a8c-charts-color-label-on-fill: #fff;
|
|
39
|
+
// The SVG axis and tick labels. `svgLabelSmall.fill` publishes THIS role, so
|
|
40
|
+
// deriving from --a8c-charts-color-label is safe and wanted: the theme field
|
|
41
|
+
// moves only these labels, while setting the broad role in CSS still moves
|
|
42
|
+
// every label at once. (--a8c-charts-color-surface cannot derive the same
|
|
43
|
+
// way — see its comment; there the theme field owns the broad role.)
|
|
44
|
+
--a8c-charts-color-label-axis: var(--a8c-charts-color-label-axis-theme, var(--a8c-charts-color-label));
|
|
45
|
+
--a8c-charts-color-annotation: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
46
|
+
--a8c-charts-color-trend-up: var(--wpds-color-foreground-content-success-weak, #008030);
|
|
47
|
+
--a8c-charts-color-trend-down: var(--wpds-color-foreground-content-error-weak, #cc1818);
|
|
48
|
+
--a8c-charts-color-trend-neutral: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
49
|
+
--a8c-charts-color-background: var(--a8c-charts-color-background-theme, var(--wpds-color-background-surface-neutral-strong, #fff));
|
|
50
|
+
// Surfaces that float over the chart — tooltips, the annotation label, the
|
|
51
|
+
// zoom-reset control. Same WPDS token as --a8c-charts-color-background, and
|
|
52
|
+
// deliberately NOT derived from it: `backgroundColor` publishes the
|
|
53
|
+
// background role, so deriving would let that theme field repaint these too.
|
|
54
|
+
// Sibling of the background role, not a child of it.
|
|
55
|
+
--a8c-charts-color-surface: var(--wpds-color-background-surface-neutral-strong, #fff);
|
|
56
|
+
--a8c-charts-color-surface-secondary: var(--wpds-color-background-surface-neutral-weak, #f4f4f4);
|
|
57
|
+
--a8c-charts-color-track: var(--wpds-color-background-track-neutral-weak, #f0f0f0);
|
|
58
|
+
// Translucent dark surface; no WPDS token carries this.
|
|
59
|
+
--a8c-charts-color-tooltip-surface: rgb(0 0 0 / 85%);
|
|
60
|
+
--a8c-charts-color-zoom-selection: var(--wpds-color-background-interactive-brand-strong, #3858e9);
|
|
61
|
+
--a8c-charts-color-zoom-selection-stroke: var(--wpds-color-stroke-interactive-brand, #3858e9);
|
|
62
|
+
|
|
63
|
+
// @wordpress/theme 1.0.0 removed the whole --wpds-elevation-* group while the
|
|
64
|
+
// elevation model is redefined (gutenberg#80099). These carry the removed spec
|
|
65
|
+
// values literally until a replacement token exists.
|
|
66
|
+
--a8c-charts-elevation-xs:
|
|
67
|
+
0 1px 1px 0 #00000008,
|
|
68
|
+
0 1px 2px 0 #00000005,
|
|
69
|
+
0 3px 3px 0 #00000005,
|
|
70
|
+
0 4px 4px 0 #00000003;
|
|
71
|
+
--a8c-charts-elevation-sm:
|
|
72
|
+
0 1px 2px 0 #0000000d,
|
|
73
|
+
0 2px 3px 0 #0000000a,
|
|
74
|
+
0 6px 6px 0 #00000008,
|
|
75
|
+
0 8px 8px 0 #00000005;
|
|
76
|
+
|
|
77
|
+
--a8c-charts-motion-duration-entrance: var(--wpds-motion-duration-xl, 400ms);
|
|
78
|
+
--a8c-charts-motion-easing-entrance: var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1));
|
|
79
|
+
--a8c-charts-border-radius-bar: var(--wpds-border-radius-md, 4px);
|
|
80
|
+
--a8c-charts-border-radius-cell: var(--wpds-border-radius-sm, 2px);
|
|
81
|
+
// Pill shape; no WPDS radius reaches it.
|
|
82
|
+
--a8c-charts-border-radius-leaderboard-bar: 9999px;
|
|
83
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
THEME_LAYERED_ROLES,
|
|
5
|
+
themeLayerVar,
|
|
6
|
+
} from '../../providers/chart-context/private/theme-override-vars';
|
|
7
|
+
|
|
8
|
+
const stylesheet = readFileSync( join( __dirname, '..', 'chart-scope.scss' ), 'utf8' );
|
|
9
|
+
const tokensDoc = readFileSync( join( __dirname, '..', '..', '..', 'TOKENS.md' ), 'utf8' );
|
|
10
|
+
|
|
11
|
+
type Entry = {
|
|
12
|
+
/** The custom property this one reads, or null when the value is a literal. */
|
|
13
|
+
reads: string | null;
|
|
14
|
+
/** The `var()` fallback, or the whole value when it reads nothing. */
|
|
15
|
+
fallback: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const normalize = ( value: string ): string => value.replace( /\s+/g, ' ' ).trim();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Unwraps the theme layer a `theme`-prop-overridable role carries, so the tables in `TOKENS.md` keep documenting what a role ultimately resolves to — its `--wpds-*` token and spec fallback — rather than restating the override plumbing in every row. Which roles carry the layer is pinned separately below.
|
|
22
|
+
*
|
|
23
|
+
* @param name - The declared property name.
|
|
24
|
+
* @param value - Its normalized value.
|
|
25
|
+
* @return The value with a leading `var(<name>-theme, … )` wrapper removed.
|
|
26
|
+
*/
|
|
27
|
+
function stripThemeLayer( name: string, value: string ): string {
|
|
28
|
+
const layered = new RegExp( `^var\\(\\s*${ themeLayerVar( name ) }\\s*,\\s*(.*)\\)$`, 's' ).exec(
|
|
29
|
+
value
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return layered ? layered[ 1 ].trim() : value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parses each catalog declaration in the stylesheet into the property it reads and the fallback it carries. Splitting the `var()` arguments at the *first* comma keeps a fallback that is itself a function call intact — `cubic-bezier(0.25, 0, 0, 1)` would otherwise be truncated at its first argument.
|
|
37
|
+
*
|
|
38
|
+
* @return Every declared `--a8c-charts-*` property, keyed by name.
|
|
39
|
+
*/
|
|
40
|
+
function parseStylesheet(): Map< string, Entry > {
|
|
41
|
+
const body = stylesheet.slice( stylesheet.indexOf( '{' ) + 1, stylesheet.lastIndexOf( '}' ) );
|
|
42
|
+
const entries = new Map< string, Entry >();
|
|
43
|
+
|
|
44
|
+
for ( const declaration of body
|
|
45
|
+
.split( '\n' )
|
|
46
|
+
.map( line => line.trim() )
|
|
47
|
+
.filter( line => line && ! line.startsWith( '//' ) )
|
|
48
|
+
.join( '\n' )
|
|
49
|
+
.split( ';' ) ) {
|
|
50
|
+
const separator = declaration.indexOf( ':' );
|
|
51
|
+
|
|
52
|
+
if ( separator === -1 ) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const name = declaration.slice( 0, separator ).trim();
|
|
57
|
+
const value = stripThemeLayer( name, normalize( declaration.slice( separator + 1 ) ) );
|
|
58
|
+
const wrapped = /^var\(\s*(.*)\)$/s.exec( value );
|
|
59
|
+
|
|
60
|
+
if ( ! wrapped ) {
|
|
61
|
+
entries.set( name, { reads: null, fallback: value } );
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const args = wrapped[ 1 ];
|
|
66
|
+
const comma = args.indexOf( ',' );
|
|
67
|
+
|
|
68
|
+
entries.set(
|
|
69
|
+
name,
|
|
70
|
+
comma === -1
|
|
71
|
+
? { reads: args.trim(), fallback: null }
|
|
72
|
+
: { reads: args.slice( 0, comma ).trim(), fallback: normalize( args.slice( comma + 1 ) ) }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parses the catalog tables in `TOKENS.md` — the ones whose header is `Role | Maps to …` — into the same shape as the stylesheet, so the two can be compared directly. Other tables in the document (theme-field mappings, deprecated aliases) are skipped.
|
|
81
|
+
*
|
|
82
|
+
* @return Every documented role, keyed by name.
|
|
83
|
+
*/
|
|
84
|
+
function parseTokensDoc(): Map< string, Entry > {
|
|
85
|
+
const entries = new Map< string, Entry >();
|
|
86
|
+
const lines = tokensDoc.split( '\n' );
|
|
87
|
+
let inCatalogTable = false;
|
|
88
|
+
|
|
89
|
+
for ( const line of lines ) {
|
|
90
|
+
if ( ! line.trim().startsWith( '|' ) ) {
|
|
91
|
+
inCatalogTable = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cells = line
|
|
96
|
+
.split( '|' )
|
|
97
|
+
.slice( 1, -1 )
|
|
98
|
+
.map( cell => cell.trim() );
|
|
99
|
+
|
|
100
|
+
if ( cells[ 0 ] === 'Role' ) {
|
|
101
|
+
inCatalogTable = cells[ 1 ]?.startsWith( 'Maps to' ) ?? false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ( ! inCatalogTable || cells.length !== 3 || /^-+$/.test( cells[ 0 ] ) ) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const [ role, mapping, fallback ] = cells.map( cell => cell.replace( /`/g, '' ) );
|
|
110
|
+
// A "none" cell explains *why* there is no mapping, and that explanation can
|
|
111
|
+
// name a `--wpds-*` token itself (the removed elevation group). Read it as
|
|
112
|
+
// "no mapping" before looking for a token name inside it.
|
|
113
|
+
const unmapped = mapping.startsWith( '_(none' );
|
|
114
|
+
const derives = /derives from (--[\w-]+)/.exec( mapping );
|
|
115
|
+
const wpds = /(--wpds-[\w-]+)/.exec( mapping );
|
|
116
|
+
|
|
117
|
+
entries.set( role, {
|
|
118
|
+
reads: unmapped ? null : derives?.[ 1 ] ?? wpds?.[ 1 ] ?? null,
|
|
119
|
+
fallback: fallback === '—' ? null : normalize( fallback ),
|
|
120
|
+
} );
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return entries;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const declared = parseStylesheet();
|
|
127
|
+
const documented = parseTokensDoc();
|
|
128
|
+
|
|
129
|
+
describe( 'chart scope catalog', () => {
|
|
130
|
+
it( 'documents every declared role, and declares every documented one', () => {
|
|
131
|
+
expect( [ ...documented.keys() ].sort() ).toEqual( [ ...declared.keys() ].sort() );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
// The tables in TOKENS.md restate the stylesheet for consumers who only have the
|
|
135
|
+
// published package. This is the check that stops the two drifting apart.
|
|
136
|
+
it.each( [ ...declared.keys() ] )( 'documents %s with the value it is declared with', token => {
|
|
137
|
+
expect( documented.get( token ) ).toEqual( declared.get( token ) );
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
// The layer is what keeps a `theme` prop override from being able to take its role down with it: an override that is invalid at computed-value time only invalidates `<role>-theme`, and the role's own fallback still resolves the mapped token. Drop the layer from a role and that role's overrides go back to blanking every read site.
|
|
141
|
+
it.each( THEME_LAYERED_ROLES )( 'declares %s reading its theme layer first', role => {
|
|
142
|
+
expect( stylesheet ).toMatch(
|
|
143
|
+
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*,` )
|
|
144
|
+
);
|
|
145
|
+
} );
|
|
146
|
+
|
|
147
|
+
it( 'gives a theme layer to the overridable roles and to nothing else', () => {
|
|
148
|
+
const layered = [ ...declared.keys() ].filter( role =>
|
|
149
|
+
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*,` ).test( stylesheet )
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect( layered.sort() ).toEqual( [ ...THEME_LAYERED_ROLES ].sort() );
|
|
153
|
+
} );
|
|
154
|
+
|
|
155
|
+
it( 'scopes the catalog to :where(.a8c-charts-scope) rather than :root', () => {
|
|
156
|
+
expect( stylesheet ).toMatch( /:where\(\.a8c-charts-scope\)\s*{/ );
|
|
157
|
+
expect( stylesheet ).not.toMatch( /(^|\s):root\s*{/ );
|
|
158
|
+
} );
|
|
159
|
+
|
|
160
|
+
it( 'declares custom properties only, so this stylesheet cannot change layout', () => {
|
|
161
|
+
expect( [ ...declared.keys() ].every( name => name.startsWith( '--a8c-charts-' ) ) ).toBe(
|
|
162
|
+
true
|
|
163
|
+
);
|
|
164
|
+
} );
|
|
165
|
+
} );
|