Skip to content

Commit 251fb30

Browse files
feat(headless): add Tabs, Tooltip, Popover, Select, Menu, Autocomplete, Collapsible primitives (#8790)
1 parent 6cf1078 commit 251fb30

98 files changed

Lines changed: 9600 additions & 117 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/headless/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,34 @@
99
"import": "./dist/primitives/accordion/index.js",
1010
"types": "./dist/primitives/accordion/index.d.ts"
1111
},
12+
"./tabs": {
13+
"import": "./dist/primitives/tabs/index.js",
14+
"types": "./dist/primitives/tabs/index.d.ts"
15+
},
16+
"./tooltip": {
17+
"import": "./dist/primitives/tooltip/index.js",
18+
"types": "./dist/primitives/tooltip/index.d.ts"
19+
},
20+
"./popover": {
21+
"import": "./dist/primitives/popover/index.js",
22+
"types": "./dist/primitives/popover/index.d.ts"
23+
},
24+
"./select": {
25+
"import": "./dist/primitives/select/index.js",
26+
"types": "./dist/primitives/select/index.d.ts"
27+
},
28+
"./menu": {
29+
"import": "./dist/primitives/menu/index.js",
30+
"types": "./dist/primitives/menu/index.d.ts"
31+
},
32+
"./autocomplete": {
33+
"import": "./dist/primitives/autocomplete/index.js",
34+
"types": "./dist/primitives/autocomplete/index.d.ts"
35+
},
36+
"./collapsible": {
37+
"import": "./dist/primitives/collapsible/index.js",
38+
"types": "./dist/primitives/collapsible/index.d.ts"
39+
},
1240
"./dialog": {
1341
"import": "./dist/primitives/dialog/index.js",
1442
"types": "./dist/primitives/dialog/index.d.ts"
@@ -42,6 +70,7 @@
4270
"happy-dom": "^18.0.1",
4371
"react": "catalog:react",
4472
"react-dom": "catalog:react",
73+
"rollup-plugin-preserve-directives": "^0.4.0",
4574
"typescript": "catalog:repo",
4675
"vite": "6.4.2",
4776
"vite-plugin-dts": "^4.5.4",
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { cleanup, render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { afterEach, describe, expect, it } from 'vitest';
4+
5+
import { Dialog } from '../primitives/dialog/index';
6+
import { Popover } from '../primitives/popover/index';
7+
import { Select } from '../primitives/select/index';
8+
import { Tooltip } from '../primitives/tooltip/index';
9+
10+
afterEach(() => {
11+
cleanup();
12+
});
13+
14+
const fruits = [
15+
{ value: 'apple', label: 'Apple' },
16+
{ value: 'banana', label: 'Banana' },
17+
{ value: 'cherry', label: 'Cherry' },
18+
];
19+
20+
describe('FloatingTree integration', () => {
21+
describe('Select inside Popover', () => {
22+
function SelectInPopover() {
23+
return (
24+
<Popover.Root>
25+
<Popover.Trigger>Open Popover</Popover.Trigger>
26+
<Popover.Positioner>
27+
<Popover.Popup>
28+
<Popover.Title>Pick a fruit</Popover.Title>
29+
<Select.Root>
30+
<Select.Trigger>
31+
<Select.Value placeholder='Choose...' />
32+
</Select.Trigger>
33+
<Select.Positioner>
34+
<Select.Popup>
35+
{fruits.map(f => (
36+
<Select.Option
37+
key={f.value}
38+
value={f.value}
39+
label={f.label}
40+
>
41+
{f.label}
42+
</Select.Option>
43+
))}
44+
</Select.Popup>
45+
</Select.Positioner>
46+
</Select.Root>
47+
</Popover.Popup>
48+
</Popover.Positioner>
49+
</Popover.Root>
50+
);
51+
}
52+
53+
it('popover stays open when select dropdown opens', async () => {
54+
const user = userEvent.setup();
55+
render(<SelectInPopover />);
56+
57+
await user.click(screen.getByText('Open Popover'));
58+
expect(screen.getByText('Pick a fruit')).toBeInTheDocument();
59+
60+
await user.click(screen.getByText('Choose...'));
61+
62+
// Popover should still be open
63+
expect(screen.getByText('Pick a fruit')).toBeInTheDocument();
64+
// Select dropdown should be visible
65+
expect(screen.getByText('Apple')).toBeInTheDocument();
66+
});
67+
68+
it('popover stays open when clicking select option', async () => {
69+
const user = userEvent.setup();
70+
render(<SelectInPopover />);
71+
72+
await user.click(screen.getByText('Open Popover'));
73+
await user.click(screen.getByText('Choose...'));
74+
await user.click(screen.getByText('Banana'));
75+
76+
// Popover should still be open after selecting
77+
expect(screen.getByText('Pick a fruit')).toBeInTheDocument();
78+
});
79+
});
80+
81+
describe('Select inside Dialog', () => {
82+
function SelectInDialog() {
83+
return (
84+
<Dialog.Root>
85+
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
86+
<Dialog.Backdrop>
87+
<Dialog.Popup>
88+
<Dialog.Title>Select a fruit</Dialog.Title>
89+
<Select.Root>
90+
<Select.Trigger>
91+
<Select.Value placeholder='Choose...' />
92+
</Select.Trigger>
93+
<Select.Positioner>
94+
<Select.Popup>
95+
{fruits.map(f => (
96+
<Select.Option
97+
key={f.value}
98+
value={f.value}
99+
label={f.label}
100+
>
101+
{f.label}
102+
</Select.Option>
103+
))}
104+
</Select.Popup>
105+
</Select.Positioner>
106+
</Select.Root>
107+
</Dialog.Popup>
108+
</Dialog.Backdrop>
109+
</Dialog.Root>
110+
);
111+
}
112+
113+
it('dialog stays open when select dropdown opens', async () => {
114+
const user = userEvent.setup();
115+
render(<SelectInDialog />);
116+
117+
await user.click(screen.getByText('Open Dialog'));
118+
expect(screen.getByText('Select a fruit')).toBeInTheDocument();
119+
120+
await user.click(screen.getByText('Choose...'));
121+
122+
// Dialog should still be open
123+
expect(screen.getByText('Select a fruit')).toBeInTheDocument();
124+
// Select options visible
125+
expect(screen.getByText('Apple')).toBeInTheDocument();
126+
});
127+
});
128+
129+
describe('Popover inside Popover', () => {
130+
function NestedPopover() {
131+
return (
132+
<Popover.Root>
133+
<Popover.Trigger>Outer</Popover.Trigger>
134+
<Popover.Positioner>
135+
<Popover.Popup>
136+
<Popover.Title>Outer Content</Popover.Title>
137+
<Popover.Root>
138+
<Popover.Trigger>Inner</Popover.Trigger>
139+
<Popover.Positioner>
140+
<Popover.Popup>
141+
<Popover.Title>Inner Content</Popover.Title>
142+
</Popover.Popup>
143+
</Popover.Positioner>
144+
</Popover.Root>
145+
</Popover.Popup>
146+
</Popover.Positioner>
147+
</Popover.Root>
148+
);
149+
}
150+
151+
it('outer popover stays open when inner popover opens', async () => {
152+
const user = userEvent.setup();
153+
render(<NestedPopover />);
154+
155+
await user.click(screen.getByText('Outer'));
156+
expect(screen.getByText('Outer Content')).toBeInTheDocument();
157+
158+
await user.click(screen.getByText('Inner'));
159+
160+
// Both should be visible
161+
expect(screen.getByText('Outer Content')).toBeInTheDocument();
162+
expect(screen.getByText('Inner Content')).toBeInTheDocument();
163+
});
164+
});
165+
166+
describe('Tooltip inside Popover', () => {
167+
function TooltipInPopover() {
168+
return (
169+
<Popover.Root>
170+
<Popover.Trigger>Open Popover</Popover.Trigger>
171+
<Popover.Positioner>
172+
<Popover.Popup>
173+
<Popover.Title>Content</Popover.Title>
174+
<Tooltip.Root>
175+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
176+
<Tooltip.Positioner>
177+
<Tooltip.Popup>Tooltip text</Tooltip.Popup>
178+
</Tooltip.Positioner>
179+
</Tooltip.Root>
180+
</Popover.Popup>
181+
</Popover.Positioner>
182+
</Popover.Root>
183+
);
184+
}
185+
186+
it('popover stays open when tooltip trigger is hovered', async () => {
187+
const user = userEvent.setup();
188+
render(<TooltipInPopover />);
189+
190+
await user.click(screen.getByText('Open Popover'));
191+
expect(screen.getByText('Content')).toBeInTheDocument();
192+
193+
await user.hover(screen.getByText('Hover me'));
194+
195+
// Popover should remain open
196+
expect(screen.getByText('Content')).toBeInTheDocument();
197+
});
198+
});
199+
200+
describe('Popover inside Dialog', () => {
201+
function PopoverInDialog() {
202+
return (
203+
<Dialog.Root>
204+
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
205+
<Dialog.Backdrop>
206+
<Dialog.Popup>
207+
<Dialog.Title>Dialog Content</Dialog.Title>
208+
<Popover.Root>
209+
<Popover.Trigger>Open Popover</Popover.Trigger>
210+
<Popover.Positioner>
211+
<Popover.Popup>
212+
<Popover.Title>Popover Content</Popover.Title>
213+
</Popover.Popup>
214+
</Popover.Positioner>
215+
</Popover.Root>
216+
</Dialog.Popup>
217+
</Dialog.Backdrop>
218+
</Dialog.Root>
219+
);
220+
}
221+
222+
it('dialog stays open when popover opens inside it', async () => {
223+
const user = userEvent.setup();
224+
render(<PopoverInDialog />);
225+
226+
await user.click(screen.getByText('Open Dialog'));
227+
expect(screen.getByText('Dialog Content')).toBeInTheDocument();
228+
229+
await user.click(screen.getByText('Open Popover'));
230+
231+
// Both should be visible
232+
expect(screen.getByText('Dialog Content')).toBeInTheDocument();
233+
expect(screen.getByText('Popover Content')).toBeInTheDocument();
234+
});
235+
});
236+
});

0 commit comments

Comments
 (0)