@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.
- package/CHANGELOG.md +30 -0
- package/TOKENS.md +6 -3
- package/dist/index.cjs +834 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +68 -17
- package/dist/index.d.cts +102 -28
- package/dist/index.d.ts +102 -28
- package/dist/index.js +838 -327
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +7 -4
- package/src/charts/area-chart/test/area-chart.test.tsx +24 -15
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/area-chart/types.ts +2 -0
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +25 -16
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +106 -62
- package/src/charts/bar-chart/types.ts +2 -0
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +16 -8
- package/src/charts/heatmap-chart/heatmap-chart.tsx +183 -66
- package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
- package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +15 -3
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +6 -3
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
- package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +231 -8
- package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
- package/src/charts/heatmap-chart/types.ts +6 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
- package/src/charts/leaderboard-chart/types.ts +6 -1
- package/src/charts/line-chart/line-chart.tsx +7 -4
- package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
- package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/line-chart/types.ts +5 -3
- package/src/charts/pie-chart/pie-chart.tsx +153 -143
- package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +126 -124
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +55 -1
- package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
- package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
- package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
- package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
- package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
- package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
- package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
- package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
- package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
- package/src/charts/private/test/label-text-color.test.ts +6 -8
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
- package/src/components/tooltip/base-tooltip.tsx +31 -32
- package/src/components/tooltip/index.ts +3 -1
- package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
- package/src/components/tooltip/private/label-value-content.tsx +8 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
- package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
- package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
- package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +283 -365
- package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
- package/src/components/tooltip/tooltip-box.tsx +43 -0
- package/src/components/tooltip/types.ts +7 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
- package/src/hooks/test/use-chart-margin.test.tsx +8 -4
- package/src/hooks/use-chart-margin.tsx +21 -18
- package/src/index.ts +8 -3
- package/src/providers/chart-context/global-charts-provider.tsx +5 -14
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
- package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
- package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
- package/src/styles/chart-scope.scss +1 -0
- package/src/test-utils/zoned-parsing-suite.ts +0 -10
- package/src/types.ts +18 -2
- package/src/utils/test/date-formatting.test.ts +0 -10
- package/src/components/tooltip/accessible-tooltip.tsx +0 -433
- 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', '#
|
|
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
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 =
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 './
|
|
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 {
|
|
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 {
|
|
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<
|
|
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
|
-
*
|
|
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 -
|
|
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
|
-
|
|
217
|
-
<
|
|
216
|
+
return (
|
|
217
|
+
<TooltipBox
|
|
218
218
|
ref={ nodeRef }
|
|
219
|
+
role="presentation"
|
|
219
220
|
data-testid="bounded-tooltip"
|
|
220
|
-
|
|
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
|
-
</
|
|
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
|
+
);
|