@crisp-ui-kit/crisp 0.46.0 → 0.48.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/dist/avatar.css +3 -3
- package/dist/avatar.layered.css +3 -3
- package/dist/cjs/components/avatar/avatar.recipe.js +3 -3
- package/dist/cjs/components/table/table.recipe.js +1 -1
- package/dist/cjs/components/tooltip/tooltip.js +85 -26
- package/dist/esm/components/avatar/avatar.recipe.js +3 -3
- package/dist/esm/components/table/table.recipe.js +1 -1
- package/dist/esm/components/tooltip/tooltip.js +83 -24
- package/dist/styles.css +4 -4
- package/dist/styles.layered.css +4 -4
- package/dist/table.css +1 -1
- package/dist/table.layered.css +1 -1
- package/dist/types/components/tooltip/tooltip.d.ts +66 -11
- package/package.json +1 -1
package/dist/avatar.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
.crisp-avatar {
|
|
2
|
-
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-
|
|
2
|
+
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
|
|
3
3
|
& img { width: 100%; height: 100%; object-fit: cover; }
|
|
4
4
|
}
|
|
5
5
|
.crisp-avatar--size_sm {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
height: 20px; width: 20px; font-size: 10px;
|
|
10
10
|
}
|
|
11
11
|
.crisp-avatar--size_lg {
|
|
12
|
-
height: 24px; width: 24px; font-size: var(--crisp-text-
|
|
12
|
+
height: 24px; width: 24px; font-size: var(--crisp-text-lg);
|
|
13
13
|
}
|
|
14
14
|
.crisp-avatar--size_xl {
|
|
15
15
|
height: 32px; width: 32px; font-size: var(--crisp-text-lg);
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
border-radius: 30%;
|
|
22
22
|
}
|
|
23
23
|
.crisp-avatar--tone_neutral {
|
|
24
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
24
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
25
25
|
}
|
|
26
26
|
.crisp-avatar--tone_brand {
|
|
27
27
|
background: var(--crisp-bg-brand-solid); color: #fff;
|
package/dist/avatar.layered.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
2
|
.crisp-avatar {
|
|
3
|
-
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-
|
|
3
|
+
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
|
|
4
4
|
& img { width: 100%; height: 100%; object-fit: cover; }
|
|
5
5
|
}
|
|
6
6
|
.crisp-avatar--size_sm {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
height: 20px; width: 20px; font-size: 10px;
|
|
11
11
|
}
|
|
12
12
|
.crisp-avatar--size_lg {
|
|
13
|
-
height: 24px; width: 24px; font-size: var(--crisp-text-
|
|
13
|
+
height: 24px; width: 24px; font-size: var(--crisp-text-lg);
|
|
14
14
|
}
|
|
15
15
|
.crisp-avatar--size_xl {
|
|
16
16
|
height: 32px; width: 32px; font-size: var(--crisp-text-lg);
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
border-radius: 30%;
|
|
23
23
|
}
|
|
24
24
|
.crisp-avatar--tone_neutral {
|
|
25
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
25
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
26
26
|
}
|
|
27
27
|
.crisp-avatar--tone_brand {
|
|
28
28
|
background: var(--crisp-bg-brand-solid); color: #fff;
|
|
@@ -11,7 +11,7 @@ const spec = (0, core_1.defineSpec)({
|
|
|
11
11
|
justifyContent: "center",
|
|
12
12
|
flexShrink: 0,
|
|
13
13
|
fontFamily: tokens_1.contract.font.sans,
|
|
14
|
-
fontWeight: tokens_1.contract.weight.
|
|
14
|
+
fontWeight: tokens_1.contract.weight.medium,
|
|
15
15
|
overflow: "hidden",
|
|
16
16
|
userSelect: "none",
|
|
17
17
|
},
|
|
@@ -26,7 +26,7 @@ const spec = (0, core_1.defineSpec)({
|
|
|
26
26
|
size: {
|
|
27
27
|
sm: { height: "16px", width: "16px", fontSize: "9px" },
|
|
28
28
|
md: { height: "20px", width: "20px", fontSize: "10px" },
|
|
29
|
-
lg: { height: "24px", width: "24px", fontSize: tokens_1.contract.text.
|
|
29
|
+
lg: { height: "24px", width: "24px", fontSize: tokens_1.contract.text.lg },
|
|
30
30
|
xl: { height: "32px", width: "32px", fontSize: tokens_1.contract.text.lg },
|
|
31
31
|
},
|
|
32
32
|
shape: {
|
|
@@ -36,7 +36,7 @@ const spec = (0, core_1.defineSpec)({
|
|
|
36
36
|
tone: {
|
|
37
37
|
neutral: {
|
|
38
38
|
background: tokens_1.contract.bgRaised,
|
|
39
|
-
color: tokens_1.contract.
|
|
39
|
+
color: tokens_1.contract.fgNeutralMuted,
|
|
40
40
|
boxShadow: `inset 0 0 0 1px ${tokens_1.contract.borderDefault}`,
|
|
41
41
|
},
|
|
42
42
|
brand: { background: tokens_1.contract.bgBrandSolid, color: "#fff" },
|
|
@@ -13,7 +13,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
13
13
|
base: `display: block; box-sizing: border-box; background: ${tokens_1.contract.bgSurface};
|
|
14
14
|
& .crisp-table-scroll { overflow-x: auto; }
|
|
15
15
|
& table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
|
|
16
|
-
& th, & td { position: relative; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
|
|
16
|
+
& th, & td { position: relative; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
17
17
|
& th { height: ${tokens_1.contract.size.tableHeader}; background: ${tokens_1.contract.bgSurface}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.14px; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); user-select: none; }
|
|
18
18
|
& td { color: ${tokens_1.contract.fgPrimary}; background: ${tokens_1.contract.bgSurface}; }
|
|
19
19
|
& th:last-child, & td:last-child { border-inline-end: none; }
|
|
@@ -33,19 +33,55 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.Tooltip =
|
|
36
|
+
exports.Tooltip = void 0;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
|
+
const react_slot_1 = require("@radix-ui/react-slot");
|
|
38
39
|
const React = __importStar(require("react"));
|
|
39
40
|
const react_dom_1 = require("react-dom");
|
|
40
41
|
const tooltip_recipe_1 = require("./tooltip.recipe");
|
|
42
|
+
function mergeRefs(...refs) {
|
|
43
|
+
return (node) => {
|
|
44
|
+
for (const ref of refs) {
|
|
45
|
+
if (!ref)
|
|
46
|
+
continue;
|
|
47
|
+
if (typeof ref === "function")
|
|
48
|
+
ref(node);
|
|
49
|
+
else
|
|
50
|
+
ref.current = node;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
41
54
|
/**
|
|
42
55
|
* A hover/focus tooltip — a dark hint chip measured live. Wraps its
|
|
43
56
|
* trigger in an inline element, shows after `delay` on pointer-enter / focus,
|
|
44
|
-
* and hides on leave / blur / Escape
|
|
45
|
-
*
|
|
57
|
+
* and hides on leave / blur / Escape (`asChild` skips the wrapper — see its
|
|
58
|
+
* doc). The bubble is portaled and positioned above the trigger (flipping
|
|
59
|
+
* below near the top edge).
|
|
60
|
+
*
|
|
61
|
+
* Composable as another component's trigger (#360: wrapping a `Popover`'s
|
|
62
|
+
* or `Menu`'s trigger in a `Tooltip` silently broke it). A composer clones
|
|
63
|
+
* a `ref`/`id`/`aria-*`/`onClick` onto whatever element it's handed —
|
|
64
|
+
* `Tooltip` wasn't `forwardRef` and didn't read any of the rest, so they
|
|
65
|
+
* landed on the `<Tooltip>` element itself, where nothing forwarded them
|
|
66
|
+
* anywhere. Both now merge onto the actual trigger via `Slot` (the same
|
|
67
|
+
* mechanism `ContextMenu`'s `asChild` uses) — the wrapping `<span>` can't
|
|
68
|
+
* carry them for the same reason it can't carry `aria-describedby` (below):
|
|
69
|
+
* a screen reader reads state off the focused element, not an ancestor.
|
|
46
70
|
*/
|
|
47
|
-
|
|
71
|
+
exports.Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, "aria-describedby": restDescribedBy,
|
|
72
|
+
// Not forwarded, even though a composer's other rest props are
|
|
73
|
+
// (below): `style` sizes a VISUAL box, and Tooltip's wrapper has none
|
|
74
|
+
// of its own -- the box that matters is whatever the consumer's own
|
|
75
|
+
// trigger renders as, which they already own and can style directly.
|
|
76
|
+
// Also matches Popover/Menu's own composer contract: their
|
|
77
|
+
// cloneElement injects ref/id/aria-*/onClick, never style.
|
|
78
|
+
style: _style, ...rest }, forwardedRef) => {
|
|
48
79
|
const tipId = React.useId();
|
|
80
|
+
// The trigger's box for positioning the bubble: the wrapper `<span>` in
|
|
81
|
+
// the default mode (it exists and hugs the child tightly even when
|
|
82
|
+
// `children` isn't a single element, so hover positioning never depends
|
|
83
|
+
// on Slot succeeding), or the child itself in `asChild` mode (there is
|
|
84
|
+
// no wrapper to measure).
|
|
49
85
|
const triggerRef = React.useRef(null);
|
|
50
86
|
const timer = React.useRef(undefined);
|
|
51
87
|
const [pos, setPos] = React.useState(null);
|
|
@@ -87,27 +123,50 @@ function Tooltip({ content, children, side = "top", delay = 350, open: openProp,
|
|
|
87
123
|
// the only way a screen reader learns of one is `aria-describedby` on the
|
|
88
124
|
// element that gets focus. It cannot go on the wrapper: a description on a
|
|
89
125
|
// non-focusable span around a button is never announced. So the trigger is
|
|
90
|
-
//
|
|
91
|
-
//
|
|
126
|
+
// Slot-merged, keeping any aria-describedby it already had (plus a
|
|
127
|
+
// composer's, if any — see restDescribedBy above), and the tipId is only
|
|
128
|
+
// present while the bubble is (a reference to a removed node is dead
|
|
92
129
|
// wiring, #225).
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
const isElement = React.isValidElement(children);
|
|
131
|
+
const ownDescribedBy = isElement
|
|
132
|
+
? children.props["aria-describedby"]
|
|
133
|
+
: undefined;
|
|
134
|
+
const describedBy = [ownDescribedBy, restDescribedBy, open ? tipId : undefined]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join(" ") || undefined;
|
|
137
|
+
// Baked onto a CLONE, not passed as a prop to <Slot> below: Slot's own
|
|
138
|
+
// mergeProps lets the child's OWN pre-existing prop win over anything
|
|
139
|
+
// unrecognised (it only special-cases style/className/on* handlers), so
|
|
140
|
+
// a `describedBy` passed to Slot would be silently discarded in favour
|
|
141
|
+
// of the child's original, un-merged `aria-describedby` the moment the
|
|
142
|
+
// child already had one.
|
|
143
|
+
const describedChild = isElement
|
|
144
|
+
? React.cloneElement(children, { "aria-describedby": describedBy })
|
|
102
145
|
: children;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
const hoverHandlers = {
|
|
147
|
+
onPointerEnter: openAfterDelay,
|
|
148
|
+
onPointerLeave: close,
|
|
149
|
+
onFocusCapture: openAfterDelay,
|
|
150
|
+
onBlurCapture: close,
|
|
151
|
+
onKeyDown: (e) => e.key === "Escape" && close(),
|
|
152
|
+
};
|
|
153
|
+
const portal = open &&
|
|
154
|
+
pos != null &&
|
|
155
|
+
(0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: tipId, role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)(), style: {
|
|
156
|
+
left: pos.left,
|
|
157
|
+
top: pos.top,
|
|
158
|
+
transform: pos.below
|
|
159
|
+
? "translate(-50%, 0)"
|
|
160
|
+
: "translate(-50%, -100%)",
|
|
161
|
+
}, children: content }), document.body);
|
|
162
|
+
if (asChild) {
|
|
163
|
+
// No wrapper at all: the child carries the hover/focus/Escape
|
|
164
|
+
// handling AND the composer's ref/id/aria-*/onClick directly. Not an
|
|
165
|
+
// element (asChild has nothing to attach to) degrades the same way
|
|
166
|
+
// #225 already documented for aria-describedby: renders, but neither
|
|
167
|
+
// hovers nor composes.
|
|
168
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [isElement ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: mergeRefs(triggerRef, forwardedRef), ...hoverHandlers, ...rest, children: describedChild })) : (children), portal] }));
|
|
169
|
+
}
|
|
170
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { ref: triggerRef, style: { display: "inline-flex" }, ...hoverHandlers, children: isElement ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: forwardedRef, ...rest, children: describedChild })) : (children) }), portal] }));
|
|
171
|
+
});
|
|
172
|
+
exports.Tooltip.displayName = "Tooltip";
|
|
@@ -8,7 +8,7 @@ const spec = defineSpec({
|
|
|
8
8
|
justifyContent: "center",
|
|
9
9
|
flexShrink: 0,
|
|
10
10
|
fontFamily: c.font.sans,
|
|
11
|
-
fontWeight: c.weight.
|
|
11
|
+
fontWeight: c.weight.medium,
|
|
12
12
|
overflow: "hidden",
|
|
13
13
|
userSelect: "none",
|
|
14
14
|
},
|
|
@@ -23,7 +23,7 @@ const spec = defineSpec({
|
|
|
23
23
|
size: {
|
|
24
24
|
sm: { height: "16px", width: "16px", fontSize: "9px" },
|
|
25
25
|
md: { height: "20px", width: "20px", fontSize: "10px" },
|
|
26
|
-
lg: { height: "24px", width: "24px", fontSize: c.text.
|
|
26
|
+
lg: { height: "24px", width: "24px", fontSize: c.text.lg },
|
|
27
27
|
xl: { height: "32px", width: "32px", fontSize: c.text.lg },
|
|
28
28
|
},
|
|
29
29
|
shape: {
|
|
@@ -33,7 +33,7 @@ const spec = defineSpec({
|
|
|
33
33
|
tone: {
|
|
34
34
|
neutral: {
|
|
35
35
|
background: c.bgRaised,
|
|
36
|
-
color: c.
|
|
36
|
+
color: c.fgNeutralMuted,
|
|
37
37
|
boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
|
|
38
38
|
},
|
|
39
39
|
brand: { background: c.bgBrandSolid, color: "#fff" },
|
|
@@ -10,7 +10,7 @@ const config = defineRecipe({
|
|
|
10
10
|
base: `display: block; box-sizing: border-box; background: ${c.bgSurface};
|
|
11
11
|
& .crisp-table-scroll { overflow-x: auto; }
|
|
12
12
|
& table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: ${c.font.sans}; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
|
|
13
|
-
& th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
|
|
13
|
+
& th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
14
14
|
& th { height: ${c.size.tableHeader}; background: ${c.bgSurface}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); user-select: none; }
|
|
15
15
|
& td { color: ${c.fgPrimary}; background: ${c.bgSurface}; }
|
|
16
16
|
& th:last-child, & td:last-child { border-inline-end: none; }
|
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
import { createPortal } from "react-dom";
|
|
4
5
|
import { tooltipRecipe } from "./tooltip.recipe.js";
|
|
6
|
+
function mergeRefs(...refs) {
|
|
7
|
+
return (node) => {
|
|
8
|
+
for (const ref of refs) {
|
|
9
|
+
if (!ref)
|
|
10
|
+
continue;
|
|
11
|
+
if (typeof ref === "function")
|
|
12
|
+
ref(node);
|
|
13
|
+
else
|
|
14
|
+
ref.current = node;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
5
18
|
/**
|
|
6
19
|
* A hover/focus tooltip — a dark hint chip measured live. Wraps its
|
|
7
20
|
* trigger in an inline element, shows after `delay` on pointer-enter / focus,
|
|
8
|
-
* and hides on leave / blur / Escape
|
|
9
|
-
*
|
|
21
|
+
* and hides on leave / blur / Escape (`asChild` skips the wrapper — see its
|
|
22
|
+
* doc). The bubble is portaled and positioned above the trigger (flipping
|
|
23
|
+
* below near the top edge).
|
|
24
|
+
*
|
|
25
|
+
* Composable as another component's trigger (#360: wrapping a `Popover`'s
|
|
26
|
+
* or `Menu`'s trigger in a `Tooltip` silently broke it). A composer clones
|
|
27
|
+
* a `ref`/`id`/`aria-*`/`onClick` onto whatever element it's handed —
|
|
28
|
+
* `Tooltip` wasn't `forwardRef` and didn't read any of the rest, so they
|
|
29
|
+
* landed on the `<Tooltip>` element itself, where nothing forwarded them
|
|
30
|
+
* anywhere. Both now merge onto the actual trigger via `Slot` (the same
|
|
31
|
+
* mechanism `ContextMenu`'s `asChild` uses) — the wrapping `<span>` can't
|
|
32
|
+
* carry them for the same reason it can't carry `aria-describedby` (below):
|
|
33
|
+
* a screen reader reads state off the focused element, not an ancestor.
|
|
10
34
|
*/
|
|
11
|
-
export
|
|
35
|
+
export const Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, "aria-describedby": restDescribedBy,
|
|
36
|
+
// Not forwarded, even though a composer's other rest props are
|
|
37
|
+
// (below): `style` sizes a VISUAL box, and Tooltip's wrapper has none
|
|
38
|
+
// of its own -- the box that matters is whatever the consumer's own
|
|
39
|
+
// trigger renders as, which they already own and can style directly.
|
|
40
|
+
// Also matches Popover/Menu's own composer contract: their
|
|
41
|
+
// cloneElement injects ref/id/aria-*/onClick, never style.
|
|
42
|
+
style: _style, ...rest }, forwardedRef) => {
|
|
12
43
|
const tipId = React.useId();
|
|
44
|
+
// The trigger's box for positioning the bubble: the wrapper `<span>` in
|
|
45
|
+
// the default mode (it exists and hugs the child tightly even when
|
|
46
|
+
// `children` isn't a single element, so hover positioning never depends
|
|
47
|
+
// on Slot succeeding), or the child itself in `asChild` mode (there is
|
|
48
|
+
// no wrapper to measure).
|
|
13
49
|
const triggerRef = React.useRef(null);
|
|
14
50
|
const timer = React.useRef(undefined);
|
|
15
51
|
const [pos, setPos] = React.useState(null);
|
|
@@ -51,27 +87,50 @@ export function Tooltip({ content, children, side = "top", delay = 350, open: op
|
|
|
51
87
|
// the only way a screen reader learns of one is `aria-describedby` on the
|
|
52
88
|
// element that gets focus. It cannot go on the wrapper: a description on a
|
|
53
89
|
// non-focusable span around a button is never announced. So the trigger is
|
|
54
|
-
//
|
|
55
|
-
//
|
|
90
|
+
// Slot-merged, keeping any aria-describedby it already had (plus a
|
|
91
|
+
// composer's, if any — see restDescribedBy above), and the tipId is only
|
|
92
|
+
// present while the bubble is (a reference to a removed node is dead
|
|
56
93
|
// wiring, #225).
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
const isElement = React.isValidElement(children);
|
|
95
|
+
const ownDescribedBy = isElement
|
|
96
|
+
? children.props["aria-describedby"]
|
|
97
|
+
: undefined;
|
|
98
|
+
const describedBy = [ownDescribedBy, restDescribedBy, open ? tipId : undefined]
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join(" ") || undefined;
|
|
101
|
+
// Baked onto a CLONE, not passed as a prop to <Slot> below: Slot's own
|
|
102
|
+
// mergeProps lets the child's OWN pre-existing prop win over anything
|
|
103
|
+
// unrecognised (it only special-cases style/className/on* handlers), so
|
|
104
|
+
// a `describedBy` passed to Slot would be silently discarded in favour
|
|
105
|
+
// of the child's original, un-merged `aria-describedby` the moment the
|
|
106
|
+
// child already had one.
|
|
107
|
+
const describedChild = isElement
|
|
108
|
+
? React.cloneElement(children, { "aria-describedby": describedBy })
|
|
66
109
|
: children;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
const hoverHandlers = {
|
|
111
|
+
onPointerEnter: openAfterDelay,
|
|
112
|
+
onPointerLeave: close,
|
|
113
|
+
onFocusCapture: openAfterDelay,
|
|
114
|
+
onBlurCapture: close,
|
|
115
|
+
onKeyDown: (e) => e.key === "Escape" && close(),
|
|
116
|
+
};
|
|
117
|
+
const portal = open &&
|
|
118
|
+
pos != null &&
|
|
119
|
+
createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe(), style: {
|
|
120
|
+
left: pos.left,
|
|
121
|
+
top: pos.top,
|
|
122
|
+
transform: pos.below
|
|
123
|
+
? "translate(-50%, 0)"
|
|
124
|
+
: "translate(-50%, -100%)",
|
|
125
|
+
}, children: content }), document.body);
|
|
126
|
+
if (asChild) {
|
|
127
|
+
// No wrapper at all: the child carries the hover/focus/Escape
|
|
128
|
+
// handling AND the composer's ref/id/aria-*/onClick directly. Not an
|
|
129
|
+
// element (asChild has nothing to attach to) degrades the same way
|
|
130
|
+
// #225 already documented for aria-describedby: renders, but neither
|
|
131
|
+
// hovers nor composes.
|
|
132
|
+
return (_jsxs(_Fragment, { children: [isElement ? (_jsx(Slot, { ref: mergeRefs(triggerRef, forwardedRef), ...hoverHandlers, ...rest, children: describedChild })) : (children), portal] }));
|
|
133
|
+
}
|
|
134
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { ref: triggerRef, style: { display: "inline-flex" }, ...hoverHandlers, children: isElement ? (_jsx(Slot, { ref: forwardedRef, ...rest, children: describedChild })) : (children) }), portal] }));
|
|
135
|
+
});
|
|
77
136
|
Tooltip.displayName = "Tooltip";
|
package/dist/styles.css
CHANGED
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
.crisp-avatar {
|
|
158
|
-
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-
|
|
158
|
+
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
|
|
159
159
|
& img { width: 100%; height: 100%; object-fit: cover; }
|
|
160
160
|
}
|
|
161
161
|
.crisp-avatar--size_sm {
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
height: 20px; width: 20px; font-size: 10px;
|
|
166
166
|
}
|
|
167
167
|
.crisp-avatar--size_lg {
|
|
168
|
-
height: 24px; width: 24px; font-size: var(--crisp-text-
|
|
168
|
+
height: 24px; width: 24px; font-size: var(--crisp-text-lg);
|
|
169
169
|
}
|
|
170
170
|
.crisp-avatar--size_xl {
|
|
171
171
|
height: 32px; width: 32px; font-size: var(--crisp-text-lg);
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
border-radius: 30%;
|
|
178
178
|
}
|
|
179
179
|
.crisp-avatar--tone_neutral {
|
|
180
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
180
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
181
181
|
}
|
|
182
182
|
.crisp-avatar--tone_brand {
|
|
183
183
|
background: var(--crisp-bg-brand-solid); color: #fff;
|
|
@@ -1514,7 +1514,7 @@
|
|
|
1514
1514
|
display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
|
|
1515
1515
|
& .crisp-table-scroll { overflow-x: auto; }
|
|
1516
1516
|
& table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
|
|
1517
|
-
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
|
|
1517
|
+
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1518
1518
|
& th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
|
|
1519
1519
|
& td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
|
|
1520
1520
|
& th:last-child, & td:last-child { border-inline-end: none; }
|
package/dist/styles.layered.css
CHANGED
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
.crisp-avatar {
|
|
159
|
-
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-
|
|
159
|
+
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
|
|
160
160
|
& img { width: 100%; height: 100%; object-fit: cover; }
|
|
161
161
|
}
|
|
162
162
|
.crisp-avatar--size_sm {
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
height: 20px; width: 20px; font-size: 10px;
|
|
167
167
|
}
|
|
168
168
|
.crisp-avatar--size_lg {
|
|
169
|
-
height: 24px; width: 24px; font-size: var(--crisp-text-
|
|
169
|
+
height: 24px; width: 24px; font-size: var(--crisp-text-lg);
|
|
170
170
|
}
|
|
171
171
|
.crisp-avatar--size_xl {
|
|
172
172
|
height: 32px; width: 32px; font-size: var(--crisp-text-lg);
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
border-radius: 30%;
|
|
179
179
|
}
|
|
180
180
|
.crisp-avatar--tone_neutral {
|
|
181
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
181
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
182
182
|
}
|
|
183
183
|
.crisp-avatar--tone_brand {
|
|
184
184
|
background: var(--crisp-bg-brand-solid); color: #fff;
|
|
@@ -1515,7 +1515,7 @@
|
|
|
1515
1515
|
display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
|
|
1516
1516
|
& .crisp-table-scroll { overflow-x: auto; }
|
|
1517
1517
|
& table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
|
|
1518
|
-
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
|
|
1518
|
+
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1519
1519
|
& th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
|
|
1520
1520
|
& td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
|
|
1521
1521
|
& th:last-child, & td:last-child { border-inline-end: none; }
|
package/dist/table.css
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
|
|
3
3
|
& .crisp-table-scroll { overflow-x: auto; }
|
|
4
4
|
& table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
|
|
5
|
-
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
|
|
5
|
+
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
6
6
|
& th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
|
|
7
7
|
& td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
|
|
8
8
|
& th:last-child, & td:last-child { border-inline-end: none; }
|
package/dist/table.layered.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
|
|
4
4
|
& .crisp-table-scroll { overflow-x: auto; }
|
|
5
5
|
& table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
|
|
6
|
-
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
|
|
6
|
+
& th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
7
7
|
& th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
|
|
8
8
|
& td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
|
|
9
9
|
& th:last-child, & td:last-child { border-inline-end: none; }
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
export type TooltipProps = {
|
|
2
|
+
export type TooltipProps = Omit<React.ComponentPropsWithoutRef<"span">, "content" | "children"> & {
|
|
3
3
|
/** The hint text/content shown on hover or focus. */
|
|
4
4
|
content: React.ReactNode;
|
|
5
5
|
/**
|
|
6
|
-
* The trigger element. A SINGLE element: it
|
|
7
|
-
* `aria-describedby` that makes the hint reachable,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The trigger element. A SINGLE element: it carries the
|
|
7
|
+
* `aria-describedby` that makes the hint reachable (and, when `Tooltip`
|
|
8
|
+
* itself is composed as another component's trigger, e.g. `Popover`'s
|
|
9
|
+
* `trigger` prop cloning in a `ref`/`id`/`aria-*`/`onClick` — #360 — those
|
|
10
|
+
* too), so anything else — a string, a fragment, several children —
|
|
11
|
+
* renders and hovers correctly but can't carry either.
|
|
10
12
|
*/
|
|
11
13
|
children: React.ReactNode;
|
|
12
14
|
/** Preferred side; flips to the other axis end when there's no room. */
|
|
@@ -20,14 +22,67 @@ export type TooltipProps = {
|
|
|
20
22
|
defaultOpen?: boolean;
|
|
21
23
|
/** Fires with the next open state, controlled or not. */
|
|
22
24
|
onOpenChange?: (open: boolean) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Attach directly to the child instead of wrapping it in a `<span>`
|
|
27
|
+
* (#360). The wrapper's `display: inline-flex` makes the child a flex
|
|
28
|
+
* item of a NEW container; flex items default to `min-width: auto` and
|
|
29
|
+
* won't shrink below their own content's width, so a child depending on
|
|
30
|
+
* an ANCESTOR's width constraint — most commonly `text-overflow:
|
|
31
|
+
* ellipsis` inside a fixed-width cell — silently stops truncating once
|
|
32
|
+
* it's wrapped. `asChild` requires a single element, the same as
|
|
33
|
+
* `ContextMenu`'s.
|
|
34
|
+
*/
|
|
35
|
+
asChild?: boolean;
|
|
23
36
|
};
|
|
24
37
|
/**
|
|
25
38
|
* A hover/focus tooltip — a dark hint chip measured live. Wraps its
|
|
26
39
|
* trigger in an inline element, shows after `delay` on pointer-enter / focus,
|
|
27
|
-
* and hides on leave / blur / Escape
|
|
28
|
-
*
|
|
40
|
+
* and hides on leave / blur / Escape (`asChild` skips the wrapper — see its
|
|
41
|
+
* doc). The bubble is portaled and positioned above the trigger (flipping
|
|
42
|
+
* below near the top edge).
|
|
43
|
+
*
|
|
44
|
+
* Composable as another component's trigger (#360: wrapping a `Popover`'s
|
|
45
|
+
* or `Menu`'s trigger in a `Tooltip` silently broke it). A composer clones
|
|
46
|
+
* a `ref`/`id`/`aria-*`/`onClick` onto whatever element it's handed —
|
|
47
|
+
* `Tooltip` wasn't `forwardRef` and didn't read any of the rest, so they
|
|
48
|
+
* landed on the `<Tooltip>` element itself, where nothing forwarded them
|
|
49
|
+
* anywhere. Both now merge onto the actual trigger via `Slot` (the same
|
|
50
|
+
* mechanism `ContextMenu`'s `asChild` uses) — the wrapping `<span>` can't
|
|
51
|
+
* carry them for the same reason it can't carry `aria-describedby` (below):
|
|
52
|
+
* a screen reader reads state off the focused element, not an ancestor.
|
|
29
53
|
*/
|
|
30
|
-
export declare
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
export declare const Tooltip: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref">, "children" | "content"> & {
|
|
55
|
+
/** The hint text/content shown on hover or focus. */
|
|
56
|
+
content: React.ReactNode;
|
|
57
|
+
/**
|
|
58
|
+
* The trigger element. A SINGLE element: it carries the
|
|
59
|
+
* `aria-describedby` that makes the hint reachable (and, when `Tooltip`
|
|
60
|
+
* itself is composed as another component's trigger, e.g. `Popover`'s
|
|
61
|
+
* `trigger` prop cloning in a `ref`/`id`/`aria-*`/`onClick` — #360 — those
|
|
62
|
+
* too), so anything else — a string, a fragment, several children —
|
|
63
|
+
* renders and hovers correctly but can't carry either.
|
|
64
|
+
*/
|
|
65
|
+
children: React.ReactNode;
|
|
66
|
+
/** Preferred side; flips to the other axis end when there's no room. */
|
|
67
|
+
side?: "top" | "bottom";
|
|
68
|
+
/** Delay in ms before showing (default 350). Hides immediately. */
|
|
69
|
+
delay?: number;
|
|
70
|
+
/** Controlled open state. Omit to let Tooltip manage it (see defaultOpen).
|
|
71
|
+
* A controlled tooltip shows at once — `delay` only paces hover. */
|
|
72
|
+
open?: boolean;
|
|
73
|
+
/** Initially open, when uncontrolled. */
|
|
74
|
+
defaultOpen?: boolean;
|
|
75
|
+
/** Fires with the next open state, controlled or not. */
|
|
76
|
+
onOpenChange?: (open: boolean) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Attach directly to the child instead of wrapping it in a `<span>`
|
|
79
|
+
* (#360). The wrapper's `display: inline-flex` makes the child a flex
|
|
80
|
+
* item of a NEW container; flex items default to `min-width: auto` and
|
|
81
|
+
* won't shrink below their own content's width, so a child depending on
|
|
82
|
+
* an ANCESTOR's width constraint — most commonly `text-overflow:
|
|
83
|
+
* ellipsis` inside a fixed-width cell — silently stops truncating once
|
|
84
|
+
* it's wrapped. `asChild` requires a single element, the same as
|
|
85
|
+
* `ContextMenu`'s.
|
|
86
|
+
*/
|
|
87
|
+
asChild?: boolean;
|
|
88
|
+
} & React.RefAttributes<HTMLElement>>;
|