@classic-homes/theme-svelte 1.22.0 → 1.22.2
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/Combobox.svelte +2 -2
- package/dist/lib/components/DataPanel.svelte +19 -5
- package/dist/lib/components/DataTable.svelte +15 -6
- package/dist/lib/components/DataTable.svelte.d.ts +1 -1
- package/dist/lib/components/DateTimePicker.svelte +31 -34
- package/dist/lib/components/DropdownMenu.svelte +52 -30
- package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
- package/dist/lib/components/FileUpload.svelte +5 -19
- package/dist/lib/components/HeaderSearch.svelte +18 -10
- package/dist/lib/components/MultiSelect.svelte +2 -2
- package/dist/lib/components/NumberInput.svelte +15 -2
- package/dist/lib/components/Select.svelte +19 -5
- package/dist/lib/components/Select.svelte.d.ts +4 -0
- package/dist/lib/components/Signature.svelte +15 -4
- package/dist/lib/components/Signature.svelte.d.ts +1 -1
- package/dist/lib/components/Skeleton.svelte.d.ts +1 -1
- package/dist/lib/components/Slider.svelte +8 -6
- package/dist/lib/components/Tabs.svelte +10 -12
- package/dist/lib/components/Toast.svelte +12 -12
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
- package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
- package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
- package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
- package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
- 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 +53 -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/useAsync.svelte.d.ts +1 -1
- package/dist/lib/composables/useAsync.svelte.js +33 -14
- package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
- package/dist/lib/composables/useForm.svelte.js +19 -7
- package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
- 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 +12 -0
- package/dist/lib/stores/sidebar.svelte.js +76 -26
- package/dist/lib/stores/toast.svelte.js +12 -1
- package/dist/lib/utils/date.d.ts +3 -1
- package/dist/lib/utils/date.js +54 -34
- package/dist/lib/utils/form.d.ts +2 -1
- package/dist/lib/utils/form.js +3 -2
- 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,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
|
-
});
|
|
@@ -1,132 +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
|
-
// Test wrapper component for DropdownMenu
|
|
6
|
-
import { createRawSnippet } from 'svelte';
|
|
7
|
-
import DropdownMenu from '../../lib/components/DropdownMenu.svelte';
|
|
8
|
-
describe('DropdownMenu accessibility', () => {
|
|
9
|
-
const items = [
|
|
10
|
-
{ label: 'Edit', value: 'edit', onSelect: () => { } },
|
|
11
|
-
{ label: 'Delete', value: 'delete', destructive: true, onSelect: () => { } },
|
|
12
|
-
];
|
|
13
|
-
// Create a snippet for the trigger
|
|
14
|
-
const createTriggerSnippet = () => createRawSnippet(() => ({
|
|
15
|
-
render: () => '<span>Menu</span>',
|
|
16
|
-
}));
|
|
17
|
-
it('should render trigger as a button element', async () => {
|
|
18
|
-
render(DropdownMenu, {
|
|
19
|
-
props: {
|
|
20
|
-
items,
|
|
21
|
-
trigger: createTriggerSnippet(),
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
// The trigger should be an actual button element, not a span with role="button"
|
|
25
|
-
const trigger = screen.getByRole('button');
|
|
26
|
-
expect(trigger).toBeInTheDocument();
|
|
27
|
-
expect(trigger.tagName.toLowerCase()).toBe('button');
|
|
28
|
-
});
|
|
29
|
-
it('trigger button should have type="button"', async () => {
|
|
30
|
-
render(DropdownMenu, {
|
|
31
|
-
props: {
|
|
32
|
-
items,
|
|
33
|
-
trigger: createTriggerSnippet(),
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
const trigger = screen.getByRole('button');
|
|
37
|
-
expect(trigger).toHaveAttribute('type', 'button');
|
|
38
|
-
});
|
|
39
|
-
it('should have no axe violations when closed', async () => {
|
|
40
|
-
const { container } = render(DropdownMenu, {
|
|
41
|
-
props: {
|
|
42
|
-
items,
|
|
43
|
-
trigger: createTriggerSnippet(),
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
const results = await axe(container);
|
|
47
|
-
expect(results).toHaveNoViolations();
|
|
48
|
-
});
|
|
49
|
-
it('trigger should be focusable', async () => {
|
|
50
|
-
render(DropdownMenu, {
|
|
51
|
-
props: {
|
|
52
|
-
items,
|
|
53
|
-
trigger: createTriggerSnippet(),
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
const trigger = screen.getByRole('button');
|
|
57
|
-
await userEvent.tab();
|
|
58
|
-
expect(trigger).toHaveFocus();
|
|
59
|
-
});
|
|
60
|
-
it('trigger should open menu on Enter key', async () => {
|
|
61
|
-
render(DropdownMenu, {
|
|
62
|
-
props: {
|
|
63
|
-
items,
|
|
64
|
-
trigger: createTriggerSnippet(),
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
const trigger = screen.getByRole('button');
|
|
68
|
-
await userEvent.tab();
|
|
69
|
-
expect(trigger).toHaveFocus();
|
|
70
|
-
await userEvent.keyboard('{Enter}');
|
|
71
|
-
// Menu should be open - check for menu items
|
|
72
|
-
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
|
73
|
-
});
|
|
74
|
-
it('trigger should open menu on Space key', async () => {
|
|
75
|
-
render(DropdownMenu, {
|
|
76
|
-
props: {
|
|
77
|
-
items,
|
|
78
|
-
trigger: createTriggerSnippet(),
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
screen.getByRole('button');
|
|
82
|
-
await userEvent.tab();
|
|
83
|
-
await userEvent.keyboard(' ');
|
|
84
|
-
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
|
85
|
-
});
|
|
86
|
-
it('should open menu with Enter key after focus', async () => {
|
|
87
|
-
render(DropdownMenu, {
|
|
88
|
-
props: {
|
|
89
|
-
items,
|
|
90
|
-
trigger: createTriggerSnippet(),
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
const trigger = screen.getByRole('button');
|
|
94
|
-
trigger.focus();
|
|
95
|
-
// Open with Enter
|
|
96
|
-
await userEvent.keyboard('{Enter}');
|
|
97
|
-
// Wait for menu to open
|
|
98
|
-
const menu = await screen.findByRole('menu');
|
|
99
|
-
expect(menu).toBeInTheDocument();
|
|
100
|
-
// Menu items should be present
|
|
101
|
-
const menuItems = screen.getAllByRole('menuitem');
|
|
102
|
-
expect(menuItems.length).toBeGreaterThan(0);
|
|
103
|
-
});
|
|
104
|
-
it('applies triggerLabel and triggerClass to the trigger button', () => {
|
|
105
|
-
render(DropdownMenu, {
|
|
106
|
-
props: {
|
|
107
|
-
items,
|
|
108
|
-
trigger: createTriggerSnippet(),
|
|
109
|
-
triggerLabel: 'More actions',
|
|
110
|
-
triggerClass: 'custom-trigger',
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
const trigger = screen.getByRole('button', { name: 'More actions' });
|
|
114
|
-
expect(trigger).toHaveClass('custom-trigger');
|
|
115
|
-
// No nested buttons
|
|
116
|
-
expect(trigger.querySelector('button')).toBeNull();
|
|
117
|
-
});
|
|
118
|
-
it('renders href items as links', async () => {
|
|
119
|
-
render(DropdownMenu, {
|
|
120
|
-
props: {
|
|
121
|
-
items: [{ label: 'Open', href: '/somewhere', icon: 'external-link' }, ...items],
|
|
122
|
-
trigger: createTriggerSnippet(),
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
// Earlier tests leave Bits UI's `pointer-events: none` on <body> in jsdom.
|
|
126
|
-
await userEvent.setup({ pointerEventsCheck: 0 }).click(screen.getByRole('button'));
|
|
127
|
-
const link = await screen.findByRole('menuitem', { name: 'Open' });
|
|
128
|
-
expect(link.tagName.toLowerCase()).toBe('a');
|
|
129
|
-
expect(link).toHaveAttribute('href', '/somewhere');
|
|
130
|
-
expect(screen.getByRole('menuitem', { name: 'Edit' }).tagName.toLowerCase()).toBe('div');
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,158 +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 FileUpload from '../../lib/components/FileUpload.svelte';
|
|
6
|
-
describe('FileUpload accessibility', () => {
|
|
7
|
-
it('should have no axe violations', async () => {
|
|
8
|
-
const { container } = render(FileUpload, {
|
|
9
|
-
props: {
|
|
10
|
-
label: 'Upload documents',
|
|
11
|
-
},
|
|
12
|
-
});
|
|
13
|
-
const results = await axe(container);
|
|
14
|
-
expect(results).toHaveNoViolations();
|
|
15
|
-
});
|
|
16
|
-
it('should use label element for the drop zone', () => {
|
|
17
|
-
render(FileUpload, {
|
|
18
|
-
props: {
|
|
19
|
-
label: 'Upload files',
|
|
20
|
-
id: 'file-upload',
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
// The drop zone should be a label wrapping the input
|
|
24
|
-
const label = screen.getByText('Click to upload').closest('label');
|
|
25
|
-
expect(label).toBeInTheDocument();
|
|
26
|
-
expect(label).toHaveAttribute('for', 'file-upload');
|
|
27
|
-
});
|
|
28
|
-
it('should have a properly associated file input', () => {
|
|
29
|
-
render(FileUpload, {
|
|
30
|
-
props: {
|
|
31
|
-
label: 'Upload files',
|
|
32
|
-
id: 'file-upload',
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
// The file input should exist and be associated with the label
|
|
36
|
-
const input = document.getElementById('file-upload');
|
|
37
|
-
expect(input).toBeInTheDocument();
|
|
38
|
-
expect(input).toHaveAttribute('type', 'file');
|
|
39
|
-
});
|
|
40
|
-
it('should be keyboard accessible', async () => {
|
|
41
|
-
render(FileUpload, {
|
|
42
|
-
props: {
|
|
43
|
-
label: 'Upload files',
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
// Tab to the file input (which is inside the label)
|
|
47
|
-
await userEvent.tab();
|
|
48
|
-
// The file input should be focusable
|
|
49
|
-
const input = screen.getByLabelText('Upload files');
|
|
50
|
-
expect(input).toBeInTheDocument();
|
|
51
|
-
});
|
|
52
|
-
it('should have no axe violations with error state', async () => {
|
|
53
|
-
const { container } = render(FileUpload, {
|
|
54
|
-
props: {
|
|
55
|
-
label: 'Upload files',
|
|
56
|
-
error: 'File too large',
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
const results = await axe(container);
|
|
60
|
-
expect(results).toHaveNoViolations();
|
|
61
|
-
});
|
|
62
|
-
it('should announce errors with role="alert"', () => {
|
|
63
|
-
render(FileUpload, {
|
|
64
|
-
props: {
|
|
65
|
-
label: 'Upload files',
|
|
66
|
-
error: 'Invalid file type',
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
const errorMessage = screen.getByRole('alert');
|
|
70
|
-
expect(errorMessage).toBeInTheDocument();
|
|
71
|
-
expect(errorMessage).toHaveTextContent('Invalid file type');
|
|
72
|
-
});
|
|
73
|
-
it('should have aria-describedby pointing to error message', () => {
|
|
74
|
-
render(FileUpload, {
|
|
75
|
-
props: {
|
|
76
|
-
label: 'Upload files',
|
|
77
|
-
id: 'upload',
|
|
78
|
-
error: 'File size exceeds limit',
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
const dropZone = screen.getByText('Click to upload').closest('label');
|
|
82
|
-
expect(dropZone).toHaveAttribute('aria-describedby', 'upload-error');
|
|
83
|
-
const errorMessage = document.getElementById('upload-error');
|
|
84
|
-
expect(errorMessage).toHaveTextContent('File size exceeds limit');
|
|
85
|
-
});
|
|
86
|
-
it('should have accessible file list when files are uploaded', async () => {
|
|
87
|
-
const files = [
|
|
88
|
-
{
|
|
89
|
-
id: '1',
|
|
90
|
-
filename: 'document.pdf',
|
|
91
|
-
contentType: 'application/pdf',
|
|
92
|
-
size: 1024,
|
|
93
|
-
uploading: false,
|
|
94
|
-
progress: 100,
|
|
95
|
-
},
|
|
96
|
-
];
|
|
97
|
-
render(FileUpload, {
|
|
98
|
-
props: {
|
|
99
|
-
label: 'Upload files',
|
|
100
|
-
files,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
// File list should be present with accessible label
|
|
104
|
-
const fileList = screen.getByRole('list', { name: 'Uploaded files' });
|
|
105
|
-
expect(fileList).toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
it('remove button should have accessible label', async () => {
|
|
108
|
-
const files = [
|
|
109
|
-
{
|
|
110
|
-
id: '1',
|
|
111
|
-
filename: 'test.pdf',
|
|
112
|
-
contentType: 'application/pdf',
|
|
113
|
-
size: 1024,
|
|
114
|
-
uploading: false,
|
|
115
|
-
progress: 100,
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
render(FileUpload, {
|
|
119
|
-
props: {
|
|
120
|
-
label: 'Upload files',
|
|
121
|
-
files,
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
const removeButton = screen.getByRole('button', { name: 'Remove test.pdf' });
|
|
125
|
-
expect(removeButton).toBeInTheDocument();
|
|
126
|
-
});
|
|
127
|
-
it('should respect disabled state', async () => {
|
|
128
|
-
render(FileUpload, {
|
|
129
|
-
props: {
|
|
130
|
-
label: 'Upload files',
|
|
131
|
-
disabled: true,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
// Find the input
|
|
135
|
-
const input = screen.getByLabelText('Upload files');
|
|
136
|
-
expect(input).toBeDisabled();
|
|
137
|
-
});
|
|
138
|
-
it('should support multiple files when maxFiles > 1', () => {
|
|
139
|
-
render(FileUpload, {
|
|
140
|
-
props: {
|
|
141
|
-
label: 'Upload files',
|
|
142
|
-
maxFiles: 5,
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
const input = screen.getByLabelText('Upload files');
|
|
146
|
-
expect(input).toHaveAttribute('multiple');
|
|
147
|
-
});
|
|
148
|
-
it('should not have multiple attribute when maxFiles is 1', () => {
|
|
149
|
-
render(FileUpload, {
|
|
150
|
-
props: {
|
|
151
|
-
label: 'Upload files',
|
|
152
|
-
maxFiles: 1,
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
const input = screen.getByLabelText('Upload files');
|
|
156
|
-
expect(input).not.toHaveAttribute('multiple');
|
|
157
|
-
});
|
|
158
|
-
});
|