@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
@@ -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
 
@@ -822,7 +823,7 @@ describe( 'BarChart', () => {
822
823
  },
823
824
  },
824
825
  } );
825
- const chart = screen.getByRole( 'grid' );
826
+ const chart = screen.getByRole( 'application' );
826
827
 
827
828
  // eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
828
829
  expect( chart.querySelector( '.test-hidden-axis' ) ).toBeNull();
@@ -834,21 +835,21 @@ describe( 'BarChart', () => {
834
835
  describe( 'Grid Visibility', () => {
835
836
  test( 'renders with different grid visibility options', () => {
836
837
  const { rerender } = renderWithTheme( { gridVisibility: 'x' } );
837
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
838
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
838
839
 
839
840
  rerender(
840
841
  <GlobalChartsProvider>
841
842
  <BarChart { ...defaultProps } gridVisibility="y" />
842
843
  </GlobalChartsProvider>
843
844
  );
844
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
845
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
845
846
 
846
847
  rerender(
847
848
  <GlobalChartsProvider>
848
849
  <BarChart { ...defaultProps } gridVisibility="xy" />
849
850
  </GlobalChartsProvider>
850
851
  );
851
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
852
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
852
853
  } );
853
854
  } );
854
855
 
@@ -930,7 +931,7 @@ describe( 'BarChart', () => {
930
931
 
931
932
  // Scoped to the chart: @visx/text parks a measuring node on document.body,
932
933
  // which a negative assertion would otherwise match.
933
- const inChart = () => within( screen.getByRole( 'grid', { name: /bar chart/i } ) );
934
+ const inChart = () => within( screen.getByRole( 'application', { name: /bar chart/i } ) );
934
935
 
935
936
  test( 'leaves tick values alone when a bucket is labelled rather than dated', () => {
936
937
  // visx builds the band domain from `label || date`, so a labelled bucket
@@ -1134,7 +1135,7 @@ describe( 'BarChart', () => {
1134
1135
  // on document.body, which outlives RTL's cleanup still holding the last
1135
1136
  // string measured. Query inside the chart so a previous test's measurement
1136
1137
  // can't answer for this one's axis.
1137
- const inChart = () => within( screen.getByRole( 'grid', { name: /bar chart/i } ) );
1138
+ const inChart = () => within( screen.getByRole( 'application', { name: /bar chart/i } ) );
1138
1139
 
1139
1140
  test( 'renders distinct date ticks for daily buckets within a year', () => {
1140
1141
  renderWithTheme( {
@@ -1570,7 +1571,7 @@ describe( 'BarChart', () => {
1570
1571
  ],
1571
1572
  } );
1572
1573
 
1573
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1574
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1574
1575
  await user.keyboard( '{ArrowRight}' );
1575
1576
 
1576
1577
  // The month tick reads "2024" here; the tooltip names the bucket itself.
@@ -1629,7 +1630,7 @@ describe( 'BarChart', () => {
1629
1630
 
1630
1631
  test( 'renders with patterns', () => {
1631
1632
  renderWithTheme( { withPatterns: true } );
1632
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
1633
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
1633
1634
 
1634
1635
  // Check that pattern definitions container is present
1635
1636
  expect( screen.getByTestId( 'bar-chart-patterns' ) ).toBeInTheDocument();
@@ -1637,7 +1638,7 @@ describe( 'BarChart', () => {
1637
1638
 
1638
1639
  test( 'renders without patterns by default', () => {
1639
1640
  renderWithTheme( { withPatterns: false } );
1640
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
1641
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
1641
1642
 
1642
1643
  // Check that no pattern definitions container is present
1643
1644
  expect( screen.queryByTestId( 'bar-chart-patterns' ) ).not.toBeInTheDocument();
@@ -1679,17 +1680,26 @@ describe( 'BarChart', () => {
1679
1680
  describe( 'Chart Focus and Accessibility Attributes', () => {
1680
1681
  test( 'chart container has proper accessibility attributes', () => {
1681
1682
  renderWithTheme();
1682
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1683
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1683
1684
 
1684
1685
  expect( chart ).toHaveAttribute( 'tabIndex', '0' );
1685
- expect( chart ).toHaveAttribute( 'role', 'grid' );
1686
+ expect( chart ).toHaveAttribute( 'role', 'application' );
1686
1687
  expect( chart ).toHaveAttribute( 'aria-label', 'Bar chart' );
1687
1688
  } );
1688
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
+
1689
1699
  test( 'chart container can receive focus', async () => {
1690
1700
  const user = userEvent.setup();
1691
1701
  renderWithTheme();
1692
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1702
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1693
1703
 
1694
1704
  await user.tab();
1695
1705
  expect( chart ).toHaveFocus();
@@ -1719,7 +1729,7 @@ describe( 'BarChart', () => {
1719
1729
  const onDatumActivate = jest.fn();
1720
1730
  renderWithTheme( { withTooltips: true, data: [ SERIES_A ], onDatumActivate } );
1721
1731
 
1722
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1732
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1723
1733
  await user.keyboard( '{ArrowRight}{ArrowRight}{Enter}' );
1724
1734
 
1725
1735
  expect( onDatumActivate ).toHaveBeenCalledTimes( 1 );
@@ -1735,7 +1745,7 @@ describe( 'BarChart', () => {
1735
1745
  const onDatumActivate = jest.fn();
1736
1746
  renderWithTheme( { withTooltips: true, data: [ SERIES_A ], onDatumActivate } );
1737
1747
 
1738
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1748
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1739
1749
  await user.keyboard( '{ArrowRight}[Space]' );
1740
1750
 
1741
1751
  expect( onDatumActivate ).toHaveBeenCalledWith( {
@@ -1753,7 +1763,7 @@ describe( 'BarChart', () => {
1753
1763
  const onDatumActivate = jest.fn();
1754
1764
  renderWithTheme( { withTooltips: true, data: [ SERIES_A, SERIES_B ], onDatumActivate } );
1755
1765
 
1756
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1766
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1757
1767
  await user.keyboard( '{ArrowRight}{ArrowRight}{Enter}' );
1758
1768
 
1759
1769
  expect( onDatumActivate ).toHaveBeenCalledWith( {
@@ -1768,7 +1778,7 @@ describe( 'BarChart', () => {
1768
1778
  const onDatumActivate = jest.fn();
1769
1779
  renderWithTheme( { withTooltips: true, data: [ SERIES_A ], onDatumActivate } );
1770
1780
 
1771
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1781
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1772
1782
  await user.keyboard( '{Enter}' );
1773
1783
 
1774
1784
  expect( onDatumActivate ).not.toHaveBeenCalled();
@@ -1802,7 +1812,7 @@ describe( 'BarChart', () => {
1802
1812
  ],
1803
1813
  } );
1804
1814
 
1805
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1815
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1806
1816
  chart.focus();
1807
1817
 
1808
1818
  // Single tab should focus on the first tooltip.
@@ -1853,7 +1863,7 @@ describe( 'BarChart', () => {
1853
1863
  ],
1854
1864
  } );
1855
1865
 
1856
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1866
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1857
1867
  chart.focus();
1858
1868
 
1859
1869
  await user.keyboard( '{ArrowRight}' );
@@ -1902,7 +1912,7 @@ describe( 'BarChart', () => {
1902
1912
  ],
1903
1913
  } );
1904
1914
 
1905
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1915
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1906
1916
  chart.focus();
1907
1917
 
1908
1918
  // Right arrow key should focus on the first tooltip.
@@ -1925,7 +1935,7 @@ describe( 'BarChart', () => {
1925
1935
  } );
1926
1936
  } );
1927
1937
 
1928
- describe( 'ARIA grid boundaries', () => {
1938
+ describe( 'Keyboard navigation boundaries', () => {
1929
1939
  // Three points, so a clamp at a boundary lands on a different index than a wrap would.
1930
1940
  const threePointData = [
1931
1941
  {
@@ -1943,7 +1953,7 @@ describe( 'BarChart', () => {
1943
1953
  const user = userEvent.setup();
1944
1954
  renderWithTheme( { withTooltips: true, data: threePointData } );
1945
1955
 
1946
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
1956
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
1947
1957
  chart.focus();
1948
1958
 
1949
1959
  // Move to the first, then on to the last (third) point.
@@ -1953,7 +1963,7 @@ describe( 'BarChart', () => {
1953
1963
  expect( screen.getByTestId( 'chart-tooltip-2' ) ).toHaveFocus();
1954
1964
 
1955
1965
  // Right arrow at the last point must not move focus: the highlighted
1956
- // bar/tooltip stays visible and focused (ARIA grid: focus does not move).
1966
+ // bar/tooltip stays visible and focused (focus does not move).
1957
1967
  await user.keyboard( '{ArrowRight}' );
1958
1968
  expect( screen.getByTestId( 'chart-tooltip-2' ) ).toBeInTheDocument();
1959
1969
  expect( screen.getByTestId( 'chart-tooltip-2' ) ).toHaveFocus();
@@ -1968,7 +1978,7 @@ describe( 'BarChart', () => {
1968
1978
  const user = userEvent.setup();
1969
1979
  renderWithTheme( { withTooltips: true, data: threePointData } );
1970
1980
 
1971
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
1981
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
1972
1982
  await user.keyboard( '{ArrowLeft}' );
1973
1983
 
1974
1984
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveFocus();
@@ -2002,7 +2012,7 @@ describe( 'BarChart', () => {
2002
2012
  ],
2003
2013
  } );
2004
2014
 
2005
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2015
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2006
2016
  chart.focus();
2007
2017
 
2008
2018
  await user.keyboard( '{ArrowRight}' );
@@ -2037,7 +2047,7 @@ describe( 'BarChart', () => {
2037
2047
  ],
2038
2048
  } );
2039
2049
 
2040
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2050
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2041
2051
  chart.focus();
2042
2052
 
2043
2053
  // Slot 0 is the first primary bar (Mon); slot 1 must be the SECOND primary
@@ -2069,7 +2079,7 @@ describe( 'BarChart', () => {
2069
2079
  ],
2070
2080
  } );
2071
2081
 
2072
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2082
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2073
2083
  await user.keyboard( '{ArrowRight}' );
2074
2084
 
2075
2085
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -2102,7 +2112,7 @@ describe( 'BarChart', () => {
2102
2112
  ],
2103
2113
  } );
2104
2114
 
2105
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2115
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2106
2116
  await user.keyboard( '{ArrowRight}' );
2107
2117
 
2108
2118
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -2162,7 +2172,7 @@ describe( 'BarChart', () => {
2162
2172
  ],
2163
2173
  } );
2164
2174
 
2165
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2175
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2166
2176
  chart.focus();
2167
2177
 
2168
2178
  // Chart should be in focus.
@@ -2202,7 +2212,7 @@ describe( 'BarChart', () => {
2202
2212
  ],
2203
2213
  } );
2204
2214
 
2205
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2215
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2206
2216
  chart.focus();
2207
2217
 
2208
2218
  // Navigate to the first bar
@@ -2237,7 +2247,7 @@ describe( 'BarChart', () => {
2237
2247
  ],
2238
2248
  } );
2239
2249
 
2240
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2250
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2241
2251
  chart.focus();
2242
2252
 
2243
2253
  // Navigate to the first bar (Series A, Jan 1)
@@ -2269,7 +2279,7 @@ describe( 'BarChart', () => {
2269
2279
  const user = userEvent.setup();
2270
2280
  renderWithTheme( { withTooltips: true, data } );
2271
2281
 
2272
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2282
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2273
2283
  await user.keyboard( '{ArrowRight}' );
2274
2284
 
2275
2285
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'No data' );
@@ -2280,7 +2290,7 @@ describe( 'BarChart', () => {
2280
2290
  const user = userEvent.setup();
2281
2291
  renderWithTheme( { withTooltips: true, data } );
2282
2292
 
2283
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2293
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2284
2294
  await user.keyboard( '{ArrowRight}{ArrowRight}' );
2285
2295
 
2286
2296
  expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveTextContent( '20' );
@@ -2313,7 +2323,7 @@ describe( 'BarChart', () => {
2313
2323
  ];
2314
2324
  renderWithTheme( { withTooltips: true, data: twoSeries } );
2315
2325
 
2316
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
2326
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2317
2327
  await user.keyboard( '{ArrowRight}{ArrowRight}{ArrowRight}{ArrowRight}' );
2318
2328
 
2319
2329
  expect( screen.getByTestId( 'chart-tooltip-3' ) ).toHaveTextContent( '15' );
@@ -2340,7 +2350,7 @@ describe( 'BarChart', () => {
2340
2350
 
2341
2351
  renderWithTheme( { withTooltips: true, renderTooltip: customTooltipRenderer } );
2342
2352
 
2343
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2353
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2344
2354
  chart.focus();
2345
2355
 
2346
2356
  // Click on right arrow key to focus on the first tooltip.
@@ -2372,7 +2382,9 @@ describe( 'BarChart', () => {
2372
2382
  ],
2373
2383
  } );
2374
2384
 
2375
- 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' );
2376
2388
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2377
2389
 
2378
2390
  // Should have 2 bars
@@ -2399,7 +2411,9 @@ describe( 'BarChart', () => {
2399
2411
  ],
2400
2412
  } );
2401
2413
 
2402
- 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' );
2403
2417
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2404
2418
 
2405
2419
  // Should have 2 bars
@@ -2432,7 +2446,9 @@ describe( 'BarChart', () => {
2432
2446
  orientation: 'horizontal',
2433
2447
  } );
2434
2448
 
2435
- 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' );
2436
2452
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2437
2453
 
2438
2454
  // Both bars should be visible in horizontal mode
@@ -2461,7 +2477,9 @@ describe( 'BarChart', () => {
2461
2477
  ],
2462
2478
  } );
2463
2479
 
2464
- 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' );
2465
2483
  const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
2466
2484
 
2467
2485
  expect( bars?.length ).toBe( 2 );
@@ -2501,7 +2519,7 @@ describe( 'BarChart', () => {
2501
2519
  },
2502
2520
  } );
2503
2521
 
2504
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
2522
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
2505
2523
  } );
2506
2524
 
2507
2525
  test( 'truncates labels with CSS text-overflow ellipsis', () => {
@@ -2594,7 +2612,7 @@ describe( 'BarChart', () => {
2594
2612
  } );
2595
2613
 
2596
2614
  // Chart should still render without errors
2597
- expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
2615
+ expect( screen.getByRole( 'application', { name: /bar chart/i } ) ).toBeInTheDocument();
2598
2616
 
2599
2617
  // Labels should still be present and have minimum width applied
2600
2618
  const label = screen.getByText( /Very Long Category Label One/i );
@@ -2890,7 +2908,7 @@ describe( 'BarChart', () => {
2890
2908
  </GlobalChartsProvider>
2891
2909
  );
2892
2910
 
2893
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
2911
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
2894
2912
  chart.focus();
2895
2913
 
2896
2914
  // Navigate through all 4 primary slots (2 series × 2 data points)
@@ -3176,8 +3194,9 @@ describe( 'BarChart', () => {
3176
3194
  </GlobalChartsProvider>
3177
3195
  );
3178
3196
 
3197
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
3179
3198
  // eslint-disable-next-line testing-library/no-node-access
3180
- const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
3199
+ const svgElement = chart.querySelector( 'svg' );
3181
3200
  // eslint-disable-next-line testing-library/no-node-access
3182
3201
  expect( svgElement?.querySelectorAll( '.visx-bar-group rect' ) ).toHaveLength( 2 );
3183
3202
 
@@ -3382,7 +3401,7 @@ describe( 'BarChart', () => {
3382
3401
  const user = userEvent.setup();
3383
3402
  const toggle = mountWithVisibilityToggle();
3384
3403
 
3385
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
3404
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
3386
3405
  for ( let i = 0; i < 6; i++ ) {
3387
3406
  await user.keyboard( '{ArrowRight}' );
3388
3407
  }
@@ -3427,7 +3446,7 @@ describe( 'BarChart', () => {
3427
3446
  ],
3428
3447
  } );
3429
3448
 
3430
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
3449
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
3431
3450
  for ( let i = 0; i < 6; i++ ) {
3432
3451
  await user.keyboard( '{ArrowRight}' );
3433
3452
  }
@@ -3484,12 +3503,12 @@ describe( 'BarChart', () => {
3484
3503
  </GlobalChartsProvider>
3485
3504
  );
3486
3505
 
3487
- screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
3506
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
3488
3507
  await user.keyboard( '{ArrowRight}{ArrowRight}' );
3489
3508
  expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveTextContent( 'Series A' );
3490
3509
 
3491
3510
  // Move focus out of the chart, then swap which series is hidden without changing the count.
3492
- screen.getByRole( 'grid', { name: /bar chart/i } ).blur();
3511
+ screen.getByRole( 'application', { name: /bar chart/i } ).blur();
3493
3512
  act( () => context.setChartHiddenSeries( 'test-equal-count-swap', [ 'Series A' ] ) );
3494
3513
 
3495
3514
  expect( screen.queryAllByTestId( /^chart-tooltip-/ ) ).toHaveLength( 0 );
@@ -3528,7 +3547,7 @@ describe( 'BarChart', () => {
3528
3547
 
3529
3548
  await user.click( screen.getByRole( 'button', { name: /Series B: visible/i } ) );
3530
3549
 
3531
- const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
3550
+ const chart = screen.getByRole( 'application', { name: /bar chart/i } );
3532
3551
  chart.focus();
3533
3552
 
3534
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
 
@@ -3,6 +3,8 @@
3
3
  // module, so it needs no runtime cascade and never lands on the DOM.
4
4
  $focus-ring-width: var(--wpds-border-width-focus);
5
5
 
6
+ $heatmap-empty-fill: var(--a8c-charts-color-track);
7
+
6
8
  // JS sets the ends (getHeatmapScale): 3:1 against background and empty cell at
7
9
  // the low end. Unresolved, the floor is 15% of the primary.
8
10
  $heatmap-fill: color-mix(in sRGB, var(--a8c-charts-color-heatmap-high, var(--a8c-charts-color-heatmap-primary)) calc(var(--a8c-charts-heatmap-cell-intensity) * 100%), var(--a8c-charts-color-heatmap-low, color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) 15%, var(--a8c-charts-color-background, #fff))));
@@ -96,9 +98,9 @@ $heatmap-fill: color-mix(in sRGB, var(--a8c-charts-color-heatmap-high, var(--a8c
96
98
 
97
99
  // Beneath its columns, set off from the last row by more than the cell gap.
98
100
  .heatmap-chart__group-label {
99
- color: var(--a8c-charts-color-label-secondary);
100
- font-size: var(--wpds-typography-font-size-xs);
101
- line-height: var(--wpds-typography-line-height-xs);
101
+ color: var(--a8c-charts-color-label);
102
+ font-size: var(--wpds-typography-font-size-md);
103
+ line-height: var(--wpds-typography-line-height-sm);
102
104
  white-space: nowrap;
103
105
  text-align: center;
104
106
  padding-block-start: var(--wpds-dimension-padding-xs);
@@ -119,7 +121,7 @@ $heatmap-fill: color-mix(in sRGB, var(--a8c-charts-color-heatmap-high, var(--a8c
119
121
  min-height: 0;
120
122
  border-radius: var(--a8c-charts-border-radius-cell);
121
123
  // Empty (no-data) default; cells with a value override it below.
122
- background: var(--a8c-charts-color-track);
124
+ background: $heatmap-empty-fill;
123
125
  }
124
126
 
125
127
  // An empty grid slot (calendar ragged edges): keeps its track but paints and
@@ -209,6 +211,10 @@ $heatmap-fill: color-mix(in sRGB, var(--a8c-charts-color-heatmap-high, var(--a8c
209
211
  background: $heatmap-fill;
210
212
  }
211
213
 
214
+ .heatmap-chart__legend-swatch--empty {
215
+ background: $heatmap-empty-fill;
216
+ }
217
+
212
218
  // One band: the steps butt against each other and only the band's ends round.
213
219
  .heatmap-chart__legend-scale--bar {
214
220
  overflow: hidden;