@automattic/charts 2.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/TOKENS.md +109 -50
- package/dist/index.cjs +529 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +118 -55
- package/dist/index.d.cts +90 -48
- package/dist/index.d.ts +90 -48
- package/dist/index.js +527 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.module.scss +8 -3
- package/src/charts/area-chart/area-chart.tsx +25 -25
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +301 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.module.scss +14 -6
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/test/bar-chart.test.tsx +255 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +23 -18
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
- package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/geo-chart/geo-chart.tsx +12 -4
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +22 -9
- package/src/charts/heatmap-chart/heatmap-chart.tsx +43 -13
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
- package/src/charts/heatmap-chart/types.ts +8 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/leaderboard-chart/types.ts +0 -2
- package/src/charts/line-chart/line-chart.module.scss +12 -7
- package/src/charts/line-chart/line-chart.tsx +32 -28
- package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +262 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +12 -14
- package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +12 -14
- package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
- package/src/charts/private/chart-instance-context/index.ts +3 -0
- package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
- package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
- package/src/components/legend/legend.tsx +4 -4
- package/src/components/legend/private/base-legend.module.scss +1 -1
- package/src/components/legend/private/base-legend.tsx +70 -19
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +17 -4
- package/src/components/tooltip/base-tooltip.tsx +8 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +20 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +4 -7
- package/src/components/trend-indicator/trend-indicator.tsx +3 -0
- package/src/hooks/index.ts +1 -1
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +19 -13
- package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/hooks/use-xychart-theme.ts +6 -4
- package/src/index.ts +12 -3
- package/src/providers/chart-context/global-charts-provider.tsx +127 -30
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
- package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -40
- package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
- package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
- package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
- package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
- package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
- package/src/providers/chart-context/themes.ts +18 -27
- package/src/providers/chart-context/types.ts +7 -4
- package/src/providers/chart-scope/chart-scope-context.ts +8 -0
- package/src/providers/chart-scope/index.ts +3 -0
- package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
- package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
- package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
- package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
- package/src/providers/index.ts +1 -0
- package/src/styles/chart-scope-class.ts +6 -0
- package/src/styles/chart-scope.scss +83 -0
- package/src/styles/test/chart-scope.test.ts +165 -0
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -34
- package/src/charts/private/grid-control/grid-control.module.scss +0 -8
- package/src/charts/private/grid-control/grid-control.tsx +0 -26
- package/src/charts/private/grid-control/index.ts +0 -1
- package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { ChartScopeContext, useChartScopeElement } from '../index';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
describe( 'useChartScopeElement', () => {
|
|
6
|
+
// The default has to stay null rather than `document.documentElement`: a chart with no scope above it must resolve nothing and fall back to its theme literal, not silently read the document root again.
|
|
7
|
+
it( 'returns null with no scope above it', () => {
|
|
8
|
+
const { result } = renderHook( () => useChartScopeElement() );
|
|
9
|
+
|
|
10
|
+
expect( result.current ).toBeNull();
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
it( 'returns the enclosing scope element', () => {
|
|
14
|
+
const scope = document.createElement( 'div' );
|
|
15
|
+
|
|
16
|
+
const { result } = renderHook( () => useChartScopeElement(), {
|
|
17
|
+
wrapper: ( { children }: { children: ReactNode } ) => (
|
|
18
|
+
<ChartScopeContext.Provider value={ scope }>{ children }</ChartScopeContext.Provider>
|
|
19
|
+
),
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
expect( result.current ).toBe( scope );
|
|
23
|
+
} );
|
|
24
|
+
} );
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { CHART_SCOPE_CLASS } from '../../../styles/chart-scope-class';
|
|
3
|
+
import { GlobalChartsProvider } from '../../chart-context/global-charts-provider';
|
|
4
|
+
import { useStandaloneScopeClass } from '../use-standalone-scope-class';
|
|
5
|
+
|
|
6
|
+
describe( 'useStandaloneScopeClass', () => {
|
|
7
|
+
it( 'returns the scope class with no provider above it', () => {
|
|
8
|
+
const { result } = renderHook( () => useStandaloneScopeClass() );
|
|
9
|
+
|
|
10
|
+
expect( result.current ).toBe( CHART_SCOPE_CLASS );
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
it( 'returns undefined inside a GlobalChartsProvider', () => {
|
|
14
|
+
const { result } = renderHook( () => useStandaloneScopeClass(), {
|
|
15
|
+
wrapper: ( { children } ) => <GlobalChartsProvider>{ children }</GlobalChartsProvider>,
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
expect( result.current ).toBeUndefined();
|
|
19
|
+
} );
|
|
20
|
+
} );
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ChartScopeContext } from './chart-scope-context';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the nearest chart scope element, or `null` when rendered outside any scope (and during SSR, before the ref is attached).
|
|
6
|
+
*
|
|
7
|
+
* @return {HTMLElement | null} The nearest chart scope element, or `null`.
|
|
8
|
+
*/
|
|
9
|
+
export const useChartScopeElement = (): HTMLElement | null => useContext( ChartScopeContext );
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
3
|
+
import { GlobalChartsContext } from '../chart-context/global-charts-provider';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the catalog class when there is no `GlobalChartsProvider` above this component, and `undefined` when there is.
|
|
7
|
+
*
|
|
8
|
+
* Standalone components need the catalog to resolve at all, but inside a provider the tokens already inherit — re-declaring them there would shadow a consumer override set anywhere in the tree.
|
|
9
|
+
*
|
|
10
|
+
* @return The scope class name, or undefined when a provider is present.
|
|
11
|
+
*/
|
|
12
|
+
export const useStandaloneScopeClass = (): string | undefined =>
|
|
13
|
+
useContext( GlobalChartsContext ) ? undefined : CHART_SCOPE_CLASS;
|
package/src/providers/index.ts
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable class that carries the `--a8c-charts-*` catalog. Applied to the `GlobalChartsProvider` wrapper, to portal-rendered surfaces (which sit outside the provider tree), and to standalone components when no provider is above them.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately unhashed: consumers are documented as being able to target it to override a token on the provider element.
|
|
5
|
+
*/
|
|
6
|
+
export const CHART_SCOPE_CLASS = 'a8c-charts-scope';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The single mapping point between the --a8c-charts-* catalog and WordPress
|
|
2
|
+
// design-system tokens.
|
|
3
|
+
|
|
4
|
+
// Declared on the GlobalChartsProvider wrapper — not at :root — because
|
|
5
|
+
// @wordpress/theme's ThemeProvider emits its generated --wpds-* ramp as inline
|
|
6
|
+
// styles on its own wrapper div, not on the document root. Substituting at the
|
|
7
|
+
// provider means a ThemeProvider above it is picked up; substituting at :root
|
|
8
|
+
// would miss it entirely.
|
|
9
|
+
|
|
10
|
+
// :where() keeps the rule at zero specificity so any consumer rule
|
|
11
|
+
// targeting the same element wins. Chart roots deliberately do NOT
|
|
12
|
+
// re-declare these: an element's own declaration beats an inherited one,
|
|
13
|
+
// so re-declaring lower down would shadow a consumer override set inside
|
|
14
|
+
// the provider tree.
|
|
15
|
+
|
|
16
|
+
// The five roles a `theme` prop field can override read a `*-theme` variable
|
|
17
|
+
// first, which GlobalChartsProvider writes inline from that field. Publishing
|
|
18
|
+
// the consumer's value one layer out, rather than as the role itself, is what
|
|
19
|
+
// keeps the catalog default reachable: a value that is invalid at
|
|
20
|
+
// computed-value time (a fallback-less `var()` naming a token nothing set,
|
|
21
|
+
// which is how premium-analytics passes its grid stroke) invalidates only the
|
|
22
|
+
// `*-theme` variable, and the role's own fallback still resolves the mapped
|
|
23
|
+
// --wpds-* token. Written as the role directly, that same value made the role
|
|
24
|
+
// guaranteed-invalid and took every bare `var(--a8c-charts-color-grid)` read
|
|
25
|
+
// site with it — gridlines painted `unset` instead of the spec grey.
|
|
26
|
+
// `src/styles/test/chart-scope.test.ts` pins which roles carry the layer.
|
|
27
|
+
|
|
28
|
+
// Declares custom properties only. This stylesheet must never change layout.
|
|
29
|
+
:where(.a8c-charts-scope) {
|
|
30
|
+
--a8c-charts-color-grid: var(--a8c-charts-color-grid-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
31
|
+
--a8c-charts-color-axis: var(--a8c-charts-color-axis-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
32
|
+
--a8c-charts-color-tick: var(--a8c-charts-color-tick-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
|
|
33
|
+
--a8c-charts-color-label: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
34
|
+
--a8c-charts-color-label-secondary: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
35
|
+
--a8c-charts-color-label-inverse: var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0);
|
|
36
|
+
// White label text sits on arbitrary series colours, so it has no WPDS
|
|
37
|
+
// content-foreground equivalent.
|
|
38
|
+
--a8c-charts-color-label-on-fill: #fff;
|
|
39
|
+
// The SVG axis and tick labels. `svgLabelSmall.fill` publishes THIS role, so
|
|
40
|
+
// deriving from --a8c-charts-color-label is safe and wanted: the theme field
|
|
41
|
+
// moves only these labels, while setting the broad role in CSS still moves
|
|
42
|
+
// every label at once. (--a8c-charts-color-surface cannot derive the same
|
|
43
|
+
// way — see its comment; there the theme field owns the broad role.)
|
|
44
|
+
--a8c-charts-color-label-axis: var(--a8c-charts-color-label-axis-theme, var(--a8c-charts-color-label));
|
|
45
|
+
--a8c-charts-color-annotation: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
46
|
+
--a8c-charts-color-trend-up: var(--wpds-color-foreground-content-success-weak, #008030);
|
|
47
|
+
--a8c-charts-color-trend-down: var(--wpds-color-foreground-content-error-weak, #cc1818);
|
|
48
|
+
--a8c-charts-color-trend-neutral: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
49
|
+
--a8c-charts-color-background: var(--a8c-charts-color-background-theme, var(--wpds-color-background-surface-neutral-strong, #fff));
|
|
50
|
+
// Surfaces that float over the chart — tooltips, the annotation label, the
|
|
51
|
+
// zoom-reset control. Same WPDS token as --a8c-charts-color-background, and
|
|
52
|
+
// deliberately NOT derived from it: `backgroundColor` publishes the
|
|
53
|
+
// background role, so deriving would let that theme field repaint these too.
|
|
54
|
+
// Sibling of the background role, not a child of it.
|
|
55
|
+
--a8c-charts-color-surface: var(--wpds-color-background-surface-neutral-strong, #fff);
|
|
56
|
+
--a8c-charts-color-surface-secondary: var(--wpds-color-background-surface-neutral-weak, #f4f4f4);
|
|
57
|
+
--a8c-charts-color-track: var(--wpds-color-background-track-neutral-weak, #f0f0f0);
|
|
58
|
+
// Translucent dark surface; no WPDS token carries this.
|
|
59
|
+
--a8c-charts-color-tooltip-surface: rgb(0 0 0 / 85%);
|
|
60
|
+
--a8c-charts-color-zoom-selection: var(--wpds-color-background-interactive-brand-strong, var(--wp-admin-theme-color, #3858e9));
|
|
61
|
+
--a8c-charts-color-zoom-selection-stroke: var(--wpds-color-stroke-interactive-brand, var(--wp-admin-theme-color, #3858e9));
|
|
62
|
+
|
|
63
|
+
// @wordpress/theme 1.0.0 removed the whole --wpds-elevation-* group while the
|
|
64
|
+
// elevation model is redefined (gutenberg#80099). These carry the removed spec
|
|
65
|
+
// values literally until a replacement token exists.
|
|
66
|
+
--a8c-charts-elevation-xs:
|
|
67
|
+
0 1px 1px 0 #00000008,
|
|
68
|
+
0 1px 2px 0 #00000005,
|
|
69
|
+
0 3px 3px 0 #00000005,
|
|
70
|
+
0 4px 4px 0 #00000003;
|
|
71
|
+
--a8c-charts-elevation-sm:
|
|
72
|
+
0 1px 2px 0 #0000000d,
|
|
73
|
+
0 2px 3px 0 #0000000a,
|
|
74
|
+
0 6px 6px 0 #00000008,
|
|
75
|
+
0 8px 8px 0 #00000005;
|
|
76
|
+
|
|
77
|
+
--a8c-charts-motion-duration-entrance: var(--wpds-motion-duration-xl, 400ms);
|
|
78
|
+
--a8c-charts-motion-easing-entrance: var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1));
|
|
79
|
+
--a8c-charts-border-radius-bar: var(--wpds-border-radius-md, 4px);
|
|
80
|
+
--a8c-charts-border-radius-cell: var(--wpds-border-radius-sm, 2px);
|
|
81
|
+
// Pill shape; no WPDS radius reaches it.
|
|
82
|
+
--a8c-charts-border-radius-leaderboard-bar: 9999px;
|
|
83
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
THEME_LAYERED_ROLES,
|
|
5
|
+
themeLayerVar,
|
|
6
|
+
} from '../../providers/chart-context/private/theme-override-vars';
|
|
7
|
+
|
|
8
|
+
const stylesheet = readFileSync( join( __dirname, '..', 'chart-scope.scss' ), 'utf8' );
|
|
9
|
+
const tokensDoc = readFileSync( join( __dirname, '..', '..', '..', 'TOKENS.md' ), 'utf8' );
|
|
10
|
+
|
|
11
|
+
type Entry = {
|
|
12
|
+
/** The custom property this one reads, or null when the value is a literal. */
|
|
13
|
+
reads: string | null;
|
|
14
|
+
/** The `var()` fallback, or the whole value when it reads nothing. */
|
|
15
|
+
fallback: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const normalize = ( value: string ): string => value.replace( /\s+/g, ' ' ).trim();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Unwraps the theme layer a `theme`-prop-overridable role carries, so the tables in `TOKENS.md` keep documenting what a role ultimately resolves to — its `--wpds-*` token and spec fallback — rather than restating the override plumbing in every row. Which roles carry the layer is pinned separately below.
|
|
22
|
+
*
|
|
23
|
+
* @param name - The declared property name.
|
|
24
|
+
* @param value - Its normalized value.
|
|
25
|
+
* @return The value with a leading `var(<name>-theme, … )` wrapper removed.
|
|
26
|
+
*/
|
|
27
|
+
function stripThemeLayer( name: string, value: string ): string {
|
|
28
|
+
const layered = new RegExp( `^var\\(\\s*${ themeLayerVar( name ) }\\s*,\\s*(.*)\\)$`, 's' ).exec(
|
|
29
|
+
value
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return layered ? layered[ 1 ].trim() : value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parses each catalog declaration in the stylesheet into the property it reads and the fallback it carries. Splitting the `var()` arguments at the *first* comma keeps a fallback that is itself a function call intact — `cubic-bezier(0.25, 0, 0, 1)` would otherwise be truncated at its first argument.
|
|
37
|
+
*
|
|
38
|
+
* @return Every declared `--a8c-charts-*` property, keyed by name.
|
|
39
|
+
*/
|
|
40
|
+
function parseStylesheet(): Map< string, Entry > {
|
|
41
|
+
const body = stylesheet.slice( stylesheet.indexOf( '{' ) + 1, stylesheet.lastIndexOf( '}' ) );
|
|
42
|
+
const entries = new Map< string, Entry >();
|
|
43
|
+
|
|
44
|
+
for ( const declaration of body
|
|
45
|
+
.split( '\n' )
|
|
46
|
+
.map( line => line.trim() )
|
|
47
|
+
.filter( line => line && ! line.startsWith( '//' ) )
|
|
48
|
+
.join( '\n' )
|
|
49
|
+
.split( ';' ) ) {
|
|
50
|
+
const separator = declaration.indexOf( ':' );
|
|
51
|
+
|
|
52
|
+
if ( separator === -1 ) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const name = declaration.slice( 0, separator ).trim();
|
|
57
|
+
const value = stripThemeLayer( name, normalize( declaration.slice( separator + 1 ) ) );
|
|
58
|
+
const wrapped = /^var\(\s*(.*)\)$/s.exec( value );
|
|
59
|
+
|
|
60
|
+
if ( ! wrapped ) {
|
|
61
|
+
entries.set( name, { reads: null, fallback: value } );
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const args = wrapped[ 1 ];
|
|
66
|
+
const comma = args.indexOf( ',' );
|
|
67
|
+
|
|
68
|
+
entries.set(
|
|
69
|
+
name,
|
|
70
|
+
comma === -1
|
|
71
|
+
? { reads: args.trim(), fallback: null }
|
|
72
|
+
: { reads: args.slice( 0, comma ).trim(), fallback: normalize( args.slice( comma + 1 ) ) }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parses the catalog tables in `TOKENS.md` — the ones whose header is `Role | Maps to …` — into the same shape as the stylesheet, so the two can be compared directly. Other tables in the document (theme-field mappings, deprecated aliases) are skipped.
|
|
81
|
+
*
|
|
82
|
+
* @return Every documented role, keyed by name.
|
|
83
|
+
*/
|
|
84
|
+
function parseTokensDoc(): Map< string, Entry > {
|
|
85
|
+
const entries = new Map< string, Entry >();
|
|
86
|
+
const lines = tokensDoc.split( '\n' );
|
|
87
|
+
let inCatalogTable = false;
|
|
88
|
+
|
|
89
|
+
for ( const line of lines ) {
|
|
90
|
+
if ( ! line.trim().startsWith( '|' ) ) {
|
|
91
|
+
inCatalogTable = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cells = line
|
|
96
|
+
.split( '|' )
|
|
97
|
+
.slice( 1, -1 )
|
|
98
|
+
.map( cell => cell.trim() );
|
|
99
|
+
|
|
100
|
+
if ( cells[ 0 ] === 'Role' ) {
|
|
101
|
+
inCatalogTable = cells[ 1 ]?.startsWith( 'Maps to' ) ?? false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ( ! inCatalogTable || cells.length !== 3 || /^-+$/.test( cells[ 0 ] ) ) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const [ role, mapping, fallback ] = cells.map( cell => cell.replace( /`/g, '' ) );
|
|
110
|
+
// A "none" cell explains *why* there is no mapping, and that explanation can
|
|
111
|
+
// name a `--wpds-*` token itself (the removed elevation group). Read it as
|
|
112
|
+
// "no mapping" before looking for a token name inside it.
|
|
113
|
+
const unmapped = mapping.startsWith( '_(none' );
|
|
114
|
+
const derives = /derives from (--[\w-]+)/.exec( mapping );
|
|
115
|
+
const wpds = /(--wpds-[\w-]+)/.exec( mapping );
|
|
116
|
+
|
|
117
|
+
entries.set( role, {
|
|
118
|
+
reads: unmapped ? null : derives?.[ 1 ] ?? wpds?.[ 1 ] ?? null,
|
|
119
|
+
fallback: fallback === '—' ? null : normalize( fallback ),
|
|
120
|
+
} );
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return entries;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const declared = parseStylesheet();
|
|
127
|
+
const documented = parseTokensDoc();
|
|
128
|
+
|
|
129
|
+
describe( 'chart scope catalog', () => {
|
|
130
|
+
it( 'documents every declared role, and declares every documented one', () => {
|
|
131
|
+
expect( [ ...documented.keys() ].sort() ).toEqual( [ ...declared.keys() ].sort() );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
// The tables in TOKENS.md restate the stylesheet for consumers who only have the
|
|
135
|
+
// published package. This is the check that stops the two drifting apart.
|
|
136
|
+
it.each( [ ...declared.keys() ] )( 'documents %s with the value it is declared with', token => {
|
|
137
|
+
expect( documented.get( token ) ).toEqual( declared.get( token ) );
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
// The layer is what keeps a `theme` prop override from being able to take its role down with it: an override that is invalid at computed-value time only invalidates `<role>-theme`, and the role's own fallback still resolves the mapped token. Drop the layer from a role and that role's overrides go back to blanking every read site.
|
|
141
|
+
it.each( THEME_LAYERED_ROLES )( 'declares %s reading its theme layer first', role => {
|
|
142
|
+
expect( stylesheet ).toMatch(
|
|
143
|
+
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*,` )
|
|
144
|
+
);
|
|
145
|
+
} );
|
|
146
|
+
|
|
147
|
+
it( 'gives a theme layer to the overridable roles and to nothing else', () => {
|
|
148
|
+
const layered = [ ...declared.keys() ].filter( role =>
|
|
149
|
+
new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*,` ).test( stylesheet )
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect( layered.sort() ).toEqual( [ ...THEME_LAYERED_ROLES ].sort() );
|
|
153
|
+
} );
|
|
154
|
+
|
|
155
|
+
it( 'scopes the catalog to :where(.a8c-charts-scope) rather than :root', () => {
|
|
156
|
+
expect( stylesheet ).toMatch( /:where\(\.a8c-charts-scope\)\s*{/ );
|
|
157
|
+
expect( stylesheet ).not.toMatch( /(^|\s):root\s*{/ );
|
|
158
|
+
} );
|
|
159
|
+
|
|
160
|
+
it( 'declares custom properties only, so this stylesheet cannot change layout', () => {
|
|
161
|
+
expect( [ ...declared.keys() ].every( name => name.startsWith( '--a8c-charts-' ) ) ).toBe(
|
|
162
|
+
true
|
|
163
|
+
);
|
|
164
|
+
} );
|
|
165
|
+
} );
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
} );
|
package/src/types.ts
CHANGED
|
@@ -639,6 +639,18 @@ export type SeriesChartLegendConfig = ChartLegendConfig< SeriesData[] > & {
|
|
|
639
639
|
collapseGroups?: boolean;
|
|
640
640
|
};
|
|
641
641
|
|
|
642
|
+
/**
|
|
643
|
+
* Initial visibility options for charts built from labelled series.
|
|
644
|
+
*/
|
|
645
|
+
export interface SeriesVisibilityProps {
|
|
646
|
+
/**
|
|
647
|
+
* Series labels to hide from the first defined value. User changes persist until
|
|
648
|
+
* the chart remounts or its ID changes; later values for the same ID are ignored.
|
|
649
|
+
* Omit to retain the provider's existing visibility for the chart ID.
|
|
650
|
+
*/
|
|
651
|
+
defaultHiddenSeries?: readonly string[];
|
|
652
|
+
}
|
|
653
|
+
|
|
642
654
|
/**
|
|
643
655
|
* Base properties shared across all chart components
|
|
644
656
|
*/
|
|
@@ -736,40 +748,6 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
|
|
|
736
748
|
};
|
|
737
749
|
};
|
|
738
750
|
|
|
739
|
-
/**
|
|
740
|
-
* Properties for grid components
|
|
741
|
-
*/
|
|
742
|
-
export type GridProps = {
|
|
743
|
-
/**
|
|
744
|
-
* Width of the grid in pixels
|
|
745
|
-
*/
|
|
746
|
-
width: number;
|
|
747
|
-
/**
|
|
748
|
-
* Height of the grid in pixels
|
|
749
|
-
*/
|
|
750
|
-
height: number;
|
|
751
|
-
/**
|
|
752
|
-
* Grid visibility. x is default.
|
|
753
|
-
*/
|
|
754
|
-
gridVisibility?: 'x' | 'y' | 'xy' | 'none';
|
|
755
|
-
/**
|
|
756
|
-
* X-axis scale for the grid
|
|
757
|
-
* TODO: Fix any type after resolving visx scale type issues
|
|
758
|
-
*/
|
|
759
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
760
|
-
xScale: any;
|
|
761
|
-
/**
|
|
762
|
-
* Y-axis scale for the grid
|
|
763
|
-
* TODO: Fix any type after resolving visx scale type issues
|
|
764
|
-
*/
|
|
765
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
766
|
-
yScale: any;
|
|
767
|
-
/**
|
|
768
|
-
* Top offset for the grid
|
|
769
|
-
*/
|
|
770
|
-
top?: number;
|
|
771
|
-
};
|
|
772
|
-
|
|
773
751
|
/**
|
|
774
752
|
* Local type definitions for Popover API attributes and events
|
|
775
753
|
* These are used to avoid extending React module types while still getting type safety
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { GridRows, GridColumns } from '@visx/grid';
|
|
2
|
-
import styles from './grid-control.module.scss';
|
|
3
|
-
import type { GridProps } from '../../../types';
|
|
4
|
-
import type { FC } from 'react';
|
|
5
|
-
|
|
6
|
-
const GridControl: FC< GridProps > = ( {
|
|
7
|
-
width,
|
|
8
|
-
height,
|
|
9
|
-
xScale,
|
|
10
|
-
yScale,
|
|
11
|
-
gridVisibility = 'x',
|
|
12
|
-
top = 0,
|
|
13
|
-
} ) => {
|
|
14
|
-
return (
|
|
15
|
-
<g transform={ `translate(0, ${ top })` } className={ styles[ 'grid-control' ] }>
|
|
16
|
-
{ gridVisibility.includes( 'x' ) && (
|
|
17
|
-
<GridRows scale={ xScale } width={ width } data-testid="x-grid" />
|
|
18
|
-
) }
|
|
19
|
-
{ gridVisibility.includes( 'y' ) && (
|
|
20
|
-
<GridColumns scale={ yScale } height={ height } data-testid="y-grid" />
|
|
21
|
-
) }
|
|
22
|
-
</g>
|
|
23
|
-
);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export default GridControl;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as GridControl } from './grid-control';
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { scaleBand, scaleLinear } from '@visx/scale';
|
|
3
|
-
import GridControl from '../grid-control';
|
|
4
|
-
|
|
5
|
-
describe( 'GridControl', () => {
|
|
6
|
-
const defaultProps = {
|
|
7
|
-
width: 200,
|
|
8
|
-
height: 200,
|
|
9
|
-
xScale: scaleBand( { domain: [ 'A', 'B', 'C' ], range: [ 0, 100 ] } ),
|
|
10
|
-
yScale: scaleLinear( { domain: [ 0, 100 ], range: [ 100, 0 ] } ),
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
test( 'renders x-axis grid lines', () => {
|
|
14
|
-
render(
|
|
15
|
-
<svg>
|
|
16
|
-
<GridControl { ...defaultProps } gridVisibility="x" />
|
|
17
|
-
</svg>
|
|
18
|
-
);
|
|
19
|
-
const xGridLines = screen.getAllByTestId( 'x-grid' );
|
|
20
|
-
expect( xGridLines.length ).toBeGreaterThan( 0 );
|
|
21
|
-
expect( screen.queryAllByTestId( 'y-grid' ) ).toHaveLength( 0 );
|
|
22
|
-
} );
|
|
23
|
-
|
|
24
|
-
test( 'renders y-axis grid lines', () => {
|
|
25
|
-
render(
|
|
26
|
-
<svg>
|
|
27
|
-
<GridControl { ...defaultProps } gridVisibility="y" />
|
|
28
|
-
</svg>
|
|
29
|
-
);
|
|
30
|
-
expect( screen.queryAllByTestId( 'x-grid' ) ).toHaveLength( 0 );
|
|
31
|
-
const yGridLines = screen.getAllByTestId( 'y-grid' );
|
|
32
|
-
expect( yGridLines.length ).toBeGreaterThan( 0 );
|
|
33
|
-
} );
|
|
34
|
-
|
|
35
|
-
test( 'renders both axes grid lines', () => {
|
|
36
|
-
render(
|
|
37
|
-
<svg>
|
|
38
|
-
<GridControl { ...defaultProps } gridVisibility="xy" />
|
|
39
|
-
</svg>
|
|
40
|
-
);
|
|
41
|
-
const xGridLines = screen.getAllByTestId( 'x-grid' );
|
|
42
|
-
const yGridLines = screen.getAllByTestId( 'y-grid' );
|
|
43
|
-
expect( xGridLines.length ).toBeGreaterThan( 0 );
|
|
44
|
-
expect( yGridLines.length ).toBeGreaterThan( 0 );
|
|
45
|
-
} );
|
|
46
|
-
|
|
47
|
-
test( 'renders no grid lines when visibility is none', () => {
|
|
48
|
-
render(
|
|
49
|
-
<svg>
|
|
50
|
-
<GridControl { ...defaultProps } gridVisibility="none" />
|
|
51
|
-
</svg>
|
|
52
|
-
);
|
|
53
|
-
expect( screen.queryAllByTestId( 'x-grid' ) ).toHaveLength( 0 );
|
|
54
|
-
expect( screen.queryAllByTestId( 'y-grid' ) ).toHaveLength( 0 );
|
|
55
|
-
} );
|
|
56
|
-
} );
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { SingleChartContext, ChartInstanceContext } from './single-chart-context';
|
|
2
|
-
export { useChartInstanceContext, useSingleChartContext } from './use-single-chart-context';
|
|
3
|
-
export type {
|
|
4
|
-
ChartInstanceContextValue,
|
|
5
|
-
ChartInstanceRef,
|
|
6
|
-
SingleChartContextValue,
|
|
7
|
-
SingleChartRef,
|
|
8
|
-
} from './single-chart-context';
|