@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
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
/* eslint-disable react/jsx-no-bind */
|
|
2
|
+
|
|
1
3
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
4
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { GlobalChartsProvider } from '../../../providers';
|
|
5
|
+
import { GlobalChartsProvider, useGlobalChartsContext } from '../../../providers';
|
|
4
6
|
import PieChart from '../pie-chart';
|
|
5
7
|
|
|
6
8
|
describe( 'PieChart', () => {
|
|
@@ -548,4 +550,74 @@ describe( 'PieChart', () => {
|
|
|
548
550
|
expect( screen.getByText( /33\.3/ ) ).toBeInTheDocument();
|
|
549
551
|
} );
|
|
550
552
|
} );
|
|
553
|
+
|
|
554
|
+
describe( 'Programmatic visibility (non-interactive legend)', () => {
|
|
555
|
+
const chartId = 'test-programmatic-hide-pie-chart';
|
|
556
|
+
const testData = [
|
|
557
|
+
{ label: 'Segment A', value: 50 },
|
|
558
|
+
{ label: 'Segment B', value: 50 },
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
const Harness = () => {
|
|
562
|
+
const { toggleSeriesVisibility } = useGlobalChartsContext();
|
|
563
|
+
return (
|
|
564
|
+
<>
|
|
565
|
+
<button onClick={ () => toggleSeriesVisibility( chartId, 'Segment A' ) }>
|
|
566
|
+
Hide Segment A
|
|
567
|
+
</button>
|
|
568
|
+
<button onClick={ () => toggleSeriesVisibility( chartId, 'Segment B' ) }>
|
|
569
|
+
Hide Segment B
|
|
570
|
+
</button>
|
|
571
|
+
<PieChart
|
|
572
|
+
data={ testData }
|
|
573
|
+
size={ 500 }
|
|
574
|
+
showLegend
|
|
575
|
+
legend={ { interactive: false } }
|
|
576
|
+
chartId={ chartId }
|
|
577
|
+
/>
|
|
578
|
+
</>
|
|
579
|
+
);
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
test( 'removes a segment hidden programmatically even though the legend is not clickable', async () => {
|
|
583
|
+
const user = userEvent.setup();
|
|
584
|
+
|
|
585
|
+
render(
|
|
586
|
+
<GlobalChartsProvider>
|
|
587
|
+
<Harness />
|
|
588
|
+
</GlobalChartsProvider>
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
expect( screen.getAllByTestId( 'pie-segment' ) ).toHaveLength( 2 );
|
|
592
|
+
|
|
593
|
+
await user.click( screen.getByRole( 'button', { name: 'Hide Segment A' } ) );
|
|
594
|
+
|
|
595
|
+
await waitFor( () => {
|
|
596
|
+
expect( screen.getAllByTestId( 'pie-segment' ) ).toHaveLength( 1 );
|
|
597
|
+
} );
|
|
598
|
+
|
|
599
|
+
// The legend itself stays non-interactive (no toggle button for the segment).
|
|
600
|
+
expect( screen.queryAllByRole( 'button', { name: /Segment A: /i } ) ).toHaveLength( 0 );
|
|
601
|
+
} );
|
|
602
|
+
|
|
603
|
+
test( 'empty-state copy omits the click instruction when the legend is not interactive', async () => {
|
|
604
|
+
const user = userEvent.setup();
|
|
605
|
+
|
|
606
|
+
render(
|
|
607
|
+
<GlobalChartsProvider>
|
|
608
|
+
<Harness />
|
|
609
|
+
</GlobalChartsProvider>
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
await user.click( screen.getByRole( 'button', { name: 'Hide Segment A' } ) );
|
|
613
|
+
await user.click( screen.getByRole( 'button', { name: 'Hide Segment B' } ) );
|
|
614
|
+
|
|
615
|
+
await waitFor( () => {
|
|
616
|
+
expect( screen.queryAllByTestId( 'pie-segment' ) ).toHaveLength( 0 );
|
|
617
|
+
} );
|
|
618
|
+
|
|
619
|
+
expect( screen.getByText( 'All segments are hidden.' ) ).toBeInTheDocument();
|
|
620
|
+
expect( screen.queryByText( /Click legend items to show data/i ) ).not.toBeInTheDocument();
|
|
621
|
+
} );
|
|
622
|
+
} );
|
|
551
623
|
} );
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.label {
|
|
9
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
10
|
-
font-size: var(--wpds-typography-font-size-lg,
|
|
9
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
10
|
+
font-size: var(--wpds-typography-font-size-lg, 15px);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.note {
|
|
@@ -2,14 +2,13 @@ import { Group } from '@visx/group';
|
|
|
2
2
|
import { Pie } from '@visx/shape';
|
|
3
3
|
import { Text } from '@visx/text';
|
|
4
4
|
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
|
5
|
-
import { __ } from '@wordpress/i18n';
|
|
6
5
|
import clsx from 'clsx';
|
|
7
6
|
import { useCallback, useContext, useMemo } from 'react';
|
|
8
7
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
9
8
|
import { BaseTooltip } from '../../components/tooltip';
|
|
10
9
|
import {
|
|
11
10
|
useDataWithPercentages,
|
|
12
|
-
|
|
11
|
+
useLegendVisibilityData,
|
|
13
12
|
usePrefersReducedMotion,
|
|
14
13
|
} from '../../hooks';
|
|
15
14
|
import {
|
|
@@ -19,13 +18,14 @@ import {
|
|
|
19
18
|
useGlobalChartsContext,
|
|
20
19
|
GlobalChartsContext,
|
|
21
20
|
} from '../../providers';
|
|
21
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
22
22
|
import { attachSubComponents } from '../../utils';
|
|
23
23
|
import { Center } from '../private/center';
|
|
24
24
|
import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
|
|
25
|
+
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
25
26
|
import { ChartLayout } from '../private/chart-layout';
|
|
26
27
|
import { RadialWipeAnimation } from '../private/radial-wipe-animation';
|
|
27
|
-
import {
|
|
28
|
-
import { SvgEmptyState } from '../private/svg-empty-state';
|
|
28
|
+
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
29
29
|
import { withResponsive } from '../private/with-responsive';
|
|
30
30
|
import styles from './pie-semi-circle-chart.module.scss';
|
|
31
31
|
import type { LegendValueDisplay } from '../../components/legend';
|
|
@@ -246,11 +246,10 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
246
246
|
// Calculate percentages from values (single source of truth)
|
|
247
247
|
const dataWithPercentages = useDataWithPercentages( data );
|
|
248
248
|
|
|
249
|
-
// Filter and recalculate data
|
|
250
|
-
const { visibleData, allSegmentsHidden, legendData } =
|
|
249
|
+
// Filter and recalculate data from the shared legend visibility state.
|
|
250
|
+
const { visibleData, allSegmentsHidden, legendData } = useLegendVisibilityData( {
|
|
251
251
|
data: dataWithPercentages,
|
|
252
252
|
chartId,
|
|
253
|
-
legendInteractive,
|
|
254
253
|
isSeriesVisible,
|
|
255
254
|
} );
|
|
256
255
|
|
|
@@ -352,7 +351,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
352
351
|
);
|
|
353
352
|
|
|
354
353
|
return (
|
|
355
|
-
<
|
|
354
|
+
<ChartInstanceContext.Provider value={ { chartId } }>
|
|
356
355
|
<ChartLayout
|
|
357
356
|
legendPosition={ legendPosition }
|
|
358
357
|
legendElement={ legendElement }
|
|
@@ -375,7 +374,9 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
375
374
|
<>
|
|
376
375
|
{ withTooltips && tooltipOpen && tooltipData && (
|
|
377
376
|
<TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
|
|
378
|
-
<div
|
|
377
|
+
<div className={ CHART_SCOPE_CLASS } role="tooltip">
|
|
378
|
+
{ renderTooltip( { tooltipData } ) }
|
|
379
|
+
</div>
|
|
379
380
|
</TooltipInPortal>
|
|
380
381
|
) }
|
|
381
382
|
{ htmlChildren }
|
|
@@ -424,10 +425,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
424
425
|
>
|
|
425
426
|
{ allSegmentsHidden ? (
|
|
426
427
|
<SvgEmptyState x={ 0 } y={ -radius / 2 } width={ width } height={ height }>
|
|
427
|
-
{
|
|
428
|
-
'All segments are hidden. Click legend items to show data.',
|
|
429
|
-
'jetpack-charts'
|
|
430
|
-
) }
|
|
428
|
+
{ getAllHiddenMessage( legendInteractive, 'segments' ) }
|
|
431
429
|
</SvgEmptyState>
|
|
432
430
|
) : (
|
|
433
431
|
<>
|
|
@@ -490,7 +488,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
490
488
|
);
|
|
491
489
|
} }
|
|
492
490
|
</ChartLayout>
|
|
493
|
-
</
|
|
491
|
+
</ChartInstanceContext.Provider>
|
|
494
492
|
);
|
|
495
493
|
};
|
|
496
494
|
|
|
@@ -15,11 +15,7 @@ export interface ChartInstanceContextValue {
|
|
|
15
15
|
chartRef?: React.RefObject< ChartInstanceRef >;
|
|
16
16
|
chartWidth?: number;
|
|
17
17
|
chartHeight?: number;
|
|
18
|
+
isSeriesVisible?: ( seriesLabel: string ) => boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const ChartInstanceContext = createContext< ChartInstanceContextValue | null >( null );
|
|
21
|
-
|
|
22
|
-
// Backward compatibility exports
|
|
23
|
-
export const SingleChartContext = ChartInstanceContext;
|
|
24
|
-
export type SingleChartContextValue = ChartInstanceContextValue;
|
|
25
|
-
export type SingleChartRef = ChartInstanceRef;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useContext } from 'react';
|
|
2
|
-
import { ChartInstanceContext, type ChartInstanceContextValue } from './
|
|
2
|
+
import { ChartInstanceContext, type ChartInstanceContextValue } from './chart-instance-context';
|
|
3
3
|
|
|
4
4
|
export const useChartInstanceContext = (): ChartInstanceContextValue => {
|
|
5
5
|
const context = useContext( ChartInstanceContext );
|
|
@@ -8,5 +8,3 @@ export const useChartInstanceContext = (): ChartInstanceContextValue => {
|
|
|
8
8
|
}
|
|
9
9
|
return context;
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
export const useSingleChartContext = useChartInstanceContext;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { _x } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
export type HiddenChartItemType = 'series' | 'segments';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return the empty-state message for a chart whose items are all hidden.
|
|
7
|
+
*
|
|
8
|
+
* Each translation call has a distinct gettext context. This prevents Terser
|
|
9
|
+
* from folding conditional calls into one call with a conditional msgid, which
|
|
10
|
+
* would make the strings unavailable to static translation extraction.
|
|
11
|
+
*
|
|
12
|
+
* @param interactive - Whether the legend can be used to show hidden items.
|
|
13
|
+
* @param itemType - The chart-specific name for its data items.
|
|
14
|
+
* @return The translated empty-state message.
|
|
15
|
+
*/
|
|
16
|
+
export const getAllHiddenMessage = (
|
|
17
|
+
interactive: boolean,
|
|
18
|
+
itemType: HiddenChartItemType
|
|
19
|
+
): string => {
|
|
20
|
+
if ( itemType === 'segments' ) {
|
|
21
|
+
if ( interactive ) {
|
|
22
|
+
return _x(
|
|
23
|
+
'All segments are hidden. Click legend items to show data.',
|
|
24
|
+
'chart empty state: interactive segments',
|
|
25
|
+
'jetpack-charts'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return _x( 'All segments are hidden.', 'chart empty state: segments', 'jetpack-charts' );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if ( interactive ) {
|
|
33
|
+
return _x(
|
|
34
|
+
'All series are hidden. Click legend items to show data.',
|
|
35
|
+
'chart empty state: interactive series',
|
|
36
|
+
'jetpack-charts'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return _x( 'All series are hidden.', 'chart empty state: series', 'jetpack-charts' );
|
|
41
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getAllHiddenMessage } from '../get-all-hidden-message';
|
|
2
|
+
|
|
3
|
+
describe( 'getAllHiddenMessage', () => {
|
|
4
|
+
it.each( [
|
|
5
|
+
[ false, 'series', 'All series are hidden.' ],
|
|
6
|
+
[ true, 'series', 'All series are hidden. Click legend items to show data.' ],
|
|
7
|
+
[ false, 'segments', 'All segments are hidden.' ],
|
|
8
|
+
[ true, 'segments', 'All segments are hidden. Click legend items to show data.' ],
|
|
9
|
+
] as const )(
|
|
10
|
+
'returns the expected message for interactive=%s %s',
|
|
11
|
+
( interactive, type, message ) => {
|
|
12
|
+
expect( getAllHiddenMessage( interactive, type ) ).toBe( message );
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
} );
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { ChartScopeContext } from '../../../../providers/chart-scope';
|
|
2
4
|
import { withResponsive } from '../index';
|
|
3
5
|
import type { BaseChartProps } from '../../../../types';
|
|
4
6
|
|
|
@@ -311,4 +313,18 @@ describe( 'withResponsive', () => {
|
|
|
311
313
|
expect( screen.getByTestId( 'responsive-container' ) ).toBeInTheDocument();
|
|
312
314
|
} );
|
|
313
315
|
} );
|
|
316
|
+
|
|
317
|
+
it( 'publishes the responsive wrapper element as the chart scope', () => {
|
|
318
|
+
// The catalog is declared on the GlobalChartsProvider wrapper (or a standalone component's own element), not on every chart root, so this wrapper doesn't carry a scope class itself — but it must still be the element the JS bridge resolves scoped CSS variables against, via ChartScopeContext, so it picks up any override set inside the provider tree the same way a CSS-painted sibling would.
|
|
319
|
+
const ScopeProbe = () => {
|
|
320
|
+
const scopeNode = useContext( ChartScopeContext );
|
|
321
|
+
return <span data-testid="scope-probe">{ scopeNode?.dataset.testid }</span>;
|
|
322
|
+
};
|
|
323
|
+
const Chart: ( props: BaseChartProps ) => JSX.Element = () => <ScopeProbe />;
|
|
324
|
+
const Wrapped = withResponsive( Chart );
|
|
325
|
+
|
|
326
|
+
render( <Wrapped data={ [] } /> );
|
|
327
|
+
|
|
328
|
+
expect( screen.getByTestId( 'scope-probe' ) ).toHaveTextContent( 'responsive-wrapper' );
|
|
329
|
+
} );
|
|
314
330
|
} );
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useParentSize } from '@visx/responsive';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
import { ChartScopeContext } from '../../../providers/chart-scope';
|
|
4
5
|
import styles from './with-responsive.module.scss';
|
|
5
6
|
import type { BaseChartProps } from '../../../types';
|
|
6
7
|
import type { ComponentType } from 'react';
|
|
@@ -66,6 +67,9 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o
|
|
|
66
67
|
|
|
67
68
|
const hasAspectRatio = aspectRatio !== undefined && aspectRatio > 0;
|
|
68
69
|
|
|
70
|
+
// The context needs a re-render when the node attaches; wrapperRef alone cannot provide that, so mirror the node into state from the same callback ref.
|
|
71
|
+
const [ scopeNode, setScopeNode ] = useState< HTMLDivElement | null >( null );
|
|
72
|
+
|
|
69
73
|
// Keep our own handle on the wrapper so we can read its live height below, and
|
|
70
74
|
// still hand the node to useParentSize's ref (a callback ref in practice; guard
|
|
71
75
|
// the object-ref shape too).
|
|
@@ -73,6 +77,7 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o
|
|
|
73
77
|
const setWrapperRef = useCallback(
|
|
74
78
|
( node: HTMLDivElement | null ) => {
|
|
75
79
|
wrapperRef.current = node;
|
|
80
|
+
setScopeNode( node );
|
|
76
81
|
if ( typeof parentRef === 'function' ) {
|
|
77
82
|
parentRef( node );
|
|
78
83
|
} else if ( parentRef ) {
|
|
@@ -160,17 +165,19 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o
|
|
|
160
165
|
...( height !== undefined ? { height } : null ),
|
|
161
166
|
} }
|
|
162
167
|
>
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
<ChartScopeContext.Provider value={ scopeNode }>
|
|
169
|
+
{ hasAspectRatio ? (
|
|
170
|
+
<div
|
|
171
|
+
data-testid="responsive-content"
|
|
172
|
+
className={ styles.content }
|
|
173
|
+
style={ { width: boxWidth, height: boxHeight } }
|
|
174
|
+
>
|
|
175
|
+
{ wrappedComponent }
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
wrappedComponent
|
|
179
|
+
) }
|
|
180
|
+
</ChartScopeContext.Provider>
|
|
174
181
|
</div>
|
|
175
182
|
);
|
|
176
183
|
};
|
|
@@ -2,7 +2,7 @@ import { act, render, renderHook, screen, waitFor } from '@testing-library/react
|
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { useCallback, useRef, useState } from 'react';
|
|
4
4
|
import { useXZoom, ZoomResetButton } from '../index';
|
|
5
|
-
import type {
|
|
5
|
+
import type { ChartInstanceRef } from '../../chart-instance-context';
|
|
6
6
|
import type { EventHandlerParams } from '@visx/xychart';
|
|
7
7
|
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ const makeChartRef = (
|
|
|
23
23
|
getScales: () => ( scale ? { xScale: scale, yScale: scale } : null ),
|
|
24
24
|
getChartDimensions: () => ( { width: 0, height: 0, margin: {} } ),
|
|
25
25
|
},
|
|
26
|
-
} ) as unknown as ReturnType< typeof useRef<
|
|
26
|
+
} ) as unknown as ReturnType< typeof useRef< ChartInstanceRef > >;
|
|
27
27
|
|
|
28
28
|
const makeParams = ( x: number ): EventHandlerParams< object > =>
|
|
29
29
|
( {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// The selection's translucency lives in the SVG opacity properties so
|
|
4
4
|
// the colors themselves can come straight from WPDS tokens.
|
|
5
5
|
&__selection {
|
|
6
|
-
fill: var(--a8c-charts-color-zoom-selection
|
|
6
|
+
fill: var(--a8c-charts-color-zoom-selection);
|
|
7
7
|
fill-opacity: 0.16;
|
|
8
|
-
stroke: var(--a8c-charts-color-zoom-selection-stroke
|
|
8
|
+
stroke: var(--a8c-charts-color-zoom-selection-stroke);
|
|
9
9
|
stroke-opacity: 0.65;
|
|
10
10
|
stroke-width: var(--wpds-border-width-xs, 1px);
|
|
11
11
|
pointer-events: none;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// through it. Scoped to the rest state so the WPDS
|
|
30
30
|
// hover/active/focus backgrounds still apply.
|
|
31
31
|
&:not(:hover, :active, :focus) {
|
|
32
|
-
background-color: var(--a8c-charts-color-
|
|
32
|
+
background-color: var(--a8c-charts-color-surface);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
3
3
|
import { IconButton } from '@wordpress/ui';
|
|
4
4
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
|
5
5
|
import styles from './x-zoom.module.scss';
|
|
6
|
-
import type {
|
|
6
|
+
import type { ChartInstanceRef } from '../chart-instance-context';
|
|
7
7
|
import type { AxisScale } from '@visx/axis';
|
|
8
8
|
import type { EventHandlerParams } from '@visx/xychart';
|
|
9
9
|
import type { KeyboardEvent, ReactNode, RefObject } from 'react';
|
|
@@ -38,7 +38,7 @@ export function useXZoom< T extends Date | number = Date >( {
|
|
|
38
38
|
userHandlers,
|
|
39
39
|
}: {
|
|
40
40
|
enabled: boolean;
|
|
41
|
-
chartRef: RefObject<
|
|
41
|
+
chartRef: RefObject< ChartInstanceRef | null >;
|
|
42
42
|
userHandlers?: {
|
|
43
43
|
onPointerDown?: PointerHandler;
|
|
44
44
|
onPointerMove?: PointerHandler;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useContext, useMemo, forwardRef } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ChartInstanceContext } from '../../charts/private/chart-instance-context';
|
|
3
3
|
import { GlobalChartsContext } from '../../providers';
|
|
4
4
|
import { BaseLegend } from './private';
|
|
5
5
|
import type { LegendProps } from './types';
|
|
@@ -20,11 +20,11 @@ export const Legend = forwardRef< HTMLDivElement, LegendProps >(
|
|
|
20
20
|
( { chartId, items, shape, ...props }, ref ) => {
|
|
21
21
|
// Get context but don't throw if it doesn't exist
|
|
22
22
|
const context = useContext( GlobalChartsContext );
|
|
23
|
-
const
|
|
23
|
+
const chartInstanceContext = useContext( ChartInstanceContext );
|
|
24
24
|
|
|
25
25
|
// When chartId is used, it is standalone mode
|
|
26
|
-
// When chartId is not provided, we use the context's chartId, meaning it is
|
|
27
|
-
const contextChartId = chartId ??
|
|
26
|
+
// When chartId is not provided, we use the context's chartId, meaning it is inside a chart instance
|
|
27
|
+
const contextChartId = chartId ?? chartInstanceContext?.chartId;
|
|
28
28
|
|
|
29
29
|
const chartData = useMemo(
|
|
30
30
|
() => ( contextChartId && context ? context.getChartData( contextChartId ) : undefined ),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Group } from '@visx/group';
|
|
2
2
|
import { LegendItem, LegendLabel, LegendOrdinal, LegendShape } from '@visx/legend';
|
|
3
3
|
import { scaleOrdinal } from '@visx/scale';
|
|
4
|
+
import { _x, sprintf } from '@wordpress/i18n';
|
|
4
5
|
import { Stack } from '@wordpress/ui';
|
|
5
6
|
import clsx from 'clsx';
|
|
6
7
|
import {
|
|
@@ -11,11 +12,13 @@ import {
|
|
|
11
12
|
useCallback,
|
|
12
13
|
useContext,
|
|
13
14
|
} from 'react';
|
|
15
|
+
import { ChartInstanceContext } from '../../../charts/private/chart-instance-context';
|
|
14
16
|
import { useTextTruncation } from '../../../hooks';
|
|
15
17
|
import { GlobalChartsContext, useGlobalChartsTheme } from '../../../providers';
|
|
18
|
+
import { useStandaloneScopeClass } from '../../../providers/chart-scope';
|
|
16
19
|
import { valueOrIdentity, valueOrIdentityString, labelTransformFactory } from '../utils';
|
|
17
20
|
import styles from './base-legend.module.scss';
|
|
18
|
-
import type { BaseLegendProps } from '../types';
|
|
21
|
+
import type { BaseLegendItem, BaseLegendProps } from '../types';
|
|
19
22
|
|
|
20
23
|
const ALIGNMENT_TO_FLEX = {
|
|
21
24
|
start: 'flex-start',
|
|
@@ -57,6 +60,52 @@ const LegendText = ( {
|
|
|
57
60
|
);
|
|
58
61
|
};
|
|
59
62
|
|
|
63
|
+
// Interactive items get a toggle affordance; non-interactive items only need a label
|
|
64
|
+
// when hidden, since a visible item's own text already serves as its accessible name.
|
|
65
|
+
const getLegendItemAriaLabel = (
|
|
66
|
+
text: string,
|
|
67
|
+
value: BaseLegendItem[ 'value' ],
|
|
68
|
+
visible: boolean,
|
|
69
|
+
interactive: boolean
|
|
70
|
+
) => {
|
|
71
|
+
const accessibleText =
|
|
72
|
+
value != null && value !== ''
|
|
73
|
+
? sprintf(
|
|
74
|
+
/* translators: 1: legend item label; 2: legend item value. */
|
|
75
|
+
_x( '%1$s, %2$s', 'legend item label and value', 'jetpack-charts' ),
|
|
76
|
+
text,
|
|
77
|
+
String( value )
|
|
78
|
+
)
|
|
79
|
+
: text;
|
|
80
|
+
|
|
81
|
+
if ( interactive ) {
|
|
82
|
+
if ( visible ) {
|
|
83
|
+
return sprintf(
|
|
84
|
+
/* translators: %s: legend item label (e.g. a series or segment name) */
|
|
85
|
+
_x(
|
|
86
|
+
'%s: visible. Toggle visibility.',
|
|
87
|
+
'visible interactive legend item',
|
|
88
|
+
'jetpack-charts'
|
|
89
|
+
),
|
|
90
|
+
accessibleText
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return sprintf(
|
|
94
|
+
/* translators: %s: legend item label (e.g. a series or segment name) */
|
|
95
|
+
_x( '%s: hidden. Toggle visibility.', 'hidden interactive legend item', 'jetpack-charts' ),
|
|
96
|
+
accessibleText
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if ( visible ) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return sprintf(
|
|
103
|
+
/* translators: %s: legend item label (e.g. a series or segment name) */
|
|
104
|
+
_x( '%s: hidden', 'hidden non-interactive legend item', 'jetpack-charts' ),
|
|
105
|
+
accessibleText
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
60
109
|
/*
|
|
61
110
|
* Base legend component that displays color-coded items with labels based on visx LegendOrdinal.
|
|
62
111
|
* We avoid using LegendOrdinal directly to enable support for advanced features such as interactivity.
|
|
@@ -102,6 +151,8 @@ export const BaseLegend: ForwardRefExoticComponent<
|
|
|
102
151
|
|
|
103
152
|
const theme = useGlobalChartsTheme();
|
|
104
153
|
const context = useContext( GlobalChartsContext );
|
|
154
|
+
const chartInstanceContext = useContext( ChartInstanceContext );
|
|
155
|
+
const standaloneScopeClass = useStandaloneScopeClass();
|
|
105
156
|
|
|
106
157
|
const legendScale = scaleOrdinal( {
|
|
107
158
|
domain: items.map( item => item.label ),
|
|
@@ -118,29 +169,28 @@ export const BaseLegend: ForwardRefExoticComponent<
|
|
|
118
169
|
const handleLegendClick = useCallback(
|
|
119
170
|
( seriesLabels: string[] ) => {
|
|
120
171
|
if ( interactive && chartId && context ) {
|
|
121
|
-
// Converge the whole group on the representative's next state: only toggle series that
|
|
122
|
-
// currently match it, so a desynced group (e.g. one member hidden programmatically)
|
|
123
|
-
// ends up uniformly hidden/shown after one click.
|
|
124
172
|
const representativeVisible = context.isSeriesVisible( chartId, seriesLabels[ 0 ] );
|
|
125
|
-
seriesLabels.forEach( label =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
} );
|
|
173
|
+
seriesLabels.forEach( label =>
|
|
174
|
+
context.setSeriesVisibility( chartId, label, ! representativeVisible )
|
|
175
|
+
);
|
|
130
176
|
}
|
|
131
177
|
},
|
|
132
178
|
[ interactive, chartId, context ]
|
|
133
179
|
);
|
|
134
180
|
|
|
135
|
-
//
|
|
181
|
+
// Visibility is display state, not interaction state: a series hidden
|
|
182
|
+
// programmatically must read as hidden even when the legend cannot be clicked.
|
|
136
183
|
const isSeriesVisible = useCallback(
|
|
137
184
|
( seriesLabel: string ) => {
|
|
138
|
-
if (
|
|
185
|
+
if ( chartInstanceContext?.isSeriesVisible ) {
|
|
186
|
+
return chartInstanceContext.isSeriesVisible( seriesLabel );
|
|
187
|
+
}
|
|
188
|
+
if ( ! chartId || ! context ) {
|
|
139
189
|
return true;
|
|
140
190
|
}
|
|
141
191
|
return context.isSeriesVisible( chartId, seriesLabel );
|
|
142
192
|
},
|
|
143
|
-
[
|
|
193
|
+
[ chartId, chartInstanceContext, context ]
|
|
144
194
|
);
|
|
145
195
|
|
|
146
196
|
// Create event handlers to avoid inline arrow functions
|
|
@@ -187,9 +237,9 @@ export const BaseLegend: ForwardRefExoticComponent<
|
|
|
187
237
|
align={ orientation === 'vertical' ? flexAlignment : undefined }
|
|
188
238
|
justify={ orientation === 'horizontal' ? flexAlignment : undefined }
|
|
189
239
|
wrap={ orientation === 'horizontal' ? 'wrap' : undefined }
|
|
190
|
-
role=
|
|
240
|
+
role={ interactive ? undefined : 'list' }
|
|
191
241
|
data-testid={ `legend-${ orientation }` }
|
|
192
|
-
className={ clsx( styles.legend, className ) }
|
|
242
|
+
className={ clsx( standaloneScopeClass, styles.legend, className ) }
|
|
193
243
|
style={ theme.legend?.containerStyles }
|
|
194
244
|
>
|
|
195
245
|
{ labels.map( ( label, i ) => {
|
|
@@ -221,14 +271,15 @@ export const BaseLegend: ForwardRefExoticComponent<
|
|
|
221
271
|
}
|
|
222
272
|
onClick={ handleClick }
|
|
223
273
|
onKeyDown={ handleKeyDown }
|
|
224
|
-
role={ interactive ? 'button' :
|
|
274
|
+
role={ interactive ? 'button' : 'listitem' }
|
|
225
275
|
tabIndex={ interactive ? 0 : undefined }
|
|
226
276
|
aria-pressed={ interactive ? visible : undefined }
|
|
227
|
-
aria-label={
|
|
277
|
+
aria-label={ getLegendItemAriaLabel(
|
|
278
|
+
label.text,
|
|
279
|
+
matchedItem?.value,
|
|
280
|
+
visible,
|
|
228
281
|
interactive
|
|
229
|
-
|
|
230
|
-
: undefined
|
|
231
|
-
}
|
|
282
|
+
) }
|
|
232
283
|
>
|
|
233
284
|
{ items[ i ]?.renderGlyph ? (
|
|
234
285
|
<svg
|