@automattic/charts 3.2.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/SECURITY.md +1 -0
  3. package/TOKENS.md +53 -58
  4. package/dist/index.cjs +1483 -664
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.css +93 -29
  7. package/dist/index.d.cts +233 -247
  8. package/dist/index.d.ts +233 -247
  9. package/dist/index.js +1484 -667
  10. package/dist/index.js.map +1 -1
  11. package/package.json +7 -6
  12. package/src/charts/area-chart/area-chart.tsx +39 -25
  13. package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
  14. package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
  15. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
  16. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
  17. package/src/charts/area-chart/types.ts +4 -7
  18. package/src/charts/bar-chart/bar-chart.tsx +2 -1
  19. package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
  20. package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
  21. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
  22. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
  23. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
  24. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
  25. package/src/charts/geo-chart/geo-chart.module.scss +1 -0
  26. package/src/charts/geo-chart/geo-chart.tsx +5 -10
  27. package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
  28. package/src/charts/heatmap-chart/heatmap-chart.module.scss +52 -16
  29. package/src/charts/heatmap-chart/heatmap-chart.tsx +89 -37
  30. package/src/charts/heatmap-chart/index.ts +2 -1
  31. package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -89
  32. package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
  33. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
  34. package/src/charts/heatmap-chart/private/index.ts +1 -1
  35. package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
  36. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
  37. package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
  38. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +142 -12
  39. package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
  40. package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
  41. package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
  42. package/src/charts/heatmap-chart/types.ts +8 -2
  43. package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
  44. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
  45. package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
  46. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
  47. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
  48. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
  49. package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
  50. package/src/charts/leaderboard-chart/types.ts +2 -2
  51. package/src/charts/line-chart/line-chart.module.scss +10 -18
  52. package/src/charts/line-chart/line-chart.tsx +128 -26
  53. package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
  54. package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
  55. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
  56. package/src/charts/line-chart/test/line-chart.test.tsx +174 -41
  57. package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
  58. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
  59. package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
  60. package/src/charts/line-chart/types.ts +17 -3
  61. package/src/charts/pie-chart/pie-chart.module.scss +13 -1
  62. package/src/charts/pie-chart/pie-chart.tsx +30 -40
  63. package/src/charts/pie-chart/test/pie-chart.test.tsx +59 -12
  64. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
  65. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
  66. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
  67. package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
  68. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  69. package/src/charts/private/test/axis-formatting.test.tsx +85 -0
  70. package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
  71. package/src/charts/private/test/time-axis-options.test.ts +267 -0
  72. package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
  73. package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
  74. package/src/charts/private/test/time-axis.test.ts +9 -69
  75. package/src/charts/private/time-axis-options.ts +106 -0
  76. package/src/charts/private/time-axis.ts +318 -136
  77. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  78. package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
  79. package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
  80. package/src/charts/sparkline/types.ts +1 -1
  81. package/src/components/legend/private/base-legend.module.scss +9 -5
  82. package/src/components/tooltip/accessible-tooltip.tsx +21 -17
  83. package/src/components/tooltip/base-tooltip.module.scss +3 -3
  84. package/src/components/tooltip/private/bounded-tooltip.tsx +227 -0
  85. package/src/components/tooltip/test/accessible-tooltip.test.tsx +30 -4
  86. package/src/components/tooltip/test/bounded-tooltip.test.tsx +225 -0
  87. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +379 -0
  88. package/src/components/tooltip/xy-chart-tooltip.tsx +326 -0
  89. package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
  90. package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
  91. package/src/hooks/test/use-chart-margin.test.tsx +173 -3
  92. package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
  93. package/src/hooks/use-chart-data-transform.ts +10 -3
  94. package/src/hooks/use-chart-margin.tsx +54 -9
  95. package/src/hooks/use-xychart-theme.ts +25 -9
  96. package/src/index.ts +8 -0
  97. package/src/providers/chart-context/global-charts-provider.tsx +37 -38
  98. package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
  99. package/src/providers/chart-context/index.ts +1 -0
  100. package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
  101. package/src/providers/chart-context/private/series-palette.ts +3 -13
  102. package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
  103. package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
  104. package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
  105. package/src/providers/chart-context/themes.ts +7 -52
  106. package/src/providers/chart-context/types.ts +3 -0
  107. package/src/styles/chart-scope.scss +39 -62
  108. package/src/styles/test/chart-scope.test.ts +19 -47
  109. package/src/test-utils/series-fixtures.ts +31 -0
  110. package/src/test-utils/zoned-parsing-suite.ts +90 -0
  111. package/src/types.ts +79 -166
  112. package/src/utils/bucket-info.ts +87 -0
  113. package/src/utils/color-utils.ts +0 -27
  114. package/src/utils/date-formatting.ts +157 -0
  115. package/src/utils/date-parsing.ts +103 -52
  116. package/src/utils/get-edge-tick-widths.ts +33 -0
  117. package/src/utils/index.ts +4 -0
  118. package/src/utils/test/bucket-info.test.ts +185 -0
  119. package/src/utils/test/color-utils.test.ts +1 -48
  120. package/src/utils/test/date-formatting.test.ts +89 -0
  121. package/src/utils/test/date-parsing.auckland.test.ts +6 -0
  122. package/src/utils/test/date-parsing.santiago.test.ts +6 -0
  123. package/src/utils/test/date-parsing.test.ts +16 -1
  124. package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
  125. package/src/utils/test/get-styles.test.ts +10 -7
  126. package/src/utils/test/merge-themes.test.ts +18 -46
  127. package/src/utils/warn-once.ts +23 -0
  128. package/src/visx/types.ts +56 -5
  129. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
  130. package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
  131. package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
  132. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
  133. package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
  134. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
  135. package/src/styles/test/wpds-fallbacks.test.ts +0 -131
@@ -1,183 +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
- labelBackgroundColor: '#556',
33
- labelTextColor: '#557',
34
- colors: [ '#661', '#662', '#663', '#664', '#665' ],
35
- } )
36
- ).toEqual( {
37
- vars: {
38
- '--a8c-charts-color-background-theme': '#111',
39
- '--a8c-charts-color-grid-theme': '#222',
40
- '--a8c-charts-color-axis-theme': '#333',
41
- '--a8c-charts-color-tick-theme': '#444',
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',
50
- },
51
- roles: [
52
- '--a8c-charts-color-background',
53
- '--a8c-charts-color-grid',
54
- '--a8c-charts-color-axis',
55
- '--a8c-charts-color-tick',
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',
64
- ],
65
- } );
66
- } );
67
-
68
- it( 'publishes only theme-layer variables, never a catalog role directly', () => {
69
- const { vars } = themeOverrideVars( {
70
- backgroundColor: '#111',
71
- gridStyles: { stroke: '#222' },
72
- xAxisLineStyles: { stroke: '#333' },
73
- xTickLineStyles: { stroke: '#444' },
74
- svgLabelSmall: { fill: '#555' },
75
- labelBackgroundColor: '#556',
76
- labelTextColor: '#557',
77
- colors: [ '#661', '#662', '#663', '#664', '#665' ],
78
- } );
79
-
80
- expect( Object.keys( vars ) ).toEqual( THEME_LAYERED_ROLES.map( themeLayerVar ) );
81
- } );
82
-
83
- // 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.
84
- it( 'publishes an svg label override as the narrow axis-label role, not the broad one', () => {
85
- expect( themeOverrideVars( { svgLabelSmall: { fill: 'red' } } ) ).toEqual( {
86
- vars: { '--a8c-charts-color-label-axis-theme': 'red' },
87
- roles: [ '--a8c-charts-color-label-axis' ],
88
- } );
89
- } );
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
-
136
- it( 'ignores a value that is itself a pointer at the role it would override', () => {
137
- expect(
138
- themeOverrideVars( { gridStyles: { stroke: 'var(--a8c-charts-color-grid, #dbdbdb)' } } ).vars
139
- ).toEqual( {} );
140
- } );
141
-
142
- // 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.
143
- it( 'still reports the role of a value it refuses to publish', () => {
144
- expect(
145
- themeOverrideVars( {
146
- gridStyles: { stroke: 'var(--brand, var(--a8c-charts-color-grid, red))' },
147
- } )
148
- ).toEqual( { vars: {}, roles: [ '--a8c-charts-color-grid' ] } );
149
- } );
150
-
151
- // `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.
152
- it( 'ignores a self-referential pointer padded with whitespace', () => {
153
- expect(
154
- themeOverrideVars( { gridStyles: { stroke: 'var( --a8c-charts-color-grid, #dbdbdb )' } } )
155
- .vars
156
- ).toEqual( {} );
157
- } );
158
-
159
- it( 'still emits a value that merely mentions a different role', () => {
160
- expect(
161
- themeOverrideVars( { gridStyles: { stroke: 'var( --a8c-charts-color-axis, #dbdbdb )' } } )
162
- .vars
163
- ).toEqual( {
164
- '--a8c-charts-color-grid-theme': 'var( --a8c-charts-color-axis, #dbdbdb )',
165
- } );
166
- } );
167
-
168
- // 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.
169
- it( 'does not mistake a longer property name for the role it guards', () => {
170
- expect(
171
- themeOverrideVars( {
172
- xAxisLineStyles: { stroke: 'var(--a8c-charts-color-axis-emphasis, #000)' },
173
- } ).vars
174
- ).toEqual( { '--a8c-charts-color-axis-theme': 'var(--a8c-charts-color-axis-emphasis, #000)' } );
175
- } );
176
-
177
- // `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.
178
- it( 'ignores a self-referential pointer written as a bare custom-property name', () => {
179
- expect(
180
- themeOverrideVars( { gridStyles: { stroke: '--a8c-charts-color-grid' } } ).vars
181
- ).toEqual( {} );
182
- } );
183
- } );
@@ -1,104 +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
- labelBackgroundColor: '#556',
15
- labelTextColor: '#557',
16
- } as Partial< ChartTheme > );
17
-
18
- const result = withCatalogPointers( merged, [
19
- '--a8c-charts-color-background',
20
- '--a8c-charts-color-grid',
21
- '--a8c-charts-color-axis',
22
- '--a8c-charts-color-tick',
23
- '--a8c-charts-color-label-axis',
24
- '--a8c-charts-color-label-background',
25
- '--a8c-charts-color-label-on-fill',
26
- ] );
27
-
28
- expect( result.backgroundColor ).toBe( defaultTheme.backgroundColor );
29
- expect( result.gridStyles.stroke ).toBe( defaultTheme.gridStyles.stroke );
30
- expect( result.xAxisLineStyles.stroke ).toBe( defaultTheme.xAxisLineStyles.stroke );
31
- expect( result.xTickLineStyles.stroke ).toBe( defaultTheme.xTickLineStyles.stroke );
32
- expect( result.svgLabelSmall.fill ).toBe( defaultTheme.svgLabelSmall.fill );
33
- expect( result.labelBackgroundColor ).toBe( defaultTheme.labelBackgroundColor );
34
- expect( result.labelTextColor ).toBe( defaultTheme.labelTextColor );
35
-
36
- expect( merged.backgroundColor ).toBe( '#111' );
37
- expect( merged.gridStyles.stroke ).toBe( '#222' );
38
- } );
39
-
40
- it( 'leaves fields alone when no role was overridden', () => {
41
- const merged = mergeThemes( defaultTheme, {
42
- tickLength: 8,
43
- leaderboardChart: { deltaColors: [ 'red', 'grey', 'green' ] },
44
- } as Partial< ChartTheme > );
45
-
46
- const result = withCatalogPointers( merged, [] );
47
-
48
- expect( result.tickLength ).toBe( 8 );
49
- expect( result.gridStyles.stroke ).toBe( defaultTheme.gridStyles.stroke );
50
- // Not a mapped role, so its literal survives even when other roles are being restored.
51
- expect( result.leaderboardChart.deltaColors ).toEqual( [ 'red', 'grey', 'green' ] );
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
- } );
104
- } );
@@ -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
- } );