@eifi1/ui-kit 0.8.1 → 0.10.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 +25 -11
- package/dist/components/alert-banner.d.ts +32 -5
- package/dist/components/alert-banner.js +57 -14
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +6 -0
- package/dist/components/breadcrumbs.d.ts +60 -0
- package/dist/components/breadcrumbs.js +88 -0
- package/dist/components/breadcrumbs.js.map +1 -0
- package/dist/components/bulk-action-bar.d.ts +66 -0
- package/dist/components/bulk-action-bar.js +149 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/calculator.d.ts +6 -0
- package/dist/components/chip.d.ts +58 -10
- package/dist/components/chip.js +91 -7
- package/dist/components/chip.js.map +1 -1
- package/dist/components/copy-button.d.ts +13 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +122 -91
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +89 -3
- package/dist/components/date-picker.js +163 -68
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +43 -6
- package/dist/components/description-list.js +91 -11
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +7 -0
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +68 -4
- package/dist/components/disclosure.js +33 -15
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -4
- package/dist/components/file-dropzone.js +2 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +29 -2
- package/dist/components/floating-panel.js +16 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +23 -4
- package/dist/components/full-bleed-dialog.js +11 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/list.d.ts +174 -0
- package/dist/components/list.js +215 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +99 -0
- package/dist/components/menu-item.js +83 -0
- package/dist/components/menu-item.js.map +1 -0
- package/dist/components/modal.d.ts +23 -1
- package/dist/components/modal.js +36 -10
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-field.d.ts +6 -0
- package/dist/components/number-input.d.ts +6 -0
- package/dist/components/numpad-sheet.d.ts +6 -0
- package/dist/components/page-contents.js +1 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.d.ts +37 -0
- package/dist/components/page-header.js +36 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/progress-bar.d.ts +16 -2
- package/dist/components/progress-bar.js +4 -2
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +3 -0
- package/dist/components/series-chart-labels.js +2 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +137 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/status-dot.d.ts +49 -0
- package/dist/components/status-dot.js +52 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +39 -6
- package/dist/components/table.js +43 -4
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +63 -0
- package/dist/components/text.js +23 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/toast.d.ts +167 -0
- package/dist/components/toast.js +226 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle-group.d.ts +29 -1
- package/dist/components/toggle-group.js +52 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +38 -15
- package/dist/components/tooltip.js +85 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +139 -16
- package/dist/components/ui.js +136 -26
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-close-transition.d.ts +7 -1
- package/dist/hooks/use-close-transition.js +5 -2
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.js +1 -1
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +84 -4
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +6 -0
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +11 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
- package/dist/i18n/locales/de-CH.d.ts +6 -0
- package/dist/i18n/locales/de-informal.d.ts +6 -0
- package/dist/i18n/locales/de.d.ts +6 -0
- package/dist/i18n/locales/de.js +31 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +6 -0
- package/dist/i18n/locales/es.js +31 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +6 -0
- package/dist/i18n/locales/fr.js +31 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +6 -0
- package/dist/i18n/locales/hu.js +31 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +6 -0
- package/dist/i18n/locales/it.js +31 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +6 -0
- package/dist/i18n/locales/zh.js +31 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +9 -0
- package/dist/lib/clipping.js +14 -0
- package/dist/lib/clipping.js.map +1 -0
- package/dist/search/command-palette.d.ts +67 -2
- package/dist/search/command-palette.js +147 -33
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +166 -0
- package/dist/search/global-search.js +293 -0
- package/dist/search/global-search.js.map +1 -0
- package/dist/search/search-index.d.ts +105 -0
- package/dist/search/search-index.js +142 -0
- package/dist/search/search-index.js.map +1 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -1
- package/dist/wizard/stepper-nav.js +3 -2
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +2 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +1 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +25 -12
- package/src/components/alert-banner.tsx +117 -18
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +256 -0
- package/src/components/chip.tsx +164 -13
- package/src/components/copy-button.tsx +29 -7
- package/src/components/data-table.tsx +81 -5
- package/src/components/date-picker.tsx +377 -118
- package/src/components/description-list.tsx +168 -15
- package/src/components/dialog-frame.tsx +7 -0
- package/src/components/disclosure.tsx +116 -28
- package/src/components/file-dropzone.tsx +9 -10
- package/src/components/floating-panel.tsx +46 -2
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +417 -0
- package/src/components/menu-item.tsx +214 -0
- package/src/components/modal.tsx +80 -17
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/progress-bar.tsx +27 -3
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +253 -3
- package/src/components/status-dot.tsx +99 -0
- package/src/components/table.tsx +91 -6
- package/src/components/text.tsx +89 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +82 -3
- package/src/components/tooltip.tsx +186 -100
- package/src/components/ui.tsx +321 -41
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/hooks/use-overlay-history.ts +181 -4
- package/src/i18n/defaults.ts +10 -0
- package/src/i18n/kit-labels.tsx +10 -0
- package/src/i18n/locales/de.ts +30 -0
- package/src/i18n/locales/es.ts +30 -0
- package/src/i18n/locales/fr.ts +30 -0
- package/src/i18n/locales/hu.ts +30 -0
- package/src/i18n/locales/it.ts +30 -0
- package/src/i18n/locales/zh.ts +30 -0
- package/src/index.ts +30 -0
- package/src/lib/clipping.ts +16 -0
- package/src/search/command-palette.tsx +241 -31
- package/src/search/global-search.tsx +525 -0
- package/src/search/search-index.ts +295 -0
- package/src/search.ts +2 -0
- package/src/wizard/stepper-nav.tsx +13 -2
- package/src/wizard/types.ts +2 -2
- package/src/wizard/use-wizard.ts +3 -3
- package/src/wizard/wizard-summary.tsx +14 -10
- package/tokens.css +97 -0
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
type ComponentPropsWithoutRef,
|
|
9
9
|
type ReactElement,
|
|
10
10
|
type ReactNode,
|
|
11
|
+
type RefObject,
|
|
11
12
|
} from "react";
|
|
12
13
|
import { createPortal } from "react-dom";
|
|
13
14
|
import { cn } from "../lib/cn";
|
|
14
15
|
import { useEscapeKey } from "../hooks/use-dismiss";
|
|
15
16
|
import { useAnchoredRect, type AnchorRect } from "../hooks/use-anchored-rect";
|
|
16
17
|
import { dirOf, type Direction } from "../lib/direction";
|
|
18
|
+
import { hasClippingAncestor } from "../lib/clipping";
|
|
17
19
|
|
|
18
20
|
/** Where the bubble sits. `start` / `end` follow the reading direction — `end` is the
|
|
19
21
|
* right in LTR and the left in RTL — and are what a layout that mirrors should ask
|
|
@@ -70,6 +72,13 @@ export interface TooltipProps extends ComponentPropsWithoutRef<"span"> {
|
|
|
70
72
|
label: ReactNode;
|
|
71
73
|
side?: TooltipSide;
|
|
72
74
|
className?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Where the bubble lives. Left out (the default since 0.10.0), the tooltip decides for
|
|
77
|
+
* itself: the bubble stays next to the trigger unless an ancestor clips or scrolls
|
|
78
|
+
* (`overflow` other than `visible`), in which case it is portalled — see "Inside a
|
|
79
|
+
* scroll container" below. `true` always portals, `false` never does; both are exactly
|
|
80
|
+
* what they were before the default existed.
|
|
81
|
+
*/
|
|
73
82
|
portal?: boolean;
|
|
74
83
|
/** Tag the bubble `data-private`, for a label that repeats the user's own data. */
|
|
75
84
|
redact?: boolean;
|
|
@@ -83,11 +92,10 @@ type TooltipVariantProps = Omit<TooltipProps, "side" | "portal"> & { side: Toolt
|
|
|
83
92
|
/**
|
|
84
93
|
* Hover/focus label for a control.
|
|
85
94
|
*
|
|
86
|
-
* Two
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* measurement.
|
|
95
|
+
* Two placements, and the choice matters more than it looks. In place, the bubble is
|
|
96
|
+
* always mounted next to the trigger and fades in on `:hover`, which costs no state and
|
|
97
|
+
* works in a plain render test. Portalled, the bubble is mounted in `document.body`
|
|
98
|
+
* only while it is up, positioned by measurement.
|
|
91
99
|
*
|
|
92
100
|
* ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs
|
|
93
101
|
* `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is
|
|
@@ -119,24 +127,41 @@ type TooltipVariantProps = Omit<TooltipProps, "side" | "portal"> & { side: Toolt
|
|
|
119
127
|
* point somewhere else — which is precisely what you cannot do when what you need to
|
|
120
128
|
* read is underneath it. The listener is the document's rather than the wrapper's
|
|
121
129
|
* because the pointer opens this with the keyboard focus somewhere else entirely, and
|
|
122
|
-
* it is subscribed only while a bubble is actually up: the
|
|
130
|
+
* it is subscribed only while a bubble is actually up: the in-place bubble is always
|
|
123
131
|
* mounted, and a table of forty tooltips must not mean forty keydown listeners.
|
|
124
132
|
*
|
|
125
|
-
* ⚠️ **Inside a scroll container,
|
|
126
|
-
* absolutely positioned, but an absolutely
|
|
127
|
-
* towards its scroll-container ancestor's
|
|
128
|
-
* bubble on a control near the right edge makes
|
|
129
|
-
* with nothing to reveal. That is what Keksdose feedback
|
|
130
|
-
* admin roster: 66px of horizontal scroll on a table that fit,
|
|
131
|
-
* tooltips nobody could see.
|
|
132
|
-
*
|
|
133
|
-
* cannot be clipped
|
|
133
|
+
* ⚠️ **Inside a scroll container, the bubble has to be portalled — and by default it
|
|
134
|
+
* now is.** An always-mounted bubble is absolutely positioned, but an absolutely
|
|
135
|
+
* positioned descendant still counts towards its scroll-container ancestor's
|
|
136
|
+
* scrollable overflow — so an invisible bubble on a control near the right edge makes
|
|
137
|
+
* the container scroll sideways with nothing to reveal. That is what Keksdose feedback
|
|
138
|
+
* dev#488 reported on the admin roster: 66px of horizontal scroll on a table that fit,
|
|
139
|
+
* 44px of it owed to tooltips nobody could see. And a visible one is clipped by the
|
|
140
|
+
* container's edge. The portalled bubble is `position: fixed` and absent until hovered,
|
|
141
|
+
* so it adds no width and cannot be clipped.
|
|
142
|
+
*
|
|
143
|
+
* Until 0.10.0 the cure was `portal` at the call site, and kastlan asked for it to stop
|
|
144
|
+
* being one: every tooltip in a table, a drawer or a scrolling card had to remember it,
|
|
145
|
+
* and the ones that forgot were only found by someone scrolling sideways. So with
|
|
146
|
+
* `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`.
|
|
148
|
+
* It looks at MOUNT, not only on open: dev#488's phantom scroll is caused by a bubble
|
|
149
|
+
* nobody opened, so a check that waited for the hover would find the damage already
|
|
150
|
+
* done. It looks again on every open, for a container that started scrolling after
|
|
151
|
+
* the tooltip mounted (a table that grew). Only the bubble changes place; the trigger
|
|
152
|
+
* and the caller's child stay mounted, so a switch never costs a focused button its
|
|
153
|
+
* focus. Outside any such container the in-place bubble is kept, which is still the
|
|
154
|
+
* cheaper one and the one a plain render test can find without a hover.
|
|
155
|
+
*
|
|
156
|
+
* Why not simply portal everything? Because the in-place bubble is the one existing
|
|
157
|
+
* app tests rely on (it is in the DOM without a hover), and because it follows its
|
|
158
|
+
* trigger through a scroll or an animation for free — the portalled one re-measures.
|
|
134
159
|
*/
|
|
135
160
|
export function Tooltip({
|
|
136
161
|
label,
|
|
137
162
|
side = "top",
|
|
138
163
|
className,
|
|
139
|
-
portal
|
|
164
|
+
portal,
|
|
140
165
|
redact = false,
|
|
141
166
|
children,
|
|
142
167
|
...rest
|
|
@@ -146,7 +171,7 @@ export function Tooltip({
|
|
|
146
171
|
// wrapper on this branch, which is the documented limit of the pass-through: there is
|
|
147
172
|
// no element left to put an attribute on.
|
|
148
173
|
if (isEmptyLabel(label)) return <>{children}</>;
|
|
149
|
-
if (portal) {
|
|
174
|
+
if (portal === true) {
|
|
150
175
|
return (
|
|
151
176
|
<PortalTooltip
|
|
152
177
|
label={label}
|
|
@@ -163,36 +188,73 @@ export function Tooltip({
|
|
|
163
188
|
// hooks: the empty-label branch above returns before either of them, and a hook after
|
|
164
189
|
// a conditional return is a hooks-order bug rather than a style violation.
|
|
165
190
|
return (
|
|
166
|
-
<
|
|
191
|
+
<InPlaceTooltip
|
|
192
|
+
label={label}
|
|
193
|
+
side={side}
|
|
194
|
+
className={className}
|
|
195
|
+
redact={redact}
|
|
196
|
+
detect={portal === undefined}
|
|
197
|
+
{...rest}
|
|
198
|
+
>
|
|
167
199
|
{children}
|
|
168
|
-
</
|
|
200
|
+
</InPlaceTooltip>
|
|
169
201
|
);
|
|
170
202
|
}
|
|
171
203
|
|
|
172
|
-
|
|
204
|
+
|
|
205
|
+
/** The variant that lives next to its trigger, and — when `detect` is on, which is the
|
|
206
|
+
* default — moves its bubble to `<body>` when that turns out to be inside a clipping
|
|
207
|
+
* container (see "Inside a scroll container" on {@link Tooltip}).
|
|
208
|
+
*
|
|
209
|
+
* In place it holds the little state it does for the two things CSS cannot express —
|
|
210
|
+
* which element to point `aria-describedby` at, and Escape — and not for the fade,
|
|
211
|
+
* which is still `group-hover`/`group-focus-within` and still costs a render nothing.
|
|
173
212
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
|
|
213
|
+
* ONE component for both placements rather than a switch between the two variants: a
|
|
214
|
+
* switch would be a different component at the same place in the tree, and React
|
|
215
|
+
* would remount the caller's child with it — a button that loses focus the moment
|
|
216
|
+
* its own tooltip decided where to go. Here the wrapper and the child stay put and
|
|
217
|
+
* only the bubble's slot changes. */
|
|
218
|
+
function InPlaceTooltip({
|
|
178
219
|
label,
|
|
179
220
|
side,
|
|
180
221
|
className,
|
|
181
222
|
redact,
|
|
223
|
+
detect,
|
|
182
224
|
children,
|
|
183
225
|
...rest
|
|
184
|
-
}: TooltipVariantProps) {
|
|
226
|
+
}: TooltipVariantProps & { detect: boolean }) {
|
|
185
227
|
const id = useId();
|
|
228
|
+
const triggerRef = useRef<HTMLSpanElement | null>(null);
|
|
229
|
+
const [clipped, setClipped] = useState(false);
|
|
186
230
|
const [hovered, setHovered] = useState(false);
|
|
187
231
|
const [focused, setFocused] = useState(false);
|
|
188
232
|
const [dismissed, setDismissed] = useState(false);
|
|
189
|
-
|
|
233
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
234
|
+
const open = (hovered || focused) && !dismissed;
|
|
235
|
+
useEscapeKey(() => setDismissed(true), open);
|
|
236
|
+
// At mount, before the first paint: the in-place bubble inside a scroller is the
|
|
237
|
+
// phantom-scroll bug whether or not anyone opens it.
|
|
238
|
+
useLayoutEffect(() => {
|
|
239
|
+
if (detect) setClipped(hasClippingAncestor(triggerRef.current));
|
|
240
|
+
}, [detect]);
|
|
241
|
+
// Re-armed by the next hover or focus rather than by an effect watching those flags:
|
|
242
|
+
// coming back to a trigger is a fresh request for its label, and an effect would also
|
|
243
|
+
// re-show the bubble under a pointer that never left. The clipping check is repeated
|
|
244
|
+
// here for a container that began to scroll after mount.
|
|
245
|
+
const arm = (el: Element) => {
|
|
246
|
+
setDismissed(false);
|
|
247
|
+
if (!detect) return;
|
|
248
|
+
setDir(dirOf(el));
|
|
249
|
+
setClipped(hasClippingAncestor(el));
|
|
250
|
+
};
|
|
190
251
|
return (
|
|
191
252
|
<span
|
|
192
253
|
// `...rest` first: the four handlers below are what decides whether a bubble is
|
|
193
254
|
// up, and a caller passing an `onFocus` of its own must not replace them.
|
|
194
255
|
{...rest}
|
|
195
|
-
|
|
256
|
+
ref={triggerRef}
|
|
257
|
+
className={cn("relative inline-flex", !clipped && "group/tooltip", className)}
|
|
196
258
|
// These four track WHETHER A BUBBLE IS UP. They activate nothing — the only thing
|
|
197
259
|
// here that can be activated is the caller's child, which keeps every handler it
|
|
198
260
|
// arrived with — so this wrapper needs no role and no key handling of its own.
|
|
@@ -200,38 +262,41 @@ function CssTooltip({
|
|
|
200
262
|
// already does about the portal variant's identical trigger below; both are left
|
|
201
263
|
// visible rather than silenced, because a rule this package ratchets should be
|
|
202
264
|
// argued with in the backlog and not in a disable comment.
|
|
203
|
-
|
|
204
|
-
// Re-armed by the next hover or focus rather than by an effect watching those
|
|
205
|
-
// flags: coming back to a trigger is a fresh request for its label, and an effect
|
|
206
|
-
// would also re-show the bubble under a pointer that never left.
|
|
207
|
-
onMouseEnter={() => {
|
|
265
|
+
onMouseEnter={(e) => {
|
|
208
266
|
setHovered(true);
|
|
209
|
-
|
|
267
|
+
arm(e.currentTarget);
|
|
210
268
|
}}
|
|
211
269
|
onMouseLeave={() => setHovered(false)}
|
|
212
|
-
onFocus={() => {
|
|
270
|
+
onFocus={(e) => {
|
|
213
271
|
setFocused(true);
|
|
214
|
-
|
|
272
|
+
arm(e.currentTarget);
|
|
215
273
|
}}
|
|
216
274
|
onBlur={() => setFocused(false)}
|
|
217
275
|
>
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
276
|
+
{/* In place the bubble is always there to point at; portalled, only while up. */}
|
|
277
|
+
{describedBy(children, clipped ? (open ? id : undefined) : dismissed ? undefined : id)}
|
|
278
|
+
{clipped ? (
|
|
279
|
+
open && (
|
|
280
|
+
<PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />
|
|
281
|
+
)
|
|
282
|
+
) : (
|
|
283
|
+
<span
|
|
284
|
+
id={id}
|
|
285
|
+
role="tooltip"
|
|
286
|
+
// The `hidden` ATTRIBUTE, not an opacity class: dismissing has to take the
|
|
287
|
+
// bubble out of the accessibility tree as well as off the screen, or a screen
|
|
288
|
+
// reader still reads out the description of a bubble the user just closed.
|
|
289
|
+
hidden={dismissed || undefined}
|
|
290
|
+
data-private={redact ? "" : undefined}
|
|
291
|
+
className={cn(
|
|
292
|
+
TOOLTIP_SURFACE,
|
|
293
|
+
"pointer-events-none absolute z-50 opacity-0 group-hover/tooltip:opacity-100 group-focus-within/tooltip:opacity-100",
|
|
294
|
+
sidePositionClass[side],
|
|
295
|
+
)}
|
|
296
|
+
>
|
|
297
|
+
{label}
|
|
298
|
+
</span>
|
|
299
|
+
)}
|
|
235
300
|
</span>
|
|
236
301
|
);
|
|
237
302
|
}
|
|
@@ -410,7 +475,6 @@ function PortalTooltip({
|
|
|
410
475
|
...rest
|
|
411
476
|
}: TooltipVariantProps) {
|
|
412
477
|
const triggerRef = useRef<HTMLSpanElement | null>(null);
|
|
413
|
-
const bubbleRef = useRef<HTMLSpanElement | null>(null);
|
|
414
478
|
const [visible, setVisible] = useState(false);
|
|
415
479
|
// The trigger's reading direction, read when the bubble is asked for (an event, not a
|
|
416
480
|
// render): it resolves `start` / `end`, and the portalled bubble — which has left the
|
|
@@ -425,9 +489,53 @@ function PortalTooltip({
|
|
|
425
489
|
// shown. The next mouseenter/focus brings it back, which is the behaviour WCAG
|
|
426
490
|
// 1.4.13 asks for: dismissible now, still available when you ask again.
|
|
427
491
|
useEscapeKey(() => setVisible(false), visible);
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<>
|
|
495
|
+
<span
|
|
496
|
+
// As in `InPlaceTooltip`: the caller's attributes first, the four handlers that
|
|
497
|
+
// run this component after them. The BUBBLE is deliberately not given them — it
|
|
498
|
+
// is portalled to `<body>`, and an id or a tour anchor duplicated onto a node
|
|
499
|
+
// that only exists while hovered would match twice or match nothing.
|
|
500
|
+
{...rest}
|
|
501
|
+
ref={triggerRef}
|
|
502
|
+
className={cn("relative inline-flex", className)}
|
|
503
|
+
onMouseEnter={(e) => show(e.currentTarget)}
|
|
504
|
+
onMouseLeave={() => setVisible(false)}
|
|
505
|
+
onFocus={(e) => show(e.currentTarget)}
|
|
506
|
+
onBlur={() => setVisible(false)}
|
|
507
|
+
>
|
|
508
|
+
{describedBy(children, visible ? id : undefined)}
|
|
509
|
+
</span>
|
|
510
|
+
{visible && (
|
|
511
|
+
<PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />
|
|
512
|
+
)}
|
|
513
|
+
</>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** The measured, `position: fixed` bubble on `<body>`, mounted only while it is up.
|
|
518
|
+
* Shared by {@link PortalTooltip} and the in-place variant's clipped mode, so the two
|
|
519
|
+
* cannot place a bubble differently. */
|
|
520
|
+
function PortalBubble({
|
|
521
|
+
triggerRef,
|
|
522
|
+
id,
|
|
523
|
+
label,
|
|
524
|
+
side,
|
|
525
|
+
dir,
|
|
526
|
+
redact,
|
|
527
|
+
}: {
|
|
528
|
+
triggerRef: RefObject<HTMLSpanElement | null>;
|
|
529
|
+
id: string;
|
|
530
|
+
label: ReactNode;
|
|
531
|
+
side: TooltipSide;
|
|
532
|
+
dir: Direction;
|
|
533
|
+
redact: boolean | undefined;
|
|
534
|
+
}) {
|
|
535
|
+
const bubbleRef = useRef<HTMLSpanElement | null>(null);
|
|
428
536
|
// The measure + scroll/resize-tracking lifecycle is owned by useAnchoredRect;
|
|
429
537
|
// here we only map the rect to a side-specific anchor point.
|
|
430
|
-
const rect = useAnchoredRect(triggerRef,
|
|
538
|
+
const rect = useAnchoredRect(triggerRef, true);
|
|
431
539
|
// The bubble's own size and the window it has to fit in — neither of which is
|
|
432
540
|
// knowable in render: the width is whatever the label wrapped to inside the
|
|
433
541
|
// cap, and reading `window` while rendering is not a pure thing to do. Both
|
|
@@ -435,8 +543,7 @@ function PortalTooltip({
|
|
|
435
543
|
// paints and there is no frame in which the label sits off the screen.
|
|
436
544
|
const [room, setRoom] = useState<{ size: TooltipSize; viewport: TooltipViewport } | null>(null);
|
|
437
545
|
useLayoutEffect(() => {
|
|
438
|
-
const measured =
|
|
439
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect -- a measurement is the one thing a layout effect is for
|
|
546
|
+
const measured = bubbleRef.current?.getBoundingClientRect();
|
|
440
547
|
setRoom((previous) => {
|
|
441
548
|
if (!measured) return null;
|
|
442
549
|
const next = {
|
|
@@ -448,7 +555,7 @@ function PortalTooltip({
|
|
|
448
555
|
// bubble forever.
|
|
449
556
|
return previous && sameRoom(previous, next) ? previous : next;
|
|
450
557
|
});
|
|
451
|
-
}, [
|
|
558
|
+
}, [rect, label]);
|
|
452
559
|
|
|
453
560
|
const physical = physicalSide(side, dir);
|
|
454
561
|
const point = rect ? tooltipAnchor(rect, physical) : null;
|
|
@@ -457,49 +564,28 @@ function PortalTooltip({
|
|
|
457
564
|
// size is known and the placement is decided properly.
|
|
458
565
|
const placed = rect && room ? placeTooltip(rect, physical, room.size, room.viewport) : null;
|
|
459
566
|
|
|
460
|
-
return
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
</span>
|
|
477
|
-
{visible &&
|
|
478
|
-
point &&
|
|
479
|
-
typeof document !== "undefined" &&
|
|
480
|
-
createPortal(
|
|
481
|
-
<span
|
|
482
|
-
ref={bubbleRef}
|
|
483
|
-
id={id}
|
|
484
|
-
role="tooltip"
|
|
485
|
-
dir={dir}
|
|
486
|
-
data-private={redact ? "" : undefined}
|
|
487
|
-
style={
|
|
488
|
-
placed
|
|
489
|
-
? { position: "fixed", left: placed.left, top: placed.top }
|
|
490
|
-
: {
|
|
491
|
-
position: "fixed",
|
|
492
|
-
left: point.left,
|
|
493
|
-
top: point.top,
|
|
494
|
-
transform: portalTransformBySide[physical],
|
|
495
|
-
}
|
|
567
|
+
if (!point || typeof document === "undefined") return null;
|
|
568
|
+
return createPortal(
|
|
569
|
+
<span
|
|
570
|
+
ref={bubbleRef}
|
|
571
|
+
id={id}
|
|
572
|
+
role="tooltip"
|
|
573
|
+
dir={dir}
|
|
574
|
+
data-private={redact ? "" : undefined}
|
|
575
|
+
style={
|
|
576
|
+
placed
|
|
577
|
+
? { position: "fixed", left: placed.left, top: placed.top }
|
|
578
|
+
: {
|
|
579
|
+
position: "fixed",
|
|
580
|
+
left: point.left,
|
|
581
|
+
top: point.top,
|
|
582
|
+
transform: portalTransformBySide[physical],
|
|
496
583
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
</>
|
|
584
|
+
}
|
|
585
|
+
className={cn(TOOLTIP_SURFACE, "pointer-events-none z-50")}
|
|
586
|
+
>
|
|
587
|
+
{label}
|
|
588
|
+
</span>,
|
|
589
|
+
document.body,
|
|
504
590
|
);
|
|
505
591
|
}
|