@automattic/charts 3.1.0 → 3.2.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 +17 -0
- package/TOKENS.md +51 -12
- package/dist/index.cjs +307 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +7 -1
- package/dist/index.d.cts +223 -127
- package/dist/index.d.ts +223 -127
- package/dist/index.js +306 -212
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/charts/area-chart/area-chart.tsx +2 -2
- package/src/charts/area-chart/test/area-chart.test.tsx +45 -0
- package/src/charts/bar-chart/bar-chart.tsx +36 -10
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +19 -12
- package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +67 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +101 -0
- package/src/charts/bar-list-chart/bar-list-chart.tsx +40 -2
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +38 -0
- package/src/charts/line-chart/line-chart.tsx +18 -3
- package/src/charts/line-chart/test/line-chart.test.tsx +98 -0
- package/src/charts/sparkline/sparkline.tsx +9 -3
- package/src/charts/sparkline/types.ts +1 -1
- package/src/components/tooltip/accessible-tooltip.tsx +32 -2
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +68 -0
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +4 -4
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +17 -10
- package/src/hooks/test/use-xychart-theme.test.tsx +50 -5
- package/src/hooks/use-xychart-theme.ts +14 -20
- package/src/providers/chart-context/global-charts-provider.tsx +44 -33
- package/src/providers/chart-context/private/series-palette.ts +45 -0
- package/src/providers/chart-context/private/theme-override-vars.ts +46 -2
- package/src/providers/chart-context/private/with-catalog-pointers.ts +45 -2
- package/src/providers/chart-context/test/sparse-theme.test.ts +16 -2
- package/src/providers/chart-context/test/theme-override-vars.test.ts +65 -0
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +8 -1
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +57 -0
- package/src/providers/chart-context/themes.ts +8 -3
- package/src/styles/chart-scope.scss +52 -24
- package/src/styles/test/chart-scope.test.ts +22 -9
- package/src/types.ts +148 -31
- package/src/utils/get-styles.ts +7 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The series palette's slot manifest.
|
|
3
|
+
*
|
|
4
|
+
* Which slots exist is a fact about the catalog `chart-scope.scss` emits, not a theme value, so it
|
|
5
|
+
* is declared here rather than in `themes.ts`, which now derives `defaultTheme.colors` from it.
|
|
6
|
+
*
|
|
7
|
+
* That is as far as the separation goes, and the limit is worth knowing before the field is
|
|
8
|
+
* deleted. `GlobalChartsProvider` still resolves `providerTheme.colors`, not this manifest,
|
|
9
|
+
* because `withCatalogPointers` parks the consumer's own color in each pointer's terminal position
|
|
10
|
+
* and that literal is the palette's only carrier where `getComputedStyle` resolves nothing — SSR
|
|
11
|
+
* and jsdom. Swapping the provider onto this manifest passes in a browser and collapses every
|
|
12
|
+
* consumer palette to the catalog seed under jsdom; 24 tests say so. Removing `colors` therefore
|
|
13
|
+
* needs a different home for that literal first, not just a change of source here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** How many series-palette slots the catalog emits. `theme.colors` entries past this are ignored. */
|
|
17
|
+
export const SERIES_SLOT_COUNT = 5;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The catalog role holding one series-palette slot.
|
|
21
|
+
*
|
|
22
|
+
* @param slot - The one-based slot number.
|
|
23
|
+
* @return The role name.
|
|
24
|
+
*/
|
|
25
|
+
export const seriesRole = ( slot: number ): string => `--a8c-charts-color-series-${ slot }`;
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Only slot 1 carries a terminal literal, and it is reached only where `getComputedStyle` resolves
|
|
29
|
+
* nothing — SSR and jsdom. The rest resolve to nothing until a consumer sets them, and the provider
|
|
30
|
+
* drops what resolves to nothing, so the palette compacts rather than repeating one color.
|
|
31
|
+
*
|
|
32
|
+
* The literal matches slot 1's own last-resort fallback in `chart-scope.scss`. It is deliberately
|
|
33
|
+
* the bare hex rather than that slot's full `var()` chain: this string is resolved against the DOM,
|
|
34
|
+
* where the chain has already been walked, and is only read at all when no DOM answered.
|
|
35
|
+
*/
|
|
36
|
+
const SERIES_SLOT_1_FALLBACK = '#3858e9';
|
|
37
|
+
|
|
38
|
+
/** The catalog pointer for every slot, in slot order. */
|
|
39
|
+
export const SERIES_PALETTE_POINTERS: readonly string[] = Array.from(
|
|
40
|
+
{ length: SERIES_SLOT_COUNT },
|
|
41
|
+
( _, index ) =>
|
|
42
|
+
index === 0
|
|
43
|
+
? `var(${ seriesRole( 1 ) }, ${ SERIES_SLOT_1_FALLBACK })`
|
|
44
|
+
: `var(${ seriesRole( index + 1 ) })`
|
|
45
|
+
);
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { SERIES_SLOT_COUNT, seriesRole } from './series-palette';
|
|
1
2
|
import type { ChartTheme } from '../../../types';
|
|
2
3
|
|
|
4
|
+
/*
|
|
5
|
+
* `process.env.NODE_ENV` is replaced by the bundler at build time. Declare a
|
|
6
|
+
* minimal `process` locally so this file type-checks as source under `jetpack:src`.
|
|
7
|
+
*/
|
|
8
|
+
declare const process: { env: Record< string, string | undefined > };
|
|
9
|
+
|
|
3
10
|
// 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.
|
|
11
|
+
//
|
|
12
|
+
// `theme.colors` is the one field mapped by position rather than by name: entry N publishes slot N+1, which is what makes it sugar over the five slots rather than a second palette with its own precedence.
|
|
4
13
|
const ROLE_FOR_FIELD: Array< [ string, ( theme: Partial< ChartTheme > ) => string | undefined ] > =
|
|
5
14
|
[
|
|
6
15
|
[ '--a8c-charts-color-background', theme => theme.backgroundColor ],
|
|
@@ -8,8 +17,37 @@ const ROLE_FOR_FIELD: Array< [ string, ( theme: Partial< ChartTheme > ) => strin
|
|
|
8
17
|
[ '--a8c-charts-color-axis', theme => theme.xAxisLineStyles?.stroke ],
|
|
9
18
|
[ '--a8c-charts-color-tick', theme => theme.xTickLineStyles?.stroke ],
|
|
10
19
|
[ '--a8c-charts-color-label-axis', theme => theme.svgLabelSmall?.fill ],
|
|
20
|
+
[ '--a8c-charts-color-label-background', theme => theme.labelBackgroundColor ],
|
|
21
|
+
[ '--a8c-charts-color-label-on-fill', theme => theme.labelTextColor ],
|
|
22
|
+
...Array.from(
|
|
23
|
+
{ length: SERIES_SLOT_COUNT },
|
|
24
|
+
( _, index ): [ string, ( theme: Partial< ChartTheme > ) => string | undefined ] => [
|
|
25
|
+
seriesRole( index + 1 ),
|
|
26
|
+
theme => theme.colors?.[ index ],
|
|
27
|
+
]
|
|
28
|
+
),
|
|
11
29
|
];
|
|
12
30
|
|
|
31
|
+
let warnedAboutExtraColors = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Warns once when a consumer passes more colors than there are slots to publish them into.
|
|
35
|
+
*
|
|
36
|
+
* @param count - How many entries `theme.colors` holds.
|
|
37
|
+
*/
|
|
38
|
+
function warnOnceAboutExtraColors( count: number ): void {
|
|
39
|
+
if ( warnedAboutExtraColors || process.env.NODE_ENV === 'production' ) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
warnedAboutExtraColors = true;
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.warn(
|
|
46
|
+
`[Charts] theme.colors holds ${ count } colors and the palette has ${ SERIES_SLOT_COUNT } slots, so entries past the ${ SERIES_SLOT_COUNT }th are ignored. ` +
|
|
47
|
+
'Set a per-series color with `options.stroke` instead.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
/**
|
|
14
52
|
* The variable a `theme` prop override is published as, one layer outside the role itself.
|
|
15
53
|
*
|
|
@@ -26,7 +64,7 @@ export const THEME_LAYERED_ROLES: readonly string[] = ROLE_FOR_FIELD.map( ( [ ro
|
|
|
26
64
|
/**
|
|
27
65
|
* Whether a value reads the role it is being published for, which would make the catalog entry depend on itself.
|
|
28
66
|
*
|
|
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
|
|
67
|
+
* 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 color. Verified in Chrome: `--role-theme: var(--role, blue); --role: var(--role-theme, green)` leaves `--role` empty, not `green`.
|
|
30
68
|
*
|
|
31
69
|
* Three things this has to get right, none of which a plain substring test gets:
|
|
32
70
|
*
|
|
@@ -54,10 +92,12 @@ export type ThemeOverrides = {
|
|
|
54
92
|
};
|
|
55
93
|
|
|
56
94
|
/**
|
|
57
|
-
* Maps a sparse consumer theme onto the theme-layer variables its overridden catalog roles read, so CSS-painted and JS-resolved
|
|
95
|
+
* Maps a sparse consumer theme onto the theme-layer variables its overridden catalog roles read, so CSS-painted and JS-resolved colors read one source.
|
|
58
96
|
*
|
|
59
97
|
* 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
98
|
*
|
|
99
|
+
* `theme.colors` publishes through the same mechanism, one slot per entry, so a CSS declaration of `--a8c-charts-color-series-N` still beats it and a short array leaves the later slots unset rather than blank.
|
|
100
|
+
*
|
|
61
101
|
* @param theme - The consumer's `theme` prop, before merging with the default theme.
|
|
62
102
|
* @return The wrapper's custom properties, and the roles the consumer overrode.
|
|
63
103
|
*/
|
|
@@ -66,6 +106,10 @@ export const themeOverrideVars = ( theme?: Partial< ChartTheme > ): ThemeOverrid
|
|
|
66
106
|
return { vars: {}, roles: [] };
|
|
67
107
|
}
|
|
68
108
|
|
|
109
|
+
if ( Array.isArray( theme.colors ) && theme.colors.length > SERIES_SLOT_COUNT ) {
|
|
110
|
+
warnOnceAboutExtraColors( theme.colors.length );
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
const vars: Record< string, string > = {};
|
|
70
114
|
const roles: string[] = [];
|
|
71
115
|
|
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
import { defaultTheme } from '../themes';
|
|
2
|
+
import { SERIES_PALETTE_POINTERS, SERIES_SLOT_COUNT, seriesRole } from './series-palette';
|
|
2
3
|
import type { CompleteChartTheme } from '../../../types';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The pointer one palette slot is restored to.
|
|
7
|
+
*
|
|
8
|
+
* Unlike the other roles, this keeps the consumer's own color as the pointer's terminal literal instead of the catalog's. The literal is only ever reached where `getComputedStyle` resolves nothing — SSR and jsdom — and there the palette is the one field where falling back to the catalog default would be visible: every series would paint the same seeded blue. In a browser the slot resolves first, so a CSS declaration of it still outranks the consumer's `theme.colors`.
|
|
9
|
+
*
|
|
10
|
+
* @param index - The slot's zero-based index in `colors`.
|
|
11
|
+
* @param value - The merged theme's entry for it.
|
|
12
|
+
* @return The catalog pointer for that slot.
|
|
13
|
+
*/
|
|
14
|
+
const seriesPointer = ( index: number, value: string | undefined ): string => {
|
|
15
|
+
const role = seriesRole( index + 1 );
|
|
16
|
+
|
|
17
|
+
// Idempotent: `withCatalogPointers` runs this once per overridden slot, so from the second call on the array already holds pointers.
|
|
18
|
+
if ( typeof value === 'string' && value.startsWith( `var(${ role }` ) ) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A slot the consumer left empty keeps the catalog's own pointer, so a CSS declaration of it still reaches the palette.
|
|
23
|
+
if ( typeof value !== 'string' || value === '' ) {
|
|
24
|
+
return SERIES_PALETTE_POINTERS[ index ];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `var(${ role }, ${ value })`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Restores each overridden role's mapped theme field to the catalog pointer already written for it — `defaultTheme` for the ordinary roles, `series-palette.ts` for the palette slots — so the value comes from the one place that declares it rather than a second, hand-copied literal.
|
|
31
|
+
//
|
|
32
|
+
// Every series slot restores the whole `colors` array, not just its own entry. `mergeThemes` replaces arrays outright, so a consumer passing two colors would otherwise leave a two-entry palette and put slots 3 to 5 out of reach of a CSS declaration that sets them.
|
|
5
33
|
const CATALOG_RESTORE_FOR_ROLE: Record<
|
|
6
34
|
string,
|
|
7
35
|
( theme: CompleteChartTheme ) => Partial< CompleteChartTheme >
|
|
8
36
|
> = {
|
|
37
|
+
// Each of the five entries rebuilds the whole array, so a consumer overriding every slot runs this five times. That is deliberate rather than merely tolerated: `seriesPointer` is idempotent, so every pass after the first is a no-op, and five allocations of a five-element array is not worth a special case in a map that is otherwise one entry per role.
|
|
38
|
+
...Object.fromEntries(
|
|
39
|
+
Array.from( { length: SERIES_SLOT_COUNT }, ( _, index ) => [
|
|
40
|
+
seriesRole( index + 1 ),
|
|
41
|
+
( theme: CompleteChartTheme ) => ( {
|
|
42
|
+
colors: SERIES_PALETTE_POINTERS.map( ( _pointer, slot ) =>
|
|
43
|
+
seriesPointer( slot, theme.colors?.[ slot ] )
|
|
44
|
+
),
|
|
45
|
+
} ),
|
|
46
|
+
] )
|
|
47
|
+
),
|
|
9
48
|
'--a8c-charts-color-background': () => ( { backgroundColor: defaultTheme.backgroundColor } ),
|
|
10
49
|
'--a8c-charts-color-grid': theme => ( {
|
|
11
50
|
gridStyles: { ...theme.gridStyles, stroke: defaultTheme.gridStyles.stroke },
|
|
@@ -19,6 +58,10 @@ const CATALOG_RESTORE_FOR_ROLE: Record<
|
|
|
19
58
|
'--a8c-charts-color-label-axis': theme => ( {
|
|
20
59
|
svgLabelSmall: { ...theme.svgLabelSmall, fill: defaultTheme.svgLabelSmall.fill },
|
|
21
60
|
} ),
|
|
61
|
+
'--a8c-charts-color-label-background': () => ( {
|
|
62
|
+
labelBackgroundColor: defaultTheme.labelBackgroundColor,
|
|
63
|
+
} ),
|
|
64
|
+
'--a8c-charts-color-label-on-fill': () => ( { labelTextColor: defaultTheme.labelTextColor } ),
|
|
22
65
|
};
|
|
23
66
|
|
|
24
67
|
/**
|
|
@@ -26,7 +69,7 @@ const CATALOG_RESTORE_FOR_ROLE: Record<
|
|
|
26
69
|
*
|
|
27
70
|
* 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
71
|
*
|
|
29
|
-
* Every field outside the
|
|
72
|
+
* Every field outside the 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
73
|
*
|
|
31
74
|
* @param merged - The consumer theme merged onto `defaultTheme` (`mergeThemes` output).
|
|
32
75
|
* @param overriddenRoles - The catalog roles the consumer overrode, from `themeOverrideVars`.
|
|
@@ -20,16 +20,30 @@ describe( 'defaultTheme', () => {
|
|
|
20
20
|
expect( strings.filter( value => value.includes( '--wpds-' ) ) ).toEqual( [] );
|
|
21
21
|
} );
|
|
22
22
|
|
|
23
|
-
it( 'points every themed
|
|
23
|
+
it( 'points every themed color at an --a8c-charts-* catalog role', () => {
|
|
24
24
|
const varStrings = strings.filter( value => value.startsWith( 'var(' ) );
|
|
25
25
|
|
|
26
26
|
expect( varStrings.length ).toBeGreaterThan( 0 );
|
|
27
27
|
expect( varStrings.every( value => value.startsWith( 'var(--a8c-charts-' ) ) ).toBe( true );
|
|
28
28
|
} );
|
|
29
29
|
|
|
30
|
+
// The palette slots past the first are the exception, below: a terminal literal there would be a color nobody chose.
|
|
30
31
|
it( 'gives every catalog pointer a terminal literal for the SSR and jsdom paths', () => {
|
|
31
|
-
const varStrings = strings
|
|
32
|
+
const varStrings = strings
|
|
33
|
+
.filter( value => value.startsWith( 'var(--a8c-charts-' ) )
|
|
34
|
+
.filter( value => ! /^var\( *--a8c-charts-color-series-[2-9] *\)$/.test( value ) );
|
|
32
35
|
|
|
33
36
|
expect( varStrings.every( value => value.includes( ',' ) ) ).toBe( true );
|
|
34
37
|
} );
|
|
38
|
+
|
|
39
|
+
// Only slot 1 seeds a palette. The rest resolve to nothing until a consumer sets them, and the provider drops what resolves to nothing, so the palette compacts instead of repeating one color.
|
|
40
|
+
it( 'leaves the palette slots past the first with no default of their own', () => {
|
|
41
|
+
expect( defaultTheme.colors ).toEqual( [
|
|
42
|
+
'var(--a8c-charts-color-series-1, #3858e9)',
|
|
43
|
+
'var(--a8c-charts-color-series-2)',
|
|
44
|
+
'var(--a8c-charts-color-series-3)',
|
|
45
|
+
'var(--a8c-charts-color-series-4)',
|
|
46
|
+
'var(--a8c-charts-color-series-5)',
|
|
47
|
+
] );
|
|
48
|
+
} );
|
|
35
49
|
} );
|
|
@@ -29,6 +29,9 @@ describe( 'themeOverrideVars', () => {
|
|
|
29
29
|
xAxisLineStyles: { stroke: '#333' },
|
|
30
30
|
xTickLineStyles: { stroke: '#444' },
|
|
31
31
|
svgLabelSmall: { fill: '#555' },
|
|
32
|
+
labelBackgroundColor: '#556',
|
|
33
|
+
labelTextColor: '#557',
|
|
34
|
+
colors: [ '#661', '#662', '#663', '#664', '#665' ],
|
|
32
35
|
} )
|
|
33
36
|
).toEqual( {
|
|
34
37
|
vars: {
|
|
@@ -37,6 +40,13 @@ describe( 'themeOverrideVars', () => {
|
|
|
37
40
|
'--a8c-charts-color-axis-theme': '#333',
|
|
38
41
|
'--a8c-charts-color-tick-theme': '#444',
|
|
39
42
|
'--a8c-charts-color-label-axis-theme': '#555',
|
|
43
|
+
'--a8c-charts-color-label-background-theme': '#556',
|
|
44
|
+
'--a8c-charts-color-label-on-fill-theme': '#557',
|
|
45
|
+
'--a8c-charts-color-series-1-theme': '#661',
|
|
46
|
+
'--a8c-charts-color-series-2-theme': '#662',
|
|
47
|
+
'--a8c-charts-color-series-3-theme': '#663',
|
|
48
|
+
'--a8c-charts-color-series-4-theme': '#664',
|
|
49
|
+
'--a8c-charts-color-series-5-theme': '#665',
|
|
40
50
|
},
|
|
41
51
|
roles: [
|
|
42
52
|
'--a8c-charts-color-background',
|
|
@@ -44,6 +54,13 @@ describe( 'themeOverrideVars', () => {
|
|
|
44
54
|
'--a8c-charts-color-axis',
|
|
45
55
|
'--a8c-charts-color-tick',
|
|
46
56
|
'--a8c-charts-color-label-axis',
|
|
57
|
+
'--a8c-charts-color-label-background',
|
|
58
|
+
'--a8c-charts-color-label-on-fill',
|
|
59
|
+
'--a8c-charts-color-series-1',
|
|
60
|
+
'--a8c-charts-color-series-2',
|
|
61
|
+
'--a8c-charts-color-series-3',
|
|
62
|
+
'--a8c-charts-color-series-4',
|
|
63
|
+
'--a8c-charts-color-series-5',
|
|
47
64
|
],
|
|
48
65
|
} );
|
|
49
66
|
} );
|
|
@@ -55,6 +72,9 @@ describe( 'themeOverrideVars', () => {
|
|
|
55
72
|
xAxisLineStyles: { stroke: '#333' },
|
|
56
73
|
xTickLineStyles: { stroke: '#444' },
|
|
57
74
|
svgLabelSmall: { fill: '#555' },
|
|
75
|
+
labelBackgroundColor: '#556',
|
|
76
|
+
labelTextColor: '#557',
|
|
77
|
+
colors: [ '#661', '#662', '#663', '#664', '#665' ],
|
|
58
78
|
} );
|
|
59
79
|
|
|
60
80
|
expect( Object.keys( vars ) ).toEqual( THEME_LAYERED_ROLES.map( themeLayerVar ) );
|
|
@@ -68,6 +88,51 @@ describe( 'themeOverrideVars', () => {
|
|
|
68
88
|
} );
|
|
69
89
|
} );
|
|
70
90
|
|
|
91
|
+
// A short array is not padded: the later slots stay unset, so the palette compacts rather than repeating a color the consumer never chose.
|
|
92
|
+
it( 'publishes one palette slot per color and leaves the rest unset', () => {
|
|
93
|
+
expect( themeOverrideVars( { colors: [ 'red', 'blue' ] } ) ).toEqual( {
|
|
94
|
+
vars: {
|
|
95
|
+
'--a8c-charts-color-series-1-theme': 'red',
|
|
96
|
+
'--a8c-charts-color-series-2-theme': 'blue',
|
|
97
|
+
},
|
|
98
|
+
roles: [ '--a8c-charts-color-series-1', '--a8c-charts-color-series-2' ],
|
|
99
|
+
} );
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'drops palette entries past the last slot', () => {
|
|
103
|
+
// Spied rather than asserted on: an over-long palette warns, and `@wordpress/jest-console` fails any test that leaves a console call unclaimed. The warning itself is covered below.
|
|
104
|
+
const warn = jest.spyOn( console, 'warn' ).mockImplementation( () => {} );
|
|
105
|
+
const { vars } = themeOverrideVars( {
|
|
106
|
+
colors: [ 'a', 'b', 'c', 'd', 'e', 'f', 'g' ],
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
expect( vars[ '--a8c-charts-color-series-5-theme' ] ).toBe( 'e' );
|
|
110
|
+
expect( vars[ '--a8c-charts-color-series-6-theme' ] ).toBeUndefined();
|
|
111
|
+
|
|
112
|
+
warn.mockRestore();
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
// Isolated so the warning's module-level latch starts unset — the assertion is that it warns once per module instance, not once per call.
|
|
116
|
+
it( 'warns once about the dropped entries, not once per render', () => {
|
|
117
|
+
jest.isolateModules( () => {
|
|
118
|
+
const warn = jest.spyOn( console, 'warn' ).mockImplementation( () => {} );
|
|
119
|
+
const {
|
|
120
|
+
themeOverrideVars: freshThemeOverrideVars,
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- isolateModules needs a synchronous require to get a module instance whose latch is unset.
|
|
122
|
+
} = require( '../private/theme-override-vars' ) as {
|
|
123
|
+
themeOverrideVars: typeof themeOverrideVars;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
freshThemeOverrideVars( { colors: [ 'a', 'b', 'c', 'd', 'e', 'f' ] } );
|
|
127
|
+
freshThemeOverrideVars( { colors: [ 'a', 'b', 'c', 'd', 'e', 'f' ] } );
|
|
128
|
+
|
|
129
|
+
expect( warn ).toHaveBeenCalledTimes( 1 );
|
|
130
|
+
expect( warn ).toHaveBeenCalledWith( expect.stringContaining( 'holds 6 colors' ) );
|
|
131
|
+
|
|
132
|
+
warn.mockRestore();
|
|
133
|
+
} );
|
|
134
|
+
} );
|
|
135
|
+
|
|
71
136
|
it( 'ignores a value that is itself a pointer at the role it would override', () => {
|
|
72
137
|
expect(
|
|
73
138
|
themeOverrideVars( { gridStyles: { stroke: 'var(--a8c-charts-color-grid, #dbdbdb)' } } ).vars
|
|
@@ -27,7 +27,14 @@ describe( 'useGlobalChartsTheme', () => {
|
|
|
27
27
|
|
|
28
28
|
const { result } = renderHook( () => useGlobalChartsTheme(), { wrapper } );
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// `theme.colors` publishes the palette's theme layers, so the merged theme holds catalog pointers rather than the consumer's literals. Each keeps its consumer color as the pointer's terminal literal, for SSR and jsdom.
|
|
31
|
+
expect( result.current.colors ).toEqual( [
|
|
32
|
+
'var(--a8c-charts-color-series-1, #FF0000)',
|
|
33
|
+
'var(--a8c-charts-color-series-2, #00FF00)',
|
|
34
|
+
'var(--a8c-charts-color-series-3, #0000FF)',
|
|
35
|
+
'var(--a8c-charts-color-series-4)',
|
|
36
|
+
'var(--a8c-charts-color-series-5)',
|
|
37
|
+
] );
|
|
31
38
|
// Other properties should still come from default theme
|
|
32
39
|
expect( result.current.backgroundColor ).toBe( defaultTheme.backgroundColor );
|
|
33
40
|
expect( result.current.gridStyles ).toEqual( defaultTheme.gridStyles );
|
|
@@ -11,6 +11,8 @@ describe( 'withCatalogPointers', () => {
|
|
|
11
11
|
xAxisLineStyles: { stroke: '#333' },
|
|
12
12
|
xTickLineStyles: { stroke: '#444' },
|
|
13
13
|
svgLabelSmall: { fill: '#555' },
|
|
14
|
+
labelBackgroundColor: '#556',
|
|
15
|
+
labelTextColor: '#557',
|
|
14
16
|
} as Partial< ChartTheme > );
|
|
15
17
|
|
|
16
18
|
const result = withCatalogPointers( merged, [
|
|
@@ -19,6 +21,8 @@ describe( 'withCatalogPointers', () => {
|
|
|
19
21
|
'--a8c-charts-color-axis',
|
|
20
22
|
'--a8c-charts-color-tick',
|
|
21
23
|
'--a8c-charts-color-label-axis',
|
|
24
|
+
'--a8c-charts-color-label-background',
|
|
25
|
+
'--a8c-charts-color-label-on-fill',
|
|
22
26
|
] );
|
|
23
27
|
|
|
24
28
|
expect( result.backgroundColor ).toBe( defaultTheme.backgroundColor );
|
|
@@ -26,6 +30,8 @@ describe( 'withCatalogPointers', () => {
|
|
|
26
30
|
expect( result.xAxisLineStyles.stroke ).toBe( defaultTheme.xAxisLineStyles.stroke );
|
|
27
31
|
expect( result.xTickLineStyles.stroke ).toBe( defaultTheme.xTickLineStyles.stroke );
|
|
28
32
|
expect( result.svgLabelSmall.fill ).toBe( defaultTheme.svgLabelSmall.fill );
|
|
33
|
+
expect( result.labelBackgroundColor ).toBe( defaultTheme.labelBackgroundColor );
|
|
34
|
+
expect( result.labelTextColor ).toBe( defaultTheme.labelTextColor );
|
|
29
35
|
|
|
30
36
|
expect( merged.backgroundColor ).toBe( '#111' );
|
|
31
37
|
expect( merged.gridStyles.stroke ).toBe( '#222' );
|
|
@@ -44,4 +50,55 @@ describe( 'withCatalogPointers', () => {
|
|
|
44
50
|
// Not a mapped role, so its literal survives even when other roles are being restored.
|
|
45
51
|
expect( result.leaderboardChart.deltaColors ).toEqual( [ 'red', 'grey', 'green' ] );
|
|
46
52
|
} );
|
|
53
|
+
|
|
54
|
+
// The palette is restored as a whole, so a consumer's short array cannot shorten it. `mergeThemes` replaces arrays outright, and a two-entry palette would leave slots 3 to 5 unreadable — a CSS declaration of one of them would have no entry to resolve through.
|
|
55
|
+
it( 'restores the full five-slot palette from a shorter consumer array', () => {
|
|
56
|
+
const merged = mergeThemes( defaultTheme, {
|
|
57
|
+
colors: [ '#111', '#222' ],
|
|
58
|
+
} as Partial< ChartTheme > );
|
|
59
|
+
|
|
60
|
+
const result = withCatalogPointers( merged, [
|
|
61
|
+
'--a8c-charts-color-series-1',
|
|
62
|
+
'--a8c-charts-color-series-2',
|
|
63
|
+
] );
|
|
64
|
+
|
|
65
|
+
expect( result.colors ).toEqual( [
|
|
66
|
+
'var(--a8c-charts-color-series-1, #111)',
|
|
67
|
+
'var(--a8c-charts-color-series-2, #222)',
|
|
68
|
+
'var(--a8c-charts-color-series-3)',
|
|
69
|
+
'var(--a8c-charts-color-series-4)',
|
|
70
|
+
'var(--a8c-charts-color-series-5)',
|
|
71
|
+
] );
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
// Each slot's restore rewrites the whole array, so restoring five of them must not wrap the pointers five times over.
|
|
75
|
+
it( 'restores the palette idempotently across every overridden slot', () => {
|
|
76
|
+
const merged = mergeThemes( defaultTheme, {
|
|
77
|
+
colors: [ '#111', '#222', '#333', '#444', '#555' ],
|
|
78
|
+
} as Partial< ChartTheme > );
|
|
79
|
+
|
|
80
|
+
const result = withCatalogPointers( merged, [
|
|
81
|
+
'--a8c-charts-color-series-1',
|
|
82
|
+
'--a8c-charts-color-series-2',
|
|
83
|
+
'--a8c-charts-color-series-3',
|
|
84
|
+
'--a8c-charts-color-series-4',
|
|
85
|
+
'--a8c-charts-color-series-5',
|
|
86
|
+
] );
|
|
87
|
+
|
|
88
|
+
expect( result.colors ).toEqual( [
|
|
89
|
+
'var(--a8c-charts-color-series-1, #111)',
|
|
90
|
+
'var(--a8c-charts-color-series-2, #222)',
|
|
91
|
+
'var(--a8c-charts-color-series-3, #333)',
|
|
92
|
+
'var(--a8c-charts-color-series-4, #444)',
|
|
93
|
+
'var(--a8c-charts-color-series-5, #555)',
|
|
94
|
+
] );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
// The literal is only reached where getComputedStyle resolves nothing, and the palette is the one field where falling back to the catalog default would be visible: every series would paint the same seeded blue.
|
|
98
|
+
it( 'keeps the consumer color as each pointer’s terminal literal, for SSR and jsdom', () => {
|
|
99
|
+
const merged = mergeThemes( defaultTheme, { colors: [ 'red' ] } as Partial< ChartTheme > );
|
|
100
|
+
const result = withCatalogPointers( merged, [ '--a8c-charts-color-series-1' ] );
|
|
101
|
+
|
|
102
|
+
expect( result.colors[ 0 ] ).toBe( 'var(--a8c-charts-color-series-1, red)' );
|
|
103
|
+
} );
|
|
47
104
|
} );
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SERIES_PALETTE_POINTERS } from './private/series-palette';
|
|
1
2
|
import type { CompleteChartTheme } from '../../types';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -5,10 +6,14 @@ import type { CompleteChartTheme } from '../../types';
|
|
|
5
6
|
*/
|
|
6
7
|
const defaultTheme: CompleteChartTheme = {
|
|
7
8
|
backgroundColor: 'var(--a8c-charts-color-background, #fff)',
|
|
8
|
-
labelBackgroundColor: '
|
|
9
|
-
// White label text sits on top of arbitrary series colors, so it has no WPDS content-foreground equivalent. Every other
|
|
9
|
+
labelBackgroundColor: 'var(--a8c-charts-color-label-background, transparent)',
|
|
10
|
+
// White label text sits on top of arbitrary series colors, so it has no WPDS content-foreground equivalent. Every other color 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.
|
|
10
11
|
labelTextColor: 'var(--a8c-charts-color-label-on-fill, #FFFFFF)',
|
|
11
|
-
|
|
12
|
+
// Derived, not authored: the slot manifest lives in `private/series-palette.ts` because which
|
|
13
|
+
// slots exist describes the catalog, not the theme. The field itself is still load-bearing —
|
|
14
|
+
// see that file for why the provider reads the palette through the theme and what has to move
|
|
15
|
+
// before this can be deleted.
|
|
16
|
+
colors: [ ...SERIES_PALETTE_POINTERS ],
|
|
12
17
|
gridStyles: {
|
|
13
18
|
stroke: 'var(--a8c-charts-color-grid, #dbdbdb)',
|
|
14
19
|
strokeWidth: 1,
|
|
@@ -1,41 +1,69 @@
|
|
|
1
1
|
// The single mapping point between the --a8c-charts-* catalog and WordPress
|
|
2
|
-
// design-system tokens.
|
|
2
|
+
// design-system tokens. Declares custom properties only — this stylesheet must
|
|
3
|
+
// never change layout.
|
|
3
4
|
|
|
4
|
-
// Declared on the GlobalChartsProvider wrapper
|
|
5
|
+
// Declared on the GlobalChartsProvider wrapper, not at :root, because
|
|
5
6
|
// @wordpress/theme's ThemeProvider emits its generated --wpds-* ramp as inline
|
|
6
|
-
// styles on its own wrapper div
|
|
7
|
-
// provider means a ThemeProvider above it is picked up; substituting at :root
|
|
8
|
-
// would miss it entirely.
|
|
7
|
+
// styles on its own wrapper div. Substituting at :root would miss it entirely.
|
|
9
8
|
|
|
10
|
-
// :where() keeps the rule at zero specificity so any consumer rule
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// the provider tree.
|
|
9
|
+
// :where() keeps the rule at zero specificity so any consumer rule targeting
|
|
10
|
+
// the same element wins. Chart roots deliberately do NOT re-declare these: an
|
|
11
|
+
// element's own declaration beats an inherited one, so re-declaring lower down
|
|
12
|
+
// would shadow a consumer override set inside the provider tree.
|
|
15
13
|
|
|
16
|
-
// The
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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.
|
|
14
|
+
// The roles a `theme` prop field can override read a `*-theme` variable first,
|
|
15
|
+
// which GlobalChartsProvider writes inline from that field. Publishing the
|
|
16
|
+
// consumer's value one layer out keeps the catalog default reachable: a value
|
|
17
|
+
// that is invalid at computed-value time (a fallback-less `var()` naming a
|
|
18
|
+
// token nothing set, which is how premium-analytics passes its grid stroke)
|
|
19
|
+
// invalidates only the `*-theme` variable, leaving the role's own fallback to
|
|
20
|
+
// resolve. Written as the role directly, it made the role guaranteed-invalid
|
|
21
|
+
// and took every bare read site with it.
|
|
26
22
|
// `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
23
|
:where(.a8c-charts-scope) {
|
|
24
|
+
// The series palette. GlobalChartsProvider resolves these five slots at its
|
|
25
|
+
// wrapper and seeds ColorCache with whatever resolves; a slot that resolves
|
|
26
|
+
// to nothing is skipped, so setting only slots 1 and 3 gives a two-color
|
|
27
|
+
// palette, and charts generate accessible colors beyond the seeds. Only
|
|
28
|
+
// slot 1 carries a default.
|
|
29
|
+
|
|
30
|
+
// Slot 1 names --wp-admin-theme-color first, because that is the only leg
|
|
31
|
+
// that does not depend on someone else's runtime. The design system's brand
|
|
32
|
+
// token tracks the admin color scheme only where a WPDS root provider
|
|
33
|
+
// boots and emits its generated ramp inline; without one the token falls to
|
|
34
|
+
// the plain stylesheet rule, a static #3858e9 with no reference to the
|
|
35
|
+
// admin color. Leading with the admin color costs nothing:
|
|
36
|
+
// @wordpress/theme writes --wp-admin-theme-color from a provider's own
|
|
37
|
+
// accent as a legacy wp-admin override, so a provider accent still wins.
|
|
38
|
+
|
|
39
|
+
// DO NOT SIMPLIFY the chain below. The innermost
|
|
40
|
+
// var(--wp-admin-theme-color, #3858e9) is unreachable — the outer leg
|
|
41
|
+
// already tried that variable — and reads like a copy-paste mistake. It is
|
|
42
|
+
// the design system's own spec value for the WPDS token, which
|
|
43
|
+
// `src/styles/test/wpds-fallbacks.test.ts` compares against literally, so
|
|
44
|
+
// collapsing it to a bare hex fails that test. Reordering to put the WPDS
|
|
45
|
+
// token first passes every test and breaks only on a host with no root
|
|
46
|
+
// provider.
|
|
47
|
+
|
|
48
|
+
// That test scans this file as text, comments included, so do not write an
|
|
49
|
+
// example --wpds-* reference in a comment here — it is read as a real one.
|
|
50
|
+
--a8c-charts-color-series-1: var(--a8c-charts-color-series-1-theme, var(--wp-admin-theme-color, var(--wpds-color-foreground-interactive-brand, var(--wp-admin-theme-color, #3858e9))));
|
|
51
|
+
--a8c-charts-color-series-2: var(--a8c-charts-color-series-2-theme);
|
|
52
|
+
--a8c-charts-color-series-3: var(--a8c-charts-color-series-3-theme);
|
|
53
|
+
--a8c-charts-color-series-4: var(--a8c-charts-color-series-4-theme);
|
|
54
|
+
--a8c-charts-color-series-5: var(--a8c-charts-color-series-5-theme);
|
|
30
55
|
--a8c-charts-color-grid: var(--a8c-charts-color-grid-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
31
56
|
--a8c-charts-color-axis: var(--a8c-charts-color-axis-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
32
57
|
--a8c-charts-color-tick: var(--a8c-charts-color-tick-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
33
58
|
--a8c-charts-color-label: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
34
59
|
--a8c-charts-color-label-secondary: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
35
60
|
--a8c-charts-color-label-inverse: var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0);
|
|
36
|
-
// White label text sits on arbitrary series
|
|
61
|
+
// White label text sits on arbitrary series colors, so it has no WPDS
|
|
37
62
|
// content-foreground equivalent.
|
|
38
|
-
--a8c-charts-color-label-on-fill: #fff;
|
|
63
|
+
--a8c-charts-color-label-on-fill: var(--a8c-charts-color-label-on-fill-theme, #fff);
|
|
64
|
+
// The plate behind a pie-slice label. Transparent by default, so a label
|
|
65
|
+
// reads straight off the slice; there is no WPDS token for "no surface".
|
|
66
|
+
--a8c-charts-color-label-background: var(--a8c-charts-color-label-background-theme, transparent);
|
|
39
67
|
// The SVG axis and tick labels. `svgLabelSmall.fill` publishes THIS role, so
|
|
40
68
|
// deriving from --a8c-charts-color-label is safe and wanted: the theme field
|
|
41
69
|
// moves only these labels, while setting the broad role in CSS still moves
|
|
@@ -20,16 +20,19 @@ const normalize = ( value: string ): string => value.replace( /\s+/g, ' ' ).trim
|
|
|
20
20
|
/**
|
|
21
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
22
|
*
|
|
23
|
+
* A role with no catalog default reads its layer with no fallback at all — the empty series-palette slots — and unwraps to nothing, which is what it resolves to until a consumer sets it.
|
|
24
|
+
*
|
|
23
25
|
* @param name - The declared property name.
|
|
24
26
|
* @param value - Its normalized value.
|
|
25
|
-
* @return The value with a leading `var(<name>-theme, … )` wrapper removed.
|
|
27
|
+
* @return The value with a leading `var(<name>-theme, … )` wrapper removed, or the empty string where that wrapper carried no fallback.
|
|
26
28
|
*/
|
|
27
29
|
function stripThemeLayer( name: string, value: string ): string {
|
|
28
|
-
const layered = new RegExp(
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const layered = new RegExp(
|
|
31
|
+
`^var\\(\\s*${ themeLayerVar( name ) }\\s*(?:,\\s*(.*))?\\)$`,
|
|
32
|
+
's'
|
|
33
|
+
).exec( value );
|
|
31
34
|
|
|
32
|
-
return layered ? layered[ 1 ].trim() : value;
|
|
35
|
+
return layered ? ( layered[ 1 ] ?? '' ).trim() : value;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -55,6 +58,13 @@ function parseStylesheet(): Map< string, Entry > {
|
|
|
55
58
|
|
|
56
59
|
const name = declaration.slice( 0, separator ).trim();
|
|
57
60
|
const value = stripThemeLayer( name, normalize( declaration.slice( separator + 1 ) ) );
|
|
61
|
+
|
|
62
|
+
// A role that unwrapped to nothing maps to nothing and falls back to nothing.
|
|
63
|
+
if ( value === '' ) {
|
|
64
|
+
entries.set( name, { reads: null, fallback: null } );
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
const wrapped = /^var\(\s*(.*)\)$/s.exec( value );
|
|
59
69
|
|
|
60
70
|
if ( ! wrapped ) {
|
|
@@ -112,10 +122,13 @@ function parseTokensDoc(): Map< string, Entry > {
|
|
|
112
122
|
// "no mapping" before looking for a token name inside it.
|
|
113
123
|
const unmapped = mapping.startsWith( '_(none' );
|
|
114
124
|
const derives = /derives from (--[\w-]+)/.exec( mapping );
|
|
115
|
-
|
|
125
|
+
// `--wp-*` rather than `--wpds-*`: the series palette maps to WordPress's own
|
|
126
|
+
// `--wp-admin-theme-color`, since the design system's brand token is a static
|
|
127
|
+
// hex on every admin screen and cannot carry the admin color scheme.
|
|
128
|
+
const token = /(--wp[\w-]+)/.exec( mapping );
|
|
116
129
|
|
|
117
130
|
entries.set( role, {
|
|
118
|
-
reads: unmapped ? null : derives?.[ 1 ] ??
|
|
131
|
+
reads: unmapped ? null : derives?.[ 1 ] ?? token?.[ 1 ] ?? null,
|
|
119
132
|
fallback: fallback === '—' ? null : normalize( fallback ),
|
|
120
133
|
} );
|
|
121
134
|
}
|
|
@@ -140,13 +153,13 @@ describe( 'chart scope catalog', () => {
|
|
|
140
153
|
// 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
154
|
it.each( THEME_LAYERED_ROLES )( 'declares %s reading its theme layer first', role => {
|
|
142
155
|
expect( stylesheet ).toMatch(
|
|
143
|
-
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s
|
|
156
|
+
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*[,)]` )
|
|
144
157
|
);
|
|
145
158
|
} );
|
|
146
159
|
|
|
147
160
|
it( 'gives a theme layer to the overridable roles and to nothing else', () => {
|
|
148
161
|
const layered = [ ...declared.keys() ].filter( role =>
|
|
149
|
-
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s
|
|
162
|
+
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*[,)]` ).test( stylesheet )
|
|
150
163
|
);
|
|
151
164
|
|
|
152
165
|
expect( layered.sort() ).toEqual( [ ...THEME_LAYERED_ROLES ].sort() );
|