@automattic/charts 4.6.0 → 4.8.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 +32 -0
- package/TOKENS.md +15 -10
- package/dist/index.cjs +427 -211
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +40 -26
- package/dist/index.d.cts +69 -29
- package/dist/index.d.ts +69 -29
- package/dist/index.js +427 -213
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +2 -2
- package/src/charts/area-chart/test/area-chart.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +20 -14
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +25 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +0 -10
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +5 -3
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +20 -1
- package/src/charts/conversion-funnel-chart/types.ts +3 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +16 -7
- package/src/charts/heatmap-chart/heatmap-chart.tsx +80 -53
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +2 -2
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +2 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +221 -25
- 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 -9
- package/src/charts/line-chart/crosshair-style-types.fixture.ts +23 -0
- package/src/charts/line-chart/line-chart.module.scss +0 -5
- package/src/charts/line-chart/line-chart.tsx +10 -42
- package/src/charts/line-chart/test/line-chart.test.tsx +19 -47
- package/src/charts/pie-chart/pie-chart.module.scss +9 -0
- package/src/charts/pie-chart/pie-chart.tsx +26 -80
- package/src/charts/pie-chart/test/pie-chart.test.tsx +21 -8
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -4
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +1 -1
- package/src/charts/private/chart-composition/chart-html.tsx +1 -1
- package/src/charts/private/chart-composition/chart-svg.tsx +1 -1
- package/src/charts/private/label-text-color.ts +116 -0
- package/src/charts/private/radial-wipe-animation/radial-wipe-animation.tsx +2 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.tsx +1 -1
- package/src/charts/private/test/label-text-color.test.ts +189 -0
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +1 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +2 -5
- package/src/components/tooltip/base-tooltip.tsx +31 -30
- package/src/components/tooltip/index.ts +3 -1
- package/src/components/tooltip/private/bounded-tooltip.tsx +32 -13
- package/src/components/tooltip/private/label-value-content.tsx +8 -0
- package/src/components/tooltip/private/tooltip-theme.tsx +37 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +82 -1
- package/src/components/tooltip/test/tool-tip.test.tsx +10 -3
- package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
- package/src/components/tooltip/test/tooltip-theme.test.tsx +61 -0
- package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +286 -356
- package/src/components/tooltip/tooltip-box.module.scss +21 -0
- 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 +411 -301
- package/src/components/trend-indicator/trend-indicator.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/test/use-chart-margin.test.tsx +8 -4
- package/src/hooks/test/use-xychart-theme.test.tsx +3 -3
- package/src/hooks/use-chart-margin.tsx +21 -18
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-xychart-theme.ts +4 -12
- 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 +3 -5
- package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
- package/src/providers/chart-context/private/perceptual-color.ts +45 -4
- package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
- package/src/providers/chart-context/test/perceptual-color.test.ts +31 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
- package/src/styles/chart-scope-class.ts +5 -0
- package/src/styles/chart-scope.scss +10 -6
- package/src/styles/test/chart-scope.test.ts +3 -3
- package/src/test-utils/zoned-parsing-suite.ts +0 -10
- package/src/utils/color-utils.ts +2 -1
- package/src/utils/test/date-formatting.test.ts +0 -10
- package/src/visx/types.ts +1 -1
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +0 -31
- package/src/components/tooltip/accessible-tooltip.tsx +0 -433
- package/src/components/tooltip/base-tooltip.module.scss +0 -20
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { MIN_LABEL_CONTRAST } from '../../../providers/chart-context/private/palette-generator';
|
|
2
|
+
import {
|
|
3
|
+
blendRgb,
|
|
4
|
+
hexToRgb,
|
|
5
|
+
luminanceContrastRatio,
|
|
6
|
+
rgbLuminance,
|
|
7
|
+
} from '../../../providers/chart-context/private/perceptual-color';
|
|
8
|
+
import { getHeatmapScale } from '../../heatmap-chart/private/heatmap-scale';
|
|
9
|
+
import {
|
|
10
|
+
pickLabelTextColor,
|
|
11
|
+
pickLabelTextColorForFill,
|
|
12
|
+
resolveLabelRoles,
|
|
13
|
+
} from '../label-text-color';
|
|
14
|
+
import type { Rgb } from '../../../providers/chart-context/private/perceptual-color';
|
|
15
|
+
import type { LabelRoles, LabelTextColor, ResolvedRole } from '../label-text-color';
|
|
16
|
+
|
|
17
|
+
const color = ( hex: string, alpha = 1 ): ResolvedRole => ( {
|
|
18
|
+
kind: 'color',
|
|
19
|
+
rgb: hexToRgb( hex ),
|
|
20
|
+
alpha,
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
const unreadable = ( raw: string | null ): ResolvedRole => ( { kind: 'unreadable', raw } );
|
|
24
|
+
|
|
25
|
+
const DEFAULT_ROLES: LabelRoles = { label: color( '#1e1e1e' ), labelInverse: color( '#f0f0f0' ) };
|
|
26
|
+
|
|
27
|
+
const WHITE = hexToRgb( '#ffffff' );
|
|
28
|
+
const MID_BLUE = hexToRgb( '#3858e9' );
|
|
29
|
+
|
|
30
|
+
describe( 'resolveLabelRoles', () => {
|
|
31
|
+
const resolverFor =
|
|
32
|
+
( label: string | null, labelInverse: string | null ) =>
|
|
33
|
+
( value: string ): string | null =>
|
|
34
|
+
value.includes( '--a8c-charts-color-label-inverse' ) ? labelInverse : label;
|
|
35
|
+
|
|
36
|
+
it( 'resolves opaque roles in any syntax d3 reads', () => {
|
|
37
|
+
expect( resolveLabelRoles( resolverFor( '#1E1E1E', 'rgb(240, 240, 240)' ) ) ).toEqual(
|
|
38
|
+
DEFAULT_ROLES
|
|
39
|
+
);
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
it( "keeps a role's alpha", () => {
|
|
43
|
+
expect( resolveLabelRoles( resolverFor( 'rgba(30, 30, 30, 0.87)', '#f0f0f0' ) ).label ).toEqual(
|
|
44
|
+
color( '#1e1e1e', 0.87 )
|
|
45
|
+
);
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'reads transparent as a role with no alpha', () => {
|
|
49
|
+
expect( resolveLabelRoles( resolverFor( 'transparent', '#f0f0f0' ) ).label ).toEqual(
|
|
50
|
+
color( '#000000', 0 )
|
|
51
|
+
);
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'keeps the raw value of a role it cannot read', () => {
|
|
55
|
+
expect( resolveLabelRoles( resolverFor( null, 'oklch(98% 0 0)' ) ) ).toEqual( {
|
|
56
|
+
label: unreadable( null ),
|
|
57
|
+
labelInverse: unreadable( 'oklch(98% 0 0)' ),
|
|
58
|
+
} );
|
|
59
|
+
} );
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
describe( 'pickLabelTextColorForFill', () => {
|
|
63
|
+
it( 'keeps the default role before the roles are read', () => {
|
|
64
|
+
expect( pickLabelTextColorForFill( MID_BLUE, null, 'label' ) ).toBe( 'label' );
|
|
65
|
+
expect( pickLabelTextColorForFill( MID_BLUE, null, 'label-inverse' ) ).toBe( 'label-inverse' );
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'picks the role that contrasts more when it reaches AA', () => {
|
|
69
|
+
expect( pickLabelTextColorForFill( WHITE, DEFAULT_ROLES, 'label-inverse' ) ).toBe( 'label' );
|
|
70
|
+
expect( pickLabelTextColorForFill( MID_BLUE, DEFAULT_ROLES, 'label' ) ).toBe( 'label-inverse' );
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
it( 'falls back to black or white when neither role reaches AA', () => {
|
|
74
|
+
expect( pickLabelTextColorForFill( hexToRgb( '#516dec' ), DEFAULT_ROLES, 'label' ) ).toBe(
|
|
75
|
+
'black'
|
|
76
|
+
);
|
|
77
|
+
expect( pickLabelTextColorForFill( hexToRgb( '#4663ea' ), DEFAULT_ROLES, 'label' ) ).toBe(
|
|
78
|
+
'white'
|
|
79
|
+
);
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
it( 'measures a translucent role as it paints over the fill', () => {
|
|
83
|
+
// Opaque, this label would read 16:1 on white; at 30% it paints a pale gray that fails AA.
|
|
84
|
+
const faintLabel: LabelRoles = {
|
|
85
|
+
label: color( '#1e1e1e', 0.3 ),
|
|
86
|
+
labelInverse: color( '#f0f0f0' ),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
expect( pickLabelTextColorForFill( WHITE, faintLabel, 'label-inverse' ) ).toBe( 'black' );
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
it( 'never picks a fully transparent role', () => {
|
|
93
|
+
const clearLabel: LabelRoles = {
|
|
94
|
+
label: color( '#000000', 0 ),
|
|
95
|
+
labelInverse: color( '#f0f0f0' ),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
expect( pickLabelTextColorForFill( WHITE, clearLabel, 'label' ) ).toBe( 'black' );
|
|
99
|
+
} );
|
|
100
|
+
|
|
101
|
+
it( 'treats two different translucent roles as two colors', () => {
|
|
102
|
+
const translucent: LabelRoles = {
|
|
103
|
+
label: color( '#000000', 0.87 ),
|
|
104
|
+
labelInverse: color( '#ffffff', 0.9 ),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
expect( pickLabelTextColorForFill( MID_BLUE, translucent, 'label' ) ).toBe( 'label-inverse' );
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
it( 'uses a readable role when the other cannot be read, else black or white', () => {
|
|
111
|
+
const unreadableInverse: LabelRoles = {
|
|
112
|
+
label: color( '#1e1e1e' ),
|
|
113
|
+
labelInverse: unreadable( 'rgb(255 255 255)' ),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect( pickLabelTextColorForFill( WHITE, unreadableInverse, 'label-inverse' ) ).toBe(
|
|
117
|
+
'label'
|
|
118
|
+
);
|
|
119
|
+
expect( pickLabelTextColorForFill( MID_BLUE, unreadableInverse, 'label' ) ).toBe( 'white' );
|
|
120
|
+
} );
|
|
121
|
+
|
|
122
|
+
it.each( [
|
|
123
|
+
[ 'readable', color( '#767676' ) ],
|
|
124
|
+
[ 'unreadable', unreadable( 'oklch(55% 0 0)' ) ],
|
|
125
|
+
] )( 'never falls back when both roles are the same %s color', ( _name, role ) => {
|
|
126
|
+
const pinned: LabelRoles = { label: role, labelInverse: { ...role } };
|
|
127
|
+
|
|
128
|
+
expect( pickLabelTextColorForFill( hexToRgb( '#516dec' ), pinned, 'label' ) ).toBe( 'label' );
|
|
129
|
+
expect( pickLabelTextColorForFill( hexToRgb( '#516dec' ), pinned, 'label-inverse' ) ).toBe(
|
|
130
|
+
'label-inverse'
|
|
131
|
+
);
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
describe.each( [
|
|
135
|
+
[ 'white', '#ffffff', '#f0f0f0' ],
|
|
136
|
+
[ 'dark', '#242424', '#272727' ],
|
|
137
|
+
] )( 'across a heatmap scale on a %s background', ( _name, background, emptyCell ) => {
|
|
138
|
+
const painted: Record< LabelTextColor, number > = {
|
|
139
|
+
label: rgbLuminance( hexToRgb( '#1e1e1e' ) ),
|
|
140
|
+
'label-inverse': rgbLuminance( hexToRgb( '#f0f0f0' ) ),
|
|
141
|
+
black: 0,
|
|
142
|
+
white: 1,
|
|
143
|
+
};
|
|
144
|
+
// The Storybook custom accent, then the wp-admin scheme colors.
|
|
145
|
+
const primaries = [
|
|
146
|
+
'#4a19ab',
|
|
147
|
+
'#2271b1',
|
|
148
|
+
'#007cba',
|
|
149
|
+
'#3858e9',
|
|
150
|
+
'#437aa8',
|
|
151
|
+
'#916745',
|
|
152
|
+
'#646c3e',
|
|
153
|
+
'#cf4339',
|
|
154
|
+
'#567958',
|
|
155
|
+
'#ad631e',
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
it.each( primaries )( 'reaches AA on every intensity of %s', primary => {
|
|
159
|
+
const { low, high } = getHeatmapScale( primary, background, emptyCell );
|
|
160
|
+
for ( let step = 0; step <= 200; step++ ) {
|
|
161
|
+
const fill = blendRgb( hexToRgb( high ), hexToRgb( low ), step / 200 );
|
|
162
|
+
const choice = pickLabelTextColorForFill( fill, DEFAULT_ROLES, 'label' );
|
|
163
|
+
// Checked on the 8-bit channels the browser paints as well as the exact mix.
|
|
164
|
+
const [ r, g, b ] = fill.map( Math.round );
|
|
165
|
+
const painted8Bit: Rgb = [ r, g, b ];
|
|
166
|
+
|
|
167
|
+
for ( const shown of [ fill, painted8Bit ] ) {
|
|
168
|
+
expect(
|
|
169
|
+
luminanceContrastRatio( rgbLuminance( shown ), painted[ choice ] )
|
|
170
|
+
).toBeGreaterThanOrEqual( MIN_LABEL_CONTRAST );
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} );
|
|
174
|
+
} );
|
|
175
|
+
} );
|
|
176
|
+
|
|
177
|
+
describe( 'pickLabelTextColor', () => {
|
|
178
|
+
it( 'reads the fill from any color syntax', () => {
|
|
179
|
+
expect( pickLabelTextColor( 'rgb(255, 255, 255)', DEFAULT_ROLES, 'label-inverse' ) ).toBe(
|
|
180
|
+
'label'
|
|
181
|
+
);
|
|
182
|
+
} );
|
|
183
|
+
|
|
184
|
+
it( 'keeps the default role for a fill it cannot read', () => {
|
|
185
|
+
expect( pickLabelTextColor( 'var(--not-set)', DEFAULT_ROLES, 'label-inverse' ) ).toBe(
|
|
186
|
+
'label-inverse'
|
|
187
|
+
);
|
|
188
|
+
} );
|
|
189
|
+
} );
|
|
@@ -3,6 +3,7 @@ import { useContext } from 'react';
|
|
|
3
3
|
import { ChartScopeContext } from '../../../../providers/chart-scope';
|
|
4
4
|
import { withResponsive } from '../index';
|
|
5
5
|
import type { BaseChartProps } from '../../../../types';
|
|
6
|
+
import type { JSX } from 'react';
|
|
6
7
|
|
|
7
8
|
// Mock the useParentSize hook
|
|
8
9
|
jest.mock( '@visx/responsive', () => ( {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useParentSize } from '@visx/responsive';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import { useCallback,
|
|
3
|
+
import { useCallback, useRef, useState } from 'react';
|
|
4
|
+
import { useIsomorphicLayoutEffect } from '../../../hooks';
|
|
4
5
|
import { ChartScopeContext } from '../../../providers/chart-scope';
|
|
5
6
|
import styles from './with-responsive.module.scss';
|
|
6
7
|
import type { BaseChartProps } from '../../../types';
|
|
@@ -33,10 +34,6 @@ export type ResponsiveConfig = {
|
|
|
33
34
|
resizeDebounceTime?: number;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
// useLayoutEffect on the client (so containment is resolved before paint, no flash),
|
|
37
|
-
// useEffect on the server to avoid React's SSR warning.
|
|
38
|
-
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
39
|
-
|
|
40
37
|
/**
|
|
41
38
|
* A higher-order component that provides responsive dimensions
|
|
42
39
|
* to the wrapped chart component using useParentSize from `@visx/responsive`.
|
|
@@ -1,35 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import styles from './base-tooltip.module.scss';
|
|
1
|
+
import { LabelValueContent } from './private/label-value-content';
|
|
2
|
+
import { TooltipBox } from './tooltip-box';
|
|
3
|
+
import type { TooltipData } from './types';
|
|
5
4
|
import type { CSSProperties, ComponentType, ReactNode } from 'react';
|
|
6
5
|
|
|
7
|
-
type TooltipData = {
|
|
8
|
-
label: string;
|
|
9
|
-
value: number;
|
|
10
|
-
valueDisplay?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
6
|
type TooltipComponentProps = {
|
|
14
7
|
data: TooltipData;
|
|
15
8
|
className?: string;
|
|
16
9
|
};
|
|
17
10
|
|
|
18
11
|
type TooltipCommonProps = {
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
16
|
style?: CSSProperties;
|
|
22
17
|
className?: string;
|
|
23
18
|
/**
|
|
24
19
|
* Whether to render the tooltip container div. When false, only renders the content.
|
|
25
|
-
*
|
|
20
|
+
* @deprecated Render the content directly; `TooltipBox` draws the box.
|
|
26
21
|
* @default true
|
|
27
22
|
*/
|
|
28
23
|
renderContainer?: boolean;
|
|
29
24
|
};
|
|
30
25
|
|
|
31
26
|
type DefaultDataTooltip = {
|
|
27
|
+
/** @deprecated Pass the content as children, or use `TooltipBox`. */
|
|
32
28
|
data: TooltipData;
|
|
29
|
+
/** @deprecated Pass the content as children, or use `TooltipBox`. */
|
|
33
30
|
component?: ComponentType< TooltipComponentProps >;
|
|
34
31
|
children?: never;
|
|
35
32
|
};
|
|
@@ -42,38 +39,42 @@ type CustomTooltip = {
|
|
|
42
39
|
|
|
43
40
|
type BaseTooltipProps = TooltipCommonProps & ( DefaultDataTooltip | CustomTooltip );
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
*/
|
|
51
56
|
export const BaseTooltip = ( {
|
|
52
57
|
data,
|
|
53
58
|
top,
|
|
54
59
|
left,
|
|
55
|
-
component: Component =
|
|
60
|
+
component: Component = LabelValueContent,
|
|
56
61
|
children,
|
|
57
62
|
className,
|
|
58
63
|
style,
|
|
59
64
|
renderContainer = true,
|
|
60
65
|
}: BaseTooltipProps ) => {
|
|
61
66
|
const content = children || ( data && <Component data={ data } className={ className } /> );
|
|
62
|
-
const standaloneScopeClass = useStandaloneScopeClass();
|
|
63
67
|
|
|
64
68
|
if ( ! renderContainer ) {
|
|
65
69
|
return content;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{ content }
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
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>;
|
|
77
78
|
};
|
|
78
79
|
|
|
79
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,15 +1,19 @@
|
|
|
1
|
-
import { Tooltip, defaultStyles } from '@visx/tooltip';
|
|
2
1
|
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { TooltipBox } from '../tooltip-box';
|
|
3
3
|
import type { TooltipPlacement } from '../../../visx/types';
|
|
4
|
-
import type {
|
|
4
|
+
import type { TooltipBoxProps } from '../tooltip-box';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
5
6
|
|
|
6
7
|
type Bounds = { left: number; top: number; right: number; bottom: number };
|
|
7
8
|
type Size = { width: number; height: number };
|
|
8
9
|
|
|
9
|
-
export type BoundedTooltipProps = Omit<
|
|
10
|
+
export type BoundedTooltipProps = Omit< TooltipBoxProps, 'children' > & {
|
|
11
|
+
children?: ReactNode;
|
|
10
12
|
/** Anchor, in the coordinates of the positioned wrapper the box renders into. */
|
|
11
13
|
left?: number;
|
|
12
14
|
top?: number;
|
|
15
|
+
offsetLeft?: number;
|
|
16
|
+
offsetTop?: number;
|
|
13
17
|
placement?: TooltipPlacement;
|
|
14
18
|
};
|
|
15
19
|
|
|
@@ -27,6 +31,15 @@ const isClipping = ( element: Element ) => {
|
|
|
27
31
|
return [ overflow, overflowX, overflowY ].some( value => value && value !== 'visible' );
|
|
28
32
|
};
|
|
29
33
|
|
|
34
|
+
// The positioned wrapper the box renders into, past the tooltip theme's `display: contents` element, which has no box to measure.
|
|
35
|
+
const findLayoutParent = ( node: Element ): Element | null => {
|
|
36
|
+
let element = node.parentElement;
|
|
37
|
+
while ( element && getComputedStyle( element ).display === 'contents' ) {
|
|
38
|
+
element = element.parentElement;
|
|
39
|
+
}
|
|
40
|
+
return element;
|
|
41
|
+
};
|
|
42
|
+
|
|
30
43
|
// The first ancestor that cuts its overflow off. The box may leave the chart
|
|
31
44
|
// wrapper but never this element, which is what a body-level portal used to
|
|
32
45
|
// guarantee by never being inside one.
|
|
@@ -122,7 +135,7 @@ export const getBoundedPosition = ( {
|
|
|
122
135
|
};
|
|
123
136
|
|
|
124
137
|
/**
|
|
125
|
-
*
|
|
138
|
+
* A `TooltipBox`, positioned like visx's `TooltipWithBounds` but kept inside the
|
|
126
139
|
* nearest clipping ancestor — or the viewport when there is none — rather than
|
|
127
140
|
* inside its own parent. Rendered in-tree, a tooltip's parent is the chart
|
|
128
141
|
* wrapper, which is often narrower than the box; measuring against the parent
|
|
@@ -131,13 +144,14 @@ export const getBoundedPosition = ( {
|
|
|
131
144
|
* Re-measures on every render, so a box whose content changes width between
|
|
132
145
|
* two hovers is placed for its current size.
|
|
133
146
|
*
|
|
134
|
-
* @param props -
|
|
147
|
+
* @param props - `TooltipBox` props plus placement.
|
|
135
148
|
* @param props.left - Anchor x, in wrapper coordinates.
|
|
136
149
|
* @param props.top - Anchor y, in wrapper coordinates.
|
|
137
150
|
* @param props.offsetLeft - Gap between the anchor and the box, horizontally.
|
|
138
151
|
* @param props.offsetTop - Gap between the anchor and the box, vertically.
|
|
139
|
-
* @param props.style -
|
|
140
|
-
* @param props.unstyled -
|
|
152
|
+
* @param props.style - Inline overrides on the surface; ignored when `unstyled`.
|
|
153
|
+
* @param props.unstyled - Drop the surface and `style`, leaving the box bare.
|
|
154
|
+
* @param props.className - Extra classes beside the surface.
|
|
141
155
|
* @param props.children - Box content.
|
|
142
156
|
* @param props.placement - Below-axis, beside without vertical flipping, or automatic flipping.
|
|
143
157
|
* @return The tooltip box.
|
|
@@ -147,8 +161,9 @@ export const BoundedTooltip = ( {
|
|
|
147
161
|
top = 0,
|
|
148
162
|
offsetLeft = DEFAULT_OFFSET,
|
|
149
163
|
offsetTop = DEFAULT_OFFSET,
|
|
150
|
-
style
|
|
164
|
+
style,
|
|
151
165
|
unstyled = false,
|
|
166
|
+
className,
|
|
152
167
|
children,
|
|
153
168
|
placement = 'auto',
|
|
154
169
|
...rest
|
|
@@ -159,7 +174,7 @@ export const BoundedTooltip = ( {
|
|
|
159
174
|
|
|
160
175
|
useLayoutEffect( () => {
|
|
161
176
|
const node = nodeRef.current;
|
|
162
|
-
const wrapper = node
|
|
177
|
+
const wrapper = node && findLayoutParent( node );
|
|
163
178
|
if ( ! node || ! wrapper ) {
|
|
164
179
|
return;
|
|
165
180
|
}
|
|
@@ -199,9 +214,12 @@ export const BoundedTooltip = ( {
|
|
|
199
214
|
const y = position?.y ?? top + ( placement === 'below-axis' ? POINTER_HEIGHT : offsetTop );
|
|
200
215
|
|
|
201
216
|
return (
|
|
202
|
-
<
|
|
217
|
+
<TooltipBox
|
|
203
218
|
ref={ nodeRef }
|
|
219
|
+
role="presentation"
|
|
204
220
|
data-testid="bounded-tooltip"
|
|
221
|
+
unstyled={ unstyled }
|
|
222
|
+
className={ className }
|
|
205
223
|
style={ {
|
|
206
224
|
position: 'absolute',
|
|
207
225
|
left: 0,
|
|
@@ -220,14 +238,15 @@ export const BoundedTooltip = ( {
|
|
|
220
238
|
left: left - x - POINTER_HEIGHT,
|
|
221
239
|
top: -POINTER_HEIGHT,
|
|
222
240
|
width: POINTER_HEIGHT * 2,
|
|
223
|
-
|
|
241
|
+
// One extra pixel overlaps the box, so anti-aliasing leaves no seam between them.
|
|
242
|
+
height: POINTER_HEIGHT + 1,
|
|
224
243
|
background: 'inherit',
|
|
225
|
-
clipPath:
|
|
244
|
+
clipPath: `polygon(50% 0, 100% ${ POINTER_HEIGHT }px, 100% 100%, 0 100%, 0 ${ POINTER_HEIGHT }px)`,
|
|
226
245
|
pointerEvents: 'none',
|
|
227
246
|
} }
|
|
228
247
|
/>
|
|
229
248
|
) }
|
|
230
249
|
{ children }
|
|
231
|
-
</
|
|
250
|
+
</TooltipBox>
|
|
232
251
|
);
|
|
233
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
|
+
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { color as d3Color } from '@visx/vendor/d3-color';
|
|
2
|
+
import { ThemeProvider } from '@wordpress/theme';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useChartScopeElement } from '../../../providers/chart-scope';
|
|
5
|
+
import { resolveCssVariable } from '../../../utils/resolve-css-var';
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_SURFACE = '#1e1e1e';
|
|
9
|
+
const TOOLTIP_SURFACE = `var(--a8c-charts-color-tooltip-surface, ${ DEFAULT_SURFACE })`;
|
|
10
|
+
|
|
11
|
+
// `ThemeProvider` throws on a seed that is not an opaque sRGB color, which would unmount the chart.
|
|
12
|
+
const toSeed = ( value: string | null ) => {
|
|
13
|
+
const parsed = value ? d3Color( value ) : null;
|
|
14
|
+
return parsed && parsed.opacity === 1 ? parsed.formatHex() : DEFAULT_SURFACE;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Themes the tooltip box for its dark surface, as `@wordpress/ui`'s Tooltip themes its popup. The box re-declares the catalog under this theme, so every role it reads is tuned for the surface, whatever theme the chart is in.
|
|
19
|
+
*
|
|
20
|
+
* @param props - Component props.
|
|
21
|
+
* @param props.children - The tooltip box.
|
|
22
|
+
* @return The themed box.
|
|
23
|
+
*/
|
|
24
|
+
export const TooltipTheme = ( { children }: { children: ReactNode } ) => {
|
|
25
|
+
const scopeElement = useChartScopeElement();
|
|
26
|
+
const color = useMemo(
|
|
27
|
+
() => ( { background: toSeed( resolveCssVariable( TOOLTIP_SURFACE, scopeElement ) ) } ),
|
|
28
|
+
[ scopeElement ]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Core's `wp-theme` in WordPress 7.0 exports only `privateApis`.
|
|
32
|
+
if ( ! ThemeProvider ) {
|
|
33
|
+
return <>{ children }</>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return <ThemeProvider color={ color }>{ children }</ThemeProvider>;
|
|
37
|
+
};
|