@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
@@ -0,0 +1,47 @@
1
+ import styles from './pie-keyboard-navigation.module.scss';
2
+
3
+ interface SelectedSegmentRingProps {
4
+ /** The chart's id, which keeps the clip path unique on a page with several charts. */
5
+ chartId: string;
6
+ /** The selected segment's path, from the same arc generator that draws the segment. */
7
+ d: string;
8
+ }
9
+
10
+ /**
11
+ * Encodes `value` for an SVG id, losslessly so distinct chart ids never share a clip path.
12
+ *
13
+ * @param value - Any string.
14
+ * @return `value` with each character outside `[A-Za-z0-9-]` written as `_<hex code point>_`.
15
+ */
16
+ export const toSvgId = ( value: string ) =>
17
+ Array.from( value, character =>
18
+ /[A-Za-z0-9-]/.test( character )
19
+ ? character
20
+ : `_${ character.codePointAt( 0 )?.toString( 16 ) }_`
21
+ ).join( '' );
22
+
23
+ /**
24
+ * Marks the selected segment with a two-tone ring drawn inside its edge, so the SVG edge cannot cut it off.
25
+ *
26
+ * @param {SelectedSegmentRingProps} props - Component props
27
+ * @return {JSX.Element} The ring
28
+ */
29
+ export const SelectedSegmentRing = ( { chartId, d }: SelectedSegmentRingProps ) => {
30
+ const clipId = `pie-selected-ring-clip-${ toSvgId( chartId ) }`;
31
+ const clipPath = `url(#${ clipId })`;
32
+
33
+ return (
34
+ <g pointerEvents="none" aria-hidden="true">
35
+ <clipPath id={ clipId }>
36
+ <path d={ d } />
37
+ </clipPath>
38
+ <path d={ d } clipPath={ clipPath } className={ styles[ 'selected-ring__contrast' ] } />
39
+ <path
40
+ d={ d }
41
+ clipPath={ clipPath }
42
+ className={ styles[ 'selected-ring__focus' ] }
43
+ data-testid="pie-selected-ring"
44
+ />
45
+ </g>
46
+ );
47
+ };
@@ -0,0 +1,34 @@
1
+ import { orderArcsForNavigation } from '../order-arcs-for-navigation';
2
+
3
+ const half = Math.PI / 2;
4
+
5
+ describe( 'orderArcsForNavigation', () => {
6
+ it.each( [
7
+ [
8
+ 'full pie, clockwise from 12 o’clock',
9
+ [
10
+ { id: 'b', startAngle: Math.PI, endAngle: 2 * Math.PI },
11
+ { id: 'a', startAngle: 0, endAngle: Math.PI },
12
+ ],
13
+ [ 'a', 'b' ],
14
+ ],
15
+ [
16
+ 'clockwise semi-circle, left to right',
17
+ [
18
+ { id: 'right', startAngle: 0, endAngle: half },
19
+ { id: 'left', startAngle: -half, endAngle: 0 },
20
+ ],
21
+ [ 'left', 'right' ],
22
+ ],
23
+ [
24
+ 'counter-clockwise semi-circle, left to right',
25
+ [
26
+ { id: 'right', startAngle: half, endAngle: 0 },
27
+ { id: 'left', startAngle: 0, endAngle: -half },
28
+ ],
29
+ [ 'left', 'right' ],
30
+ ],
31
+ ] )( 'orders a %s', ( _, arcs, expected ) => {
32
+ expect( orderArcsForNavigation( arcs ).map( arc => arc.id ) ).toEqual( expected );
33
+ } );
34
+ } );
@@ -0,0 +1,14 @@
1
+ import { toSvgId } from '../selected-segment-ring';
2
+
3
+ describe( 'toSvgId', () => {
4
+ it.each( [
5
+ [ 'devices:1', 'devices/1' ],
6
+ [ 'a_3a_', 'a:' ],
7
+ [ ':r1:', '_r1_' ],
8
+ ] )( 'keeps %s and %s apart', ( first, second ) => {
9
+ const ids = [ toSvgId( first ), toSvgId( second ) ];
10
+
11
+ expect( ids[ 0 ] ).not.toBe( ids[ 1 ] );
12
+ ids.forEach( id => expect( id ).toMatch( /^[A-Za-z0-9_-]+$/ ) );
13
+ } );
14
+ } );
@@ -0,0 +1,139 @@
1
+ import { useTooltip } from '@visx/tooltip';
2
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
3
+ import { useKeyboardNavigation } from '../../../components/tooltip';
4
+ import type { DataPointPercentageCalculated } from '../../../types';
5
+ import type { MouseEvent } from 'react';
6
+
7
+ interface UsePieKeyboardNavigationProps< Arc > {
8
+ /** The visible arcs in `orderArcsForNavigation` order, which indexes run over. */
9
+ navigationArcs: Arc[];
10
+ withTooltips: boolean;
11
+ tooltipOffsetX: number;
12
+ tooltipOffsetY: number;
13
+ }
14
+
15
+ export const usePieKeyboardNavigation = < Arc extends { data: DataPointPercentageCalculated } >( {
16
+ navigationArcs,
17
+ withTooltips,
18
+ tooltipOffsetX,
19
+ tooltipOffsetY,
20
+ }: UsePieKeyboardNavigationProps< Arc > ) => {
21
+ const chartRef = useRef< HTMLDivElement >( null );
22
+ const svgRef = useRef< SVGSVGElement >( null );
23
+ const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
24
+ const [ isNavigating, setIsNavigating ] = useState( false );
25
+ const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
26
+ useTooltip< DataPointPercentageCalculated >();
27
+
28
+ const {
29
+ tooltipRef: focusTooltip,
30
+ onChartFocus,
31
+ onChartBlur,
32
+ onChartKeyDown,
33
+ onChartPointerMove,
34
+ } = useKeyboardNavigation( {
35
+ selectedIndex,
36
+ setSelectedIndex,
37
+ isNavigating,
38
+ setIsNavigating,
39
+ chartRef,
40
+ totalPoints: navigationArcs.length,
41
+ visibleSeriesKey: JSON.stringify( navigationArcs.map( arcDatum => arcDatum.data.label ) ),
42
+ } );
43
+
44
+ const selectedArc = selectedIndex === undefined ? undefined : navigationArcs[ selectedIndex ];
45
+
46
+ const keyboardTooltip = useRef< HTMLDivElement | null >( null );
47
+ const tooltipLostFocus = useRef( false );
48
+
49
+ // React detaches the ref before removing the node, so the tooltip still holds focus here.
50
+ const tooltipRef = useCallback(
51
+ ( element: HTMLDivElement | null ) => {
52
+ if ( element ) {
53
+ keyboardTooltip.current = element;
54
+ focusTooltip( element );
55
+ } else if (
56
+ keyboardTooltip.current?.ownerDocument.activeElement === keyboardTooltip.current
57
+ ) {
58
+ tooltipLostFocus.current = true;
59
+ }
60
+ },
61
+ [ focusTooltip ]
62
+ );
63
+
64
+ // Arrow keys mount the next tooltip in the same commit, so focus only reaches the body when the selection's tooltip went away.
65
+ useLayoutEffect( () => {
66
+ if ( ! tooltipLostFocus.current ) {
67
+ return;
68
+ }
69
+ tooltipLostFocus.current = false;
70
+ const { activeElement, body } = chartRef.current?.ownerDocument ?? document;
71
+ if ( activeElement === null || activeElement === body ) {
72
+ chartRef.current?.focus();
73
+ }
74
+ } );
75
+
76
+ const getSegmentHandlers = ( data: DataPointPercentageCalculated, navigationIndex: number ) => ( {
77
+ onMouseMove: ( event: MouseEvent< SVGElement > ) => {
78
+ onChartPointerMove( navigationIndex );
79
+ if ( ! withTooltips ) {
80
+ return;
81
+ }
82
+
83
+ // The tooltip renders inside `chartRef`, so pointer coordinates are taken relative to it.
84
+ const bounds = chartRef.current?.getBoundingClientRect();
85
+ if ( ! bounds ) {
86
+ return;
87
+ }
88
+
89
+ showTooltip( {
90
+ tooltipData: data,
91
+ tooltipLeft: event.clientX - bounds.left + tooltipOffsetX,
92
+ tooltipTop: event.clientY - bounds.top + tooltipOffsetY,
93
+ } );
94
+ },
95
+ onMouseLeave: withTooltips ? hideTooltip : undefined,
96
+ } );
97
+
98
+ /**
99
+ * Anchors the keyboard tooltip at `point`, an offset from the origin of the group the arcs are drawn in.
100
+ *
101
+ * @param originX - The group's x in SVG coordinates.
102
+ * @param originY - The group's y in SVG coordinates.
103
+ * @param point - The point relative to the group, such as an arc centroid.
104
+ * @return The anchor in SVG coordinates, or undefined without tooltips.
105
+ */
106
+ const getKeyboardTooltipAnchor = useCallback(
107
+ ( originX: number, originY: number, [ x, y ]: [ number, number ] ) =>
108
+ withTooltips
109
+ ? { left: originX + x + tooltipOffsetX, top: originY + y + tooltipOffsetY }
110
+ : undefined,
111
+ [ withTooltips, tooltipOffsetX, tooltipOffsetY ]
112
+ );
113
+
114
+ return {
115
+ chartRef,
116
+ svgRef,
117
+ selectedArc,
118
+ getSegmentHandlers,
119
+ getKeyboardTooltipAnchor,
120
+ selectionOutputProps: {
121
+ withTooltips,
122
+ selectedIndex,
123
+ selectedData: selectedArc?.data,
124
+ chartRef,
125
+ svgRef,
126
+ tooltipRef,
127
+ pointerTooltip: tooltipOpen
128
+ ? { data: tooltipData, left: tooltipLeft || 0, top: tooltipTop || 0 }
129
+ : undefined,
130
+ },
131
+ chartProps: {
132
+ role: 'application',
133
+ tabIndex: 0,
134
+ onKeyDown: onChartKeyDown,
135
+ onFocus: onChartFocus,
136
+ onBlur: onChartBlur,
137
+ } as const,
138
+ };
139
+ };
@@ -5,6 +5,7 @@ import {
5
5
  luminanceContrastRatio,
6
6
  rgbLuminance,
7
7
  } from '../../../providers/chart-context/private/perceptual-color';
8
+ import { getHeatmapScale } from '../../heatmap-chart/private/heatmap-scale';
8
9
  import {
9
10
  pickLabelTextColor,
10
11
  pickLabelTextColorForFill,
@@ -131,9 +132,9 @@ describe( 'pickLabelTextColorForFill', () => {
131
132
  } );
132
133
 
133
134
  describe.each( [
134
- [ 'white', '#ffffff' ],
135
- [ 'dark', '#1e1e1e' ],
136
- ] )( 'across a heatmap scale on a %s background', ( _name, background ) => {
135
+ [ 'white', '#ffffff', '#f0f0f0' ],
136
+ [ 'dark', '#242424', '#272727' ],
137
+ ] )( 'across a heatmap scale on a %s background', ( _name, background, emptyCell ) => {
137
138
  const painted: Record< LabelTextColor, number > = {
138
139
  label: rgbLuminance( hexToRgb( '#1e1e1e' ) ),
139
140
  'label-inverse': rgbLuminance( hexToRgb( '#f0f0f0' ) ),
@@ -155,12 +156,9 @@ describe( 'pickLabelTextColorForFill', () => {
155
156
  ];
156
157
 
157
158
  it.each( primaries )( 'reaches AA on every intensity of %s', primary => {
159
+ const { low, high } = getHeatmapScale( primary, background, emptyCell );
158
160
  for ( let step = 0; step <= 200; step++ ) {
159
- const fill = blendRgb(
160
- hexToRgb( primary ),
161
- hexToRgb( background ),
162
- 0.15 + 0.85 * ( step / 200 )
163
- );
161
+ const fill = blendRgb( hexToRgb( high ), hexToRgb( low ), step / 200 );
164
162
  const choice = pickLabelTextColorForFill( fill, DEFAULT_ROLES, 'label' );
165
163
  // Checked on the 8-bit channels the browser paints as well as the exact mix.
166
164
  const [ r, g, b ] = fill.map( Math.round );
@@ -212,11 +212,12 @@ describe( 'ZoomResetButton', () => {
212
212
 
213
213
  test( 'keyboard activation survives the chart wrapper keydown handler', async () => {
214
214
  const onClick = jest.fn();
215
- // Mirrors the chart's grid wrapper, whose keyboard-navigation handler
215
+ // Mirrors the chart's focusable container, whose keyboard-navigation handler
216
216
  // calls preventDefault() on bubbled keydowns — which would cancel the
217
217
  // native Enter/Space button activation.
218
218
  render(
219
- <div role="grid" tabIndex={ 0 } onKeyDown={ preventDefaultKeydown }>
219
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- mirrors the charts' container.
220
+ <div role="application" tabIndex={ 0 } onKeyDown={ preventDefaultKeydown }>
220
221
  <ZoomResetButton onClick={ onClick } />
221
222
  </div>
222
223
  );
@@ -1,36 +1,32 @@
1
- import { formatNumber } from '@automattic/number-formatters';
2
- import clsx from 'clsx';
3
- import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
4
- import styles from './base-tooltip.module.scss';
5
- import { TooltipTheme } from './private/tooltip-theme';
1
+ import { LabelValueContent } from './private/label-value-content';
2
+ import { TooltipBox } from './tooltip-box';
3
+ import type { TooltipData } from './types';
6
4
  import type { CSSProperties, ComponentType, ReactNode } from 'react';
7
5
 
8
- type TooltipData = {
9
- label: string;
10
- value: number;
11
- valueDisplay?: string;
12
- };
13
-
14
6
  type TooltipComponentProps = {
15
7
  data: TooltipData;
16
8
  className?: string;
17
9
  };
18
10
 
19
11
  type TooltipCommonProps = {
20
- top: number;
21
- left: number;
12
+ /** @deprecated Position the box with `style`, or use `TooltipBox`. */
13
+ top?: number;
14
+ /** @deprecated Position the box with `style`, or use `TooltipBox`. */
15
+ left?: number;
22
16
  style?: CSSProperties;
23
17
  className?: string;
24
18
  /**
25
19
  * Whether to render the tooltip container div. When false, only renders the content.
26
- * Useful when the tooltip is rendered inside a portal or custom container.
20
+ * @deprecated Render the content directly; `TooltipBox` draws the box.
27
21
  * @default true
28
22
  */
29
23
  renderContainer?: boolean;
30
24
  };
31
25
 
32
26
  type DefaultDataTooltip = {
27
+ /** @deprecated Pass the content as children, or use `TooltipBox`. */
33
28
  data: TooltipData;
29
+ /** @deprecated Pass the content as children, or use `TooltipBox`. */
34
30
  component?: ComponentType< TooltipComponentProps >;
35
31
  children?: never;
36
32
  };
@@ -43,17 +39,25 @@ type CustomTooltip = {
43
39
 
44
40
  type BaseTooltipProps = TooltipCommonProps & ( DefaultDataTooltip | CustomTooltip );
45
41
 
46
- const DefaultTooltipContent = ( { data }: TooltipComponentProps ) => (
47
- <>
48
- { data?.label }: { data?.valueDisplay || formatNumber( data?.value ) }
49
- </>
50
- );
51
-
42
+ /**
43
+ * A tooltip box with children or `label: value` content. Prefer `TooltipBox`.
44
+ *
45
+ * @param props - Tooltip props.
46
+ * @param props.data - Data for the default content.
47
+ * @param props.top - Deprecated vertical position.
48
+ * @param props.left - Deprecated horizontal position.
49
+ * @param props.component - Deprecated content component.
50
+ * @param props.children - Tooltip content.
51
+ * @param props.className - Class name passed to the content component.
52
+ * @param props.style - Styles for the box.
53
+ * @param props.renderContainer - Deprecated; render the content alone when false.
54
+ * @return The tooltip.
55
+ */
52
56
  export const BaseTooltip = ( {
53
57
  data,
54
58
  top,
55
59
  left,
56
- component: Component = DefaultTooltipContent,
60
+ component: Component = LabelValueContent,
57
61
  children,
58
62
  className,
59
63
  style,
@@ -65,17 +69,12 @@ export const BaseTooltip = ( {
65
69
  return content;
66
70
  }
67
71
 
68
- return (
69
- <TooltipTheme>
70
- <div
71
- className={ clsx( CHART_SCOPE_CLASS, styles.tooltip ) }
72
- style={ { top, left, ...style } }
73
- role="tooltip"
74
- >
75
- { content }
76
- </div>
77
- </TooltipTheme>
78
- );
72
+ const placement =
73
+ top === undefined && left === undefined
74
+ ? undefined
75
+ : ( { position: 'absolute', top, left, transform: 'translate(-50%, -100%)' } as const );
76
+
77
+ return <TooltipBox style={ { ...placement, ...style } }>{ content }</TooltipBox>;
79
78
  };
80
79
 
81
80
  export type { BaseTooltipProps, TooltipData };
@@ -1,4 +1,6 @@
1
1
  export { BaseTooltip } from './base-tooltip';
2
- export { AccessibleTooltip, useKeyboardNavigation } from './accessible-tooltip';
2
+ export { AccessibleTooltip, XYChartTooltip, useKeyboardNavigation } from './xy-chart-tooltip';
3
+ export { TooltipBox } from './tooltip-box';
3
4
  export type { BaseTooltipProps, TooltipData } from './base-tooltip';
5
+ export type { TooltipBoxProps } from './tooltip-box';
4
6
  export type { TooltipProps } from './types';
@@ -1,19 +1,19 @@
1
- import { Tooltip } from '@visx/tooltip';
2
- import clsx from 'clsx';
3
1
  import { useLayoutEffect, useRef, useState } from 'react';
4
- import { TOOLTIP_SCOPE_CLASS } from '../../../styles/chart-scope-class';
5
- import styles from '../base-tooltip.module.scss';
6
- import { TooltipTheme } from './tooltip-theme';
2
+ import { TooltipBox } from '../tooltip-box';
7
3
  import type { TooltipPlacement } from '../../../visx/types';
8
- import type { TooltipProps } from '@visx/tooltip';
4
+ import type { TooltipBoxProps } from '../tooltip-box';
5
+ import type { ReactNode } from 'react';
9
6
 
10
7
  type Bounds = { left: number; top: number; right: number; bottom: number };
11
8
  type Size = { width: number; height: number };
12
9
 
13
- export type BoundedTooltipProps = Omit< TooltipProps, 'left' | 'top' | 'applyPositionStyle' > & {
10
+ export type BoundedTooltipProps = Omit< TooltipBoxProps, 'children' > & {
11
+ children?: ReactNode;
14
12
  /** Anchor, in the coordinates of the positioned wrapper the box renders into. */
15
13
  left?: number;
16
14
  top?: number;
15
+ offsetLeft?: number;
16
+ offsetTop?: number;
17
17
  placement?: TooltipPlacement;
18
18
  };
19
19
 
@@ -135,7 +135,7 @@ export const getBoundedPosition = ( {
135
135
  };
136
136
 
137
137
  /**
138
- * visx's `Tooltip`, positioned like its `TooltipWithBounds` but kept inside the
138
+ * A `TooltipBox`, positioned like visx's `TooltipWithBounds` but kept inside the
139
139
  * nearest clipping ancestor — or the viewport when there is none — rather than
140
140
  * inside its own parent. Rendered in-tree, a tooltip's parent is the chart
141
141
  * wrapper, which is often narrower than the box; measuring against the parent
@@ -144,7 +144,7 @@ export const getBoundedPosition = ( {
144
144
  * Re-measures on every render, so a box whose content changes width between
145
145
  * two hovers is placed for its current size.
146
146
  *
147
- * @param props - visx `Tooltip` props.
147
+ * @param props - `TooltipBox` props plus placement.
148
148
  * @param props.left - Anchor x, in wrapper coordinates.
149
149
  * @param props.top - Anchor y, in wrapper coordinates.
150
150
  * @param props.offsetLeft - Gap between the anchor and the box, horizontally.
@@ -213,11 +213,13 @@ export const BoundedTooltip = ( {
213
213
  const x = position?.x ?? left + offsetLeft;
214
214
  const y = position?.y ?? top + ( placement === 'below-axis' ? POINTER_HEIGHT : offsetTop );
215
215
 
216
- const box = (
217
- <Tooltip
216
+ return (
217
+ <TooltipBox
218
218
  ref={ nodeRef }
219
+ role="presentation"
219
220
  data-testid="bounded-tooltip"
220
- className={ clsx( ! unstyled && [ TOOLTIP_SCOPE_CLASS, styles.surface ], className ) }
221
+ unstyled={ unstyled }
222
+ className={ className }
221
223
  style={ {
222
224
  position: 'absolute',
223
225
  left: 0,
@@ -245,8 +247,6 @@ export const BoundedTooltip = ( {
245
247
  />
246
248
  ) }
247
249
  { children }
248
- </Tooltip>
250
+ </TooltipBox>
249
251
  );
250
-
251
- return unstyled ? box : <TooltipTheme>{ box }</TooltipTheme>;
252
252
  };
@@ -0,0 +1,8 @@
1
+ import { formatNumber } from '@automattic/number-formatters';
2
+ import type { TooltipData } from '../types';
3
+
4
+ export const LabelValueContent = ( { data }: { data: TooltipData } ) => (
5
+ <>
6
+ { data.label }: { data.valueDisplay || formatNumber( data.value ) }
7
+ </>
8
+ );