@automattic/charts 3.1.1 → 4.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 +41 -0
- package/TOKENS.md +80 -46
- package/dist/index.cjs +1224 -662
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -23
- package/dist/index.d.cts +230 -162
- package/dist/index.d.ts +230 -162
- package/dist/index.js +1224 -663
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +38 -11
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +67 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +79 -0
- package/src/charts/bar-list-chart/bar-list-chart.tsx +45 -2
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +38 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
- package/src/charts/heatmap-chart/types.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +109 -25
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +74 -3
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/types.ts +4 -1
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/sparkline.tsx +9 -3
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +2 -2
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +38 -10
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +67 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +17 -10
- package/src/hooks/test/use-xychart-theme.test.tsx +63 -5
- package/src/hooks/use-xychart-theme.ts +33 -23
- package/src/index.ts +4 -0
- package/src/providers/chart-context/global-charts-provider.tsx +64 -54
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +35 -0
- package/src/providers/chart-context/test/catalog-pointers.test.ts +61 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -18
- package/src/providers/chart-context/themes.ts +7 -47
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +63 -58
- package/src/styles/test/chart-scope.test.ts +25 -40
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/types.ts +103 -73
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +174 -0
- package/src/utils/get-styles.ts +7 -1
- package/src/utils/index.ts +3 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/visx/types.ts +29 -2
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -87
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -50
- package/src/providers/chart-context/test/sparse-theme.test.ts +0 -35
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -118
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -47
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
} );
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
} );
|
|
@@ -1,118 +0,0 @@
|
|
|
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
|
-
} );
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
} );
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { globSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import dsTokenFallbacks from '@wordpress/theme/postcss-plugins/postcss-ds-token-fallbacks';
|
|
4
|
-
|
|
5
|
-
const SOURCE = join( __dirname, '..', '..' );
|
|
6
|
-
|
|
7
|
-
// @wordpress/theme publishes its token map only through this plugin. The `Declaration`
|
|
8
|
-
// visitor rewrites a declaration's `value` in place and reads nothing else off the node,
|
|
9
|
-
// so a synthetic node is enough to ask it what a token is worth. It throws on a token the
|
|
10
|
-
// installed design system does not define, which is how a rename surfaces here.
|
|
11
|
-
const { Declaration: rewriteValue } = dsTokenFallbacks() as {
|
|
12
|
-
Declaration: ( declaration: { value: string } ) => void;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const normalize = ( value: string ): string => value.replace( /\s+/g, ' ' ).trim();
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Reports the fallback expression the installed design system defines for a token.
|
|
19
|
-
*
|
|
20
|
-
* @param token - A `--wpds-*` custom property name.
|
|
21
|
-
* @return The design system's value for it.
|
|
22
|
-
*/
|
|
23
|
-
function specFallback( token: string ): string {
|
|
24
|
-
const declaration = { value: `var(${ token })` };
|
|
25
|
-
rewriteValue( declaration );
|
|
26
|
-
|
|
27
|
-
// The plugin is expected to throw on a token the design system does not define. Should a
|
|
28
|
-
// future version return the reference untouched instead, slicing off a prefix that isn't
|
|
29
|
-
// there would yield a fragment of the token name and fail some later assertion with a
|
|
30
|
-
// value nothing wrote. Fail here, naming the token, rather than there.
|
|
31
|
-
const prefix = `var(${ token }, `;
|
|
32
|
-
|
|
33
|
-
if ( ! declaration.value.startsWith( prefix ) || ! declaration.value.endsWith( ')' ) ) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`@wordpress/theme did not supply a fallback for ${ token } — got "${ declaration.value }".`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return declaration.value.slice( prefix.length, -1 );
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type Reference = {
|
|
43
|
-
file: string;
|
|
44
|
-
token: string;
|
|
45
|
-
/** The written fallback, or null where the reference is bare. */
|
|
46
|
-
fallback: string | null;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Finds every `--wpds-*` reference in a file. Scanning for the closing parenthesis by depth, rather than matching a regex, keeps a fallback that is itself a function call or a nested `var()` intact — `cubic-bezier(0.15, 0, 0.15, 1)` and `var(--wp-admin-theme-color, #3858e9)` both end at a parenthesis an inner call opened.
|
|
51
|
-
*
|
|
52
|
-
* @param source - The file's contents.
|
|
53
|
-
* @param file - Its path, relative to `src`.
|
|
54
|
-
* @return Every reference the file makes.
|
|
55
|
-
*/
|
|
56
|
-
function referencesIn( source: string, file: string ): Reference[] {
|
|
57
|
-
const opener = /var\(\s*(--wpds-[\w-]+)\s*/g;
|
|
58
|
-
const references: Reference[] = [];
|
|
59
|
-
let match;
|
|
60
|
-
|
|
61
|
-
while ( ( match = opener.exec( source ) ) !== null ) {
|
|
62
|
-
const token = match[ 1 ];
|
|
63
|
-
|
|
64
|
-
if ( source[ opener.lastIndex ] !== ',' ) {
|
|
65
|
-
references.push( { file, token, fallback: null } );
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let index = opener.lastIndex + 1;
|
|
70
|
-
let depth = 1;
|
|
71
|
-
|
|
72
|
-
while ( index < source.length && depth > 0 ) {
|
|
73
|
-
if ( source[ index ] === '(' ) {
|
|
74
|
-
depth++;
|
|
75
|
-
} else if ( source[ index ] === ')' ) {
|
|
76
|
-
depth--;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if ( depth > 0 ) {
|
|
80
|
-
index++;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
references.push( {
|
|
85
|
-
file,
|
|
86
|
-
token,
|
|
87
|
-
fallback: normalize( source.slice( opener.lastIndex + 1, index ) ),
|
|
88
|
-
} );
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return references;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const references = globSync( '**/*.{scss,css,ts,tsx,mdx}', { cwd: SOURCE } )
|
|
95
|
-
// Paths are relative to `src`, so a top-level `src/test/` arrives without a leading slash.
|
|
96
|
-
.filter( file => ! /(^|\/)test\//.test( file ) )
|
|
97
|
-
.flatMap( file => referencesIn( readFileSync( join( SOURCE, file ), 'utf8' ), file ) );
|
|
98
|
-
|
|
99
|
-
describe( 'design token references', () => {
|
|
100
|
-
it( 'finds the references, so a broken scan cannot pass as a clean one', () => {
|
|
101
|
-
expect( references.length ).toBeGreaterThan( 30 );
|
|
102
|
-
} );
|
|
103
|
-
|
|
104
|
-
// Every fallback here is hand-written, because both of this package's consumption paths
|
|
105
|
-
// have to carry it: `@wordpress/build` apps read `dist/`, while webpack apps resolve the
|
|
106
|
-
// SCSS through the `jetpack:src` export condition and compile it themselves. Neither
|
|
107
|
-
// build injects fallbacks, so a bare reference in a stylesheet renders as nothing at all
|
|
108
|
-
// in a host that has not loaded the design-system stylesheet — and WordPress ships none.
|
|
109
|
-
//
|
|
110
|
-
// A bare reference in TS can be deliberate: `stories/theme-config.tsx` seeds a series
|
|
111
|
-
// colour from a token `ThemeProvider` generates, where resolving to nothing is the
|
|
112
|
-
// wanted behaviour when no provider is above the chart.
|
|
113
|
-
it( 'writes a fallback for every token a stylesheet reads', () => {
|
|
114
|
-
expect(
|
|
115
|
-
references.filter(
|
|
116
|
-
reference => reference.fallback === null && /\.(scss|css)$/.test( reference.file )
|
|
117
|
-
)
|
|
118
|
-
).toEqual( [] );
|
|
119
|
-
} );
|
|
120
|
-
|
|
121
|
-
// This is the check that keeps a hand-written value honest. It is what `499` — a font
|
|
122
|
-
// weight that was spec once and drifted — needed and did not have.
|
|
123
|
-
it( 'writes the design system value as that fallback', () => {
|
|
124
|
-
const drifted = references
|
|
125
|
-
.filter( reference => reference.fallback !== null )
|
|
126
|
-
.map( reference => ( { ...reference, spec: normalize( specFallback( reference.token ) ) } ) )
|
|
127
|
-
.filter( reference => reference.fallback !== reference.spec );
|
|
128
|
-
|
|
129
|
-
expect( drifted ).toEqual( [] );
|
|
130
|
-
} );
|
|
131
|
-
} );
|