@automattic/charts 4.6.0 → 4.7.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 +16 -0
- package/TOKENS.md +12 -10
- package/dist/index.cjs +244 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -20
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +245 -146
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +0 -10
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +4 -2
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +11 -1
- package/src/charts/conversion-funnel-chart/types.ts +3 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -3
- package/src/charts/heatmap-chart/heatmap-chart.tsx +54 -37
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +129 -25
- package/src/charts/heatmap-chart/types.ts +2 -6
- 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 +8 -40
- 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 +22 -76
- package/src/charts/pie-chart/test/pie-chart.test.tsx +21 -8
- 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 +191 -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/accessible-tooltip.tsx +1 -1
- package/src/components/tooltip/base-tooltip.module.scss +12 -8
- package/src/components/tooltip/base-tooltip.tsx +11 -9
- package/src/components/tooltip/private/bounded-tooltip.tsx +27 -8
- package/src/components/tooltip/private/tooltip-theme.tsx +37 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +82 -1
- package/src/components/tooltip/test/tool-tip.test.tsx +5 -0
- package/src/components/tooltip/test/tooltip-theme.test.tsx +61 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +41 -28
- package/src/components/tooltip/xy-chart-tooltip.tsx +29 -29
- package/src/components/trend-indicator/trend-indicator.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/test/use-xychart-theme.test.tsx +3 -3
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-xychart-theme.ts +4 -12
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +2 -4
- package/src/providers/chart-context/private/perceptual-color.ts +45 -4
- package/src/providers/chart-context/test/perceptual-color.test.ts +31 -0
- package/src/styles/chart-scope-class.ts +5 -0
- package/src/styles/chart-scope.scss +9 -6
- package/src/styles/test/chart-scope.test.ts +3 -3
- package/src/utils/color-utils.ts +2 -1
- package/src/visx/types.ts +1 -1
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +0 -31
|
@@ -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`.
|
|
@@ -19,7 +19,7 @@ export type FlattenedTooltipData = {
|
|
|
19
19
|
dataPointIndex: number;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// The XY chart tooltip, as LineChart, AreaChart and BarChart render it: place it inside a visx `XYChart`, with `useKeyboardNavigation` for keyboard access.
|
|
23
23
|
interface AccessibleTooltipProps extends Omit<
|
|
24
24
|
XyChartTooltipProps< DataPointDate >,
|
|
25
25
|
'renderTooltip'
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
// `.surface` is the look alone, for a chart that positions the box itself.
|
|
2
|
+
// The box carries the chart scope class inside TooltipTheme, so these roles
|
|
3
|
+
// resolve for the dark tooltip theme, as in @wordpress/ui's Tooltip popup.
|
|
2
4
|
.surface,
|
|
3
5
|
.tooltip {
|
|
4
|
-
padding: var(--wpds-dimension-padding-sm);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// white-on-dark content foreground token. The role carries the
|
|
8
|
-
// hardcoded translucent value as its fallback.
|
|
9
|
-
background-color: var(--a8c-charts-color-tooltip-surface, rgba(0, 0, 0, 0.85));
|
|
10
|
-
color: #fff;
|
|
6
|
+
padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm);
|
|
7
|
+
background-color: var(--a8c-charts-color-surface);
|
|
8
|
+
color: var(--a8c-charts-color-label);
|
|
11
9
|
border-radius: var(--wpds-border-radius-md);
|
|
12
|
-
font-
|
|
10
|
+
font-family: var(--wpds-typography-font-family-body);
|
|
11
|
+
font-size: var(--wpds-typography-font-size-sm);
|
|
12
|
+
line-height: 1.4;
|
|
13
13
|
box-shadow: var(--a8c-charts-elevation-sm, 0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005);
|
|
14
14
|
pointer-events: none;
|
|
15
|
+
|
|
16
|
+
@media (forced-colors: active) {
|
|
17
|
+
border: var(--wpds-border-width-xs) solid CanvasText;
|
|
18
|
+
}
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
.tooltip {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { formatNumber } from '@automattic/number-formatters';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import {
|
|
3
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
4
4
|
import styles from './base-tooltip.module.scss';
|
|
5
|
+
import { TooltipTheme } from './private/tooltip-theme';
|
|
5
6
|
import type { CSSProperties, ComponentType, ReactNode } from 'react';
|
|
6
7
|
|
|
7
8
|
type TooltipData = {
|
|
@@ -59,20 +60,21 @@ export const BaseTooltip = ( {
|
|
|
59
60
|
renderContainer = true,
|
|
60
61
|
}: BaseTooltipProps ) => {
|
|
61
62
|
const content = children || ( data && <Component data={ data } className={ className } /> );
|
|
62
|
-
const standaloneScopeClass = useStandaloneScopeClass();
|
|
63
63
|
|
|
64
64
|
if ( ! renderContainer ) {
|
|
65
65
|
return content;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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>
|
|
76
78
|
);
|
|
77
79
|
};
|
|
78
80
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { Tooltip
|
|
1
|
+
import { Tooltip } from '@visx/tooltip';
|
|
2
|
+
import clsx from 'clsx';
|
|
2
3
|
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';
|
|
3
7
|
import type { TooltipPlacement } from '../../../visx/types';
|
|
4
8
|
import type { TooltipProps } from '@visx/tooltip';
|
|
5
9
|
|
|
@@ -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.
|
|
@@ -136,8 +149,9 @@ export const getBoundedPosition = ( {
|
|
|
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
|
}
|
|
@@ -198,10 +213,11 @@ export const BoundedTooltip = ( {
|
|
|
198
213
|
const x = position?.x ?? left + offsetLeft;
|
|
199
214
|
const y = position?.y ?? top + ( placement === 'below-axis' ? POINTER_HEIGHT : offsetTop );
|
|
200
215
|
|
|
201
|
-
|
|
216
|
+
const box = (
|
|
202
217
|
<Tooltip
|
|
203
218
|
ref={ nodeRef }
|
|
204
219
|
data-testid="bounded-tooltip"
|
|
220
|
+
className={ clsx( ! unstyled && [ TOOLTIP_SCOPE_CLASS, styles.surface ], className ) }
|
|
205
221
|
style={ {
|
|
206
222
|
position: 'absolute',
|
|
207
223
|
left: 0,
|
|
@@ -220,9 +236,10 @@ export const BoundedTooltip = ( {
|
|
|
220
236
|
left: left - x - POINTER_HEIGHT,
|
|
221
237
|
top: -POINTER_HEIGHT,
|
|
222
238
|
width: POINTER_HEIGHT * 2,
|
|
223
|
-
|
|
239
|
+
// One extra pixel overlaps the box, so anti-aliasing leaves no seam between them.
|
|
240
|
+
height: POINTER_HEIGHT + 1,
|
|
224
241
|
background: 'inherit',
|
|
225
|
-
clipPath:
|
|
242
|
+
clipPath: `polygon(50% 0, 100% ${ POINTER_HEIGHT }px, 100% 100%, 0 100%, 0 ${ POINTER_HEIGHT }px)`,
|
|
226
243
|
pointerEvents: 'none',
|
|
227
244
|
} }
|
|
228
245
|
/>
|
|
@@ -230,4 +247,6 @@ export const BoundedTooltip = ( {
|
|
|
230
247
|
{ children }
|
|
231
248
|
</Tooltip>
|
|
232
249
|
);
|
|
250
|
+
|
|
251
|
+
return unstyled ? box : <TooltipTheme>{ box }</TooltipTheme>;
|
|
233
252
|
};
|
|
@@ -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
|
+
};
|
|
@@ -126,6 +126,87 @@ describe( 'BoundedTooltip', () => {
|
|
|
126
126
|
jest.restoreAllMocks();
|
|
127
127
|
} );
|
|
128
128
|
|
|
129
|
+
test( 'draws the package surface, not the visx default box', () => {
|
|
130
|
+
render(
|
|
131
|
+
<BoundedTooltip left={ 10 } top={ 10 } data-testid="box">
|
|
132
|
+
content
|
|
133
|
+
</BoundedTooltip>
|
|
134
|
+
);
|
|
135
|
+
const box = screen.getByTestId( 'box' );
|
|
136
|
+
expect( box ).toHaveClass( 'surface' );
|
|
137
|
+
expect( box ).not.toHaveStyle( { backgroundColor: 'white' } );
|
|
138
|
+
// toHaveStyle can't assert an absent inline style; box.style reads the empty string directly.
|
|
139
|
+
// eslint-disable-next-line jest-dom/prefer-to-have-style
|
|
140
|
+
expect( box.style.boxShadow ).toBe( '' );
|
|
141
|
+
// eslint-disable-next-line jest-dom/prefer-to-have-style
|
|
142
|
+
expect( box.style.padding ).toBe( '' );
|
|
143
|
+
} );
|
|
144
|
+
|
|
145
|
+
test( 'keeps a caller class beside the surface', () => {
|
|
146
|
+
render(
|
|
147
|
+
<BoundedTooltip left={ 10 } top={ 10 } className="extra" data-testid="box">
|
|
148
|
+
content
|
|
149
|
+
</BoundedTooltip>
|
|
150
|
+
);
|
|
151
|
+
expect( screen.getByTestId( 'box' ) ).toHaveClass( 'surface', 'extra' );
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
test( 'drops the surface when unstyled', () => {
|
|
155
|
+
render(
|
|
156
|
+
<BoundedTooltip left={ 10 } top={ 10 } unstyled data-testid="box">
|
|
157
|
+
content
|
|
158
|
+
</BoundedTooltip>
|
|
159
|
+
);
|
|
160
|
+
expect( screen.getByTestId( 'box' ) ).not.toHaveClass( 'surface' );
|
|
161
|
+
expect( screen.getByTestId( 'box' ) ).not.toHaveClass( 'a8c-charts-tooltip-scope' );
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
test( 're-declares the chart catalog on a styled box under its own class, out of reach of page-wide chart overrides', () => {
|
|
165
|
+
render(
|
|
166
|
+
<BoundedTooltip left={ 10 } top={ 10 } data-testid="box">
|
|
167
|
+
content
|
|
168
|
+
</BoundedTooltip>
|
|
169
|
+
);
|
|
170
|
+
const box = screen.getByTestId( 'box' );
|
|
171
|
+
expect( box ).toHaveClass( 'a8c-charts-tooltip-scope', 'surface' );
|
|
172
|
+
expect( box ).not.toHaveClass( 'a8c-charts-scope' );
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
test( 'measures against the positioned wrapper past a display: contents element', () => {
|
|
176
|
+
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
177
|
+
this: Element
|
|
178
|
+
) {
|
|
179
|
+
if ( this.classList.contains( 'visx-tooltip' ) ) {
|
|
180
|
+
return rect( 0, 0, 208, 36 );
|
|
181
|
+
}
|
|
182
|
+
return this.getAttribute( 'data-testid' ) === 'contents'
|
|
183
|
+
? rect( 0, 0, 0, 0 )
|
|
184
|
+
: rect( 0, 0, 600, 200 );
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
render(
|
|
188
|
+
<div style={ { position: 'relative', overflow: 'hidden' } }>
|
|
189
|
+
<div data-testid="contents" style={ { display: 'contents' } }>
|
|
190
|
+
<BoundedTooltip left={ 300 } top={ 50 } unstyled data-testid="box">
|
|
191
|
+
content
|
|
192
|
+
</BoundedTooltip>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Measured against the zero-size contents element, the box would flip to (82, 4).
|
|
198
|
+
expect( screen.getByTestId( 'box' ) ).toHaveStyle( { transform: 'translate(310px, 60px)' } );
|
|
199
|
+
} );
|
|
200
|
+
|
|
201
|
+
test( 'lets an inline style override the surface', () => {
|
|
202
|
+
render(
|
|
203
|
+
<BoundedTooltip left={ 10 } top={ 10 } style={ { background: 'red' } } data-testid="box">
|
|
204
|
+
content
|
|
205
|
+
</BoundedTooltip>
|
|
206
|
+
);
|
|
207
|
+
expect( screen.getByTestId( 'box' ) ).toHaveStyle( { background: 'red' } );
|
|
208
|
+
} );
|
|
209
|
+
|
|
129
210
|
test( 'keeps the box inside an overflow-hidden ancestor that is wider than the wrapper', () => {
|
|
130
211
|
// Card at x=400 (400 wide, overflow hidden); plot wrapper at x=440, 346 wide.
|
|
131
212
|
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
@@ -209,7 +290,7 @@ describe( 'BoundedTooltip', () => {
|
|
|
209
290
|
left: `${ pointerLeft }px`,
|
|
210
291
|
top: '-6px',
|
|
211
292
|
width: '12px',
|
|
212
|
-
height: '
|
|
293
|
+
height: '7px',
|
|
213
294
|
} );
|
|
214
295
|
expect( screen.getByTestId( 'tooltip-axis-pointer' ) ).toHaveAttribute(
|
|
215
296
|
'aria-hidden',
|
|
@@ -36,6 +36,11 @@ describe( 'BaseTooltip', () => {
|
|
|
36
36
|
} );
|
|
37
37
|
} );
|
|
38
38
|
|
|
39
|
+
test( 'renders the standalone container on the tooltip chrome', () => {
|
|
40
|
+
render( <BaseTooltip { ...defaultProps } /> );
|
|
41
|
+
expect( screen.getByRole( 'tooltip' ) ).toHaveClass( 'tooltip' );
|
|
42
|
+
} );
|
|
43
|
+
|
|
39
44
|
test( 'handles missing valueDisplay', () => {
|
|
40
45
|
const propsWithoutDisplay = {
|
|
41
46
|
...defaultProps,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { resolveCssVariable } from '../../../utils/resolve-css-var';
|
|
3
|
+
import { TooltipTheme } from '../private/tooltip-theme';
|
|
4
|
+
|
|
5
|
+
jest.mock( '@wordpress/theme', () => ( { ...jest.requireActual( '@wordpress/theme' ) } ) );
|
|
6
|
+
jest.mock( '../../../utils/resolve-css-var', () => ( { resolveCssVariable: jest.fn() } ) );
|
|
7
|
+
|
|
8
|
+
const theme = jest.requireMock( '@wordpress/theme' );
|
|
9
|
+
const { ThemeProvider } = jest.requireActual( '@wordpress/theme' );
|
|
10
|
+
|
|
11
|
+
const spyOnThemeProvider = () =>
|
|
12
|
+
jest.spyOn( theme, 'ThemeProvider' ).mockImplementation( ( { children } ) => children );
|
|
13
|
+
|
|
14
|
+
describe( 'TooltipTheme', () => {
|
|
15
|
+
afterEach( () => {
|
|
16
|
+
jest.restoreAllMocks();
|
|
17
|
+
theme.ThemeProvider = ThemeProvider;
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
test( 'renders the box unthemed when core exports no public ThemeProvider', () => {
|
|
21
|
+
theme.ThemeProvider = undefined;
|
|
22
|
+
jest.mocked( resolveCssVariable ).mockReturnValue( '#1e1e1e' );
|
|
23
|
+
|
|
24
|
+
render( <TooltipTheme>box</TooltipTheme> );
|
|
25
|
+
|
|
26
|
+
expect( screen.getByText( 'box' ) ).toBeInTheDocument();
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
test.each( [ 'rgba(0, 0, 0, 0.85)', 'not-a-color', null ] )(
|
|
30
|
+
'seeds the default surface when the surface role resolves to %p',
|
|
31
|
+
value => {
|
|
32
|
+
const provider = spyOnThemeProvider();
|
|
33
|
+
jest.mocked( resolveCssVariable ).mockReturnValue( value );
|
|
34
|
+
|
|
35
|
+
render( <TooltipTheme>box</TooltipTheme> );
|
|
36
|
+
|
|
37
|
+
expect( provider.mock.calls[ 0 ][ 0 ] ).toMatchObject( {
|
|
38
|
+
color: { background: '#1e1e1e' },
|
|
39
|
+
} );
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
test( 'seeds an opaque surface role as hex', () => {
|
|
44
|
+
const provider = spyOnThemeProvider();
|
|
45
|
+
jest.mocked( resolveCssVariable ).mockReturnValue( 'rgb(255, 255, 255)' );
|
|
46
|
+
|
|
47
|
+
render( <TooltipTheme>box</TooltipTheme> );
|
|
48
|
+
|
|
49
|
+
expect( provider.mock.calls[ 0 ][ 0 ] ).toMatchObject( {
|
|
50
|
+
color: { background: '#ffffff' },
|
|
51
|
+
} );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
test( 'renders with the real ThemeProvider when the surface role is translucent', () => {
|
|
55
|
+
jest.mocked( resolveCssVariable ).mockReturnValue( 'rgba(0, 0, 0, 0.85)' );
|
|
56
|
+
|
|
57
|
+
render( <TooltipTheme>box</TooltipTheme> );
|
|
58
|
+
|
|
59
|
+
expect( screen.getByText( 'box' ) ).toBeInTheDocument();
|
|
60
|
+
} );
|
|
61
|
+
} );
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
-
import { BarSeries,
|
|
2
|
+
import { BarSeries, LineSeries, TooltipContext, XYChart } from '@visx/xychart';
|
|
3
3
|
import { useContext, useEffect } from 'react';
|
|
4
4
|
import { XyChartTooltip } from '../xy-chart-tooltip';
|
|
5
5
|
import type { XyChartTooltipProps } from '../../../visx/types';
|
|
@@ -103,20 +103,35 @@ const renderChart = (
|
|
|
103
103
|
|
|
104
104
|
describe( 'XyChartTooltip', () => {
|
|
105
105
|
test.each( [
|
|
106
|
-
[ '
|
|
107
|
-
[ '
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
[ 'bounded', {} ],
|
|
107
|
+
[ 'unbounded', { detectBounds: false } ],
|
|
108
|
+
[ 'below-axis', { tooltipPlacement: 'below-axis' as const } ],
|
|
109
|
+
] )( 'draws the %s box on the package surface, with no visx box styles', async ( _, props ) => {
|
|
110
|
+
renderChart( props );
|
|
111
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
112
|
+
expect( box ).toHaveClass( 'surface' );
|
|
113
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'box-shadow' ) );
|
|
114
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'background-color' ) );
|
|
115
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'padding' ) );
|
|
116
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'border-radius' ) );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
test( 'strips the surface from an unbounded box when unstyled', async () => {
|
|
120
|
+
renderChart( { detectBounds: false, unstyled: true } );
|
|
121
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
122
|
+
expect( box ).not.toHaveClass( 'surface' );
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
test( 'strips the surface from a bounded box when unstyled', async () => {
|
|
126
|
+
renderChart( { unstyled: true } );
|
|
127
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
128
|
+
expect( box ).not.toHaveClass( 'surface' );
|
|
129
|
+
} );
|
|
118
130
|
|
|
119
|
-
|
|
131
|
+
test( 'keeps a caller class beside the surface on an unbounded box', async () => {
|
|
132
|
+
renderChart( { detectBounds: false, className: 'extra' } );
|
|
133
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
134
|
+
expect( box ).toHaveClass( 'surface', 'extra' );
|
|
120
135
|
} );
|
|
121
136
|
|
|
122
137
|
test( 'renders the tooltip box inside the chart wrapper, not in a body-level portal', async () => {
|
|
@@ -351,25 +366,23 @@ describe( 'XyChartTooltip', () => {
|
|
|
351
366
|
} );
|
|
352
367
|
|
|
353
368
|
test.each( [ 'auto', 'below-axis' ] as const )(
|
|
354
|
-
'
|
|
369
|
+
'keeps the %s surface and stacking under a partial style override',
|
|
355
370
|
async tooltipPlacement => {
|
|
356
|
-
const { unmount } = renderChart( { tooltipPlacement } );
|
|
357
|
-
const defaultBox = await screen.findByTestId( 'tooltip-box' );
|
|
358
|
-
const background = defaultBox.style.backgroundColor;
|
|
359
|
-
expect( background ).not.toBe( '' );
|
|
360
|
-
expect( background ).not.toBe( 'transparent' );
|
|
361
|
-
unmount();
|
|
362
|
-
|
|
363
371
|
renderChart( { tooltipPlacement, style: { color: 'red' } } );
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
zIndex: '3',
|
|
368
|
-
color: 'rgb(255, 0, 0)',
|
|
369
|
-
} );
|
|
372
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
373
|
+
expect( box ).toHaveClass( 'surface' );
|
|
374
|
+
expect( box ).toHaveStyle( { zIndex: '3', color: 'rgb(255, 0, 0)' } );
|
|
370
375
|
}
|
|
371
376
|
);
|
|
372
377
|
|
|
378
|
+
test( 'lets a style override paint the box', async () => {
|
|
379
|
+
renderChart( { style: { background: 'rgb(0, 0, 0)', color: 'rgb(255, 255, 255)' } } );
|
|
380
|
+
await expect( screen.findByTestId( 'tooltip-box' ) ).resolves.toHaveStyle( {
|
|
381
|
+
background: 'rgb(0, 0, 0)',
|
|
382
|
+
color: 'rgb(255, 255, 255)',
|
|
383
|
+
} );
|
|
384
|
+
} );
|
|
385
|
+
|
|
373
386
|
test( 'accepts the portal-era options without passing them to the box', async () => {
|
|
374
387
|
renderChart( { scroll: true, debounce: 50, resizeObserverPolyfill: undefined } );
|
|
375
388
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { Tooltip
|
|
1
|
+
import { Tooltip } from '@visx/tooltip';
|
|
2
2
|
import { DataContext, TooltipContext } from '@visx/xychart';
|
|
3
|
+
import clsx from 'clsx';
|
|
3
4
|
import { useCallback, useContext, useState } from 'react';
|
|
4
5
|
import { createPortal } from 'react-dom';
|
|
5
|
-
import {
|
|
6
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
7
|
+
import styles from './base-tooltip.module.scss';
|
|
6
8
|
import { BoundedTooltip, TOOLTIP_Z_INDEX } from './private/bounded-tooltip';
|
|
9
|
+
import { TooltipTheme } from './private/tooltip-theme';
|
|
7
10
|
import type {
|
|
8
11
|
CrosshairStyle,
|
|
9
12
|
RenderTooltipGlyphProps,
|
|
@@ -215,16 +218,29 @@ const XyChartTooltipContent = < Datum extends object >( {
|
|
|
215
218
|
const marginLeft = margin?.left ?? 0;
|
|
216
219
|
|
|
217
220
|
const TooltipComponent = detectBounds || tooltipPlacement !== 'auto' ? BoundedTooltip : Tooltip;
|
|
218
|
-
const boxStyle: CSSProperties = {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
const boxStyle: CSSProperties = { zIndex, ...style };
|
|
222
|
+
// BoundedTooltip themes its own box.
|
|
223
|
+
const themesOwnBox = TooltipComponent === Tooltip && ! tooltipProps.unstyled;
|
|
224
|
+
const box = (
|
|
225
|
+
<TooltipComponent
|
|
226
|
+
left={ tooltipLeft }
|
|
227
|
+
top={
|
|
228
|
+
tooltipPlacement === 'below-axis'
|
|
229
|
+
? marginTop + innerHeight + ( margin?.bottom ?? 0 )
|
|
230
|
+
: ( tooltipAnchorTop ?? tooltipTop )
|
|
231
|
+
}
|
|
232
|
+
style={ boxStyle }
|
|
233
|
+
applyPositionStyle
|
|
234
|
+
{ ...tooltipProps }
|
|
235
|
+
className={ clsx(
|
|
236
|
+
themesOwnBox && [ CHART_SCOPE_CLASS, styles.surface ],
|
|
237
|
+
tooltipProps.className
|
|
238
|
+
) }
|
|
239
|
+
{ ...( tooltipPlacement !== 'auto' && { placement: tooltipPlacement } ) }
|
|
240
|
+
>
|
|
241
|
+
{ tooltipContent }
|
|
242
|
+
</TooltipComponent>
|
|
243
|
+
);
|
|
228
244
|
|
|
229
245
|
return (
|
|
230
246
|
<>
|
|
@@ -258,23 +274,7 @@ const XyChartTooltipContent = < Datum extends object >( {
|
|
|
258
274
|
{ glyphs }
|
|
259
275
|
</g>
|
|
260
276
|
{ container &&
|
|
261
|
-
createPortal(
|
|
262
|
-
<TooltipComponent
|
|
263
|
-
left={ tooltipLeft }
|
|
264
|
-
top={
|
|
265
|
-
tooltipPlacement === 'below-axis'
|
|
266
|
-
? marginTop + innerHeight + ( margin?.bottom ?? 0 )
|
|
267
|
-
: ( tooltipAnchorTop ?? tooltipTop )
|
|
268
|
-
}
|
|
269
|
-
style={ boxStyle }
|
|
270
|
-
applyPositionStyle
|
|
271
|
-
{ ...tooltipProps }
|
|
272
|
-
{ ...( tooltipPlacement !== 'auto' && { placement: tooltipPlacement } ) }
|
|
273
|
-
>
|
|
274
|
-
{ tooltipContent }
|
|
275
|
-
</TooltipComponent>,
|
|
276
|
-
container
|
|
277
|
-
) }
|
|
277
|
+
createPortal( themesOwnBox ? <TooltipTheme>{ box }</TooltipTheme> : box, container ) }
|
|
278
278
|
</>
|
|
279
279
|
);
|
|
280
280
|
};
|
|
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|
|
2
2
|
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
3
3
|
import styles from './trend-indicator.module.scss';
|
|
4
4
|
import type { TrendIndicatorProps, TrendDirection } from './types';
|
|
5
|
+
import type { JSX } from 'react';
|
|
5
6
|
|
|
6
7
|
const DIRECTION_LABELS: Record< TrendDirection, string > = {
|
|
7
8
|
up: 'Increase',
|
package/src/hooks/index.ts
CHANGED
|
@@ -9,3 +9,4 @@ export { useZeroValueDisplay } from './use-zero-value-display';
|
|
|
9
9
|
export { useDataWithPercentages } from './use-data-with-percentages';
|
|
10
10
|
export { useLegendVisibilityData } from './use-legend-visibility-data';
|
|
11
11
|
export { usePrefersReducedMotion } from './use-prefers-reduced-motion';
|
|
12
|
+
export { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
2
3
|
import { ChartScopeContext } from '../../providers/chart-scope';
|
|
3
4
|
import { useXYChartTheme } from '../use-xychart-theme';
|
|
4
5
|
import type { ReactNode } from 'react';
|
|
@@ -48,8 +49,7 @@ describe( 'useXYChartTheme', () => {
|
|
|
48
49
|
document.body.removeChild( scope );
|
|
49
50
|
} );
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
it( 'resolves the tooltip label color while the tick labels keep the pointer', () => {
|
|
52
|
+
it( 'hands visx the label pointer for the HTML label, even with an override in scope', () => {
|
|
53
53
|
const scope = document.createElement( 'div' );
|
|
54
54
|
scope.style.setProperty( '--a8c-charts-color-label-axis', '#0000ff' );
|
|
55
55
|
document.body.appendChild( scope );
|
|
@@ -60,7 +60,7 @@ describe( 'useXYChartTheme', () => {
|
|
|
60
60
|
|
|
61
61
|
const { result } = renderHook( () => useXYChartTheme( [] ), { wrapper } );
|
|
62
62
|
|
|
63
|
-
expect( result.current.htmlLabel.color ).toBe(
|
|
63
|
+
expect( result.current.htmlLabel.color ).toBe( CATALOG_POINTERS.labelAxis );
|
|
64
64
|
expect( result.current.svgLabelSmall.fill ).toBe(
|
|
65
65
|
'var(--a8c-charts-color-label-axis, #1e1e1e)'
|
|
66
66
|
);
|