@automattic/charts 3.0.0 → 3.1.1
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 +18 -0
- package/TOKENS.md +3 -3
- package/dist/index.cjs +250 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +23 -14
- package/dist/index.d.cts +51 -14
- package/dist/index.d.ts +51 -14
- package/dist/index.js +251 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +26 -26
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +346 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +19 -12
- package/src/charts/bar-chart/test/bar-chart.test.tsx +277 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +6 -6
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +31 -8
- 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.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/line-chart/line-chart.tsx +33 -29
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +313 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +8 -13
- 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 +8 -13
- 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/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- 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 +67 -18
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +13 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +1 -1
- package/src/hooks/index.ts +1 -1
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/index.ts +3 -1
- package/src/providers/chart-context/global-charts-provider.tsx +92 -18
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -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/types.ts +7 -1
- package/src/styles/chart-scope.scss +2 -2
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -0
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -44,6 +44,10 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
44
44
|
const [ hiddenSeries, setHiddenSeries ] = useState< Map< string, Set< string > > >(
|
|
45
45
|
() => new Map()
|
|
46
46
|
);
|
|
47
|
+
// Which charts have had their defaults seeded. Kept apart from `hiddenSeries`
|
|
48
|
+
// because that map drops a chart's entry once its hidden set empties, so an
|
|
49
|
+
// absent entry cannot tell "never seeded" from "the reader revealed the lot".
|
|
50
|
+
const seededCharts = useRef< Set< string > >( new Set() );
|
|
47
51
|
|
|
48
52
|
// Ref to the wrapper element for resolving scoped CSS variables
|
|
49
53
|
const wrapperRef = useRef< HTMLDivElement >( null );
|
|
@@ -242,28 +246,90 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
242
246
|
[ providerTheme, resolveColor ]
|
|
243
247
|
);
|
|
244
248
|
|
|
245
|
-
//
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
// Keep hidden-series updates and no-op detection consistent across setters.
|
|
250
|
+
const updateHiddenSeries = useCallback(
|
|
251
|
+
( chartId: string, update: ( current: Set< string > ) => Set< string > ) => {
|
|
252
|
+
setHiddenSeries( prev => {
|
|
253
|
+
const current = prev.get( chartId );
|
|
254
|
+
const next = update( new Set( current ?? [] ) );
|
|
251
255
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
if ( ! current && next.size === 0 ) {
|
|
257
|
+
return prev;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Preserve identity for no-op updates.
|
|
261
|
+
if (
|
|
262
|
+
current &&
|
|
263
|
+
current.size === next.size &&
|
|
264
|
+
[ ...next ].every( label => current.has( label ) )
|
|
265
|
+
) {
|
|
266
|
+
return prev;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const newMap = new Map( prev );
|
|
270
|
+
if ( next.size === 0 ) {
|
|
271
|
+
newMap.delete( chartId );
|
|
272
|
+
} else {
|
|
273
|
+
newMap.set( chartId, next );
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return newMap;
|
|
277
|
+
} );
|
|
278
|
+
},
|
|
279
|
+
[]
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const toggleSeriesVisibility = useCallback(
|
|
283
|
+
( chartId: string, seriesLabel: string ) => {
|
|
284
|
+
updateHiddenSeries( chartId, current => {
|
|
285
|
+
if ( current.has( seriesLabel ) ) {
|
|
286
|
+
current.delete( seriesLabel );
|
|
287
|
+
} else {
|
|
288
|
+
current.add( seriesLabel );
|
|
289
|
+
}
|
|
290
|
+
return current;
|
|
291
|
+
} );
|
|
292
|
+
},
|
|
293
|
+
[ updateHiddenSeries ]
|
|
294
|
+
);
|
|
257
295
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
296
|
+
const setSeriesVisibility = useCallback(
|
|
297
|
+
( chartId: string, seriesLabel: string, visible: boolean ) => {
|
|
298
|
+
updateHiddenSeries( chartId, current => {
|
|
299
|
+
if ( visible ) {
|
|
300
|
+
current.delete( seriesLabel );
|
|
301
|
+
} else {
|
|
302
|
+
current.add( seriesLabel );
|
|
303
|
+
}
|
|
304
|
+
return current;
|
|
305
|
+
} );
|
|
306
|
+
},
|
|
307
|
+
[ updateHiddenSeries ]
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const setChartHiddenSeries = useCallback(
|
|
311
|
+
( chartId: string, seriesLabels: readonly string[] ) => {
|
|
312
|
+
updateHiddenSeries( chartId, () => new Set( seriesLabels ) );
|
|
313
|
+
},
|
|
314
|
+
[ updateHiddenSeries ]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const hasSeededChart = useCallback(
|
|
318
|
+
( chartId: string ) => seededCharts.current.has( chartId ),
|
|
319
|
+
[]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const seedChartHiddenSeries = useCallback(
|
|
323
|
+
( chartId: string, seriesLabels: readonly string[] ) => {
|
|
324
|
+
if ( seededCharts.current.has( chartId ) ) {
|
|
325
|
+
return;
|
|
262
326
|
}
|
|
263
327
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
328
|
+
seededCharts.current.add( chartId );
|
|
329
|
+
setChartHiddenSeries( chartId, seriesLabels );
|
|
330
|
+
},
|
|
331
|
+
[ setChartHiddenSeries ]
|
|
332
|
+
);
|
|
267
333
|
|
|
268
334
|
const isSeriesVisible = useCallback(
|
|
269
335
|
( chartId: string, seriesLabel: string ) => {
|
|
@@ -290,6 +356,10 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
290
356
|
theme: providerTheme,
|
|
291
357
|
getElementStyles,
|
|
292
358
|
toggleSeriesVisibility,
|
|
359
|
+
setSeriesVisibility,
|
|
360
|
+
setChartHiddenSeries,
|
|
361
|
+
seedChartHiddenSeries,
|
|
362
|
+
hasSeededChart,
|
|
293
363
|
isSeriesVisible,
|
|
294
364
|
getHiddenSeries,
|
|
295
365
|
isColorPaletteResolved,
|
|
@@ -302,6 +372,10 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
302
372
|
providerTheme,
|
|
303
373
|
getElementStyles,
|
|
304
374
|
toggleSeriesVisibility,
|
|
375
|
+
setSeriesVisibility,
|
|
376
|
+
setChartHiddenSeries,
|
|
377
|
+
seedChartHiddenSeries,
|
|
378
|
+
hasSeededChart,
|
|
305
379
|
isSeriesVisible,
|
|
306
380
|
getHiddenSeries,
|
|
307
381
|
isColorPaletteResolved,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { useGlobalChartsContext } from './use-global-charts-context';
|
|
3
|
+
|
|
4
|
+
// Hide seeded series before paint without triggering React's SSR warning.
|
|
5
|
+
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Seeds a chart's hidden series once per chart ID, the first time the provider
|
|
9
|
+
* sees that ID.
|
|
10
|
+
*
|
|
11
|
+
* The provider owns the record of what has been seeded, not this hook: a
|
|
12
|
+
* mount-scoped one would re-apply the defaults on every remount, and a chart
|
|
13
|
+
* remounts for reasons the reader never thinks of as a new chart — switching
|
|
14
|
+
* between line and bar, tabbing away and back, a resize flipping tabs into a
|
|
15
|
+
* dropdown. Each of those would throw away the series they had just revealed.
|
|
16
|
+
*
|
|
17
|
+
* @param chartId - The chart ID passed to the provider's visibility methods.
|
|
18
|
+
* @param seriesLabels - Labels to hide when this chart ID is first seen. Later values are ignored.
|
|
19
|
+
* @return The resolved hidden-series set.
|
|
20
|
+
*/
|
|
21
|
+
export const useDefaultHiddenSeries = (
|
|
22
|
+
chartId: string,
|
|
23
|
+
seriesLabels?: readonly string[]
|
|
24
|
+
): Set< string > => {
|
|
25
|
+
const { getHiddenSeries, seedChartHiddenSeries, hasSeededChart } = useGlobalChartsContext();
|
|
26
|
+
// Read through a ref so the effect never depends on a new array identity.
|
|
27
|
+
const labelsRef = useRef( seriesLabels );
|
|
28
|
+
labelsRef.current = seriesLabels;
|
|
29
|
+
const hasSeriesLabels = seriesLabels !== undefined;
|
|
30
|
+
const shouldUseDefaults = hasSeriesLabels && ! hasSeededChart( chartId );
|
|
31
|
+
|
|
32
|
+
useIsomorphicLayoutEffect( () => {
|
|
33
|
+
if ( labelsRef.current === undefined ) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
seedChartHiddenSeries( chartId, labelsRef.current );
|
|
37
|
+
}, [ chartId, hasSeriesLabels, seedChartHiddenSeries ] );
|
|
38
|
+
|
|
39
|
+
// getHiddenSeries hands out a fresh copy per call, so hold the set still between
|
|
40
|
+
// visibility changes. Charts derive their rendered series from it, and a new
|
|
41
|
+
// identity every render invalidates every memo downstream — including the one
|
|
42
|
+
// the accessible tooltip watches, which closes a tooltip mid-navigation.
|
|
43
|
+
return useMemo(
|
|
44
|
+
() => ( shouldUseDefaults ? new Set( labelsRef.current ) : getHiddenSeries( chartId ) ),
|
|
45
|
+
[ shouldUseDefaults, getHiddenSeries, chartId ]
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -1728,6 +1728,148 @@ describe( 'ChartContext', () => {
|
|
|
1728
1728
|
// Should have empty set after all series are visible
|
|
1729
1729
|
expect( hidden.size ).toBe( 0 );
|
|
1730
1730
|
} );
|
|
1731
|
+
|
|
1732
|
+
it( 'setSeriesVisibility sets state absolutely rather than toggling', () => {
|
|
1733
|
+
let contextValue: GlobalChartsContextValue;
|
|
1734
|
+
const TestComponent = () => {
|
|
1735
|
+
contextValue = useGlobalChartsContext();
|
|
1736
|
+
return <div>Test</div>;
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
render(
|
|
1740
|
+
<GlobalChartsProvider>
|
|
1741
|
+
<TestComponent />
|
|
1742
|
+
</GlobalChartsProvider>
|
|
1743
|
+
);
|
|
1744
|
+
|
|
1745
|
+
act( () => {
|
|
1746
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1747
|
+
} );
|
|
1748
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( false );
|
|
1749
|
+
|
|
1750
|
+
// Calling it again with the same value must not flip it back.
|
|
1751
|
+
act( () => {
|
|
1752
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1753
|
+
} );
|
|
1754
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( false );
|
|
1755
|
+
|
|
1756
|
+
act( () => {
|
|
1757
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', true );
|
|
1758
|
+
} );
|
|
1759
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( true );
|
|
1760
|
+
} );
|
|
1761
|
+
|
|
1762
|
+
it( 'setChartHiddenSeries replaces the hidden set rather than merging into it', () => {
|
|
1763
|
+
let contextValue: GlobalChartsContextValue;
|
|
1764
|
+
const TestComponent = () => {
|
|
1765
|
+
contextValue = useGlobalChartsContext();
|
|
1766
|
+
return <div>Test</div>;
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
render(
|
|
1770
|
+
<GlobalChartsProvider>
|
|
1771
|
+
<TestComponent />
|
|
1772
|
+
</GlobalChartsProvider>
|
|
1773
|
+
);
|
|
1774
|
+
|
|
1775
|
+
act( () => {
|
|
1776
|
+
contextValue.setChartHiddenSeries( 'test-chart', [ 'Series 1', 'Series 2' ] );
|
|
1777
|
+
} );
|
|
1778
|
+
expect( contextValue.getHiddenSeries( 'test-chart' ) ).toEqual(
|
|
1779
|
+
new Set( [ 'Series 1', 'Series 2' ] )
|
|
1780
|
+
);
|
|
1781
|
+
|
|
1782
|
+
act( () => {
|
|
1783
|
+
contextValue.setChartHiddenSeries( 'test-chart', [ 'Series 2' ] );
|
|
1784
|
+
} );
|
|
1785
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 1' ) ).toBe( true );
|
|
1786
|
+
expect( contextValue.isSeriesVisible( 'test-chart', 'Series 2' ) ).toBe( false );
|
|
1787
|
+
|
|
1788
|
+
act( () => {
|
|
1789
|
+
contextValue.setChartHiddenSeries( 'test-chart', [] );
|
|
1790
|
+
} );
|
|
1791
|
+
expect( contextValue.getHiddenSeries( 'test-chart' ) ).toEqual( new Set() );
|
|
1792
|
+
} );
|
|
1793
|
+
|
|
1794
|
+
it( 'setChartHiddenSeries leaves other charts alone', () => {
|
|
1795
|
+
let contextValue: GlobalChartsContextValue;
|
|
1796
|
+
const TestComponent = () => {
|
|
1797
|
+
contextValue = useGlobalChartsContext();
|
|
1798
|
+
return <div>Test</div>;
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
render(
|
|
1802
|
+
<GlobalChartsProvider>
|
|
1803
|
+
<TestComponent />
|
|
1804
|
+
</GlobalChartsProvider>
|
|
1805
|
+
);
|
|
1806
|
+
|
|
1807
|
+
act( () => {
|
|
1808
|
+
contextValue.setChartHiddenSeries( 'chart-a', [ 'Series 1' ] );
|
|
1809
|
+
contextValue.setChartHiddenSeries( 'chart-b', [ 'Series 2' ] );
|
|
1810
|
+
} );
|
|
1811
|
+
|
|
1812
|
+
expect( contextValue.isSeriesVisible( 'chart-a', 'Series 1' ) ).toBe( false );
|
|
1813
|
+
expect( contextValue.isSeriesVisible( 'chart-a', 'Series 2' ) ).toBe( true );
|
|
1814
|
+
expect( contextValue.isSeriesVisible( 'chart-b', 'Series 2' ) ).toBe( false );
|
|
1815
|
+
} );
|
|
1816
|
+
|
|
1817
|
+
it( 'a redundant setSeriesVisibility write does not change context identity or re-render', () => {
|
|
1818
|
+
let contextValue: GlobalChartsContextValue;
|
|
1819
|
+
let childUpdateTally = 0;
|
|
1820
|
+
const TestComponent = () => {
|
|
1821
|
+
contextValue = useGlobalChartsContext();
|
|
1822
|
+
childUpdateTally++;
|
|
1823
|
+
return <div>Test</div>;
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
render(
|
|
1827
|
+
<GlobalChartsProvider>
|
|
1828
|
+
<TestComponent />
|
|
1829
|
+
</GlobalChartsProvider>
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
act( () => {
|
|
1833
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1834
|
+
} );
|
|
1835
|
+
const isSeriesVisibleAfterFirstWrite = contextValue.isSeriesVisible;
|
|
1836
|
+
const tallyAfterFirstWrite = childUpdateTally;
|
|
1837
|
+
|
|
1838
|
+
// Redundant write: the series is already hidden, so this must be a no-op.
|
|
1839
|
+
act( () => {
|
|
1840
|
+
contextValue.setSeriesVisibility( 'test-chart', 'Series 1', false );
|
|
1841
|
+
} );
|
|
1842
|
+
|
|
1843
|
+
expect( contextValue.isSeriesVisible ).toBe( isSeriesVisibleAfterFirstWrite );
|
|
1844
|
+
expect( childUpdateTally ).toBe( tallyAfterFirstWrite );
|
|
1845
|
+
} );
|
|
1846
|
+
|
|
1847
|
+
it( 'setting an already-empty hidden set does not create then delete a Map entry', () => {
|
|
1848
|
+
let contextValue: GlobalChartsContextValue;
|
|
1849
|
+
let childUpdateTally = 0;
|
|
1850
|
+
const TestComponent = () => {
|
|
1851
|
+
contextValue = useGlobalChartsContext();
|
|
1852
|
+
childUpdateTally++;
|
|
1853
|
+
return <div>Test</div>;
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
render(
|
|
1857
|
+
<GlobalChartsProvider>
|
|
1858
|
+
<TestComponent />
|
|
1859
|
+
</GlobalChartsProvider>
|
|
1860
|
+
);
|
|
1861
|
+
|
|
1862
|
+
const tallyBeforeWrite = childUpdateTally;
|
|
1863
|
+
const isSeriesVisibleBefore = contextValue.isSeriesVisible;
|
|
1864
|
+
|
|
1865
|
+
// This chart has no hidden-series entry at all yet.
|
|
1866
|
+
act( () => {
|
|
1867
|
+
contextValue.setChartHiddenSeries( 'test-chart', [] );
|
|
1868
|
+
} );
|
|
1869
|
+
|
|
1870
|
+
expect( childUpdateTally ).toBe( tallyBeforeWrite );
|
|
1871
|
+
expect( contextValue.isSeriesVisible ).toBe( isSeriesVisibleBefore );
|
|
1872
|
+
} );
|
|
1731
1873
|
} );
|
|
1732
1874
|
|
|
1733
1875
|
describe( 'GlobalChartsProvider - CSS Variable Support', () => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { screen } from '@testing-library/react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { GlobalChartsProvider } from '../global-charts-provider';
|
|
5
|
+
import { useDefaultHiddenSeries } from '../hooks/use-default-hidden-series';
|
|
6
|
+
import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
|
|
7
|
+
import type { Root } from 'react-dom/client';
|
|
8
|
+
|
|
9
|
+
describe( 'useDefaultHiddenSeries paint timing', () => {
|
|
10
|
+
let host: HTMLDivElement;
|
|
11
|
+
let root: Root;
|
|
12
|
+
let previousActEnvironment: boolean | undefined;
|
|
13
|
+
|
|
14
|
+
beforeEach( () => {
|
|
15
|
+
previousActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
|
|
16
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = false;
|
|
17
|
+
host = document.createElement( 'div' );
|
|
18
|
+
document.body.appendChild( host );
|
|
19
|
+
root = createRoot( host );
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
afterEach( () => {
|
|
23
|
+
flushSync( () => root.unmount() );
|
|
24
|
+
host.remove();
|
|
25
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'updates provider visibility before the first paint', () => {
|
|
29
|
+
const Chart = () => {
|
|
30
|
+
useDefaultHiddenSeries( 'chart', [ 'Visitors' ] );
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const Visibility = () => {
|
|
35
|
+
const { isSeriesVisible } = useGlobalChartsContext();
|
|
36
|
+
return (
|
|
37
|
+
<span data-testid="series-visibility">
|
|
38
|
+
{ isSeriesVisible( 'chart', 'Visitors' ) ? 'visible' : 'hidden' }
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// flushSync lets us inspect the layout-effect result before a later passive
|
|
44
|
+
// effect could seed the provider.
|
|
45
|
+
flushSync( () => {
|
|
46
|
+
root.render(
|
|
47
|
+
<GlobalChartsProvider>
|
|
48
|
+
<Chart />
|
|
49
|
+
<Visibility />
|
|
50
|
+
</GlobalChartsProvider>
|
|
51
|
+
);
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
expect( screen.getByTestId( 'series-visibility' ) ).toHaveTextContent( 'hidden' );
|
|
55
|
+
} );
|
|
56
|
+
} );
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
+
import { useDefaultHiddenSeries } from '../hooks/use-default-hidden-series';
|
|
5
|
+
|
|
6
|
+
const mockGetHiddenSeries = jest.fn( () => new Set( [ 'Provider hidden' ] ) );
|
|
7
|
+
const mockSeedChartHiddenSeries = jest.fn();
|
|
8
|
+
const mockHasSeededChart = jest.fn( () => false );
|
|
9
|
+
|
|
10
|
+
jest.mock( '../hooks/use-global-charts-context', () => ( {
|
|
11
|
+
useGlobalChartsContext: () => ( {
|
|
12
|
+
getHiddenSeries: mockGetHiddenSeries,
|
|
13
|
+
seedChartHiddenSeries: mockSeedChartHiddenSeries,
|
|
14
|
+
hasSeededChart: mockHasSeededChart,
|
|
15
|
+
} ),
|
|
16
|
+
} ) );
|
|
17
|
+
|
|
18
|
+
describe( 'useDefaultHiddenSeries SSR', () => {
|
|
19
|
+
it( 'applies defaults to the server-rendered output before effects run', () => {
|
|
20
|
+
const Chart = () => {
|
|
21
|
+
const hiddenSeries = useDefaultHiddenSeries( 'chart', [ 'Visitors' ] );
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{ [ 'Visitors', 'Views' ].map( label =>
|
|
26
|
+
! hiddenSeries.has( label ) ? <span key={ label }>{ label }</span> : null
|
|
27
|
+
) }
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const view = renderToStaticMarkup( <Chart /> );
|
|
33
|
+
|
|
34
|
+
expect( view ).not.toContain( 'Visitors' );
|
|
35
|
+
expect( view ).toContain( 'Views' );
|
|
36
|
+
expect( mockGetHiddenSeries ).not.toHaveBeenCalled();
|
|
37
|
+
expect( mockSeedChartHiddenSeries ).not.toHaveBeenCalled();
|
|
38
|
+
} );
|
|
39
|
+
} );
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { render, act } from '@testing-library/react';
|
|
2
|
+
import { GlobalChartsProvider } from '../global-charts-provider';
|
|
3
|
+
import { useDefaultHiddenSeries } from '../hooks/use-default-hidden-series';
|
|
4
|
+
import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
|
|
5
|
+
import type { GlobalChartsContextValue } from '../types';
|
|
6
|
+
|
|
7
|
+
describe( 'useDefaultHiddenSeries', () => {
|
|
8
|
+
let contextValue: GlobalChartsContextValue;
|
|
9
|
+
let resolvedHiddenSeries: Set< string >;
|
|
10
|
+
|
|
11
|
+
const Grab = () => {
|
|
12
|
+
contextValue = useGlobalChartsContext();
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Chart = ( { defaults, chartId = 'chart' }: { defaults?: string[]; chartId?: string } ) => {
|
|
17
|
+
resolvedHiddenSeries = useDefaultHiddenSeries( chartId, defaults );
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it( 'hides the named series at mount', () => {
|
|
22
|
+
render(
|
|
23
|
+
<GlobalChartsProvider>
|
|
24
|
+
<Grab />
|
|
25
|
+
<Chart defaults={ [ 'Visitors' ] } />
|
|
26
|
+
</GlobalChartsProvider>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
30
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Views' ) ).toBe( true );
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
it( 'writes nothing when no defaults are given', () => {
|
|
34
|
+
render(
|
|
35
|
+
<GlobalChartsProvider>
|
|
36
|
+
<Grab />
|
|
37
|
+
<Chart />
|
|
38
|
+
</GlobalChartsProvider>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set() );
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
it( 'reflects provider visibility changes after mount', () => {
|
|
45
|
+
render(
|
|
46
|
+
<GlobalChartsProvider>
|
|
47
|
+
<Grab />
|
|
48
|
+
<Chart />
|
|
49
|
+
</GlobalChartsProvider>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect( resolvedHiddenSeries.has( 'Visitors' ) ).toBe( false );
|
|
53
|
+
|
|
54
|
+
act( () => {
|
|
55
|
+
contextValue.setSeriesVisibility( 'chart', 'Visitors', false );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
expect( resolvedHiddenSeries.has( 'Visitors' ) ).toBe( true );
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'seeds the first defined value and ignores later values for the same chart ID', () => {
|
|
62
|
+
const Harness = ( { defaults }: { defaults?: string[] } ) => (
|
|
63
|
+
<GlobalChartsProvider>
|
|
64
|
+
<Grab />
|
|
65
|
+
<Chart defaults={ defaults } />
|
|
66
|
+
</GlobalChartsProvider>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const { rerender } = render( <Harness /> );
|
|
70
|
+
|
|
71
|
+
rerender( <Harness defaults={ [ 'Visitors' ] } /> );
|
|
72
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set( [ 'Visitors' ] ) );
|
|
73
|
+
|
|
74
|
+
rerender( <Harness defaults={ [ 'Views' ] } /> );
|
|
75
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set( [ 'Visitors' ] ) );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'treats an empty array as a defined default', () => {
|
|
79
|
+
const Harness = ( { defaults }: { defaults: string[] } ) => (
|
|
80
|
+
<GlobalChartsProvider>
|
|
81
|
+
<Grab />
|
|
82
|
+
<Chart defaults={ defaults } />
|
|
83
|
+
</GlobalChartsProvider>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const { rerender } = render( <Harness defaults={ [] } /> );
|
|
87
|
+
|
|
88
|
+
rerender( <Harness defaults={ [ 'Visitors' ] } /> );
|
|
89
|
+
|
|
90
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set() );
|
|
91
|
+
} );
|
|
92
|
+
|
|
93
|
+
it( 'does not re-hide a series the user revealed', () => {
|
|
94
|
+
const Harness = () => (
|
|
95
|
+
<GlobalChartsProvider>
|
|
96
|
+
<Grab />
|
|
97
|
+
<Chart defaults={ [ 'Visitors' ] } />
|
|
98
|
+
</GlobalChartsProvider>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const { rerender } = render( <Harness /> );
|
|
102
|
+
|
|
103
|
+
act( () => {
|
|
104
|
+
contextValue.setSeriesVisibility( 'chart', 'Visitors', true );
|
|
105
|
+
} );
|
|
106
|
+
|
|
107
|
+
rerender( <Harness /> );
|
|
108
|
+
|
|
109
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( true );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( "keeps the reader's visibility choices across a remount", () => {
|
|
113
|
+
// The provider stays mounted; only the chart unmounts and comes back — a
|
|
114
|
+
// chart-type switch, a tab round-trip, a resize flipping tabs into a
|
|
115
|
+
// dropdown. None of those read as a new chart, so none may re-seed.
|
|
116
|
+
const Harness = ( { mounted }: { mounted: boolean } ) => (
|
|
117
|
+
<GlobalChartsProvider>
|
|
118
|
+
<Grab />
|
|
119
|
+
{ mounted && <Chart defaults={ [ 'Visitors' ] } /> }
|
|
120
|
+
</GlobalChartsProvider>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const { rerender } = render( <Harness mounted /> );
|
|
124
|
+
|
|
125
|
+
act( () => {
|
|
126
|
+
contextValue.setSeriesVisibility( 'chart', 'Views', false );
|
|
127
|
+
} );
|
|
128
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Views' ) ).toBe( false );
|
|
129
|
+
|
|
130
|
+
rerender( <Harness mounted={ false } /> );
|
|
131
|
+
rerender( <Harness mounted /> );
|
|
132
|
+
|
|
133
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
134
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Views' ) ).toBe( false );
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
it( 'leaves a fully revealed chart revealed after a remount', () => {
|
|
138
|
+
// Revealing the last hidden series empties the chart's hidden set, and the
|
|
139
|
+
// provider drops the entry with it. The seeded record has to survive that,
|
|
140
|
+
// or the defaults come back the next time the chart mounts.
|
|
141
|
+
const Harness = ( { mounted }: { mounted: boolean } ) => (
|
|
142
|
+
<GlobalChartsProvider>
|
|
143
|
+
<Grab />
|
|
144
|
+
{ mounted && <Chart defaults={ [ 'Visitors' ] } /> }
|
|
145
|
+
</GlobalChartsProvider>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const { rerender } = render( <Harness mounted /> );
|
|
149
|
+
|
|
150
|
+
act( () => {
|
|
151
|
+
contextValue.setSeriesVisibility( 'chart', 'Visitors', true );
|
|
152
|
+
} );
|
|
153
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set() );
|
|
154
|
+
|
|
155
|
+
rerender( <Harness mounted={ false } /> );
|
|
156
|
+
rerender( <Harness mounted /> );
|
|
157
|
+
|
|
158
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( true );
|
|
159
|
+
} );
|
|
160
|
+
|
|
161
|
+
it( 'retains provider visibility when remounted without defaults', () => {
|
|
162
|
+
const Harness = ( { defaults, mounted }: { defaults?: string[]; mounted: boolean } ) => (
|
|
163
|
+
<GlobalChartsProvider>
|
|
164
|
+
<Grab />
|
|
165
|
+
{ mounted && <Chart defaults={ defaults } /> }
|
|
166
|
+
</GlobalChartsProvider>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const { rerender } = render( <Harness defaults={ [ 'Visitors' ] } mounted /> );
|
|
170
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
171
|
+
|
|
172
|
+
rerender( <Harness mounted={ false } /> );
|
|
173
|
+
rerender( <Harness mounted /> );
|
|
174
|
+
|
|
175
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
it( 'keeps the same set across renders until visibility changes', () => {
|
|
179
|
+
// Charts memoize their rendered series off this set. A fresh identity per
|
|
180
|
+
// render invalidates those memos, which re-runs the accessible tooltip's
|
|
181
|
+
// effect and closes a tooltip the user is still navigating with.
|
|
182
|
+
const Harness = () => (
|
|
183
|
+
<GlobalChartsProvider>
|
|
184
|
+
<Grab />
|
|
185
|
+
<Chart defaults={ [ 'Visitors' ] } />
|
|
186
|
+
</GlobalChartsProvider>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const { rerender } = render( <Harness /> );
|
|
190
|
+
const seeded = resolvedHiddenSeries;
|
|
191
|
+
|
|
192
|
+
rerender( <Harness /> );
|
|
193
|
+
expect( resolvedHiddenSeries ).toBe( seeded );
|
|
194
|
+
|
|
195
|
+
act( () => {
|
|
196
|
+
contextValue.setSeriesVisibility( 'chart', 'Views', false );
|
|
197
|
+
} );
|
|
198
|
+
expect( resolvedHiddenSeries ).not.toBe( seeded );
|
|
199
|
+
} );
|
|
200
|
+
|
|
201
|
+
it( 're-seeds the defaults when chartId changes mid-mount', () => {
|
|
202
|
+
// Same mount, only the chartId prop changes (e.g. <LineChart chartId={ metric } ... />
|
|
203
|
+
// with `metric` going 'views' -> 'clicks'), so the component never unmounts.
|
|
204
|
+
const Harness = ( { chartId }: { chartId: string } ) => (
|
|
205
|
+
<GlobalChartsProvider>
|
|
206
|
+
<Grab />
|
|
207
|
+
<Chart chartId={ chartId } defaults={ [ 'Visitors' ] } />
|
|
208
|
+
</GlobalChartsProvider>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const { rerender } = render( <Harness chartId="views" /> );
|
|
212
|
+
|
|
213
|
+
expect( contextValue.isSeriesVisible( 'views', 'Visitors' ) ).toBe( false );
|
|
214
|
+
|
|
215
|
+
rerender( <Harness chartId="clicks" /> );
|
|
216
|
+
|
|
217
|
+
// The new chart id gets the same declared defaults seeded...
|
|
218
|
+
expect( contextValue.isSeriesVisible( 'clicks', 'Visitors' ) ).toBe( false );
|
|
219
|
+
// ...and the old id's hidden set is left as it was, not silently cleared.
|
|
220
|
+
expect( contextValue.isSeriesVisible( 'views', 'Visitors' ) ).toBe( false );
|
|
221
|
+
} );
|
|
222
|
+
} );
|
|
@@ -38,8 +38,14 @@ export interface GlobalChartsContextValue {
|
|
|
38
38
|
getChartData: ( id: string ) => ChartRegistration | undefined;
|
|
39
39
|
theme: CompleteChartTheme;
|
|
40
40
|
getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
|
|
41
|
-
// Series visibility management
|
|
41
|
+
// Series visibility management shared by charts, legends, and programmatic controls.
|
|
42
42
|
toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
|
|
43
|
+
setSeriesVisibility: ( chartId: string, seriesLabel: string, visible: boolean ) => void;
|
|
44
|
+
setChartHiddenSeries: ( chartId: string, seriesLabels: readonly string[] ) => void;
|
|
45
|
+
/** Applies a chart's defaults once. A no-op for a chart that has already been seeded. */
|
|
46
|
+
seedChartHiddenSeries: ( chartId: string, seriesLabels: readonly string[] ) => void;
|
|
47
|
+
/** Whether this chart's defaults have already been seeded, in this or an earlier mount. */
|
|
48
|
+
hasSeededChart: ( chartId: string ) => boolean;
|
|
43
49
|
isSeriesVisible: ( chartId: string, seriesLabel: string ) => boolean;
|
|
44
50
|
getHiddenSeries: ( chartId: string ) => Set< string >;
|
|
45
51
|
isColorPaletteResolved: boolean;
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
--a8c-charts-color-track: var(--wpds-color-background-track-neutral-weak, #f0f0f0);
|
|
58
58
|
// Translucent dark surface; no WPDS token carries this.
|
|
59
59
|
--a8c-charts-color-tooltip-surface: rgb(0 0 0 / 85%);
|
|
60
|
-
--a8c-charts-color-zoom-selection: var(--wpds-color-background-interactive-brand-strong, #3858e9);
|
|
61
|
-
--a8c-charts-color-zoom-selection-stroke: var(--wpds-color-stroke-interactive-brand, #3858e9);
|
|
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
62
|
|
|
63
63
|
// @wordpress/theme 1.0.0 removed the whole --wpds-elevation-* group while the
|
|
64
64
|
// elevation model is redefined (gutenberg#80099). These carry the removed spec
|