@tensor_1/react 2.41.0 → 2.43.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 +24 -0
- package/dist/components/Button.d.ts +19 -1
- package/dist/components/Button.js +13 -8
- package/dist/components/DescriptionList.d.ts +11 -0
- package/dist/components/DiscardPrompt.d.ts +54 -0
- package/dist/components/DiscardPrompt.js +245 -0
- package/dist/components/Drawer.d.ts +19 -3
- package/dist/components/Drawer.js +60 -21
- package/dist/components/EditorialHome.d.ts +1 -1
- package/dist/components/Field.d.ts +7 -1
- package/dist/components/Field.js +5 -4
- package/dist/components/Input.d.ts +5 -0
- package/dist/components/Input.js +2 -1
- package/dist/components/Kbd.d.ts +20 -1
- package/dist/components/Kbd.js +30 -7
- package/dist/components/Menu.d.ts +11 -4
- package/dist/components/Menu.js +17 -3
- package/dist/components/MetricInput.d.ts +8 -1
- package/dist/components/MetricInput.js +11 -3
- package/dist/components/Modal.d.ts +15 -1
- package/dist/components/Modal.js +58 -13
- package/dist/components/OrgChart.d.ts +121 -0
- package/dist/components/OrgChart.js +683 -0
- package/dist/components/SearchBar.d.ts +67 -0
- package/dist/components/SearchBar.js +117 -0
- package/dist/components/Select.js +5 -4
- package/dist/components/ShortcutHint.d.ts +29 -0
- package/dist/components/ShortcutHint.js +149 -0
- package/dist/components/Tooltip.d.ts +10 -1
- package/dist/components/Tooltip.js +15 -4
- package/dist/emoji.cjs +108 -31
- package/dist/hooks/useAnchoredPosition.d.ts +13 -1
- package/dist/hooks/useAnchoredPosition.js +19 -11
- package/dist/hooks/useDialog.d.ts +6 -1
- package/dist/hooks/useDialog.js +10 -1
- package/dist/index.cjs +3911 -2567
- package/dist/index.d.cts +340 -39
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -1
- package/dist/labels-context.d.ts +11 -0
- package/dist/rich-text.cjs +17 -10
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, Ref } from 'react';
|
|
3
|
+
import { ButtonVariant } from './Button.js';
|
|
4
|
+
import '../link.js';
|
|
5
|
+
import './Kbd.js';
|
|
6
|
+
|
|
7
|
+
/** The words of a `SearchBar` (HAR-2030); the field's own name is its `label`. */
|
|
8
|
+
interface SearchBarLabels {
|
|
9
|
+
/** Accessible name of the clear button. */
|
|
10
|
+
clear: string;
|
|
11
|
+
/** Text of the submit button (`submitButton`). */
|
|
12
|
+
submit: string;
|
|
13
|
+
}
|
|
14
|
+
interface SearchBarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSubmit' | 'onChange' | 'defaultValue'> {
|
|
15
|
+
/**
|
|
16
|
+
* Accessible name of the field and of its search landmark, e.g. "Search services". It is not
|
|
17
|
+
* shown (the placeholder is not a label): the field shows `placeholder`.
|
|
18
|
+
*/
|
|
19
|
+
label: string;
|
|
20
|
+
/** The text in the field (controlled). Leave it out, with `defaultValue`, for an uncontrolled field. */
|
|
21
|
+
value?: string;
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
/** Called on every edit, and with `''` when the field is cleared. */
|
|
24
|
+
onValueChange?: (value: string) => void;
|
|
25
|
+
/** Enter in the field, or the submit button: search for this text. */
|
|
26
|
+
onSubmit?: (value: string) => void;
|
|
27
|
+
/** After the clear button or Escape emptied the field. Focus stays in the field. */
|
|
28
|
+
onClear?: () => void;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
/** `md` (default) for a header or a toolbar; `lg` for a page-level or public search: 44 px, the touch floor. */
|
|
31
|
+
size?: 'md' | 'lg';
|
|
32
|
+
/** A submit button after the field, with `labels.submit`. Enter submits either way. */
|
|
33
|
+
submitButton?: boolean;
|
|
34
|
+
/** The submit button's variant. Default `primary`. */
|
|
35
|
+
submitVariant?: ButtonVariant;
|
|
36
|
+
/**
|
|
37
|
+
* A `search` landmark named by `label` (default). Pass `false` for a field that filters a list
|
|
38
|
+
* or a table, where one more landmark per toolbar would crowd the landmark list. Never wrap it
|
|
39
|
+
* in a `<search>` or `role="search"` of your own: that nests two landmarks.
|
|
40
|
+
*/
|
|
41
|
+
landmark?: boolean;
|
|
42
|
+
/** Ids of elements that describe the field (a hint, a result count), as its `aria-describedby`. */
|
|
43
|
+
inputDescribedBy?: string;
|
|
44
|
+
/** The words. Default: `UixLabelsProvider` `searchBar`, then English. */
|
|
45
|
+
labels?: Partial<SearchBarLabels>;
|
|
46
|
+
/** The field's `name`. */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** The field's `id`, e.g. for a "/" shortcut that focuses it. Default: generated. */
|
|
49
|
+
inputId?: string;
|
|
50
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
51
|
+
autoFocus?: boolean;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One search field look and behaviour (HAR-2030; TENSOR audit UXC-02) over the `.uix-searchbar`
|
|
56
|
+
* contract: a leading magnifier, a clear button while there is text, Enter to search, Escape to
|
|
57
|
+
* clear (an empty field lets Escape through, so it still closes the dialog it sits in). It is a
|
|
58
|
+
* `search` landmark named by `label`.
|
|
59
|
+
*
|
|
60
|
+
* It is not a `<form>`, so it can sit inside one: Enter calls `onSubmit` and never submits a
|
|
61
|
+
* surrounding form. For suggestions while typing, use `SearchSuggest`. Inside an overlay that
|
|
62
|
+
* takes Escape itself before the field (a ConfirmPopover, the table's view menu), Escape closes
|
|
63
|
+
* the overlay rather than clearing the text.
|
|
64
|
+
*/
|
|
65
|
+
declare function SearchBar({ label, value: valueProp, defaultValue, onValueChange, onSubmit, onClear, placeholder, size, submitButton, submitVariant, landmark, inputDescribedBy, labels: labelsProp, name, inputId, inputRef, autoFocus, disabled, className, ...props }: SearchBarProps): react.JSX.Element;
|
|
66
|
+
|
|
67
|
+
export { SearchBar, type SearchBarLabels, type SearchBarProps };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { useUixLabels } from "../labels-context.js";
|
|
6
|
+
import { Button } from "./Button.js";
|
|
7
|
+
const DEFAULT_SEARCH_BAR_LABELS = { clear: "Clear search", submit: "Search" };
|
|
8
|
+
const SearchIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [
|
|
9
|
+
/* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
|
|
10
|
+
/* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })
|
|
11
|
+
] });
|
|
12
|
+
const ClearIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
|
|
13
|
+
function SearchBar({
|
|
14
|
+
label,
|
|
15
|
+
value: valueProp,
|
|
16
|
+
defaultValue = "",
|
|
17
|
+
onValueChange,
|
|
18
|
+
onSubmit,
|
|
19
|
+
onClear,
|
|
20
|
+
placeholder,
|
|
21
|
+
size = "md",
|
|
22
|
+
submitButton = false,
|
|
23
|
+
submitVariant = "primary",
|
|
24
|
+
landmark = true,
|
|
25
|
+
inputDescribedBy,
|
|
26
|
+
labels: labelsProp,
|
|
27
|
+
name,
|
|
28
|
+
inputId,
|
|
29
|
+
inputRef,
|
|
30
|
+
autoFocus,
|
|
31
|
+
disabled,
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}) {
|
|
35
|
+
const uixLabels = useUixLabels();
|
|
36
|
+
const labels = { ...DEFAULT_SEARCH_BAR_LABELS, ...uixLabels.searchBar, ...labelsProp };
|
|
37
|
+
const id = useId();
|
|
38
|
+
const fieldId = inputId ?? `${id}-field`;
|
|
39
|
+
const labelId = `${id}-label`;
|
|
40
|
+
const field = useRef(null);
|
|
41
|
+
const [own, setOwn] = useState(defaultValue);
|
|
42
|
+
const value = valueProp ?? own;
|
|
43
|
+
const change = (next) => {
|
|
44
|
+
setOwn(next);
|
|
45
|
+
onValueChange?.(next);
|
|
46
|
+
};
|
|
47
|
+
const clear = () => {
|
|
48
|
+
change("");
|
|
49
|
+
field.current?.focus();
|
|
50
|
+
onClear?.();
|
|
51
|
+
};
|
|
52
|
+
const onKeyDown = (event) => {
|
|
53
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
54
|
+
if (event.key === "Enter") {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
onSubmit?.(value);
|
|
57
|
+
} else if (event.key === "Escape" && value !== "") {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
clear();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const setField = useCallback((node) => {
|
|
64
|
+
field.current = node;
|
|
65
|
+
if (typeof inputRef === "function") inputRef(node);
|
|
66
|
+
else if (inputRef) inputRef.current = node;
|
|
67
|
+
}, [inputRef]);
|
|
68
|
+
return /* @__PURE__ */ jsxs(
|
|
69
|
+
"div",
|
|
70
|
+
{
|
|
71
|
+
role: landmark ? "search" : void 0,
|
|
72
|
+
"aria-labelledby": landmark ? labelId : void 0,
|
|
73
|
+
className: cx("uix-searchbar", size === "md" && "uix-searchbar--md", className),
|
|
74
|
+
...props,
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-searchbar__wrap", children: [
|
|
77
|
+
/* @__PURE__ */ jsx("label", { id: labelId, htmlFor: fieldId, className: "uix-visually-hidden", children: label }),
|
|
78
|
+
/* @__PURE__ */ jsx(SearchIcon, {}),
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"input",
|
|
81
|
+
{
|
|
82
|
+
ref: setField,
|
|
83
|
+
id: fieldId,
|
|
84
|
+
type: "search",
|
|
85
|
+
name,
|
|
86
|
+
className: "uix-input",
|
|
87
|
+
value,
|
|
88
|
+
placeholder,
|
|
89
|
+
autoComplete: "off",
|
|
90
|
+
enterKeyHint: "search",
|
|
91
|
+
"aria-describedby": inputDescribedBy,
|
|
92
|
+
autoFocus,
|
|
93
|
+
disabled,
|
|
94
|
+
onChange: (event) => change(event.currentTarget.value),
|
|
95
|
+
onKeyDown
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
value !== "" && !disabled && /* @__PURE__ */ jsx(
|
|
99
|
+
"button",
|
|
100
|
+
{
|
|
101
|
+
type: "button",
|
|
102
|
+
className: "uix-searchbar__clear",
|
|
103
|
+
"aria-label": labels.clear,
|
|
104
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
105
|
+
onClick: clear,
|
|
106
|
+
children: /* @__PURE__ */ jsx(ClearIcon, {})
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
] }),
|
|
110
|
+
submitButton && /* @__PURE__ */ jsx(Button, { type: "button", variant: submitVariant, size: size === "lg" ? "lg" : "md", disabled, onClick: () => onSubmit?.(value), children: labels.submit })
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
SearchBar
|
|
117
|
+
};
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Children, Fragment as Fragment2, forwardRef, isValidElement, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useUixLabels } from "../labels-context.js";
|
|
6
|
-
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
+
import { useAnchoredPosition, zoomOf } from "../hooks/useAnchoredPosition.js";
|
|
7
7
|
import { Drawer } from "./Drawer.js";
|
|
8
8
|
import {
|
|
9
9
|
buildSelectModel,
|
|
@@ -537,9 +537,10 @@ const Select = forwardRef(function Select2(props, forwardedRef) {
|
|
|
537
537
|
}
|
|
538
538
|
};
|
|
539
539
|
if (showing) {
|
|
540
|
+
const zoom = zoomOf(trigger);
|
|
540
541
|
const r = trigger.getBoundingClientRect();
|
|
541
|
-
const room = Math.max(r.top, window.innerHeight - r.bottom) - OFFSET - EDGE;
|
|
542
|
-
popup.style.minWidth = `${Math.round(r.width)}px`;
|
|
542
|
+
const room = Math.max(r.top, window.innerHeight - r.bottom) / zoom - OFFSET - EDGE;
|
|
543
|
+
popup.style.minWidth = `${Math.round(r.width / zoom)}px`;
|
|
543
544
|
popup.style.maxHeight = `${Math.max(96, Math.min(MAX_HEIGHT, Math.floor(room)))}px`;
|
|
544
545
|
try {
|
|
545
546
|
if (!isOpen()) popup.showPopover();
|
|
@@ -581,7 +582,7 @@ const Select = forwardRef(function Select2(props, forwardedRef) {
|
|
|
581
582
|
doc.addEventListener("pointerdown", onDown, true);
|
|
582
583
|
return () => doc.removeEventListener("pointerdown", onDown, true);
|
|
583
584
|
}, [showing]);
|
|
584
|
-
|
|
585
|
+
useIsomorphicLayoutEffect(() => {
|
|
585
586
|
if (!open) return;
|
|
586
587
|
if (active >= entries.length || active < 0 && entries.length) setActive(entries.length ? selectedIndexIn(entries) >= 0 ? selectedIndexIn(entries) : firstEnabled(entries) : -1);
|
|
587
588
|
}, [open, entries.length, query]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { KeyShortcut } from './Kbd.js';
|
|
3
|
+
|
|
4
|
+
interface ShortcutHintProps {
|
|
5
|
+
keys: KeyShortcut;
|
|
6
|
+
/** Text before the keys, e.g. an icon button's name. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Set the button's `aria-keyshortcuts`. `false` when the button already has its own (a prop, or a Tooltip's). */
|
|
9
|
+
ownsAria: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The hover / keyboard-focus hint of a `Button` with `shortcut` (HAR-2031). In the button it
|
|
13
|
+
* leaves only an empty, hidden marker, so the button's text, name, layout and selectors are
|
|
14
|
+
* unchanged (no wrapper, no sibling). The bubble itself is portalled after mount into the
|
|
15
|
+
* nearest `<dialog>` (outside one a modal dialog makes everything inert) or `<body>`, as a
|
|
16
|
+
* top-layer popover, `aria-hidden`: assistive technology gets the key from the button's
|
|
17
|
+
* `aria-keyshortcuts`, which this sets for the viewer's platform (Mod is Meta on Apple, Control
|
|
18
|
+
* elsewhere) unless the button already has its own.
|
|
19
|
+
*
|
|
20
|
+
* It opens on hover and on keyboard focus (`:focus-visible`), so focus a menu or dialog sends back
|
|
21
|
+
* after a mouse choice does not open it. It closes on leave, blur, Escape and when the button is
|
|
22
|
+
* disabled, and stays while the pointer is on it (WCAG 1.4.13). Inside a `Tooltip` it steps aside:
|
|
23
|
+
* the Tooltip shows the keys in its own bubble.
|
|
24
|
+
*
|
|
25
|
+
* It is its own client component so that `Button`, which has no hooks, still renders on a server.
|
|
26
|
+
*/
|
|
27
|
+
declare function ShortcutHint({ keys, label, ownsAria }: ShortcutHintProps): react.JSX.Element;
|
|
28
|
+
|
|
29
|
+
export { ShortcutHint, type ShortcutHintProps };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
+
import { KbdCombo, ariaKeyShortcuts, useKbdPlatform } from "./Kbd.js";
|
|
7
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
8
|
+
const isShown = (el) => {
|
|
9
|
+
try {
|
|
10
|
+
return !!el?.matches(":popover-open");
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const focusVisible = (el) => {
|
|
16
|
+
try {
|
|
17
|
+
return el.matches(":focus-visible");
|
|
18
|
+
} catch {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const stop = (event) => event.stopPropagation();
|
|
23
|
+
function ShortcutHint({ keys, label, ownsAria }) {
|
|
24
|
+
const markerRef = useRef(null);
|
|
25
|
+
const bubbleRef = useRef(null);
|
|
26
|
+
const anchorRef = useRef(null);
|
|
27
|
+
const [host, setHost] = useState(null);
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const platform = useKbdPlatform();
|
|
30
|
+
const shortcuts = ariaKeyShortcuts(keys, platform);
|
|
31
|
+
useAnchoredPosition(anchorRef, bubbleRef, { open, placement: "top", offset: 6 });
|
|
32
|
+
useIsomorphicLayoutEffect(() => {
|
|
33
|
+
const button = markerRef.current?.parentElement;
|
|
34
|
+
if (!button || !ownsAria) return void 0;
|
|
35
|
+
const before = button.getAttribute("aria-keyshortcuts");
|
|
36
|
+
button.setAttribute("aria-keyshortcuts", shortcuts);
|
|
37
|
+
return () => {
|
|
38
|
+
if (button.getAttribute("aria-keyshortcuts") !== shortcuts) return;
|
|
39
|
+
if (before == null) button.removeAttribute("aria-keyshortcuts");
|
|
40
|
+
else button.setAttribute("aria-keyshortcuts", before);
|
|
41
|
+
};
|
|
42
|
+
}, [shortcuts, ownsAria]);
|
|
43
|
+
useIsomorphicLayoutEffect(() => {
|
|
44
|
+
const button = markerRef.current?.parentElement ?? null;
|
|
45
|
+
anchorRef.current = button;
|
|
46
|
+
if (!button || button.parentElement?.closest(".uix-tooltip")) {
|
|
47
|
+
setHost(null);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setHost(button.closest("dialog") ?? button.ownerDocument.body);
|
|
51
|
+
}, []);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const button = anchorRef.current;
|
|
54
|
+
const bubble = bubbleRef.current;
|
|
55
|
+
if (!host || !button || !bubble) return void 0;
|
|
56
|
+
let timer;
|
|
57
|
+
const show = () => {
|
|
58
|
+
window.clearTimeout(timer);
|
|
59
|
+
if (!isShown(bubble)) {
|
|
60
|
+
try {
|
|
61
|
+
bubble.showPopover();
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
setOpen(true);
|
|
66
|
+
};
|
|
67
|
+
const hide = () => {
|
|
68
|
+
window.clearTimeout(timer);
|
|
69
|
+
if (isShown(bubble)) {
|
|
70
|
+
try {
|
|
71
|
+
bubble.hidePopover();
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
setOpen(false);
|
|
76
|
+
};
|
|
77
|
+
const hideSoon = () => {
|
|
78
|
+
window.clearTimeout(timer);
|
|
79
|
+
timer = window.setTimeout(() => {
|
|
80
|
+
if (button.ownerDocument.activeElement === button && focusVisible(button)) return;
|
|
81
|
+
hide();
|
|
82
|
+
}, 100);
|
|
83
|
+
};
|
|
84
|
+
const onFocus = () => {
|
|
85
|
+
if (focusVisible(button)) show();
|
|
86
|
+
};
|
|
87
|
+
const onKey = (event) => {
|
|
88
|
+
if (event.key === "Escape") hide();
|
|
89
|
+
};
|
|
90
|
+
const watch = new MutationObserver(() => {
|
|
91
|
+
if (button.hasAttribute("disabled") || button.getAttribute("aria-disabled") === "true") hide();
|
|
92
|
+
});
|
|
93
|
+
watch.observe(button, { attributes: true, attributeFilter: ["disabled", "aria-disabled"] });
|
|
94
|
+
button.addEventListener("mouseenter", show);
|
|
95
|
+
button.addEventListener("mouseleave", hideSoon);
|
|
96
|
+
button.addEventListener("focus", onFocus);
|
|
97
|
+
button.addEventListener("blur", hide);
|
|
98
|
+
bubble.addEventListener("mouseenter", show);
|
|
99
|
+
bubble.addEventListener("mouseleave", hideSoon);
|
|
100
|
+
button.ownerDocument.addEventListener("keydown", onKey);
|
|
101
|
+
return () => {
|
|
102
|
+
window.clearTimeout(timer);
|
|
103
|
+
watch.disconnect();
|
|
104
|
+
button.removeEventListener("mouseenter", show);
|
|
105
|
+
button.removeEventListener("mouseleave", hideSoon);
|
|
106
|
+
button.removeEventListener("focus", onFocus);
|
|
107
|
+
button.removeEventListener("blur", hide);
|
|
108
|
+
bubble.removeEventListener("mouseenter", show);
|
|
109
|
+
bubble.removeEventListener("mouseleave", hideSoon);
|
|
110
|
+
button.ownerDocument.removeEventListener("keydown", onKey);
|
|
111
|
+
if (isShown(bubble)) {
|
|
112
|
+
try {
|
|
113
|
+
bubble.hidePopover();
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}, [host]);
|
|
119
|
+
const bubbleAttr = { popover: "manual" };
|
|
120
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
121
|
+
/* @__PURE__ */ jsx("span", { ref: markerRef, hidden: true, "data-uix-shortcut-hint": "" }),
|
|
122
|
+
host && createPortal(
|
|
123
|
+
/* @__PURE__ */ jsxs(
|
|
124
|
+
"span",
|
|
125
|
+
{
|
|
126
|
+
ref: bubbleRef,
|
|
127
|
+
className: "uix-tooltip__bubble uix-tooltip__bubble--hint",
|
|
128
|
+
"aria-hidden": "true",
|
|
129
|
+
...bubbleAttr,
|
|
130
|
+
onClick: stop,
|
|
131
|
+
onDoubleClick: stop,
|
|
132
|
+
onContextMenu: stop,
|
|
133
|
+
onPointerDown: stop,
|
|
134
|
+
onPointerUp: stop,
|
|
135
|
+
onMouseDown: stop,
|
|
136
|
+
onMouseUp: stop,
|
|
137
|
+
children: [
|
|
138
|
+
label && /* @__PURE__ */ jsx("span", { children: label }),
|
|
139
|
+
/* @__PURE__ */ jsx("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ jsx(KbdCombo, { keys: [...keys], platform }) })
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
),
|
|
143
|
+
host
|
|
144
|
+
)
|
|
145
|
+
] });
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
ShortcutHint
|
|
149
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
import { Placement } from '../overlay-position.js';
|
|
4
|
+
import { KeyShortcut } from './Kbd.js';
|
|
4
5
|
|
|
5
6
|
interface TooltipProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'content'> {
|
|
6
7
|
/** Tooltip text. Pass this or `content`. */
|
|
@@ -13,6 +14,14 @@ interface TooltipProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'content'>
|
|
|
13
14
|
content?: ReactNode;
|
|
14
15
|
/** Preferred placement. Default `'top'`. */
|
|
15
16
|
placement?: Placement;
|
|
17
|
+
/**
|
|
18
|
+
* The trigger's keyboard shortcut, in `KbdCombo` key names, e.g. `['Mod', 'K']` (HAR-2031):
|
|
19
|
+
* shown with `Kbd` after the text, and set as the trigger's `aria-keyshortcuts` for the
|
|
20
|
+
* viewer's platform. Default: the `shortcut` of a `Button` child, so a Tooltip around a Button
|
|
21
|
+
* with a shortcut shows one bubble with the label and the keys. The kit only shows it: the
|
|
22
|
+
* product binds the key.
|
|
23
|
+
*/
|
|
24
|
+
shortcut?: KeyShortcut;
|
|
16
25
|
children?: ReactNode;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
@@ -23,6 +32,6 @@ interface TooltipProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'content'>
|
|
|
23
32
|
* (document-level, per WCAG 1.4.13), and hoverable: the bubble stays open while
|
|
24
33
|
* the pointer is over it (UIX-A11Y-1).
|
|
25
34
|
*/
|
|
26
|
-
declare function Tooltip({ label, content, placement, className, children, ...props }: TooltipProps): react.JSX.Element;
|
|
35
|
+
declare function Tooltip({ label, content, placement, shortcut, className, children, ...props }: TooltipProps): react.JSX.Element;
|
|
27
36
|
|
|
28
37
|
export { Tooltip, type TooltipProps };
|
|
@@ -3,12 +3,18 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { cloneElement, isValidElement, useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
-
|
|
6
|
+
import { KbdCombo, ariaKeyShortcuts, isKeyShortcut, useKbdPlatform } from "./Kbd.js";
|
|
7
|
+
function Tooltip({ label, content, placement = "top", shortcut, className, children, ...props }) {
|
|
7
8
|
const triggerRef = useRef(null);
|
|
8
9
|
const bubbleRef = useRef(null);
|
|
9
10
|
const closeTimer = useRef(null);
|
|
10
11
|
const [open, setOpen] = useState(false);
|
|
11
12
|
const id = useId();
|
|
13
|
+
const platform = useKbdPlatform();
|
|
14
|
+
const childShortcut = isValidElement(children) ? children.props.shortcut : void 0;
|
|
15
|
+
const own = shortcut ?? (isKeyShortcut(childShortcut) ? childShortcut : void 0);
|
|
16
|
+
const keys = own && own.length > 0 ? own : null;
|
|
17
|
+
const keyshortcuts = keys ? ariaKeyShortcuts(keys, platform) : void 0;
|
|
12
18
|
useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
|
|
13
19
|
const cancelClose = () => {
|
|
14
20
|
if (closeTimer.current != null) {
|
|
@@ -63,7 +69,8 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
|
|
|
63
69
|
const bubbleAttr = { popover: "manual" };
|
|
64
70
|
const single = isValidElement(children);
|
|
65
71
|
const child = single ? cloneElement(children, {
|
|
66
|
-
"aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" ")
|
|
72
|
+
"aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" "),
|
|
73
|
+
...keyshortcuts ? { "aria-keyshortcuts": keyshortcuts } : {}
|
|
67
74
|
}) : children;
|
|
68
75
|
return /* @__PURE__ */ jsxs(
|
|
69
76
|
"span",
|
|
@@ -71,6 +78,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
|
|
|
71
78
|
ref: triggerRef,
|
|
72
79
|
className: cx("uix-tooltip", className),
|
|
73
80
|
"aria-describedby": single ? void 0 : id,
|
|
81
|
+
"aria-keyshortcuts": single ? void 0 : keyshortcuts,
|
|
74
82
|
tabIndex: single ? void 0 : 0,
|
|
75
83
|
onMouseEnter: show,
|
|
76
84
|
onMouseLeave: scheduleHide,
|
|
@@ -82,7 +90,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
|
|
|
82
90
|
...props,
|
|
83
91
|
children: [
|
|
84
92
|
child,
|
|
85
|
-
/* @__PURE__ */
|
|
93
|
+
/* @__PURE__ */ jsxs(
|
|
86
94
|
"span",
|
|
87
95
|
{
|
|
88
96
|
ref: bubbleRef,
|
|
@@ -92,7 +100,10 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
|
|
|
92
100
|
onMouseEnter: cancelClose,
|
|
93
101
|
onMouseLeave: scheduleHide,
|
|
94
102
|
...bubbleAttr,
|
|
95
|
-
children:
|
|
103
|
+
children: [
|
|
104
|
+
content ?? label,
|
|
105
|
+
keys && /* @__PURE__ */ jsx("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ jsx(KbdCombo, { keys: [...keys], platform }) })
|
|
106
|
+
]
|
|
96
107
|
}
|
|
97
108
|
)
|
|
98
109
|
]
|