@automattic/charts 4.7.0 → 4.9.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 +30 -0
- package/TOKENS.md +6 -3
- package/dist/index.cjs +834 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +68 -17
- package/dist/index.d.cts +102 -28
- package/dist/index.d.ts +102 -28
- package/dist/index.js +838 -327
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +7 -4
- package/src/charts/area-chart/test/area-chart.test.tsx +24 -15
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/area-chart/types.ts +2 -0
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +25 -16
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +106 -62
- package/src/charts/bar-chart/types.ts +2 -0
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +16 -8
- package/src/charts/heatmap-chart/heatmap-chart.tsx +183 -66
- package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
- package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +15 -3
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +6 -3
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
- package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +231 -8
- package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
- package/src/charts/heatmap-chart/types.ts +6 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
- package/src/charts/leaderboard-chart/types.ts +6 -1
- package/src/charts/line-chart/line-chart.tsx +7 -4
- package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
- package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/line-chart/types.ts +5 -3
- package/src/charts/pie-chart/pie-chart.tsx +153 -143
- package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +126 -124
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +55 -1
- package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
- package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
- package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
- package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
- package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
- package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
- package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
- package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
- package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
- package/src/charts/private/test/label-text-color.test.ts +6 -8
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
- package/src/components/tooltip/base-tooltip.tsx +31 -32
- package/src/components/tooltip/index.ts +3 -1
- package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
- package/src/components/tooltip/private/label-value-content.tsx +8 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
- package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
- package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
- package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +283 -365
- package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
- package/src/components/tooltip/tooltip-box.tsx +43 -0
- package/src/components/tooltip/types.ts +7 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
- package/src/hooks/test/use-chart-margin.test.tsx +8 -4
- package/src/hooks/use-chart-margin.tsx +21 -18
- package/src/index.ts +8 -3
- package/src/providers/chart-context/global-charts-provider.tsx +5 -14
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
- package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
- package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
- package/src/styles/chart-scope.scss +1 -0
- package/src/test-utils/zoned-parsing-suite.ts +0 -10
- package/src/types.ts +18 -2
- package/src/utils/test/date-formatting.test.ts +0 -10
- package/src/components/tooltip/accessible-tooltip.tsx +0 -433
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MIN_BACKGROUND_CONTRAST } from '../../../providers/chart-context/private/palette-generator';
|
|
2
|
+
import { contrastRatio } from '../../../providers/chart-context/private/perceptual-color';
|
|
3
|
+
import { mixHexColors } from '../../../utils/color-utils';
|
|
4
|
+
import { getHeatmapScale, HEATMAP_HIGH_CONTRAST } from '../private/heatmap-scale';
|
|
5
|
+
|
|
6
|
+
// The wp-admin scheme colors, then the pre-7.0 ones, which include a sunrise below 3:1 on white.
|
|
7
|
+
const PRIMARIES = [
|
|
8
|
+
'#007cba',
|
|
9
|
+
'#3858e9',
|
|
10
|
+
'#437aa8',
|
|
11
|
+
'#916745',
|
|
12
|
+
'#646c3e',
|
|
13
|
+
'#cf4339',
|
|
14
|
+
'#567958',
|
|
15
|
+
'#ad631e',
|
|
16
|
+
'#0085ba',
|
|
17
|
+
'#dd823b',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe.each( [
|
|
21
|
+
[ 'light', '#ffffff', '#f0f0f0' ],
|
|
22
|
+
[ 'dark', '#242424', '#272727' ],
|
|
23
|
+
] )( 'getHeatmapScale on a %s background', ( _mode, background, emptyCell ) => {
|
|
24
|
+
it.each( PRIMARIES )(
|
|
25
|
+
'keeps the lowest step of %s at 3:1 against the background and empty cell, no further',
|
|
26
|
+
primary => {
|
|
27
|
+
const { low } = getHeatmapScale( primary, background, emptyCell );
|
|
28
|
+
const contrasts = [ contrastRatio( low, background ), contrastRatio( low, emptyCell ) ];
|
|
29
|
+
|
|
30
|
+
expect( Math.min( ...contrasts ) ).toBeGreaterThanOrEqual( MIN_BACKGROUND_CONTRAST );
|
|
31
|
+
expect( Math.min( ...contrasts ) ).toBeLessThan( MIN_BACKGROUND_CONTRAST + 0.1 );
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
it.each( PRIMARIES )( 'runs the scale of %s out to 9:1', primary => {
|
|
36
|
+
const { high } = getHeatmapScale( primary, background );
|
|
37
|
+
|
|
38
|
+
expect( contrastRatio( high, background ) ).toBeGreaterThanOrEqual( HEATMAP_HIGH_CONTRAST );
|
|
39
|
+
} );
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
describe( 'getHeatmapScale', () => {
|
|
43
|
+
it( 'keeps a primary already past 9:1 as the high end', () => {
|
|
44
|
+
expect( getHeatmapScale( '#1d2327', '#ffffff' ).high ).toBe( '#1d2327' );
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
it.each( [
|
|
48
|
+
[ '#ffffff', '#808080' ],
|
|
49
|
+
[ '#ffd000', '#767676' ],
|
|
50
|
+
[ '#3858e9', '#808080' ],
|
|
51
|
+
[ '#1e1e1e', '#999999' ],
|
|
52
|
+
] )( 'keeps every step of %s at 3:1 on a mid-gray %s', ( primary, background ) => {
|
|
53
|
+
const { low, high } = getHeatmapScale( primary, background );
|
|
54
|
+
|
|
55
|
+
for ( let step = 0; step <= 20; step++ ) {
|
|
56
|
+
expect(
|
|
57
|
+
contrastRatio( mixHexColors( low, high, step / 20 ), background )
|
|
58
|
+
).toBeGreaterThanOrEqual( MIN_BACKGROUND_CONTRAST );
|
|
59
|
+
}
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
it.each( [
|
|
63
|
+
[ 'no step clears it', '#ffffff', '#555555' ],
|
|
64
|
+
[ 'clearing it would pass the high end', '#ffffff', '#767676' ],
|
|
65
|
+
[ 'clearing it would pass the high end', '#1e1e1e', '#6b6b6b' ],
|
|
66
|
+
] )(
|
|
67
|
+
'measures against the background alone when %s (%s, %s)',
|
|
68
|
+
( _case, background, emptyCell ) => {
|
|
69
|
+
expect( getHeatmapScale( '#3858e9', background, emptyCell ) ).toEqual(
|
|
70
|
+
getHeatmapScale( '#3858e9', background )
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
it( 'ends on black or white when the background leaves no room for 9:1', () => {
|
|
76
|
+
expect( getHeatmapScale( '#3858e9', '#808080' ).high ).toBe( '#000000' );
|
|
77
|
+
} );
|
|
78
|
+
} );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getValueExtent, getNormalizedValue } from '../private/use-heatmap-colors';
|
|
1
|
+
import { getValueExtent, getNormalizedValue, isEmptyValue } from '../private/use-heatmap-colors';
|
|
2
2
|
import type { HeatmapColumn } from '../types';
|
|
3
3
|
|
|
4
4
|
const data: HeatmapColumn[] = [
|
|
@@ -7,8 +7,12 @@ const data: HeatmapColumn[] = [
|
|
|
7
7
|
];
|
|
8
8
|
|
|
9
9
|
describe( 'getValueExtent', () => {
|
|
10
|
-
test( 'returns [min, max] ignoring null/NaN', () => {
|
|
11
|
-
expect( getValueExtent( data ) ).toEqual( [
|
|
10
|
+
test( 'returns [min, max] ignoring null/NaN and empty zeros', () => {
|
|
11
|
+
expect( getValueExtent( data ) ).toEqual( [ 5, 20 ] );
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
test( 'keeps a zero in the extent when the data has negatives', () => {
|
|
15
|
+
expect( getValueExtent( [ { data: [ { value: -4 }, { value: 0 } ] } ] ) ).toEqual( [ -4, 0 ] );
|
|
12
16
|
} );
|
|
13
17
|
|
|
14
18
|
test( 'leaves summary columns out of the extent', () => {
|
|
@@ -16,11 +20,12 @@ describe( 'getValueExtent', () => {
|
|
|
16
20
|
...data,
|
|
17
21
|
{ label: 'Total', summary: true, data: [ { value: 500 }, { value: 900 }, { value: null } ] },
|
|
18
22
|
];
|
|
19
|
-
expect( getValueExtent( withTotals ) ).toEqual( [
|
|
23
|
+
expect( getValueExtent( withTotals ) ).toEqual( [ 5, 20 ] );
|
|
20
24
|
} );
|
|
21
25
|
|
|
22
26
|
test( 'returns [0, 0] for all-empty data', () => {
|
|
23
27
|
expect( getValueExtent( [ { data: [ { value: null } ] } ] ) ).toEqual( [ 0, 0 ] );
|
|
28
|
+
expect( getValueExtent( [ { data: [ { value: 0 }, { value: 0 } ] } ] ) ).toEqual( [ 0, 0 ] );
|
|
24
29
|
} );
|
|
25
30
|
} );
|
|
26
31
|
|
|
@@ -39,8 +44,15 @@ describe( 'getNormalizedValue', () => {
|
|
|
39
44
|
test( 'returns 1 when min === max', () => {
|
|
40
45
|
expect( getNormalizedValue( 7, [ 7, 7 ] ) ).toBe( 1 );
|
|
41
46
|
} );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
describe( 'isEmptyValue', () => {
|
|
50
|
+
test( 'paints a zero as empty in data with no negatives', () => {
|
|
51
|
+
expect( isEmptyValue( 0, [ 0, 20 ] ) ).toBe( true );
|
|
52
|
+
expect( isEmptyValue( 5, [ 0, 20 ] ) ).toBe( false );
|
|
53
|
+
} );
|
|
42
54
|
|
|
43
|
-
test( '
|
|
44
|
-
expect(
|
|
55
|
+
test( 'keeps a zero on the scale when the data has negatives', () => {
|
|
56
|
+
expect( isEmptyValue( 0, [ -4, 6 ] ) ).toBe( false );
|
|
45
57
|
} );
|
|
46
58
|
} );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BaseChartProps } from '../../types';
|
|
2
2
|
import type { CSSProperties, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
/** A single heatmap cell. `value: null` marks an empty cell. */
|
|
4
|
+
/** A single heatmap cell. `value: null` marks an empty cell, as does a zero when no value is negative. */
|
|
5
5
|
export type HeatmapCell = {
|
|
6
6
|
/** Per-cell label used in the tooltip / accessible name. */
|
|
7
7
|
label?: string;
|
|
@@ -78,6 +78,8 @@ export interface HeatmapChartProps extends Omit<
|
|
|
78
78
|
ariaLabel?: string;
|
|
79
79
|
/** Compact mode: hide in-cell values, tighten gap, thin axis labels. Default false. */
|
|
80
80
|
compact?: boolean;
|
|
81
|
+
/** 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. */
|
|
82
|
+
fitCells?: boolean;
|
|
81
83
|
/** Render the numeric value inside each cell. Default `! compact`. */
|
|
82
84
|
showValues?: boolean;
|
|
83
85
|
/**
|
|
@@ -102,8 +104,9 @@ export interface HeatmapChartProps extends Omit<
|
|
|
102
104
|
/** Floor a cell's height (px) in non-compact mode; see `minCellWidth`. */
|
|
103
105
|
minCellHeight?: number;
|
|
104
106
|
/**
|
|
105
|
-
* Color the cell scale
|
|
106
|
-
* first series palette slot,
|
|
107
|
+
* Color the cell scale is built from, deepened at the highest value toward 9:1
|
|
108
|
+
* against the background. Defaults to the first series palette slot,
|
|
109
|
+
* `--a8c-charts-color-series-1`.
|
|
107
110
|
*/
|
|
108
111
|
primaryColor?: string;
|
|
109
112
|
renderTooltip?: ( data: HeatmapTooltipData ) => ReactNode;
|
|
@@ -193,8 +193,10 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
193
193
|
var(--wpds-motion-easing-subtle);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
&:
|
|
196
|
+
// A layered row's own link or button does not advertise the drill-down.
|
|
197
|
+
&:hover:not(:has(:is(a, button):not(.rowButton):hover)),
|
|
198
|
+
&:focus-visible,
|
|
199
|
+
&:has(> .rowButton:focus-visible) {
|
|
198
200
|
|
|
199
201
|
// Fixed reserve for the chevron — the value slides by it in full, and the
|
|
200
202
|
// bar width inset (see leaderboard-chart.tsx) scales it by the bar's share.
|
|
@@ -214,8 +216,9 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
// In RTL the value slides toward the inline start the other way (rightward).
|
|
217
|
-
[dir="rtl"] &:hover .valueContainer,
|
|
218
|
-
[dir="rtl"] &:focus-visible .valueContainer
|
|
219
|
+
[dir="rtl"] &:hover:not(:has(:is(a, button):not(.rowButton):hover)) .valueContainer,
|
|
220
|
+
[dir="rtl"] &:focus-visible .valueContainer,
|
|
221
|
+
[dir="rtl"] &:has(> .rowButton:focus-visible) .valueContainer {
|
|
219
222
|
transform: translateX(var(--a8c-charts-dimension-leaderboard-bar-hover-inset));
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -231,6 +234,45 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
231
234
|
}
|
|
232
235
|
}
|
|
233
236
|
|
|
237
|
+
// A row whose label holds a link: the button follows the cells for tab order
|
|
238
|
+
// but is layered beneath them, and the cells pass clicks through except on the
|
|
239
|
+
// label's own links and buttons. The label's isolate traps their z-index, so
|
|
240
|
+
// layering the cells is what keeps them above the button.
|
|
241
|
+
.layeredRow {
|
|
242
|
+
isolation: isolate;
|
|
243
|
+
|
|
244
|
+
> :not(.rowButton, .chevron) {
|
|
245
|
+
position: relative;
|
|
246
|
+
z-index: 1;
|
|
247
|
+
pointer-events: none;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
:is(a, button):not(.rowButton) {
|
|
251
|
+
position: relative;
|
|
252
|
+
z-index: 1;
|
|
253
|
+
pointer-events: auto;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.rowButton {
|
|
258
|
+
position: absolute;
|
|
259
|
+
inset: 0;
|
|
260
|
+
appearance: none;
|
|
261
|
+
padding: 0;
|
|
262
|
+
border: 0;
|
|
263
|
+
background: none;
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
z-index: 0;
|
|
266
|
+
|
|
267
|
+
&:focus-visible {
|
|
268
|
+
border-radius: var(--wpds-border-radius-sm);
|
|
269
|
+
outline:
|
|
270
|
+
$focus-ring-width solid
|
|
271
|
+
var(--wpds-color-stroke-focus);
|
|
272
|
+
outline-offset: calc(-1 * #{$focus-ring-width});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
234
276
|
.chevron {
|
|
235
277
|
position: absolute;
|
|
236
278
|
inset-inline-end: var(--wpds-dimension-padding-xs);
|
|
@@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
4
4
|
import { Icon, chevronRight } from '@wordpress/icons';
|
|
5
5
|
import { Stack, Text } from '@wordpress/ui';
|
|
6
6
|
import clsx from 'clsx';
|
|
7
|
-
import { useContext, useMemo, type FC } from 'react';
|
|
7
|
+
import { useContext, useMemo, type FC, type MouseEvent } from 'react';
|
|
8
8
|
import { Legend } from '../../components/legend';
|
|
9
9
|
import { usePrefersReducedMotion } from '../../hooks';
|
|
10
10
|
import {
|
|
@@ -89,6 +89,20 @@ const hasDeltaValue = (
|
|
|
89
89
|
delta: number;
|
|
90
90
|
} => hasPreviousValue( entry ) && entry.delta != null;
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* A layered row's cell that takes pointer events of its own (a value tooltip)
|
|
94
|
+
* sits above the row button; pass its clicks on so they still drill down.
|
|
95
|
+
*
|
|
96
|
+
* @param event - The click on the row.
|
|
97
|
+
*/
|
|
98
|
+
function forwardCellClickToRowButton( event: MouseEvent< HTMLDivElement > ) {
|
|
99
|
+
if ( ( event.target as Element ).closest( 'a, button' ) ) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
event.currentTarget.querySelector< HTMLButtonElement >( ':scope > button' )?.click();
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
const BarLabel = ( { label }: { label: LeaderboardEntry[ 'label' ] } ) => (
|
|
93
107
|
<>{ typeof label === 'string' ? <Text className={ styles.label }>{ label }</Text> : label }</>
|
|
94
108
|
);
|
|
@@ -449,6 +463,28 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
449
463
|
</>
|
|
450
464
|
);
|
|
451
465
|
|
|
466
|
+
if ( entry.onClick && entry.hasInteractiveLabel ) {
|
|
467
|
+
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
|
+
<div
|
|
470
|
+
key={ entry.id }
|
|
471
|
+
data-row-index={ rowIndex }
|
|
472
|
+
style={ rowStyle }
|
|
473
|
+
className={ clsx( styles.row, styles.interactiveRow, styles.layeredRow ) }
|
|
474
|
+
onClick={ forwardCellClickToRowButton }
|
|
475
|
+
>
|
|
476
|
+
{ rowCells }
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
className={ styles.rowButton }
|
|
480
|
+
onClick={ entry.onClick }
|
|
481
|
+
aria-label={ entry.ariaLabel }
|
|
482
|
+
/>
|
|
483
|
+
<Icon className={ styles.chevron } icon={ chevronRight } size={ 24 } />
|
|
484
|
+
</div>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
452
488
|
if ( entry.onClick ) {
|
|
453
489
|
return (
|
|
454
490
|
<button
|
|
@@ -547,6 +547,59 @@ describe( 'LeaderboardChart', () => {
|
|
|
547
547
|
expect( screen.getByRole( 'button' ).tagName ).toBe( 'BUTTON' );
|
|
548
548
|
} );
|
|
549
549
|
|
|
550
|
+
it( 'keeps a label link beside the row button, not inside it, when the label is interactive', async () => {
|
|
551
|
+
const user = userEvent.setup();
|
|
552
|
+
const onClick = jest.fn();
|
|
553
|
+
render(
|
|
554
|
+
<LeaderboardChart
|
|
555
|
+
data={ [
|
|
556
|
+
{
|
|
557
|
+
...mockData[ 0 ],
|
|
558
|
+
label: <a href="/author/1">Author</a>,
|
|
559
|
+
onClick,
|
|
560
|
+
ariaLabel: 'View posts by Author',
|
|
561
|
+
hasInteractiveLabel: true,
|
|
562
|
+
},
|
|
563
|
+
] }
|
|
564
|
+
/>
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const button = screen.getByRole( 'button', { name: 'View posts by Author' } );
|
|
568
|
+
const link = screen.getByRole( 'link', { name: 'Author' } );
|
|
569
|
+
link.addEventListener( 'click', event => event.preventDefault() );
|
|
570
|
+
expect( button ).not.toContainElement( link );
|
|
571
|
+
|
|
572
|
+
await user.click( button );
|
|
573
|
+
expect( onClick ).toHaveBeenCalledTimes( 1 );
|
|
574
|
+
await user.click( link );
|
|
575
|
+
expect( onClick ).toHaveBeenCalledTimes( 1 );
|
|
576
|
+
// A value tooltip takes pointer events above the button; its click still drills down.
|
|
577
|
+
await user.click( screen.getByText( '12.5K' ) );
|
|
578
|
+
expect( onClick ).toHaveBeenCalledTimes( 2 );
|
|
579
|
+
} );
|
|
580
|
+
|
|
581
|
+
it( 'tabs to the label link before the row button when the label is interactive', async () => {
|
|
582
|
+
const user = userEvent.setup();
|
|
583
|
+
render(
|
|
584
|
+
<LeaderboardChart
|
|
585
|
+
data={ [
|
|
586
|
+
{
|
|
587
|
+
...mockData[ 0 ],
|
|
588
|
+
label: <a href="/author/1">Author</a>,
|
|
589
|
+
onClick: jest.fn(),
|
|
590
|
+
ariaLabel: 'View posts by Author',
|
|
591
|
+
hasInteractiveLabel: true,
|
|
592
|
+
},
|
|
593
|
+
] }
|
|
594
|
+
/>
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
await user.tab();
|
|
598
|
+
expect( screen.getByRole( 'link', { name: 'Author' } ) ).toHaveFocus();
|
|
599
|
+
await user.tab();
|
|
600
|
+
expect( screen.getByRole( 'button', { name: 'View posts by Author' } ) ).toHaveFocus();
|
|
601
|
+
} );
|
|
602
|
+
|
|
550
603
|
it( 'calls onClick when the row is clicked', async () => {
|
|
551
604
|
const user = userEvent.setup();
|
|
552
605
|
const onClick = jest.fn();
|
|
@@ -4,7 +4,6 @@ import { BaseChartProps, LeaderboardEntry } from '../../types';
|
|
|
4
4
|
export interface LeaderboardChartProps extends Pick<
|
|
5
5
|
BaseChartProps< LeaderboardEntry >,
|
|
6
6
|
| 'className'
|
|
7
|
-
| 'data'
|
|
8
7
|
| 'showLegend'
|
|
9
8
|
| 'legend'
|
|
10
9
|
| 'chartId'
|
|
@@ -14,6 +13,12 @@ export interface LeaderboardChartProps extends Pick<
|
|
|
14
13
|
| 'gap'
|
|
15
14
|
| 'animation'
|
|
16
15
|
> {
|
|
16
|
+
/**
|
|
17
|
+
* Entries to display. Declared here rather than picked, since the base type
|
|
18
|
+
* would split the entry union into one array type per member.
|
|
19
|
+
*/
|
|
20
|
+
data: LeaderboardEntry[];
|
|
21
|
+
|
|
17
22
|
/**
|
|
18
23
|
* Whether to show comparison data
|
|
19
24
|
*/
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
useCallback,
|
|
16
16
|
} from 'react';
|
|
17
17
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
18
|
-
import {
|
|
18
|
+
import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
|
|
19
19
|
import {
|
|
20
20
|
useXYChartTheme,
|
|
21
21
|
useChartDataTransform,
|
|
@@ -232,6 +232,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
232
232
|
(
|
|
233
233
|
{
|
|
234
234
|
data,
|
|
235
|
+
ariaLabel,
|
|
235
236
|
chartId: providedChartId,
|
|
236
237
|
width,
|
|
237
238
|
height,
|
|
@@ -624,10 +625,11 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
624
625
|
const chartHeight = contentHeight > 0 ? contentHeight : height;
|
|
625
626
|
|
|
626
627
|
return (
|
|
628
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the application role hands arrow keys to the chart's point navigation.
|
|
627
629
|
<div
|
|
628
630
|
ref={ chartRef }
|
|
629
|
-
role="
|
|
630
|
-
aria-label={ __( 'Line chart', 'jetpack-charts' ) }
|
|
631
|
+
role="application"
|
|
632
|
+
aria-label={ ariaLabel ?? __( 'Line chart', 'jetpack-charts' ) }
|
|
631
633
|
tabIndex={ 0 }
|
|
632
634
|
onKeyDown={ onChartKeyDown }
|
|
633
635
|
onFocus={ onChartFocus }
|
|
@@ -637,6 +639,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
637
639
|
<div className={ plotStyles[ 'xy-plot' ] }>
|
|
638
640
|
{ zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
|
|
639
641
|
<XYChart
|
|
642
|
+
accessibilityLabel=""
|
|
640
643
|
theme={ theme }
|
|
641
644
|
width={ width }
|
|
642
645
|
height={ chartHeight }
|
|
@@ -784,7 +787,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
784
787
|
</ZoomClip>
|
|
785
788
|
|
|
786
789
|
{ withTooltips && (
|
|
787
|
-
<
|
|
790
|
+
<XYChartTooltip
|
|
788
791
|
detectBounds
|
|
789
792
|
snapTooltipToDatumX
|
|
790
793
|
tooltipPlacement={ tooltipPlacement }
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { __ } from '@wordpress/i18n';
|
|
2
2
|
import { Stack } from '@wordpress/ui';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { useEffect, useId, useRef, useState, version as reactVersion } from 'react';
|
|
5
5
|
import { isSafari } from '../../../utils';
|
|
6
6
|
import styles from '../line-chart.module.scss';
|
|
7
7
|
import type { ButtonWithPopover, PopoverElement, ToggleEvent } from '../../../types';
|
|
8
|
-
import type { FC } from 'react';
|
|
8
|
+
import type { FC, ReactNode } from 'react';
|
|
9
9
|
|
|
10
10
|
export const POPOVER_BUTTON_SIZE = 44;
|
|
11
11
|
|
|
12
|
+
// @todo Remove this (and uses of it below) when we drop support for WordPress versions shipping React 18.
|
|
13
|
+
const isReact18 = reactVersion.startsWith( '18.' );
|
|
14
|
+
|
|
12
15
|
const CloseIcon = () => (
|
|
13
16
|
<svg
|
|
14
17
|
width="16"
|
|
@@ -29,8 +32,8 @@ const CloseIcon = () => (
|
|
|
29
32
|
interface LineChartAnnotationLabelWithPopoverProps {
|
|
30
33
|
title: string;
|
|
31
34
|
subtitle?: string;
|
|
32
|
-
renderLabel:
|
|
33
|
-
renderLabelPopover:
|
|
35
|
+
renderLabel: ( props: { title: string; subtitle?: string } ) => ReactNode;
|
|
36
|
+
renderLabelPopover: ( props: { title: string; subtitle?: string } ) => ReactNode;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
const LineChartAnnotationLabelWithPopover: FC< LineChartAnnotationLabelWithPopoverProps > = ( {
|
|
@@ -83,7 +86,9 @@ const LineChartAnnotationLabelWithPopover: FC< LineChartAnnotationLabelWithPopov
|
|
|
83
86
|
<div className={ styles[ 'line-chart__annotation-label' ] }>
|
|
84
87
|
<button
|
|
85
88
|
ref={ buttonRef }
|
|
86
|
-
{ ...(
|
|
89
|
+
{ ...( isReact18
|
|
90
|
+
? ( { popovertarget: popoverId } as ButtonWithPopover )
|
|
91
|
+
: { popoverTarget: popoverId } ) }
|
|
87
92
|
className={ styles[ 'line-chart__annotation-label-trigger-button' ] }
|
|
88
93
|
style={ {
|
|
89
94
|
width: `${ POPOVER_BUTTON_SIZE }px`,
|
|
@@ -110,10 +115,15 @@ const LineChartAnnotationLabelWithPopover: FC< LineChartAnnotationLabelWithPopov
|
|
|
110
115
|
{ renderLabelPopover( { title, subtitle } ) }
|
|
111
116
|
</div>
|
|
112
117
|
<button
|
|
113
|
-
{ ...(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
{ ...( isReact18
|
|
119
|
+
? ( {
|
|
120
|
+
popovertarget: popoverId,
|
|
121
|
+
popovertargetaction: 'hide',
|
|
122
|
+
} as ButtonWithPopover )
|
|
123
|
+
: {
|
|
124
|
+
popoverTarget: popoverId,
|
|
125
|
+
popoverTargetAction: 'hide',
|
|
126
|
+
} ) }
|
|
117
127
|
className={ styles[ 'line-chart__annotation-label-popover-close-button' ] }
|
|
118
128
|
aria-label={ __( 'Close', 'jetpack-charts' ) }
|
|
119
129
|
>
|
|
@@ -66,7 +66,7 @@ describe( 'renderDefaultTooltip date heading', () => {
|
|
|
66
66
|
</GlobalChartsProvider>
|
|
67
67
|
);
|
|
68
68
|
|
|
69
|
-
screen.getByRole( '
|
|
69
|
+
screen.getByRole( 'application', { name: /line chart/i } ).focus();
|
|
70
70
|
await user.keyboard( '{ArrowRight}' );
|
|
71
71
|
|
|
72
72
|
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( '3.8.2026' );
|