@automattic/charts 4.1.0 → 4.1.1
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 +9 -0
- package/TOKENS.md +6 -4
- package/dist/index.cjs +241 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +9 -0
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +241 -235
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/charts/area-chart/area-chart.tsx +2 -1
- package/src/charts/area-chart/test/area-chart.test.tsx +31 -0
- package/src/charts/heatmap-chart/{private/build-calendar-data.ts → build-calendar-data.ts} +11 -35
- package/src/charts/heatmap-chart/index.ts +9 -3
- package/src/charts/heatmap-chart/private/index.ts +0 -2
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +1 -1
- package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +1 -1
- package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +1 -1
- package/src/charts/heatmap-chart/types.ts +29 -0
- package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +2 -2
- package/src/charts/line-chart/line-chart.tsx +2 -1
- package/src/charts/line-chart/test/line-chart.test.tsx +31 -0
- package/src/components/tooltip/accessible-tooltip.tsx +32 -10
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +32 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +22 -3
- package/src/components/tooltip/xy-chart-tooltip.tsx +2 -1
- package/src/styles/chart-scope.scss +11 -3
- package/src/styles/test/chart-scope.test.ts +35 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Display charts within Automattic products.",
|
|
5
5
|
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"typecheck": "tsgo --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@automattic/number-formatters": "^1.2.
|
|
66
|
+
"@automattic/number-formatters": "^1.2.9",
|
|
67
67
|
"@babel/runtime": "7.29.7",
|
|
68
68
|
"@date-fns/tz": "1.4.1",
|
|
69
69
|
"@react-spring/web": "10.1.1",
|
|
@@ -114,9 +114,9 @@
|
|
|
114
114
|
"@wordpress/components": "40.0.0",
|
|
115
115
|
"@wordpress/element": "8.6.0",
|
|
116
116
|
"@wordpress/private-apis": "1.54.0",
|
|
117
|
-
"babel-jest": "30.
|
|
117
|
+
"babel-jest": "30.5.1",
|
|
118
118
|
"identity-obj-proxy": "^3.0.0",
|
|
119
|
-
"jest": "30.
|
|
119
|
+
"jest": "30.5.1",
|
|
120
120
|
"jest-extended": "7.0.0",
|
|
121
121
|
"magic-string": "0.30.21",
|
|
122
122
|
"react": "18.3.1",
|
|
@@ -411,6 +411,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
411
411
|
|
|
412
412
|
return (
|
|
413
413
|
<div
|
|
414
|
+
ref={ chartRef }
|
|
414
415
|
role="grid"
|
|
415
416
|
aria-label={ __( 'Area chart', 'jetpack-charts' ) }
|
|
416
417
|
tabIndex={ 0 }
|
|
@@ -419,7 +420,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
419
420
|
onBlur={ onChartBlur }
|
|
420
421
|
>
|
|
421
422
|
{ chartHeight > 0 && (
|
|
422
|
-
<div
|
|
423
|
+
<div className={ plotStyles[ 'xy-plot' ] }>
|
|
423
424
|
{ zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
|
|
424
425
|
<XYChart
|
|
425
426
|
theme={ theme }
|
|
@@ -52,6 +52,37 @@ describe( 'AreaChart', () => {
|
|
|
52
52
|
);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
test.each( [ [ 'Escape', '{Escape}' ] ] )(
|
|
56
|
+
'returns focus to the grid after %s',
|
|
57
|
+
async ( _name, keys ) => {
|
|
58
|
+
jest.useFakeTimers();
|
|
59
|
+
try {
|
|
60
|
+
const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
|
|
61
|
+
renderWithProvider();
|
|
62
|
+
const chart = screen.getByRole( 'grid', { name: /area chart/i } );
|
|
63
|
+
|
|
64
|
+
await user.tab();
|
|
65
|
+
expect( chart ).toHaveFocus();
|
|
66
|
+
await user.keyboard( '{ArrowRight}' );
|
|
67
|
+
expect( screen.getByRole( 'tooltip' ) ).toHaveFocus();
|
|
68
|
+
|
|
69
|
+
await user.keyboard( keys );
|
|
70
|
+
await act( async () => {
|
|
71
|
+
jest.advanceTimersByTime( 5000 );
|
|
72
|
+
} );
|
|
73
|
+
expect( chart ).toHaveFocus();
|
|
74
|
+
expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
|
|
75
|
+
|
|
76
|
+
await user.keyboard( '{ArrowRight}' );
|
|
77
|
+
const tooltip = screen.getByRole( 'tooltip' );
|
|
78
|
+
expect( tooltip ).toHaveFocus();
|
|
79
|
+
expect( tooltip ).toHaveTextContent( 'Series A:10' );
|
|
80
|
+
} finally {
|
|
81
|
+
jest.useRealTimers();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
55
86
|
describe( 'Data Validation', () => {
|
|
56
87
|
test( 'shows error when data is empty', () => {
|
|
57
88
|
renderWithProvider( { data: [] } );
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createDateFormatter, sanitizeFormatting } from '
|
|
2
|
-
import { warnOnce } from '
|
|
1
|
+
import { createDateFormatter, sanitizeFormatting } from '../../utils/date-formatting';
|
|
2
|
+
import { warnOnce } from '../../utils/warn-once';
|
|
3
3
|
import {
|
|
4
4
|
addCivilDays,
|
|
5
5
|
civilDate,
|
|
@@ -9,39 +9,15 @@ import {
|
|
|
9
9
|
pointDayKey,
|
|
10
10
|
startOfCivilWeek,
|
|
11
11
|
writtenDayKey,
|
|
12
|
-
} from './civil-day';
|
|
13
|
-
import type {
|
|
14
|
-
import type {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type CalendarHeatmapOptions = {
|
|
23
|
-
weekStartsOn?: 0 | 1;
|
|
24
|
-
/**
|
|
25
|
-
* Mark the days completing the first and last week outside the series' span as
|
|
26
|
-
* hidden cells rather than blank ones, giving the calendar ragged edges. Days
|
|
27
|
-
* inside the span stay blank even where the series has no entry.
|
|
28
|
-
*/
|
|
29
|
-
hideOutOfRangeDays?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Draw the grid over this span (`yyyy-MM-dd` bounds) instead of the series' own.
|
|
32
|
-
* Days inside the grid but outside the series become placeholder cells: painted,
|
|
33
|
-
* but reporting nothing, since they were never measured. A start bound opens from
|
|
34
|
-
* the beginning of its week, and bounds narrower than the series are ignored.
|
|
35
|
-
*/
|
|
36
|
-
gridSpan?: { start?: string; end?: string };
|
|
37
|
-
/** BCP-47 tag the labels are written in. Defaults to the runtime's locale. */
|
|
38
|
-
locale?: string;
|
|
39
|
-
/**
|
|
40
|
-
* IANA zone the series' instants are bucketed into days in. Defaults to the
|
|
41
|
-
* runtime's zone. A `dateString` carrying no offset is taken as written.
|
|
42
|
-
*/
|
|
43
|
-
timeZone?: string;
|
|
44
|
-
};
|
|
12
|
+
} from './private/civil-day';
|
|
13
|
+
import type { CivilDate, DayKey } from './private/civil-day';
|
|
14
|
+
import type {
|
|
15
|
+
CalendarHeatmapOptions,
|
|
16
|
+
CalendarHeatmapResult,
|
|
17
|
+
HeatmapCell,
|
|
18
|
+
HeatmapColumn,
|
|
19
|
+
} from './types';
|
|
20
|
+
import type { DataPointDate } from '../../types';
|
|
45
21
|
|
|
46
22
|
/** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
|
|
47
23
|
const LABELLED_ROWS = [ 0, 2, 4 ];
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export { default as HeatmapChart, HeatmapChartUnresponsive } from './heatmap-chart';
|
|
2
|
-
export { buildCalendarHeatmapData } from './
|
|
2
|
+
export { buildCalendarHeatmapData } from './build-calendar-data';
|
|
3
3
|
export { useCalendarHeatmapData } from './use-calendar-heatmap-data';
|
|
4
|
-
export type {
|
|
5
|
-
|
|
4
|
+
export type {
|
|
5
|
+
CalendarHeatmapOptions,
|
|
6
|
+
CalendarHeatmapResult,
|
|
7
|
+
HeatmapChartProps,
|
|
8
|
+
HeatmapColumn,
|
|
9
|
+
HeatmapCell,
|
|
10
|
+
HeatmapTooltipData,
|
|
11
|
+
} from './types';
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export { getValueExtent, getNormalizedValue, isPresent } from './use-heatmap-colors';
|
|
2
|
-
export { buildCalendarHeatmapData } from './build-calendar-data';
|
|
3
2
|
export { HeatmapLegend } from './heatmap-legend';
|
|
4
3
|
export { HeatmapContext } from './heatmap-context';
|
|
5
|
-
export type { CalendarHeatmapOptions, CalendarHeatmapResult } from './build-calendar-data';
|
|
6
4
|
export type { HeatmapLegendProps } from './heatmap-legend';
|
|
7
5
|
export type { HeatmapContextValue } from './heatmap-context';
|
|
@@ -8,7 +8,7 @@ import { render } from '@testing-library/react';
|
|
|
8
8
|
import { GlobalChartsProvider } from '../../../providers';
|
|
9
9
|
import { useCalendarHeatmapData } from '../use-calendar-heatmap-data';
|
|
10
10
|
import type { DataPointDate } from '../../../types';
|
|
11
|
-
import type { CalendarHeatmapOptions, CalendarHeatmapResult } from '../
|
|
11
|
+
import type { CalendarHeatmapOptions, CalendarHeatmapResult } from '../types';
|
|
12
12
|
|
|
13
13
|
// 2026-08-03 08:00 in Asia/Tokyo, 2026-08-02 16:00 in America/Los_Angeles.
|
|
14
14
|
const series: DataPointDate[] = [ { date: new Date( '2026-08-02T23:00:00Z' ), value: 7 } ];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// Chatham runs ahead of UTC. A worker behind it renders a UTC-midnight proxy on
|
|
6
6
|
// its own day, so it cannot tell the grid's UTC arithmetic from local arithmetic.
|
|
7
|
-
import { buildCalendarHeatmapData } from '../
|
|
7
|
+
import { buildCalendarHeatmapData } from '../build-calendar-data';
|
|
8
8
|
import type { DataPointDate } from '../../../types';
|
|
9
9
|
|
|
10
10
|
const labelsCarryingValues = ( data: ReturnType< typeof buildCalendarHeatmapData >[ 'data' ] ) =>
|
|
@@ -82,3 +82,32 @@ export interface HeatmapChartProps
|
|
|
82
82
|
renderTooltip?: ( data: HeatmapTooltipData ) => ReactNode;
|
|
83
83
|
children?: ReactNode;
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
export type CalendarHeatmapResult = {
|
|
87
|
+
data: HeatmapColumn[];
|
|
88
|
+
rowLabels: string[];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type CalendarHeatmapOptions = {
|
|
92
|
+
weekStartsOn?: 0 | 1;
|
|
93
|
+
/**
|
|
94
|
+
* Mark the days completing the first and last week outside the series' span as
|
|
95
|
+
* hidden cells rather than blank ones, giving the calendar ragged edges. Days
|
|
96
|
+
* inside the span stay blank even where the series has no entry.
|
|
97
|
+
*/
|
|
98
|
+
hideOutOfRangeDays?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Draw the grid over this span (`yyyy-MM-dd` bounds) instead of the series' own.
|
|
101
|
+
* Days inside the grid but outside the series become placeholder cells: painted,
|
|
102
|
+
* but reporting nothing, since they were never measured. A start bound opens from
|
|
103
|
+
* the beginning of its week, and bounds narrower than the series are ignored.
|
|
104
|
+
*/
|
|
105
|
+
gridSpan?: { start?: string; end?: string };
|
|
106
|
+
/** BCP-47 tag the labels are written in. Defaults to the runtime's locale. */
|
|
107
|
+
locale?: string;
|
|
108
|
+
/**
|
|
109
|
+
* IANA zone the series' instants are bucketed into days in. Defaults to the
|
|
110
|
+
* runtime's zone. A `dateString` carrying no offset is taken as written.
|
|
111
|
+
*/
|
|
112
|
+
timeZone?: string;
|
|
113
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { useChartFormatting } from '../../providers';
|
|
3
|
-
import { buildCalendarHeatmapData } from './
|
|
4
|
-
import type { CalendarHeatmapOptions, CalendarHeatmapResult } from './
|
|
3
|
+
import { buildCalendarHeatmapData } from './build-calendar-data';
|
|
4
|
+
import type { CalendarHeatmapOptions, CalendarHeatmapResult } from './types';
|
|
5
5
|
import type { DataPointDate } from '../../types';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -562,6 +562,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
562
562
|
|
|
563
563
|
return (
|
|
564
564
|
<div
|
|
565
|
+
ref={ chartRef }
|
|
565
566
|
role="grid"
|
|
566
567
|
aria-label={ __( 'Line chart', 'jetpack-charts' ) }
|
|
567
568
|
tabIndex={ 0 }
|
|
@@ -570,7 +571,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
570
571
|
onBlur={ onChartBlur }
|
|
571
572
|
>
|
|
572
573
|
{ chartHeight > 0 && (
|
|
573
|
-
<div
|
|
574
|
+
<div className={ plotStyles[ 'xy-plot' ] }>
|
|
574
575
|
{ zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
|
|
575
576
|
<XYChart
|
|
576
577
|
theme={ theme }
|
|
@@ -204,6 +204,37 @@ describe( 'LineChart', () => {
|
|
|
204
204
|
);
|
|
205
205
|
} );
|
|
206
206
|
|
|
207
|
+
test.each( [ [ 'Escape', '{Escape}' ] ] )(
|
|
208
|
+
'returns focus to the grid after %s',
|
|
209
|
+
async ( _name, keys ) => {
|
|
210
|
+
jest.useFakeTimers();
|
|
211
|
+
try {
|
|
212
|
+
const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
|
|
213
|
+
renderWithTheme();
|
|
214
|
+
const chart = screen.getByRole( 'grid', { name: /line chart/i } );
|
|
215
|
+
|
|
216
|
+
await user.tab();
|
|
217
|
+
expect( chart ).toHaveFocus();
|
|
218
|
+
await user.keyboard( '{ArrowRight}' );
|
|
219
|
+
expect( screen.getByRole( 'tooltip' ) ).toHaveFocus();
|
|
220
|
+
|
|
221
|
+
await user.keyboard( keys );
|
|
222
|
+
await act( async () => {
|
|
223
|
+
jest.advanceTimersByTime( 5000 );
|
|
224
|
+
} );
|
|
225
|
+
expect( chart ).toHaveFocus();
|
|
226
|
+
expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
|
|
227
|
+
|
|
228
|
+
await user.keyboard( '{ArrowRight}' );
|
|
229
|
+
const tooltip = screen.getByRole( 'tooltip' );
|
|
230
|
+
expect( tooltip ).toHaveFocus();
|
|
231
|
+
expect( tooltip ).toHaveTextContent( 'Series A:10' );
|
|
232
|
+
} finally {
|
|
233
|
+
jest.useRealTimers();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
|
|
207
238
|
describe( 'Data Validation', () => {
|
|
208
239
|
test( 'handles empty data array', () => {
|
|
209
240
|
renderWithTheme( { data: [] } );
|
|
@@ -227,12 +227,18 @@ export const useKeyboardNavigation = ( {
|
|
|
227
227
|
[ preventTooltipScroll, selectedIndex ]
|
|
228
228
|
);
|
|
229
229
|
|
|
230
|
-
//
|
|
231
|
-
const onChartFocus = useCallback(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
230
|
+
// Returning focus from the tooltip must not restore the selection Escape just cleared.
|
|
231
|
+
const onChartFocus = useCallback(
|
|
232
|
+
( event: React.FocusEvent< HTMLDivElement > ) => {
|
|
233
|
+
if ( event.currentTarget.contains( event.relatedTarget ) ) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if ( ! isNavigating && selectedIndex !== undefined ) {
|
|
237
|
+
setSelectedIndex( 0 );
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[ isNavigating, selectedIndex, setSelectedIndex ]
|
|
241
|
+
);
|
|
236
242
|
|
|
237
243
|
// On each blur of chart, keyboard navigation should restart from first tooltip
|
|
238
244
|
const onChartBlur = useCallback( () => {
|
|
@@ -243,9 +249,17 @@ export const useKeyboardNavigation = ( {
|
|
|
243
249
|
( event: React.KeyboardEvent< HTMLDivElement > ) => {
|
|
244
250
|
if ( totalPoints === 0 ) return;
|
|
245
251
|
|
|
252
|
+
const focusChart = () => {
|
|
253
|
+
if ( preventTooltipScroll ) {
|
|
254
|
+
chartRef.current?.focus( { preventScroll: true } );
|
|
255
|
+
} else {
|
|
256
|
+
chartRef.current?.focus();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
246
260
|
// Keep focus on the chart if tab is pressed
|
|
247
261
|
if ( event.key === 'Tab' ) {
|
|
248
|
-
|
|
262
|
+
focusChart();
|
|
249
263
|
setSelectedIndex( undefined );
|
|
250
264
|
setIsNavigating( false );
|
|
251
265
|
return;
|
|
@@ -254,7 +268,7 @@ export const useKeyboardNavigation = ( {
|
|
|
254
268
|
const currentSelectedIndex = selectedIndex === undefined ? -1 : selectedIndex;
|
|
255
269
|
|
|
256
270
|
if ( currentSelectedIndex + 1 >= totalPoints && [ 'ArrowRight' ].includes( event.key ) ) {
|
|
257
|
-
|
|
271
|
+
focusChart();
|
|
258
272
|
setSelectedIndex( undefined );
|
|
259
273
|
setIsNavigating( false );
|
|
260
274
|
return;
|
|
@@ -271,12 +285,20 @@ export const useKeyboardNavigation = ( {
|
|
|
271
285
|
} else if ( event.key === 'Escape' ) {
|
|
272
286
|
setSelectedIndex( undefined );
|
|
273
287
|
setIsNavigating( false );
|
|
274
|
-
|
|
288
|
+
focusChart();
|
|
275
289
|
} else if ( ( event.key === 'Enter' || event.key === ' ' ) && selectedIndex !== undefined ) {
|
|
276
290
|
onActivate?.( selectedIndex );
|
|
277
291
|
}
|
|
278
292
|
},
|
|
279
|
-
[
|
|
293
|
+
[
|
|
294
|
+
totalPoints,
|
|
295
|
+
selectedIndex,
|
|
296
|
+
setSelectedIndex,
|
|
297
|
+
setIsNavigating,
|
|
298
|
+
chartRef,
|
|
299
|
+
onActivate,
|
|
300
|
+
preventTooltipScroll,
|
|
301
|
+
]
|
|
280
302
|
);
|
|
281
303
|
|
|
282
304
|
return {
|
|
@@ -86,6 +86,38 @@ describe( 'AccessibleTooltip', () => {
|
|
|
86
86
|
}
|
|
87
87
|
} );
|
|
88
88
|
|
|
89
|
+
it.each( [
|
|
90
|
+
[ 'Tab', '{Tab}' ],
|
|
91
|
+
[ 'Escape', '{Escape}' ],
|
|
92
|
+
] )( 'requests scroll suppression when returning focus after %s', async ( _name, keys ) => {
|
|
93
|
+
const user = userEvent.setup();
|
|
94
|
+
renderChart( undefined, 'below-axis' );
|
|
95
|
+
await openTooltip();
|
|
96
|
+
const focus = jest.spyOn( screen.getByRole( 'grid' ), 'focus' );
|
|
97
|
+
try {
|
|
98
|
+
await user.keyboard( keys );
|
|
99
|
+
expect( focus ).toHaveBeenCalledWith( { preventScroll: true } );
|
|
100
|
+
} finally {
|
|
101
|
+
focus.mockRestore();
|
|
102
|
+
}
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
it.each( [
|
|
106
|
+
[ 'Tab', '{Tab}' ],
|
|
107
|
+
[ 'Escape', '{Escape}' ],
|
|
108
|
+
] )( 'keeps default focus scrolling when returning focus after %s', async ( _name, keys ) => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
renderChart();
|
|
111
|
+
await openTooltip();
|
|
112
|
+
const focus = jest.spyOn( screen.getByRole( 'grid' ), 'focus' );
|
|
113
|
+
try {
|
|
114
|
+
await user.keyboard( keys );
|
|
115
|
+
expect( focus ).toHaveBeenCalledWith();
|
|
116
|
+
} finally {
|
|
117
|
+
focus.mockRestore();
|
|
118
|
+
}
|
|
119
|
+
} );
|
|
120
|
+
|
|
89
121
|
it( 'falls back to the catalog default when the role is unset', async () => {
|
|
90
122
|
renderChart();
|
|
91
123
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
-
import { BarSeries, LineSeries, TooltipContext, XYChart } from '@visx/xychart';
|
|
2
|
+
import { BarSeries, buildChartTheme, 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';
|
|
6
|
-
import type { EventHandlerParams } from '@visx/xychart';
|
|
6
|
+
import type { EventHandlerParams, XYChartTheme } from '@visx/xychart';
|
|
7
7
|
|
|
8
8
|
type Datum = { x: number; y: number };
|
|
9
9
|
|
|
@@ -76,7 +76,8 @@ const renderChart = (
|
|
|
76
76
|
params: EventHandlerParams< Datum >[] = [
|
|
77
77
|
{ key: 'A', index: 1, datum: SERIES_A[ 1 ], svgPoint: { x: 123, y: 45 } },
|
|
78
78
|
],
|
|
79
|
-
margin = NO_MARGIN
|
|
79
|
+
margin = NO_MARGIN,
|
|
80
|
+
theme?: XYChartTheme
|
|
80
81
|
) =>
|
|
81
82
|
render(
|
|
82
83
|
<div data-testid="wrapper" style={ { position: 'relative' } }>
|
|
@@ -84,6 +85,7 @@ const renderChart = (
|
|
|
84
85
|
width={ 200 }
|
|
85
86
|
height={ 100 }
|
|
86
87
|
margin={ margin }
|
|
88
|
+
theme={ theme }
|
|
87
89
|
xScale={ { type: 'linear', domain: [ 0, 10 ] } }
|
|
88
90
|
yScale={ { type: 'linear', domain: [ 0, 100 ] } }
|
|
89
91
|
>
|
|
@@ -100,6 +102,23 @@ const renderChart = (
|
|
|
100
102
|
);
|
|
101
103
|
|
|
102
104
|
describe( 'XyChartTooltip', () => {
|
|
105
|
+
test.each( [
|
|
106
|
+
[ 'CanvasText', '0 1px 2px #22222255' ],
|
|
107
|
+
[ '#123456', '0 1px 2px #12345655' ],
|
|
108
|
+
] )( 'renders a valid shadow for htmlLabel color %s', async ( color, boxShadow ) => {
|
|
109
|
+
const theme = buildChartTheme( {
|
|
110
|
+
backgroundColor: '#ffffff',
|
|
111
|
+
colors: [ '#123456' ],
|
|
112
|
+
tickLength: 4,
|
|
113
|
+
gridColor: '#dddddd',
|
|
114
|
+
gridColorDark: '#222222',
|
|
115
|
+
htmlLabel: { color },
|
|
116
|
+
} );
|
|
117
|
+
renderChart( {}, undefined, undefined, theme );
|
|
118
|
+
|
|
119
|
+
await expect( screen.findByTestId( 'tooltip-box' ) ).resolves.toHaveStyle( { boxShadow } );
|
|
120
|
+
} );
|
|
121
|
+
|
|
103
122
|
test( 'renders the tooltip box inside the chart wrapper, not in a body-level portal', async () => {
|
|
104
123
|
const { container } = renderChart();
|
|
105
124
|
|
|
@@ -2,6 +2,7 @@ import { Tooltip, defaultStyles } from '@visx/tooltip';
|
|
|
2
2
|
import { DataContext, TooltipContext } from '@visx/xychart';
|
|
3
3
|
import { useCallback, useContext, useState } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
|
+
import { isValidHexColor } from '../../utils';
|
|
5
6
|
import { BoundedTooltip } from './private/bounded-tooltip';
|
|
6
7
|
import type {
|
|
7
8
|
CrosshairStyle,
|
|
@@ -220,7 +221,7 @@ const XyChartTooltipContent = < Datum extends object >( {
|
|
|
220
221
|
zIndex,
|
|
221
222
|
backgroundColor: theme?.backgroundColor ?? 'white',
|
|
222
223
|
boxShadow: `0 1px 2px ${
|
|
223
|
-
theme?.htmlLabel?.color ? `${ theme.htmlLabel.color }55` : '#22222255'
|
|
224
|
+
isValidHexColor( theme?.htmlLabel?.color ) ? `${ theme.htmlLabel.color }55` : '#22222255'
|
|
224
225
|
}`,
|
|
225
226
|
...theme?.htmlLabel,
|
|
226
227
|
...style,
|
|
@@ -42,9 +42,7 @@
|
|
|
42
42
|
// The literal is load-bearing: a `fill` reading a fallback-less var() on an
|
|
43
43
|
// undeclared property drops the declaration and paints black instead.
|
|
44
44
|
--a8c-charts-color-label-background: transparent;
|
|
45
|
-
//
|
|
46
|
-
// derived from it, so setting the broad role moves every label at once while
|
|
47
|
-
// setting this one moves only the axis.
|
|
45
|
+
// See TOKENS.md's "Broad roles and narrow ones" for label inheritance.
|
|
48
46
|
--a8c-charts-color-label-axis: var(--a8c-charts-color-label);
|
|
49
47
|
--a8c-charts-color-annotation: var(--wpds-color-foreground-content-neutral);
|
|
50
48
|
--a8c-charts-color-trend-up: var(--wpds-color-foreground-content-success-weak);
|
|
@@ -86,3 +84,13 @@
|
|
|
86
84
|
--a8c-charts-dimension-leaderboard-row-gap: var(--wpds-dimension-gap-md);
|
|
87
85
|
--a8c-charts-dimension-leaderboard-column-gap: var(--wpds-dimension-gap-xs);
|
|
88
86
|
}
|
|
87
|
+
|
|
88
|
+
@media (forced-colors: active) {
|
|
89
|
+
|
|
90
|
+
:where(.a8c-charts-scope) {
|
|
91
|
+
--a8c-charts-color-label-axis: CanvasText;
|
|
92
|
+
--a8c-charts-color-grid: GrayText;
|
|
93
|
+
--a8c-charts-color-axis-x: GrayText;
|
|
94
|
+
--a8c-charts-color-tick-x: GrayText;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { compileString } from 'sass-embedded';
|
|
3
4
|
|
|
4
5
|
const stylesheet = readFileSync( join( __dirname, '..', 'chart-scope.scss' ), 'utf8' );
|
|
5
6
|
const tokensDoc = readFileSync( join( __dirname, '..', '..', '..', 'TOKENS.md' ), 'utf8' );
|
|
@@ -19,7 +20,7 @@ type Entry = {
|
|
|
19
20
|
* @return Every declared `--a8c-charts-*` property, keyed by name.
|
|
20
21
|
*/
|
|
21
22
|
function parseStylesheet(): Map< string, Entry > {
|
|
22
|
-
const body = stylesheet.slice( stylesheet.indexOf( '{' ) + 1, stylesheet.
|
|
23
|
+
const body = stylesheet.slice( stylesheet.indexOf( '{' ) + 1, stylesheet.indexOf( '}' ) );
|
|
23
24
|
const entries = new Map< string, Entry >();
|
|
24
25
|
|
|
25
26
|
for ( const declaration of body
|
|
@@ -112,6 +113,39 @@ const declared = parseStylesheet();
|
|
|
112
113
|
const documented = parseTokensDoc();
|
|
113
114
|
|
|
114
115
|
describe( 'chart scope catalog', () => {
|
|
116
|
+
it( 'uses system colors for axis text and visible lines only under forced colors', () => {
|
|
117
|
+
const style = document.createElement( 'style' );
|
|
118
|
+
style.textContent = compileString( stylesheet ).css;
|
|
119
|
+
document.head.appendChild( style );
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const mediaRules = Array.from( style.sheet!.cssRules ).filter(
|
|
123
|
+
( rule ): rule is CSSMediaRule => rule.type === CSSRule.MEDIA_RULE
|
|
124
|
+
);
|
|
125
|
+
expect( mediaRules ).toHaveLength( 1 );
|
|
126
|
+
expect( mediaRules[ 0 ].conditionText ).toBe( '(forced-colors: active)' );
|
|
127
|
+
expect( mediaRules[ 0 ].cssRules ).toHaveLength( 1 );
|
|
128
|
+
|
|
129
|
+
const rule = mediaRules[ 0 ].cssRules[ 0 ] as CSSStyleRule;
|
|
130
|
+
expect( rule.selectorText ).toBe( ':where(.a8c-charts-scope)' );
|
|
131
|
+
expect(
|
|
132
|
+
Object.fromEntries(
|
|
133
|
+
Array.from( rule.style ).map( property => [
|
|
134
|
+
property,
|
|
135
|
+
rule.style.getPropertyValue( property ).trim().toLowerCase(),
|
|
136
|
+
] )
|
|
137
|
+
)
|
|
138
|
+
).toEqual( {
|
|
139
|
+
'--a8c-charts-color-label-axis': 'canvastext',
|
|
140
|
+
'--a8c-charts-color-grid': 'graytext',
|
|
141
|
+
'--a8c-charts-color-axis-x': 'graytext',
|
|
142
|
+
'--a8c-charts-color-tick-x': 'graytext',
|
|
143
|
+
} );
|
|
144
|
+
} finally {
|
|
145
|
+
style.remove();
|
|
146
|
+
}
|
|
147
|
+
} );
|
|
148
|
+
|
|
115
149
|
it( 'documents every declared role, and declares every documented one', () => {
|
|
116
150
|
expect( [ ...documented.keys() ].sort() ).toEqual( [ ...declared.keys() ].sort() );
|
|
117
151
|
} );
|