@automattic/charts 4.9.0 → 4.10.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 +14 -0
- package/dist/index.cjs +159 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +14 -0
- package/dist/index.d.cts +27 -22
- package/dist/index.d.ts +27 -22
- package/dist/index.js +159 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +2 -1
- package/src/charts/area-chart/test/area-chart.test.tsx +0 -8
- package/src/charts/area-chart/types.ts +0 -2
- package/src/charts/bar-chart/bar-chart.tsx +7 -2
- package/src/charts/bar-chart/test/bar-chart.test.tsx +0 -8
- package/src/charts/bar-chart/types.ts +0 -2
- package/src/charts/bar-list-chart/bar-list-chart.tsx +3 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +23 -7
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +22 -0
- package/src/charts/conversion-funnel-chart/types.ts +1 -1
- package/src/charts/geo-chart/geo-chart.tsx +17 -2
- package/src/charts/geo-chart/test/geo-chart.test.tsx +9 -4
- package/src/charts/geo-chart/types.ts +1 -1
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +0 -6
- package/src/charts/heatmap-chart/types.ts +0 -2
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +6 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +34 -19
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +27 -1
- package/src/charts/leaderboard-chart/types.ts +1 -0
- package/src/charts/line-chart/line-chart.tsx +3 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +0 -8
- package/src/charts/line-chart/types.ts +0 -2
- package/src/charts/pie-chart/pie-chart.module.scss +3 -0
- package/src/charts/pie-chart/pie-chart.tsx +0 -5
- package/src/charts/pie-chart/test/pie-chart.test.tsx +4 -8
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +3 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +0 -5
- package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +8 -3
- package/src/charts/test/accessible-name.test.tsx +181 -0
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +63 -3
- package/src/components/legend/hooks/use-chart-legend-items.ts +63 -1
- package/src/components/legend/private/base-legend.tsx +27 -15
- package/src/components/legend/test/legend.test.tsx +42 -0
- package/src/components/legend/types.ts +2 -0
- package/src/hooks/test/use-xychart-theme.test.tsx +22 -0
- package/src/hooks/use-xychart-theme.ts +33 -2
- package/src/index.ts +1 -0
- package/src/types.ts +19 -1
- package/src/utils/resolve-font-size.ts +9 -10
- package/src/utils/test/resolve-font-size.test.ts +11 -2
package/package.json
CHANGED
|
@@ -262,8 +262,9 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
262
262
|
glyphSize: 0,
|
|
263
263
|
collapseGroups: legend.collapseGroups ?? false,
|
|
264
264
|
comparisonItem: legend.comparisonItem ?? false,
|
|
265
|
+
comparisonSeriesItems: legend.comparisonSeriesItems,
|
|
265
266
|
} ),
|
|
266
|
-
[ legend.collapseGroups, legend.comparisonItem ]
|
|
267
|
+
[ legend.collapseGroups, legend.comparisonItem, legend.comparisonSeriesItems ]
|
|
267
268
|
);
|
|
268
269
|
const legendItems = useChartLegendItems( dataSorted, legendOptions, legendShape );
|
|
269
270
|
|
|
@@ -785,14 +785,6 @@ describe( 'AreaChart', () => {
|
|
|
785
785
|
const chart = screen.getByRole( 'application', { name: /area chart/i } );
|
|
786
786
|
expect( chart ).toHaveAttribute( 'tabIndex', '0' );
|
|
787
787
|
expect( chart ).toHaveAttribute( 'aria-label', 'Area chart' );
|
|
788
|
-
} );
|
|
789
|
-
|
|
790
|
-
test.each( [
|
|
791
|
-
[ undefined, 'Area chart' ],
|
|
792
|
-
[ 'Views over time', 'Views over time' ],
|
|
793
|
-
] )( 'names the chart container from ariaLabel %p', ( ariaLabel, name ) => {
|
|
794
|
-
renderWithProvider( { ariaLabel } );
|
|
795
|
-
expect( screen.getByRole( 'application', { name } ) ).toBeInTheDocument();
|
|
796
788
|
expect( screen.queryByLabelText( 'XYChart' ) ).not.toBeInTheDocument();
|
|
797
789
|
} );
|
|
798
790
|
} );
|
|
@@ -11,8 +11,6 @@ import type { CurveType } from '../line-chart/types';
|
|
|
11
11
|
import type { ReactNode } from 'react';
|
|
12
12
|
|
|
13
13
|
export interface AreaChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
14
|
-
/** Accessible name of the chart. Defaults to a localized "Area chart". */
|
|
15
|
-
ariaLabel?: string;
|
|
16
14
|
/**
|
|
17
15
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
18
16
|
*/
|
|
@@ -139,6 +139,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
139
139
|
const legendInteractive = legend.interactive ?? false;
|
|
140
140
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
141
141
|
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
142
|
+
const legendComparisonSeriesItems = legend.comparisonSeriesItems;
|
|
142
143
|
const horizontal = orientation === 'horizontal';
|
|
143
144
|
const chartId = useChartId( providedChartId );
|
|
144
145
|
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
@@ -159,8 +160,12 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
159
160
|
|
|
160
161
|
// Create legend items using the reusable hook
|
|
161
162
|
const legendOptions = useMemo(
|
|
162
|
-
() => ( {
|
|
163
|
-
|
|
163
|
+
() => ( {
|
|
164
|
+
collapseGroups: legendCollapseGroups,
|
|
165
|
+
comparisonItem: legendComparisonItem,
|
|
166
|
+
comparisonSeriesItems: legendComparisonSeriesItems,
|
|
167
|
+
} ),
|
|
168
|
+
[ legendCollapseGroups, legendComparisonItem, legendComparisonSeriesItems ]
|
|
164
169
|
);
|
|
165
170
|
const legendItems = useChartLegendItems( dataSorted, legendOptions );
|
|
166
171
|
|
|
@@ -1685,14 +1685,6 @@ describe( 'BarChart', () => {
|
|
|
1685
1685
|
expect( chart ).toHaveAttribute( 'tabIndex', '0' );
|
|
1686
1686
|
expect( chart ).toHaveAttribute( 'role', 'application' );
|
|
1687
1687
|
expect( chart ).toHaveAttribute( 'aria-label', 'Bar chart' );
|
|
1688
|
-
} );
|
|
1689
|
-
|
|
1690
|
-
test.each( [
|
|
1691
|
-
[ undefined, 'Bar chart' ],
|
|
1692
|
-
[ 'Views over time', 'Views over time' ],
|
|
1693
|
-
] )( 'names the chart container from ariaLabel %p', ( ariaLabel, name ) => {
|
|
1694
|
-
renderWithTheme( { ariaLabel } );
|
|
1695
|
-
expect( screen.getByRole( 'application', { name } ) ).toBeInTheDocument();
|
|
1696
1688
|
expect( screen.queryByLabelText( 'XYChart' ) ).not.toBeInTheDocument();
|
|
1697
1689
|
} );
|
|
1698
1690
|
|
|
@@ -19,8 +19,6 @@ export type BandHighlightSelection = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export interface BarChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
22
|
-
/** Accessible name of the chart. Defaults to a localized "Bar chart". */
|
|
23
|
-
ariaLabel?: string;
|
|
24
22
|
/**
|
|
25
23
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
26
24
|
*/
|
|
@@ -2,6 +2,7 @@ import { formatNumberCompact } from '@automattic/number-formatters';
|
|
|
2
2
|
import { Group } from '@visx/group';
|
|
3
3
|
import { createScale, scaleBand } from '@visx/scale';
|
|
4
4
|
import { Text, type TextProps } from '@visx/text';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
5
6
|
import clsx from 'clsx';
|
|
6
7
|
import { useContext, useMemo } from 'react';
|
|
7
8
|
import { GlobalChartsContext, GlobalChartsProvider, useGlobalChartsContext } from '../../providers';
|
|
@@ -235,6 +236,7 @@ const BarListChartInternal: FC< BarListChartProps > = ( {
|
|
|
235
236
|
top: 0,
|
|
236
237
|
},
|
|
237
238
|
className,
|
|
239
|
+
ariaLabel,
|
|
238
240
|
...rest
|
|
239
241
|
} ) => {
|
|
240
242
|
const { getElementStyles } = useGlobalChartsContext();
|
|
@@ -333,6 +335,7 @@ const BarListChartInternal: FC< BarListChartProps > = ( {
|
|
|
333
335
|
xScale: chartOptions.xScale,
|
|
334
336
|
yScale: chartOptions.yScale,
|
|
335
337
|
} }
|
|
338
|
+
ariaLabel={ ariaLabel ?? __( 'Bar list chart', 'jetpack-charts' ) }
|
|
336
339
|
{ ...rest }
|
|
337
340
|
/>
|
|
338
341
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useTooltip } from '@visx/tooltip';
|
|
2
|
+
import { __ } from '@wordpress/i18n';
|
|
2
3
|
import { Stack } from '@wordpress/ui';
|
|
3
4
|
import clsx from 'clsx';
|
|
4
5
|
import { type FC, useRef, useMemo, useEffect, useState, useCallback, useContext } from 'react';
|
|
5
6
|
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
6
|
-
import { usePrefersReducedMotion } from '../../hooks';
|
|
7
|
+
import { useElementSize, usePrefersReducedMotion } from '../../hooks';
|
|
7
8
|
import {
|
|
8
9
|
GlobalChartsProvider,
|
|
9
10
|
GlobalChartsContext,
|
|
@@ -19,6 +20,9 @@ import styles from './conversion-funnel-chart.module.scss';
|
|
|
19
20
|
import { useFunnelSelection } from './private';
|
|
20
21
|
import type { FunnelStep, ConversionFunnelChartProps } from './types';
|
|
21
22
|
|
|
23
|
+
/** Chart height (px) below which the funnel tightens its gaps so the bars keep their room. */
|
|
24
|
+
const SHORT_CHART_HEIGHT = 180;
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
27
|
* Internal ConversionFunnelChart component with chart registration
|
|
24
28
|
*
|
|
@@ -30,6 +34,7 @@ import type { FunnelStep, ConversionFunnelChartProps } from './types';
|
|
|
30
34
|
* @param props.loading - Whether the chart is in loading state
|
|
31
35
|
* @param props.animation - Whether to show chart animation on initial render or not
|
|
32
36
|
* @param props.className - Additional CSS class name
|
|
37
|
+
* @param props.ariaLabel - Accessible name for the chart group
|
|
33
38
|
* @param props.height - Height of the chart container. Falls back to style.height if set, otherwise defaults to "100%".
|
|
34
39
|
* @param props.style - Custom styling
|
|
35
40
|
* @param props.renderStepLabel - Custom render function for step labels
|
|
@@ -46,6 +51,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
46
51
|
loading = false,
|
|
47
52
|
animation,
|
|
48
53
|
className,
|
|
54
|
+
ariaLabel,
|
|
49
55
|
chartId: providedChartId,
|
|
50
56
|
height,
|
|
51
57
|
style,
|
|
@@ -69,11 +75,19 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
69
75
|
const { handleBarClick, handleBarKeyDown, clearSelection, getStepState } =
|
|
70
76
|
useFunnelSelection( hideTooltip );
|
|
71
77
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
78
|
+
const [ measureChartRef, , chartHeight ] = useElementSize< HTMLDivElement >();
|
|
79
|
+
// A zero height is an unmeasured or hidden chart, not a short one.
|
|
80
|
+
const isShort = chartHeight > 0 && chartHeight < SHORT_CHART_HEIGHT;
|
|
81
|
+
const gap = isShort ? 'sm' : 'xl';
|
|
72
82
|
// Stable identity so React doesn't detach/reattach (and re-render the scope context) on every commit. Keep it firing on unmount (node === null).
|
|
73
|
-
const setChartRef = useCallback(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
const setChartRef = useCallback(
|
|
84
|
+
( node: HTMLDivElement | null ) => {
|
|
85
|
+
chartRef.current = node;
|
|
86
|
+
setScopeNode( node );
|
|
87
|
+
measureChartRef( node );
|
|
88
|
+
},
|
|
89
|
+
[ measureChartRef ]
|
|
90
|
+
);
|
|
77
91
|
|
|
78
92
|
// Wrapper to clear selectedBarRef after clearing selection
|
|
79
93
|
const clearSelectionAndRef = useCallback( () => {
|
|
@@ -309,8 +323,10 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
309
323
|
return (
|
|
310
324
|
<Stack
|
|
311
325
|
direction="column"
|
|
312
|
-
gap=
|
|
326
|
+
gap={ gap }
|
|
313
327
|
data-testid="conversion-funnel-chart"
|
|
328
|
+
role="group"
|
|
329
|
+
aria-label={ ariaLabel ?? __( 'Conversion funnel chart', 'jetpack-charts' ) }
|
|
314
330
|
ref={ setChartRef }
|
|
315
331
|
className={ clsx(
|
|
316
332
|
'conversion-funnel-chart',
|
|
@@ -351,7 +367,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
351
367
|
isColorPaletteResolved && styles[ 'funnel-step--animated' ],
|
|
352
368
|
isBlurred && styles[ 'funnel-step--blurred' ]
|
|
353
369
|
) }
|
|
354
|
-
gap=
|
|
370
|
+
gap={ gap }
|
|
355
371
|
>
|
|
356
372
|
{ /* Step Label and Rate */ }
|
|
357
373
|
<Stack direction="column" gap="xs">
|
|
@@ -3,6 +3,11 @@ import userEvent from '@testing-library/user-event';
|
|
|
3
3
|
import ConversionFunnelChart from '../conversion-funnel-chart';
|
|
4
4
|
import type { FunnelStep } from '../types';
|
|
5
5
|
|
|
6
|
+
let mockChartHeight = 0;
|
|
7
|
+
jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
8
|
+
useElementSize: () => [ jest.fn(), 500, mockChartHeight ],
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
6
11
|
// Mock data for testing
|
|
7
12
|
const mockSteps: FunnelStep[] = [
|
|
8
13
|
{
|
|
@@ -42,6 +47,10 @@ const renderWithoutTheme = ( component: React.ReactElement ) => {
|
|
|
42
47
|
};
|
|
43
48
|
|
|
44
49
|
describe( 'ConversionFunnelChart', () => {
|
|
50
|
+
afterEach( () => {
|
|
51
|
+
mockChartHeight = 0;
|
|
52
|
+
} );
|
|
53
|
+
|
|
45
54
|
describe( 'Basic Rendering', () => {
|
|
46
55
|
it( 'renders the main conversion rate', () => {
|
|
47
56
|
renderWithoutTheme( <ConversionFunnelChart { ...defaultProps } /> );
|
|
@@ -116,6 +125,19 @@ describe( 'ConversionFunnelChart', () => {
|
|
|
116
125
|
height: '250px',
|
|
117
126
|
} );
|
|
118
127
|
} );
|
|
128
|
+
|
|
129
|
+
it.each( [
|
|
130
|
+
[ 'unmeasured', 0, 'xl' ],
|
|
131
|
+
[ 'short', 120, 'sm' ],
|
|
132
|
+
[ 'tall', 320, 'xl' ],
|
|
133
|
+
] )( 'spaces a %s (%ipx) chart with the %s gap', ( _, height, gap ) => {
|
|
134
|
+
mockChartHeight = height;
|
|
135
|
+
renderWithoutTheme( <ConversionFunnelChart { ...defaultProps } /> );
|
|
136
|
+
|
|
137
|
+
expect( screen.getByTestId( 'conversion-funnel-chart' ).style.gap ).toContain(
|
|
138
|
+
`gap-${ gap }`
|
|
139
|
+
);
|
|
140
|
+
} );
|
|
119
141
|
} );
|
|
120
142
|
|
|
121
143
|
describe( 'Empty State', () => {
|
|
@@ -59,7 +59,7 @@ export interface TooltipRenderProps {
|
|
|
59
59
|
*/
|
|
60
60
|
export interface ConversionFunnelChartProps extends Pick<
|
|
61
61
|
BaseChartProps,
|
|
62
|
-
'className' | 'chartId' | 'animation'
|
|
62
|
+
'className' | 'chartId' | 'animation' | 'ariaLabel'
|
|
63
63
|
> {
|
|
64
64
|
/** Main conversion rate to highlight */
|
|
65
65
|
mainRate: number;
|
|
@@ -126,6 +126,7 @@ function normalizeGeoChartError( eventArgs: unknown ): GeoChartError {
|
|
|
126
126
|
*
|
|
127
127
|
* @param props - The props for the GeoChart component
|
|
128
128
|
* @param props.data - Data in Google Charts format (array of arrays with headers)
|
|
129
|
+
* @param props.ariaLabel - Accessible name of the chart
|
|
129
130
|
* @param props.width - Width of the chart in pixels
|
|
130
131
|
* @param props.height - Height of the chart in pixels
|
|
131
132
|
* @param props.region - Region to display ('world', 'US', or ISO 3166-1 alpha-2 code)
|
|
@@ -140,6 +141,7 @@ function normalizeGeoChartError( eventArgs: unknown ): GeoChartError {
|
|
|
140
141
|
const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
141
142
|
className,
|
|
142
143
|
data,
|
|
144
|
+
ariaLabel,
|
|
143
145
|
width,
|
|
144
146
|
height,
|
|
145
147
|
region = 'world',
|
|
@@ -289,12 +291,23 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
289
291
|
]
|
|
290
292
|
);
|
|
291
293
|
|
|
292
|
-
const chartEvents = useMemo< ReactGoogleChartEvent[]
|
|
294
|
+
const chartEvents = useMemo< ReactGoogleChartEvent[] >( () => {
|
|
295
|
+
const readyEvent: ReactGoogleChartEvent = {
|
|
296
|
+
eventName: 'ready',
|
|
297
|
+
// Google hardcodes aria-label="A chart." on its SVG, which repeats the chart's own name.
|
|
298
|
+
callback: () => {
|
|
299
|
+
containerRef.current
|
|
300
|
+
?.querySelectorAll( 'svg' )
|
|
301
|
+
.forEach( svg => svg.setAttribute( 'aria-hidden', 'true' ) );
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
293
305
|
if ( ! onError ) {
|
|
294
|
-
return
|
|
306
|
+
return [ readyEvent ];
|
|
295
307
|
}
|
|
296
308
|
|
|
297
309
|
return [
|
|
310
|
+
readyEvent,
|
|
298
311
|
{
|
|
299
312
|
eventName: 'error',
|
|
300
313
|
callback: ( { eventArgs } ) => {
|
|
@@ -307,6 +320,8 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
307
320
|
return (
|
|
308
321
|
<Center
|
|
309
322
|
ref={ containerRef }
|
|
323
|
+
role="group"
|
|
324
|
+
aria-label={ ariaLabel ?? __( 'Geo chart', 'jetpack-charts' ) }
|
|
310
325
|
className={ clsx( 'geo-chart', styles.container, className ) }
|
|
311
326
|
data-testid="geo-chart"
|
|
312
327
|
style={ { width, height } }
|
|
@@ -281,13 +281,18 @@ describe( 'GeoChart', () => {
|
|
|
281
281
|
} );
|
|
282
282
|
|
|
283
283
|
describe( 'Chart Events', () => {
|
|
284
|
-
test( '
|
|
284
|
+
test( "hides Google's own SVG label from screen readers once the map is ready", () => {
|
|
285
285
|
renderWithTheme();
|
|
286
286
|
|
|
287
|
-
const
|
|
288
|
-
|
|
287
|
+
const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
|
|
288
|
+
screen.getByTestId( 'google-chart-mock' ).appendChild( svg );
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
const chartProps = ChartMock.mock.calls[ ChartMock.mock.calls.length - 1 ]?.[ 0 ];
|
|
291
|
+
chartProps.chartEvents
|
|
292
|
+
.find( ( event: { eventName: string } ) => event.eventName === 'ready' )
|
|
293
|
+
.callback( {} );
|
|
294
|
+
|
|
295
|
+
expect( svg ).toHaveAttribute( 'aria-hidden', 'true' );
|
|
291
296
|
} );
|
|
292
297
|
|
|
293
298
|
test( 'calls onError when Google Charts emits an error', () => {
|
|
@@ -31,7 +31,7 @@ export interface GeoChartError {
|
|
|
31
31
|
|
|
32
32
|
export interface GeoChartProps extends Pick<
|
|
33
33
|
BaseChartProps,
|
|
34
|
-
'className' | 'chartId' | 'width' | 'height'
|
|
34
|
+
'className' | 'chartId' | 'ariaLabel' | 'width' | 'height'
|
|
35
35
|
> {
|
|
36
36
|
/**
|
|
37
37
|
* Data in Google Charts native format for maximum flexibility.
|
|
@@ -1222,12 +1222,6 @@ describe( 'HeatmapChart calendar navigation', () => {
|
|
|
1222
1222
|
} );
|
|
1223
1223
|
|
|
1224
1224
|
describe( 'HeatmapChart naming and focus', () => {
|
|
1225
|
-
test( 'names the grid from ariaLabel when given', () => {
|
|
1226
|
-
renderChart( { ariaLabel: 'Monthly posting activity' } );
|
|
1227
|
-
expect( screen.getByRole( 'grid', { name: 'Monthly posting activity' } ) ).toBeInTheDocument();
|
|
1228
|
-
expect( screen.queryByRole( 'grid', { name: /heatmap chart/i } ) ).not.toBeInTheDocument();
|
|
1229
|
-
} );
|
|
1230
|
-
|
|
1231
1225
|
test( 'scrolls the keyboard-selected cell into view', async () => {
|
|
1232
1226
|
// jsdom has no scrollIntoView; the assertion is that the chart asks for it.
|
|
1233
1227
|
const scrollIntoView = jest.fn();
|
|
@@ -74,8 +74,6 @@ export interface HeatmapChartProps extends Omit<
|
|
|
74
74
|
* group past the edge, Page Up/Down by one group. Default `grid`.
|
|
75
75
|
*/
|
|
76
76
|
keyboardNavigation?: 'grid' | 'calendar';
|
|
77
|
-
/** Accessible name of the grid. Defaults to a localized "Heatmap chart". */
|
|
78
|
-
ariaLabel?: string;
|
|
79
77
|
/** Compact mode: hide in-cell values, tighten gap, thin axis labels. Default false. */
|
|
80
78
|
compact?: boolean;
|
|
81
79
|
/** Compact mode only: grow the cells to fill the chart's box, wrapping the column groups onto more rows when that makes them larger. Default false. */
|
|
@@ -16,8 +16,8 @@ export const SUBPIXEL_TOLERANCE = 0.5;
|
|
|
16
16
|
* depends on `--a8c-charts-dimension-leaderboard-row-gap`, label wrapping, and
|
|
17
17
|
* whatever a caller renders as a label, so any restated number would drift.
|
|
18
18
|
*
|
|
19
|
-
* Every row is a direct grid child marked with `data-row-index`;
|
|
20
|
-
*
|
|
19
|
+
* Every row is a `listitem` div, a direct grid child marked with `data-row-index`;
|
|
20
|
+
* interactive rows hold a button inside it.
|
|
21
21
|
*
|
|
22
22
|
* @param enabled - Whether to measure at all. When false every row fits.
|
|
23
23
|
* @param rowCount - Total number of rows rendered.
|
|
@@ -147,6 +147,12 @@
|
|
|
147
147
|
// needs no runtime cascade and never lands on the DOM as an unprefixed var.
|
|
148
148
|
$focus-ring-width: var(--wpds-border-width-focus);
|
|
149
149
|
|
|
150
|
+
.rowItem {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-column: 1 / -1;
|
|
153
|
+
grid-template-columns: subgrid;
|
|
154
|
+
}
|
|
155
|
+
|
|
150
156
|
.row {
|
|
151
157
|
display: grid;
|
|
152
158
|
grid-column: 1 / -1;
|
|
@@ -188,6 +188,7 @@ const BarWithLabel = ( {
|
|
|
188
188
|
* @param props.children - Child components for composition API
|
|
189
189
|
* @param props.className - Additional CSS class name
|
|
190
190
|
* @param props.style - Custom styling for the chart container
|
|
191
|
+
* @param props.ariaLabel - Accessible name for the list, defaulting to "Leaderboard chart"
|
|
191
192
|
* @return JSX element representing the leaderboard chart
|
|
192
193
|
*/
|
|
193
194
|
const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
@@ -208,6 +209,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
208
209
|
legend = {},
|
|
209
210
|
legendLabels,
|
|
210
211
|
gap = 'md',
|
|
212
|
+
ariaLabel,
|
|
211
213
|
className,
|
|
212
214
|
style,
|
|
213
215
|
children,
|
|
@@ -389,6 +391,8 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
389
391
|
</div>
|
|
390
392
|
) : (
|
|
391
393
|
<Grid
|
|
394
|
+
role="list"
|
|
395
|
+
aria-label={ ariaLabel ?? __( 'Leaderboard chart', 'jetpack-charts' ) }
|
|
392
396
|
templateColumns="minmax(0, 1fr) auto"
|
|
393
397
|
rowGap={ ROW_GAP }
|
|
394
398
|
columnGap={ COLUMN_GAP }
|
|
@@ -465,46 +469,57 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
465
469
|
|
|
466
470
|
if ( entry.onClick && entry.hasInteractiveLabel ) {
|
|
467
471
|
return (
|
|
468
|
-
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Mouse-only fallback; the row button carries keyboard access.
|
|
469
472
|
<div
|
|
470
473
|
key={ entry.id }
|
|
474
|
+
role="listitem"
|
|
471
475
|
data-row-index={ rowIndex }
|
|
472
476
|
style={ rowStyle }
|
|
473
|
-
className={
|
|
474
|
-
onClick={ forwardCellClickToRowButton }
|
|
477
|
+
className={ styles.rowItem }
|
|
475
478
|
>
|
|
476
|
-
{
|
|
477
|
-
<
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
479
|
+
{ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Mouse-only fallback; the row button carries keyboard access. */ }
|
|
480
|
+
<div
|
|
481
|
+
className={ clsx( styles.row, styles.interactiveRow, styles.layeredRow ) }
|
|
482
|
+
onClick={ forwardCellClickToRowButton }
|
|
483
|
+
>
|
|
484
|
+
{ rowCells }
|
|
485
|
+
<button
|
|
486
|
+
type="button"
|
|
487
|
+
className={ styles.rowButton }
|
|
488
|
+
onClick={ entry.onClick }
|
|
489
|
+
aria-label={ entry.ariaLabel }
|
|
490
|
+
/>
|
|
491
|
+
<Icon className={ styles.chevron } icon={ chevronRight } size={ 24 } />
|
|
492
|
+
</div>
|
|
484
493
|
</div>
|
|
485
494
|
);
|
|
486
495
|
}
|
|
487
496
|
|
|
488
497
|
if ( entry.onClick ) {
|
|
489
498
|
return (
|
|
490
|
-
<
|
|
499
|
+
<div
|
|
491
500
|
key={ entry.id }
|
|
492
|
-
|
|
501
|
+
role="listitem"
|
|
493
502
|
data-row-index={ rowIndex }
|
|
494
503
|
style={ rowStyle }
|
|
495
|
-
className={
|
|
496
|
-
onClick={ entry.onClick }
|
|
497
|
-
aria-label={ entry.ariaLabel }
|
|
504
|
+
className={ styles.rowItem }
|
|
498
505
|
>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
506
|
+
<button
|
|
507
|
+
type="button"
|
|
508
|
+
className={ clsx( styles.row, styles.interactiveRow ) }
|
|
509
|
+
onClick={ entry.onClick }
|
|
510
|
+
aria-label={ entry.ariaLabel }
|
|
511
|
+
>
|
|
512
|
+
{ rowCells }
|
|
513
|
+
<Icon className={ styles.chevron } icon={ chevronRight } size={ 24 } />
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
502
516
|
);
|
|
503
517
|
}
|
|
504
518
|
|
|
505
519
|
return (
|
|
506
520
|
<div
|
|
507
521
|
key={ entry.id }
|
|
522
|
+
role="listitem"
|
|
508
523
|
data-row-index={ rowIndex }
|
|
509
524
|
style={ rowStyle }
|
|
510
525
|
className={ styles.row }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen, act } from '@testing-library/react';
|
|
1
|
+
import { render, screen, act, within } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { GlobalChartsProvider } from '../../../providers';
|
|
4
4
|
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
@@ -706,6 +706,32 @@ describe( 'LeaderboardChart', () => {
|
|
|
706
706
|
);
|
|
707
707
|
} );
|
|
708
708
|
|
|
709
|
+
it( 'renders each entry as a list item, keeping clickable rows as named buttons', () => {
|
|
710
|
+
render(
|
|
711
|
+
<LeaderboardChart
|
|
712
|
+
data={ [
|
|
713
|
+
mockData[ 0 ],
|
|
714
|
+
{ ...mockData[ 1 ], ariaLabel: 'Open Home', onClick: jest.fn() },
|
|
715
|
+
{
|
|
716
|
+
...mockData[ 1 ],
|
|
717
|
+
id: 'about',
|
|
718
|
+
ariaLabel: 'Open About',
|
|
719
|
+
onClick: jest.fn(),
|
|
720
|
+
hasInteractiveLabel: true,
|
|
721
|
+
},
|
|
722
|
+
] }
|
|
723
|
+
/>
|
|
724
|
+
);
|
|
725
|
+
const items = screen.getAllByRole( 'listitem' );
|
|
726
|
+
expect( items ).toHaveLength( 3 );
|
|
727
|
+
expect(
|
|
728
|
+
within( items[ 1 ] ).getByRole( 'button', { name: 'Open Home' } )
|
|
729
|
+
).toBeInTheDocument();
|
|
730
|
+
expect(
|
|
731
|
+
within( items[ 2 ] ).getByRole( 'button', { name: 'Open About' } )
|
|
732
|
+
).toBeInTheDocument();
|
|
733
|
+
} );
|
|
734
|
+
|
|
709
735
|
it( 'does not render a button for entries without onClick', () => {
|
|
710
736
|
render( <LeaderboardChart data={ [ mockData[ 0 ] ] } /> );
|
|
711
737
|
expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument();
|
|
@@ -272,6 +272,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
272
272
|
const legendInteractive = legend.interactive ?? false;
|
|
273
273
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
274
274
|
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
275
|
+
const legendComparisonSeriesItems = legend.comparisonSeriesItems;
|
|
275
276
|
const legendShape = legend.shape ?? 'line';
|
|
276
277
|
const legendPosition = legend.position ?? 'bottom';
|
|
277
278
|
|
|
@@ -523,6 +524,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
523
524
|
glyphSize: Math.max( 0, toNumber( glyphStyle?.radius ) ?? 4 ),
|
|
524
525
|
collapseGroups: legendCollapseGroups,
|
|
525
526
|
comparisonItem: legendComparisonItem,
|
|
527
|
+
comparisonSeriesItems: legendComparisonSeriesItems,
|
|
526
528
|
renderGlyph,
|
|
527
529
|
} ),
|
|
528
530
|
[
|
|
@@ -530,6 +532,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
530
532
|
glyphStyle?.radius,
|
|
531
533
|
legendCollapseGroups,
|
|
532
534
|
legendComparisonItem,
|
|
535
|
+
legendComparisonSeriesItems,
|
|
533
536
|
renderGlyph,
|
|
534
537
|
]
|
|
535
538
|
);
|
|
@@ -1610,14 +1610,6 @@ describe( 'LineChart', () => {
|
|
|
1610
1610
|
expect( chart ).toHaveAttribute( 'tabIndex', '0' );
|
|
1611
1611
|
expect( chart ).toHaveAttribute( 'role', 'application' );
|
|
1612
1612
|
expect( chart ).toHaveAttribute( 'aria-label', 'Line chart' );
|
|
1613
|
-
} );
|
|
1614
|
-
|
|
1615
|
-
test.each( [
|
|
1616
|
-
[ undefined, 'Line chart' ],
|
|
1617
|
-
[ 'Views over time', 'Views over time' ],
|
|
1618
|
-
] )( 'names the chart container from ariaLabel %p', ( ariaLabel, name ) => {
|
|
1619
|
-
renderWithTheme( { ariaLabel } );
|
|
1620
|
-
expect( screen.getByRole( 'application', { name } ) ).toBeInTheDocument();
|
|
1621
1613
|
expect( screen.queryByLabelText( 'XYChart' ) ).not.toBeInTheDocument();
|
|
1622
1614
|
} );
|
|
1623
1615
|
|
|
@@ -31,8 +31,6 @@ export type RenderLineGlyphProps< Datum extends object > = GlyphProps< Datum > &
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
34
|
-
/** Accessible name of the chart. Defaults to a localized "Line chart". */
|
|
35
|
-
ariaLabel?: string;
|
|
36
34
|
/**
|
|
37
35
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
38
36
|
*/
|
|
@@ -138,11 +138,6 @@ export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
|
|
|
138
138
|
* When provided, replaces the default `label: value` tooltip with custom content.
|
|
139
139
|
*/
|
|
140
140
|
renderTooltip?: ( params: PieChartRenderTooltipParams ) => ReactNode;
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Accessible name of the chart. Defaults to a localized "Pie chart".
|
|
144
|
-
*/
|
|
145
|
-
ariaLabel?: string;
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
// Base props type with optional responsive properties
|
|
@@ -780,14 +780,6 @@ describe( 'PieChart', () => {
|
|
|
780
780
|
{ label: 'Linux', value: 22000, valueDisplay: '22K' },
|
|
781
781
|
];
|
|
782
782
|
|
|
783
|
-
test.each( [
|
|
784
|
-
[ undefined, 'Pie chart' ],
|
|
785
|
-
[ 'Devices', 'Devices' ],
|
|
786
|
-
] )( 'names the focusable chart from ariaLabel %p', ( ariaLabel, name ) => {
|
|
787
|
-
render( <PieChart data={ data } ariaLabel={ ariaLabel } /> );
|
|
788
|
-
expect( screen.getByRole( 'application', { name } ) ).toHaveAttribute( 'tabindex', '0' );
|
|
789
|
-
} );
|
|
790
|
-
|
|
791
783
|
const expectRingOn = ( segment: HTMLElement ) =>
|
|
792
784
|
expect( screen.getByTestId( 'pie-selected-ring' ) ).toHaveAttribute(
|
|
793
785
|
'd',
|
|
@@ -799,6 +791,10 @@ describe( 'PieChart', () => {
|
|
|
799
791
|
render( <PieChart data={ data } withTooltips /> );
|
|
800
792
|
const [ macos, linux ] = screen.getAllByTestId( 'pie-segment' );
|
|
801
793
|
expect( screen.queryByTestId( 'pie-selected-ring' ) ).not.toBeInTheDocument();
|
|
794
|
+
expect( screen.getByRole( 'application', { name: 'Pie chart' } ) ).toHaveAttribute(
|
|
795
|
+
'tabindex',
|
|
796
|
+
'0'
|
|
797
|
+
);
|
|
802
798
|
|
|
803
799
|
await user.tab();
|
|
804
800
|
await user.keyboard( '{ArrowRight}' );
|