@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/TOKENS.md +6 -3
  3. package/dist/index.cjs +834 -321
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +68 -17
  6. package/dist/index.d.cts +102 -28
  7. package/dist/index.d.ts +102 -28
  8. package/dist/index.js +838 -327
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/charts/area-chart/area-chart.tsx +7 -4
  12. package/src/charts/area-chart/test/area-chart.test.tsx +24 -15
  13. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
  14. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
  15. package/src/charts/area-chart/types.ts +2 -0
  16. package/src/charts/bar-chart/bar-chart.module.scss +7 -0
  17. package/src/charts/bar-chart/bar-chart.tsx +25 -16
  18. package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
  19. package/src/charts/bar-chart/test/bar-chart.test.tsx +106 -62
  20. package/src/charts/bar-chart/types.ts +2 -0
  21. package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
  22. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  23. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
  24. package/src/charts/heatmap-chart/heatmap-chart.module.scss +16 -8
  25. package/src/charts/heatmap-chart/heatmap-chart.tsx +183 -66
  26. package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
  27. package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
  28. package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
  29. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +15 -3
  30. package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
  31. package/src/charts/heatmap-chart/private/index.ts +6 -3
  32. package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
  33. package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
  34. package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
  35. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +231 -8
  36. package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
  37. package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
  38. package/src/charts/heatmap-chart/types.ts +6 -3
  39. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
  40. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
  41. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
  42. package/src/charts/leaderboard-chart/types.ts +6 -1
  43. package/src/charts/line-chart/line-chart.tsx +7 -4
  44. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
  45. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
  46. package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
  47. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
  48. package/src/charts/line-chart/types.ts +5 -3
  49. package/src/charts/pie-chart/pie-chart.tsx +153 -143
  50. package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
  51. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +126 -124
  52. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +55 -1
  53. package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
  54. package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
  55. package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
  56. package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
  57. package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
  58. package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
  59. package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
  60. package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
  61. package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
  62. package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
  63. package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
  64. package/src/charts/private/test/label-text-color.test.ts +6 -8
  65. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
  66. package/src/components/tooltip/base-tooltip.tsx +31 -32
  67. package/src/components/tooltip/index.ts +3 -1
  68. package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
  69. package/src/components/tooltip/private/label-value-content.tsx +8 -0
  70. package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
  71. package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
  72. package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
  73. package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
  74. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +283 -365
  75. package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
  76. package/src/components/tooltip/tooltip-box.tsx +43 -0
  77. package/src/components/tooltip/types.ts +7 -1
  78. package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
  79. package/src/hooks/test/use-chart-margin.test.tsx +8 -4
  80. package/src/hooks/use-chart-margin.tsx +21 -18
  81. package/src/index.ts +8 -3
  82. package/src/providers/chart-context/global-charts-provider.tsx +5 -14
  83. package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
  84. package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
  85. package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
  86. package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
  87. package/src/styles/chart-scope.scss +1 -0
  88. package/src/test-utils/zoned-parsing-suite.ts +0 -10
  89. package/src/types.ts +18 -2
  90. package/src/utils/test/date-formatting.test.ts +0 -10
  91. package/src/components/tooltip/accessible-tooltip.tsx +0 -433
  92. package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
@@ -42,12 +42,12 @@ describe( 'BarChart', () => {
42
42
  // visx renders bars and grid lines without accessible roles or configurable test IDs.
43
43
  const getBarRects = () => {
44
44
  // eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
45
- return screen.getByRole( 'grid' ).querySelectorAll( '.visx-bar-group rect' );
45
+ return screen.getByRole( 'application' ).querySelectorAll( '.visx-bar-group rect' );
46
46
  };
47
47
 
48
48
  const getGridLines = () => {
49
49
  // eslint-disable-next-line testing-library/no-node-access -- visx grid lines cannot receive test IDs.
50
- return screen.getByRole( 'grid' ).querySelectorAll( '.visx-rows line' );
50
+ return screen.getByRole( 'application' ).querySelectorAll( '.visx-rows line' );
51
51
  };
52
52
 
53
53
  const renderWithTheme = ( props = {}, children = undefined ) => {
@@ -151,20 +151,21 @@ describe( 'BarChart', () => {
151
151
 
152
152
  const pointer = ( type: string, clientX: number, clientY: number ) => {
153
153
  // visx owns the pointer capture rect and does not expose an attribute prop for it.
154
+ const chart = screen.getByRole( 'application' );
154
155
  // eslint-disable-next-line testing-library/no-node-access
155
- const target = screen.getByRole( 'grid' ).querySelector( 'svg > rect[fill="transparent"]' );
156
+ const target = chart.querySelector( 'svg > rect[fill="transparent"]' );
156
157
  fireEvent( target, new MouseEvent( type, { bubbles: true, clientX, clientY } ) );
157
158
  };
158
159
 
159
160
  const hover = ( clientX: number, clientY: number ) =>
160
161
  pointer( 'pointermove', clientX, clientY );
161
162
 
162
- test( 'Escape returns focus to the grid without reopening either tooltip', async () => {
163
+ test( 'Escape returns focus to the chart without reopening either tooltip', async () => {
163
164
  jest.useFakeTimers();
164
165
  try {
165
166
  const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
166
167
  renderWithTheme( { withTooltips: true } );
167
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
168
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
168
169
  await user.tab();
169
170
  await user.keyboard( '{ArrowRight}' );
170
171
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveFocus();
@@ -369,7 +370,7 @@ describe( 'BarChart', () => {
369
370
  unmount();
370
371
  renderWithTheme( { withBandHighlight: true, onBandHighlightChange } );
371
372
  expect( warn ).toHaveBeenCalledTimes( 1 );
372
- expect( screen.getByRole( 'grid' ) ).toBeInTheDocument();
373
+ expect( screen.getByRole( 'application' ) ).toBeInTheDocument();
373
374
  expect( warn ).toHaveBeenCalledWith(
374
375
  '[Charts] BarChart: withBandHighlight and onBandHighlightChange require withTooltips.'
375
376
  );
@@ -473,7 +474,7 @@ describe( 'BarChart', () => {
473
474
 
474
475
  test( 'labels a whole-number range smaller than the tick count once per whole number', () => {
475
476
  renderWithTheme( { data: wholeNumberData } );
476
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
477
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
477
478
  const ticks = within( chart )
478
479
  .getAllByText( /^-?[\d.,]+$/ )
479
480
  .map( el => el.textContent );
@@ -482,7 +483,7 @@ describe( 'BarChart', () => {
482
483
 
483
484
  test( 'labels a horizontal whole-number range once per whole number', () => {
484
485
  renderWithTheme( { data: wholeNumberData, orientation: 'horizontal' } );
485
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
486
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
486
487
  const ticks = within( chart )
487
488
  .getAllByText( /^-?[\d.,]+$/ )
488
489
  .map( el => el.textContent );
@@ -501,7 +502,7 @@ describe( 'BarChart', () => {
501
502
  },
502
503
  ],
503
504
  } );
504
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
505
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
505
506
  const ticks = within( chart )
506
507
  .getAllByText( /^-?[\d.,]+$/ )
507
508
  .map( el => el.textContent );
@@ -530,7 +531,7 @@ describe( 'BarChart', () => {
530
531
  },
531
532
  },
532
533
  } );
533
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
534
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
534
535
  expect( within( chart ).getAllByText( /^\d+%$/ ) ).toHaveLength( 6 );
535
536
  }
536
537
  );
@@ -540,7 +541,7 @@ describe( 'BarChart', () => {
540
541
  data: wholeNumberData,
541
542
  options: { axis: { y: { tickValues: [ 0, 0.5, 1 ] } } },
542
543
  } );
543
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
544
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
544
545
  expect( within( chart ).getAllByText( /^-?[\d.,]+$/ ) ).toHaveLength( 3 );
545
546
  } );
546
547
  } );
@@ -584,7 +585,7 @@ describe( 'BarChart', () => {
584
585
  },
585
586
  ],
586
587
  } );
587
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
588
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
588
589
  } );
589
590
 
590
591
  test( 'handles negative values', () => {
@@ -600,7 +601,7 @@ describe( 'BarChart', () => {
600
601
  },
601
602
  ],
602
603
  } );
603
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
604
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
604
605
  } );
605
606
 
606
607
  test( 'renders a bucket with no reading instead of failing the whole chart', () => {
@@ -617,7 +618,7 @@ describe( 'BarChart', () => {
617
618
  ],
618
619
  } );
619
620
  expect( screen.queryByText( /invalid data/i ) ).not.toBeInTheDocument();
620
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
621
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
621
622
  } );
622
623
 
623
624
  test( 'draws no bar for a bucket with no reading', () => {
@@ -736,7 +737,7 @@ describe( 'BarChart', () => {
736
737
  expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
737
738
  const html = document.body.innerHTML;
738
739
  expect( html.indexOf( 'data-testid="legend-horizontal"' ) ).toBeLessThan(
739
- html.indexOf( 'role="grid"' )
740
+ html.indexOf( 'role="application"' )
740
741
  );
741
742
  } );
742
743
  } );
@@ -744,7 +745,7 @@ describe( 'BarChart', () => {
744
745
  describe( 'Grid tick counts', () => {
745
746
  const getPositions = ( selector: string, coordinate: string ) => {
746
747
  // eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
747
- const lines = screen.getByRole( 'grid' ).querySelectorAll( selector );
748
+ const lines = screen.getByRole( 'application' ).querySelectorAll( selector );
748
749
  return Array.from( lines, line => Number( line.getAttribute( coordinate ) ) );
749
750
  };
750
751
 
@@ -806,24 +807,49 @@ describe( 'BarChart', () => {
806
807
  );
807
808
  } );
808
809
 
810
+ describe( 'Axis display', () => {
811
+ test.each( [
812
+ [ 'vertical', 'x', 'y' ],
813
+ [ 'vertical', 'y', 'x' ],
814
+ [ 'horizontal', 'x', 'y' ],
815
+ [ 'horizontal', 'y', 'x' ],
816
+ ] )( 'skips a hidden axis (%s, hidden %s)', ( orientation, hidden, shown ) => {
817
+ renderWithTheme( {
818
+ orientation,
819
+ options: {
820
+ axis: {
821
+ [ hidden ]: { display: false, axisClassName: 'test-hidden-axis' },
822
+ [ shown ]: { axisClassName: 'test-shown-axis' },
823
+ },
824
+ },
825
+ } );
826
+ const chart = screen.getByRole( 'application' );
827
+
828
+ // eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
829
+ expect( chart.querySelector( '.test-hidden-axis' ) ).toBeNull();
830
+ // eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
831
+ expect( chart.querySelector( '.test-shown-axis' ) ).not.toBeNull();
832
+ } );
833
+ } );
834
+
809
835
  describe( 'Grid Visibility', () => {
810
836
  test( 'renders with different grid visibility options', () => {
811
837
  const { rerender } = renderWithTheme( { gridVisibility: 'x' } );
812
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
838
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
813
839
 
814
840
  rerender(
815
841
  <GlobalChartsProvider>
816
842
  <BarChart { ...defaultProps } gridVisibility="y" />
817
843
  </GlobalChartsProvider>
818
844
  );
819
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
845
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
820
846
 
821
847
  rerender(
822
848
  <GlobalChartsProvider>
823
849
  <BarChart { ...defaultProps } gridVisibility="xy" />
824
850
  </GlobalChartsProvider>
825
851
  );
826
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
852
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
827
853
  } );
828
854
  } );
829
855
 
@@ -905,7 +931,7 @@ describe( 'BarChart', () => {
905
931
 
906
932
  // Scoped to the chart: @visx/text parks a measuring node on document.body,
907
933
  // which a negative assertion would otherwise match.
908
- const inChart = () => within( screen.getByRole( 'grid', { name: /bar chart/i } ) );
934
+ const inChart = () => within( screen.getByRole( 'application', { name: /bar chart/i } ) );
909
935
 
910
936
  test( 'leaves tick values alone when a bucket is labelled rather than dated', () => {
911
937
  // visx builds the band domain from `label || date`, so a labelled bucket
@@ -1109,7 +1135,7 @@ describe( 'BarChart', () => {
1109
1135
  // on document.body, which outlives RTL's cleanup still holding the last
1110
1136
  // string measured. Query inside the chart so a previous test's measurement
1111
1137
  // can't answer for this one's axis.
1112
- const inChart = () => within( screen.getByRole( 'grid', { name: /bar chart/i } ) );
1138
+ const inChart = () => within( screen.getByRole( 'application', { name: /bar chart/i } ) );
1113
1139
 
1114
1140
  test( 'renders distinct date ticks for daily buckets within a year', () => {
1115
1141
  renderWithTheme( {
@@ -1545,7 +1571,7 @@ describe( 'BarChart', () => {
1545
1571
  ],
1546
1572
  } );
1547
1573
 
1548
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1574
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1549
1575
  await user.keyboard( '{ArrowRight}' );
1550
1576
 
1551
1577
  // The month tick reads "2024" here; the tooltip names the bucket itself.
@@ -1604,7 +1630,7 @@ describe( 'BarChart', () => {
1604
1630
 
1605
1631
  test( 'renders with patterns', () => {
1606
1632
  renderWithTheme( { withPatterns: true } );
1607
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
1633
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
1608
1634
 
1609
1635
  // Check that pattern definitions container is present
1610
1636
  expect( screen.getByTestId( 'bar-chart-patterns' ) ).toBeInTheDocument();
@@ -1612,7 +1638,7 @@ describe( 'BarChart', () => {
1612
1638
 
1613
1639
  test( 'renders without patterns by default', () => {
1614
1640
  renderWithTheme( { withPatterns: false } );
1615
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
1641
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
1616
1642
 
1617
1643
  // Check that no pattern definitions container is present
1618
1644
  expect( screen.queryByTestId( 'bar-chart-patterns' ) ).not.toBeInTheDocument();
@@ -1654,17 +1680,26 @@ describe( 'BarChart', () => {
1654
1680
  describe( 'Chart Focus and Accessibility Attributes', () => {
1655
1681
  test( 'chart container has proper accessibility attributes', () => {
1656
1682
  renderWithTheme();
1657
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1683
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1658
1684
 
1659
1685
  expect( chart ).toHaveAttribute( 'tabIndex', '0' );
1660
- expect( chart ).toHaveAttribute( 'role', 'grid' );
1686
+ expect( chart ).toHaveAttribute( 'role', 'application' );
1661
1687
  expect( chart ).toHaveAttribute( 'aria-label', 'Bar chart' );
1662
1688
  } );
1663
1689
 
1690
+ test.each( [
1691
+ [ undefined, 'Bar chart' ],
1692
+ [ 'Views over time', 'Views over time' ],
1693
+ ] )( 'names the chart container from ariaLabel %p', ( ariaLabel, name ) => {
1694
+ renderWithTheme( { ariaLabel } );
1695
+ expect( screen.getByRole( 'application', { name } ) ).toBeInTheDocument();
1696
+ expect( screen.queryByLabelText( 'XYChart' ) ).not.toBeInTheDocument();
1697
+ } );
1698
+
1664
1699
  test( 'chart container can receive focus', async () => {
1665
1700
  const user = userEvent.setup();
1666
1701
  renderWithTheme();
1667
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1702
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1668
1703
 
1669
1704
  await user.tab();
1670
1705
  expect( chart ).toHaveFocus();
@@ -1694,7 +1729,7 @@ describe( 'BarChart', () => {
1694
1729
  const onDatumActivate = jest.fn();
1695
1730
  renderWithTheme( { withTooltips: true, data: [ SERIES_A ], onDatumActivate } );
1696
1731
 
1697
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1732
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1698
1733
  await user.keyboard( '{ArrowRight}{ArrowRight}{Enter}' );
1699
1734
 
1700
1735
  expect( onDatumActivate ).toHaveBeenCalledTimes( 1 );
@@ -1710,7 +1745,7 @@ describe( 'BarChart', () => {
1710
1745
  const onDatumActivate = jest.fn();
1711
1746
  renderWithTheme( { withTooltips: true, data: [ SERIES_A ], onDatumActivate } );
1712
1747
 
1713
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1748
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1714
1749
  await user.keyboard( '{ArrowRight}[Space]' );
1715
1750
 
1716
1751
  expect( onDatumActivate ).toHaveBeenCalledWith( {
@@ -1728,7 +1763,7 @@ describe( 'BarChart', () => {
1728
1763
  const onDatumActivate = jest.fn();
1729
1764
  renderWithTheme( { withTooltips: true, data: [ SERIES_A, SERIES_B ], onDatumActivate } );
1730
1765
 
1731
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1766
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1732
1767
  await user.keyboard( '{ArrowRight}{ArrowRight}{Enter}' );
1733
1768
 
1734
1769
  expect( onDatumActivate ).toHaveBeenCalledWith( {
@@ -1743,7 +1778,7 @@ describe( 'BarChart', () => {
1743
1778
  const onDatumActivate = jest.fn();
1744
1779
  renderWithTheme( { withTooltips: true, data: [ SERIES_A ], onDatumActivate } );
1745
1780
 
1746
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1781
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1747
1782
  await user.keyboard( '{Enter}' );
1748
1783
 
1749
1784
  expect( onDatumActivate ).not.toHaveBeenCalled();
@@ -1777,7 +1812,7 @@ describe( 'BarChart', () => {
1777
1812
  ],
1778
1813
  } );
1779
1814
 
1780
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1815
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1781
1816
  chart.focus();
1782
1817
 
1783
1818
  // Single tab should focus on the first tooltip.
@@ -1828,7 +1863,7 @@ describe( 'BarChart', () => {
1828
1863
  ],
1829
1864
  } );
1830
1865
 
1831
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1866
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1832
1867
  chart.focus();
1833
1868
 
1834
1869
  await user.keyboard( '{ArrowRight}' );
@@ -1877,7 +1912,7 @@ describe( 'BarChart', () => {
1877
1912
  ],
1878
1913
  } );
1879
1914
 
1880
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1915
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1881
1916
  chart.focus();
1882
1917
 
1883
1918
  // Right arrow key should focus on the first tooltip.
@@ -1900,7 +1935,7 @@ describe( 'BarChart', () => {
1900
1935
  } );
1901
1936
  } );
1902
1937
 
1903
- describe( 'ARIA grid boundaries', () => {
1938
+ describe( 'Keyboard navigation boundaries', () => {
1904
1939
  // Three points, so a clamp at a boundary lands on a different index than a wrap would.
1905
1940
  const threePointData = [
1906
1941
  {
@@ -1918,7 +1953,7 @@ describe( 'BarChart', () => {
1918
1953
  const user = userEvent.setup();
1919
1954
  renderWithTheme( { withTooltips: true, data: threePointData } );
1920
1955
 
1921
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1956
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1922
1957
  chart.focus();
1923
1958
 
1924
1959
  // Move to the first, then on to the last (third) point.
@@ -1928,7 +1963,7 @@ describe( 'BarChart', () => {
1928
1963
  expect( screen.getByTestId( 'chart-tooltip-2' ) ).toHaveFocus();
1929
1964
 
1930
1965
  // Right arrow at the last point must not move focus: the highlighted
1931
- // bar/tooltip stays visible and focused (ARIA grid: focus does not move).
1966
+ // bar/tooltip stays visible and focused (focus does not move).
1932
1967
  await user.keyboard( '{ArrowRight}' );
1933
1968
  expect( screen.getByTestId( 'chart-tooltip-2' ) ).toBeInTheDocument();
1934
1969
  expect( screen.getByTestId( 'chart-tooltip-2' ) ).toHaveFocus();
@@ -1943,7 +1978,7 @@ describe( 'BarChart', () => {
1943
1978
  const user = userEvent.setup();
1944
1979
  renderWithTheme( { withTooltips: true, data: threePointData } );
1945
1980
 
1946
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1981
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1947
1982
  await user.keyboard( '{ArrowLeft}' );
1948
1983
 
1949
1984
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveFocus();
@@ -1977,7 +2012,7 @@ describe( 'BarChart', () => {
1977
2012
  ],
1978
2013
  } );
1979
2014
 
1980
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2015
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1981
2016
  chart.focus();
1982
2017
 
1983
2018
  await user.keyboard( '{ArrowRight}' );
@@ -2012,7 +2047,7 @@ describe( 'BarChart', () => {
2012
2047
  ],
2013
2048
  } );
2014
2049
 
2015
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2050
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2016
2051
  chart.focus();
2017
2052
 
2018
2053
  // Slot 0 is the first primary bar (Mon); slot 1 must be the SECOND primary
@@ -2044,7 +2079,7 @@ describe( 'BarChart', () => {
2044
2079
  ],
2045
2080
  } );
2046
2081
 
2047
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2082
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2048
2083
  await user.keyboard( '{ArrowRight}' );
2049
2084
 
2050
2085
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -2077,7 +2112,7 @@ describe( 'BarChart', () => {
2077
2112
  ],
2078
2113
  } );
2079
2114
 
2080
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2115
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2081
2116
  await user.keyboard( '{ArrowRight}' );
2082
2117
 
2083
2118
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -2137,7 +2172,7 @@ describe( 'BarChart', () => {
2137
2172
  ],
2138
2173
  } );
2139
2174
 
2140
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2175
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2141
2176
  chart.focus();
2142
2177
 
2143
2178
  // Chart should be in focus.
@@ -2177,7 +2212,7 @@ describe( 'BarChart', () => {
2177
2212
  ],
2178
2213
  } );
2179
2214
 
2180
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2215
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2181
2216
  chart.focus();
2182
2217
 
2183
2218
  // Navigate to the first bar
@@ -2212,7 +2247,7 @@ describe( 'BarChart', () => {
2212
2247
  ],
2213
2248
  } );
2214
2249
 
2215
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2250
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2216
2251
  chart.focus();
2217
2252
 
2218
2253
  // Navigate to the first bar (Series A, Jan 1)
@@ -2244,7 +2279,7 @@ describe( 'BarChart', () => {
2244
2279
  const user = userEvent.setup();
2245
2280
  renderWithTheme( { withTooltips: true, data } );
2246
2281
 
2247
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2282
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2248
2283
  await user.keyboard( '{ArrowRight}' );
2249
2284
 
2250
2285
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'No data' );
@@ -2255,7 +2290,7 @@ describe( 'BarChart', () => {
2255
2290
  const user = userEvent.setup();
2256
2291
  renderWithTheme( { withTooltips: true, data } );
2257
2292
 
2258
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2293
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2259
2294
  await user.keyboard( '{ArrowRight}{ArrowRight}' );
2260
2295
 
2261
2296
  expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveTextContent( '20' );
@@ -2288,7 +2323,7 @@ describe( 'BarChart', () => {
2288
2323
  ];
2289
2324
  renderWithTheme( { withTooltips: true, data: twoSeries } );
2290
2325
 
2291
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2326
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2292
2327
  await user.keyboard( '{ArrowRight}{ArrowRight}{ArrowRight}{ArrowRight}' );
2293
2328
 
2294
2329
  expect( screen.getByTestId( 'chart-tooltip-3' ) ).toHaveTextContent( '15' );
@@ -2315,7 +2350,7 @@ describe( 'BarChart', () => {
2315
2350
 
2316
2351
  renderWithTheme( { withTooltips: true, renderTooltip: customTooltipRenderer } );
2317
2352
 
2318
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2353
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2319
2354
  chart.focus();
2320
2355
 
2321
2356
  // Click on right arrow key to focus on the first tooltip.
@@ -2347,7 +2382,9 @@ describe( 'BarChart', () => {
2347
2382
  ],
2348
2383
  } );
2349
2384
 
2350
- const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
2385
+ const svgElement = screen
2386
+ .getByRole( 'application', { name: /bar chart/i } )
2387
+ .querySelector( 'svg' );
2351
2388
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2352
2389
 
2353
2390
  // Should have 2 bars
@@ -2374,7 +2411,9 @@ describe( 'BarChart', () => {
2374
2411
  ],
2375
2412
  } );
2376
2413
 
2377
- const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
2414
+ const svgElement = screen
2415
+ .getByRole( 'application', { name: /bar chart/i } )
2416
+ .querySelector( 'svg' );
2378
2417
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2379
2418
 
2380
2419
  // Should have 2 bars
@@ -2407,7 +2446,9 @@ describe( 'BarChart', () => {
2407
2446
  orientation: 'horizontal',
2408
2447
  } );
2409
2448
 
2410
- const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
2449
+ const svgElement = screen
2450
+ .getByRole( 'application', { name: /bar chart/i } )
2451
+ .querySelector( 'svg' );
2411
2452
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2412
2453
 
2413
2454
  // Both bars should be visible in horizontal mode
@@ -2436,7 +2477,9 @@ describe( 'BarChart', () => {
2436
2477
  ],
2437
2478
  } );
2438
2479
 
2439
- const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
2480
+ const svgElement = screen
2481
+ .getByRole( 'application', { name: /bar chart/i } )
2482
+ .querySelector( 'svg' );
2440
2483
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2441
2484
 
2442
2485
  expect( bars?.length ).toBe( 2 );
@@ -2476,7 +2519,7 @@ describe( 'BarChart', () => {
2476
2519
  },
2477
2520
  } );
2478
2521
 
2479
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
2522
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
2480
2523
  } );
2481
2524
 
2482
2525
  test( 'truncates labels with CSS text-overflow ellipsis', () => {
@@ -2569,7 +2612,7 @@ describe( 'BarChart', () => {
2569
2612
  } );
2570
2613
 
2571
2614
  // Chart should still render without errors
2572
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
2615
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
2573
2616
 
2574
2617
  // Labels should still be present and have minimum width applied
2575
2618
  const label = screen.getByText( /Very Long Category Label One/i );
@@ -2865,7 +2908,7 @@ describe( 'BarChart', () => {
2865
2908
  </GlobalChartsProvider>
2866
2909
  );
2867
2910
 
2868
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2911
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2869
2912
  chart.focus();
2870
2913
 
2871
2914
  // Navigate through all 4 primary slots (2 series × 2 data points)
@@ -3151,8 +3194,9 @@ describe( 'BarChart', () => {
3151
3194
  </GlobalChartsProvider>
3152
3195
  );
3153
3196
 
3197
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
3154
3198
  // eslint-disable-next-line testing-library/no-node-access
3155
- const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
3199
+ const svgElement = chart.querySelector( 'svg' );
3156
3200
  // eslint-disable-next-line testing-library/no-node-access
3157
3201
  expect( svgElement?.querySelectorAll( '.visx-bar-group rect' ) ).toHaveLength( 2 );
3158
3202
 
@@ -3357,7 +3401,7 @@ describe( 'BarChart', () => {
3357
3401
  const user = userEvent.setup();
3358
3402
  const toggle = mountWithVisibilityToggle();
3359
3403
 
3360
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
3404
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
3361
3405
  for ( let i = 0; i < 6; i++ ) {
3362
3406
  await user.keyboard( '{ArrowRight}' );
3363
3407
  }
@@ -3402,7 +3446,7 @@ describe( 'BarChart', () => {
3402
3446
  ],
3403
3447
  } );
3404
3448
 
3405
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
3449
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
3406
3450
  for ( let i = 0; i < 6; i++ ) {
3407
3451
  await user.keyboard( '{ArrowRight}' );
3408
3452
  }
@@ -3459,12 +3503,12 @@ describe( 'BarChart', () => {
3459
3503
  </GlobalChartsProvider>
3460
3504
  );
3461
3505
 
3462
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
3506
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
3463
3507
  await user.keyboard( '{ArrowRight}{ArrowRight}' );
3464
3508
  expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveTextContent( 'Series A' );
3465
3509
 
3466
3510
  // Move focus out of the chart, then swap which series is hidden without changing the count.
3467
- screen.getByRole( 'grid', { name: /bar chart/i } ).blur();
3511
+ screen.getByRole( 'application', { name: /bar chart/i } ).blur();
3468
3512
  act( () => context.setChartHiddenSeries( 'test-equal-count-swap', [ 'Series A' ] ) );
3469
3513
 
3470
3514
  expect( screen.queryAllByTestId( /^chart-tooltip-/ ) ).toHaveLength( 0 );
@@ -3503,7 +3547,7 @@ describe( 'BarChart', () => {
3503
3547
 
3504
3548
  await user.click( screen.getByRole( 'button', { name: /Series B: visible/i } ) );
3505
3549
 
3506
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
3550
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
3507
3551
  chart.focus();
3508
3552
 
3509
3553
  for ( let i = 0; i < 5; i++ ) {
@@ -19,6 +19,8 @@ export type BandHighlightSelection = {
19
19
  };
20
20
 
21
21
  export interface BarChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
22
+ /** Accessible name of the chart. Defaults to a localized "Bar chart". */
23
+ ariaLabel?: string;
22
24
  /**
23
25
  * Legend configuration. Supports `collapseGroups` on top of the shared options.
24
26
  */
@@ -72,7 +72,9 @@ describe( 'BarListChart', () => {
72
72
  /* eslint-disable testing-library/no-node-access */
73
73
  const barFills = () =>
74
74
  Array.from(
75
- screen.getByRole( 'grid' ).querySelectorAll< SVGRectElement >( '.visx-bar-group rect' )
75
+ screen
76
+ .getByRole( 'application' )
77
+ .querySelectorAll< SVGRectElement >( '.visx-bar-group rect' )
76
78
  ).map( bar => bar.getAttribute( 'fill' ) );
77
79
  /* eslint-enable testing-library/no-node-access */
78
80
 
@@ -427,7 +427,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
427
427
  className={ standaloneScopeClass }
428
428
  style={ tooltipStyle }
429
429
  >
430
- { tooltipContent }
430
+ <div role="tooltip">{ tooltipContent }</div>
431
431
  </BoundedTooltip>
432
432
  );
433
433
  } )() }
@@ -430,6 +430,15 @@ describe( 'ConversionFunnelChart', () => {
430
430
  } );
431
431
  } );
432
432
 
433
+ it( 'exposes the tooltip content with role="tooltip"', async () => {
434
+ const user = userEvent.setup();
435
+ renderWithoutTheme( <ConversionFunnelChart { ...defaultProps } /> );
436
+
437
+ await user.click( screen.getByRole( 'button', { name: /cart/i } ) );
438
+
439
+ expect( screen.getByRole( 'tooltip' ) ).toHaveTextContent( 'Cart' );
440
+ } );
441
+
433
442
  it( 'merges tooltipStyle onto the tooltip box', async () => {
434
443
  const user = userEvent.setup();
435
444
  renderWithoutTheme(