@automattic/charts 1.10.2 → 1.11.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 +72 -0
- package/dist/index.cjs +149 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +60 -47
- package/dist/index.d.cts +17 -7
- package/dist/index.d.ts +17 -7
- package/dist/index.js +150 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +7 -7
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +18 -13
- package/src/charts/heatmap-chart/heatmap-chart.tsx +11 -7
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +3 -3
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +13 -7
- package/src/charts/leaderboard-chart/hooks/index.ts +1 -0
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +115 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +47 -20
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +72 -16
- package/src/charts/leaderboard-chart/test/leaderboard-chart-fit-rows.test.tsx +365 -0
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +73 -2
- package/src/charts/leaderboard-chart/types.ts +12 -0
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/private/grid-control/grid-control.module.scss +1 -1
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/x-zoom.module.scss +12 -10
- package/src/components/tooltip/base-tooltip.module.scss +3 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +6 -5
- package/src/providers/chart-context/themes.ts +33 -19
- package/src/types.ts +2 -2
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
.main-rate {
|
|
14
14
|
overflow: hidden;
|
|
15
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
15
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
16
16
|
text-overflow: ellipsis;
|
|
17
17
|
font-size: var(--wpds-typography-font-size-xl, 18px);
|
|
18
18
|
font-style: normal;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
.step-label {
|
|
57
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
57
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
58
58
|
font-size: var(--wpds-typography-font-size-sm, 12px);
|
|
59
59
|
font-weight: var(--wpds-typography-font-weight-regular, 400);
|
|
60
60
|
line-height: var(--wpds-typography-line-height-xs, 16px);
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
.step-rate {
|
|
67
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
67
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
68
68
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
69
69
|
font-weight: var(--wpds-typography-font-weight-medium, 499);
|
|
70
70
|
line-height: var(--wpds-typography-line-height-xs, 16px);
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.tooltip-wrapper {
|
|
103
|
-
background: var(--wpds-color-background-surface-neutral-strong, #fff);
|
|
103
|
+
background: var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff));
|
|
104
104
|
|
|
105
105
|
// Override .visx-tooltip inline styles.
|
|
106
106
|
border-radius: var(--wpds-border-radius-md, 4px) !important;
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
.tooltip-title {
|
|
113
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
113
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
114
114
|
font-size: var(--wpds-typography-font-size-sm, 12px);
|
|
115
115
|
font-style: normal;
|
|
116
116
|
font-weight: var(--wpds-typography-font-weight-regular, 400);
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
.tooltip-content {
|
|
121
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
121
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
122
122
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
123
123
|
font-style: normal;
|
|
124
124
|
font-weight: var(--wpds-typography-font-weight-medium, 499);
|
|
@@ -127,6 +127,6 @@
|
|
|
127
127
|
|
|
128
128
|
.empty-state {
|
|
129
129
|
text-align: center;
|
|
130
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
130
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
131
131
|
font-size: var(--wpds-typography-font-size-lg, 16px);
|
|
132
132
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Focus-ring width shared across the grid and cell-selected focus outlines. A
|
|
2
|
+
// Sass var, not a custom property: a compile-time constant scoped to this
|
|
3
|
+
// module, so it needs no runtime cascade and never lands on the DOM.
|
|
4
|
+
$focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px));
|
|
5
|
+
|
|
1
6
|
.heatmap-chart {
|
|
2
7
|
width: 100%;
|
|
3
8
|
height: 100%;
|
|
@@ -14,14 +19,14 @@
|
|
|
14
19
|
|
|
15
20
|
.heatmap-chart__empty {
|
|
16
21
|
padding: var(--wpds-dimension-padding-lg, 16px);
|
|
17
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
22
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
// Track template is set inline because CSS `repeat()` won't take a `var()`
|
|
21
26
|
// count. ARIA rows are `display: contents` so their cells join this grid.
|
|
22
27
|
.heatmap-chart__grid {
|
|
23
28
|
display: grid;
|
|
24
|
-
gap: var(--heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
|
|
29
|
+
gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
|
|
25
30
|
flex: 1 1 0;
|
|
26
31
|
width: 100%;
|
|
27
32
|
// Override flex's auto minimum so short widgets do not scroll.
|
|
@@ -29,8 +34,8 @@
|
|
|
29
34
|
|
|
30
35
|
&:focus-visible {
|
|
31
36
|
outline:
|
|
32
|
-
|
|
33
|
-
var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
|
|
37
|
+
$focus-ring-width solid
|
|
38
|
+
var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
|
|
34
39
|
outline-offset: 2px;
|
|
35
40
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
36
41
|
}
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
|
|
59
64
|
.heatmap-chart__col-label,
|
|
60
65
|
.heatmap-chart__row-label {
|
|
61
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
66
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
62
67
|
font-size: var(--wpds-typography-font-size-xs, 11px);
|
|
63
68
|
white-space: nowrap;
|
|
64
69
|
overflow: visible;
|
|
@@ -84,7 +89,7 @@
|
|
|
84
89
|
min-height: 0;
|
|
85
90
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
86
91
|
// Empty (no-data) default; cells with a value override it below.
|
|
87
|
-
background: var(--wpds-color-background-track-neutral-weak, #f0f0f0);
|
|
92
|
+
background: var(--a8c-charts-color-track, var(--wpds-color-background-track-neutral-weak, #f0f0f0));
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
// An empty grid slot (calendar ragged edges): keeps its track but paints and
|
|
@@ -97,30 +102,30 @@
|
|
|
97
102
|
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
98
103
|
// an empty cell.
|
|
99
104
|
.heatmap-chart__cell--filled {
|
|
100
|
-
background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), var(--heatmap-
|
|
105
|
+
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-heatmap-background, #fff));
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
.heatmap-chart__cell--selected {
|
|
104
109
|
outline:
|
|
105
|
-
|
|
106
|
-
var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
|
|
107
|
-
outline-offset: calc(-1 *
|
|
110
|
+
$focus-ring-width solid
|
|
111
|
+
var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
|
|
112
|
+
outline-offset: calc(-1 * #{$focus-ring-width});
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
.heatmap-chart__cell-value {
|
|
111
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
116
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
112
117
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// Light text on fills dark enough to out-contrast dark text (decided in JS from
|
|
116
121
|
// the blended fill luminance).
|
|
117
122
|
.heatmap-chart__cell--strong .heatmap-chart__cell-value {
|
|
118
|
-
color: var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0);
|
|
123
|
+
color: var(--a8c-charts-color-label-inverse, var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0));
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
.heatmap-chart__legend-swatch {
|
|
122
127
|
width: var(--wpds-dimension-size-3xs, 12px);
|
|
123
128
|
height: var(--wpds-dimension-size-3xs, 12px);
|
|
124
129
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
125
|
-
background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), var(--heatmap-
|
|
130
|
+
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-heatmap-background, #fff));
|
|
126
131
|
}
|
|
@@ -277,20 +277,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
277
277
|
// and a min floor makes the grid overflow (for a scrollable wrapper)
|
|
278
278
|
// rather than crushing cells on long ranges.
|
|
279
279
|
const columnTrack = compact
|
|
280
|
-
? 'var(--heatmap-cell-size)'
|
|
280
|
+
? 'var(--a8c-charts-dimension-heatmap-cell-size)'
|
|
281
281
|
: `minmax(${ minCellWidth ?? 0 }px, ${ maxCellWidth ? `${ maxCellWidth }px` : '1fr' })`;
|
|
282
282
|
const rowTrack = compact
|
|
283
|
-
? 'var(--heatmap-cell-size)'
|
|
283
|
+
? 'var(--a8c-charts-dimension-heatmap-cell-size)'
|
|
284
284
|
: `minmax(${ minCellHeight ?? 0 }px, ${ maxCellHeight ? `${ maxCellHeight }px` : '1fr' })`;
|
|
285
285
|
const gridStyle: Record< string, string | number > = {
|
|
286
|
-
'--heatmap-primary': primaryColorHex,
|
|
287
|
-
'--heatmap-
|
|
286
|
+
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
287
|
+
'--a8c-charts-color-heatmap-background': theme.backgroundColor,
|
|
288
288
|
gridTemplateColumns: `auto repeat(${ columns }, ${ columnTrack })`,
|
|
289
289
|
gridTemplateRows: `auto repeat(${ rows }, ${ rowTrack })`,
|
|
290
290
|
};
|
|
291
291
|
if ( compact ) {
|
|
292
|
-
gridStyle[ '--heatmap-cell-gap' ] = `${ compactCellGap }px`;
|
|
293
|
-
gridStyle[ '--heatmap-cell-size' ] = `${ compactCellSize }px`;
|
|
292
|
+
gridStyle[ '--a8c-charts-dimension-heatmap-cell-gap' ] = `${ compactCellGap }px`;
|
|
293
|
+
gridStyle[ '--a8c-charts-dimension-heatmap-cell-size' ] = `${ compactCellSize }px`;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
const activeDescendant =
|
|
@@ -419,7 +419,11 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
419
419
|
selectedIndex === flatIndex,
|
|
420
420
|
} ) }
|
|
421
421
|
style={
|
|
422
|
-
present
|
|
422
|
+
present
|
|
423
|
+
? ( {
|
|
424
|
+
'--a8c-charts-heatmap-cell-intensity': normalized,
|
|
425
|
+
} as CSSProperties )
|
|
426
|
+
: undefined
|
|
423
427
|
}
|
|
424
428
|
onMouseMove={ handleCellMouseMove }
|
|
425
429
|
onMouseLeave={ handleCellMouseLeave }
|
|
@@ -37,9 +37,9 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
37
37
|
className={ styles[ 'heatmap-chart__legend-swatch' ] }
|
|
38
38
|
style={
|
|
39
39
|
{
|
|
40
|
-
'--heatmap-primary': primaryColorHex,
|
|
41
|
-
'--heatmap-
|
|
42
|
-
'--intensity': intensity,
|
|
40
|
+
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
41
|
+
'--a8c-charts-color-heatmap-background': backgroundColor,
|
|
42
|
+
'--a8c-charts-heatmap-cell-intensity': intensity,
|
|
43
43
|
} as CSSProperties
|
|
44
44
|
}
|
|
45
45
|
/>
|
|
@@ -285,7 +285,7 @@ describe( 'HeatmapChart', () => {
|
|
|
285
285
|
renderChart();
|
|
286
286
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
287
287
|
// Non-compact sets no inline gap — the SCSS falls back to the WPDS token.
|
|
288
|
-
expect( grid.style.getPropertyValue( '--heatmap-cell-gap' ) ).toBe( '' );
|
|
288
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-gap' ) ).toBe( '' );
|
|
289
289
|
} );
|
|
290
290
|
|
|
291
291
|
test( 'applies the compact gap inline from the theme compactCellGap', () => {
|
|
@@ -295,7 +295,9 @@ describe( 'HeatmapChart', () => {
|
|
|
295
295
|
</GlobalChartsProvider>
|
|
296
296
|
);
|
|
297
297
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
298
|
-
expect( grid.style.getPropertyValue( '--heatmap-cell-gap' ) ).toBe(
|
|
298
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-gap' ) ).toBe(
|
|
299
|
+
'3px'
|
|
300
|
+
);
|
|
299
301
|
} );
|
|
300
302
|
|
|
301
303
|
test( 'sizes compact cells to the theme compactCellSize', () => {
|
|
@@ -305,9 +307,13 @@ describe( 'HeatmapChart', () => {
|
|
|
305
307
|
</GlobalChartsProvider>
|
|
306
308
|
);
|
|
307
309
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
308
|
-
expect( grid.style.getPropertyValue( '--heatmap-cell-size' ) ).toBe(
|
|
310
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-size' ) ).toBe(
|
|
311
|
+
'20px'
|
|
312
|
+
);
|
|
309
313
|
// Compact track template is built from the fixed cell size.
|
|
310
|
-
expect( grid.style.gridTemplateColumns ).toContain(
|
|
314
|
+
expect( grid.style.gridTemplateColumns ).toContain(
|
|
315
|
+
'var(--a8c-charts-dimension-heatmap-cell-size)'
|
|
316
|
+
);
|
|
311
317
|
} );
|
|
312
318
|
|
|
313
319
|
test( 'caps cell width without changing the normal vertical layout', () => {
|
|
@@ -343,7 +349,7 @@ describe( 'HeatmapChart', () => {
|
|
|
343
349
|
test( 'applies the primaryColor prop as the cell-scale color', () => {
|
|
344
350
|
renderChart( { primaryColor: '#abcdef' } );
|
|
345
351
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
346
|
-
expect( grid.style.getPropertyValue( '--heatmap-primary' ) ).toBe( '#abcdef' );
|
|
352
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#abcdef' );
|
|
347
353
|
} );
|
|
348
354
|
|
|
349
355
|
test( 'resolves primaryColor from the chart theme', () => {
|
|
@@ -353,7 +359,7 @@ describe( 'HeatmapChart', () => {
|
|
|
353
359
|
</GlobalChartsProvider>
|
|
354
360
|
);
|
|
355
361
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
356
|
-
expect( grid.style.getPropertyValue( '--heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
362
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
357
363
|
} );
|
|
358
364
|
|
|
359
365
|
test( 'falls back to the palette colors[0] when no prop or theme primaryColor is set', () => {
|
|
@@ -363,7 +369,7 @@ describe( 'HeatmapChart', () => {
|
|
|
363
369
|
</GlobalChartsProvider>
|
|
364
370
|
);
|
|
365
371
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
366
|
-
expect( grid.style.getPropertyValue( '--heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
372
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
367
373
|
} );
|
|
368
374
|
|
|
369
375
|
test( 'the unresponsive export pins explicit width and height', () => {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fractional layout rounding can put a row's bottom a hair past the container's
|
|
5
|
+
* without any visible clipping. Allow that much slack before hiding a row.
|
|
6
|
+
*
|
|
7
|
+
* Exported so tests and stories assert against the real tolerance rather than
|
|
8
|
+
* restating it.
|
|
9
|
+
*/
|
|
10
|
+
export const SUBPIXEL_TOLERANCE = 0.5;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Counts how many leading rows fit inside the content container.
|
|
14
|
+
*
|
|
15
|
+
* Rows are read from the DOM rather than derived from constants: row height
|
|
16
|
+
* depends on the theme's row gap, label wrapping, and whatever a caller renders
|
|
17
|
+
* as a label, so any restated number would drift.
|
|
18
|
+
*
|
|
19
|
+
* Every row is a direct grid child marked with `data-row-index`; interactive rows
|
|
20
|
+
* use a button wrapper and non-interactive rows use a div wrapper.
|
|
21
|
+
*
|
|
22
|
+
* @param enabled - Whether to measure at all. When false every row fits.
|
|
23
|
+
* @param rowCount - Total number of rows rendered.
|
|
24
|
+
* @param data - Rendered row data, whose geometry may change without changing the count.
|
|
25
|
+
* @return Ref for the content container and the number of leading rows that fit.
|
|
26
|
+
*/
|
|
27
|
+
export function useFittedRowCount( enabled: boolean, rowCount: number, data: unknown ) {
|
|
28
|
+
const contentRef = useRef< HTMLDivElement | null >( null );
|
|
29
|
+
const [ fittedCount, setFittedCount ] = useState( rowCount );
|
|
30
|
+
const [ isMeasurable, setIsMeasurable ] = useState( true );
|
|
31
|
+
|
|
32
|
+
const measure = useCallback( () => {
|
|
33
|
+
const content = contentRef.current;
|
|
34
|
+
if ( ! content ) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Switching from the default scrollable mode can leave the content at a
|
|
39
|
+
// non-zero scroll offset, which would shift every measurement below.
|
|
40
|
+
content.scrollTop = 0;
|
|
41
|
+
|
|
42
|
+
const rows = content.querySelectorAll< HTMLElement >(
|
|
43
|
+
':scope > [data-leaderboard-grid] > [data-row-index]'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const rowBottoms: number[] = [];
|
|
47
|
+
rows.forEach( row => {
|
|
48
|
+
const index = Number( row.getAttribute( 'data-row-index' ) );
|
|
49
|
+
// An unparseable index would write a non-numeric key, leaving a hole
|
|
50
|
+
// that silently truncates the scan below at the preceding row.
|
|
51
|
+
if ( ! Number.isInteger( index ) || index < 0 || index >= rowCount ) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const { bottom } = row.getBoundingClientRect();
|
|
55
|
+
rowBottoms[ index ] = Math.max( rowBottoms[ index ] ?? -Infinity, bottom );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
// Fail open. If the rows cannot be measured at all — a changed DOM shape,
|
|
59
|
+
// a detached container — falling back to the scrollable default keeps the
|
|
60
|
+
// data reachable, where hiding every row reads as a broken tile.
|
|
61
|
+
if ( rowBottoms.length === 0 ) {
|
|
62
|
+
setIsMeasurable( false );
|
|
63
|
+
setFittedCount( rowCount );
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setIsMeasurable( true );
|
|
67
|
+
|
|
68
|
+
const contentBottom = content.getBoundingClientRect().bottom + SUBPIXEL_TOLERANCE;
|
|
69
|
+
|
|
70
|
+
let fits = 0;
|
|
71
|
+
while ( fits < rowBottoms.length && rowBottoms[ fits ] <= contentBottom ) {
|
|
72
|
+
fits++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Bail before setState: setState inside a ResizeObserver callback is the
|
|
76
|
+
// usual way this grows a render loop.
|
|
77
|
+
setFittedCount( current => ( current === fits ? current : fits ) );
|
|
78
|
+
}, [ rowCount ] );
|
|
79
|
+
|
|
80
|
+
// Measure after row data changes so geometry updates are caught even when the
|
|
81
|
+
// row count and the grid's overall size stay the same (for example, one label
|
|
82
|
+
// wraps while another unwraps after a same-length data update).
|
|
83
|
+
useLayoutEffect( () => {
|
|
84
|
+
if ( ! enabled ) {
|
|
85
|
+
setFittedCount( rowCount );
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
measure();
|
|
90
|
+
}, [ enabled, rowCount, data, measure ] );
|
|
91
|
+
|
|
92
|
+
useLayoutEffect( () => {
|
|
93
|
+
if ( ! enabled ) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const content = contentRef.current;
|
|
98
|
+
if ( ! content ) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const observer = new ResizeObserver( measure );
|
|
103
|
+
// The container catches tile resizing; the grid catches font, image,
|
|
104
|
+
// theme, or label changes that alter row geometry on an unchanged tile.
|
|
105
|
+
observer.observe( content );
|
|
106
|
+
const grid = content.querySelector< HTMLElement >( ':scope > [data-leaderboard-grid]' );
|
|
107
|
+
if ( grid ) {
|
|
108
|
+
observer.observe( grid );
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return () => observer.disconnect();
|
|
112
|
+
}, [ enabled, rowCount, measure ] );
|
|
113
|
+
|
|
114
|
+
return { contentRef, fittedCount: enabled ? fittedCount : rowCount, isMeasurable };
|
|
115
|
+
}
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
flex: 1;
|
|
17
17
|
min-height: 0;
|
|
18
18
|
overflow: auto;
|
|
19
|
+
|
|
20
|
+
// With fitRows the chart hides the rows that would not fit, so there is
|
|
21
|
+
// nothing left to scroll to — a scrollbar here would only ever reveal the
|
|
22
|
+
// hidden rows' reserved space.
|
|
23
|
+
&--fit {
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
// Anchors .fitEmptyState, which is laid over the hidden rows.
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
|
|
@@ -51,7 +60,7 @@
|
|
|
51
60
|
// row's fixed-pixel hover inset from collapsing a narrow bar to nothing.
|
|
52
61
|
min-width: var(--wpds-dimension-size-5xs, 4px);
|
|
53
62
|
|
|
54
|
-
border-radius: var(--a8c--charts--leaderboard--bar--border-radius, 9999px);
|
|
63
|
+
border-radius: var(--a8c-charts-border-radius-leaderboard-bar, var(--a8c--charts--leaderboard--bar--border-radius, 9999px));
|
|
55
64
|
z-index: -1; /* places it behind the label */
|
|
56
65
|
|
|
57
66
|
&--animated {
|
|
@@ -95,28 +104,47 @@
|
|
|
95
104
|
.emptyState {
|
|
96
105
|
padding: var(--wpds-dimension-padding-3xl, 32px) var(--wpds-dimension-padding-lg, 16px);
|
|
97
106
|
text-align: center;
|
|
98
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
107
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
99
108
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
100
109
|
font-style: italic;
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
.
|
|
104
|
-
|
|
112
|
+
// Shown when not even one row fits. The rows stay in the layout underneath so
|
|
113
|
+
// they remain measurable, so this is taken out of flow to sit over them.
|
|
114
|
+
.fitEmptyState {
|
|
115
|
+
position: absolute;
|
|
116
|
+
inset: 0;
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
// The shared empty state reserves generous vertical whitespace. This overlay
|
|
121
|
+
// is specifically for compact containers, where the text may fit even though
|
|
122
|
+
// a complete data row does not.
|
|
123
|
+
padding-block: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Focus-ring width shared so a standard row's reserved padding and an
|
|
127
|
+
// interactive row's outline (and its inset offset) stay equal. A Sass var,
|
|
128
|
+
// not a custom property: a compile-time constant scoped to this module, so it
|
|
129
|
+
// needs no runtime cascade and never lands on the DOM as an unprefixed var.
|
|
130
|
+
$focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px));
|
|
105
131
|
|
|
132
|
+
.row {
|
|
106
133
|
display: grid;
|
|
107
134
|
grid-column: 1 / -1;
|
|
108
135
|
grid-template-columns: subgrid;
|
|
109
136
|
align-items: center;
|
|
137
|
+
// Every row reserves the same space for the interactive focus ring. Keeping
|
|
138
|
+
// this on the shared wrapper ensures that adding or removing an action never
|
|
139
|
+
// changes the row height or subgrid column edges.
|
|
140
|
+
padding: $focus-ring-width;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.interactiveRow {
|
|
110
144
|
position: relative;
|
|
111
145
|
|
|
112
146
|
// Native button reset.
|
|
113
|
-
box-sizing: border-box;
|
|
114
147
|
appearance: none;
|
|
115
|
-
width: 100%;
|
|
116
|
-
margin: 0;
|
|
117
|
-
// Reserve a ring's worth of space on every edge so the inset focus outline
|
|
118
|
-
// hugs the row without the bar painting over it (see :focus-visible below).
|
|
119
|
-
padding: var(--focus-ring-width);
|
|
120
148
|
border: 0;
|
|
121
149
|
background: none;
|
|
122
150
|
color: inherit;
|
|
@@ -152,14 +180,14 @@
|
|
|
152
180
|
|
|
153
181
|
// Fixed reserve for the chevron — the value slides by it in full, and the
|
|
154
182
|
// bar width inset (see leaderboard-chart.tsx) scales it by the bar's share.
|
|
155
|
-
--a8c
|
|
183
|
+
--a8c-charts-dimension-leaderboard-bar-hover-inset: var(--wpds-dimension-gap-2xl, 32px);
|
|
156
184
|
|
|
157
185
|
.bar {
|
|
158
186
|
opacity: 0.5;
|
|
159
187
|
}
|
|
160
188
|
|
|
161
189
|
.valueContainer {
|
|
162
|
-
transform: translateX(calc(var(--a8c
|
|
190
|
+
transform: translateX(calc(var(--a8c-charts-dimension-leaderboard-bar-hover-inset) * -1));
|
|
163
191
|
}
|
|
164
192
|
|
|
165
193
|
.chevron {
|
|
@@ -170,19 +198,18 @@
|
|
|
170
198
|
// In RTL the value slides toward the inline start the other way (rightward).
|
|
171
199
|
[dir="rtl"] &:hover .valueContainer,
|
|
172
200
|
[dir="rtl"] &:focus-visible .valueContainer {
|
|
173
|
-
transform: translateX(var(--a8c
|
|
201
|
+
transform: translateX(var(--a8c-charts-dimension-leaderboard-bar-hover-inset));
|
|
174
202
|
}
|
|
175
203
|
|
|
176
204
|
&:focus-visible {
|
|
177
205
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
178
206
|
outline:
|
|
179
|
-
|
|
180
|
-
var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
|
|
207
|
+
$focus-ring-width solid
|
|
208
|
+
var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
|
|
181
209
|
// Inset by its own width so the outer edge hugs the row boundary: the
|
|
182
|
-
// outline then sits in the row
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
outline-offset: calc(-1 * var(--focus-ring-width));
|
|
210
|
+
// outline then sits in the shared row padding, clear of the bar, and stays
|
|
211
|
+
// inside the overflow:auto container that would clip an outset ring.
|
|
212
|
+
outline-offset: calc(-1 * #{$focus-ring-width});
|
|
186
213
|
}
|
|
187
214
|
}
|
|
188
215
|
|
|
@@ -193,7 +220,7 @@
|
|
|
193
220
|
bottom: 0;
|
|
194
221
|
margin-block: auto;
|
|
195
222
|
opacity: 0;
|
|
196
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
223
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
197
224
|
transition:
|
|
198
225
|
opacity var(--wpds-motion-duration-sm, 100ms)
|
|
199
226
|
var(--wpds-motion-easing-subtle, cubic-bezier(0.15, 0, 0.15, 1));
|