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