@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.cjs +624 -228
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +58 -8
  5. package/dist/index.d.cts +42 -8
  6. package/dist/index.d.ts +42 -8
  7. package/dist/index.js +628 -232
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/charts/area-chart/area-chart.tsx +5 -2
  11. package/src/charts/area-chart/test/area-chart.test.tsx +23 -14
  12. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
  13. package/src/charts/area-chart/types.ts +2 -0
  14. package/src/charts/bar-chart/bar-chart.tsx +5 -2
  15. package/src/charts/bar-chart/test/bar-chart.test.tsx +82 -63
  16. package/src/charts/bar-chart/types.ts +2 -0
  17. package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
  18. package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -4
  19. package/src/charts/heatmap-chart/heatmap-chart.tsx +144 -37
  20. package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
  21. package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
  22. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +14 -2
  23. package/src/charts/heatmap-chart/private/index.ts +4 -2
  24. package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
  25. package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
  26. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +132 -1
  27. package/src/charts/heatmap-chart/types.ts +2 -0
  28. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
  29. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
  30. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
  31. package/src/charts/leaderboard-chart/types.ts +6 -1
  32. package/src/charts/line-chart/line-chart.tsx +5 -2
  33. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
  34. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
  35. package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
  36. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
  37. package/src/charts/line-chart/types.ts +5 -3
  38. package/src/charts/pie-chart/pie-chart.tsx +149 -139
  39. package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
  40. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +122 -120
  41. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +54 -0
  42. package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
  43. package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
  44. package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
  45. package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
  46. package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
  47. package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
  48. package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
  49. package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
  50. package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
  51. package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
  52. package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
  53. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
  54. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +20 -19
  55. package/src/components/tooltip/xy-chart-tooltip.tsx +3 -3
  56. 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 grid wrapper, whose keyboard-navigation handler
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
- <div role="grid" tabIndex={ 0 } onKeyDown={ preventDefaultKeydown }>
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( 'grid', { name: /line chart/i } ).focus();
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( 'grid' ), 'focus' );
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( 'grid' ), 'focus' );
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 grid wraps the element `chartRef` points at.
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="grid"
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( 'grid', { name: 'Harness' } ).focus();
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 grid has focus', async () => {
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( 'grid', { name: 'Harness' } ) ).toHaveFocus();
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 grid on Escape when there are no points', async () => {
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( 'grid', { name: 'Harness' } ) ).toHaveFocus();
218
+ expect( screen.getByRole( 'application', { name: 'Harness' } ) ).toHaveFocus();
218
219
  } );
219
220
 
220
- it( 'returns focus to the grid when the count drops to zero while focus is in the chart', async () => {
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( 'grid', { name: 'Harness' } ) ).toHaveFocus();
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( 'grid', { name: 'Harness' } ) ).toHaveFocus();
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 grid = screen.getByRole( 'grid', { name: 'Harness' } );
258
+ const chart = screen.getByRole( 'application', { name: 'Harness' } );
258
259
 
259
- act( () => grid.focus() );
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( () => grid.focus() );
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( 'grid', { name: 'Harness' } ).focus();
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( 'grid', { name: 'Harness' } ).focus();
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( 'grid', { name: 'Harness' } ).focus();
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 grid, so the grid is what focus returns to and is measured against.
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="grid"]' ) ?? chartRef.current,
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
- // WAI-ARIA grid: arrows stop at the first and last cell.
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
- export type LeaderboardEntry = {
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;