paris 0.19.0 → 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.
- package/CHANGELOG.md +10 -0
- package/package.json +17 -2
- package/src/stories/accordion/Accordion.test.tsx +140 -0
- package/src/stories/accordionselect/AccordionSelect.test.tsx +252 -0
- package/src/stories/avatar/Avatar.test.tsx +77 -0
- package/src/stories/button/Button.test.tsx +266 -0
- package/src/stories/callout/Callout.test.tsx +79 -0
- package/src/stories/card/Card.test.tsx +81 -0
- package/src/stories/cardbutton/CardButton.test.tsx +174 -0
- package/src/stories/checkbox/Checkbox.test.tsx +531 -0
- package/src/stories/combobox/Combobox.test.tsx +164 -0
- package/src/stories/dialog/Dialog.module.scss +2 -2
- package/src/stories/dialog/Dialog.test.tsx +244 -0
- package/src/stories/drawer/Drawer.module.scss +2 -2
- package/src/stories/drawer/Drawer.test.tsx +259 -0
- package/src/stories/field/Field.test.tsx +146 -0
- package/src/stories/icon/Icon.test.tsx +59 -0
- package/src/stories/informationaltooltip/InformationalTooltip.test.tsx +178 -0
- package/src/stories/input/Input.test.tsx +174 -0
- package/src/stories/markdown/Markdown.test.tsx +228 -0
- package/src/stories/markdowneditor/FixedToolbar.tsx +44 -14
- package/src/stories/markdowneditor/LinkPopover.module.scss +1 -1
- package/src/stories/markdowneditor/MarkdownEditor.stories.tsx +4 -1
- package/src/stories/markdowneditor/MarkdownEditor.test.tsx +115 -0
- package/src/stories/markdowneditor/MarkdownEditor.tsx +11 -1
- package/src/stories/markdowneditor/MarkdownEditorContext.tsx +3 -0
- package/src/stories/markdowneditor/index.ts +1 -0
- package/src/stories/menu/Menu.module.scss +1 -1
- package/src/stories/menu/Menu.test.tsx +211 -0
- package/src/stories/pagination/usePagination.test.ts +259 -0
- package/src/stories/popover/Popover.test.tsx +152 -0
- package/src/stories/select/Select.module.scss +2 -1
- package/src/stories/select/Select.test.tsx +233 -0
- package/src/stories/styledlink/StyledLink.test.tsx +59 -0
- package/src/stories/table/Table.test.tsx +156 -0
- package/src/stories/tabs/Tabs.module.scss +1 -1
- package/src/stories/tabs/Tabs.test.tsx +167 -0
- package/src/stories/tag/Tag.test.tsx +90 -0
- package/src/stories/text/Text.test.tsx +81 -0
- package/src/stories/textarea/TextArea.test.tsx +147 -0
- package/src/stories/theme/themes.ts +16 -0
- package/src/stories/tilt/Tilt.test.tsx +203 -0
- package/src/stories/toast/Toast.test.tsx +86 -0
- package/src/stories/utility/Dropdown.module.scss +1 -1
- package/src/stories/utility/Utility.test.tsx +96 -0
- package/src/test/render.tsx +20 -0
- package/src/test/setup.ts +32 -0
|
@@ -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
|
+
});
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
top: 100%;
|
|
34
34
|
left: 0;
|
|
35
35
|
right: 0;
|
|
36
|
-
z-index:
|
|
36
|
+
z-index: var(--pte-new-layers-dropdown);
|
|
37
37
|
|
|
38
38
|
display: flex;
|
|
39
39
|
flex-direction: column;
|
|
@@ -265,6 +265,7 @@
|
|
|
265
265
|
justify-content: center;
|
|
266
266
|
align-items: center;
|
|
267
267
|
position: relative;
|
|
268
|
+
isolation: isolate;
|
|
268
269
|
transition: var(--pte-animations-interaction);
|
|
269
270
|
|
|
270
271
|
&.tall {
|