@automattic/charts 4.8.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/index.cjs +766 -277
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +72 -8
  5. package/dist/index.d.cts +54 -15
  6. package/dist/index.d.ts +54 -15
  7. package/dist/index.js +770 -281
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/charts/area-chart/area-chart.tsx +7 -3
  11. package/src/charts/area-chart/test/area-chart.test.tsx +15 -14
  12. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
  13. package/src/charts/bar-chart/bar-chart.tsx +12 -4
  14. package/src/charts/bar-chart/test/bar-chart.test.tsx +74 -63
  15. package/src/charts/bar-list-chart/bar-list-chart.tsx +3 -0
  16. package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
  17. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +23 -7
  18. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +22 -0
  19. package/src/charts/conversion-funnel-chart/types.ts +1 -1
  20. package/src/charts/geo-chart/geo-chart.tsx +17 -2
  21. package/src/charts/geo-chart/test/geo-chart.test.tsx +9 -4
  22. package/src/charts/geo-chart/types.ts +1 -1
  23. package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -4
  24. package/src/charts/heatmap-chart/heatmap-chart.tsx +144 -37
  25. package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
  26. package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
  27. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +14 -2
  28. package/src/charts/heatmap-chart/private/index.ts +4 -2
  29. package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
  30. package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
  31. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +132 -7
  32. package/src/charts/heatmap-chart/types.ts +2 -2
  33. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
  34. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +52 -4
  35. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +60 -9
  36. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +80 -1
  37. package/src/charts/leaderboard-chart/types.ts +7 -1
  38. package/src/charts/line-chart/line-chart.tsx +8 -2
  39. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
  40. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
  41. package/src/charts/line-chart/test/line-chart.test.tsx +37 -35
  42. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
  43. package/src/charts/line-chart/types.ts +3 -3
  44. package/src/charts/pie-chart/pie-chart.module.scss +3 -0
  45. package/src/charts/pie-chart/pie-chart.tsx +144 -139
  46. package/src/charts/pie-chart/test/pie-chart.test.tsx +167 -0
  47. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +3 -0
  48. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +117 -120
  49. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +54 -0
  50. package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
  51. package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
  52. package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
  53. package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
  54. package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
  55. package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
  56. package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +49 -0
  57. package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
  58. package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
  59. package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
  60. package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
  61. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
  62. package/src/charts/test/accessible-name.test.tsx +181 -0
  63. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +63 -3
  64. package/src/components/legend/hooks/use-chart-legend-items.ts +63 -1
  65. package/src/components/legend/private/base-legend.tsx +27 -15
  66. package/src/components/legend/test/legend.test.tsx +42 -0
  67. package/src/components/legend/types.ts +2 -0
  68. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +20 -19
  69. package/src/components/tooltip/xy-chart-tooltip.tsx +3 -3
  70. package/src/hooks/test/use-xychart-theme.test.tsx +22 -0
  71. package/src/hooks/use-xychart-theme.ts +33 -2
  72. package/src/index.ts +1 -0
  73. package/src/types.ts +37 -3
  74. package/src/utils/resolve-font-size.ts +9 -10
  75. package/src/utils/test/resolve-font-size.test.ts +11 -2
@@ -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'
@@ -13,7 +12,14 @@ export interface LeaderboardChartProps extends Pick<
13
12
  | 'size'
14
13
  | 'gap'
15
14
  | 'animation'
15
+ | 'ariaLabel'
16
16
  > {
17
+ /**
18
+ * Entries to display. Declared here rather than picked, since the base type
19
+ * would split the entry union into one array type per member.
20
+ */
21
+ data: LeaderboardEntry[];
22
+
17
23
  /**
18
24
  * Whether to show comparison data
19
25
  */
@@ -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,
@@ -271,6 +272,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
271
272
  const legendInteractive = legend.interactive ?? false;
272
273
  const legendCollapseGroups = legend.collapseGroups ?? false;
273
274
  const legendComparisonItem = legend.comparisonItem ?? false;
275
+ const legendComparisonSeriesItems = legend.comparisonSeriesItems;
274
276
  const legendShape = legend.shape ?? 'line';
275
277
  const legendPosition = legend.position ?? 'bottom';
276
278
 
@@ -522,6 +524,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
522
524
  glyphSize: Math.max( 0, toNumber( glyphStyle?.radius ) ?? 4 ),
523
525
  collapseGroups: legendCollapseGroups,
524
526
  comparisonItem: legendComparisonItem,
527
+ comparisonSeriesItems: legendComparisonSeriesItems,
525
528
  renderGlyph,
526
529
  } ),
527
530
  [
@@ -529,6 +532,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
529
532
  glyphStyle?.radius,
530
533
  legendCollapseGroups,
531
534
  legendComparisonItem,
535
+ legendComparisonSeriesItems,
532
536
  renderGlyph,
533
537
  ]
534
538
  );
@@ -624,10 +628,11 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
624
628
  const chartHeight = contentHeight > 0 ? contentHeight : height;
625
629
 
626
630
  return (
631
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the application role hands arrow keys to the chart's point navigation.
627
632
  <div
628
633
  ref={ chartRef }
629
- role="grid"
630
- aria-label={ __( 'Line chart', 'jetpack-charts' ) }
634
+ role="application"
635
+ aria-label={ ariaLabel ?? __( 'Line chart', 'jetpack-charts' ) }
631
636
  tabIndex={ 0 }
632
637
  onKeyDown={ onChartKeyDown }
633
638
  onFocus={ onChartFocus }
@@ -637,6 +642,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
637
642
  <div className={ plotStyles[ 'xy-plot' ] }>
638
643
  { zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
639
644
  <XYChart
645
+ accessibilityLabel=""
640
646
  theme={ theme }
641
647
  width={ width }
642
648
  height={ chartHeight }
@@ -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: FC< { title: string; subtitle?: string } >;
33
- renderLabelPopover: FC< { title: string; subtitle?: string } >;
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
- { ...( { popovertarget: popoverId } as ButtonWithPopover ) }
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
- popovertarget: popoverId,
115
- popovertargetaction: 'hide',
116
- } as ButtonWithPopover ) }
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( 'grid', { name: /line chart/i } ).focus();
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' );
@@ -120,7 +120,7 @@ describe( 'LineChart', () => {
120
120
  tooltipPlacement,
121
121
  tooltipStyle: { backgroundColor: 'black', color: 'white', boxShadow: 'none' },
122
122
  } );
123
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
123
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
124
124
  await user.keyboard( '{ArrowRight}' );
125
125
 
126
126
  const vertical = screen.getByTestId( 'xy-chart-tooltip-crosshair-vertical' );
@@ -143,7 +143,7 @@ describe( 'LineChart', () => {
143
143
  test( 'passes a color-only tooltipStyle through with no background added', async () => {
144
144
  const user = userEvent.setup();
145
145
  renderWithTheme( { tooltipStyle: { color: 'white' } } );
146
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
146
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
147
147
  await user.keyboard( '{ArrowRight}' );
148
148
 
149
149
  const box = screen.getByTestId( 'bounded-tooltip' );
@@ -154,7 +154,7 @@ describe( 'LineChart', () => {
154
154
  test( 'paints a tooltipStyle background on the box, not on the default content', async () => {
155
155
  const user = userEvent.setup();
156
156
  renderWithTheme( { tooltipStyle: { background: 'black' } } );
157
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
157
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
158
158
  await user.keyboard( '{ArrowRight}' );
159
159
 
160
160
  expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( { background: 'black' } );
@@ -164,7 +164,7 @@ describe( 'LineChart', () => {
164
164
  test( 'preserves the existing tooltip colors for noncolor overrides', async () => {
165
165
  const user = userEvent.setup();
166
166
  renderWithTheme( { tooltipStyle: { boxShadow: 'none' } } );
167
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
167
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
168
168
  await user.keyboard( '{ArrowRight}' );
169
169
 
170
170
  const content = screen.getByTestId( 'line-chart-tooltip-content' );
@@ -179,13 +179,13 @@ describe( 'LineChart', () => {
179
179
  } );
180
180
 
181
181
  test.each( [ [ 'Escape', '{Escape}' ] ] )(
182
- 'returns focus to the grid after %s',
182
+ 'returns focus to the chart after %s',
183
183
  async ( _name, keys ) => {
184
184
  jest.useFakeTimers();
185
185
  try {
186
186
  const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
187
187
  renderWithTheme();
188
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
188
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
189
189
 
190
190
  await user.tab();
191
191
  expect( chart ).toHaveFocus();
@@ -225,7 +225,7 @@ describe( 'LineChart', () => {
225
225
  ],
226
226
  } );
227
227
  // Should render without crashing and show the single point
228
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
228
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
229
229
  } );
230
230
 
231
231
  test( 'renders a bucket with no reading instead of failing the whole chart', () => {
@@ -241,7 +241,7 @@ describe( 'LineChart', () => {
241
241
  ],
242
242
  } );
243
243
  expect( screen.queryByText( /invalid data/i ) ).not.toBeInTheDocument();
244
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
244
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
245
245
  } );
246
246
 
247
247
  test( 'tooltip reads No data for a bucket with no reading', async () => {
@@ -258,7 +258,7 @@ describe( 'LineChart', () => {
258
258
  ],
259
259
  } );
260
260
 
261
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
261
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
262
262
  await user.keyboard( '{ArrowRight}' );
263
263
 
264
264
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -282,7 +282,7 @@ describe( 'LineChart', () => {
282
282
 
283
283
  expect( screen.queryByText( /invalid data/i ) ).not.toBeInTheDocument();
284
284
 
285
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
285
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
286
286
  const ticks = within( chart )
287
287
  .getAllByText( /^-?[\d.,]+$/ )
288
288
  .map( el => el.textContent ?? '' );
@@ -305,7 +305,7 @@ describe( 'LineChart', () => {
305
305
  ],
306
306
  } );
307
307
 
308
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
308
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
309
309
  const ticks = within( chart )
310
310
  .getAllByText( /^-?[\d.,]+$/ )
311
311
  .map( el => el.textContent );
@@ -333,7 +333,7 @@ describe( 'LineChart', () => {
333
333
  ],
334
334
  } );
335
335
 
336
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
336
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
337
337
  const ticks = within( chart )
338
338
  .getAllByText( /^-?[\d.,]+$/ )
339
339
  .map( el => el.textContent );
@@ -353,7 +353,7 @@ describe( 'LineChart', () => {
353
353
  ],
354
354
  } );
355
355
 
356
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
356
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
357
357
  expect( within( chart ).getAllByText( /^-?[\d.,]+$/ ).length ).toBeGreaterThan( 2 );
358
358
  } );
359
359
 
@@ -368,7 +368,7 @@ describe( 'LineChart', () => {
368
368
  options: { axis: { y: { tickValues: [ 0, 0.5, 1 ] } } },
369
369
  } );
370
370
 
371
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
371
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
372
372
  expect( within( chart ).getAllByText( /^-?[\d.,]+$/ ) ).toHaveLength( 3 );
373
373
  } );
374
374
 
@@ -389,7 +389,7 @@ describe( 'LineChart', () => {
389
389
  },
390
390
  } );
391
391
 
392
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
392
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
393
393
  expect( within( chart ).getAllByText( /^\d+%$/ ) ).toHaveLength( 6 );
394
394
  } );
395
395
 
@@ -544,9 +544,9 @@ describe( 'LineChart', () => {
544
544
  ],
545
545
  } );
546
546
 
547
- const grid = screen.getByRole( 'grid', { name: /line chart/i } );
547
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
548
548
  // eslint-disable-next-line testing-library/no-node-access -- visx's stroke is a <path class="visx-line">; the axis and grid lines are plain <line> elements sharing that class.
549
- const linePath = grid.querySelector( 'path.visx-line' );
549
+ const linePath = chart.querySelector( 'path.visx-line' );
550
550
  const gapCount = linePath?.getAttribute( 'd' )?.match( /M/g )?.length;
551
551
  expect( gapCount ).toBe( 2 );
552
552
  } );
@@ -615,7 +615,7 @@ describe( 'LineChart', () => {
615
615
  expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
616
616
  const html = document.body.innerHTML;
617
617
  expect( html.indexOf( 'data-testid="legend-horizontal"' ) ).toBeLessThan(
618
- html.indexOf( 'role="grid"' )
618
+ html.indexOf( 'role="application"' )
619
619
  );
620
620
  } );
621
621
  } );
@@ -805,7 +805,7 @@ describe( 'LineChart', () => {
805
805
  expect( gradient ).toBeInTheDocument();
806
806
 
807
807
  // Verify that the chart container exists
808
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
808
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
809
809
  expect( chart ).toBeInTheDocument();
810
810
  } );
811
811
  } );
@@ -821,7 +821,7 @@ describe( 'LineChart', () => {
821
821
  },
822
822
  } );
823
823
  // The chart should render with the custom axis configuration
824
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
824
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
825
825
  } );
826
826
  } );
827
827
 
@@ -1067,7 +1067,7 @@ describe( 'LineChart', () => {
1067
1067
 
1068
1068
  // Instead of checking styles, verify the chart renders
1069
1069
  expect( screen.getByTestId( 'line-chart' ) ).toBeInTheDocument();
1070
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
1070
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
1071
1071
  } );
1072
1072
  } );
1073
1073
 
@@ -1605,17 +1605,18 @@ describe( 'LineChart', () => {
1605
1605
  describe( 'Chart Focus and Accessibility Attributes', () => {
1606
1606
  test( 'chart container has proper accessibility attributes', () => {
1607
1607
  renderWithTheme();
1608
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1608
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1609
1609
 
1610
1610
  expect( chart ).toHaveAttribute( 'tabIndex', '0' );
1611
- expect( chart ).toHaveAttribute( 'role', 'grid' );
1611
+ expect( chart ).toHaveAttribute( 'role', 'application' );
1612
1612
  expect( chart ).toHaveAttribute( 'aria-label', 'Line chart' );
1613
+ expect( screen.queryByLabelText( 'XYChart' ) ).not.toBeInTheDocument();
1613
1614
  } );
1614
1615
 
1615
1616
  test( 'chart container can receive focus', async () => {
1616
1617
  const user = userEvent.setup();
1617
1618
  renderWithTheme();
1618
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1619
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1619
1620
 
1620
1621
  await user.tab();
1621
1622
  expect( chart ).toHaveFocus();
@@ -1646,7 +1647,7 @@ describe( 'LineChart', () => {
1646
1647
  const onDatumActivate = jest.fn();
1647
1648
  renderWithTheme( { data: [ SERIES_A, SERIES_B ], onDatumActivate } );
1648
1649
 
1649
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
1650
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
1650
1651
  await user.keyboard( '{ArrowRight}{ArrowRight}{Enter}' );
1651
1652
 
1652
1653
  expect( onDatumActivate ).toHaveBeenCalledTimes( 1 );
@@ -1662,7 +1663,7 @@ describe( 'LineChart', () => {
1662
1663
  const onDatumActivate = jest.fn();
1663
1664
  renderWithTheme( { data: [ SERIES_A ], onDatumActivate } );
1664
1665
 
1665
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
1666
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
1666
1667
  await user.keyboard( '{Enter}' );
1667
1668
 
1668
1669
  expect( onDatumActivate ).not.toHaveBeenCalled();
@@ -1696,7 +1697,7 @@ describe( 'LineChart', () => {
1696
1697
  ],
1697
1698
  } );
1698
1699
 
1699
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1700
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1700
1701
  chart.focus();
1701
1702
 
1702
1703
  // Single tab should focus on the first tooltip.
@@ -1741,7 +1742,7 @@ describe( 'LineChart', () => {
1741
1742
  ],
1742
1743
  } );
1743
1744
 
1744
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1745
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1745
1746
  chart.focus();
1746
1747
 
1747
1748
  // Right arrow key should focus on the first tooltip.
@@ -1788,7 +1789,7 @@ describe( 'LineChart', () => {
1788
1789
  ],
1789
1790
  } );
1790
1791
 
1791
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1792
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1792
1793
  chart.focus();
1793
1794
 
1794
1795
  // Chart should be in focus.
@@ -1819,7 +1820,7 @@ describe( 'LineChart', () => {
1819
1820
 
1820
1821
  renderWithTheme( { renderTooltip: customTooltipRenderer } );
1821
1822
 
1822
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1823
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1823
1824
  chart.focus();
1824
1825
 
1825
1826
  // Click on right arrow key to focus on the first tooltip.
@@ -2049,8 +2050,9 @@ describe( 'LineChart', () => {
2049
2050
 
2050
2051
  const pointer = ( type: string, clientX: number, clientY: number ) => {
2051
2052
  // visx owns the pointer capture rect and does not expose an attribute prop for it.
2053
+ const chart = screen.getByRole( 'application' );
2052
2054
  // eslint-disable-next-line testing-library/no-node-access
2053
- const target = screen.getByRole( 'grid' ).querySelector( 'svg > rect[fill="transparent"]' );
2055
+ const target = chart.querySelector( 'svg > rect[fill="transparent"]' );
2054
2056
  fireEvent( target, new MouseEvent( type, { bubbles: true, clientX, clientY } ) );
2055
2057
  };
2056
2058
 
@@ -2398,7 +2400,7 @@ describe( 'LineChart', () => {
2398
2400
  options: {},
2399
2401
  },
2400
2402
  ];
2401
- // Bare numbers inside the plot (scoped via the chart's grid role, away from visx's
2403
+ // Bare numbers inside the plot (scoped via the chart's application role, away from visx's
2402
2404
  // off-screen text-measurement SVGs) are value-axis ticks: the series labels carry no digits
2403
2405
  // and the empty-state message is text-only.
2404
2406
  const numericTick = /^[\d,]+$/;
@@ -2414,7 +2416,7 @@ describe( 'LineChart', () => {
2414
2416
  data: twoSeries,
2415
2417
  } );
2416
2418
 
2417
- const chart = screen.getByRole( 'grid' );
2419
+ const chart = screen.getByRole( 'application' );
2418
2420
  const ticksBefore = within( chart )
2419
2421
  .getAllByText( numericTick )
2420
2422
  .map( el => el.textContent )
@@ -2441,7 +2443,7 @@ describe( 'LineChart', () => {
2441
2443
  data: twoSeries,
2442
2444
  } );
2443
2445
 
2444
- const chart = screen.getByRole( 'grid' );
2446
+ const chart = screen.getByRole( 'application' );
2445
2447
  const ticksBefore = within( chart )
2446
2448
  .getAllByText( numericTick )
2447
2449
  .map( el => el.textContent )
@@ -2469,7 +2471,7 @@ describe( 'LineChart', () => {
2469
2471
  data: twoSeries,
2470
2472
  } );
2471
2473
 
2472
- const chart = screen.getByRole( 'grid' );
2474
+ const chart = screen.getByRole( 'application' );
2473
2475
  expect( within( chart ).getAllByText( numericTick ).length ).toBeGreaterThan( 0 );
2474
2476
 
2475
2477
  const buttons = screen.getAllByRole( 'button' );
@@ -93,7 +93,7 @@ describe( 'default tooltip at hourly resolution', () => {
93
93
  </GlobalChartsProvider>
94
94
  );
95
95
 
96
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
96
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
97
97
  await user.keyboard( '{ArrowRight}' );
98
98
 
99
99
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( '2.8.2026, 09 Uhr' );
@@ -130,7 +130,7 @@ describe( 'default tooltip at hourly resolution', () => {
130
130
  </GlobalChartsProvider>
131
131
  );
132
132
 
133
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
133
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
134
134
  await user.keyboard( '{ArrowRight}' );
135
135
 
136
136
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -10,7 +10,7 @@ import type {
10
10
  } from '../../types';
11
11
  import type { CrosshairStyle, RenderTooltipParams } from '../../visx/types';
12
12
  import type { GlyphProps } from '@visx/xychart';
13
- import type { ReactNode, SVGProps, FC, CSSProperties } from 'react';
13
+ import type { ReactNode, SVGProps, CSSProperties } from 'react';
14
14
 
15
15
  export type LineChartAnnotationProps = {
16
16
  datum: DataPointDate;
@@ -19,8 +19,8 @@ export type LineChartAnnotationProps = {
19
19
  subjectType?: 'circle' | 'line-vertical' | 'line-horizontal';
20
20
  styles?: AnnotationStyles;
21
21
  testId?: string;
22
- renderLabel?: FC< { title: string; subtitle?: string } >;
23
- renderLabelPopover?: FC< { title: string; subtitle?: string } >;
22
+ renderLabel?: ( props: { title: string; subtitle?: string } ) => ReactNode;
23
+ renderLabelPopover?: ( props: { title: string; subtitle?: string } ) => ReactNode;
24
24
  };
25
25
 
26
26
  export type CurveType = 'smooth' | 'linear' | 'monotone';
@@ -1,4 +1,7 @@
1
1
  .pie-chart {
2
+ // Keeps the tooltip's z-index inside the chart, as `.xy-plot` does.
3
+ isolation: isolate;
4
+
2
5
  // Anchors the tooltip, which renders next to the SVG in here.
3
6
  &__plot {
4
7
  position: relative;