@getmicdrop/svelte-components 5.17.1 → 5.17.4
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/calendar/Calendar/MiniMonthCalendar.svelte +5 -7
- package/dist/calendar/Calendar/MiniMonthCalendar.svelte.d.ts.map +1 -1
- package/dist/calendar/MonthSwitcher/MonthSwitcher.svelte +2 -3
- package/dist/calendar/MonthSwitcher/MonthSwitcher.svelte.d.ts.map +1 -1
- package/dist/calendar/PublicCard/PublicCard.svelte +23 -14
- package/dist/calendar/PublicCard/PublicCard.svelte.d.ts.map +1 -1
- package/dist/calendar/ShowCard/ShowCard.spec.js +1 -7
- package/dist/calendar/ShowCard/ShowCard.svelte +10 -1
- package/dist/calendar/ShowCard/ShowCard.svelte.d.ts.map +1 -1
- package/dist/calendar/ShowTimeCard/ShowTimeCard.svelte +11 -0
- package/dist/calendar/ShowTimeCard/ShowTimeCard.svelte.d.ts +2 -0
- package/dist/calendar/ShowTimeCard/ShowTimeCard.svelte.d.ts.map +1 -1
- package/dist/components/Heading.spec.d.ts +2 -0
- package/dist/components/Heading.spec.d.ts.map +1 -0
- package/dist/components/Heading.spec.js +89 -0
- package/dist/components/Layout/__tests__/AppShell.test.js +140 -0
- package/dist/components/Text.spec.d.ts +2 -0
- package/dist/components/Text.spec.d.ts.map +1 -0
- package/dist/components/Text.spec.js +89 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.js +147 -1
- package/dist/datetime/README.md +323 -0
- package/dist/forms/createFormStore.svelte.spec.d.ts +2 -0
- package/dist/forms/createFormStore.svelte.spec.d.ts.map +1 -0
- package/dist/forms/createFormStore.svelte.spec.js +387 -0
- package/dist/messages.d.ts +43 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +57 -0
- package/dist/patterns/chat/ChatActivityNotice.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatActivityNotice.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatActivityNotice.spec.js +59 -0
- package/dist/patterns/chat/ChatBubble.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatBubble.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatBubble.spec.js +91 -0
- package/dist/patterns/chat/ChatContainer.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatContainer.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatContainer.spec.js +30 -0
- package/dist/patterns/chat/ChatDateDivider.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatDateDivider.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatDateDivider.spec.js +30 -0
- package/dist/patterns/chat/ChatInvitationBubble.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatInvitationBubble.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatInvitationBubble.spec.js +46 -0
- package/dist/patterns/chat/ChatInvitationNotice.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatInvitationNotice.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatInvitationNotice.spec.js +32 -0
- package/dist/patterns/chat/ChatMessageGroup.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatMessageGroup.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatMessageGroup.spec.js +58 -0
- package/dist/patterns/chat/ChatSlotUpdate.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatSlotUpdate.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatSlotUpdate.spec.js +65 -0
- package/dist/patterns/chat/ChatStatusBadge.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatStatusBadge.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatStatusBadge.spec.js +79 -0
- package/dist/patterns/chat/ChatStatusTransition.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatStatusTransition.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatStatusTransition.spec.js +81 -0
- package/dist/patterns/chat/ChatTextBubble.spec.d.ts +2 -0
- package/dist/patterns/chat/ChatTextBubble.spec.d.ts.map +1 -0
- package/dist/patterns/chat/ChatTextBubble.spec.js +35 -0
- package/dist/patterns/data/DataTable.spec.js +61 -0
- package/dist/patterns/forms/FormGrid.spec.js +34 -0
- package/dist/patterns/layout/Sidebar.spec.js +240 -1
- package/dist/patterns/layout/SidebarTestWrapper.svelte +34 -0
- package/dist/patterns/layout/SidebarTestWrapper.svelte.d.ts +23 -0
- package/dist/patterns/layout/SidebarTestWrapper.svelte.d.ts.map +1 -0
- package/dist/patterns/navigation/Header.spec.js +123 -0
- package/dist/primitives/Accordion/Accordion.spec.js +112 -2
- package/dist/primitives/Accordion/AccordionToggleWrapper.test.svelte +28 -0
- package/dist/primitives/Accordion/AccordionToggleWrapper.test.svelte.d.ts +7 -0
- package/dist/primitives/Accordion/AccordionToggleWrapper.test.svelte.d.ts.map +1 -0
- package/dist/primitives/Avatar/Avatar.spec.js +23 -0
- package/dist/primitives/BottomSheet/BottomSheet.spec.js +102 -0
- package/dist/primitives/BottomSheet/BottomSheetWithActions.test.svelte +20 -0
- package/dist/primitives/BottomSheet/BottomSheetWithActions.test.svelte.d.ts +10 -0
- package/dist/primitives/BottomSheet/BottomSheetWithActions.test.svelte.d.ts.map +1 -0
- package/dist/primitives/Button/ButtonGroup.spec.d.ts +2 -0
- package/dist/primitives/Button/ButtonGroup.spec.d.ts.map +1 -0
- package/dist/primitives/Button/ButtonGroup.spec.js +44 -0
- package/dist/primitives/Checkbox/Checkbox.spec.js +32 -0
- package/dist/primitives/Drawer/Drawer.spec.js +437 -0
- package/dist/primitives/Drawer/DrawerTestWrapper.svelte +86 -0
- package/dist/primitives/Drawer/DrawerTestWrapper.svelte.d.ts +26 -0
- package/dist/primitives/Drawer/DrawerTestWrapper.svelte.d.ts.map +1 -0
- package/dist/primitives/Dropdown/Dropdown.spec.js +116 -0
- package/dist/primitives/Dropdown/DropdownDivider.spec.d.ts +2 -0
- package/dist/primitives/Dropdown/DropdownDivider.spec.d.ts.map +1 -0
- package/dist/primitives/Dropdown/DropdownDivider.spec.js +30 -0
- package/dist/primitives/Dropdown/DropdownItem.spec.js +155 -1
- package/dist/primitives/Dropdown/DropdownItemTestWrapper.svelte +43 -0
- package/dist/primitives/Dropdown/DropdownItemTestWrapper.svelte.d.ts +17 -0
- package/dist/primitives/Dropdown/DropdownItemTestWrapper.svelte.d.ts.map +1 -0
- package/dist/primitives/Helper/Helper.spec.d.ts +2 -0
- package/dist/primitives/Helper/Helper.spec.d.ts.map +1 -0
- package/dist/primitives/Helper/Helper.spec.js +57 -0
- package/dist/primitives/Input/Input.spec.js +664 -0
- package/dist/primitives/Input/Input.svelte +18 -10
- package/dist/primitives/Input/Input.svelte.d.ts.map +1 -1
- package/dist/primitives/Input/Select.spec.js +414 -0
- package/dist/primitives/Label/Label.spec.js +9 -0
- package/dist/primitives/LandingButton/LandingButton.spec.d.ts +2 -0
- package/dist/primitives/LandingButton/LandingButton.spec.d.ts.map +1 -0
- package/dist/primitives/LandingButton/LandingButton.spec.js +61 -0
- package/dist/primitives/MenuItem/MenuItem.spec.d.ts +2 -0
- package/dist/primitives/MenuItem/MenuItem.spec.d.ts.map +1 -0
- package/dist/primitives/MenuItem/MenuItem.spec.js +130 -0
- package/dist/primitives/Modal/Modal.spec.js +215 -0
- package/dist/primitives/NavItem/NavItem.spec.d.ts +2 -0
- package/dist/primitives/NavItem/NavItem.spec.d.ts.map +1 -0
- package/dist/primitives/NavItem/NavItem.spec.js +97 -0
- package/dist/primitives/SearchResultItem/SearchResultItem.spec.d.ts +2 -0
- package/dist/primitives/SearchResultItem/SearchResultItem.spec.d.ts.map +1 -0
- package/dist/primitives/SearchResultItem/SearchResultItem.spec.js +78 -0
- package/dist/primitives/SidebarToggle/SidebarToggle.spec.d.ts +2 -0
- package/dist/primitives/SidebarToggle/SidebarToggle.spec.d.ts.map +1 -0
- package/dist/primitives/SidebarToggle/SidebarToggle.spec.js +61 -0
- package/dist/primitives/Spinner/Spinner.spec.js +13 -0
- package/dist/primitives/Toggle.spec.js +75 -0
- package/dist/primitives/ToggleTestWrapper.svelte +30 -0
- package/dist/primitives/ToggleTestWrapper.svelte.d.ts +29 -0
- package/dist/primitives/ToggleTestWrapper.svelte.d.ts.map +1 -0
- package/dist/primitives/Tooltip/Tooltip.spec.d.ts +2 -0
- package/dist/primitives/Tooltip/Tooltip.spec.d.ts.map +1 -0
- package/dist/primitives/Tooltip/Tooltip.spec.js +126 -0
- package/dist/recipes/inputs/Search.spec.js +66 -2
- package/dist/recipes/modals/ConfirmationModal.spec.js +190 -0
- package/dist/schemas/__tests__/auth.test.d.ts +2 -0
- package/dist/schemas/__tests__/auth.test.d.ts.map +1 -0
- package/dist/schemas/__tests__/auth.test.js +210 -0
- package/dist/schemas/__tests__/common.test.d.ts +2 -0
- package/dist/schemas/__tests__/common.test.d.ts.map +1 -0
- package/dist/schemas/__tests__/common.test.js +340 -0
- package/dist/schemas/__tests__/domain.test.d.ts +2 -0
- package/dist/schemas/__tests__/domain.test.d.ts.map +1 -0
- package/dist/schemas/__tests__/domain.test.js +293 -0
- package/dist/schemas/__tests__/order.test.d.ts +2 -0
- package/dist/schemas/__tests__/order.test.d.ts.map +1 -0
- package/dist/schemas/__tests__/order.test.js +349 -0
- package/dist/schemas/__tests__/user.test.d.ts +2 -0
- package/dist/schemas/__tests__/user.test.d.ts.map +1 -0
- package/dist/schemas/__tests__/user.test.js +325 -0
- package/dist/schemas/auth.d.ts +41 -0
- package/dist/schemas/auth.d.ts.map +1 -0
- package/dist/schemas/auth.js +69 -0
- package/dist/schemas/common.d.ts +43 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +157 -0
- package/dist/schemas/event.d.ts +82 -0
- package/dist/schemas/event.d.ts.map +1 -0
- package/dist/schemas/event.js +58 -0
- package/dist/schemas/index.d.ts +10 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +9 -0
- package/dist/schemas/order.d.ts +111 -0
- package/dist/schemas/order.d.ts.map +1 -0
- package/dist/schemas/order.js +73 -0
- package/dist/schemas/performer.d.ts +133 -0
- package/dist/schemas/performer.d.ts.map +1 -0
- package/dist/schemas/performer.js +73 -0
- package/dist/schemas/promo.d.ts +87 -0
- package/dist/schemas/promo.d.ts.map +1 -0
- package/dist/schemas/promo.js +98 -0
- package/dist/schemas/ticket.d.ts +104 -0
- package/dist/schemas/ticket.d.ts.map +1 -0
- package/dist/schemas/ticket.js +82 -0
- package/dist/schemas/user.d.ts +92 -0
- package/dist/schemas/user.d.ts.map +1 -0
- package/dist/schemas/user.js +53 -0
- package/dist/schemas/venue.d.ts +95 -0
- package/dist/schemas/venue.d.ts.map +1 -0
- package/dist/schemas/venue.js +52 -0
- package/dist/stores/auth.svelte.spec.d.ts +2 -0
- package/dist/stores/auth.svelte.spec.d.ts.map +1 -0
- package/dist/stores/auth.svelte.spec.js +112 -0
- package/dist/stores/formDataStore.svelte.spec.d.ts +2 -0
- package/dist/stores/formDataStore.svelte.spec.d.ts.map +1 -0
- package/dist/stores/formDataStore.svelte.spec.js +150 -0
- package/dist/stores/formSave.svelte.spec.d.ts +2 -0
- package/dist/stores/formSave.svelte.spec.d.ts.map +1 -0
- package/dist/stores/formSave.svelte.spec.js +196 -0
- package/dist/stores/navigation.spec.d.ts +2 -0
- package/dist/stores/navigation.spec.d.ts.map +1 -0
- package/dist/stores/navigation.spec.js +53 -0
- package/dist/telemetry.spec.js +5 -0
- package/dist/tokens/__tests__/sizing.test.js +2 -2
- package/dist/tokens/sizing.d.ts +5 -0
- package/dist/tokens/sizing.d.ts.map +1 -1
- package/dist/tokens/sizing.js +6 -0
- package/dist/utils/haptic.spec.d.ts +2 -0
- package/dist/utils/haptic.spec.d.ts.map +1 -0
- package/dist/utils/haptic.spec.js +153 -0
- package/dist/utils/imageOptimizer.spec.d.ts +2 -0
- package/dist/utils/imageOptimizer.spec.d.ts.map +1 -0
- package/dist/utils/imageOptimizer.spec.js +201 -0
- package/dist/utils/logger.spec.d.ts +2 -0
- package/dist/utils/logger.spec.d.ts.map +1 -0
- package/dist/utils/logger.spec.js +95 -0
- package/package.json +1 -2
|
@@ -2,6 +2,7 @@ import { render, screen, waitFor } from '@testing-library/svelte';
|
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { expect, describe, test, vi, beforeEach, afterEach } from 'vitest';
|
|
4
4
|
import Drawer from './Drawer.svelte';
|
|
5
|
+
import DrawerTestWrapper from './DrawerTestWrapper.svelte';
|
|
5
6
|
|
|
6
7
|
describe('Drawer Component', () => {
|
|
7
8
|
beforeEach(() => {
|
|
@@ -209,4 +210,440 @@ describe('Drawer Callbacks', () => {
|
|
|
209
210
|
});
|
|
210
211
|
expect(container.querySelector('[role="dialog"]')).toBeInTheDocument();
|
|
211
212
|
});
|
|
213
|
+
|
|
214
|
+
test('backdrop click calls onclose', async () => {
|
|
215
|
+
const onclose = vi.fn();
|
|
216
|
+
const { container } = render(Drawer, {
|
|
217
|
+
props: { open: true, closeOnBackdropClick: true, onclose }
|
|
218
|
+
});
|
|
219
|
+
const backdrop = container.querySelector('[role="presentation"]');
|
|
220
|
+
expect(backdrop).toBeInTheDocument();
|
|
221
|
+
await userEvent.click(backdrop);
|
|
222
|
+
expect(onclose).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('escape key closes drawer', async () => {
|
|
226
|
+
const onclose = vi.fn();
|
|
227
|
+
render(Drawer, {
|
|
228
|
+
props: { open: true, closeOnEscape: true, onclose }
|
|
229
|
+
});
|
|
230
|
+
await userEvent.keyboard('{Escape}');
|
|
231
|
+
expect(onclose).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('escape key does not close when closeOnEscape is false', async () => {
|
|
235
|
+
const onclose = vi.fn();
|
|
236
|
+
render(Drawer, {
|
|
237
|
+
props: { open: true, closeOnEscape: false, onclose }
|
|
238
|
+
});
|
|
239
|
+
await userEvent.keyboard('{Escape}');
|
|
240
|
+
expect(onclose).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('Drawer Additional Props', () => {
|
|
245
|
+
test('xl width applies w-[28rem]', () => {
|
|
246
|
+
const { container } = render(Drawer, { props: { open: true, width: 'xl' } });
|
|
247
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
248
|
+
expect(dialog).toHaveClass('w-[28rem]');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('default placement falls back to left', () => {
|
|
252
|
+
const { container } = render(Drawer, { props: { open: true } });
|
|
253
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
254
|
+
expect(dialog).toHaveClass('left-0');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('unknown width falls back to md', () => {
|
|
258
|
+
const { container } = render(Drawer, { props: { open: true, width: 'unknown' } });
|
|
259
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
260
|
+
expect(dialog).toHaveClass('w-80');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('sets aria-labelledby only when title and id both provided', () => {
|
|
264
|
+
const { container } = render(Drawer, { props: { open: true, title: '' } });
|
|
265
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
266
|
+
expect(dialog).not.toHaveAttribute('aria-labelledby');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('renders header when title is set', () => {
|
|
270
|
+
const { container } = render(Drawer, {
|
|
271
|
+
props: { open: true, title: 'My Title', id: 'test-id' }
|
|
272
|
+
});
|
|
273
|
+
expect(container.querySelector('header')).toBeInTheDocument();
|
|
274
|
+
expect(screen.getByText('My Title')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('no header when title is empty and no header snippet', () => {
|
|
278
|
+
const { container } = render(Drawer, { props: { open: true, title: '' } });
|
|
279
|
+
expect(container.querySelector('header')).not.toBeInTheDocument();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('right placement has border-l', () => {
|
|
283
|
+
const { container } = render(Drawer, { props: { open: true, placement: 'right' } });
|
|
284
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
285
|
+
expect(dialog).toHaveClass('border-l');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('top placement has full width', () => {
|
|
289
|
+
const { container } = render(Drawer, { props: { open: true, placement: 'top' } });
|
|
290
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
291
|
+
expect(dialog).toHaveClass('w-full');
|
|
292
|
+
expect(dialog).toHaveClass('border-b');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('bottom placement has full width', () => {
|
|
296
|
+
const { container } = render(Drawer, { props: { open: true, placement: 'bottom' } });
|
|
297
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
298
|
+
expect(dialog).toHaveClass('w-full');
|
|
299
|
+
expect(dialog).toHaveClass('border-t');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ─── NEW TESTS: Uncovered branches ───────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
describe('Drawer Snippet Rendering (via TestWrapper)', () => {
|
|
306
|
+
test('renders custom header snippet instead of title', () => {
|
|
307
|
+
const { container } = render(DrawerTestWrapper, {
|
|
308
|
+
props: { open: true, showHeader: true, headerText: 'My Custom Header' }
|
|
309
|
+
});
|
|
310
|
+
expect(container.querySelector('header')).toBeInTheDocument();
|
|
311
|
+
expect(screen.getByTestId('custom-header')).toBeInTheDocument();
|
|
312
|
+
expect(screen.getByText('My Custom Header')).toBeInTheDocument();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('renders header section when header snippet is provided even without title', () => {
|
|
316
|
+
const { container } = render(DrawerTestWrapper, {
|
|
317
|
+
props: { open: true, title: '', showHeader: true }
|
|
318
|
+
});
|
|
319
|
+
// The header section should render because the header snippet is always passed from the wrapper
|
|
320
|
+
expect(container.querySelector('header')).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('renders children content', () => {
|
|
324
|
+
const { container } = render(DrawerTestWrapper, {
|
|
325
|
+
props: { open: true, showChildren: true, childrenText: 'Hello Drawer Body' }
|
|
326
|
+
});
|
|
327
|
+
expect(screen.getByTestId('drawer-children')).toBeInTheDocument();
|
|
328
|
+
expect(screen.getByText('Hello Drawer Body')).toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('renders actions footer when actions snippet is provided', () => {
|
|
332
|
+
const { container } = render(DrawerTestWrapper, {
|
|
333
|
+
props: { open: true, showActions: true, actionsText: 'Save Changes' }
|
|
334
|
+
});
|
|
335
|
+
// The footer element should be rendered (line 219)
|
|
336
|
+
expect(container.querySelector('footer')).toBeInTheDocument();
|
|
337
|
+
expect(screen.getByTestId('drawer-actions')).toBeInTheDocument();
|
|
338
|
+
expect(screen.getByText('Save Changes')).toBeInTheDocument();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('actions footer has correct styling classes', () => {
|
|
342
|
+
const { container } = render(DrawerTestWrapper, {
|
|
343
|
+
props: { open: true, showActions: true }
|
|
344
|
+
});
|
|
345
|
+
const footer = container.querySelector('footer');
|
|
346
|
+
expect(footer).toBeInTheDocument();
|
|
347
|
+
expect(footer).toHaveClass('shrink-0');
|
|
348
|
+
expect(footer).toHaveClass('p-4');
|
|
349
|
+
expect(footer).toHaveClass('border-t');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('no footer when actions snippet is not provided', () => {
|
|
353
|
+
const { container } = render(Drawer, { props: { open: true } });
|
|
354
|
+
expect(container.querySelector('footer')).not.toBeInTheDocument();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('Drawer Unknown/Default Placement Branch', () => {
|
|
359
|
+
test('invalid placement falls back to left-0 via default switch case for placementClasses', () => {
|
|
360
|
+
const { container } = render(Drawer, { props: { open: true, placement: 'invalid' } });
|
|
361
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
362
|
+
// The default case in placementClasses returns the same as 'left'
|
|
363
|
+
expect(dialog).toHaveClass('left-0');
|
|
364
|
+
expect(dialog).toHaveClass('h-screen');
|
|
365
|
+
expect(dialog).toHaveClass('border-r');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('invalid placement falls back to left fly transition via default switch case', () => {
|
|
369
|
+
// This exercises the default case in flyParams (line 69)
|
|
370
|
+
const { container } = render(Drawer, { props: { open: true, placement: 'diagonal' } });
|
|
371
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
372
|
+
// If the default case is hit, it still renders with left-like classes
|
|
373
|
+
expect(dialog).toHaveClass('left-0');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('Drawer Edge Mode - Edge Click and Callbacks', () => {
|
|
378
|
+
test('edge trigger click opens drawer and calls onopen', async () => {
|
|
379
|
+
const onopen = vi.fn();
|
|
380
|
+
const { container } = render(Drawer, {
|
|
381
|
+
props: { edge: true, placement: 'bottom', open: false, onopen }
|
|
382
|
+
});
|
|
383
|
+
const edgeTrigger = container.querySelector('.cursor-pointer');
|
|
384
|
+
expect(edgeTrigger).toBeInTheDocument();
|
|
385
|
+
await userEvent.click(edgeTrigger);
|
|
386
|
+
expect(onopen).toHaveBeenCalled();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('edge trigger does not show when edge=false', () => {
|
|
390
|
+
const { container } = render(Drawer, {
|
|
391
|
+
props: { edge: false, placement: 'bottom', open: false }
|
|
392
|
+
});
|
|
393
|
+
const edgeTrigger = container.querySelector('.cursor-pointer.fixed');
|
|
394
|
+
expect(edgeTrigger).not.toBeInTheDocument();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('edge trigger does not show when placement is not bottom', () => {
|
|
398
|
+
const { container } = render(Drawer, {
|
|
399
|
+
props: { edge: true, placement: 'left', open: false }
|
|
400
|
+
});
|
|
401
|
+
const edgeTrigger = container.querySelector('.cursor-pointer.fixed');
|
|
402
|
+
expect(edgeTrigger).not.toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('edge trigger applies custom edgeOffset style', () => {
|
|
406
|
+
const { container } = render(Drawer, {
|
|
407
|
+
props: { edge: true, placement: 'bottom', open: false, edgeOffset: 100 }
|
|
408
|
+
});
|
|
409
|
+
const edgeTrigger = container.querySelector('.cursor-pointer');
|
|
410
|
+
expect(edgeTrigger).toBeInTheDocument();
|
|
411
|
+
expect(edgeTrigger).toHaveStyle('bottom: 100px');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('edge trigger uses default edgeOffset of 60', () => {
|
|
415
|
+
const { container } = render(Drawer, {
|
|
416
|
+
props: { edge: true, placement: 'bottom', open: false }
|
|
417
|
+
});
|
|
418
|
+
const edgeTrigger = container.querySelector('.cursor-pointer');
|
|
419
|
+
expect(edgeTrigger).toBeInTheDocument();
|
|
420
|
+
expect(edgeTrigger).toHaveStyle('bottom: 60px');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('Drawer Backdrop Click Branches', () => {
|
|
425
|
+
test('backdrop click does NOT close when closeOnBackdropClick is false', async () => {
|
|
426
|
+
const onclose = vi.fn();
|
|
427
|
+
const { container } = render(Drawer, {
|
|
428
|
+
props: { open: true, closeOnBackdropClick: false, onclose }
|
|
429
|
+
});
|
|
430
|
+
const backdrop = container.querySelector('[role="presentation"]');
|
|
431
|
+
expect(backdrop).toBeInTheDocument();
|
|
432
|
+
await userEvent.click(backdrop);
|
|
433
|
+
expect(onclose).not.toHaveBeenCalled();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('clicking inside drawer (not backdrop) does not trigger close', async () => {
|
|
437
|
+
const onclose = vi.fn();
|
|
438
|
+
const { container } = render(Drawer, {
|
|
439
|
+
props: { open: true, closeOnBackdropClick: true, onclose, title: 'Test' }
|
|
440
|
+
});
|
|
441
|
+
// Click the drawer dialog itself, not the backdrop
|
|
442
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
443
|
+
await userEvent.click(dialog);
|
|
444
|
+
expect(onclose).not.toHaveBeenCalled();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe('Drawer Focus Management', () => {
|
|
449
|
+
test('drawer receives focus when opened', async () => {
|
|
450
|
+
vi.useFakeTimers();
|
|
451
|
+
const { container } = render(Drawer, { props: { open: true } });
|
|
452
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
453
|
+
// The setTimeout in the effect focuses the drawer
|
|
454
|
+
vi.runAllTimers();
|
|
455
|
+
await waitFor(() => {
|
|
456
|
+
expect(dialog).toBe(document.activeElement);
|
|
457
|
+
});
|
|
458
|
+
vi.useRealTimers();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('focus is restored to previously focused element when drawer closes', async () => {
|
|
462
|
+
vi.useFakeTimers();
|
|
463
|
+
// Create a button to focus before opening the drawer
|
|
464
|
+
const button = document.createElement('button');
|
|
465
|
+
button.textContent = 'Focus Me';
|
|
466
|
+
document.body.appendChild(button);
|
|
467
|
+
button.focus();
|
|
468
|
+
expect(document.activeElement).toBe(button);
|
|
469
|
+
|
|
470
|
+
// Open the drawer - this stores the previously focused element
|
|
471
|
+
const { rerender } = render(Drawer, { props: { open: true } });
|
|
472
|
+
vi.runAllTimers();
|
|
473
|
+
|
|
474
|
+
// Close the drawer - this should restore focus (line 143-145 / line 147)
|
|
475
|
+
await rerender({ open: false });
|
|
476
|
+
vi.runAllTimers();
|
|
477
|
+
|
|
478
|
+
await waitFor(() => {
|
|
479
|
+
expect(document.activeElement).toBe(button);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Clean up
|
|
483
|
+
document.body.removeChild(button);
|
|
484
|
+
vi.useRealTimers();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('closing drawer when previouslyFocusedElement is null does not throw', async () => {
|
|
488
|
+
vi.useFakeTimers();
|
|
489
|
+
// Render open, then close without having a previously focused element
|
|
490
|
+
const { rerender } = render(Drawer, { props: { open: true } });
|
|
491
|
+
vi.runAllTimers();
|
|
492
|
+
|
|
493
|
+
// Close drawer - previouslyFocusedElement should be body or null, no error expected
|
|
494
|
+
await rerender({ open: false });
|
|
495
|
+
vi.runAllTimers();
|
|
496
|
+
vi.useRealTimers();
|
|
497
|
+
// If we get here without throwing, the test passes
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe('Drawer Keyboard Interaction - handleKeydown Branches', () => {
|
|
502
|
+
test('keydown handler does nothing when drawer is not visible', async () => {
|
|
503
|
+
const onclose = vi.fn();
|
|
504
|
+
render(Drawer, {
|
|
505
|
+
props: { open: false, closeOnEscape: true, onclose }
|
|
506
|
+
});
|
|
507
|
+
await userEvent.keyboard('{Escape}');
|
|
508
|
+
// handleKeydown returns early because isVisible is false
|
|
509
|
+
expect(onclose).not.toHaveBeenCalled();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('non-escape key does not close drawer', async () => {
|
|
513
|
+
const onclose = vi.fn();
|
|
514
|
+
render(Drawer, {
|
|
515
|
+
props: { open: true, closeOnEscape: true, onclose }
|
|
516
|
+
});
|
|
517
|
+
await userEvent.keyboard('{Enter}');
|
|
518
|
+
expect(onclose).not.toHaveBeenCalled();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test('Tab key does not close drawer', async () => {
|
|
522
|
+
const onclose = vi.fn();
|
|
523
|
+
render(Drawer, {
|
|
524
|
+
props: { open: true, closeOnEscape: true, onclose }
|
|
525
|
+
});
|
|
526
|
+
await userEvent.keyboard('{Tab}');
|
|
527
|
+
expect(onclose).not.toHaveBeenCalled();
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('Drawer Focus Trap (Tab key)', () => {
|
|
532
|
+
test('forward Tab from last focusable element wraps to first', async () => {
|
|
533
|
+
vi.useFakeTimers();
|
|
534
|
+
const { container } = render(DrawerTestWrapper, {
|
|
535
|
+
props: { open: true, showActions: true, actionsText: 'Save' }
|
|
536
|
+
});
|
|
537
|
+
vi.runAllTimers();
|
|
538
|
+
|
|
539
|
+
// Get focusable elements inside the drawer
|
|
540
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
541
|
+
const buttons = dialog.querySelectorAll('button');
|
|
542
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
543
|
+
|
|
544
|
+
const lastButton = buttons[buttons.length - 1];
|
|
545
|
+
lastButton.focus();
|
|
546
|
+
expect(document.activeElement).toBe(lastButton);
|
|
547
|
+
|
|
548
|
+
// Simulate Tab (forward) when focus is on the last element
|
|
549
|
+
const tabEvent = new KeyboardEvent('keydown', {
|
|
550
|
+
key: 'Tab',
|
|
551
|
+
shiftKey: false,
|
|
552
|
+
bubbles: true,
|
|
553
|
+
cancelable: true
|
|
554
|
+
});
|
|
555
|
+
window.dispatchEvent(tabEvent);
|
|
556
|
+
|
|
557
|
+
vi.useRealTimers();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('Shift+Tab from first focusable element wraps to last', async () => {
|
|
561
|
+
vi.useFakeTimers();
|
|
562
|
+
const { container } = render(DrawerTestWrapper, {
|
|
563
|
+
props: { open: true, showActions: true, actionsText: 'Save' }
|
|
564
|
+
});
|
|
565
|
+
vi.runAllTimers();
|
|
566
|
+
|
|
567
|
+
// Get focusable elements inside the drawer
|
|
568
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
569
|
+
const buttons = dialog.querySelectorAll('button');
|
|
570
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
571
|
+
|
|
572
|
+
const firstButton = buttons[0];
|
|
573
|
+
firstButton.focus();
|
|
574
|
+
expect(document.activeElement).toBe(firstButton);
|
|
575
|
+
|
|
576
|
+
// Simulate Shift+Tab when focus is on the first element
|
|
577
|
+
const shiftTabEvent = new KeyboardEvent('keydown', {
|
|
578
|
+
key: 'Tab',
|
|
579
|
+
shiftKey: true,
|
|
580
|
+
bubbles: true,
|
|
581
|
+
cancelable: true
|
|
582
|
+
});
|
|
583
|
+
window.dispatchEvent(shiftTabEvent);
|
|
584
|
+
|
|
585
|
+
vi.useRealTimers();
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
describe('Drawer aria-labelledby Edge Cases', () => {
|
|
590
|
+
test('no aria-labelledby when title provided but id is empty', () => {
|
|
591
|
+
const { container } = render(Drawer, {
|
|
592
|
+
props: { open: true, title: 'Test Title', id: '' }
|
|
593
|
+
});
|
|
594
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
595
|
+
// aria-labelledby is `${id}-label` which is "-label" when id is empty,
|
|
596
|
+
// but the condition is `title ? \`${id}-label\` : undefined`
|
|
597
|
+
// With title truthy and id empty, it produces "-label"
|
|
598
|
+
expect(dialog).toHaveAttribute('aria-labelledby', '-label');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('h5 id is undefined when id prop is empty', () => {
|
|
602
|
+
const { container } = render(Drawer, {
|
|
603
|
+
props: { open: true, title: 'Test Title', id: '' }
|
|
604
|
+
});
|
|
605
|
+
const h5 = container.querySelector('h5');
|
|
606
|
+
expect(h5).toBeInTheDocument();
|
|
607
|
+
// id={id ? `${id}-label` : undefined} -- with empty id, it's undefined
|
|
608
|
+
expect(h5).not.toHaveAttribute('id');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test('h5 has id when id prop is provided', () => {
|
|
612
|
+
const { container } = render(Drawer, {
|
|
613
|
+
props: { open: true, title: 'Test Title', id: 'my-drawer' }
|
|
614
|
+
});
|
|
615
|
+
const h5 = container.querySelector('h5');
|
|
616
|
+
expect(h5).toBeInTheDocument();
|
|
617
|
+
expect(h5).toHaveAttribute('id', 'my-drawer-label');
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
describe('Drawer Without onclose/onopen Callbacks', () => {
|
|
622
|
+
test('backdrop click does not throw when onclose is not provided', async () => {
|
|
623
|
+
const { container } = render(Drawer, {
|
|
624
|
+
props: { open: true, closeOnBackdropClick: true }
|
|
625
|
+
});
|
|
626
|
+
const backdrop = container.querySelector('[role="presentation"]');
|
|
627
|
+
expect(backdrop).toBeInTheDocument();
|
|
628
|
+
// Should not throw even though onclose is not provided (tests onclose?.() optional chaining)
|
|
629
|
+
await userEvent.click(backdrop);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test('escape key does not throw when onclose is not provided', async () => {
|
|
633
|
+
render(Drawer, {
|
|
634
|
+
props: { open: true, closeOnEscape: true }
|
|
635
|
+
});
|
|
636
|
+
// Should not throw even though onclose is not provided (tests onclose?.() optional chaining)
|
|
637
|
+
await userEvent.keyboard('{Escape}');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('edge click does not throw when onopen is not provided', async () => {
|
|
641
|
+
const { container } = render(Drawer, {
|
|
642
|
+
props: { edge: true, placement: 'bottom', open: false }
|
|
643
|
+
});
|
|
644
|
+
const edgeTrigger = container.querySelector('.cursor-pointer');
|
|
645
|
+
expect(edgeTrigger).toBeInTheDocument();
|
|
646
|
+
// Should not throw even though onopen is not provided (tests onopen?.() optional chaining)
|
|
647
|
+
await userEvent.click(edgeTrigger);
|
|
648
|
+
});
|
|
212
649
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Drawer from "./Drawer.svelte";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
title?: string;
|
|
7
|
+
placement?: string;
|
|
8
|
+
width?: string;
|
|
9
|
+
backdrop?: boolean;
|
|
10
|
+
closeOnBackdropClick?: boolean;
|
|
11
|
+
closeOnEscape?: boolean;
|
|
12
|
+
id?: string;
|
|
13
|
+
edge?: boolean;
|
|
14
|
+
edgeOffset?: number;
|
|
15
|
+
onclose?: () => void;
|
|
16
|
+
onopen?: () => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
showHeader?: boolean;
|
|
19
|
+
headerText?: string;
|
|
20
|
+
showChildren?: boolean;
|
|
21
|
+
childrenText?: string;
|
|
22
|
+
showActions?: boolean;
|
|
23
|
+
actionsText?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
open = $bindable(false),
|
|
29
|
+
title = "",
|
|
30
|
+
placement = "left",
|
|
31
|
+
width = "md",
|
|
32
|
+
backdrop = true,
|
|
33
|
+
closeOnBackdropClick = true,
|
|
34
|
+
closeOnEscape = true,
|
|
35
|
+
id = "",
|
|
36
|
+
edge = false,
|
|
37
|
+
edgeOffset = 60,
|
|
38
|
+
onclose,
|
|
39
|
+
onopen,
|
|
40
|
+
class: className = "",
|
|
41
|
+
showHeader = false,
|
|
42
|
+
headerText = "Custom Header",
|
|
43
|
+
showChildren = false,
|
|
44
|
+
childrenText = "Drawer Content",
|
|
45
|
+
showActions = false,
|
|
46
|
+
actionsText = "Action Button",
|
|
47
|
+
...restProps
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<Drawer
|
|
52
|
+
bind:open
|
|
53
|
+
{title}
|
|
54
|
+
{placement}
|
|
55
|
+
{width}
|
|
56
|
+
{backdrop}
|
|
57
|
+
{closeOnBackdropClick}
|
|
58
|
+
{closeOnEscape}
|
|
59
|
+
{id}
|
|
60
|
+
{edge}
|
|
61
|
+
{edgeOffset}
|
|
62
|
+
{onclose}
|
|
63
|
+
{onopen}
|
|
64
|
+
class={className}
|
|
65
|
+
{...restProps}
|
|
66
|
+
>
|
|
67
|
+
{#snippet header()}
|
|
68
|
+
{#if showHeader}
|
|
69
|
+
<div data-testid="custom-header">{headerText}</div>
|
|
70
|
+
{/if}
|
|
71
|
+
{/snippet}
|
|
72
|
+
|
|
73
|
+
{#snippet children()}
|
|
74
|
+
{#if showChildren}
|
|
75
|
+
<div data-testid="drawer-children">{childrenText}</div>
|
|
76
|
+
{/if}
|
|
77
|
+
{/snippet}
|
|
78
|
+
|
|
79
|
+
{#snippet actions()}
|
|
80
|
+
{#if showActions}
|
|
81
|
+
<div data-testid="drawer-actions">
|
|
82
|
+
<button type="button">{actionsText}</button>
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
{/snippet}
|
|
86
|
+
</Drawer>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
title?: string;
|
|
4
|
+
placement?: string;
|
|
5
|
+
width?: string;
|
|
6
|
+
backdrop?: boolean;
|
|
7
|
+
closeOnBackdropClick?: boolean;
|
|
8
|
+
closeOnEscape?: boolean;
|
|
9
|
+
id?: string;
|
|
10
|
+
edge?: boolean;
|
|
11
|
+
edgeOffset?: number;
|
|
12
|
+
onclose?: () => void;
|
|
13
|
+
onopen?: () => void;
|
|
14
|
+
class?: string;
|
|
15
|
+
showHeader?: boolean;
|
|
16
|
+
headerText?: string;
|
|
17
|
+
showChildren?: boolean;
|
|
18
|
+
childrenText?: string;
|
|
19
|
+
showActions?: boolean;
|
|
20
|
+
actionsText?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare const DrawerTestWrapper: import("svelte").Component<Props, {}, "open">;
|
|
24
|
+
type DrawerTestWrapper = ReturnType<typeof DrawerTestWrapper>;
|
|
25
|
+
export default DrawerTestWrapper;
|
|
26
|
+
//# sourceMappingURL=DrawerTestWrapper.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DrawerTestWrapper.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Drawer/DrawerTestWrapper.svelte.ts"],"names":[],"mappings":"AAME,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAqDH,QAAA,MAAM,iBAAiB,+CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/svelte';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { createRawSnippet } from 'svelte';
|
|
3
4
|
import { expect, describe, test, vi, beforeEach, afterEach } from 'vitest';
|
|
4
5
|
import Dropdown from './Dropdown.svelte';
|
|
5
6
|
|
|
@@ -351,6 +352,121 @@ describe('Dropdown Context', () => {
|
|
|
351
352
|
});
|
|
352
353
|
});
|
|
353
354
|
|
|
355
|
+
describe('Dropdown Placement', () => {
|
|
356
|
+
test('renders with top placement', () => {
|
|
357
|
+
const { container } = render(Dropdown, { props: { open: true, placement: 'top' } });
|
|
358
|
+
expect(container.querySelector('[role="menu"]')).toBeInTheDocument();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('renders with left placement', () => {
|
|
362
|
+
const { container } = render(Dropdown, { props: { open: true, placement: 'left' } });
|
|
363
|
+
expect(container.querySelector('[role="menu"]')).toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('renders with right placement', () => {
|
|
367
|
+
const { container } = render(Dropdown, { props: { open: true, placement: 'right' } });
|
|
368
|
+
expect(container.querySelector('[role="menu"]')).toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('falls back to default origin for unknown placement', () => {
|
|
372
|
+
const { container } = render(Dropdown, { props: { open: true, placement: 'invalid-placement' } });
|
|
373
|
+
expect(container.querySelector('[role="menu"]')).toBeInTheDocument();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('Dropdown Children Rendering', () => {
|
|
378
|
+
test('renders children snippet when provided', () => {
|
|
379
|
+
const { container } = render(Dropdown, {
|
|
380
|
+
props: {
|
|
381
|
+
open: true,
|
|
382
|
+
children: createRawSnippet(() => ({
|
|
383
|
+
render: () => `<li role="menuitem">Item 1</li>`
|
|
384
|
+
}))
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
const item = container.querySelector('[role="menuitem"]');
|
|
388
|
+
expect(item).toBeInTheDocument();
|
|
389
|
+
expect(item.textContent).toBe('Item 1');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('renders empty list when no children provided', () => {
|
|
393
|
+
const { container } = render(Dropdown, { props: { open: true } });
|
|
394
|
+
const ul = container.querySelector('ul');
|
|
395
|
+
expect(ul).toBeInTheDocument();
|
|
396
|
+
expect(ul.children.length).toBe(0);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('Dropdown Trigger Element Detection', () => {
|
|
401
|
+
let cleanup;
|
|
402
|
+
|
|
403
|
+
afterEach(() => {
|
|
404
|
+
if (cleanup) {
|
|
405
|
+
cleanup();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('clicking on trigger sibling does not close dropdown', async () => {
|
|
410
|
+
const onclose = vi.fn();
|
|
411
|
+
|
|
412
|
+
// Create a wrapper with a trigger button followed by the dropdown
|
|
413
|
+
const wrapper = document.createElement('div');
|
|
414
|
+
const triggerBtn = document.createElement('button');
|
|
415
|
+
triggerBtn.textContent = 'Toggle';
|
|
416
|
+
wrapper.appendChild(triggerBtn);
|
|
417
|
+
document.body.appendChild(wrapper);
|
|
418
|
+
|
|
419
|
+
const result = render(Dropdown, {
|
|
420
|
+
props: { open: true, onclose },
|
|
421
|
+
target: wrapper
|
|
422
|
+
});
|
|
423
|
+
cleanup = () => {
|
|
424
|
+
result.unmount();
|
|
425
|
+
document.body.removeChild(wrapper);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Click on the trigger button (sibling before the dropdown container)
|
|
429
|
+
const clickEvent = new MouseEvent('mousedown', { bubbles: true });
|
|
430
|
+
Object.defineProperty(clickEvent, 'target', { value: triggerBtn, enumerable: true });
|
|
431
|
+
document.dispatchEvent(clickEvent);
|
|
432
|
+
|
|
433
|
+
// Should not close since click was on the trigger element
|
|
434
|
+
expect(onclose).not.toHaveBeenCalled();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('Escape returns focus to trigger element', async () => {
|
|
438
|
+
const onclose = vi.fn();
|
|
439
|
+
|
|
440
|
+
// Create wrapper with trigger sibling before dropdown
|
|
441
|
+
const wrapper = document.createElement('div');
|
|
442
|
+
const triggerBtn = document.createElement('button');
|
|
443
|
+
triggerBtn.textContent = 'Toggle';
|
|
444
|
+
wrapper.appendChild(triggerBtn);
|
|
445
|
+
document.body.appendChild(wrapper);
|
|
446
|
+
|
|
447
|
+
const result = render(Dropdown, {
|
|
448
|
+
props: { open: true, onclose },
|
|
449
|
+
target: wrapper
|
|
450
|
+
});
|
|
451
|
+
cleanup = () => {
|
|
452
|
+
result.unmount();
|
|
453
|
+
document.body.removeChild(wrapper);
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const focusSpy = vi.spyOn(triggerBtn, 'focus');
|
|
457
|
+
const ul = result.container.querySelector('ul');
|
|
458
|
+
|
|
459
|
+
// Dispatch Escape from within the dropdown
|
|
460
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
|
|
461
|
+
Object.defineProperty(event, 'target', { value: ul, enumerable: true });
|
|
462
|
+
document.dispatchEvent(event);
|
|
463
|
+
|
|
464
|
+
expect(onclose).toHaveBeenCalled();
|
|
465
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
466
|
+
focusSpy.mockRestore();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
354
470
|
describe('Dropdown Lifecycle', () => {
|
|
355
471
|
test('cleans up event listeners on destroy', () => {
|
|
356
472
|
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|