@eifi1/ui-kit 0.24.1 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/chart.d.ts +2 -2
- package/dist/components/account-chips.d.ts +1 -1
- package/dist/components/amount-input.d.ts +2 -2
- package/dist/components/button-group.d.ts +2 -2
- package/dist/components/calculator.d.ts +2 -2
- package/dist/components/column-mapper.d.ts +28 -9
- package/dist/components/column-mapper.js +63 -77
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +2 -2
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/danger-confirm.d.ts +2 -2
- package/dist/components/data-table-cells.d.ts +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-labels.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +1 -1
- package/dist/components/data-table-pagination.js +6 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +7 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -2
- package/dist/components/file-button.d.ts +2 -2
- package/dist/components/file-dropzone.d.ts +2 -2
- package/dist/components/form-actions.d.ts +2 -2
- package/dist/components/iban-input.d.ts +2 -2
- package/dist/components/language-select.d.ts +2 -2
- package/dist/components/money-field.d.ts +2 -2
- package/dist/components/number-field.d.ts +2 -2
- package/dist/components/number-input.d.ts +2 -2
- package/dist/components/numpad-sheet.d.ts +2 -2
- package/dist/components/phone-input.d.ts +2 -2
- package/dist/components/series-chart.d.ts +2 -2
- package/dist/components/settings-fields.d.ts +2 -2
- package/dist/components/share-card.d.ts +2 -2
- package/dist/components/swipeable-row.js +1 -1
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table.d.ts +35 -1
- package/dist/components/table.js +4 -0
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +2 -2
- package/dist/components/time-input.d.ts +2 -2
- package/dist/components/tooltip.d.ts +103 -14
- package/dist/components/tooltip.js +194 -57
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +9 -1
- package/dist/components/translation-review-editor.js +11 -1
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-labels.d.ts +10 -0
- package/dist/components/translation-review-labels.js +5 -1
- package/dist/components/translation-review-labels.js.map +1 -1
- package/dist/components/translation-review.d.ts +145 -8
- package/dist/components/translation-review.js +372 -91
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui.d.ts +2 -2
- package/dist/components/ui.js +55 -18
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-labels-B7OdnM0S.d.ts → data-table-labels-Cdn1QS2k.d.ts} +21 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/feedback/feedback-attachment.d.ts +2 -2
- package/dist/feedback/feedback-dialog.d.ts +2 -2
- package/dist/feedback/feedback-dialog.js +6 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +2 -2
- package/dist/feedback/feedback-inbox.js +2 -0
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +2 -2
- package/dist/{feedback-DOwPu-Il.d.ts → feedback-BXKH-ToU.d.ts} +32 -3
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-body-scroll-lock.js +16 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +2 -2
- package/dist/hooks/use-search-param-state.d.ts +58 -3
- package/dist/hooks/use-search-param-state.js +107 -35
- package/dist/hooks/use-search-param-state.js.map +1 -1
- package/dist/i18n/defaults.d.ts +2 -2
- package/dist/i18n/german.d.ts +2 -2
- package/dist/i18n/german.js +8 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/languages.d.ts +2 -2
- package/dist/i18n/locales/de-CH.d.ts +2 -2
- package/dist/i18n/locales/en.d.ts +2 -2
- package/dist/i18n/locales/en.js +5 -1
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -2
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -2
- package/dist/i18n/locales/fr.js +6 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -2
- 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 -2
- package/dist/i18n/locales/it.js +6 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -2
- package/dist/i18n/locales/zh.js +6 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +2 -2
- package/dist/i18n/review.js +11 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/strip-fade.d.ts +84 -5
- package/dist/lib/strip-fade.js +109 -11
- package/dist/lib/strip-fade.js.map +1 -1
- package/dist/lib/translation-review.d.ts +45 -1
- package/dist/lib/translation-review.js +38 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/rhf/fields.d.ts +2 -2
- package/dist/rhf/form.d.ts +2 -2
- package/dist/rhf.d.ts +2 -2
- package/dist/shell/app-shell.d.ts +2 -2
- package/dist/shell/top-bar-brand.d.ts +2 -2
- package/dist/shell.d.ts +2 -2
- package/dist/wizard/stepper-nav.d.ts +2 -2
- package/dist/wizard.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/column-mapper.tsx +120 -113
- package/src/components/data-table-pagination.tsx +10 -1
- package/src/components/data-table.tsx +34 -1
- package/src/components/swipeable-row.tsx +1 -1
- package/src/components/table.tsx +39 -0
- package/src/components/tooltip.tsx +487 -104
- package/src/components/translation-review-editor.tsx +21 -1
- package/src/components/translation-review-labels.ts +17 -0
- package/src/components/translation-review.tsx +657 -109
- package/src/components/ui.tsx +104 -4
- package/src/feedback/feedback-dialog.tsx +21 -0
- package/src/feedback/feedback-inbox.tsx +19 -2
- package/src/hooks/use-body-scroll-lock.ts +25 -0
- package/src/hooks/use-search-param-state.ts +283 -41
- package/src/i18n/german.ts +10 -0
- package/src/i18n/locales/en.ts +7 -0
- package/src/i18n/locales/es.ts +9 -0
- package/src/i18n/locales/fr.ts +9 -0
- package/src/i18n/locales/hu.ts +11 -0
- package/src/i18n/locales/it.ts +9 -0
- package/src/i18n/locales/zh.ts +8 -0
- package/src/i18n/review.ts +11 -0
- package/src/index.ts +12 -0
- package/src/lib/strip-fade.ts +220 -15
- package/src/lib/translation-review.ts +85 -0
|
@@ -8,6 +8,17 @@ export { CLIPS_ATTRIBUTE } from '../lib/clipping.js';
|
|
|
8
8
|
* for. `left` / `right` stay physical, for a bubble tied to something that does not
|
|
9
9
|
* mirror (a chart axis, a map). */
|
|
10
10
|
type TooltipSide = "top" | "bottom" | "left" | "right" | "start" | "end";
|
|
11
|
+
/** What a TAP — a touch or a pen press, never the mouse — on the trigger does to the
|
|
12
|
+
* bubble. See "A tap is not a hover" on {@link Tooltip}.
|
|
13
|
+
*
|
|
14
|
+
* - `"auto"` (the default): a tap that activates something shows nothing; a tap that
|
|
15
|
+
* activates nothing — on a disabled or `aria-disabled` control (a write lock's
|
|
16
|
+
* reason), or on a trigger with no control at all (a truncated name, a badge) —
|
|
17
|
+
* toggles the bubble, since showing it is then the tap's only answer.
|
|
18
|
+
* - `"toggle"`: every tap toggles it — for a control that exists only to explain, such
|
|
19
|
+
* as a "?" whose click does nothing.
|
|
20
|
+
* - `"ignore"`: a tap never shows it. */
|
|
21
|
+
type TooltipTap = "auto" | "toggle" | "ignore";
|
|
11
22
|
/** The placement the maths works in: a logical side resolved against the trigger. */
|
|
12
23
|
type PhysicalSide = "top" | "bottom" | "left" | "right";
|
|
13
24
|
/**
|
|
@@ -46,15 +57,22 @@ interface TooltipProps extends ComponentPropsWithoutRef<"span"> {
|
|
|
46
57
|
lazy?: boolean;
|
|
47
58
|
/** Tag the bubble `data-private`, for a label that repeats the user's own data. */
|
|
48
59
|
redact?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* What a tap (touch or pen) on the trigger does — see {@link TooltipTap} and "A tap is
|
|
62
|
+
* not a hover" below. Default `"auto"`: a tap that activates the control shows nothing;
|
|
63
|
+
* one that activates nothing (a disabled control, plain text) toggles the bubble.
|
|
64
|
+
*/
|
|
65
|
+
tap?: TooltipTap;
|
|
49
66
|
children: ReactNode;
|
|
50
67
|
}
|
|
51
68
|
/**
|
|
52
69
|
* Hover/focus label for a control.
|
|
53
70
|
*
|
|
54
71
|
* Two placements, and the choice matters more than it looks. In place, the bubble is
|
|
55
|
-
* always mounted next to the trigger
|
|
56
|
-
*
|
|
57
|
-
* only while it is up,
|
|
72
|
+
* always mounted next to the trigger — so a plain render test finds it without a hover —
|
|
73
|
+
* and shown while it is up (`display: none` otherwise, see "never widens the page"
|
|
74
|
+
* below). Portalled, the bubble is mounted in `document.body` only while it is up,
|
|
75
|
+
* positioned by measurement.
|
|
58
76
|
*
|
|
59
77
|
* ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs
|
|
60
78
|
* `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is
|
|
@@ -173,8 +191,8 @@ interface TooltipProps extends ComponentPropsWithoutRef<"span"> {
|
|
|
173
191
|
* So now, when an in-place bubble goes up (default or `lazy`), a layout effect measures
|
|
174
192
|
* it and, if it crosses the viewport edge minus the same margin the portalled bubble
|
|
175
193
|
* keeps, slides it back along its CROSS axis only — sideways for `top` / `bottom`, up or
|
|
176
|
-
* down for the four side placements — with an inline `transform
|
|
177
|
-
*
|
|
194
|
+
* down for the four side placements — with an inline margin (a `transform` until 0.25; see
|
|
195
|
+
* {@link shiftStyle} for why that widened the page). The main axis is left alone on purpose: sliding a
|
|
178
196
|
* `start` bubble along the main axis would slide it over its own trigger, and turning it
|
|
179
197
|
* round is a measured-placement decision this CSS-placed bubble does not make (pass
|
|
180
198
|
* `portal` for that). The width is already capped to the viewport (see
|
|
@@ -183,15 +201,86 @@ interface TooltipProps extends ComponentPropsWithoutRef<"span"> {
|
|
|
183
201
|
*
|
|
184
202
|
* Why on by default, with no prop? Because it is a no-op for every bubble that already
|
|
185
203
|
* fits — the shift is zero unless the bubble would overflow — so the only placements it
|
|
186
|
-
* changes are ones that were broken. The maths is in physical viewport pixels
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* an
|
|
204
|
+
* changes are ones that were broken. The maths is in physical viewport pixels: an RTL
|
|
205
|
+
* row puts its long label at the RIGHT edge and gets slid left by the same code, and the
|
|
206
|
+
* reading direction only decides which edge is kept when a bubble is wider than the room
|
|
207
|
+
* (its start, as {@link placeTooltip} keeps). Under jsdom there is no layout — every rect
|
|
208
|
+
* is zero-sized — and a zero-sized bubble is taken to be unmeasured, so tests see no
|
|
209
|
+
* transform at all. It is measured on open (and when the label or side changes while
|
|
210
|
+
* open), not on every scroll: an in-place bubble follows its trigger for free, and a page
|
|
211
|
+
* that scrolls sideways under an open tooltip is not a case worth a listener per tooltip.
|
|
212
|
+
*
|
|
213
|
+
* ⚠️ **An in-place bubble never widens the page (keksdose run 72, live #381).** Two
|
|
214
|
+
* holes let it, and a page wider than the screen is worse than a bubble cut off: on a
|
|
215
|
+
* phone Chrome then lays every `position: fixed` layer out against the WIDER page while
|
|
216
|
+
* the glass still shows the old width. Measured on a 406px phone: the sync icon at x=254,
|
|
217
|
+
* its `bottom` bubble at 102–422, the page 426px wide, and a FloatingPanel 17px off the
|
|
218
|
+
* glass. First, the clamp compared against `window.innerWidth` — and on a phone that is
|
|
219
|
+
* the layout viewport, which GROWS to the page's width once something sticks out (430 on
|
|
220
|
+
* a 406px screen, measured in Chromium's mobile emulation), so a bubble already past the
|
|
221
|
+
* edge found itself "inside" and stayed there. It now measures against the root's client
|
|
222
|
+
* width, which on a phone is the screen (the initial containing block, the width `100vw`
|
|
223
|
+
* and media queries see) whatever overflows, and on a desktop also leaves out a classic
|
|
224
|
+
* scrollbar; the window is the fallback where there is no layout (jsdom). The portalled
|
|
225
|
+
* bubble takes the same width, since a `fixed` bubble placed against a grown layout
|
|
226
|
+
* viewport is off the glass for the same reason. Second, the always-mounted bubble was
|
|
227
|
+
* `opacity: 0` while closed — invisible, but still laid out where the CSS puts it, so a
|
|
228
|
+
* bubble nobody opened, centred on a trigger near the end edge, widened the page all by
|
|
229
|
+
* itself (dev#488's phantom scroll, at the scale of the page instead of a scroller, where
|
|
230
|
+
* the auto-portal cannot help: the page is not an ancestor it can portal out of). It is
|
|
231
|
+
* now `display: none` until it is up, so a closed bubble takes no room at all, and an open
|
|
232
|
+
* one is slid onto the glass in the same layout pass that shows it, before paint. jsdom
|
|
233
|
+
* computes no Tailwind, so tests still find the closed bubble without a hover.
|
|
234
|
+
*
|
|
235
|
+
* ⚠️ **A tap is not a hover (keksdose run 72, live #379).** The bubble used to show on
|
|
236
|
+
* `mouseenter` and on `focus`, and hide only on `mouseleave` and `blur`. A tap on a phone
|
|
237
|
+
* fires an emulated `mouseenter` and, on a button, focuses it — and the virtual mouse and
|
|
238
|
+
* the focus both stay on the button until the next tap somewhere else, so every
|
|
239
|
+
* IconButton's label (the top-bar icons, the sync indicator, a row's actions) stayed up
|
|
240
|
+
* over whatever the tap had just opened. Now a touch or pen press is told apart from the
|
|
241
|
+
* mouse: the emulated `mouseenter` that follows a tap is ignored, the press itself closes
|
|
242
|
+
* a bubble that was up, and a focus that arrives while the page's last input was a finger
|
|
243
|
+
* or a pen — the tap's own, or one a closing dialog RETURNS to the icon it was opened
|
|
244
|
+
* from — shows nothing. The next key press (a Bluetooth keyboard on a tablet) or mouse
|
|
245
|
+
* press makes focus count again, and a mouse entering the trigger hovers as ever, so a
|
|
246
|
+
* hybrid laptop gets both.
|
|
247
|
+
*
|
|
248
|
+
* Focus otherwise shows the bubble, as it always did — the keyboard, a script, a test's
|
|
249
|
+
* `.focus()` — with one more exception: the focus a mouse press on the trigger gives it.
|
|
250
|
+
* There it follows `:focus-visible`, so a text field still shows its bubble while you type
|
|
251
|
+
* and a clicked button does not hold it: a desktop click behaves as it always did while
|
|
252
|
+
* the pointer is over the button (the hover shows it) and lets it go when the pointer
|
|
253
|
+
* leaves, where the in-place bubble used to linger until the button lost focus — the same
|
|
254
|
+
* left-behind bubble on a smaller scale, and what the portalled bubble always did. A
|
|
255
|
+
* keyboard user's bubble, in turn, now stays up when a mouse passes over and leaves (the
|
|
256
|
+
* portalled one used to close). Any `pointerdown` outside the trigger closes an open
|
|
257
|
+
* bubble too — the Safari case, where a tap elsewhere moves no focus.
|
|
258
|
+
*
|
|
259
|
+
* What a tap DOES show is {@link TooltipTap} (`tap`). By default a tap that activates
|
|
260
|
+
* something shows nothing — the action is the answer — while a tap that activates nothing
|
|
261
|
+
* toggles the bubble, because there the bubble is the only answer: a write-locked or
|
|
262
|
+
* disabled control, whose bubble is the reason it did not respond, and a trigger with no
|
|
263
|
+
* control at all, a truncated payee or a badge, whose bubble is the rest of the text. A
|
|
264
|
+
* second tap, a tap anywhere else, Escape or the focus leaving closes it. "Activates" is
|
|
265
|
+
* read off the DOM — a link, a button, a field, a label, an element with a widget role
|
|
266
|
+
* (`button`, `link`, `checkbox`, `option`, …), or a DataTable row (`tr[tabindex]`) — from
|
|
267
|
+
* the tapped element outwards, past the tooltip, so a glyph inside a clickable row or a
|
|
268
|
+
* button counts as that row or button. What it cannot read is a click handler on an
|
|
269
|
+
* element with no role (give it `role="button"`, which it needs anyway, or pass
|
|
270
|
+
* `tap="ignore"`), and a button that exists only to explain, whose click does nothing —
|
|
271
|
+
* FieldHint's "?" — which says so with `tap="toggle"`.
|
|
272
|
+
*
|
|
273
|
+
* Why not show the label on a long press, as Android does for its own icons? Because the
|
|
274
|
+
* web gives no reliable long press on a control: browsers disagree on whether the release
|
|
275
|
+
* that ends one still clicks the button, and the button's own long press — the context
|
|
276
|
+
* menu, text selection, a row's drag — claims the gesture first on others. A gesture meant
|
|
277
|
+
* to ask "what does this do" must not risk doing it. Screen readers do not need it (the
|
|
278
|
+
* bubble is in `aria-describedby`, and an IconButton's label is its name), and a tap on
|
|
279
|
+
* anything that does nothing already shows it. No scroll listener either: every touch scroll begins with a `pointerdown`, which
|
|
280
|
+
* already closes the bubble, and closing on scroll would close a keyboard user's bubble
|
|
281
|
+
* the moment Tab scrolled its control into view.
|
|
193
282
|
*/
|
|
194
|
-
declare function Tooltip({ label, side, className, portal, lazy, redact, children, ...rest }: TooltipProps): react.JSX.Element;
|
|
283
|
+
declare function Tooltip({ label, side, className, portal, lazy, redact, tap, children, ...rest }: TooltipProps): react.JSX.Element;
|
|
195
284
|
interface TooltipSize {
|
|
196
285
|
width: number;
|
|
197
286
|
height: number;
|
|
@@ -230,4 +319,4 @@ interface TooltipPlacement {
|
|
|
230
319
|
*/
|
|
231
320
|
declare function placeTooltip(r: AnchorRect, side: PhysicalSide, size: TooltipSize, viewport: TooltipViewport): TooltipPlacement;
|
|
232
321
|
|
|
233
|
-
export { Tooltip, type TooltipPlacement, type TooltipProps, type TooltipSide, type TooltipSize, type TooltipViewport, placeTooltip };
|
|
322
|
+
export { Tooltip, type TooltipPlacement, type TooltipProps, type TooltipSide, type TooltipSize, type TooltipTap, type TooltipViewport, placeTooltip };
|
|
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import {
|
|
4
4
|
cloneElement,
|
|
5
5
|
isValidElement,
|
|
6
|
+
useEffect,
|
|
6
7
|
useId,
|
|
7
8
|
useLayoutEffect,
|
|
8
9
|
useRef,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
} from "react";
|
|
11
12
|
import { createPortal } from "react-dom";
|
|
12
13
|
import { cn } from "../lib/cn.js";
|
|
13
|
-
import { useEscapeKey } from "../hooks/use-dismiss.js";
|
|
14
|
+
import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss.js";
|
|
14
15
|
import { useAnchoredRect } from "../hooks/use-anchored-rect.js";
|
|
15
16
|
import { dirOf } from "../lib/direction.js";
|
|
16
17
|
import { hasClippingAncestor } from "../lib/clipping.js";
|
|
@@ -38,6 +39,7 @@ function Tooltip({
|
|
|
38
39
|
portal,
|
|
39
40
|
lazy = false,
|
|
40
41
|
redact = false,
|
|
42
|
+
tap = "auto",
|
|
41
43
|
children,
|
|
42
44
|
...rest
|
|
43
45
|
}) {
|
|
@@ -50,6 +52,7 @@ function Tooltip({
|
|
|
50
52
|
side,
|
|
51
53
|
className,
|
|
52
54
|
redact,
|
|
55
|
+
tap,
|
|
53
56
|
...rest,
|
|
54
57
|
children
|
|
55
58
|
}
|
|
@@ -64,11 +67,152 @@ function Tooltip({
|
|
|
64
67
|
redact,
|
|
65
68
|
detect: portal === void 0,
|
|
66
69
|
lazy,
|
|
70
|
+
tap,
|
|
67
71
|
...rest,
|
|
68
72
|
children
|
|
69
73
|
}
|
|
70
74
|
);
|
|
71
75
|
}
|
|
76
|
+
function isTap(pointerType) {
|
|
77
|
+
return pointerType === "touch" || pointerType === "pen";
|
|
78
|
+
}
|
|
79
|
+
const ACTIVATES = [
|
|
80
|
+
"a[href]",
|
|
81
|
+
"button",
|
|
82
|
+
"input",
|
|
83
|
+
"select",
|
|
84
|
+
"textarea",
|
|
85
|
+
"label",
|
|
86
|
+
"summary",
|
|
87
|
+
'[contenteditable=""]',
|
|
88
|
+
'[contenteditable="true"]',
|
|
89
|
+
"tr[tabindex]",
|
|
90
|
+
...[
|
|
91
|
+
"button",
|
|
92
|
+
"link",
|
|
93
|
+
"checkbox",
|
|
94
|
+
"radio",
|
|
95
|
+
"switch",
|
|
96
|
+
"tab",
|
|
97
|
+
"menuitem",
|
|
98
|
+
"menuitemcheckbox",
|
|
99
|
+
"menuitemradio",
|
|
100
|
+
"option",
|
|
101
|
+
"treeitem",
|
|
102
|
+
"slider",
|
|
103
|
+
"spinbutton",
|
|
104
|
+
"combobox"
|
|
105
|
+
].map((role) => `[role="${role}"]`)
|
|
106
|
+
].join(",");
|
|
107
|
+
function tapShows(tap, target) {
|
|
108
|
+
if (tap !== "auto") return tap === "toggle";
|
|
109
|
+
if (!(target instanceof Element)) return true;
|
|
110
|
+
const control = target.closest(ACTIVATES);
|
|
111
|
+
return control === null || control.matches(":disabled") || control.closest('[aria-disabled="true"]') !== null;
|
|
112
|
+
}
|
|
113
|
+
let lastInput = null;
|
|
114
|
+
let trackingInput = false;
|
|
115
|
+
function trackInput() {
|
|
116
|
+
if (trackingInput || typeof document === "undefined") return;
|
|
117
|
+
trackingInput = true;
|
|
118
|
+
document.addEventListener(
|
|
119
|
+
"pointerdown",
|
|
120
|
+
(e) => {
|
|
121
|
+
lastInput = e.pointerType;
|
|
122
|
+
},
|
|
123
|
+
{ capture: true, passive: true }
|
|
124
|
+
);
|
|
125
|
+
document.addEventListener(
|
|
126
|
+
"keydown",
|
|
127
|
+
(e) => {
|
|
128
|
+
if (e.key === "Unidentified" || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
129
|
+
lastInput = "keyboard";
|
|
130
|
+
},
|
|
131
|
+
{ capture: true, passive: true }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
function focusShows(target, pressedByMouse) {
|
|
135
|
+
if (lastInput !== null && isTap(lastInput)) return false;
|
|
136
|
+
if (!pressedByMouse || !(target instanceof Element)) return true;
|
|
137
|
+
try {
|
|
138
|
+
return target.matches(":focus-visible");
|
|
139
|
+
} catch {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function useTooltipTrigger(triggerRef, tap, passed, onArm) {
|
|
144
|
+
const [hovered, setHovered] = useState(false);
|
|
145
|
+
const [focused, setFocused] = useState(false);
|
|
146
|
+
const [tapped, setTapped] = useState(false);
|
|
147
|
+
const [dismissed, setDismissed] = useState(false);
|
|
148
|
+
const pressedByTouch = useRef(false);
|
|
149
|
+
const pressedByMouse = useRef(false);
|
|
150
|
+
const press = useRef(null);
|
|
151
|
+
useEffect(trackInput, []);
|
|
152
|
+
const open = (hovered || focused || tapped) && !dismissed;
|
|
153
|
+
const close = () => {
|
|
154
|
+
setHovered(false);
|
|
155
|
+
setFocused(false);
|
|
156
|
+
setTapped(false);
|
|
157
|
+
};
|
|
158
|
+
useEscapeKey(() => setDismissed(true), open);
|
|
159
|
+
useOutsideClick(triggerRef, close, open);
|
|
160
|
+
const arm = (el) => {
|
|
161
|
+
setDismissed(false);
|
|
162
|
+
onArm(el);
|
|
163
|
+
};
|
|
164
|
+
const handlers = {
|
|
165
|
+
onPointerEnter: (e) => {
|
|
166
|
+
if (e.pointerType === "touch") pressedByTouch.current = true;
|
|
167
|
+
else if (e.buttons === 0) pressedByTouch.current = false;
|
|
168
|
+
passed.onPointerEnter?.(e);
|
|
169
|
+
},
|
|
170
|
+
onPointerDown: (e) => {
|
|
171
|
+
if (isTap(e.pointerType)) {
|
|
172
|
+
pressedByTouch.current = true;
|
|
173
|
+
press.current = { wasOpen: open };
|
|
174
|
+
close();
|
|
175
|
+
} else {
|
|
176
|
+
pressedByTouch.current = false;
|
|
177
|
+
pressedByMouse.current = true;
|
|
178
|
+
}
|
|
179
|
+
passed.onPointerDown?.(e);
|
|
180
|
+
},
|
|
181
|
+
onPointerUp: (e) => {
|
|
182
|
+
pressedByMouse.current = false;
|
|
183
|
+
const began = press.current;
|
|
184
|
+
press.current = null;
|
|
185
|
+
if (began && !began.wasOpen && isTap(e.pointerType) && tapShows(tap, e.target)) {
|
|
186
|
+
setTapped(true);
|
|
187
|
+
arm(e.currentTarget);
|
|
188
|
+
}
|
|
189
|
+
passed.onPointerUp?.(e);
|
|
190
|
+
},
|
|
191
|
+
// The browser took the gesture over — a scroll, a pinch: not a tap.
|
|
192
|
+
onPointerCancel: (e) => {
|
|
193
|
+
pressedByMouse.current = false;
|
|
194
|
+
press.current = null;
|
|
195
|
+
passed.onPointerCancel?.(e);
|
|
196
|
+
},
|
|
197
|
+
onMouseEnter: (e) => {
|
|
198
|
+
if (pressedByTouch.current) return;
|
|
199
|
+
setHovered(true);
|
|
200
|
+
arm(e.currentTarget);
|
|
201
|
+
},
|
|
202
|
+
onMouseLeave: () => setHovered(false),
|
|
203
|
+
onFocus: (e) => {
|
|
204
|
+
if (!focusShows(e.target, pressedByMouse.current)) return;
|
|
205
|
+
setFocused(true);
|
|
206
|
+
arm(e.currentTarget);
|
|
207
|
+
},
|
|
208
|
+
onBlur: (e) => {
|
|
209
|
+
setFocused(false);
|
|
210
|
+
if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;
|
|
211
|
+
setTapped(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
return { open, dismissed, handlers };
|
|
215
|
+
}
|
|
72
216
|
function InPlaceTooltip({
|
|
73
217
|
label,
|
|
74
218
|
side,
|
|
@@ -76,57 +220,40 @@ function InPlaceTooltip({
|
|
|
76
220
|
redact,
|
|
77
221
|
detect,
|
|
78
222
|
lazy,
|
|
223
|
+
tap,
|
|
79
224
|
children,
|
|
80
225
|
...rest
|
|
81
226
|
}) {
|
|
82
227
|
const id = useId();
|
|
83
228
|
const triggerRef = useRef(null);
|
|
84
229
|
const [clipped, setClipped] = useState(false);
|
|
85
|
-
const [hovered, setHovered] = useState(false);
|
|
86
|
-
const [focused, setFocused] = useState(false);
|
|
87
|
-
const [dismissed, setDismissed] = useState(false);
|
|
88
230
|
const [dir, setDir] = useState("ltr");
|
|
89
|
-
const open = (
|
|
90
|
-
|
|
231
|
+
const { open, dismissed, handlers } = useTooltipTrigger(triggerRef, tap, rest, (el) => {
|
|
232
|
+
setDir(dirOf(el));
|
|
233
|
+
if (detect) setClipped(hasClippingAncestor(el));
|
|
234
|
+
});
|
|
91
235
|
const bubbleRef = useRef(null);
|
|
92
|
-
const shift = useViewportClamp(bubbleRef, open && !clipped, side, label);
|
|
236
|
+
const { shift, measuring } = useViewportClamp(bubbleRef, open && !clipped, side, label, dir);
|
|
93
237
|
useLayoutEffect(() => {
|
|
94
238
|
if (detect) setClipped(hasClippingAncestor(triggerRef.current));
|
|
95
239
|
}, [detect]);
|
|
96
|
-
const arm = (el) => {
|
|
97
|
-
setDismissed(false);
|
|
98
|
-
if (!detect) return;
|
|
99
|
-
setDir(dirOf(el));
|
|
100
|
-
setClipped(hasClippingAncestor(el));
|
|
101
|
-
};
|
|
102
240
|
return (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
241
|
+
// No role for this span: its handlers track whether the bubble is up and activate
|
|
242
|
+
// nothing; the caller's child is the interactive element, keeps its own handlers,
|
|
243
|
+
// and its focus shows the bubble too.
|
|
106
244
|
/* @__PURE__ */ jsxs(
|
|
107
245
|
"span",
|
|
108
246
|
{
|
|
109
247
|
...rest,
|
|
110
248
|
ref: triggerRef,
|
|
249
|
+
style: measuring ? { ...rest.style, overflow: "clip" } : rest.style,
|
|
111
250
|
className: cn("relative inline-flex", !clipped && "group/tooltip", className),
|
|
112
|
-
|
|
113
|
-
setHovered(true);
|
|
114
|
-
arm(e.currentTarget);
|
|
115
|
-
},
|
|
116
|
-
onMouseLeave: () => setHovered(false),
|
|
117
|
-
onFocus: (e) => {
|
|
118
|
-
setFocused(true);
|
|
119
|
-
arm(e.currentTarget);
|
|
120
|
-
},
|
|
121
|
-
onBlur: () => setFocused(false),
|
|
251
|
+
...handlers,
|
|
122
252
|
children: [
|
|
123
253
|
describedBy(children, clipped || lazy ? open ? id : void 0 : dismissed ? void 0 : id),
|
|
124
254
|
clipped ? open && /* @__PURE__ */ jsx(PortalBubble, { triggerRef, id, label, side, dir, redact }) : lazy ? (
|
|
125
255
|
// keksdose F6: the in-place slot and classes, the portalled lifetime. Mounted only
|
|
126
|
-
// while up, so it is visible whenever it exists
|
|
127
|
-
// than the `group-hover` switch, which would also be right but would make the
|
|
128
|
-
// bubble's visibility depend on two sources (the state that mounted it and the
|
|
129
|
-
// CSS that shows it) that can disagree for a frame after Escape.
|
|
256
|
+
// while up, so it is visible whenever it exists.
|
|
130
257
|
open && /* @__PURE__ */ jsx(
|
|
131
258
|
"span",
|
|
132
259
|
{
|
|
@@ -150,7 +277,8 @@ function InPlaceTooltip({
|
|
|
150
277
|
"data-private": redact ? "" : void 0,
|
|
151
278
|
className: cn(
|
|
152
279
|
TOOLTIP_SURFACE,
|
|
153
|
-
"pointer-events-none absolute z-50
|
|
280
|
+
"pointer-events-none absolute z-50",
|
|
281
|
+
open ? "opacity-100" : "hidden",
|
|
154
282
|
sidePositionClass[side]
|
|
155
283
|
),
|
|
156
284
|
children: label
|
|
@@ -174,31 +302,46 @@ function isEmptyLabel(label) {
|
|
|
174
302
|
const TOOLTIP_GAP = 4;
|
|
175
303
|
const TOOLTIP_MARGIN = 4;
|
|
176
304
|
const NO_SHIFT = { x: 0, y: 0 };
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
305
|
+
const UNPLACED = { shift: NO_SHIFT, for: null };
|
|
306
|
+
function slideInto(low, high, extent, keepHigh = false) {
|
|
307
|
+
const min = TOOLTIP_MARGIN;
|
|
308
|
+
const max = extent - TOOLTIP_MARGIN;
|
|
309
|
+
if (high - low > max - min) return keepHigh ? max - high : min - low;
|
|
310
|
+
if (low < min) return min - low;
|
|
311
|
+
if (high > max) return max - high;
|
|
180
312
|
return 0;
|
|
181
313
|
}
|
|
182
|
-
function
|
|
183
|
-
|
|
314
|
+
function viewportSize() {
|
|
315
|
+
return {
|
|
316
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
317
|
+
height: window.innerHeight
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function useViewportClamp(bubbleRef, active, side, label, dir) {
|
|
321
|
+
const [placement, setPlacement] = useState(UNPLACED);
|
|
322
|
+
const measured = placement.for !== null && placement.for.side === side && placement.for.label === label && placement.for.dir === dir;
|
|
184
323
|
useLayoutEffect(() => {
|
|
185
324
|
const el = bubbleRef.current;
|
|
186
325
|
if (!active || !el) {
|
|
187
|
-
|
|
326
|
+
setPlacement(UNPLACED);
|
|
188
327
|
return;
|
|
189
328
|
}
|
|
329
|
+
if (measured) return;
|
|
190
330
|
const r = el.getBoundingClientRect();
|
|
191
|
-
|
|
331
|
+
const unlaid = r.width === 0 && r.height === 0;
|
|
192
332
|
const horizontal = side === "top" || side === "bottom";
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
333
|
+
const viewport = viewportSize();
|
|
334
|
+
setPlacement(({ shift: previous }) => {
|
|
335
|
+
const next = unlaid ? previous : horizontal ? { x: slideInto(r.left - previous.x, r.right - previous.x, viewport.width, dir === "rtl"), y: 0 } : { x: 0, y: slideInto(r.top - previous.y, r.bottom - previous.y, viewport.height) };
|
|
336
|
+
const shift = next.x === previous.x && next.y === previous.y ? previous : next;
|
|
337
|
+
return { shift, for: { side, label, dir } };
|
|
196
338
|
});
|
|
197
|
-
}, [bubbleRef, active, side, label]);
|
|
198
|
-
return shift;
|
|
339
|
+
}, [bubbleRef, active, measured, side, label, dir]);
|
|
340
|
+
return { shift: placement.shift, measuring: active && !measured };
|
|
199
341
|
}
|
|
200
342
|
function shiftStyle(shift) {
|
|
201
|
-
|
|
343
|
+
if (shift.x === 0 && shift.y === 0) return void 0;
|
|
344
|
+
return shift.x !== 0 ? { marginLeft: shift.x } : { marginTop: shift.y };
|
|
202
345
|
}
|
|
203
346
|
const portalTransformBySide = {
|
|
204
347
|
right: "translate(0, -50%)",
|
|
@@ -257,18 +400,14 @@ function PortalTooltip({
|
|
|
257
400
|
side,
|
|
258
401
|
className,
|
|
259
402
|
redact,
|
|
403
|
+
tap,
|
|
260
404
|
children,
|
|
261
405
|
...rest
|
|
262
406
|
}) {
|
|
263
407
|
const triggerRef = useRef(null);
|
|
264
|
-
const [visible, setVisible] = useState(false);
|
|
265
408
|
const [dir, setDir] = useState("ltr");
|
|
266
|
-
const show = (el) => {
|
|
267
|
-
setDir(dirOf(el));
|
|
268
|
-
setVisible(true);
|
|
269
|
-
};
|
|
270
409
|
const id = useId();
|
|
271
|
-
|
|
410
|
+
const { open, handlers } = useTooltipTrigger(triggerRef, tap, rest, (el) => setDir(dirOf(el)));
|
|
272
411
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
273
412
|
/* @__PURE__ */ jsx(
|
|
274
413
|
"span",
|
|
@@ -276,14 +415,11 @@ function PortalTooltip({
|
|
|
276
415
|
...rest,
|
|
277
416
|
ref: triggerRef,
|
|
278
417
|
className: cn("relative inline-flex", className),
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
onFocus: (e) => show(e.currentTarget),
|
|
282
|
-
onBlur: () => setVisible(false),
|
|
283
|
-
children: describedBy(children, visible ? id : void 0)
|
|
418
|
+
...handlers,
|
|
419
|
+
children: describedBy(children, open ? id : void 0)
|
|
284
420
|
}
|
|
285
421
|
),
|
|
286
|
-
|
|
422
|
+
open && /* @__PURE__ */ jsx(PortalBubble, { triggerRef, id, label, side, dir, redact })
|
|
287
423
|
] });
|
|
288
424
|
}
|
|
289
425
|
function PortalBubble({
|
|
@@ -303,7 +439,8 @@ function PortalBubble({
|
|
|
303
439
|
if (!measured) return null;
|
|
304
440
|
const next = {
|
|
305
441
|
size: { width: measured.width, height: measured.height },
|
|
306
|
-
|
|
442
|
+
// The glass, not the window: see viewportSize (live #381).
|
|
443
|
+
viewport: viewportSize()
|
|
307
444
|
};
|
|
308
445
|
return previous && sameRoom(previous, next) ? previous : next;
|
|
309
446
|
});
|