@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,50 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } 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 SegmentedControl from '../../lib/components/SegmentedControl.svelte';
|
|
6
|
-
const options = [
|
|
7
|
-
{ value: 'community', label: 'Community' },
|
|
8
|
-
{ value: 'collection', label: 'Collection' },
|
|
9
|
-
{ value: 'floor-plan', label: 'Floor Plan', disabled: true },
|
|
10
|
-
];
|
|
11
|
-
describe('SegmentedControl accessibility', () => {
|
|
12
|
-
it('should have no axe violations', async () => {
|
|
13
|
-
const { container } = render(SegmentedControl, {
|
|
14
|
-
props: { options, label: 'Group homes by' },
|
|
15
|
-
});
|
|
16
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
17
|
-
});
|
|
18
|
-
it('is a labelled radiogroup of radios', () => {
|
|
19
|
-
render(SegmentedControl, { props: { options, label: 'Group homes by' } });
|
|
20
|
-
const group = screen.getByRole('radiogroup', { name: 'Group homes by' });
|
|
21
|
-
expect(group).toBeInTheDocument();
|
|
22
|
-
expect(screen.getAllByRole('radio')).toHaveLength(3);
|
|
23
|
-
});
|
|
24
|
-
it('selects the first enabled option by default', () => {
|
|
25
|
-
render(SegmentedControl, { props: { options, label: 'Group homes by' } });
|
|
26
|
-
expect(screen.getByRole('radio', { name: 'Community' })).toHaveAttribute('aria-checked', 'true');
|
|
27
|
-
expect(screen.getByRole('radio', { name: 'Floor Plan' })).toBeDisabled();
|
|
28
|
-
});
|
|
29
|
-
it('moves selection with arrow keys and skips disabled options', async () => {
|
|
30
|
-
const user = userEvent.setup();
|
|
31
|
-
const onValueChange = vi.fn();
|
|
32
|
-
render(SegmentedControl, { props: { options, label: 'Group homes by', onValueChange } });
|
|
33
|
-
const community = screen.getByRole('radio', { name: 'Community' });
|
|
34
|
-
const collection = screen.getByRole('radio', { name: 'Collection' });
|
|
35
|
-
community.focus();
|
|
36
|
-
await user.keyboard('{ArrowRight}');
|
|
37
|
-
expect(collection).toHaveAttribute('aria-checked', 'true');
|
|
38
|
-
expect(onValueChange).toHaveBeenLastCalledWith('collection');
|
|
39
|
-
// Floor Plan is disabled, so the next step loops back to Community
|
|
40
|
-
await user.keyboard('{ArrowRight}');
|
|
41
|
-
expect(community).toHaveAttribute('aria-checked', 'true');
|
|
42
|
-
});
|
|
43
|
-
it('never deselects: clicking the selected option keeps it checked', async () => {
|
|
44
|
-
const user = userEvent.setup();
|
|
45
|
-
render(SegmentedControl, { props: { options, label: 'Group homes by' } });
|
|
46
|
-
const community = screen.getByRole('radio', { name: 'Community' });
|
|
47
|
-
await user.click(community);
|
|
48
|
-
expect(community).toHaveAttribute('aria-checked', 'true');
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,95 +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 Switch from '../../lib/components/Switch.svelte';
|
|
6
|
-
describe('Switch accessibility', () => {
|
|
7
|
-
it('should have no axe violations with aria-label', async () => {
|
|
8
|
-
const { container } = render(Switch, {
|
|
9
|
-
props: { 'aria-label': 'Enable notifications' },
|
|
10
|
-
});
|
|
11
|
-
const results = await axe(container);
|
|
12
|
-
expect(results).toHaveNoViolations();
|
|
13
|
-
});
|
|
14
|
-
it('should have no axe violations with aria-labelledby', async () => {
|
|
15
|
-
const { container } = render(Switch, {
|
|
16
|
-
props: { 'aria-labelledby': 'switch-label' },
|
|
17
|
-
});
|
|
18
|
-
const labelSpan = document.createElement('span');
|
|
19
|
-
labelSpan.id = 'switch-label';
|
|
20
|
-
labelSpan.textContent = 'Enable notifications';
|
|
21
|
-
container.insertBefore(labelSpan, container.firstChild);
|
|
22
|
-
const results = await axe(container);
|
|
23
|
-
expect(results).toHaveNoViolations();
|
|
24
|
-
});
|
|
25
|
-
it('should have no axe violations when checked', async () => {
|
|
26
|
-
const { container } = render(Switch, {
|
|
27
|
-
props: { 'aria-label': 'Enable feature', checked: true },
|
|
28
|
-
});
|
|
29
|
-
const results = await axe(container);
|
|
30
|
-
expect(results).toHaveNoViolations();
|
|
31
|
-
});
|
|
32
|
-
it('should be focusable', async () => {
|
|
33
|
-
render(Switch, {
|
|
34
|
-
props: { 'aria-label': 'Test switch' },
|
|
35
|
-
});
|
|
36
|
-
const switchElement = screen.getByRole('switch');
|
|
37
|
-
await userEvent.tab();
|
|
38
|
-
expect(switchElement).toHaveFocus();
|
|
39
|
-
});
|
|
40
|
-
it('should be togglable with keyboard (Space)', async () => {
|
|
41
|
-
let checked = false;
|
|
42
|
-
render(Switch, {
|
|
43
|
-
props: {
|
|
44
|
-
'aria-label': 'Test switch',
|
|
45
|
-
onchange: (value) => {
|
|
46
|
-
checked = value;
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
await userEvent.tab();
|
|
51
|
-
await userEvent.keyboard(' ');
|
|
52
|
-
expect(checked).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
it('should not be focusable when disabled', async () => {
|
|
55
|
-
render(Switch, {
|
|
56
|
-
props: { 'aria-label': 'Disabled switch', disabled: true },
|
|
57
|
-
});
|
|
58
|
-
const switchElement = screen.getByRole('switch');
|
|
59
|
-
expect(switchElement).toBeDisabled();
|
|
60
|
-
});
|
|
61
|
-
it('should have correct aria-checked state when unchecked', () => {
|
|
62
|
-
render(Switch, {
|
|
63
|
-
props: { 'aria-label': 'Test switch', checked: false },
|
|
64
|
-
});
|
|
65
|
-
const switchElement = screen.getByRole('switch');
|
|
66
|
-
expect(switchElement).toHaveAttribute('aria-checked', 'false');
|
|
67
|
-
});
|
|
68
|
-
it('should have correct aria-checked state when checked', () => {
|
|
69
|
-
render(Switch, {
|
|
70
|
-
props: { 'aria-label': 'Test switch', checked: true },
|
|
71
|
-
});
|
|
72
|
-
const switchElement = screen.getByRole('switch');
|
|
73
|
-
expect(switchElement).toHaveAttribute('aria-checked', 'true');
|
|
74
|
-
});
|
|
75
|
-
it('should support aria-labelledby', () => {
|
|
76
|
-
const { container } = render(Switch, {
|
|
77
|
-
props: { 'aria-labelledby': 'label' },
|
|
78
|
-
});
|
|
79
|
-
const labelSpan = document.createElement('span');
|
|
80
|
-
labelSpan.id = 'label';
|
|
81
|
-
labelSpan.textContent = 'Custom Label';
|
|
82
|
-
container.insertBefore(labelSpan, container.firstChild);
|
|
83
|
-
const switchElement = screen.getByRole('switch');
|
|
84
|
-
expect(switchElement).toHaveAttribute('aria-labelledby', 'label');
|
|
85
|
-
});
|
|
86
|
-
it('should have visible focus indicator', async () => {
|
|
87
|
-
render(Switch, {
|
|
88
|
-
props: { 'aria-label': 'Test switch' },
|
|
89
|
-
});
|
|
90
|
-
const switchElement = screen.getByRole('switch');
|
|
91
|
-
await userEvent.tab();
|
|
92
|
-
expect(switchElement).toHaveFocus();
|
|
93
|
-
// Focus styles are applied via focus-visible classes
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,116 +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 Tabs from '../../lib/components/Tabs.svelte';
|
|
7
|
-
describe('Tabs accessibility', () => {
|
|
8
|
-
const defaultTabs = [
|
|
9
|
-
{ id: 'tab1', label: 'Tab 1' },
|
|
10
|
-
{ id: 'tab2', label: 'Tab 2' },
|
|
11
|
-
{ id: 'tab3', label: 'Tab 3' },
|
|
12
|
-
];
|
|
13
|
-
it('should have no axe violations', async () => {
|
|
14
|
-
const { container } = render(Tabs, {
|
|
15
|
-
props: { tabs: defaultTabs, label: 'Test tabs', id: 'test-tabs' },
|
|
16
|
-
});
|
|
17
|
-
// Add tab panels so aria-controls references are valid
|
|
18
|
-
defaultTabs.forEach((tab, i) => {
|
|
19
|
-
const panel = document.createElement('div');
|
|
20
|
-
panel.id = `test-tabs-panel-${tab.id}`;
|
|
21
|
-
panel.setAttribute('role', 'tabpanel');
|
|
22
|
-
panel.setAttribute('aria-labelledby', `test-tabs-tab-${tab.id}`);
|
|
23
|
-
panel.textContent = `Panel ${i + 1}`;
|
|
24
|
-
if (i > 0)
|
|
25
|
-
panel.hidden = true;
|
|
26
|
-
container.appendChild(panel);
|
|
27
|
-
});
|
|
28
|
-
const results = await axe(container);
|
|
29
|
-
expect(results).toHaveNoViolations();
|
|
30
|
-
});
|
|
31
|
-
it('should have proper tablist role', () => {
|
|
32
|
-
render(Tabs, {
|
|
33
|
-
props: { tabs: defaultTabs, label: 'Test tabs' },
|
|
34
|
-
});
|
|
35
|
-
const tablist = screen.getByRole('tablist');
|
|
36
|
-
expect(tablist).toBeInTheDocument();
|
|
37
|
-
});
|
|
38
|
-
it('should have proper tab roles', () => {
|
|
39
|
-
render(Tabs, {
|
|
40
|
-
props: { tabs: defaultTabs, label: 'Test tabs' },
|
|
41
|
-
});
|
|
42
|
-
const tabs = screen.getAllByRole('tab');
|
|
43
|
-
expect(tabs).toHaveLength(3);
|
|
44
|
-
});
|
|
45
|
-
it('should have accessible label on tablist', () => {
|
|
46
|
-
render(Tabs, {
|
|
47
|
-
props: { tabs: defaultTabs, label: 'Account settings' },
|
|
48
|
-
});
|
|
49
|
-
const tablist = screen.getByRole('tablist');
|
|
50
|
-
expect(tablist).toHaveAttribute('aria-label', 'Account settings');
|
|
51
|
-
});
|
|
52
|
-
it('should indicate selected tab with aria-selected', () => {
|
|
53
|
-
render(Tabs, {
|
|
54
|
-
props: { tabs: defaultTabs, value: 'tab1', label: 'Test tabs' },
|
|
55
|
-
});
|
|
56
|
-
const tabs = screen.getAllByRole('tab');
|
|
57
|
-
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
|
|
58
|
-
expect(tabs[1]).toHaveAttribute('aria-selected', 'false');
|
|
59
|
-
expect(tabs[2]).toHaveAttribute('aria-selected', 'false');
|
|
60
|
-
});
|
|
61
|
-
it('should navigate with arrow keys', async () => {
|
|
62
|
-
render(Tabs, {
|
|
63
|
-
props: { tabs: defaultTabs, label: 'Test tabs' },
|
|
64
|
-
});
|
|
65
|
-
const tabs = screen.getAllByRole('tab');
|
|
66
|
-
// Focus first tab
|
|
67
|
-
tabs[0].focus();
|
|
68
|
-
expect(tabs[0]).toHaveFocus();
|
|
69
|
-
// Navigate right
|
|
70
|
-
await userEvent.keyboard('{ArrowRight}');
|
|
71
|
-
expect(tabs[1]).toHaveFocus();
|
|
72
|
-
// Navigate right again
|
|
73
|
-
await userEvent.keyboard('{ArrowRight}');
|
|
74
|
-
expect(tabs[2]).toHaveFocus();
|
|
75
|
-
// Navigate left
|
|
76
|
-
await userEvent.keyboard('{ArrowLeft}');
|
|
77
|
-
expect(tabs[1]).toHaveFocus();
|
|
78
|
-
});
|
|
79
|
-
it('should activate tab with click', async () => {
|
|
80
|
-
const user = userEvent.setup();
|
|
81
|
-
render(Tabs, {
|
|
82
|
-
props: { tabs: defaultTabs, value: 'tab1', label: 'Test tabs' },
|
|
83
|
-
});
|
|
84
|
-
const tabs = screen.getAllByRole('tab');
|
|
85
|
-
// Click second tab
|
|
86
|
-
await user.click(tabs[1]);
|
|
87
|
-
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
|
|
88
|
-
});
|
|
89
|
-
it('should omit aria-controls when no panel exists', () => {
|
|
90
|
-
// Without children there is no panel in the DOM; an aria-controls pointing
|
|
91
|
-
// at a missing element is an ARIA validity failure (axe: aria-valid-attr-value)
|
|
92
|
-
render(Tabs, {
|
|
93
|
-
props: { tabs: defaultTabs, label: 'Test tabs' },
|
|
94
|
-
});
|
|
95
|
-
for (const tab of screen.getAllByRole('tab')) {
|
|
96
|
-
expect(tab).not.toHaveAttribute('aria-controls');
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
it('should have aria-controls referencing a real panel when children exist', () => {
|
|
100
|
-
const children = createRawSnippet(() => ({
|
|
101
|
-
render: () => '<div id="test-tabs-panel-tab1" role="tabpanel">Panel 1</div>',
|
|
102
|
-
}));
|
|
103
|
-
render(Tabs, {
|
|
104
|
-
props: { tabs: defaultTabs, value: 'tab1', id: 'test-tabs', label: 'Test tabs', children },
|
|
105
|
-
});
|
|
106
|
-
const tabs = screen.getAllByRole('tab');
|
|
107
|
-
const selectedTab = tabs.find((tab) => tab.getAttribute('aria-selected') === 'true');
|
|
108
|
-
expect(selectedTab).toHaveAttribute('aria-controls', 'test-tabs-panel-tab1');
|
|
109
|
-
// The reference must resolve to an element that exists
|
|
110
|
-
expect(document.getElementById('test-tabs-panel-tab1')).not.toBeNull();
|
|
111
|
-
// Unselected tabs have no panel mounted, so no aria-controls
|
|
112
|
-
for (const tab of tabs.filter((t) => t !== selectedTab)) {
|
|
113
|
-
expect(tab).not.toHaveAttribute('aria-controls');
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
});
|
|
@@ -1,111 +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 Textarea from '../../lib/components/Textarea.svelte';
|
|
6
|
-
describe('Textarea accessibility', () => {
|
|
7
|
-
it('should have no axe violations with aria-label', async () => {
|
|
8
|
-
const { container } = render(Textarea, {
|
|
9
|
-
props: { 'aria-label': 'Description' },
|
|
10
|
-
});
|
|
11
|
-
const results = await axe(container);
|
|
12
|
-
expect(results).toHaveNoViolations();
|
|
13
|
-
});
|
|
14
|
-
it('should have no axe violations with aria-invalid', async () => {
|
|
15
|
-
const { container } = render(Textarea, {
|
|
16
|
-
props: {
|
|
17
|
-
'aria-label': 'Description',
|
|
18
|
-
'aria-invalid': true,
|
|
19
|
-
'aria-describedby': 'error',
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
const errorSpan = document.createElement('span');
|
|
23
|
-
errorSpan.id = 'error';
|
|
24
|
-
errorSpan.textContent = 'Description is required';
|
|
25
|
-
container.appendChild(errorSpan);
|
|
26
|
-
const results = await axe(container);
|
|
27
|
-
expect(results).toHaveNoViolations();
|
|
28
|
-
});
|
|
29
|
-
it('should be focusable', async () => {
|
|
30
|
-
render(Textarea, {
|
|
31
|
-
props: { 'aria-label': 'Test textarea' },
|
|
32
|
-
});
|
|
33
|
-
const textarea = screen.getByRole('textbox');
|
|
34
|
-
await userEvent.tab();
|
|
35
|
-
expect(textarea).toHaveFocus();
|
|
36
|
-
});
|
|
37
|
-
it('should support aria-describedby for help text', () => {
|
|
38
|
-
const { container } = render(Textarea, {
|
|
39
|
-
props: { 'aria-label': 'Comments', 'aria-describedby': 'help' },
|
|
40
|
-
});
|
|
41
|
-
const helpSpan = document.createElement('span');
|
|
42
|
-
helpSpan.id = 'help';
|
|
43
|
-
helpSpan.textContent = 'Enter your comments here';
|
|
44
|
-
container.appendChild(helpSpan);
|
|
45
|
-
const textarea = screen.getByRole('textbox');
|
|
46
|
-
expect(textarea).toHaveAttribute('aria-describedby', 'help');
|
|
47
|
-
});
|
|
48
|
-
it('should support aria-labelledby', () => {
|
|
49
|
-
const { container } = render(Textarea, {
|
|
50
|
-
props: { 'aria-labelledby': 'label' },
|
|
51
|
-
});
|
|
52
|
-
const labelSpan = document.createElement('span');
|
|
53
|
-
labelSpan.id = 'label';
|
|
54
|
-
labelSpan.textContent = 'Custom Label';
|
|
55
|
-
container.insertBefore(labelSpan, container.firstChild);
|
|
56
|
-
const textarea = screen.getByRole('textbox');
|
|
57
|
-
expect(textarea).toHaveAttribute('aria-labelledby', 'label');
|
|
58
|
-
});
|
|
59
|
-
it('should apply error styling when aria-invalid is true', () => {
|
|
60
|
-
render(Textarea, {
|
|
61
|
-
props: { 'aria-label': 'Description', 'aria-invalid': true },
|
|
62
|
-
});
|
|
63
|
-
const textarea = screen.getByRole('textbox');
|
|
64
|
-
expect(textarea).toHaveAttribute('aria-invalid', 'true');
|
|
65
|
-
expect(textarea.className).toContain('border-destructive');
|
|
66
|
-
});
|
|
67
|
-
it('should apply error styling when aria-invalid is "true" string', () => {
|
|
68
|
-
render(Textarea, {
|
|
69
|
-
props: { 'aria-label': 'Description', 'aria-invalid': 'true' },
|
|
70
|
-
});
|
|
71
|
-
const textarea = screen.getByRole('textbox');
|
|
72
|
-
expect(textarea.className).toContain('border-destructive');
|
|
73
|
-
});
|
|
74
|
-
it('should not apply error styling when aria-invalid is false', () => {
|
|
75
|
-
render(Textarea, {
|
|
76
|
-
props: { 'aria-label': 'Description', 'aria-invalid': false },
|
|
77
|
-
});
|
|
78
|
-
const textarea = screen.getByRole('textbox');
|
|
79
|
-
expect(textarea.className).not.toContain('border-destructive');
|
|
80
|
-
});
|
|
81
|
-
it('should be disabled when disabled prop is true', () => {
|
|
82
|
-
render(Textarea, {
|
|
83
|
-
props: { 'aria-label': 'Disabled textarea', disabled: true },
|
|
84
|
-
});
|
|
85
|
-
const textarea = screen.getByRole('textbox');
|
|
86
|
-
expect(textarea).toBeDisabled();
|
|
87
|
-
});
|
|
88
|
-
it('should support required attribute', () => {
|
|
89
|
-
render(Textarea, {
|
|
90
|
-
props: { 'aria-label': 'Required field', required: true },
|
|
91
|
-
});
|
|
92
|
-
const textarea = screen.getByRole('textbox');
|
|
93
|
-
expect(textarea).toBeRequired();
|
|
94
|
-
});
|
|
95
|
-
it('should support readonly attribute', () => {
|
|
96
|
-
render(Textarea, {
|
|
97
|
-
props: { 'aria-label': 'Readonly field', readonly: true },
|
|
98
|
-
});
|
|
99
|
-
const textarea = screen.getByRole('textbox');
|
|
100
|
-
expect(textarea).toHaveAttribute('readonly');
|
|
101
|
-
});
|
|
102
|
-
it('should have visible focus indicator', async () => {
|
|
103
|
-
render(Textarea, {
|
|
104
|
-
props: { 'aria-label': 'Test textarea' },
|
|
105
|
-
});
|
|
106
|
-
const textarea = screen.getByRole('textbox');
|
|
107
|
-
await userEvent.tab();
|
|
108
|
-
expect(textarea).toHaveFocus();
|
|
109
|
-
// Focus styles are applied via focus-visible classes
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import { axe } from 'vitest-axe';
|
|
4
|
-
import { createRawSnippet } from 'svelte';
|
|
5
|
-
import Toolbar from '../../lib/components/Toolbar.svelte';
|
|
6
|
-
const snippet = (html) => createRawSnippet(() => ({ render: () => html }));
|
|
7
|
-
describe('Toolbar accessibility', () => {
|
|
8
|
-
it('should have no axe violations', async () => {
|
|
9
|
-
const { container } = render(Toolbar, {
|
|
10
|
-
props: {
|
|
11
|
-
label: 'Inventory tools',
|
|
12
|
-
children: snippet('<button type="button">Filters</button>'),
|
|
13
|
-
end: snippet('<button type="button">Map</button>'),
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
17
|
-
});
|
|
18
|
-
it('is a labelled group that renders both slots', () => {
|
|
19
|
-
render(Toolbar, {
|
|
20
|
-
props: {
|
|
21
|
-
label: 'Inventory tools',
|
|
22
|
-
children: snippet('<span>89 homes</span>'),
|
|
23
|
-
end: snippet('<button type="button">Map</button>'),
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
const group = screen.getByRole('group', { name: 'Inventory tools' });
|
|
27
|
-
expect(group).toHaveClass('bg-toolbar');
|
|
28
|
-
expect(screen.getByText('89 homes')).toBeInTheDocument();
|
|
29
|
-
expect(screen.getByRole('button', { name: 'Map' })).toBeInTheDocument();
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } 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 ViewToggle from '../../lib/components/ViewToggle.svelte';
|
|
6
|
-
describe('ViewToggle accessibility', () => {
|
|
7
|
-
it('should have no axe violations', async () => {
|
|
8
|
-
const { container } = render(ViewToggle);
|
|
9
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
10
|
-
});
|
|
11
|
-
it('exposes list / grid / map radios with accessible names', () => {
|
|
12
|
-
render(ViewToggle);
|
|
13
|
-
expect(screen.getByRole('radiogroup', { name: 'View' })).toBeInTheDocument();
|
|
14
|
-
expect(screen.getByRole('radio', { name: 'List view' })).toHaveAttribute('aria-checked', 'true');
|
|
15
|
-
expect(screen.getByRole('radio', { name: 'Grid view' })).toBeInTheDocument();
|
|
16
|
-
expect(screen.getByRole('radio', { name: 'Map view' })).toBeInTheDocument();
|
|
17
|
-
});
|
|
18
|
-
it('changes the view with the keyboard and reports it', async () => {
|
|
19
|
-
const user = userEvent.setup();
|
|
20
|
-
const onValueChange = vi.fn();
|
|
21
|
-
render(ViewToggle, { props: { onValueChange } });
|
|
22
|
-
screen.getByRole('radio', { name: 'List view' }).focus();
|
|
23
|
-
await user.keyboard('{ArrowRight}');
|
|
24
|
-
expect(screen.getByRole('radio', { name: 'Grid view' })).toHaveAttribute('aria-checked', 'true');
|
|
25
|
-
expect(onValueChange).toHaveBeenLastCalledWith('grid');
|
|
26
|
-
});
|
|
27
|
-
it('honours a custom option set and value', () => {
|
|
28
|
-
render(ViewToggle, {
|
|
29
|
-
props: {
|
|
30
|
-
label: 'Layout',
|
|
31
|
-
value: 'cards',
|
|
32
|
-
options: [
|
|
33
|
-
{ value: 'table', icon: 'list', label: 'Table' },
|
|
34
|
-
{ value: 'cards', icon: 'grid', label: 'Cards' },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
expect(screen.getByRole('radio', { name: 'Cards' })).toHaveAttribute('aria-checked', 'true');
|
|
39
|
-
});
|
|
40
|
-
});
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { render, screen, within } 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 ActionHeader from '../lib/components/ActionHeader.svelte';
|
|
7
|
-
// Bits UI leaves `pointer-events: none` on <body> after a menu closes in jsdom.
|
|
8
|
-
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
9
|
-
const snippet = (html) => createRawSnippet(() => ({ render: () => html }));
|
|
10
|
-
const actions = [
|
|
11
|
-
{ label: 'Export', icon: 'download', onSelect: () => { } },
|
|
12
|
-
{ label: 'Add sign', icon: 'plus', priority: 'primary', onSelect: () => { } },
|
|
13
|
-
{ label: 'Archive', priority: 'overflow', onSelect: () => { } },
|
|
14
|
-
{ label: 'Delete', priority: 'overflow', destructive: true, onSelect: () => { } },
|
|
15
|
-
{ label: 'Secret', priority: 'overflow', hidden: true, onSelect: () => { } },
|
|
16
|
-
];
|
|
17
|
-
async function openMenu(container, selector) {
|
|
18
|
-
const wrapper = container.querySelector(selector);
|
|
19
|
-
await user.click(within(wrapper).getByRole('button', { name: 'More actions' }));
|
|
20
|
-
return screen.findByRole('menu');
|
|
21
|
-
}
|
|
22
|
-
describe('ActionHeader', () => {
|
|
23
|
-
it('renders title and subtitle with the requested heading level', () => {
|
|
24
|
-
render(ActionHeader, {
|
|
25
|
-
props: { title: 'Signs', subtitle: 'All signage', headingLevel: 2 },
|
|
26
|
-
});
|
|
27
|
-
expect(screen.getByRole('heading', { level: 2, name: 'Signs' })).toBeInTheDocument();
|
|
28
|
-
expect(screen.getByText('All signage')).toBeInTheDocument();
|
|
29
|
-
});
|
|
30
|
-
it('styles the title per heading level: serif italic h1, sans h2', () => {
|
|
31
|
-
const { unmount } = render(ActionHeader, { props: { title: 'Homes' } });
|
|
32
|
-
expect(screen.getByRole('heading', { level: 1 })).toHaveClass('font-serif', 'italic');
|
|
33
|
-
unmount();
|
|
34
|
-
render(ActionHeader, { props: { title: 'Flying Horse', headingLevel: 2 } });
|
|
35
|
-
expect(screen.getByRole('heading', { level: 2 })).toHaveClass('font-sans', 'font-semibold');
|
|
36
|
-
});
|
|
37
|
-
it('renders the caption under the actions', () => {
|
|
38
|
-
const { container } = render(ActionHeader, {
|
|
39
|
-
props: { title: 'Homes', caption: 'Updated 5 minutes ago', actions },
|
|
40
|
-
});
|
|
41
|
-
expect(screen.getByText('Updated 5 minutes ago')).toHaveClass('ah-caption');
|
|
42
|
-
expect(container.querySelector('header')).toHaveAttribute('data-has-caption');
|
|
43
|
-
});
|
|
44
|
-
it('defaults to h1 and renders no action cluster without actions', () => {
|
|
45
|
-
const { container } = render(ActionHeader, { props: { title: 'Account' } });
|
|
46
|
-
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
|
47
|
-
expect(container.querySelector('.ah-actions')).toBeNull();
|
|
48
|
-
expect(screen.queryByRole('button')).toBeNull();
|
|
49
|
-
});
|
|
50
|
-
it('renders a labelled back link', () => {
|
|
51
|
-
render(ActionHeader, { props: { title: 'Sign 12', backHref: '/signs', backLabel: 'Signs' } });
|
|
52
|
-
const back = screen.getByRole('link', { name: 'Back to Signs' });
|
|
53
|
-
expect(back).toHaveAttribute('href', '/signs');
|
|
54
|
-
});
|
|
55
|
-
it('places primary and secondary actions inline, and omits hidden ones', () => {
|
|
56
|
-
const { container } = render(ActionHeader, { props: { title: 'Signs', actions } });
|
|
57
|
-
const secondary = container.querySelector('.ah-secondary');
|
|
58
|
-
expect(within(secondary).getByRole('button', { name: 'Export' })).toBeInTheDocument();
|
|
59
|
-
expect(screen.getByRole('button', { name: 'Add sign' })).toBeInTheDocument();
|
|
60
|
-
expect(screen.queryByText('Secret')).toBeNull();
|
|
61
|
-
});
|
|
62
|
-
it('wide menu holds only overflow actions, destructive last', async () => {
|
|
63
|
-
const { container } = render(ActionHeader, { props: { title: 'Signs', actions } });
|
|
64
|
-
const menu = await openMenu(container, '.ah-menu-wide');
|
|
65
|
-
const names = within(menu)
|
|
66
|
-
.getAllByRole('menuitem')
|
|
67
|
-
.map((el) => el.textContent?.trim());
|
|
68
|
-
expect(names).toEqual(['Archive', 'Delete']);
|
|
69
|
-
const separator = within(menu).queryByRole('separator') ?? menu.querySelector('.h-px');
|
|
70
|
-
expect(separator).not.toBeNull();
|
|
71
|
-
});
|
|
72
|
-
it('narrow menu folds secondary actions in before overflow', async () => {
|
|
73
|
-
const { container } = render(ActionHeader, { props: { title: 'Signs', actions } });
|
|
74
|
-
const menu = await openMenu(container, '.ah-menu-narrow');
|
|
75
|
-
const names = within(menu)
|
|
76
|
-
.getAllByRole('menuitem')
|
|
77
|
-
.map((el) => el.textContent?.trim());
|
|
78
|
-
expect(names).toEqual(['Export', 'Archive', 'Delete']);
|
|
79
|
-
});
|
|
80
|
-
it('with no secondary actions renders a single overflow menu', () => {
|
|
81
|
-
const { container } = render(ActionHeader, {
|
|
82
|
-
props: {
|
|
83
|
-
title: 'Sign',
|
|
84
|
-
actions: [
|
|
85
|
-
{ label: 'Check in', priority: 'primary' },
|
|
86
|
-
{ label: 'Delete', priority: 'overflow', destructive: true },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
expect(container.querySelector('.ah-menu-narrow')).toBeNull();
|
|
91
|
-
expect(screen.getAllByRole('button', { name: 'More actions' })).toHaveLength(1);
|
|
92
|
-
});
|
|
93
|
-
it('collapseAt="never" keeps every non-primary action inline with no menu', () => {
|
|
94
|
-
render(ActionHeader, { props: { title: 'Signs', actions, collapseAt: 'never' } });
|
|
95
|
-
expect(screen.getByRole('button', { name: 'Archive' })).toBeInTheDocument();
|
|
96
|
-
expect(screen.queryByRole('button', { name: 'More actions' })).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
it('auto collapse uses lg with 2+ secondary actions, md otherwise', () => {
|
|
99
|
-
const { container, unmount } = render(ActionHeader, { props: { title: 'A', actions } });
|
|
100
|
-
expect(container.querySelector('header')).toHaveAttribute('data-collapse', 'md');
|
|
101
|
-
unmount();
|
|
102
|
-
const two = [{ label: 'One' }, { label: 'Two' }];
|
|
103
|
-
const second = render(ActionHeader, { props: { title: 'B', actions: two } });
|
|
104
|
-
expect(second.container.querySelector('header')).toHaveAttribute('data-collapse', 'lg');
|
|
105
|
-
});
|
|
106
|
-
it('calls onSelect for inline and menu actions', async () => {
|
|
107
|
-
const onPrimary = vi.fn();
|
|
108
|
-
const onOverflow = vi.fn();
|
|
109
|
-
const { container } = render(ActionHeader, {
|
|
110
|
-
props: {
|
|
111
|
-
title: 'Signs',
|
|
112
|
-
actions: [
|
|
113
|
-
{ label: 'Add', priority: 'primary', onSelect: onPrimary },
|
|
114
|
-
{ label: 'Archive', priority: 'overflow', onSelect: onOverflow },
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
await user.click(screen.getByRole('button', { name: 'Add' }));
|
|
119
|
-
expect(onPrimary).toHaveBeenCalledOnce();
|
|
120
|
-
const menu = await openMenu(container, '.ah');
|
|
121
|
-
await user.click(within(menu).getByRole('menuitem', { name: 'Archive' }));
|
|
122
|
-
expect(onOverflow).toHaveBeenCalledOnce();
|
|
123
|
-
});
|
|
124
|
-
it('renders badges, tools and toolbar snippets', () => {
|
|
125
|
-
render(ActionHeader, {
|
|
126
|
-
props: {
|
|
127
|
-
title: 'Sign 12',
|
|
128
|
-
badges: snippet('<span>Active</span>'),
|
|
129
|
-
tools: snippet('<button type="button" aria-label="Related">R</button>'),
|
|
130
|
-
toolbar: snippet('<div>View toggle</div>'),
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
134
|
-
expect(screen.getByRole('button', { name: 'Related' })).toBeInTheDocument();
|
|
135
|
-
expect(screen.getByText('View toggle')).toBeInTheDocument();
|
|
136
|
-
});
|
|
137
|
-
it('has no axe violations', async () => {
|
|
138
|
-
const { container } = render(ActionHeader, {
|
|
139
|
-
props: {
|
|
140
|
-
title: 'Sign 12',
|
|
141
|
-
subtitle: 'Main entrance monument',
|
|
142
|
-
backHref: '/signs',
|
|
143
|
-
backLabel: 'Signs',
|
|
144
|
-
actions,
|
|
145
|
-
badges: snippet('<span>Active</span>'),
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
149
|
-
});
|
|
150
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AlertDialog: however it closes — Cancel, confirm, Escape — exactly one of
|
|
3
|
-
* onConfirm / onCancel runs, so `{#if x}<AlertDialog open onCancel=…>` callers
|
|
4
|
-
* can reset their state on Escape too.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
7
|
-
import { mount, unmount, flushSync, tick } from 'svelte';
|
|
8
|
-
import AlertDialog from '../lib/components/AlertDialog.svelte';
|
|
9
|
-
let instance = null;
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
if (instance)
|
|
12
|
-
unmount(instance);
|
|
13
|
-
instance = null;
|
|
14
|
-
document.body.innerHTML = '';
|
|
15
|
-
});
|
|
16
|
-
async function mountOpen() {
|
|
17
|
-
const onConfirm = vi.fn();
|
|
18
|
-
const onCancel = vi.fn();
|
|
19
|
-
instance = mount(AlertDialog, {
|
|
20
|
-
target: document.body,
|
|
21
|
-
props: { open: true, title: 'Delete?', onConfirm, onCancel },
|
|
22
|
-
});
|
|
23
|
-
flushSync();
|
|
24
|
-
await tick();
|
|
25
|
-
const button = (label) => [...document.querySelectorAll('button')].find((b) => b.textContent?.trim() === label);
|
|
26
|
-
return { onConfirm, onCancel, button };
|
|
27
|
-
}
|
|
28
|
-
describe('AlertDialog dismiss', () => {
|
|
29
|
-
it('calls onCancel once on Cancel', async () => {
|
|
30
|
-
const { onConfirm, onCancel, button } = await mountOpen();
|
|
31
|
-
button('Cancel').click();
|
|
32
|
-
flushSync();
|
|
33
|
-
await tick();
|
|
34
|
-
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
35
|
-
expect(onConfirm).not.toHaveBeenCalled();
|
|
36
|
-
});
|
|
37
|
-
it('calls onConfirm once and not onCancel on confirm', async () => {
|
|
38
|
-
const { onConfirm, onCancel, button } = await mountOpen();
|
|
39
|
-
button('Continue').click();
|
|
40
|
-
flushSync();
|
|
41
|
-
await tick();
|
|
42
|
-
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
43
|
-
expect(onCancel).not.toHaveBeenCalled();
|
|
44
|
-
});
|
|
45
|
-
it('calls onCancel on Escape', async () => {
|
|
46
|
-
const { onConfirm, onCancel } = await mountOpen();
|
|
47
|
-
const content = document.querySelector('[role="alertdialog"]');
|
|
48
|
-
content.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
49
|
-
flushSync();
|
|
50
|
-
await tick();
|
|
51
|
-
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
52
|
-
expect(onConfirm).not.toHaveBeenCalled();
|
|
53
|
-
});
|
|
54
|
-
});
|