@classic-homes/theme-svelte 1.22.0 → 1.22.1
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/lib/components/Alert.svelte +8 -10
- package/dist/lib/components/DataPanel.svelte +19 -5
- package/dist/lib/components/HeaderSearch.svelte +18 -10
- package/dist/lib/components/Skeleton.svelte.d.ts +1 -1
- package/dist/lib/components/layout/AppShell.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +15 -6
- package/dist/lib/components/layout/Footer.svelte +6 -6
- package/dist/lib/components/layout/FormPageLayout.svelte +4 -3
- package/dist/lib/components/layout/Sidebar.svelte +25 -21
- package/dist/lib/components/media/Image.svelte.d.ts +1 -1
- package/dist/lib/components/media/Lightbox.svelte +13 -9
- package/dist/lib/components/media/MediaCarousel.svelte +5 -0
- package/dist/lib/components/media/MediaGrid.svelte +4 -1
- package/dist/lib/components/notifications/NotificationDialog.svelte +8 -5
- package/dist/lib/components/notifications/NotificationItem.svelte +16 -6
- package/dist/lib/components/studio/InsertPalette.svelte +4 -1
- package/dist/lib/components/studio/StudioCanvas.svelte +23 -2
- package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +5 -1
- package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
- package/dist/lib/notices/sanitize.js +10 -3
- package/dist/lib/notices/store.d.ts +3 -3
- package/dist/lib/notices/store.js +14 -2
- package/dist/lib/notices/types.d.ts +8 -8
- package/dist/lib/notices/types.js +5 -1
- package/dist/lib/stores/sidebar.svelte.d.ts +6 -0
- package/dist/lib/stores/sidebar.svelte.js +61 -26
- package/dist/lib/utils/media.js +4 -3
- package/dist/lib/utils/url.d.ts +10 -0
- package/dist/lib/utils/url.js +21 -0
- package/dist/lib/utils.js +14 -1
- package/package.json +5 -4
- package/dist/__tests__/FileUpload.test.js +0 -45
- package/dist/__tests__/a11y/Alert.a11y.test.js +0 -84
- package/dist/__tests__/a11y/AlertDialog.a11y.test.js +0 -126
- package/dist/__tests__/a11y/Button.a11y.test.js +0 -66
- package/dist/__tests__/a11y/Card.a11y.test.js +0 -77
- package/dist/__tests__/a11y/Combobox.a11y.test.js +0 -209
- package/dist/__tests__/a11y/Dialog.a11y.test.js +0 -103
- package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +0 -132
- package/dist/__tests__/a11y/FileUpload.a11y.test.js +0 -158
- package/dist/__tests__/a11y/FilterToolbar.a11y.test.js +0 -62
- package/dist/__tests__/a11y/Icon.a11y.test.js +0 -119
- package/dist/__tests__/a11y/Input.a11y.test.js +0 -96
- package/dist/__tests__/a11y/LiveRegion.a11y.test.js +0 -102
- package/dist/__tests__/a11y/Media.a11y.test.js +0 -145
- package/dist/__tests__/a11y/PublicLayout.a11y.test.js +0 -139
- package/dist/__tests__/a11y/SegmentedControl.a11y.test.js +0 -50
- package/dist/__tests__/a11y/Switch.a11y.test.js +0 -95
- package/dist/__tests__/a11y/Tabs.a11y.test.js +0 -116
- package/dist/__tests__/a11y/Textarea.a11y.test.js +0 -111
- package/dist/__tests__/a11y/Toolbar.a11y.test.js +0 -31
- package/dist/__tests__/a11y/ViewToggle.a11y.test.js +0 -40
- package/dist/__tests__/action-header.test.js +0 -150
- package/dist/__tests__/alert-dialog-dismiss.test.js +0 -54
- package/dist/__tests__/code-field.test.js +0 -65
- package/dist/__tests__/control-text-color.test.js +0 -29
- package/dist/__tests__/data-panel-card-gestures.test.js +0 -138
- package/dist/__tests__/data-panel-viewport.test.js +0 -45
- package/dist/__tests__/dropdown-trigger.test.js +0 -39
- package/dist/__tests__/header-search.test.js +0 -90
- package/dist/__tests__/media-components.test.js +0 -206
- package/dist/__tests__/media-hls.test.js +0 -273
- package/dist/__tests__/media-matterport.test.js +0 -296
- package/dist/__tests__/media-players.test.js +0 -244
- package/dist/__tests__/media-scrubber.test.js +0 -44
- package/dist/__tests__/media-utils.test.js +0 -427
- package/dist/__tests__/nav-access.test.js +0 -145
- package/dist/__tests__/page-header.test.js +0 -36
- package/dist/__tests__/public-layout-drawer.test.js +0 -116
- package/dist/__tests__/setup.js +0 -56
- package/dist/__tests__/sidebar-collapse.test.js +0 -58
- package/dist/__tests__/sidebar-docs-parity.test.js +0 -134
- package/dist/__tests__/sidebar-expansion.test.js +0 -59
- package/dist/__tests__/sidebar-items.test.js +0 -59
- package/dist/__tests__/sidebar-mobile.test.js +0 -86
- package/dist/__tests__/studio-canvas-item.svelte.test.js +0 -98
- package/dist/__tests__/studio.test.js +0 -240
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { screen } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { axe } from 'vitest-axe';
|
|
5
|
-
describe('AlertDialog accessibility', () => {
|
|
6
|
-
it('should have no axe violations when open', async () => {
|
|
7
|
-
const container = document.createElement('div');
|
|
8
|
-
document.body.appendChild(container);
|
|
9
|
-
container.innerHTML = `
|
|
10
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title" aria-describedby="alertdialog-desc" class="fixed z-50 bg-background p-6 shadow-lg">
|
|
11
|
-
<h2 id="alertdialog-title" class="text-lg font-semibold">Confirm Action</h2>
|
|
12
|
-
<p id="alertdialog-desc" class="text-sm text-muted-foreground">Are you sure you want to proceed?</p>
|
|
13
|
-
<div>
|
|
14
|
-
<button type="button">Cancel</button>
|
|
15
|
-
<button type="button">Continue</button>
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
`;
|
|
19
|
-
const results = await axe(container);
|
|
20
|
-
expect(results).toHaveNoViolations();
|
|
21
|
-
document.body.removeChild(container);
|
|
22
|
-
});
|
|
23
|
-
it('should have role="alertdialog"', () => {
|
|
24
|
-
const container = document.createElement('div');
|
|
25
|
-
document.body.appendChild(container);
|
|
26
|
-
container.innerHTML = `
|
|
27
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title">
|
|
28
|
-
<h2 id="alertdialog-title">Confirm Action</h2>
|
|
29
|
-
<button type="button">Cancel</button>
|
|
30
|
-
<button type="button">Continue</button>
|
|
31
|
-
</div>
|
|
32
|
-
`;
|
|
33
|
-
const dialog = screen.getByRole('alertdialog');
|
|
34
|
-
expect(dialog).toBeInTheDocument();
|
|
35
|
-
document.body.removeChild(container);
|
|
36
|
-
});
|
|
37
|
-
it('should have aria-modal="true"', () => {
|
|
38
|
-
const container = document.createElement('div');
|
|
39
|
-
document.body.appendChild(container);
|
|
40
|
-
container.innerHTML = `
|
|
41
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title">
|
|
42
|
-
<h2 id="alertdialog-title">Confirm Action</h2>
|
|
43
|
-
<button type="button">Cancel</button>
|
|
44
|
-
<button type="button">Continue</button>
|
|
45
|
-
</div>
|
|
46
|
-
`;
|
|
47
|
-
const dialog = screen.getByRole('alertdialog');
|
|
48
|
-
expect(dialog).toHaveAttribute('aria-modal', 'true');
|
|
49
|
-
document.body.removeChild(container);
|
|
50
|
-
});
|
|
51
|
-
it('should have accessible title', () => {
|
|
52
|
-
const container = document.createElement('div');
|
|
53
|
-
document.body.appendChild(container);
|
|
54
|
-
container.innerHTML = `
|
|
55
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title">
|
|
56
|
-
<h2 id="alertdialog-title">Delete Item</h2>
|
|
57
|
-
<button type="button">Cancel</button>
|
|
58
|
-
<button type="button">Delete</button>
|
|
59
|
-
</div>
|
|
60
|
-
`;
|
|
61
|
-
expect(screen.getByText('Delete Item')).toBeInTheDocument();
|
|
62
|
-
document.body.removeChild(container);
|
|
63
|
-
});
|
|
64
|
-
it('should have accessible description', () => {
|
|
65
|
-
const container = document.createElement('div');
|
|
66
|
-
document.body.appendChild(container);
|
|
67
|
-
container.innerHTML = `
|
|
68
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title" aria-describedby="alertdialog-desc">
|
|
69
|
-
<h2 id="alertdialog-title">Delete Item</h2>
|
|
70
|
-
<p id="alertdialog-desc">This action cannot be undone.</p>
|
|
71
|
-
<button type="button">Cancel</button>
|
|
72
|
-
<button type="button">Delete</button>
|
|
73
|
-
</div>
|
|
74
|
-
`;
|
|
75
|
-
expect(screen.getByText('This action cannot be undone.')).toBeInTheDocument();
|
|
76
|
-
document.body.removeChild(container);
|
|
77
|
-
});
|
|
78
|
-
it('should have cancel and confirm buttons', () => {
|
|
79
|
-
const container = document.createElement('div');
|
|
80
|
-
document.body.appendChild(container);
|
|
81
|
-
container.innerHTML = `
|
|
82
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title">
|
|
83
|
-
<h2 id="alertdialog-title">Confirm Action</h2>
|
|
84
|
-
<button type="button">Cancel</button>
|
|
85
|
-
<button type="button">Confirm</button>
|
|
86
|
-
</div>
|
|
87
|
-
`;
|
|
88
|
-
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
|
89
|
-
expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument();
|
|
90
|
-
document.body.removeChild(container);
|
|
91
|
-
});
|
|
92
|
-
it('should be keyboard accessible', async () => {
|
|
93
|
-
const user = userEvent.setup();
|
|
94
|
-
const container = document.createElement('div');
|
|
95
|
-
document.body.appendChild(container);
|
|
96
|
-
container.innerHTML = `
|
|
97
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title">
|
|
98
|
-
<h2 id="alertdialog-title">Confirm Action</h2>
|
|
99
|
-
<button type="button" id="cancel-btn">Cancel</button>
|
|
100
|
-
<button type="button" id="confirm-btn">Confirm</button>
|
|
101
|
-
</div>
|
|
102
|
-
`;
|
|
103
|
-
const cancelBtn = container.querySelector('#cancel-btn');
|
|
104
|
-
const confirmBtn = container.querySelector('#confirm-btn');
|
|
105
|
-
cancelBtn.focus();
|
|
106
|
-
expect(cancelBtn).toHaveFocus();
|
|
107
|
-
await user.tab();
|
|
108
|
-
expect(confirmBtn).toHaveFocus();
|
|
109
|
-
document.body.removeChild(container);
|
|
110
|
-
});
|
|
111
|
-
it('should have labeled buttons that describe the action', () => {
|
|
112
|
-
const container = document.createElement('div');
|
|
113
|
-
document.body.appendChild(container);
|
|
114
|
-
container.innerHTML = `
|
|
115
|
-
<div role="alertdialog" aria-modal="true" aria-labelledby="alertdialog-title" aria-describedby="alertdialog-desc">
|
|
116
|
-
<h2 id="alertdialog-title">Delete Account</h2>
|
|
117
|
-
<p id="alertdialog-desc">This will permanently delete your account and all associated data.</p>
|
|
118
|
-
<button type="button">Cancel</button>
|
|
119
|
-
<button type="button">Delete Account</button>
|
|
120
|
-
</div>
|
|
121
|
-
`;
|
|
122
|
-
const deleteBtn = screen.getByRole('button', { name: 'Delete Account' });
|
|
123
|
-
expect(deleteBtn).toBeInTheDocument();
|
|
124
|
-
document.body.removeChild(container);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { axe } from 'vitest-axe';
|
|
5
|
-
import { createRawSnippet } from 'svelte';
|
|
6
|
-
import Button from '../../lib/components/Button.svelte';
|
|
7
|
-
function textSnippet(text) {
|
|
8
|
-
return createRawSnippet(() => ({
|
|
9
|
-
render: () => `<span>${text}</span>`,
|
|
10
|
-
}));
|
|
11
|
-
}
|
|
12
|
-
describe('Button accessibility', () => {
|
|
13
|
-
it('should have no axe violations', async () => {
|
|
14
|
-
const { container } = render(Button, {
|
|
15
|
-
props: { children: textSnippet('Click me') },
|
|
16
|
-
});
|
|
17
|
-
const results = await axe(container);
|
|
18
|
-
expect(results).toHaveNoViolations();
|
|
19
|
-
});
|
|
20
|
-
it('should have no axe violations with different variants', async () => {
|
|
21
|
-
const { container } = render(Button, {
|
|
22
|
-
props: { variant: 'default', children: textSnippet('Default') },
|
|
23
|
-
});
|
|
24
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
25
|
-
const { container: container2 } = render(Button, {
|
|
26
|
-
props: { variant: 'secondary', children: textSnippet('Secondary') },
|
|
27
|
-
});
|
|
28
|
-
expect(await axe(container2)).toHaveNoViolations();
|
|
29
|
-
const { container: container3 } = render(Button, {
|
|
30
|
-
props: { variant: 'destructive', children: textSnippet('Destructive') },
|
|
31
|
-
});
|
|
32
|
-
expect(await axe(container3)).toHaveNoViolations();
|
|
33
|
-
});
|
|
34
|
-
it('should be focusable', async () => {
|
|
35
|
-
render(Button, {
|
|
36
|
-
props: { children: textSnippet('Click me') },
|
|
37
|
-
});
|
|
38
|
-
const button = screen.getByRole('button');
|
|
39
|
-
await userEvent.tab();
|
|
40
|
-
expect(button).toHaveFocus();
|
|
41
|
-
});
|
|
42
|
-
it('should be activatable with keyboard', async () => {
|
|
43
|
-
let clicked = false;
|
|
44
|
-
render(Button, {
|
|
45
|
-
props: {
|
|
46
|
-
children: textSnippet('Click me'),
|
|
47
|
-
onclick: () => {
|
|
48
|
-
clicked = true;
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
await userEvent.tab();
|
|
53
|
-
await userEvent.keyboard('{Enter}');
|
|
54
|
-
expect(clicked).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
it('should not be focusable when disabled', async () => {
|
|
57
|
-
render(Button, {
|
|
58
|
-
props: {
|
|
59
|
-
disabled: true,
|
|
60
|
-
children: textSnippet('Disabled'),
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
const button = screen.getByRole('button');
|
|
64
|
-
expect(button).toBeDisabled();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { screen } from '@testing-library/svelte';
|
|
3
|
-
import { axe } from 'vitest-axe';
|
|
4
|
-
describe('Card accessibility', () => {
|
|
5
|
-
it('should have no axe violations with basic card structure', async () => {
|
|
6
|
-
// Create a card structure manually for testing
|
|
7
|
-
const container = document.createElement('div');
|
|
8
|
-
document.body.appendChild(container);
|
|
9
|
-
container.innerHTML = `
|
|
10
|
-
<div class="rounded-md bg-card text-card-foreground border border-border">
|
|
11
|
-
<div class="flex flex-col space-y-1.5 p-6">
|
|
12
|
-
<h3 class="text-2xl font-semibold leading-none tracking-tight">Card Title</h3>
|
|
13
|
-
<p class="text-sm text-muted-foreground">Card description text</p>
|
|
14
|
-
</div>
|
|
15
|
-
<div class="p-6 pt-0">Card content goes here</div>
|
|
16
|
-
<div class="flex items-center p-6 pt-0">Footer content</div>
|
|
17
|
-
</div>
|
|
18
|
-
`;
|
|
19
|
-
const results = await axe(container);
|
|
20
|
-
expect(results).toHaveNoViolations();
|
|
21
|
-
document.body.removeChild(container);
|
|
22
|
-
});
|
|
23
|
-
it('should have no axe violations with different heading levels', async () => {
|
|
24
|
-
const container = document.createElement('main');
|
|
25
|
-
document.body.appendChild(container);
|
|
26
|
-
container.innerHTML = `
|
|
27
|
-
<h1>Page Title</h1>
|
|
28
|
-
<section>
|
|
29
|
-
<h2>Section Title</h2>
|
|
30
|
-
<div class="rounded-md bg-card text-card-foreground border border-border">
|
|
31
|
-
<h3 class="text-2xl font-semibold leading-none tracking-tight">Card Title</h3>
|
|
32
|
-
<p>Card content</p>
|
|
33
|
-
</div>
|
|
34
|
-
</section>
|
|
35
|
-
`;
|
|
36
|
-
const results = await axe(container);
|
|
37
|
-
expect(results).toHaveNoViolations();
|
|
38
|
-
document.body.removeChild(container);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
describe('CardTitle heading levels', () => {
|
|
42
|
-
it('should support configurable heading level via as prop', async () => {
|
|
43
|
-
// Import the actual component to verify the API exists
|
|
44
|
-
await import('../../lib/components/CardTitle.svelte');
|
|
45
|
-
// Verify the component exists and can be rendered
|
|
46
|
-
// We test structure with manual HTML since snippets don't work well in tests
|
|
47
|
-
const container = document.createElement('div');
|
|
48
|
-
document.body.appendChild(container);
|
|
49
|
-
// Test h2 level structure
|
|
50
|
-
container.innerHTML = `<h2 class="text-2xl font-semibold leading-none tracking-tight">H2 Title</h2>`;
|
|
51
|
-
let heading = screen.getByRole('heading', { level: 2 });
|
|
52
|
-
expect(heading).toBeInTheDocument();
|
|
53
|
-
expect(heading).toHaveTextContent('H2 Title');
|
|
54
|
-
// Clean up and test h3 level
|
|
55
|
-
container.innerHTML = `<h3 class="text-2xl font-semibold leading-none tracking-tight">H3 Title</h3>`;
|
|
56
|
-
heading = screen.getByRole('heading', { level: 3 });
|
|
57
|
-
expect(heading).toBeInTheDocument();
|
|
58
|
-
expect(heading).toHaveTextContent('H3 Title');
|
|
59
|
-
// Test h4 level
|
|
60
|
-
container.innerHTML = `<h4 class="text-2xl font-semibold leading-none tracking-tight">H4 Title</h4>`;
|
|
61
|
-
heading = screen.getByRole('heading', { level: 4 });
|
|
62
|
-
expect(heading).toBeInTheDocument();
|
|
63
|
-
expect(heading).toHaveTextContent('H4 Title');
|
|
64
|
-
document.body.removeChild(container);
|
|
65
|
-
});
|
|
66
|
-
it('should have no axe violations at any heading level', async () => {
|
|
67
|
-
const container = document.createElement('div');
|
|
68
|
-
document.body.appendChild(container);
|
|
69
|
-
// Test each heading level
|
|
70
|
-
for (const level of [1, 2, 3, 4, 5, 6]) {
|
|
71
|
-
container.innerHTML = `<h${level} class="text-2xl font-semibold leading-none tracking-tight">Heading Level ${level}</h${level}>`;
|
|
72
|
-
const results = await axe(container);
|
|
73
|
-
expect(results).toHaveNoViolations();
|
|
74
|
-
}
|
|
75
|
-
document.body.removeChild(container);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { axe } from 'vitest-axe';
|
|
5
|
-
import Combobox from '../../lib/components/Combobox.svelte';
|
|
6
|
-
describe('Combobox accessibility', () => {
|
|
7
|
-
const options = [
|
|
8
|
-
{ value: 'apple', label: 'Apple' },
|
|
9
|
-
{ value: 'banana', label: 'Banana' },
|
|
10
|
-
{ value: 'cherry', label: 'Cherry' },
|
|
11
|
-
{ value: 'date', label: 'Date' },
|
|
12
|
-
];
|
|
13
|
-
it('should have no axe violations when closed', async () => {
|
|
14
|
-
const { container } = render(Combobox, {
|
|
15
|
-
props: {
|
|
16
|
-
options,
|
|
17
|
-
label: 'Select a fruit',
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
const results = await axe(container);
|
|
21
|
-
expect(results).toHaveNoViolations();
|
|
22
|
-
});
|
|
23
|
-
it('should open dropdown when clicked', async () => {
|
|
24
|
-
render(Combobox, {
|
|
25
|
-
props: {
|
|
26
|
-
options,
|
|
27
|
-
label: 'Select a fruit',
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
// Open the combobox
|
|
31
|
-
const input = screen.getByRole('combobox');
|
|
32
|
-
await userEvent.click(input);
|
|
33
|
-
// Wait for dropdown to appear
|
|
34
|
-
await waitFor(() => {
|
|
35
|
-
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
36
|
-
});
|
|
37
|
-
// Should have options in listbox
|
|
38
|
-
const listOptions = screen.getAllByRole('option');
|
|
39
|
-
expect(listOptions.length).toBe(options.length);
|
|
40
|
-
});
|
|
41
|
-
it('should have proper combobox role', () => {
|
|
42
|
-
render(Combobox, {
|
|
43
|
-
props: {
|
|
44
|
-
options,
|
|
45
|
-
label: 'Select a fruit',
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
const combobox = screen.getByRole('combobox');
|
|
49
|
-
expect(combobox).toBeInTheDocument();
|
|
50
|
-
});
|
|
51
|
-
it('should be keyboard navigable', async () => {
|
|
52
|
-
render(Combobox, {
|
|
53
|
-
props: {
|
|
54
|
-
options,
|
|
55
|
-
label: 'Select a fruit',
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
const combobox = screen.getByRole('combobox');
|
|
59
|
-
await userEvent.tab();
|
|
60
|
-
expect(combobox).toHaveFocus();
|
|
61
|
-
});
|
|
62
|
-
it('should open on focus', async () => {
|
|
63
|
-
render(Combobox, {
|
|
64
|
-
props: {
|
|
65
|
-
options,
|
|
66
|
-
label: 'Select a fruit',
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
const combobox = screen.getByRole('combobox');
|
|
70
|
-
await userEvent.click(combobox);
|
|
71
|
-
await waitFor(() => {
|
|
72
|
-
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
it('should have aria-invalid when error is present', () => {
|
|
76
|
-
render(Combobox, {
|
|
77
|
-
props: {
|
|
78
|
-
options,
|
|
79
|
-
label: 'Select a fruit',
|
|
80
|
-
error: 'This field is required',
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
const combobox = screen.getByRole('combobox');
|
|
84
|
-
expect(combobox).toHaveAttribute('aria-invalid', 'true');
|
|
85
|
-
});
|
|
86
|
-
it('should have aria-describedby pointing to error', () => {
|
|
87
|
-
render(Combobox, {
|
|
88
|
-
props: {
|
|
89
|
-
options,
|
|
90
|
-
label: 'Select a fruit',
|
|
91
|
-
id: 'fruit-select',
|
|
92
|
-
error: 'Please select a fruit',
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
const combobox = screen.getByRole('combobox');
|
|
96
|
-
expect(combobox).toHaveAttribute('aria-describedby', expect.stringContaining('fruit-select-error'));
|
|
97
|
-
const errorMessage = document.getElementById('fruit-select-error');
|
|
98
|
-
expect(errorMessage).toHaveTextContent('Please select a fruit');
|
|
99
|
-
});
|
|
100
|
-
it('error should have role="alert"', () => {
|
|
101
|
-
render(Combobox, {
|
|
102
|
-
props: {
|
|
103
|
-
options,
|
|
104
|
-
label: 'Select a fruit',
|
|
105
|
-
error: 'Selection required',
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
const alert = screen.getByRole('alert');
|
|
109
|
-
expect(alert).toHaveTextContent('Selection required');
|
|
110
|
-
});
|
|
111
|
-
it('should have LiveRegion for screen reader announcements', async () => {
|
|
112
|
-
const { container } = render(Combobox, {
|
|
113
|
-
props: {
|
|
114
|
-
options,
|
|
115
|
-
label: 'Select a fruit',
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
// The LiveRegion component should be rendered
|
|
119
|
-
const liveRegion = container.querySelector('[aria-live]');
|
|
120
|
-
expect(liveRegion).toBeInTheDocument();
|
|
121
|
-
});
|
|
122
|
-
it('should announce number of results when filtering', async () => {
|
|
123
|
-
const { container } = render(Combobox, {
|
|
124
|
-
props: {
|
|
125
|
-
options,
|
|
126
|
-
label: 'Select a fruit',
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
const combobox = screen.getByRole('combobox');
|
|
130
|
-
await userEvent.click(combobox);
|
|
131
|
-
// Wait for dropdown
|
|
132
|
-
await waitFor(() => {
|
|
133
|
-
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
134
|
-
});
|
|
135
|
-
// Type to filter
|
|
136
|
-
await userEvent.type(combobox, 'a');
|
|
137
|
-
// Check for live region announcement
|
|
138
|
-
const liveRegion = container.querySelector('[aria-live="polite"]');
|
|
139
|
-
expect(liveRegion).toBeInTheDocument();
|
|
140
|
-
});
|
|
141
|
-
it('should announce loading state', async () => {
|
|
142
|
-
const { container } = render(Combobox, {
|
|
143
|
-
props: {
|
|
144
|
-
options: [],
|
|
145
|
-
label: 'Select a fruit',
|
|
146
|
-
loading: true,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
const combobox = screen.getByRole('combobox');
|
|
150
|
-
await userEvent.click(combobox);
|
|
151
|
-
// The live region should announce loading
|
|
152
|
-
await waitFor(() => {
|
|
153
|
-
const liveRegion = container.querySelector('[aria-live="polite"]');
|
|
154
|
-
expect(liveRegion?.textContent).toContain('Loading');
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
it('should announce empty state', async () => {
|
|
158
|
-
const { container } = render(Combobox, {
|
|
159
|
-
props: {
|
|
160
|
-
options: [],
|
|
161
|
-
label: 'Select a fruit',
|
|
162
|
-
emptyMessage: 'No fruits found',
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
const combobox = screen.getByRole('combobox');
|
|
166
|
-
await userEvent.click(combobox);
|
|
167
|
-
await waitFor(() => {
|
|
168
|
-
const liveRegion = container.querySelector('[aria-live="polite"]');
|
|
169
|
-
expect(liveRegion?.textContent).toContain('No fruits found');
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
it('should have proper label association', () => {
|
|
173
|
-
render(Combobox, {
|
|
174
|
-
props: {
|
|
175
|
-
options,
|
|
176
|
-
label: 'Favorite fruit',
|
|
177
|
-
id: 'fav-fruit',
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
const combobox = screen.getByRole('combobox');
|
|
181
|
-
expect(combobox).toHaveAttribute('id', 'fav-fruit');
|
|
182
|
-
const label = screen.getByText('Favorite fruit');
|
|
183
|
-
expect(label).toHaveAttribute('for', 'fav-fruit');
|
|
184
|
-
});
|
|
185
|
-
it('should indicate required field', () => {
|
|
186
|
-
render(Combobox, {
|
|
187
|
-
props: {
|
|
188
|
-
options,
|
|
189
|
-
label: 'Required fruit',
|
|
190
|
-
required: true,
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
// Visual indicator
|
|
194
|
-
expect(screen.getByText('*')).toBeInTheDocument();
|
|
195
|
-
// Screen reader text
|
|
196
|
-
expect(screen.getByText('(required)')).toHaveClass('sr-only');
|
|
197
|
-
});
|
|
198
|
-
it('should respect disabled state', () => {
|
|
199
|
-
render(Combobox, {
|
|
200
|
-
props: {
|
|
201
|
-
options,
|
|
202
|
-
label: 'Disabled combobox',
|
|
203
|
-
disabled: true,
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
const combobox = screen.getByRole('combobox');
|
|
207
|
-
expect(combobox).toBeDisabled();
|
|
208
|
-
});
|
|
209
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { screen } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { axe } from 'vitest-axe';
|
|
5
|
-
describe('Dialog accessibility', () => {
|
|
6
|
-
it('should have no axe violations when open', async () => {
|
|
7
|
-
const container = document.createElement('div');
|
|
8
|
-
document.body.appendChild(container);
|
|
9
|
-
container.innerHTML = `
|
|
10
|
-
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc" class="fixed z-50 bg-background p-6 shadow-lg">
|
|
11
|
-
<h2 id="dialog-title" class="text-lg font-semibold">Test Dialog</h2>
|
|
12
|
-
<p id="dialog-desc" class="text-sm text-muted-foreground">This is a test dialog description</p>
|
|
13
|
-
<button type="button" aria-label="Close dialog">Close</button>
|
|
14
|
-
</div>
|
|
15
|
-
`;
|
|
16
|
-
const results = await axe(container);
|
|
17
|
-
expect(results).toHaveNoViolations();
|
|
18
|
-
document.body.removeChild(container);
|
|
19
|
-
});
|
|
20
|
-
it('should have role="dialog"', () => {
|
|
21
|
-
const container = document.createElement('div');
|
|
22
|
-
document.body.appendChild(container);
|
|
23
|
-
container.innerHTML = `
|
|
24
|
-
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
|
25
|
-
<h2 id="dialog-title">Test Dialog</h2>
|
|
26
|
-
<p>Dialog content</p>
|
|
27
|
-
</div>
|
|
28
|
-
`;
|
|
29
|
-
const dialog = screen.getByRole('dialog');
|
|
30
|
-
expect(dialog).toBeInTheDocument();
|
|
31
|
-
document.body.removeChild(container);
|
|
32
|
-
});
|
|
33
|
-
it('should have aria-modal="true"', () => {
|
|
34
|
-
const container = document.createElement('div');
|
|
35
|
-
document.body.appendChild(container);
|
|
36
|
-
container.innerHTML = `
|
|
37
|
-
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
|
38
|
-
<h2 id="dialog-title">Test Dialog</h2>
|
|
39
|
-
</div>
|
|
40
|
-
`;
|
|
41
|
-
const dialog = screen.getByRole('dialog');
|
|
42
|
-
expect(dialog).toHaveAttribute('aria-modal', 'true');
|
|
43
|
-
document.body.removeChild(container);
|
|
44
|
-
});
|
|
45
|
-
it('should have accessible title', () => {
|
|
46
|
-
const container = document.createElement('div');
|
|
47
|
-
document.body.appendChild(container);
|
|
48
|
-
container.innerHTML = `
|
|
49
|
-
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
|
50
|
-
<h2 id="dialog-title">My Dialog Title</h2>
|
|
51
|
-
</div>
|
|
52
|
-
`;
|
|
53
|
-
expect(screen.getByText('My Dialog Title')).toBeInTheDocument();
|
|
54
|
-
document.body.removeChild(container);
|
|
55
|
-
});
|
|
56
|
-
it('should have accessible description', () => {
|
|
57
|
-
const container = document.createElement('div');
|
|
58
|
-
document.body.appendChild(container);
|
|
59
|
-
container.innerHTML = `
|
|
60
|
-
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
|
|
61
|
-
<h2 id="dialog-title">Test Dialog</h2>
|
|
62
|
-
<p id="dialog-desc">Dialog description text</p>
|
|
63
|
-
</div>
|
|
64
|
-
`;
|
|
65
|
-
expect(screen.getByText('Dialog description text')).toBeInTheDocument();
|
|
66
|
-
document.body.removeChild(container);
|
|
67
|
-
});
|
|
68
|
-
it('should have accessible close button', () => {
|
|
69
|
-
const container = document.createElement('div');
|
|
70
|
-
document.body.appendChild(container);
|
|
71
|
-
container.innerHTML = `
|
|
72
|
-
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
|
73
|
-
<h2 id="dialog-title">Test Dialog</h2>
|
|
74
|
-
<button type="button" aria-label="Close dialog">
|
|
75
|
-
<span aria-hidden="true">×</span>
|
|
76
|
-
</button>
|
|
77
|
-
</div>
|
|
78
|
-
`;
|
|
79
|
-
const closeButton = screen.getByRole('button', { name: /close/i });
|
|
80
|
-
expect(closeButton).toBeInTheDocument();
|
|
81
|
-
expect(closeButton).toHaveAttribute('aria-label');
|
|
82
|
-
document.body.removeChild(container);
|
|
83
|
-
});
|
|
84
|
-
it('should be keyboard accessible', async () => {
|
|
85
|
-
const user = userEvent.setup();
|
|
86
|
-
const container = document.createElement('div');
|
|
87
|
-
document.body.appendChild(container);
|
|
88
|
-
container.innerHTML = `
|
|
89
|
-
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
|
90
|
-
<h2 id="dialog-title">Test Dialog</h2>
|
|
91
|
-
<button type="button" id="close-btn" aria-label="Close dialog">Close</button>
|
|
92
|
-
<button type="button" id="action-btn">Action</button>
|
|
93
|
-
</div>
|
|
94
|
-
`;
|
|
95
|
-
const closeBtn = container.querySelector('#close-btn');
|
|
96
|
-
const actionBtn = container.querySelector('#action-btn');
|
|
97
|
-
closeBtn.focus();
|
|
98
|
-
expect(closeBtn).toHaveFocus();
|
|
99
|
-
await user.tab();
|
|
100
|
-
expect(actionBtn).toHaveFocus();
|
|
101
|
-
document.body.removeChild(container);
|
|
102
|
-
});
|
|
103
|
-
});
|