@automattic/charts 4.8.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 +14 -0
- package/dist/index.cjs +624 -228
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -8
- package/dist/index.d.cts +42 -8
- package/dist/index.d.ts +42 -8
- package/dist/index.js +628 -232
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +5 -2
- package/src/charts/area-chart/test/area-chart.test.tsx +23 -14
- 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.tsx +5 -2
- package/src/charts/bar-chart/test/bar-chart.test.tsx +82 -63
- 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/heatmap-chart/heatmap-chart.module.scss +10 -4
- package/src/charts/heatmap-chart/heatmap-chart.tsx +144 -37
- 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-legend.tsx +14 -2
- package/src/charts/heatmap-chart/private/index.ts +4 -2
- 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 +132 -1
- package/src/charts/heatmap-chart/types.ts +2 -0
- 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 +5 -2
- 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 +149 -139
- package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +122 -120
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +54 -0
- 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/x-zoom/test/x-zoom.test.tsx +3 -2
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +20 -19
- package/src/components/tooltip/xy-chart-tooltip.tsx +3 -3
- package/src/types.ts +18 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { orderArcsForNavigation } from '../order-arcs-for-navigation';
|
|
2
|
+
|
|
3
|
+
const half = Math.PI / 2;
|
|
4
|
+
|
|
5
|
+
describe( 'orderArcsForNavigation', () => {
|
|
6
|
+
it.each( [
|
|
7
|
+
[
|
|
8
|
+
'full pie, clockwise from 12 o’clock',
|
|
9
|
+
[
|
|
10
|
+
{ id: 'b', startAngle: Math.PI, endAngle: 2 * Math.PI },
|
|
11
|
+
{ id: 'a', startAngle: 0, endAngle: Math.PI },
|
|
12
|
+
],
|
|
13
|
+
[ 'a', 'b' ],
|
|
14
|
+
],
|
|
15
|
+
[
|
|
16
|
+
'clockwise semi-circle, left to right',
|
|
17
|
+
[
|
|
18
|
+
{ id: 'right', startAngle: 0, endAngle: half },
|
|
19
|
+
{ id: 'left', startAngle: -half, endAngle: 0 },
|
|
20
|
+
],
|
|
21
|
+
[ 'left', 'right' ],
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
'counter-clockwise semi-circle, left to right',
|
|
25
|
+
[
|
|
26
|
+
{ id: 'right', startAngle: half, endAngle: 0 },
|
|
27
|
+
{ id: 'left', startAngle: 0, endAngle: -half },
|
|
28
|
+
],
|
|
29
|
+
[ 'left', 'right' ],
|
|
30
|
+
],
|
|
31
|
+
] )( 'orders a %s', ( _, arcs, expected ) => {
|
|
32
|
+
expect( orderArcsForNavigation( arcs ).map( arc => arc.id ) ).toEqual( expected );
|
|
33
|
+
} );
|
|
34
|
+
} );
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { toSvgId } from '../selected-segment-ring';
|
|
2
|
+
|
|
3
|
+
describe( 'toSvgId', () => {
|
|
4
|
+
it.each( [
|
|
5
|
+
[ 'devices:1', 'devices/1' ],
|
|
6
|
+
[ 'a_3a_', 'a:' ],
|
|
7
|
+
[ ':r1:', '_r1_' ],
|
|
8
|
+
] )( 'keeps %s and %s apart', ( first, second ) => {
|
|
9
|
+
const ids = [ toSvgId( first ), toSvgId( second ) ];
|
|
10
|
+
|
|
11
|
+
expect( ids[ 0 ] ).not.toBe( ids[ 1 ] );
|
|
12
|
+
ids.forEach( id => expect( id ).toMatch( /^[A-Za-z0-9_-]+$/ ) );
|
|
13
|
+
} );
|
|
14
|
+
} );
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useTooltip } from '@visx/tooltip';
|
|
2
|
+
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useKeyboardNavigation } from '../../../components/tooltip';
|
|
4
|
+
import type { DataPointPercentageCalculated } from '../../../types';
|
|
5
|
+
import type { MouseEvent } from 'react';
|
|
6
|
+
|
|
7
|
+
interface UsePieKeyboardNavigationProps< Arc > {
|
|
8
|
+
/** The visible arcs in `orderArcsForNavigation` order, which indexes run over. */
|
|
9
|
+
navigationArcs: Arc[];
|
|
10
|
+
withTooltips: boolean;
|
|
11
|
+
tooltipOffsetX: number;
|
|
12
|
+
tooltipOffsetY: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const usePieKeyboardNavigation = < Arc extends { data: DataPointPercentageCalculated } >( {
|
|
16
|
+
navigationArcs,
|
|
17
|
+
withTooltips,
|
|
18
|
+
tooltipOffsetX,
|
|
19
|
+
tooltipOffsetY,
|
|
20
|
+
}: UsePieKeyboardNavigationProps< Arc > ) => {
|
|
21
|
+
const chartRef = useRef< HTMLDivElement >( null );
|
|
22
|
+
const svgRef = useRef< SVGSVGElement >( null );
|
|
23
|
+
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
|
|
24
|
+
const [ isNavigating, setIsNavigating ] = useState( false );
|
|
25
|
+
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
|
|
26
|
+
useTooltip< DataPointPercentageCalculated >();
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
tooltipRef: focusTooltip,
|
|
30
|
+
onChartFocus,
|
|
31
|
+
onChartBlur,
|
|
32
|
+
onChartKeyDown,
|
|
33
|
+
onChartPointerMove,
|
|
34
|
+
} = useKeyboardNavigation( {
|
|
35
|
+
selectedIndex,
|
|
36
|
+
setSelectedIndex,
|
|
37
|
+
isNavigating,
|
|
38
|
+
setIsNavigating,
|
|
39
|
+
chartRef,
|
|
40
|
+
totalPoints: navigationArcs.length,
|
|
41
|
+
visibleSeriesKey: JSON.stringify( navigationArcs.map( arcDatum => arcDatum.data.label ) ),
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
const selectedArc = selectedIndex === undefined ? undefined : navigationArcs[ selectedIndex ];
|
|
45
|
+
|
|
46
|
+
const keyboardTooltip = useRef< HTMLDivElement | null >( null );
|
|
47
|
+
const tooltipLostFocus = useRef( false );
|
|
48
|
+
|
|
49
|
+
// React detaches the ref before removing the node, so the tooltip still holds focus here.
|
|
50
|
+
const tooltipRef = useCallback(
|
|
51
|
+
( element: HTMLDivElement | null ) => {
|
|
52
|
+
if ( element ) {
|
|
53
|
+
keyboardTooltip.current = element;
|
|
54
|
+
focusTooltip( element );
|
|
55
|
+
} else if (
|
|
56
|
+
keyboardTooltip.current?.ownerDocument.activeElement === keyboardTooltip.current
|
|
57
|
+
) {
|
|
58
|
+
tooltipLostFocus.current = true;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[ focusTooltip ]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Arrow keys mount the next tooltip in the same commit, so focus only reaches the body when the selection's tooltip went away.
|
|
65
|
+
useLayoutEffect( () => {
|
|
66
|
+
if ( ! tooltipLostFocus.current ) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
tooltipLostFocus.current = false;
|
|
70
|
+
const { activeElement, body } = chartRef.current?.ownerDocument ?? document;
|
|
71
|
+
if ( activeElement === null || activeElement === body ) {
|
|
72
|
+
chartRef.current?.focus();
|
|
73
|
+
}
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
const getSegmentHandlers = ( data: DataPointPercentageCalculated, navigationIndex: number ) => ( {
|
|
77
|
+
onMouseMove: ( event: MouseEvent< SVGElement > ) => {
|
|
78
|
+
onChartPointerMove( navigationIndex );
|
|
79
|
+
if ( ! withTooltips ) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The tooltip renders inside `chartRef`, so pointer coordinates are taken relative to it.
|
|
84
|
+
const bounds = chartRef.current?.getBoundingClientRect();
|
|
85
|
+
if ( ! bounds ) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
showTooltip( {
|
|
90
|
+
tooltipData: data,
|
|
91
|
+
tooltipLeft: event.clientX - bounds.left + tooltipOffsetX,
|
|
92
|
+
tooltipTop: event.clientY - bounds.top + tooltipOffsetY,
|
|
93
|
+
} );
|
|
94
|
+
},
|
|
95
|
+
onMouseLeave: withTooltips ? hideTooltip : undefined,
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Anchors the keyboard tooltip at `point`, an offset from the origin of the group the arcs are drawn in.
|
|
100
|
+
*
|
|
101
|
+
* @param originX - The group's x in SVG coordinates.
|
|
102
|
+
* @param originY - The group's y in SVG coordinates.
|
|
103
|
+
* @param point - The point relative to the group, such as an arc centroid.
|
|
104
|
+
* @return The anchor in SVG coordinates, or undefined without tooltips.
|
|
105
|
+
*/
|
|
106
|
+
const getKeyboardTooltipAnchor = useCallback(
|
|
107
|
+
( originX: number, originY: number, [ x, y ]: [ number, number ] ) =>
|
|
108
|
+
withTooltips
|
|
109
|
+
? { left: originX + x + tooltipOffsetX, top: originY + y + tooltipOffsetY }
|
|
110
|
+
: undefined,
|
|
111
|
+
[ withTooltips, tooltipOffsetX, tooltipOffsetY ]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
chartRef,
|
|
116
|
+
svgRef,
|
|
117
|
+
selectedArc,
|
|
118
|
+
getSegmentHandlers,
|
|
119
|
+
getKeyboardTooltipAnchor,
|
|
120
|
+
selectionOutputProps: {
|
|
121
|
+
withTooltips,
|
|
122
|
+
selectedIndex,
|
|
123
|
+
selectedData: selectedArc?.data,
|
|
124
|
+
chartRef,
|
|
125
|
+
svgRef,
|
|
126
|
+
tooltipRef,
|
|
127
|
+
pointerTooltip: tooltipOpen
|
|
128
|
+
? { data: tooltipData, left: tooltipLeft || 0, top: tooltipTop || 0 }
|
|
129
|
+
: undefined,
|
|
130
|
+
},
|
|
131
|
+
chartProps: {
|
|
132
|
+
role: 'application',
|
|
133
|
+
tabIndex: 0,
|
|
134
|
+
onKeyDown: onChartKeyDown,
|
|
135
|
+
onFocus: onChartFocus,
|
|
136
|
+
onBlur: onChartBlur,
|
|
137
|
+
} as const,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
@@ -212,11 +212,12 @@ describe( 'ZoomResetButton', () => {
|
|
|
212
212
|
|
|
213
213
|
test( 'keyboard activation survives the chart wrapper keydown handler', async () => {
|
|
214
214
|
const onClick = jest.fn();
|
|
215
|
-
// Mirrors the chart's
|
|
215
|
+
// Mirrors the chart's focusable container, whose keyboard-navigation handler
|
|
216
216
|
// calls preventDefault() on bubbled keydowns — which would cancel the
|
|
217
217
|
// native Enter/Space button activation.
|
|
218
218
|
render(
|
|
219
|
-
|
|
219
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- mirrors the charts' container.
|
|
220
|
+
<div role="application" tabIndex={ 0 } onKeyDown={ preventDefaultKeydown }>
|
|
220
221
|
<ZoomResetButton onClick={ onClick } />
|
|
221
222
|
</div>
|
|
222
223
|
);
|
|
@@ -42,7 +42,7 @@ const renderChart = ( scope?: HTMLElement, tooltipPlacement?: 'auto' | 'below-ax
|
|
|
42
42
|
const openTooltip = async () => {
|
|
43
43
|
const user = userEvent.setup();
|
|
44
44
|
|
|
45
|
-
screen.getByRole( '
|
|
45
|
+
screen.getByRole( 'application', { name: /line chart/i } ).focus();
|
|
46
46
|
await user.keyboard( '{ArrowRight}' );
|
|
47
47
|
|
|
48
48
|
return screen.getByTestId( 'xy-chart-tooltip-crosshair-vertical' );
|
|
@@ -99,7 +99,7 @@ describe( 'XYChartTooltip', () => {
|
|
|
99
99
|
const user = userEvent.setup();
|
|
100
100
|
renderChart( undefined, 'below-axis' );
|
|
101
101
|
await openTooltip();
|
|
102
|
-
const focus = jest.spyOn( screen.getByRole( '
|
|
102
|
+
const focus = jest.spyOn( screen.getByRole( 'application' ), 'focus' );
|
|
103
103
|
try {
|
|
104
104
|
await user.keyboard( keys );
|
|
105
105
|
expect( focus ).toHaveBeenCalledWith( { preventScroll: true } );
|
|
@@ -115,7 +115,7 @@ describe( 'XYChartTooltip', () => {
|
|
|
115
115
|
const user = userEvent.setup();
|
|
116
116
|
renderChart();
|
|
117
117
|
await openTooltip();
|
|
118
|
-
const focus = jest.spyOn( screen.getByRole( '
|
|
118
|
+
const focus = jest.spyOn( screen.getByRole( 'application' ), 'focus' );
|
|
119
119
|
try {
|
|
120
120
|
await user.keyboard( keys );
|
|
121
121
|
expect( focus ).toHaveBeenCalledWith();
|
|
@@ -131,7 +131,7 @@ describe( 'XYChartTooltip', () => {
|
|
|
131
131
|
} );
|
|
132
132
|
} );
|
|
133
133
|
|
|
134
|
-
// Mirrors the charts' markup: the focusable
|
|
134
|
+
// Mirrors the charts' markup: the focusable chart container wraps the element `chartRef` points at.
|
|
135
135
|
const NavigationHarness = ( {
|
|
136
136
|
totalPoints,
|
|
137
137
|
trackBlur = false,
|
|
@@ -155,8 +155,9 @@ const NavigationHarness = ( {
|
|
|
155
155
|
|
|
156
156
|
return (
|
|
157
157
|
<>
|
|
158
|
+
{ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- mirrors the charts' container. */ }
|
|
158
159
|
<div
|
|
159
|
-
role="
|
|
160
|
+
role="application"
|
|
160
161
|
aria-label="Harness"
|
|
161
162
|
tabIndex={ 0 }
|
|
162
163
|
onKeyDown={ onChartKeyDown }
|
|
@@ -179,7 +180,7 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
179
180
|
const user = userEvent.setup();
|
|
180
181
|
const view = render( <NavigationHarness totalPoints={ 6 } /> );
|
|
181
182
|
|
|
182
|
-
screen.getByRole( '
|
|
183
|
+
screen.getByRole( 'application', { name: 'Harness' } ).focus();
|
|
183
184
|
for ( let i = 0; i < presses; i++ ) {
|
|
184
185
|
await user.keyboard( '{ArrowRight}' );
|
|
185
186
|
}
|
|
@@ -188,13 +189,13 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
188
189
|
return { user, view };
|
|
189
190
|
};
|
|
190
191
|
|
|
191
|
-
it( 'moves the selection to the last point when the count shrinks while the
|
|
192
|
+
it( 'moves the selection to the last point when the count shrinks while the chart has focus', async () => {
|
|
192
193
|
const { view } = await navigate( 6 );
|
|
193
194
|
|
|
194
195
|
view.rerender( <NavigationHarness totalPoints={ 3 } /> );
|
|
195
196
|
|
|
196
197
|
expect( screen.getByTestId( 'selected-index' ) ).toHaveTextContent( '2' );
|
|
197
|
-
expect( screen.getByRole( '
|
|
198
|
+
expect( screen.getByRole( 'application', { name: 'Harness' } ) ).toHaveFocus();
|
|
198
199
|
} );
|
|
199
200
|
|
|
200
201
|
// In range rather than past the end: the effect reconciles on any count change, not only an overflow.
|
|
@@ -207,24 +208,24 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
207
208
|
expect( screen.getByTestId( 'selected-index' ) ).toHaveTextContent( 'none' );
|
|
208
209
|
} );
|
|
209
210
|
|
|
210
|
-
it( 'returns focus to the
|
|
211
|
+
it( 'returns focus to the chart on Escape when there are no points', async () => {
|
|
211
212
|
const user = userEvent.setup();
|
|
212
213
|
render( <NavigationHarness totalPoints={ 0 } /> );
|
|
213
214
|
|
|
214
215
|
screen.getByRole( 'button', { name: 'Inside' } ).focus();
|
|
215
216
|
await user.keyboard( '{Escape}' );
|
|
216
217
|
|
|
217
|
-
expect( screen.getByRole( '
|
|
218
|
+
expect( screen.getByRole( 'application', { name: 'Harness' } ) ).toHaveFocus();
|
|
218
219
|
} );
|
|
219
220
|
|
|
220
|
-
it( 'returns focus to the
|
|
221
|
+
it( 'returns focus to the chart when the count drops to zero while focus is in the chart', async () => {
|
|
221
222
|
const { view } = await navigate( 2 );
|
|
222
223
|
|
|
223
224
|
screen.getByRole( 'button', { name: 'Inside' } ).focus();
|
|
224
225
|
view.rerender( <NavigationHarness totalPoints={ 0 } /> );
|
|
225
226
|
|
|
226
227
|
expect( screen.getByTestId( 'selected-index' ) ).toHaveTextContent( 'none' );
|
|
227
|
-
expect( screen.getByRole( '
|
|
228
|
+
expect( screen.getByRole( 'application', { name: 'Harness' } ) ).toHaveFocus();
|
|
228
229
|
} );
|
|
229
230
|
|
|
230
231
|
it( 'hands a keyboard selection to the pointer and continues from where the pointer is', async () => {
|
|
@@ -234,7 +235,7 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
234
235
|
await user.hover( screen.getByTestId( 'point-4' ) );
|
|
235
236
|
|
|
236
237
|
expect( screen.getByTestId( 'selected-index' ) ).toHaveTextContent( 'none' );
|
|
237
|
-
expect( screen.getByRole( '
|
|
238
|
+
expect( screen.getByRole( 'application', { name: 'Harness' } ) ).toHaveFocus();
|
|
238
239
|
|
|
239
240
|
await user.keyboard( '{ArrowRight}' );
|
|
240
241
|
|
|
@@ -254,13 +255,13 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
254
255
|
it( 'restarts from the first point once focus leaves after the pointer took over', async () => {
|
|
255
256
|
const user = userEvent.setup();
|
|
256
257
|
render( <NavigationHarness totalPoints={ 6 } trackBlur /> );
|
|
257
|
-
const
|
|
258
|
+
const chart = screen.getByRole( 'application', { name: 'Harness' } );
|
|
258
259
|
|
|
259
|
-
act( () =>
|
|
260
|
+
act( () => chart.focus() );
|
|
260
261
|
await user.keyboard( '{ArrowRight}{ArrowRight}' );
|
|
261
262
|
await user.hover( screen.getByTestId( 'point-4' ) );
|
|
262
263
|
act( () => screen.getByRole( 'button', { name: 'Outside' } ).focus() );
|
|
263
|
-
act( () =>
|
|
264
|
+
act( () => chart.focus() );
|
|
264
265
|
await user.keyboard( '{ArrowRight}' );
|
|
265
266
|
|
|
266
267
|
expect( screen.getByTestId( 'selected-index' ) ).toHaveTextContent( '0' );
|
|
@@ -281,7 +282,7 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
281
282
|
const user = userEvent.setup();
|
|
282
283
|
render( <NavigationHarness totalPoints={ 6 } /> );
|
|
283
284
|
|
|
284
|
-
screen.getByRole( '
|
|
285
|
+
screen.getByRole( 'application', { name: 'Harness' } ).focus();
|
|
285
286
|
await user.hover( screen.getByTestId( 'point-4' ) );
|
|
286
287
|
await user.keyboard( '{ArrowRight}' );
|
|
287
288
|
|
|
@@ -301,7 +302,7 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
301
302
|
const { onKeyDown, stop } = recordPageKeyDown();
|
|
302
303
|
|
|
303
304
|
try {
|
|
304
|
-
screen.getByRole( '
|
|
305
|
+
screen.getByRole( 'application', { name: 'Harness' } ).focus();
|
|
305
306
|
await user.keyboard( '{ArrowDown}{PageDown}[Space]' );
|
|
306
307
|
|
|
307
308
|
expect( onKeyDown ).toHaveBeenCalledTimes( 3 );
|
|
@@ -320,7 +321,7 @@ describe( 'useKeyboardNavigation', () => {
|
|
|
320
321
|
const { onKeyDown, stop } = recordPageKeyDown();
|
|
321
322
|
|
|
322
323
|
try {
|
|
323
|
-
screen.getByRole( '
|
|
324
|
+
screen.getByRole( 'application', { name: 'Harness' } ).focus();
|
|
324
325
|
await user.keyboard( '{Escape}' );
|
|
325
326
|
|
|
326
327
|
expect( onKeyDown.mock.calls[ 0 ][ 0 ].defaultPrevented ).toBe( false );
|
|
@@ -261,10 +261,10 @@ export const useKeyboardNavigation = ( {
|
|
|
261
261
|
preventTooltipScroll = false,
|
|
262
262
|
visibleSeriesKey,
|
|
263
263
|
}: UseKeyboardNavigationProps ) => {
|
|
264
|
-
// `chartRef` sits inside the focusable
|
|
264
|
+
// `chartRef` sits inside the focusable chart container, so that container is what focus returns to and is measured against.
|
|
265
265
|
const getChartRoot = useCallback(
|
|
266
266
|
(): HTMLElement | null =>
|
|
267
|
-
chartRef.current?.closest< HTMLElement >( '[role="
|
|
267
|
+
chartRef.current?.closest< HTMLElement >( '[role="application"]' ) ?? chartRef.current,
|
|
268
268
|
[ chartRef ]
|
|
269
269
|
);
|
|
270
270
|
|
|
@@ -401,7 +401,7 @@ export const useKeyboardNavigation = ( {
|
|
|
401
401
|
pointerIndex.current = undefined;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
//
|
|
404
|
+
// Arrows stop at the first and last point.
|
|
405
405
|
if ( event.key === 'ArrowRight' ) {
|
|
406
406
|
event.preventDefault();
|
|
407
407
|
setIsNavigating( true );
|
package/src/types.ts
CHANGED
|
@@ -191,7 +191,7 @@ export type DataPointDate = {
|
|
|
191
191
|
color?: string;
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
type LeaderboardEntryFields = {
|
|
195
195
|
/**
|
|
196
196
|
* Unique internal key (e.g., 'key-direct')
|
|
197
197
|
*/
|
|
@@ -245,7 +245,8 @@ export type LeaderboardEntry = {
|
|
|
245
245
|
* For links or other interactive affordances (external-link icons, info
|
|
246
246
|
* tooltips), put them in the `label` render prop instead of using onClick —
|
|
247
247
|
* a row is either a button (onClick) or carries interactive label content,
|
|
248
|
-
* never both, since interactive elements cannot be nested in HTML.
|
|
248
|
+
* never both, since interactive elements cannot be nested in HTML. The
|
|
249
|
+
* exception is a row that sets `hasInteractiveLabel`.
|
|
249
250
|
*/
|
|
250
251
|
onClick?: ( event: MouseEvent< HTMLButtonElement > ) => void;
|
|
251
252
|
|
|
@@ -261,6 +262,21 @@ export type LeaderboardEntry = {
|
|
|
261
262
|
ariaLabel?: string;
|
|
262
263
|
};
|
|
263
264
|
|
|
265
|
+
export type LeaderboardEntry = LeaderboardEntryFields &
|
|
266
|
+
(
|
|
267
|
+
| { hasInteractiveLabel?: false }
|
|
268
|
+
| {
|
|
269
|
+
/**
|
|
270
|
+
* Set when the `label` holds its own link or button. The row's button
|
|
271
|
+
* then sits behind the row instead of around it, so both stay operable.
|
|
272
|
+
* Requires `onClick`, and `ariaLabel` since the button wraps no text.
|
|
273
|
+
*/
|
|
274
|
+
hasInteractiveLabel: true;
|
|
275
|
+
onClick: ( event: MouseEvent< HTMLButtonElement > ) => void;
|
|
276
|
+
ariaLabel: string;
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
264
280
|
export type GradientStop = {
|
|
265
281
|
offset: string;
|
|
266
282
|
color?: string;
|