@eifi1/ui-kit 0.10.0 → 0.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/README.md +20 -16
- package/dist/components/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -0
- package/dist/components/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +58 -3
- package/dist/components/button-group.js +56 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +2 -0
- package/dist/components/calendar-heatmap.d.ts +124 -0
- package/dist/components/calendar-heatmap.js +295 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/choice-card.d.ts +34 -2
- package/dist/components/choice-card.js +49 -0
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -0
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/field.d.ts +43 -0
- package/dist/components/field.js +52 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +1 -0
- package/dist/components/file-dropzone.d.ts +1 -0
- package/dist/components/floating-panel.d.ts +147 -4
- package/dist/components/floating-panel.js +188 -28
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/list.d.ts +14 -1
- package/dist/components/list.js +7 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/menu-item.d.ts +11 -1
- package/dist/components/menu-item.js +6 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +70 -5
- package/dist/components/mini-calendar.js +166 -62
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/nav-pills.d.ts +66 -0
- package/dist/components/nav-pills.js +65 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +2 -0
- package/dist/components/number-input.d.ts +2 -0
- package/dist/components/numpad-sheet.d.ts +2 -0
- package/dist/components/progress-bar.d.ts +31 -2
- package/dist/components/progress-bar.js +73 -7
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -0
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +40 -8
- package/dist/components/table.js +41 -7
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -0
- package/dist/components/toggle-group.d.ts +15 -0
- package/dist/components/toggle-group.js +23 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +55 -4
- package/dist/components/ui.js +84 -7
- package/dist/components/ui.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -0
- package/dist/i18n/defaults.d.ts +2 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
- package/dist/i18n/locales/de-CH.d.ts +2 -0
- package/dist/i18n/locales/de-informal.d.ts +2 -0
- package/dist/i18n/locales/de.d.ts +2 -0
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -0
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -0
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -0
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -0
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +14 -10
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/rhf.js +1 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +73 -3
- package/dist/shell/topbar-action-menu.js +100 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +1 -0
- package/dist/wizard/wizard-context.d.ts +7 -1
- package/dist/wizard/wizard-context.js +4 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +36 -7
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +124 -4
- package/src/components/calendar-heatmap.tsx +504 -0
- package/src/components/choice-card.tsx +97 -1
- package/src/components/data-table.tsx +7 -0
- package/src/components/field.tsx +137 -0
- package/src/components/floating-panel.tsx +366 -15
- package/src/components/list.tsx +28 -2
- package/src/components/menu-item.tsx +22 -1
- package/src/components/mini-calendar.tsx +245 -54
- package/src/components/nav-pills.tsx +145 -0
- package/src/components/progress-bar.tsx +129 -11
- package/src/components/series-chart.tsx +21 -0
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +85 -11
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +48 -3
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +149 -11
- package/src/components/user-avatar.tsx +57 -3
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -0
- package/src/i18n/locales/de.ts +9 -0
- package/src/i18n/locales/es.ts +9 -0
- package/src/i18n/locales/fr.ts +9 -0
- package/src/i18n/locales/hu.ts +9 -0
- package/src/i18n/locales/it.ts +9 -0
- package/src/i18n/locales/zh.ts +9 -0
- package/src/index.ts +5 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +2 -0
- package/src/shell/topbar-action-menu.tsx +239 -49
- package/src/wizard/wizard-context.tsx +9 -0
|
@@ -41,6 +41,49 @@ const FILL: Record<ProgressBarTone, string> = {
|
|
|
41
41
|
|
|
42
42
|
const TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: "h-1", slim: "h-1.5", md: "h-2", lg: "h-3" };
|
|
43
43
|
|
|
44
|
+
// A segment with no tone of its own takes the next categorical chart colour, so a
|
|
45
|
+
// stacked bar of unnamed parts is still a set of tellable parts rather than one fill.
|
|
46
|
+
const SERIES_FILL = [
|
|
47
|
+
"bg-[var(--chart-1)]",
|
|
48
|
+
"bg-[var(--chart-2)]",
|
|
49
|
+
"bg-[var(--chart-3)]",
|
|
50
|
+
"bg-[var(--chart-4)]",
|
|
51
|
+
"bg-[var(--chart-5)]",
|
|
52
|
+
"bg-[var(--chart-6)]",
|
|
53
|
+
"bg-[var(--chart-7)]",
|
|
54
|
+
"bg-[var(--chart-8)]",
|
|
55
|
+
"bg-[var(--chart-9)]",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** Each segment's fill classes: its own classes, else its tone, else (with no `color`)
|
|
59
|
+
* the next chart colour. "Next" counts the UNCOLOURED segments only — a segment with
|
|
60
|
+
* its own `className`/`tone`/`color` does not use up `--chart-1`, so an "Other" in grey
|
|
61
|
+
* ahead of two plain parts leaves them `--chart-1` and `--chart-2`. */
|
|
62
|
+
function segmentFills(segments: ProgressBarSegment[]): Array<string | undefined> {
|
|
63
|
+
let next = 0;
|
|
64
|
+
return segments.map((seg) => {
|
|
65
|
+
if (seg.className) return seg.className;
|
|
66
|
+
if (seg.tone) return FILL[seg.tone];
|
|
67
|
+
return seg.color ? undefined : SERIES_FILL[next++ % SERIES_FILL.length];
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One part of a stacked bar. See {@link ProgressBarProps.segments}. */
|
|
72
|
+
export interface ProgressBarSegment {
|
|
73
|
+
/** Its size, on the bar's own `min`–`max` scale. */
|
|
74
|
+
value: number;
|
|
75
|
+
/** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */
|
|
76
|
+
tone?: ProgressBarTone;
|
|
77
|
+
/** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */
|
|
78
|
+
color?: string;
|
|
79
|
+
/** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */
|
|
80
|
+
className?: string;
|
|
81
|
+
/** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */
|
|
82
|
+
label?: ReactNode;
|
|
83
|
+
/** A stable React key, when `label` is not a string. */
|
|
84
|
+
key?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
44
87
|
export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
45
88
|
/** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
|
|
46
89
|
* and nobody knows how much is left (an import that reports no count). */
|
|
@@ -69,6 +112,20 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
69
112
|
tone?: ProgressBarTone;
|
|
70
113
|
size?: ProgressBarSize;
|
|
71
114
|
locale?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap
|
|
117
|
+
* between them — keksdose's discretionary-spending bar (cut-card:133), a whole
|
|
118
|
+
* split into the steps of a scale. Forces `variant="meter"`: parts of a whole are
|
|
119
|
+
* never "busy". `aria-valuenow` is the sum; `aria-valuetext` names every part
|
|
120
|
+
* ("Rent: 40%, Food: 25%"), each part's value through `formatValue` and joined in the
|
|
121
|
+
* kit's locale — the words keksdose's hand-drawn bar had to hide from a reader
|
|
122
|
+
* altogether, leaving the legend as the only way in. The sum is clamped to `max`,
|
|
123
|
+
* and parts past it are cut off at the track's end.
|
|
124
|
+
*/
|
|
125
|
+
segments?: ProgressBarSegment[];
|
|
126
|
+
/** With `segments`: a list under the bar naming each part with its swatch and value
|
|
127
|
+
* — the table view of the bar, in text tokens rather than the segment colour. */
|
|
128
|
+
legend?: boolean;
|
|
72
129
|
}
|
|
73
130
|
|
|
74
131
|
/**
|
|
@@ -99,6 +156,8 @@ export function ProgressBar({
|
|
|
99
156
|
tone = "brand",
|
|
100
157
|
size = "md",
|
|
101
158
|
locale,
|
|
159
|
+
segments,
|
|
160
|
+
legend = false,
|
|
102
161
|
className,
|
|
103
162
|
"aria-label": ariaLabel,
|
|
104
163
|
"aria-labelledby": ariaLabelledBy,
|
|
@@ -108,19 +167,37 @@ export function ProgressBar({
|
|
|
108
167
|
const kitLocale = useKitLocale(locale);
|
|
109
168
|
const labelId = useId();
|
|
110
169
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const indeterminate = current === undefined;
|
|
170
|
+
const stacked = segments !== undefined;
|
|
171
|
+
const meter = stacked || variant === "meter";
|
|
114
172
|
const span = max - min;
|
|
173
|
+
const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);
|
|
174
|
+
const fills = stacked ? segmentFills(segments) : [];
|
|
175
|
+
const partTotal = stacked ? min + segments.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;
|
|
176
|
+
const format = (v: number) =>
|
|
177
|
+
formatValue
|
|
178
|
+
? formatValue(v, max, min)
|
|
179
|
+
: new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(
|
|
180
|
+
span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,
|
|
181
|
+
);
|
|
182
|
+
const partTexts = stacked
|
|
183
|
+
? segments.map((seg) => {
|
|
184
|
+
const text = format(min + partValue(seg.value));
|
|
185
|
+
return typeof seg.label === "string" || typeof seg.label === "number"
|
|
186
|
+
? common.fieldValue(String(seg.label), text)
|
|
187
|
+
: text;
|
|
188
|
+
})
|
|
189
|
+
: [];
|
|
190
|
+
const current =
|
|
191
|
+
partTotal ?? (value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value);
|
|
192
|
+
const indeterminate = current === undefined;
|
|
115
193
|
const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));
|
|
116
194
|
const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;
|
|
117
195
|
|
|
118
|
-
const valueText =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
:
|
|
122
|
-
|
|
123
|
-
: new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(fraction);
|
|
196
|
+
const valueText = clamped === undefined ? undefined : format(clamped);
|
|
197
|
+
const ariaValueText =
|
|
198
|
+
stacked && partTexts.length > 0
|
|
199
|
+
? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts)
|
|
200
|
+
: valueText;
|
|
124
201
|
|
|
125
202
|
const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);
|
|
126
203
|
const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));
|
|
@@ -151,13 +228,32 @@ export function ProgressBar({
|
|
|
151
228
|
aria-valuemin={indeterminate ? undefined : min}
|
|
152
229
|
aria-valuemax={indeterminate ? undefined : max}
|
|
153
230
|
aria-valuenow={clamped}
|
|
154
|
-
aria-valuetext={
|
|
231
|
+
aria-valuetext={ariaValueText}
|
|
155
232
|
aria-busy={!meter && indeterminate ? true : undefined}
|
|
156
233
|
// `relative` + `overflow-hidden`: the indeterminate segment is positioned in
|
|
157
234
|
// here and must not paint past the rounded ends.
|
|
158
235
|
className={cn("relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]", TRACK_HEIGHT[size])}
|
|
159
236
|
>
|
|
160
|
-
{
|
|
237
|
+
{stacked ? (
|
|
238
|
+
// The parts in a flex row with a 2px gap, so two neighbouring steps of one
|
|
239
|
+
// hue are parted by the track rather than by their difference in lightness.
|
|
240
|
+
<div className="flex h-full w-full gap-0.5">
|
|
241
|
+
{segments.map((seg, i) => (
|
|
242
|
+
<div
|
|
243
|
+
key={seg.key ?? i}
|
|
244
|
+
data-part="segment"
|
|
245
|
+
className={cn(
|
|
246
|
+
"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
|
|
247
|
+
fills[i],
|
|
248
|
+
)}
|
|
249
|
+
style={{
|
|
250
|
+
width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,
|
|
251
|
+
backgroundColor: seg.className ? undefined : seg.color,
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
) : indeterminate ? (
|
|
161
257
|
<div
|
|
162
258
|
data-part="fill"
|
|
163
259
|
className={cn(
|
|
@@ -182,6 +278,28 @@ export function ProgressBar({
|
|
|
182
278
|
/>
|
|
183
279
|
)}
|
|
184
280
|
</div>
|
|
281
|
+
{stacked && legend && segments.length > 0 && (
|
|
282
|
+
<ul className="mt-2 space-y-1 text-sm" data-part="legend">
|
|
283
|
+
{segments.map((seg, i) => (
|
|
284
|
+
<li key={seg.key ?? i} className="flex items-center gap-2">
|
|
285
|
+
<span
|
|
286
|
+
aria-hidden
|
|
287
|
+
className={cn(
|
|
288
|
+
"size-2.5 shrink-0 rounded-sm",
|
|
289
|
+
fills[i],
|
|
290
|
+
)}
|
|
291
|
+
style={{ backgroundColor: seg.className ? undefined : seg.color }}
|
|
292
|
+
/>
|
|
293
|
+
{seg.label != null && (
|
|
294
|
+
<span className="min-w-0 flex-1 truncate text-[var(--text-secondary)]">{seg.label}</span>
|
|
295
|
+
)}
|
|
296
|
+
<span className="ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]">
|
|
297
|
+
{format(min + partValue(seg.value))}
|
|
298
|
+
</span>
|
|
299
|
+
</li>
|
|
300
|
+
))}
|
|
301
|
+
</ul>
|
|
302
|
+
)}
|
|
185
303
|
</div>
|
|
186
304
|
);
|
|
187
305
|
}
|
|
@@ -129,6 +129,18 @@ export interface SeriesChartSeries {
|
|
|
129
129
|
* `{ r }` sets its radius. Default: recharts' dot. Bars have none, and ignore it.
|
|
130
130
|
*/
|
|
131
131
|
activeDot?: boolean | { r?: number };
|
|
132
|
+
/**
|
|
133
|
+
* The shortest a bar of this series is drawn, in px — recharts' `minPointSize`. A
|
|
134
|
+
* zero or a value too small to see keeps a visible stub, so an empty slot still reads
|
|
135
|
+
* as a slot with a value rather than a gap: keksdose's weekday pattern, where a
|
|
136
|
+
* Sunday with no spending drew a 2% stub by hand. Only the DRAWING changes: the axis
|
|
137
|
+
* is fitted to the values, and the tooltip, the keyboard stops and every accessible
|
|
138
|
+
* name still say 0. A hole (no value in the row) stays a hole. Overrides the chart's
|
|
139
|
+
* {@link SeriesChartProps.minBarLength}; `0` turns it off for this series. Meant for
|
|
140
|
+
* unstacked bars — in a `stack` the stub is drawn over the layer above it. Lines and
|
|
141
|
+
* areas ignore it.
|
|
142
|
+
*/
|
|
143
|
+
minBarLength?: number;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
/** What a series draws. See {@link SeriesChartSeries.type}. */
|
|
@@ -507,6 +519,10 @@ export interface SeriesChartProps {
|
|
|
507
519
|
locale?: string;
|
|
508
520
|
/** Onto the chart's root (`ChartContainer`) — e.g. an app's own grid ink. */
|
|
509
521
|
className?: string;
|
|
522
|
+
/** The shortest bar drawn, in px, for every bar series that does not set its own
|
|
523
|
+
* {@link SeriesChartSeries.minBarLength}. Drawing only: domain, tooltip and names
|
|
524
|
+
* keep the real value. Default: none — a zero draws nothing. */
|
|
525
|
+
minBarLength?: number;
|
|
510
526
|
}
|
|
511
527
|
|
|
512
528
|
/** How much of an axis band the rotated title takes, in px. Reserved rather than
|
|
@@ -1011,6 +1027,7 @@ function SeriesPlot({
|
|
|
1011
1027
|
labels: labelsProp,
|
|
1012
1028
|
locale: localeProp,
|
|
1013
1029
|
className,
|
|
1030
|
+
minBarLength,
|
|
1014
1031
|
}: PlotProps) {
|
|
1015
1032
|
const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, labelsProp);
|
|
1016
1033
|
const locale = useKitLocale(localeProp);
|
|
@@ -1418,6 +1435,7 @@ function SeriesPlot({
|
|
|
1418
1435
|
const curve = entry.step ? "stepAfter" : (entry.curve ?? "monotone");
|
|
1419
1436
|
const width = entry.strokeWidth ?? (entry.step ? STEP_WIDTH : LINE_WIDTH);
|
|
1420
1437
|
if (type === "bar") {
|
|
1438
|
+
const stub = entry.minBarLength ?? minBarLength;
|
|
1421
1439
|
return (
|
|
1422
1440
|
<Bar
|
|
1423
1441
|
key={entry.key}
|
|
@@ -1427,6 +1445,9 @@ function SeriesPlot({
|
|
|
1427
1445
|
fill={color}
|
|
1428
1446
|
fillOpacity={entry.fillOpacity}
|
|
1429
1447
|
radius={entry.stack === undefined ? BAR_RADIUS : 0}
|
|
1448
|
+
// Read off the row rather than recharts' `value`: in a stack that is the
|
|
1449
|
+
// layer's running top, which a hole has too — and a hole gets no stub.
|
|
1450
|
+
{...(stub ? { minPointSize: (_value: unknown, index: number) => (finite(plotted[index]?.[entry.key]) === undefined ? 0 : stub) } : {})}
|
|
1430
1451
|
// Only the focused stop's series swaps its shape, and only for as long
|
|
1431
1452
|
// as it has focus: every other bar keeps recharts' own.
|
|
1432
1453
|
{...(focusedStop?.key === entry.key ? { shape: focusedBarShape(focusedStop.index) } : {})}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
3
|
import type { ProgressBarTone } from "./progress-bar";
|
|
4
|
+
import type { ChipHue } from "./chip";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The tones are `ProgressBar`'s, name for name — which are `Chip`'s, less nothing — so a
|
|
7
8
|
* dot in a legend, the bar it explains and the badge beside it can be given the same
|
|
8
9
|
* word and agree. The money pair is here for the same reason it is there: a legend
|
|
9
|
-
* swatch for income is money, not "success".
|
|
10
|
+
* swatch for income is money, not "success". Chip's categorical hues are here too
|
|
11
|
+
* (0.11): a unit-status dot in a combobox (kastlan lease-unit-step.tsx:35, "rented"
|
|
12
|
+
* is blue) must match the Chip that states the same status elsewhere.
|
|
10
13
|
*/
|
|
11
|
-
export type StatusDotTone = ProgressBarTone;
|
|
14
|
+
export type StatusDotTone = ProgressBarTone | ChipHue;
|
|
12
15
|
|
|
13
16
|
/** `sm` 8px (keksdose's unread dot on the support trigger), `md` 10px (the avatar's
|
|
14
17
|
* unread dot), `lg` 12px (kastlan's occupancy legend swatch). */
|
|
@@ -23,6 +26,11 @@ const FILL: Record<StatusDotTone, string> = {
|
|
|
23
26
|
info: "bg-[var(--info)]",
|
|
24
27
|
income: "bg-[var(--money-income)]",
|
|
25
28
|
expense: "bg-[var(--money-expense)]",
|
|
29
|
+
blue: "bg-[var(--hue-blue)]",
|
|
30
|
+
indigo: "bg-[var(--hue-indigo)]",
|
|
31
|
+
purple: "bg-[var(--hue-purple)]",
|
|
32
|
+
teal: "bg-[var(--hue-teal)]",
|
|
33
|
+
orange: "bg-[var(--hue-orange)]",
|
|
26
34
|
};
|
|
27
35
|
|
|
28
36
|
const SIZE: Record<StatusDotSize, string> = { sm: "size-2", md: "size-2.5", lg: "size-3" };
|
package/src/components/table.tsx
CHANGED
|
@@ -13,6 +13,21 @@ export type TableDensity = "comfortable" | "compact" | "none";
|
|
|
13
13
|
* figures for room. */
|
|
14
14
|
export type TableLayout = "auto" | "fixed";
|
|
15
15
|
export type TableAlign = "start" | "center" | "end";
|
|
16
|
+
/**
|
|
17
|
+
* Vertical alignment of a cell's content (0.11.0). The defaults stay as they were — a
|
|
18
|
+
* head cell sits on the `bottom` (a wrapped heading ends on the line above the
|
|
19
|
+
* figures), a body cell at the `top` (a wrapped note starts level with its row). A
|
|
20
|
+
* table whose cells are INPUTS wants `middle`: keksdose's VAT summary
|
|
21
|
+
* (invoices/vat-summary.tsx) sets fields beside derived read-out figures in one row,
|
|
22
|
+
* and a figure at the top of its cell sat above the middle of the field next to it —
|
|
23
|
+
* so it writes `<tr className="align-middle">` over raw `<th>`/`<td>`, because the
|
|
24
|
+
* kit's cells each stated `align-top` and a class on the row could not reach them.
|
|
25
|
+
*/
|
|
26
|
+
export type TableVAlign = "top" | "middle" | "bottom";
|
|
27
|
+
/** The head cell's type size: `xs` (the head's default) or `sm` (the body's). */
|
|
28
|
+
export type TableHeaderCellSize = "xs" | "sm";
|
|
29
|
+
/** The header cell's weight. Default `medium`. */
|
|
30
|
+
export type TableHeaderCellWeight = "normal" | "medium" | "semibold";
|
|
16
31
|
|
|
17
32
|
interface TableContextValue {
|
|
18
33
|
density: TableDensity;
|
|
@@ -33,6 +48,28 @@ const TableContext = createContext<TableContextValue>({
|
|
|
33
48
|
/** Which section a row sits in — zebra and hover apply to body rows only. */
|
|
34
49
|
const SectionContext = createContext<"head" | "body" | "foot">("body");
|
|
35
50
|
|
|
51
|
+
/** A {@link TableRow}'s `valign`, which its cells take unless they set their own. It
|
|
52
|
+
* cannot be left to CSS inheritance: every cell states its own default alignment,
|
|
53
|
+
* and a class on the cell beats one on the row. */
|
|
54
|
+
const RowVAlignContext = createContext<TableVAlign | undefined>(undefined);
|
|
55
|
+
|
|
56
|
+
const VALIGN: Record<TableVAlign, string> = {
|
|
57
|
+
top: "align-top",
|
|
58
|
+
middle: "align-middle",
|
|
59
|
+
bottom: "align-bottom",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const HEADER_SIZE: Record<TableHeaderCellSize, string> = {
|
|
63
|
+
xs: "text-xs",
|
|
64
|
+
sm: "text-sm",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const HEADER_WEIGHT: Record<TableHeaderCellWeight, string> = {
|
|
68
|
+
normal: "font-normal",
|
|
69
|
+
medium: "font-medium",
|
|
70
|
+
semibold: "font-semibold",
|
|
71
|
+
};
|
|
72
|
+
|
|
36
73
|
const CELL_PAD: Record<TableDensity, string> = {
|
|
37
74
|
comfortable: "px-3 py-2",
|
|
38
75
|
compact: "px-2 py-1",
|
|
@@ -125,6 +162,7 @@ export function Table({
|
|
|
125
162
|
{...(overflowing ? regionName : null)}
|
|
126
163
|
tabIndex={overflowing ? 0 : undefined}
|
|
127
164
|
data-overflowing={overflowing || undefined}
|
|
165
|
+
data-clips=""
|
|
128
166
|
className={cn(
|
|
129
167
|
// `relative`: the containing block for an `sr-only` caption.
|
|
130
168
|
"relative w-full overflow-x-auto",
|
|
@@ -252,23 +290,31 @@ export function TableFoot({ className, ...rest }: TableFootProps) {
|
|
|
252
290
|
);
|
|
253
291
|
}
|
|
254
292
|
|
|
255
|
-
|
|
293
|
+
/** The deprecated HTML `valign` attribute is replaced by a class-backed one. */
|
|
294
|
+
export interface TableRowProps extends ComponentPropsWithoutRef<"tr"> {
|
|
295
|
+
/** Vertical alignment for every cell of the row that does not set its own. See
|
|
296
|
+
* {@link TableVAlign}. Left out, each cell keeps its default. */
|
|
297
|
+
valign?: TableVAlign;
|
|
298
|
+
}
|
|
256
299
|
|
|
257
|
-
export function TableRow({ className, ...rest }: TableRowProps) {
|
|
300
|
+
export function TableRow({ valign, className, ...rest }: TableRowProps) {
|
|
258
301
|
const { zebra, hover } = useContext(TableContext);
|
|
259
302
|
const section = useContext(SectionContext);
|
|
260
303
|
const body = section === "body";
|
|
261
|
-
|
|
304
|
+
const row = (
|
|
262
305
|
<tr
|
|
263
306
|
{...rest}
|
|
264
307
|
className={cn(
|
|
265
308
|
body && "border-b border-[var(--border)]",
|
|
266
309
|
body && zebra && "even:bg-[var(--bg-surface-2)]",
|
|
267
310
|
body && hover && "transition-colors hover:bg-[var(--bg-hover)]",
|
|
311
|
+
// On the row as well, for a raw `<td>` of the caller's, which inherits it.
|
|
312
|
+
valign && VALIGN[valign],
|
|
268
313
|
className,
|
|
269
314
|
)}
|
|
270
315
|
/>
|
|
271
316
|
);
|
|
317
|
+
return valign ? <RowVAlignContext.Provider value={valign}>{row}</RowVAlignContext.Provider> : row;
|
|
272
318
|
}
|
|
273
319
|
|
|
274
320
|
interface CellAlignProps {
|
|
@@ -276,19 +322,44 @@ interface CellAlignProps {
|
|
|
276
322
|
numeric?: boolean;
|
|
277
323
|
/** Logical alignment. Default `start` (`end` when `numeric`). */
|
|
278
324
|
align?: TableAlign;
|
|
325
|
+
/** Vertical alignment. Default: the row's `valign`, else `bottom` for a head cell and
|
|
326
|
+
* `top` for a body cell. See {@link TableVAlign}. */
|
|
327
|
+
valign?: TableVAlign;
|
|
279
328
|
}
|
|
280
329
|
|
|
281
|
-
/** The deprecated HTML `align`
|
|
282
|
-
export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align">, CellAlignProps {
|
|
330
|
+
/** The deprecated HTML `align` and `valign` attributes are replaced by class-backed ones. */
|
|
331
|
+
export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align" | "valign">, CellAlignProps {
|
|
332
|
+
/**
|
|
333
|
+
* Type size. Left out: `xs` in the head, the table's own size in the body — as
|
|
334
|
+
* before. keksdose's VAT summary heads its columns at the body size and normal weight
|
|
335
|
+
* (vat-summary.tsx:158–168, `font-normal` on each `<th>`), a quiet header over a
|
|
336
|
+
* small table of figures, and could not say so without overriding classes.
|
|
337
|
+
*/
|
|
338
|
+
size?: TableHeaderCellSize;
|
|
339
|
+
/** Default `medium`. `normal` for the quiet header above; see {@link size}. */
|
|
340
|
+
weight?: TableHeaderCellWeight;
|
|
341
|
+
}
|
|
283
342
|
|
|
284
343
|
/**
|
|
285
344
|
* A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header
|
|
286
345
|
* cell in a body row is the row's label ("Net rent" in a key-value table), and a
|
|
287
346
|
* reader needs the scope to read it with each cell beside it.
|
|
288
347
|
*/
|
|
289
|
-
export function TableHeaderCell({
|
|
348
|
+
export function TableHeaderCell({
|
|
349
|
+
numeric = false,
|
|
350
|
+
align,
|
|
351
|
+
valign,
|
|
352
|
+
size,
|
|
353
|
+
weight = "medium",
|
|
354
|
+
scope,
|
|
355
|
+
className,
|
|
356
|
+
...rest
|
|
357
|
+
}: TableHeaderCellProps) {
|
|
290
358
|
const { density } = useContext(TableContext);
|
|
291
359
|
const section = useContext(SectionContext);
|
|
360
|
+
const rowVAlign = useContext(RowVAlignContext);
|
|
361
|
+
const head = section === "head";
|
|
362
|
+
const textSize = size ?? (head ? "xs" : undefined);
|
|
292
363
|
return (
|
|
293
364
|
<th
|
|
294
365
|
{...rest}
|
|
@@ -297,24 +368,27 @@ export function TableHeaderCell({ numeric = false, align, scope, className, ...r
|
|
|
297
368
|
CELL_PAD[density],
|
|
298
369
|
ALIGN[align ?? (numeric ? "end" : "start")],
|
|
299
370
|
numeric && "tabular-nums",
|
|
300
|
-
"
|
|
301
|
-
|
|
371
|
+
VALIGN[valign ?? rowVAlign ?? (head ? "bottom" : "top")],
|
|
372
|
+
HEADER_WEIGHT[weight],
|
|
373
|
+
head ? "text-[var(--text-muted)]" : "text-[var(--text-secondary)]",
|
|
374
|
+
textSize && HEADER_SIZE[textSize],
|
|
302
375
|
className,
|
|
303
376
|
)}
|
|
304
377
|
/>
|
|
305
378
|
);
|
|
306
379
|
}
|
|
307
380
|
|
|
308
|
-
export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align">, CellAlignProps {}
|
|
381
|
+
export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align" | "valign">, CellAlignProps {}
|
|
309
382
|
|
|
310
|
-
export function TableCell({ numeric = false, align, className, ...rest }: TableCellProps) {
|
|
383
|
+
export function TableCell({ numeric = false, align, valign, className, ...rest }: TableCellProps) {
|
|
311
384
|
const { density } = useContext(TableContext);
|
|
385
|
+
const rowVAlign = useContext(RowVAlignContext);
|
|
312
386
|
return (
|
|
313
387
|
<td
|
|
314
388
|
{...rest}
|
|
315
389
|
className={cn(
|
|
316
390
|
CELL_PAD[density],
|
|
317
|
-
"
|
|
391
|
+
VALIGN[valign ?? rowVAlign ?? "top"],
|
|
318
392
|
ALIGN[align ?? (numeric ? "end" : "start")],
|
|
319
393
|
numeric && "tabular-nums whitespace-nowrap",
|
|
320
394
|
className,
|
package/src/components/text.tsx
CHANGED
|
@@ -9,8 +9,15 @@ import { cn } from "../lib/cn";
|
|
|
9
9
|
* (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a
|
|
10
10
|
* dense panel (a legend, a menu) or above a block of page content, and one size looked
|
|
11
11
|
* wrong in the other place.
|
|
12
|
+
*
|
|
13
|
+
* `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels
|
|
14
|
+
* its figures ("Assigned", "Activity", "Upcoming", "Available" —
|
|
15
|
+
* budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line
|
|
16
|
+
* (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a
|
|
17
|
+
* figure that size, 12px competed with it. Named `md` rather than slotted in order
|
|
18
|
+
* because `sm` was already the default and renaming it would move every caller.
|
|
12
19
|
*/
|
|
13
|
-
export type SectionLabelSize = "xs" | "sm";
|
|
20
|
+
export type SectionLabelSize = "xs" | "md" | "sm";
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* The small, uppercase, tracked, muted label — as a class string, per size.
|
|
@@ -23,6 +30,7 @@ export type SectionLabelSize = "xs" | "sm";
|
|
|
23
30
|
*/
|
|
24
31
|
export const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {
|
|
25
32
|
xs: "text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
33
|
+
md: "text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
26
34
|
sm: "text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
27
35
|
};
|
|
28
36
|
|
|
@@ -76,11 +76,25 @@ export interface ToggleGroupBaseProps<T extends string>
|
|
|
76
76
|
error?: ReactNode;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* A line under the group saying what the CHOSEN option means — lenkbank's ToggleField
|
|
81
|
+
* `hint` (features/gear/common.tsx:97): not help behind a "?" but a caption, and one
|
|
82
|
+
* that changes as the choice does. Pass a function of the value for that; it is
|
|
83
|
+
* attached with `aria-describedby`, and a function caption is also a polite live
|
|
84
|
+
* region, because a description is read when the group is entered and not again when
|
|
85
|
+
* an arrow key changes the choice underneath it.
|
|
86
|
+
*
|
|
87
|
+
* In muted 11px text under the field (or the bare group), above an `error`.
|
|
88
|
+
*/
|
|
89
|
+
type ToggleGroupCaption<V> = ReactNode | ((value: V) => ReactNode);
|
|
90
|
+
|
|
79
91
|
/** The group as it has always been: one option is always the answer. */
|
|
80
92
|
export interface ToggleGroupRequiredProps<T extends string> extends ToggleGroupBaseProps<T> {
|
|
81
93
|
allowEmpty?: false;
|
|
82
94
|
value: T;
|
|
83
95
|
onChange: (value: T) => void;
|
|
96
|
+
/** See {@link ToggleGroupCaption}. */
|
|
97
|
+
caption?: ToggleGroupCaption<T>;
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
/**
|
|
@@ -97,6 +111,8 @@ export interface ToggleGroupClearableProps<T extends string> extends ToggleGroup
|
|
|
97
111
|
allowEmpty: true;
|
|
98
112
|
value: T | null;
|
|
99
113
|
onChange: (value: T | null) => void;
|
|
114
|
+
/** See {@link ToggleGroupCaption}. `null` while nothing is chosen. */
|
|
115
|
+
caption?: ToggleGroupCaption<T | null>;
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
/** `allowEmpty` picks the shape, so `onChange` is typed `(T) => void` unless the group
|
|
@@ -138,7 +154,11 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
138
154
|
const hasError = field && error !== undefined && error !== null && error !== false && error !== "";
|
|
139
155
|
// Taken off the rest so neither reaches the DOM; `props` keeps them paired, which is
|
|
140
156
|
// what lets the `onChange` below be called with `null` only in the mode that allows it.
|
|
141
|
-
const { allowEmpty: _allowEmpty, onChange: _onChange, ...rest } = restWithMode;
|
|
157
|
+
const { allowEmpty: _allowEmpty, onChange: _onChange, caption, ...rest } = restWithMode;
|
|
158
|
+
const captionId = useId();
|
|
159
|
+
const captionIsLive = typeof caption === "function";
|
|
160
|
+
const captionNode = captionIsLive ? (caption as (v: T | null) => ReactNode)(value) : caption;
|
|
161
|
+
const hasCaption = captionNode !== undefined && captionNode !== null && captionNode !== false && captionNode !== "";
|
|
142
162
|
const choose = (next: T) => {
|
|
143
163
|
if (props.allowEmpty) props.onChange(next === value ? null : next);
|
|
144
164
|
else props.onChange(next);
|
|
@@ -185,7 +205,7 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
185
205
|
aria-labelledby={field && ariaLabelAttr === undefined && ariaLabel === undefined ? labelId : rest["aria-labelledby"]}
|
|
186
206
|
aria-invalid={hasError || rest["aria-invalid"] || undefined}
|
|
187
207
|
aria-describedby={
|
|
188
|
-
|
|
208
|
+
[rest["aria-describedby"], hasCaption && captionId, hasError && errorId].filter(Boolean).join(" ") || undefined
|
|
189
209
|
}
|
|
190
210
|
// `aria-disabled` on the group as well as `disabled` on each button: a radio
|
|
191
211
|
// group is what the user is being refused, and a screen reader announcing
|
|
@@ -273,7 +293,31 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
273
293
|
})}
|
|
274
294
|
</div>
|
|
275
295
|
);
|
|
276
|
-
|
|
296
|
+
// Rendered whenever the caption is — `aria-live` must be on the element BEFORE its
|
|
297
|
+
// text changes, or the change is not announced — but empty (no height, no margin)
|
|
298
|
+
// when there is nothing to say, so a function caption that returns null for some
|
|
299
|
+
// options leaves no gap. Empty rather than `hidden`: some readers do not announce
|
|
300
|
+
// text that appears inside an element coming back from `display: none`.
|
|
301
|
+
const captionEl =
|
|
302
|
+
hasCaption || captionIsLive ? (
|
|
303
|
+
<p
|
|
304
|
+
id={captionId}
|
|
305
|
+
aria-live={captionIsLive ? "polite" : undefined}
|
|
306
|
+
className={cn("text-[11px] leading-snug text-[var(--text-muted)]", hasCaption && "mt-1")}
|
|
307
|
+
>
|
|
308
|
+
{hasCaption ? captionNode : null}
|
|
309
|
+
</p>
|
|
310
|
+
) : null;
|
|
311
|
+
if (!field) {
|
|
312
|
+
if (!captionEl) return group;
|
|
313
|
+
// The group keeps its `className`, as without a caption; the wrapper only stacks.
|
|
314
|
+
return (
|
|
315
|
+
<div className="min-w-0">
|
|
316
|
+
{group}
|
|
317
|
+
{captionEl}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
277
321
|
return (
|
|
278
322
|
// `h-full` + `flex-1`: the chrome fills a grid or stretched flex row, which is what
|
|
279
323
|
// levels it with a select beside it whatever the browser makes of the select.
|
|
@@ -294,6 +338,7 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
294
338
|
{group}
|
|
295
339
|
</div>
|
|
296
340
|
</FloatingField>
|
|
341
|
+
{captionEl}
|
|
297
342
|
{hasError && (
|
|
298
343
|
<p id={errorId} className="mt-1 text-[11px] leading-tight text-[var(--danger)]">
|
|
299
344
|
{error}
|
|
@@ -17,6 +17,13 @@ import { useAnchoredRect, type AnchorRect } from "../hooks/use-anchored-rect";
|
|
|
17
17
|
import { dirOf, type Direction } from "../lib/direction";
|
|
18
18
|
import { hasClippingAncestor } from "../lib/clipping";
|
|
19
19
|
|
|
20
|
+
/** The attribute that marks an element as a clipping container for {@link Tooltip}'s
|
|
21
|
+
* auto-portal, whatever its computed `overflow` — `<div data-clips>` or
|
|
22
|
+
* `<div {...{ [CLIPS_ATTRIBUTE]: "" }}>`. Put it on an app's own scroller so a test
|
|
23
|
+
* environment without stylesheets (jsdom) portals the same tooltips the browser
|
|
24
|
+
* does. DataTable's body and Table's wrapper already carry it. */
|
|
25
|
+
export { CLIPS_ATTRIBUTE } from "../lib/clipping";
|
|
26
|
+
|
|
20
27
|
/** Where the bubble sits. `start` / `end` follow the reading direction — `end` is the
|
|
21
28
|
* right in LTR and the left in RTL — and are what a layout that mirrors should ask
|
|
22
29
|
* for. `left` / `right` stay physical, for a bubble tied to something that does not
|
|
@@ -75,8 +82,8 @@ export interface TooltipProps extends ComponentPropsWithoutRef<"span"> {
|
|
|
75
82
|
/**
|
|
76
83
|
* Where the bubble lives. Left out (the default since 0.10.0), the tooltip decides for
|
|
77
84
|
* itself: the bubble stays next to the trigger unless an ancestor clips or scrolls
|
|
78
|
-
* (`overflow` other than `visible
|
|
79
|
-
* scroll container" below. `true` always portals, `false` never does; both are exactly
|
|
85
|
+
* (`overflow` other than `visible`, or the {@link CLIPS_ATTRIBUTE} marker), in which
|
|
86
|
+
* case it is portalled — see "Inside a scroll container" below. `true` always portals, `false` never does; both are exactly
|
|
80
87
|
* what they were before the default existed.
|
|
81
88
|
*/
|
|
82
89
|
portal?: boolean;
|
|
@@ -144,7 +151,14 @@ type TooltipVariantProps = Omit<TooltipProps, "side" | "portal"> & { side: Toolt
|
|
|
144
151
|
* being one: every tooltip in a table, a drawer or a scrolling card had to remember it,
|
|
145
152
|
* and the ones that forgot were only found by someone scrolling sideways. So with
|
|
146
153
|
* `portal` left out, the tooltip looks for a clipping ancestor itself — any element
|
|
147
|
-
* between it and `<body>` whose computed `overflow-x` / `overflow-y` is not `visible
|
|
154
|
+
* between it and `<body>` whose computed `overflow-x` / `overflow-y` is not `visible`,
|
|
155
|
+
* or that carries {@link CLIPS_ATTRIBUTE} (`data-clips`). The marker is what makes the
|
|
156
|
+
* answer the same under test: jsdom computes no Tailwind, so there every scroller
|
|
157
|
+
* reads `visible` and a table-cell tooltip used to stay in place — its always-mounted
|
|
158
|
+
* bubble then repeated the label in the cell's accessible name and `textContent`
|
|
159
|
+
* ("CheckingChecking"), and apps pinned `portal` to stop it. The kit's own scrollers
|
|
160
|
+
* are marked, so a tooltip in a DataTable or Table cell portals in jsdom exactly as it
|
|
161
|
+
* does in the browser.
|
|
148
162
|
* It looks at MOUNT, not only on open: dev#488's phantom scroll is caused by a bubble
|
|
149
163
|
* nobody opened, so a check that waited for the hover would find the damage already
|
|
150
164
|
* done. It looks again on every open, for a container that started scrolling after
|