paris 0.18.1 → 0.20.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +31 -2
  3. package/src/stories/accordion/Accordion.test.tsx +140 -0
  4. package/src/stories/accordionselect/AccordionSelect.test.tsx +252 -0
  5. package/src/stories/avatar/Avatar.test.tsx +77 -0
  6. package/src/stories/button/Button.test.tsx +266 -0
  7. package/src/stories/callout/Callout.test.tsx +79 -0
  8. package/src/stories/card/Card.test.tsx +81 -0
  9. package/src/stories/cardbutton/CardButton.test.tsx +174 -0
  10. package/src/stories/checkbox/Checkbox.test.tsx +531 -0
  11. package/src/stories/combobox/Combobox.test.tsx +164 -0
  12. package/src/stories/dialog/Dialog.module.scss +2 -2
  13. package/src/stories/dialog/Dialog.test.tsx +244 -0
  14. package/src/stories/drawer/Drawer.module.scss +2 -2
  15. package/src/stories/drawer/Drawer.test.tsx +259 -0
  16. package/src/stories/field/Field.test.tsx +146 -0
  17. package/src/stories/icon/Icon.test.tsx +59 -0
  18. package/src/stories/informationaltooltip/InformationalTooltip.test.tsx +178 -0
  19. package/src/stories/input/Input.test.tsx +174 -0
  20. package/src/stories/markdown/Markdown.test.tsx +228 -0
  21. package/src/stories/markdowneditor/FixedToolbar.module.scss +24 -0
  22. package/src/stories/markdowneditor/FixedToolbar.tsx +274 -0
  23. package/src/stories/markdowneditor/FloatingToolbar.module.scss +21 -0
  24. package/src/stories/markdowneditor/FloatingToolbar.tsx +94 -0
  25. package/src/stories/markdowneditor/LinkPopover.module.scss +124 -0
  26. package/src/stories/markdowneditor/LinkPopover.tsx +135 -0
  27. package/src/stories/markdowneditor/MarkdownEditor.module.scss +405 -0
  28. package/src/stories/markdowneditor/MarkdownEditor.stories.tsx +226 -0
  29. package/src/stories/markdowneditor/MarkdownEditor.test.tsx +115 -0
  30. package/src/stories/markdowneditor/MarkdownEditor.tsx +133 -0
  31. package/src/stories/markdowneditor/MarkdownEditorContext.tsx +20 -0
  32. package/src/stories/markdowneditor/ToolbarButton.module.scss +35 -0
  33. package/src/stories/markdowneditor/ToolbarButton.tsx +52 -0
  34. package/src/stories/markdowneditor/features.ts +92 -0
  35. package/src/stories/markdowneditor/index.ts +12 -0
  36. package/src/stories/markdowneditor/useMarkdownEditor.ts +75 -0
  37. package/src/stories/menu/Menu.module.scss +1 -1
  38. package/src/stories/menu/Menu.test.tsx +211 -0
  39. package/src/stories/pagination/usePagination.test.ts +259 -0
  40. package/src/stories/popover/Popover.test.tsx +152 -0
  41. package/src/stories/select/Select.module.scss +2 -1
  42. package/src/stories/select/Select.test.tsx +233 -0
  43. package/src/stories/styledlink/StyledLink.test.tsx +59 -0
  44. package/src/stories/table/Table.test.tsx +156 -0
  45. package/src/stories/tabs/Tabs.module.scss +1 -3
  46. package/src/stories/tabs/Tabs.test.tsx +167 -0
  47. package/src/stories/tabs/Tabs.tsx +9 -0
  48. package/src/stories/tag/Tag.test.tsx +90 -0
  49. package/src/stories/text/Text.test.tsx +81 -0
  50. package/src/stories/textarea/TextArea.test.tsx +147 -0
  51. package/src/stories/theme/themes.ts +16 -0
  52. package/src/stories/tilt/Tilt.test.tsx +203 -0
  53. package/src/stories/toast/Toast.test.tsx +86 -0
  54. package/src/stories/utility/Dropdown.module.scss +1 -1
  55. package/src/stories/utility/Utility.test.tsx +96 -0
  56. package/src/test/render.tsx +20 -0
  57. package/src/test/setup.ts +32 -0
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import { useEditor } from '@tiptap/react';
4
+ import { useEffect, useMemo, useRef } from 'react';
5
+ import type { MarkdownEditorFeature } from './features';
6
+ import { ALL_FEATURES, buildExtensions } from './features';
7
+
8
+ export type UseMarkdownEditorOptions = {
9
+ /** The markdown string value (controlled). */
10
+ value: string;
11
+ /** Fires with the updated markdown string on every edit. */
12
+ onChange?: (value: string) => void;
13
+ /** Whitelist of formatting features. Defaults to ALL_FEATURES. */
14
+ features?: MarkdownEditorFeature[];
15
+ /** Placeholder text when the editor is empty. */
16
+ placeholder?: string;
17
+ /** Whether the editor is editable. @default true */
18
+ editable?: boolean;
19
+ /** Whether to autofocus the editor on mount. @default false */
20
+ autofocus?: boolean;
21
+ };
22
+
23
+ export function useMarkdownEditor({
24
+ value,
25
+ onChange,
26
+ features = ALL_FEATURES,
27
+ placeholder,
28
+ editable = true,
29
+ autofocus = false,
30
+ }: UseMarkdownEditorOptions) {
31
+ const featureSet = useMemo(() => new Set(features), [features]);
32
+ const extensions = useMemo(() => buildExtensions(featureSet, placeholder), [featureSet, placeholder]);
33
+
34
+ // Track the last value we sent via onChange to prevent circular updates
35
+ const lastOnChangeValue = useRef(value);
36
+
37
+ const editor = useEditor({
38
+ extensions,
39
+ // Empty strings produce no DOM nodes with contentType: 'markdown',
40
+ // which breaks the placeholder. Only use markdown parsing for non-empty content.
41
+ content: value || undefined,
42
+ contentType: value ? 'markdown' : undefined,
43
+ editable,
44
+ autofocus,
45
+ immediatelyRender: false,
46
+ onUpdate: ({ editor: ed }) => {
47
+ const markdown = ed.getMarkdown();
48
+ lastOnChangeValue.current = markdown;
49
+ onChange?.(markdown);
50
+ },
51
+ });
52
+
53
+ // Sync external value changes into the editor (controlled mode)
54
+ useEffect(() => {
55
+ if (!editor || editor.isDestroyed) return;
56
+
57
+ // Skip if this value came from our own onChange
58
+ if (value === lastOnChangeValue.current) return;
59
+
60
+ const currentMarkdown = editor.getMarkdown();
61
+ if (value !== currentMarkdown) {
62
+ editor.commands.setContent(value, { contentType: 'markdown', emitUpdate: false });
63
+ lastOnChangeValue.current = value;
64
+ }
65
+ }, [editor, value]);
66
+
67
+ // Sync editable state
68
+ useEffect(() => {
69
+ if (editor && !editor.isDestroyed) {
70
+ editor.setEditable(editable);
71
+ }
72
+ }, [editor, editable]);
73
+
74
+ return { editor, featureSet };
75
+ }
@@ -19,7 +19,7 @@
19
19
  align-items: flex-start;
20
20
 
21
21
  overflow: hidden;
22
- z-index: 100;
22
+ z-index: var(--pte-new-layers-menu);
23
23
 
24
24
  transition: var(--pte-animations-interaction);
25
25
  opacity: 1;
@@ -0,0 +1,211 @@
1
+ import { render, screen, waitFor } from '../../test/render';
2
+ import { Menu, MenuButton, MenuItem, MenuItems } from './Menu';
3
+
4
+ function renderMenu(props?: { onItemClick?: () => void; position?: 'left' | 'right' }) {
5
+ return render(
6
+ <Menu>
7
+ <MenuButton>Options</MenuButton>
8
+ <MenuItems position={props?.position}>
9
+ <MenuItem as="button" onClick={props?.onItemClick}>
10
+ Edit
11
+ </MenuItem>
12
+ <MenuItem as="button">Delete</MenuItem>
13
+ <MenuItem as="button" disabled>
14
+ Archive
15
+ </MenuItem>
16
+ </MenuItems>
17
+ </Menu>,
18
+ );
19
+ }
20
+
21
+ describe('Menu', () => {
22
+ it('renders the menu button', () => {
23
+ renderMenu();
24
+
25
+ expect(screen.getByText('Options')).toBeInTheDocument();
26
+ });
27
+
28
+ it('does not show menu items initially', () => {
29
+ renderMenu();
30
+
31
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
32
+ });
33
+
34
+ it('opens menu when clicking the button', async () => {
35
+ const { user } = renderMenu();
36
+
37
+ await user.click(screen.getByText('Options'));
38
+
39
+ await waitFor(() => {
40
+ expect(screen.getByText('Edit')).toBeInTheDocument();
41
+ });
42
+
43
+ expect(screen.getByText('Delete')).toBeInTheDocument();
44
+ expect(screen.getByText('Archive')).toBeInTheDocument();
45
+ });
46
+
47
+ it('renders all menu items when open', async () => {
48
+ const { user } = renderMenu();
49
+
50
+ await user.click(screen.getByText('Options'));
51
+
52
+ await waitFor(() => {
53
+ expect(screen.getAllByRole('menuitem')).toHaveLength(3);
54
+ });
55
+ });
56
+
57
+ it('fires callback when clicking a menu item', async () => {
58
+ const onItemClick = vi.fn();
59
+ const { user } = renderMenu({ onItemClick });
60
+
61
+ await user.click(screen.getByText('Options'));
62
+
63
+ await waitFor(() => {
64
+ expect(screen.getByText('Edit')).toBeInTheDocument();
65
+ });
66
+
67
+ await user.click(screen.getByText('Edit'));
68
+
69
+ expect(onItemClick).toHaveBeenCalledOnce();
70
+ });
71
+
72
+ it('closes menu after clicking a menu item', async () => {
73
+ const { user } = renderMenu();
74
+
75
+ await user.click(screen.getByText('Options'));
76
+
77
+ await waitFor(() => {
78
+ expect(screen.getByText('Edit')).toBeInTheDocument();
79
+ });
80
+
81
+ await user.click(screen.getByText('Edit'));
82
+
83
+ await waitFor(() => {
84
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
85
+ });
86
+ });
87
+
88
+ it('closes menu when pressing Escape', async () => {
89
+ const { user } = renderMenu();
90
+
91
+ await user.click(screen.getByText('Options'));
92
+
93
+ await waitFor(() => {
94
+ expect(screen.getByText('Edit')).toBeInTheDocument();
95
+ });
96
+
97
+ await user.keyboard('{Escape}');
98
+
99
+ await waitFor(() => {
100
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
101
+ });
102
+ });
103
+
104
+ it('supports keyboard navigation with arrow keys', async () => {
105
+ const { user } = renderMenu();
106
+
107
+ await user.click(screen.getByText('Options'));
108
+
109
+ await waitFor(() => {
110
+ expect(screen.getByText('Edit')).toBeInTheDocument();
111
+ });
112
+
113
+ // HeadlessUI Menu uses arrow keys for navigation
114
+ await user.keyboard('{ArrowDown}');
115
+ await user.keyboard('{ArrowDown}');
116
+
117
+ // Verify focus moved (no crash)
118
+ const items = screen.getAllByRole('menuitem');
119
+ expect(items.length).toBeGreaterThan(0);
120
+ });
121
+
122
+ it('renders with isNew styling on a menu item', async () => {
123
+ const { user } = render(
124
+ <Menu>
125
+ <MenuButton>Options</MenuButton>
126
+ <MenuItems>
127
+ <MenuItem as="button" isNew>
128
+ New Feature
129
+ </MenuItem>
130
+ </MenuItems>
131
+ </Menu>,
132
+ );
133
+
134
+ await user.click(screen.getByText('Options'));
135
+
136
+ await waitFor(() => {
137
+ expect(screen.getByText('New Feature')).toBeInTheDocument();
138
+ });
139
+ });
140
+
141
+ it('supports right position for menu items', async () => {
142
+ const { user } = renderMenu({ position: 'right' });
143
+
144
+ await user.click(screen.getByText('Options'));
145
+
146
+ await waitFor(() => {
147
+ expect(screen.getByText('Edit')).toBeInTheDocument();
148
+ });
149
+ });
150
+
151
+ it('supports left position for menu items', async () => {
152
+ const { user } = renderMenu({ position: 'left' });
153
+
154
+ await user.click(screen.getByText('Options'));
155
+
156
+ await waitFor(() => {
157
+ expect(screen.getByText('Edit')).toBeInTheDocument();
158
+ });
159
+ });
160
+
161
+ it('applies custom className to Menu', () => {
162
+ const { container } = render(
163
+ <Menu className="custom-menu">
164
+ <MenuButton>Options</MenuButton>
165
+ <MenuItems>
166
+ <MenuItem as="button">Item</MenuItem>
167
+ </MenuItems>
168
+ </Menu>,
169
+ );
170
+
171
+ expect(container.querySelector('.custom-menu')).toBeInTheDocument();
172
+ });
173
+
174
+ it('renders menu button with correct role', () => {
175
+ renderMenu();
176
+
177
+ expect(screen.getByRole('button', { name: 'Options' })).toBeInTheDocument();
178
+ });
179
+
180
+ it('toggles menu open and closed with the button', async () => {
181
+ const { user } = renderMenu();
182
+
183
+ const button = screen.getByText('Options');
184
+
185
+ // Open
186
+ await user.click(button);
187
+ await waitFor(() => {
188
+ expect(screen.getByText('Edit')).toBeInTheDocument();
189
+ });
190
+
191
+ // Close
192
+ await user.click(button);
193
+ await waitFor(() => {
194
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
195
+ });
196
+ });
197
+
198
+ it('handles disabled menu items', async () => {
199
+ const { user } = renderMenu();
200
+
201
+ await user.click(screen.getByText('Options'));
202
+
203
+ await waitFor(() => {
204
+ expect(screen.getByText('Archive')).toBeInTheDocument();
205
+ });
206
+
207
+ // The disabled item should be rendered but marked as disabled
208
+ const archiveItem = screen.getByText('Archive');
209
+ expect(archiveItem.closest('[data-disabled]')).toBeInTheDocument();
210
+ });
211
+ });
@@ -0,0 +1,259 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { usePagination } from './usePagination';
3
+
4
+ describe('usePagination', () => {
5
+ it('initializes with the given initial page', () => {
6
+ const { result } = renderHook(() => usePagination('page1'));
7
+
8
+ expect(result.current.currentPage).toBe('page1');
9
+ });
10
+
11
+ it('initializes history with the initial page', () => {
12
+ const { result } = renderHook(() => usePagination('page1'));
13
+
14
+ expect(result.current.history).toEqual(['page1']);
15
+ });
16
+
17
+ it('cannot go back from the initial page', () => {
18
+ const { result } = renderHook(() => usePagination('page1'));
19
+
20
+ expect(result.current.canGoBack()).toBe(false);
21
+ });
22
+
23
+ it('cannot go forward from the initial page', () => {
24
+ const { result } = renderHook(() => usePagination('page1'));
25
+
26
+ expect(result.current.canGoForward()).toBe(false);
27
+ });
28
+
29
+ it('opens a new page and updates currentPage', () => {
30
+ const { result } = renderHook(() => usePagination('page1'));
31
+
32
+ act(() => {
33
+ result.current.open('page2');
34
+ });
35
+
36
+ expect(result.current.currentPage).toBe('page2');
37
+ });
38
+
39
+ it('adds opened page to history', () => {
40
+ const { result } = renderHook(() => usePagination('page1'));
41
+
42
+ act(() => {
43
+ result.current.open('page2');
44
+ });
45
+
46
+ expect(result.current.history).toEqual(['page1', 'page2']);
47
+ });
48
+
49
+ it('can go back after opening a new page', () => {
50
+ const { result } = renderHook(() => usePagination('page1'));
51
+
52
+ act(() => {
53
+ result.current.open('page2');
54
+ });
55
+
56
+ expect(result.current.canGoBack()).toBe(true);
57
+ });
58
+
59
+ it('goes back to the previous page', () => {
60
+ const { result } = renderHook(() => usePagination('page1'));
61
+
62
+ act(() => {
63
+ result.current.open('page2');
64
+ });
65
+
66
+ act(() => {
67
+ result.current.back();
68
+ });
69
+
70
+ expect(result.current.currentPage).toBe('page1');
71
+ });
72
+
73
+ it('does nothing when going back on the first page', () => {
74
+ const { result } = renderHook(() => usePagination('page1'));
75
+
76
+ act(() => {
77
+ result.current.back();
78
+ });
79
+
80
+ expect(result.current.currentPage).toBe('page1');
81
+ });
82
+
83
+ it('can go forward after going back', () => {
84
+ const { result } = renderHook(() => usePagination('page1'));
85
+
86
+ act(() => {
87
+ result.current.open('page2');
88
+ });
89
+
90
+ act(() => {
91
+ result.current.back();
92
+ });
93
+
94
+ expect(result.current.canGoForward()).toBe(true);
95
+ });
96
+
97
+ it('goes forward to the next page in history', () => {
98
+ const { result } = renderHook(() => usePagination('page1'));
99
+
100
+ act(() => {
101
+ result.current.open('page2');
102
+ });
103
+
104
+ act(() => {
105
+ result.current.back();
106
+ });
107
+
108
+ act(() => {
109
+ result.current.forward();
110
+ });
111
+
112
+ expect(result.current.currentPage).toBe('page2');
113
+ });
114
+
115
+ it('does nothing when going forward at the end of history', () => {
116
+ const { result } = renderHook(() => usePagination('page1'));
117
+
118
+ act(() => {
119
+ result.current.forward();
120
+ });
121
+
122
+ expect(result.current.currentPage).toBe('page1');
123
+ });
124
+
125
+ it('truncates forward history when opening a new page after going back', () => {
126
+ const { result } = renderHook(() => usePagination('page1'));
127
+
128
+ act(() => {
129
+ result.current.open('page2');
130
+ });
131
+
132
+ act(() => {
133
+ result.current.open('page3');
134
+ });
135
+
136
+ act(() => {
137
+ result.current.back();
138
+ });
139
+
140
+ // Now on page2, open page4 — page3 should be removed from history
141
+ act(() => {
142
+ result.current.open('page4');
143
+ });
144
+
145
+ expect(result.current.currentPage).toBe('page4');
146
+ expect(result.current.history).toEqual(['page1', 'page2', 'page4']);
147
+ expect(result.current.canGoForward()).toBe(false);
148
+ });
149
+
150
+ it('does not add duplicate page when opening the current page', () => {
151
+ const { result } = renderHook(() => usePagination('page1'));
152
+
153
+ act(() => {
154
+ result.current.open('page1');
155
+ });
156
+
157
+ expect(result.current.history).toEqual(['page1']);
158
+ expect(result.current.currentPage).toBe('page1');
159
+ });
160
+
161
+ it('resets to initial state', () => {
162
+ const { result } = renderHook(() => usePagination('page1'));
163
+
164
+ act(() => {
165
+ result.current.open('page2');
166
+ });
167
+
168
+ act(() => {
169
+ result.current.open('page3');
170
+ });
171
+
172
+ act(() => {
173
+ result.current.reset();
174
+ });
175
+
176
+ expect(result.current.currentPage).toBe('page1');
177
+ expect(result.current.history).toEqual(['page1']);
178
+ expect(result.current.canGoBack()).toBe(false);
179
+ expect(result.current.canGoForward()).toBe(false);
180
+ });
181
+
182
+ it('supports navigating through multiple pages in sequence', () => {
183
+ const { result } = renderHook(() => usePagination('page1'));
184
+
185
+ act(() => {
186
+ result.current.open('page2');
187
+ });
188
+
189
+ act(() => {
190
+ result.current.open('page3');
191
+ });
192
+
193
+ act(() => {
194
+ result.current.open('page4');
195
+ });
196
+
197
+ expect(result.current.history).toEqual(['page1', 'page2', 'page3', 'page4']);
198
+ expect(result.current.currentPage).toBe('page4');
199
+
200
+ act(() => {
201
+ result.current.back();
202
+ });
203
+
204
+ act(() => {
205
+ result.current.back();
206
+ });
207
+
208
+ expect(result.current.currentPage).toBe('page2');
209
+ expect(result.current.canGoBack()).toBe(true);
210
+ expect(result.current.canGoForward()).toBe(true);
211
+ });
212
+
213
+ it('supports typed page keys', () => {
214
+ const pages = ['home', 'settings', 'profile'] as const;
215
+ const { result } = renderHook(() => usePagination<typeof pages>('home'));
216
+
217
+ act(() => {
218
+ result.current.open('settings');
219
+ });
220
+
221
+ expect(result.current.currentPage).toBe('settings');
222
+
223
+ act(() => {
224
+ result.current.open('profile');
225
+ });
226
+
227
+ expect(result.current.history).toEqual(['home', 'settings', 'profile']);
228
+ });
229
+
230
+ it('back and forward are idempotent at boundaries', () => {
231
+ const { result } = renderHook(() => usePagination('page1'));
232
+
233
+ // Multiple backs at start should not throw or change state
234
+ act(() => {
235
+ result.current.back();
236
+ });
237
+
238
+ act(() => {
239
+ result.current.back();
240
+ });
241
+
242
+ expect(result.current.currentPage).toBe('page1');
243
+
244
+ act(() => {
245
+ result.current.open('page2');
246
+ });
247
+
248
+ // Multiple forwards at end should not throw or change state
249
+ act(() => {
250
+ result.current.forward();
251
+ });
252
+
253
+ act(() => {
254
+ result.current.forward();
255
+ });
256
+
257
+ expect(result.current.currentPage).toBe('page2');
258
+ });
259
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { render, screen, waitFor } from '../../test/render';
3
+ import { Popover } from './Popover';
4
+
5
+ describe('Popover', () => {
6
+ it('renders the trigger element', () => {
7
+ render(
8
+ <Popover trigger={<button type="button">Open Popover</button>}>
9
+ <p>Popover content</p>
10
+ </Popover>,
11
+ );
12
+
13
+ expect(screen.getByText('Open Popover')).toBeInTheDocument();
14
+ });
15
+
16
+ it('does not show popover content initially', () => {
17
+ render(
18
+ <Popover trigger={<button type="button">Open Popover</button>}>
19
+ <p>Popover content</p>
20
+ </Popover>,
21
+ );
22
+
23
+ expect(screen.queryByText('Popover content')).not.toBeInTheDocument();
24
+ });
25
+
26
+ it('shows popover content when the trigger is clicked', async () => {
27
+ const { user } = render(
28
+ <Popover trigger={<button type="button">Open Popover</button>}>
29
+ <p>Popover content</p>
30
+ </Popover>,
31
+ );
32
+
33
+ await user.click(screen.getByText('Open Popover'));
34
+
35
+ await waitFor(() => {
36
+ expect(screen.getByText('Popover content')).toBeInTheDocument();
37
+ });
38
+ });
39
+
40
+ it('hides popover content when the trigger is clicked again', async () => {
41
+ const { user } = render(
42
+ <Popover trigger={<button type="button">Open Popover</button>}>
43
+ <p>Popover content</p>
44
+ </Popover>,
45
+ );
46
+
47
+ await user.click(screen.getByText('Open Popover'));
48
+
49
+ await waitFor(() => {
50
+ expect(screen.getByText('Popover content')).toBeInTheDocument();
51
+ });
52
+
53
+ await user.click(screen.getByText('Open Popover'));
54
+
55
+ await waitFor(() => {
56
+ expect(screen.queryByText('Popover content')).not.toBeInTheDocument();
57
+ });
58
+ });
59
+
60
+ it('supports controlled isOpen state', () => {
61
+ render(
62
+ <Popover trigger={<button type="button">Open Popover</button>} isOpen={true} setIsOpen={vi.fn()}>
63
+ <p>Controlled content</p>
64
+ </Popover>,
65
+ );
66
+
67
+ expect(screen.getByText('Controlled content')).toBeInTheDocument();
68
+ });
69
+
70
+ it('does not show content when controlled isOpen is false', () => {
71
+ render(
72
+ <Popover trigger={<button type="button">Open Popover</button>} isOpen={false} setIsOpen={vi.fn()}>
73
+ <p>Controlled content</p>
74
+ </Popover>,
75
+ );
76
+
77
+ expect(screen.queryByText('Controlled content')).not.toBeInTheDocument();
78
+ });
79
+
80
+ it('calls setIsOpen when trigger is clicked in controlled mode', async () => {
81
+ const setIsOpen = vi.fn();
82
+ const { user } = render(
83
+ <Popover trigger={<button type="button">Open Popover</button>} isOpen={false} setIsOpen={setIsOpen}>
84
+ <p>Controlled content</p>
85
+ </Popover>,
86
+ );
87
+
88
+ await user.click(screen.getByText('Open Popover'));
89
+
90
+ expect(setIsOpen).toHaveBeenCalledWith(true);
91
+ });
92
+
93
+ it('renders children content inside the popover', async () => {
94
+ const { user } = render(
95
+ <Popover trigger={<button type="button">Open Popover</button>}>
96
+ <div>
97
+ <h3>Title</h3>
98
+ <p>Description text</p>
99
+ </div>
100
+ </Popover>,
101
+ );
102
+
103
+ await user.click(screen.getByText('Open Popover'));
104
+
105
+ await waitFor(() => {
106
+ expect(screen.getByText('Title')).toBeInTheDocument();
107
+ expect(screen.getByText('Description text')).toBeInTheDocument();
108
+ });
109
+ });
110
+
111
+ it('renders with custom positions', async () => {
112
+ const { user } = render(
113
+ <Popover trigger={<button type="button">Open Popover</button>} positions={['top', 'bottom']}>
114
+ <p>Positioned content</p>
115
+ </Popover>,
116
+ );
117
+
118
+ await user.click(screen.getByText('Open Popover'));
119
+
120
+ await waitFor(() => {
121
+ expect(screen.getByText('Positioned content')).toBeInTheDocument();
122
+ });
123
+ });
124
+
125
+ it('renders with custom alignment', async () => {
126
+ const { user } = render(
127
+ <Popover trigger={<button type="button">Open Popover</button>} align="center">
128
+ <p>Aligned content</p>
129
+ </Popover>,
130
+ );
131
+
132
+ await user.click(screen.getByText('Open Popover'));
133
+
134
+ await waitFor(() => {
135
+ expect(screen.getByText('Aligned content')).toBeInTheDocument();
136
+ });
137
+ });
138
+
139
+ it('renders with custom padding', async () => {
140
+ const { user } = render(
141
+ <Popover trigger={<button type="button">Open Popover</button>} padding={16}>
142
+ <p>Padded content</p>
143
+ </Popover>,
144
+ );
145
+
146
+ await user.click(screen.getByText('Open Popover'));
147
+
148
+ await waitFor(() => {
149
+ expect(screen.getByText('Padded content')).toBeInTheDocument();
150
+ });
151
+ });
152
+ });