@nationaldesignstudio/react 0.2.0 → 0.5.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/component-registry.md +1310 -127
- package/dist/components/atoms/background/background.d.ts +13 -27
- package/dist/components/atoms/button/button.d.ts +64 -72
- package/dist/components/atoms/button/button.figma.d.ts +1 -0
- package/dist/components/atoms/button/icon-button.d.ts +62 -110
- package/dist/components/atoms/input/input-group.d.ts +278 -0
- package/dist/components/atoms/input/input.d.ts +121 -0
- package/dist/components/atoms/popover/popover.d.ts +195 -0
- package/dist/components/atoms/select/select.d.ts +131 -0
- package/dist/components/atoms/tooltip/tooltip.d.ts +161 -0
- package/dist/components/organisms/card/card.d.ts +3 -3
- package/dist/components/sections/hero/hero.d.ts +2 -2
- package/dist/components/sections/prose/prose.d.ts +3 -3
- package/dist/components/sections/river/river.d.ts +1 -1
- package/dist/components/sections/tout/tout.d.ts +4 -4
- package/dist/components/shared/floating-arrow.d.ts +34 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13935 -7622
- package/dist/index.js.map +1 -1
- package/dist/lib/form-control.d.ts +106 -0
- package/dist/tokens.css +4725 -19065
- package/package.json +2 -1
- package/src/components/atoms/accordion/accordion.stories.tsx +1 -1
- package/src/components/atoms/accordion/accordion.tsx +2 -2
- package/src/components/atoms/background/background.tsx +71 -109
- package/src/components/atoms/button/button.figma.tsx +37 -0
- package/src/components/atoms/button/button.stories.tsx +253 -115
- package/src/components/atoms/button/button.test.tsx +289 -5
- package/src/components/atoms/button/button.tsx +40 -101
- package/src/components/atoms/button/button.visual.test.tsx +28 -32
- package/src/components/atoms/button/icon-button.stories.tsx +44 -101
- package/src/components/atoms/button/icon-button.test.tsx +26 -94
- package/src/components/atoms/button/icon-button.tsx +81 -224
- package/src/components/atoms/input/index.ts +17 -0
- package/src/components/atoms/input/input-group.stories.tsx +646 -0
- package/src/components/atoms/input/input-group.test.tsx +362 -0
- package/src/components/atoms/input/input-group.tsx +409 -0
- package/src/components/atoms/input/input.stories.tsx +228 -0
- package/src/components/atoms/input/input.test.tsx +167 -0
- package/src/components/atoms/input/input.tsx +104 -0
- package/src/components/atoms/pager-control/pager-control.stories.tsx +6 -8
- package/src/components/atoms/pager-control/pager-control.tsx +12 -12
- package/src/components/atoms/popover/index.ts +30 -0
- package/src/components/atoms/popover/popover.stories.tsx +531 -0
- package/src/components/atoms/popover/popover.test.tsx +486 -0
- package/src/components/atoms/popover/popover.tsx +488 -0
- package/src/components/atoms/select/index.ts +18 -0
- package/src/components/atoms/select/select.stories.tsx +455 -0
- package/src/components/atoms/select/select.tsx +324 -0
- package/src/components/atoms/tooltip/index.ts +24 -0
- package/src/components/atoms/tooltip/tooltip.stories.tsx +348 -0
- package/src/components/atoms/tooltip/tooltip.test.tsx +363 -0
- package/src/components/atoms/tooltip/tooltip.tsx +347 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +8 -17
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +3 -3
- package/src/components/foundation/typography/typography.stories.tsx +401 -0
- package/src/components/organisms/card/card.stories.tsx +19 -19
- package/src/components/organisms/card/card.test.tsx +1 -1
- package/src/components/organisms/card/card.tsx +3 -3
- package/src/components/organisms/card/card.visual.test.tsx +11 -11
- package/src/components/organisms/navbar/navbar.tsx +2 -2
- package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
- package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +2 -2
- package/src/components/sections/banner/banner.stories.tsx +1 -5
- package/src/components/sections/banner/banner.test.tsx +2 -2
- package/src/components/sections/banner/banner.tsx +6 -6
- package/src/components/sections/card-grid/card-grid.tsx +5 -5
- package/src/components/sections/faq-section/faq-section.tsx +2 -2
- package/src/components/sections/hero/hero.stories.tsx +7 -7
- package/src/components/sections/hero/hero.test.tsx +5 -5
- package/src/components/sections/hero/hero.tsx +10 -11
- package/src/components/sections/prose/prose.test.tsx +2 -2
- package/src/components/sections/prose/prose.tsx +6 -7
- package/src/components/sections/river/river.stories.tsx +8 -8
- package/src/components/sections/river/river.test.tsx +4 -4
- package/src/components/sections/river/river.tsx +8 -16
- package/src/components/sections/tout/tout.stories.tsx +7 -31
- package/src/components/sections/tout/tout.test.tsx +1 -1
- package/src/components/sections/tout/tout.tsx +11 -11
- package/src/components/sections/two-column-section/two-column-section.tsx +7 -9
- package/src/components/shared/floating-arrow.tsx +78 -0
- package/src/components/shared/index.ts +5 -0
- package/src/index.ts +98 -0
- package/src/lib/form-control.ts +71 -0
- package/src/stories/grid-system.stories.tsx +309 -0
- package/src/stories/{Introduction.mdx → introduction.mdx} +29 -15
- package/src/stories/{ThemeProvider.stories.tsx → theme-provider.stories.tsx} +8 -22
- package/src/stories/{TokenShowcase.stories.tsx → token-showcase.stories.tsx} +1 -20
- package/src/stories/token-showcase.tsx +777 -0
- package/src/styles.css +3 -0
- package/src/tests/token-resolution.test.tsx +298 -0
- package/src/theme/hooks.ts +1 -1
- package/src/theme/index.ts +1 -1
- package/src/theme/theme-provider.test.tsx +270 -0
- package/src/theme/{ThemeProvider.tsx → theme-provider.tsx} +18 -2
- package/src/stories/GridSystem.stories.tsx +0 -84
- package/src/stories/TokenShowcase.tsx +0 -1429
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { page, userEvent } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
import {
|
|
6
|
+
Tooltip,
|
|
7
|
+
TooltipArrow,
|
|
8
|
+
TooltipParts,
|
|
9
|
+
TooltipPopup,
|
|
10
|
+
TooltipPortal,
|
|
11
|
+
TooltipPositioner,
|
|
12
|
+
TooltipProvider,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from "./tooltip";
|
|
15
|
+
|
|
16
|
+
describe("Tooltip", () => {
|
|
17
|
+
describe("Accessibility", () => {
|
|
18
|
+
test("trigger element is accessible", async () => {
|
|
19
|
+
render(
|
|
20
|
+
<Tooltip content="Helpful tip">
|
|
21
|
+
<Button>Hover me</Button>
|
|
22
|
+
</Tooltip>,
|
|
23
|
+
);
|
|
24
|
+
await expect
|
|
25
|
+
.element(page.getByRole("button", { name: "Hover me" }))
|
|
26
|
+
.toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("tooltip has correct role when visible", async () => {
|
|
30
|
+
render(
|
|
31
|
+
<Tooltip content="Helpful tip" defaultOpen>
|
|
32
|
+
<Button>Hover me</Button>
|
|
33
|
+
</Tooltip>,
|
|
34
|
+
);
|
|
35
|
+
await expect
|
|
36
|
+
.element(page.getByRole("tooltip", { name: "Helpful tip" }))
|
|
37
|
+
.toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("trigger element is focusable", async () => {
|
|
41
|
+
render(
|
|
42
|
+
<Tooltip content="Helpful tip">
|
|
43
|
+
<Button>Focus me</Button>
|
|
44
|
+
</Tooltip>,
|
|
45
|
+
);
|
|
46
|
+
await userEvent.keyboard("{Tab}");
|
|
47
|
+
await expect
|
|
48
|
+
.element(page.getByRole("button", { name: "Focus me" }))
|
|
49
|
+
.toHaveFocus();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("Interactions", () => {
|
|
54
|
+
test("shows tooltip on hover", async () => {
|
|
55
|
+
render(
|
|
56
|
+
<Tooltip content="Hover tooltip">
|
|
57
|
+
<Button>Hover me</Button>
|
|
58
|
+
</Tooltip>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Tooltip should not be visible initially
|
|
62
|
+
await expect
|
|
63
|
+
.element(page.getByRole("tooltip", { name: "Hover tooltip" }))
|
|
64
|
+
.not.toBeInTheDocument();
|
|
65
|
+
|
|
66
|
+
// Hover over the trigger
|
|
67
|
+
await page.getByRole("button", { name: "Hover me" }).hover();
|
|
68
|
+
|
|
69
|
+
// Tooltip should now be visible
|
|
70
|
+
await expect
|
|
71
|
+
.element(page.getByRole("tooltip", { name: "Hover tooltip" }))
|
|
72
|
+
.toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("shows tooltip on focus", async () => {
|
|
76
|
+
render(
|
|
77
|
+
<Tooltip content="Focus tooltip">
|
|
78
|
+
<Button>Focus me</Button>
|
|
79
|
+
</Tooltip>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Tooltip should not be visible initially
|
|
83
|
+
await expect
|
|
84
|
+
.element(page.getByRole("tooltip", { name: "Focus tooltip" }))
|
|
85
|
+
.not.toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
// Focus the trigger
|
|
88
|
+
page.getByRole("button", { name: "Focus me" }).element().focus();
|
|
89
|
+
|
|
90
|
+
// Tooltip should now be visible
|
|
91
|
+
await expect
|
|
92
|
+
.element(page.getByRole("tooltip", { name: "Focus tooltip" }))
|
|
93
|
+
.toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("hides tooltip on blur", async () => {
|
|
97
|
+
render(
|
|
98
|
+
<>
|
|
99
|
+
<Tooltip content="Focus tooltip">
|
|
100
|
+
<Button>Focus me</Button>
|
|
101
|
+
</Tooltip>
|
|
102
|
+
<Button>Other button</Button>
|
|
103
|
+
</>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Focus the trigger to show tooltip
|
|
107
|
+
page.getByRole("button", { name: "Focus me" }).element().focus();
|
|
108
|
+
await expect
|
|
109
|
+
.element(page.getByRole("tooltip", { name: "Focus tooltip" }))
|
|
110
|
+
.toBeInTheDocument();
|
|
111
|
+
|
|
112
|
+
// Blur by focusing another element
|
|
113
|
+
page.getByRole("button", { name: "Other button" }).element().focus();
|
|
114
|
+
|
|
115
|
+
// Tooltip should be hidden
|
|
116
|
+
await expect
|
|
117
|
+
.element(page.getByRole("tooltip", { name: "Focus tooltip" }))
|
|
118
|
+
.not.toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("Controlled State", () => {
|
|
123
|
+
test("respects controlled open state", async () => {
|
|
124
|
+
render(
|
|
125
|
+
<Tooltip content="Controlled tooltip" open={true}>
|
|
126
|
+
<Button>Trigger</Button>
|
|
127
|
+
</Tooltip>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Tooltip should be visible when open=true
|
|
131
|
+
await expect
|
|
132
|
+
.element(page.getByRole("tooltip", { name: "Controlled tooltip" }))
|
|
133
|
+
.toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("respects controlled closed state", async () => {
|
|
137
|
+
render(
|
|
138
|
+
<Tooltip content="Controlled tooltip" open={false}>
|
|
139
|
+
<Button>Trigger</Button>
|
|
140
|
+
</Tooltip>,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Tooltip should not be visible when open=false
|
|
144
|
+
await expect
|
|
145
|
+
.element(page.getByRole("tooltip", { name: "Controlled tooltip" }))
|
|
146
|
+
.not.toBeInTheDocument();
|
|
147
|
+
|
|
148
|
+
// Should not show on hover when controlled
|
|
149
|
+
await page.getByRole("button", { name: "Trigger" }).hover();
|
|
150
|
+
await expect
|
|
151
|
+
.element(page.getByRole("tooltip", { name: "Controlled tooltip" }))
|
|
152
|
+
.not.toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("calls onOpenChange callback", async () => {
|
|
156
|
+
const handleOpenChange = vi.fn();
|
|
157
|
+
render(
|
|
158
|
+
<Tooltip content="Callback tooltip" onOpenChange={handleOpenChange}>
|
|
159
|
+
<Button>Trigger</Button>
|
|
160
|
+
</Tooltip>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Hover to trigger open
|
|
164
|
+
await page.getByRole("button", { name: "Trigger" }).hover();
|
|
165
|
+
|
|
166
|
+
// Wait for tooltip to appear (ensures callback has been called)
|
|
167
|
+
await expect
|
|
168
|
+
.element(page.getByRole("tooltip", { name: "Callback tooltip" }))
|
|
169
|
+
.toBeInTheDocument();
|
|
170
|
+
|
|
171
|
+
// Callback should have been called with true as first argument
|
|
172
|
+
// (Base UI passes additional context as second argument)
|
|
173
|
+
expect(handleOpenChange).toHaveBeenCalled();
|
|
174
|
+
expect(handleOpenChange.mock.calls[0][0]).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("defaultOpen shows tooltip initially", async () => {
|
|
178
|
+
render(
|
|
179
|
+
<Tooltip content="Default open tooltip" defaultOpen>
|
|
180
|
+
<Button>Trigger</Button>
|
|
181
|
+
</Tooltip>,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Tooltip should be visible initially
|
|
185
|
+
await expect
|
|
186
|
+
.element(page.getByRole("tooltip", { name: "Default open tooltip" }))
|
|
187
|
+
.toBeInTheDocument();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("Content", () => {
|
|
192
|
+
test("displays text content", async () => {
|
|
193
|
+
render(
|
|
194
|
+
<Tooltip content="Simple text" defaultOpen>
|
|
195
|
+
<Button>Trigger</Button>
|
|
196
|
+
</Tooltip>,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await expect.element(page.getByText("Simple text")).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("displays React node content", async () => {
|
|
203
|
+
render(
|
|
204
|
+
<Tooltip
|
|
205
|
+
content={
|
|
206
|
+
<span data-testid="custom-content">
|
|
207
|
+
<strong>Bold</strong> text
|
|
208
|
+
</span>
|
|
209
|
+
}
|
|
210
|
+
defaultOpen
|
|
211
|
+
>
|
|
212
|
+
<Button>Trigger</Button>
|
|
213
|
+
</Tooltip>,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await expect
|
|
217
|
+
.element(page.getByTestId("custom-content"))
|
|
218
|
+
.toBeInTheDocument();
|
|
219
|
+
await expect.element(page.getByText("Bold")).toBeInTheDocument();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Arrow", () => {
|
|
224
|
+
test("shows arrow by default", async () => {
|
|
225
|
+
render(
|
|
226
|
+
<Tooltip content="With arrow" defaultOpen>
|
|
227
|
+
<Button>Trigger</Button>
|
|
228
|
+
</Tooltip>,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// The arrow is an SVG element rendered by Base UI
|
|
232
|
+
const tooltip = page.getByRole("tooltip");
|
|
233
|
+
await expect.element(tooltip).toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("hides arrow when showArrow is false", async () => {
|
|
237
|
+
render(
|
|
238
|
+
<Tooltip content="Without arrow" defaultOpen showArrow={false}>
|
|
239
|
+
<Button>Trigger</Button>
|
|
240
|
+
</Tooltip>,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const tooltip = page.getByRole("tooltip");
|
|
244
|
+
await expect.element(tooltip).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("Compound Components", () => {
|
|
249
|
+
test("renders with compound component API", async () => {
|
|
250
|
+
render(
|
|
251
|
+
<TooltipParts defaultOpen>
|
|
252
|
+
<TooltipTrigger>
|
|
253
|
+
<Button>Compound trigger</Button>
|
|
254
|
+
</TooltipTrigger>
|
|
255
|
+
<TooltipPortal>
|
|
256
|
+
<TooltipPositioner>
|
|
257
|
+
<TooltipPopup>
|
|
258
|
+
<TooltipArrow />
|
|
259
|
+
Compound tooltip content
|
|
260
|
+
</TooltipPopup>
|
|
261
|
+
</TooltipPositioner>
|
|
262
|
+
</TooltipPortal>
|
|
263
|
+
</TooltipParts>,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
await expect
|
|
267
|
+
.element(page.getByRole("button", { name: "Compound trigger" }))
|
|
268
|
+
.toBeInTheDocument();
|
|
269
|
+
await expect
|
|
270
|
+
.element(page.getByText("Compound tooltip content"))
|
|
271
|
+
.toBeInTheDocument();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("Provider", () => {
|
|
276
|
+
test("TooltipProvider wraps children", async () => {
|
|
277
|
+
render(
|
|
278
|
+
<TooltipProvider>
|
|
279
|
+
<Tooltip content="Provider tooltip" defaultOpen>
|
|
280
|
+
<Button>Trigger</Button>
|
|
281
|
+
</Tooltip>
|
|
282
|
+
</TooltipProvider>,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
await expect
|
|
286
|
+
.element(page.getByRole("tooltip", { name: "Provider tooltip" }))
|
|
287
|
+
.toBeInTheDocument();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("Styles", () => {
|
|
292
|
+
test("tooltip popup has dark background", async () => {
|
|
293
|
+
render(
|
|
294
|
+
<Tooltip content="Styled tooltip" defaultOpen>
|
|
295
|
+
<Button>Trigger</Button>
|
|
296
|
+
</Tooltip>,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const tooltip = page.getByRole("tooltip");
|
|
300
|
+
await expect.element(tooltip).toBeInTheDocument();
|
|
301
|
+
|
|
302
|
+
const element = tooltip.element();
|
|
303
|
+
const styles = window.getComputedStyle(element);
|
|
304
|
+
|
|
305
|
+
// Tooltip should have a light background (not transparent)
|
|
306
|
+
expect(styles.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
307
|
+
expect(styles.backgroundColor).not.toBe("transparent");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("tooltip has border radius", async () => {
|
|
311
|
+
render(
|
|
312
|
+
<Tooltip content="Styled tooltip" defaultOpen>
|
|
313
|
+
<Button>Trigger</Button>
|
|
314
|
+
</Tooltip>,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const tooltip = page.getByRole("tooltip");
|
|
318
|
+
await expect.element(tooltip).toBeInTheDocument();
|
|
319
|
+
|
|
320
|
+
const element = tooltip.element();
|
|
321
|
+
const styles = window.getComputedStyle(element);
|
|
322
|
+
|
|
323
|
+
// Should have border radius
|
|
324
|
+
const borderRadius = parseFloat(styles.borderRadius);
|
|
325
|
+
expect(borderRadius).toBeGreaterThan(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("tooltip has shadow", async () => {
|
|
329
|
+
render(
|
|
330
|
+
<Tooltip content="Styled tooltip" defaultOpen>
|
|
331
|
+
<Button>Trigger</Button>
|
|
332
|
+
</Tooltip>,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const tooltip = page.getByRole("tooltip");
|
|
336
|
+
await expect.element(tooltip).toBeInTheDocument();
|
|
337
|
+
|
|
338
|
+
const element = tooltip.element();
|
|
339
|
+
const styles = window.getComputedStyle(element);
|
|
340
|
+
|
|
341
|
+
// Should have a box shadow
|
|
342
|
+
expect(styles.boxShadow).not.toBe("none");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("tooltip has high z-index", async () => {
|
|
346
|
+
render(
|
|
347
|
+
<Tooltip content="Styled tooltip" defaultOpen>
|
|
348
|
+
<Button>Trigger</Button>
|
|
349
|
+
</Tooltip>,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const tooltip = page.getByRole("tooltip");
|
|
353
|
+
await expect.element(tooltip).toBeInTheDocument();
|
|
354
|
+
|
|
355
|
+
const element = tooltip.element();
|
|
356
|
+
const styles = window.getComputedStyle(element);
|
|
357
|
+
|
|
358
|
+
// Should have z-index of 50 or higher
|
|
359
|
+
const zIndex = parseInt(styles.zIndex, 10);
|
|
360
|
+
expect(zIndex).toBeGreaterThanOrEqual(50);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tooltip as BaseTooltip } from "@base-ui-components/react/tooltip";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import {
|
|
8
|
+
FloatingArrowSvg,
|
|
9
|
+
floatingArrowVariants,
|
|
10
|
+
} from "../../shared/floating-arrow";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tooltip popup variants
|
|
14
|
+
*
|
|
15
|
+
* Uses semantic tokens for themeable styling:
|
|
16
|
+
* - color.tooltip.bg - Dark background
|
|
17
|
+
* - color.tooltip.text - Light text
|
|
18
|
+
* - surface.tooltip.radius - Small border radius
|
|
19
|
+
* - spatial.component.tooltip.padding-x/y - Consistent padding
|
|
20
|
+
*/
|
|
21
|
+
const tooltipPopupVariants = tv({
|
|
22
|
+
base: [
|
|
23
|
+
// Layout - uses component tooltip tokens
|
|
24
|
+
"px-spatial-component-tooltip-padding-x py-spatial-component-tooltip-padding-y",
|
|
25
|
+
// Background and text - uses tooltip color tokens
|
|
26
|
+
"bg-tooltip-bg text-tooltip-text",
|
|
27
|
+
// Border radius - uses surface tooltip token
|
|
28
|
+
"rounded-surface-tooltip",
|
|
29
|
+
// Typography - uses semantic body text composite
|
|
30
|
+
"typography-body-sm-md font-medium",
|
|
31
|
+
// Shadow for elevation
|
|
32
|
+
"shadow-md",
|
|
33
|
+
// Allow arrow to extend outside popup bounds
|
|
34
|
+
"overflow-visible",
|
|
35
|
+
// Animation
|
|
36
|
+
"origin-[var(--transform-origin)]",
|
|
37
|
+
"transition-[transform,scale,opacity] duration-150",
|
|
38
|
+
"data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
|
|
39
|
+
"data-[ending-style]:scale-95 data-[ending-style]:opacity-0",
|
|
40
|
+
// Ensure it's above other content
|
|
41
|
+
"z-50",
|
|
42
|
+
],
|
|
43
|
+
variants: {
|
|
44
|
+
variant: {
|
|
45
|
+
default: "",
|
|
46
|
+
// Future variants can be added here (e.g., light, primary)
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultVariants: {
|
|
50
|
+
variant: "default",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Tooltip arrow variants - uses shared floating arrow variants
|
|
56
|
+
*/
|
|
57
|
+
const tooltipArrowVariants = floatingArrowVariants;
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Tooltip Provider
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
export interface TooltipProviderProps extends BaseTooltip.Provider.Props {
|
|
64
|
+
children: React.ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Tooltip Provider
|
|
69
|
+
*
|
|
70
|
+
* Manages shared delays across multiple tooltips.
|
|
71
|
+
* Wrap your app or a section with this to enable tooltip delay grouping.
|
|
72
|
+
*/
|
|
73
|
+
const TooltipProvider = ({ children, ...props }: TooltipProviderProps) => {
|
|
74
|
+
return <BaseTooltip.Provider {...props}>{children}</BaseTooltip.Provider>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Tooltip Root
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
export interface TooltipRootProps extends BaseTooltip.Root.Props {
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Tooltip Root
|
|
87
|
+
*
|
|
88
|
+
* Groups all tooltip parts. Does not render an element.
|
|
89
|
+
*/
|
|
90
|
+
const TooltipRoot = ({ children, ...props }: TooltipRootProps) => {
|
|
91
|
+
return <BaseTooltip.Root {...props}>{children}</BaseTooltip.Root>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Tooltip Trigger
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
export interface TooltipTriggerProps
|
|
99
|
+
extends React.ComponentProps<typeof BaseTooltip.Trigger> {
|
|
100
|
+
className?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Tooltip Trigger
|
|
105
|
+
*
|
|
106
|
+
* The element that triggers the tooltip on hover/focus.
|
|
107
|
+
* Renders as the child element with tooltip behavior attached.
|
|
108
|
+
* When children is a single React element, uses `render` prop to avoid wrapper element.
|
|
109
|
+
*/
|
|
110
|
+
const TooltipTrigger = React.forwardRef<HTMLButtonElement, TooltipTriggerProps>(
|
|
111
|
+
({ className, children, ...props }, ref) => {
|
|
112
|
+
// If children is a single React element, use render prop to avoid wrapper
|
|
113
|
+
const isSingleElement = React.isValidElement(children);
|
|
114
|
+
|
|
115
|
+
if (isSingleElement) {
|
|
116
|
+
return (
|
|
117
|
+
<BaseTooltip.Trigger
|
|
118
|
+
ref={ref}
|
|
119
|
+
className={className}
|
|
120
|
+
render={children}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<BaseTooltip.Trigger ref={ref} className={className} {...props}>
|
|
128
|
+
{children}
|
|
129
|
+
</BaseTooltip.Trigger>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
TooltipTrigger.displayName = "TooltipTrigger";
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Tooltip Portal
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
export interface TooltipPortalProps extends BaseTooltip.Portal.Props {
|
|
140
|
+
children: React.ReactNode;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Tooltip Portal
|
|
145
|
+
*
|
|
146
|
+
* Renders the tooltip popup in a portal outside the DOM hierarchy.
|
|
147
|
+
*/
|
|
148
|
+
const TooltipPortal = ({ children, ...props }: TooltipPortalProps) => {
|
|
149
|
+
return <BaseTooltip.Portal {...props}>{children}</BaseTooltip.Portal>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Tooltip Positioner
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
export interface TooltipPositionerProps
|
|
157
|
+
extends Omit<
|
|
158
|
+
React.ComponentProps<typeof BaseTooltip.Positioner>,
|
|
159
|
+
"className"
|
|
160
|
+
> {
|
|
161
|
+
className?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Tooltip Positioner
|
|
166
|
+
*
|
|
167
|
+
* Positions the tooltip popup relative to the trigger.
|
|
168
|
+
*/
|
|
169
|
+
const TooltipPositioner = React.forwardRef<
|
|
170
|
+
HTMLDivElement,
|
|
171
|
+
TooltipPositionerProps
|
|
172
|
+
>(({ className, side = "top", sideOffset = 8, ...props }, ref) => {
|
|
173
|
+
return (
|
|
174
|
+
<BaseTooltip.Positioner
|
|
175
|
+
ref={ref}
|
|
176
|
+
side={side}
|
|
177
|
+
sideOffset={sideOffset}
|
|
178
|
+
className={className}
|
|
179
|
+
{...props}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
TooltipPositioner.displayName = "TooltipPositioner";
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Tooltip Popup
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
export interface TooltipPopupProps
|
|
190
|
+
extends Omit<React.ComponentProps<typeof BaseTooltip.Popup>, "className">,
|
|
191
|
+
VariantProps<typeof tooltipPopupVariants> {
|
|
192
|
+
className?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Tooltip Popup
|
|
197
|
+
*
|
|
198
|
+
* The tooltip content container with styled appearance.
|
|
199
|
+
*/
|
|
200
|
+
const TooltipPopup = React.forwardRef<HTMLDivElement, TooltipPopupProps>(
|
|
201
|
+
({ className, variant, ...props }, ref) => {
|
|
202
|
+
return (
|
|
203
|
+
<BaseTooltip.Popup
|
|
204
|
+
ref={ref}
|
|
205
|
+
role="tooltip"
|
|
206
|
+
className={cn(tooltipPopupVariants({ variant }), className)}
|
|
207
|
+
{...props}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
TooltipPopup.displayName = "TooltipPopup";
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Tooltip Arrow
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
export interface TooltipArrowProps
|
|
219
|
+
extends Omit<React.ComponentProps<typeof BaseTooltip.Arrow>, "className"> {
|
|
220
|
+
className?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Tooltip Arrow
|
|
225
|
+
*
|
|
226
|
+
* Visual pointer element for the tooltip.
|
|
227
|
+
* Uses shared FloatingArrowSvg with tooltip-bg color token.
|
|
228
|
+
*/
|
|
229
|
+
const TooltipArrow = React.forwardRef<HTMLDivElement, TooltipArrowProps>(
|
|
230
|
+
({ className, ...props }, ref) => {
|
|
231
|
+
return (
|
|
232
|
+
<BaseTooltip.Arrow
|
|
233
|
+
ref={ref}
|
|
234
|
+
className={cn(tooltipArrowVariants(), className)}
|
|
235
|
+
{...props}
|
|
236
|
+
>
|
|
237
|
+
<FloatingArrowSvg fillClassName="fill-tooltip-bg" />
|
|
238
|
+
</BaseTooltip.Arrow>
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
TooltipArrow.displayName = "TooltipArrow";
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Simple Tooltip Component
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
export interface TooltipProps {
|
|
249
|
+
/** The content to show in the tooltip */
|
|
250
|
+
content: React.ReactNode;
|
|
251
|
+
/** The element that triggers the tooltip */
|
|
252
|
+
children: React.ReactNode;
|
|
253
|
+
/** Side of the trigger to show the tooltip */
|
|
254
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
255
|
+
/** Offset from the trigger */
|
|
256
|
+
sideOffset?: number;
|
|
257
|
+
/** Alignment along the side */
|
|
258
|
+
align?: "start" | "center" | "end";
|
|
259
|
+
/** Delay before showing the tooltip (ms) */
|
|
260
|
+
delay?: number;
|
|
261
|
+
/** Delay before hiding the tooltip (ms) */
|
|
262
|
+
closeDelay?: number;
|
|
263
|
+
/** Whether to show an arrow */
|
|
264
|
+
showArrow?: boolean;
|
|
265
|
+
/** Controlled open state */
|
|
266
|
+
open?: boolean;
|
|
267
|
+
/** Default open state */
|
|
268
|
+
defaultOpen?: boolean;
|
|
269
|
+
/** Callback when open state changes */
|
|
270
|
+
onOpenChange?: (open: boolean) => void;
|
|
271
|
+
/** Additional className for the popup */
|
|
272
|
+
className?: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Tooltip
|
|
277
|
+
*
|
|
278
|
+
* A simple, pre-composed tooltip component for common use cases.
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```tsx
|
|
282
|
+
* <Tooltip content="Save your changes">
|
|
283
|
+
* <Button>Save</Button>
|
|
284
|
+
* </Tooltip>
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
const Tooltip = ({
|
|
288
|
+
content,
|
|
289
|
+
children,
|
|
290
|
+
side = "top",
|
|
291
|
+
sideOffset = 8,
|
|
292
|
+
align = "center",
|
|
293
|
+
delay,
|
|
294
|
+
closeDelay,
|
|
295
|
+
showArrow = true,
|
|
296
|
+
open,
|
|
297
|
+
defaultOpen,
|
|
298
|
+
onOpenChange,
|
|
299
|
+
className,
|
|
300
|
+
}: TooltipProps) => {
|
|
301
|
+
return (
|
|
302
|
+
<TooltipRoot
|
|
303
|
+
open={open}
|
|
304
|
+
defaultOpen={defaultOpen}
|
|
305
|
+
onOpenChange={onOpenChange}
|
|
306
|
+
>
|
|
307
|
+
<TooltipTrigger delay={delay} closeDelay={closeDelay}>
|
|
308
|
+
{children}
|
|
309
|
+
</TooltipTrigger>
|
|
310
|
+
<TooltipPortal>
|
|
311
|
+
<TooltipPositioner side={side} sideOffset={sideOffset} align={align}>
|
|
312
|
+
<TooltipPopup className={className}>
|
|
313
|
+
{showArrow && <TooltipArrow />}
|
|
314
|
+
{content}
|
|
315
|
+
</TooltipPopup>
|
|
316
|
+
</TooltipPositioner>
|
|
317
|
+
</TooltipPortal>
|
|
318
|
+
</TooltipRoot>
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// Compound Component Export
|
|
324
|
+
// ============================================================================
|
|
325
|
+
|
|
326
|
+
export const TooltipParts = Object.assign(TooltipRoot, {
|
|
327
|
+
Provider: TooltipProvider,
|
|
328
|
+
Root: TooltipRoot,
|
|
329
|
+
Trigger: TooltipTrigger,
|
|
330
|
+
Portal: TooltipPortal,
|
|
331
|
+
Positioner: TooltipPositioner,
|
|
332
|
+
Popup: TooltipPopup,
|
|
333
|
+
Arrow: TooltipArrow,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
export {
|
|
337
|
+
Tooltip,
|
|
338
|
+
TooltipProvider,
|
|
339
|
+
TooltipRoot,
|
|
340
|
+
TooltipTrigger,
|
|
341
|
+
TooltipPortal,
|
|
342
|
+
TooltipPositioner,
|
|
343
|
+
TooltipPopup,
|
|
344
|
+
TooltipArrow,
|
|
345
|
+
tooltipPopupVariants,
|
|
346
|
+
tooltipArrowVariants,
|
|
347
|
+
};
|