@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
package/dist/lib/utils/date.js
CHANGED
|
@@ -104,6 +104,37 @@ export function isSameDay(date1, date2) {
|
|
|
104
104
|
date1.getMonth() === date2.getMonth() &&
|
|
105
105
|
date1.getDate() === date2.getDate());
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Build a local Date, or null when a part is out of range. `new Date()` rolls
|
|
109
|
+
* overflow into the next unit (Feb 30 -> Mar 1, 25:00 -> next day), so the
|
|
110
|
+
* date parts must round-trip and the time parts must be in range.
|
|
111
|
+
*/
|
|
112
|
+
function buildDate(year, monthIndex, day, hour, minute) {
|
|
113
|
+
if (hour > 23 || minute > 59)
|
|
114
|
+
return null;
|
|
115
|
+
const date = new Date(year, monthIndex, day, hour, minute);
|
|
116
|
+
if (date.getFullYear() !== year || date.getMonth() !== monthIndex || date.getDate() !== day) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return date;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Convert a typed hour (with optional AM/PM) to 0-23, or null when out of
|
|
123
|
+
* range for its clock (e.g. `13 PM`, `0 AM`).
|
|
124
|
+
*/
|
|
125
|
+
function toHour24(hour, meridiem) {
|
|
126
|
+
if (!hour)
|
|
127
|
+
return 0;
|
|
128
|
+
const parsed = parseInt(hour);
|
|
129
|
+
if (!meridiem)
|
|
130
|
+
return parsed;
|
|
131
|
+
if (parsed < 1 || parsed > 12)
|
|
132
|
+
return null;
|
|
133
|
+
const isPm = meridiem.toUpperCase() === 'PM';
|
|
134
|
+
if (parsed === 12)
|
|
135
|
+
return isPm ? 12 : 0;
|
|
136
|
+
return isPm ? parsed + 12 : parsed;
|
|
137
|
+
}
|
|
107
138
|
/**
|
|
108
139
|
* Parse a user-typed date input string into a Date object.
|
|
109
140
|
*
|
|
@@ -111,7 +142,9 @@ export function isSameDay(date1, date2) {
|
|
|
111
142
|
* - ISO: `2024-03-15`, `2024-03-15T14:30`
|
|
112
143
|
* - US: `03/15/2024`, `3/15/24`, `03-15-2024`
|
|
113
144
|
* - Natural: `Mar 15, 2024`, `March 15 2024`
|
|
114
|
-
* - With time: `03/15/2024 2:30 PM`, `03/15/2024 14:30`
|
|
145
|
+
* - With time: `03/15/2024 2:30 PM`, `03/15/2024 14:30`, `Mar 15, 2024 at 2:30 PM`
|
|
146
|
+
*
|
|
147
|
+
* Out-of-range parts (`02/30/2024`, `25:99`) are rejected, not rolled over.
|
|
115
148
|
*
|
|
116
149
|
* @param input - The user-typed date string
|
|
117
150
|
* @param options - Parsing options
|
|
@@ -128,17 +161,17 @@ export function parseDateInput(input, options) {
|
|
|
128
161
|
}
|
|
129
162
|
// Try parsing with various patterns
|
|
130
163
|
let date = null;
|
|
131
|
-
let
|
|
164
|
+
let matched = false;
|
|
132
165
|
// Pattern 1: ISO format (2024-03-15 or 2024-03-15T14:30)
|
|
133
166
|
const isoMatch = trimmed.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T ](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
|
|
134
167
|
if (isoMatch) {
|
|
135
168
|
const [, year, month, day, hour, minute] = isoMatch;
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
matched = true;
|
|
170
|
+
date = buildDate(parseInt(year), parseInt(month) - 1, parseInt(day), hour ? parseInt(hour) : 0, minute ? parseInt(minute) : 0);
|
|
138
171
|
}
|
|
139
172
|
// Pattern 2: US format with slashes or dashes (03/15/2024, 3/15/24, 03-15-2024)
|
|
140
|
-
if (!
|
|
141
|
-
const usMatch = trimmed.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})(?:\s+(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
|
|
173
|
+
if (!matched) {
|
|
174
|
+
const usMatch = trimmed.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})(?:\s+(?:at\s+)?(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
|
|
142
175
|
if (usMatch) {
|
|
143
176
|
const [, month, day, yearStr, hour, minute, meridiem] = usMatch;
|
|
144
177
|
let year = parseInt(yearStr);
|
|
@@ -146,22 +179,16 @@ export function parseDateInput(input, options) {
|
|
|
146
179
|
if (year < 100) {
|
|
147
180
|
year += year < 50 ? 2000 : 1900;
|
|
148
181
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
else if (upper === 'AM' && parsedHour === 12) {
|
|
156
|
-
parsedHour = 0;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
date = new Date(year, parseInt(month) - 1, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
|
|
160
|
-
hasTime = !!hour;
|
|
182
|
+
matched = true;
|
|
183
|
+
const parsedHour = toHour24(hour, meridiem);
|
|
184
|
+
date =
|
|
185
|
+
parsedHour === null
|
|
186
|
+
? null
|
|
187
|
+
: buildDate(year, parseInt(month) - 1, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
|
|
161
188
|
}
|
|
162
189
|
}
|
|
163
190
|
// Pattern 3: Natural format (Mar 15, 2024 or March 15 2024)
|
|
164
|
-
if (!
|
|
191
|
+
if (!matched) {
|
|
165
192
|
const monthNames = {
|
|
166
193
|
jan: 0,
|
|
167
194
|
january: 0,
|
|
@@ -188,23 +215,16 @@ export function parseDateInput(input, options) {
|
|
|
188
215
|
dec: 11,
|
|
189
216
|
december: 11,
|
|
190
217
|
};
|
|
191
|
-
const naturalMatch = trimmed.match(/^([a-zA-Z]+)\s+(\d{1,2}),?\s+(\d{4})(?:\s+(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
|
|
218
|
+
const naturalMatch = trimmed.match(/^([a-zA-Z]+)\s+(\d{1,2}),?\s+(\d{4})(?:\s+(?:at\s+)?(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
|
|
192
219
|
if (naturalMatch) {
|
|
193
220
|
const [, monthStr, day, year, hour, minute, meridiem] = naturalMatch;
|
|
194
221
|
const monthNum = monthNames[monthStr.toLowerCase()];
|
|
195
222
|
if (monthNum !== undefined) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
parsedHour
|
|
201
|
-
}
|
|
202
|
-
else if (upper === 'AM' && parsedHour === 12) {
|
|
203
|
-
parsedHour = 0;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
date = new Date(parseInt(year), monthNum, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
|
|
207
|
-
hasTime = !!hour;
|
|
223
|
+
const parsedHour = toHour24(hour, meridiem);
|
|
224
|
+
date =
|
|
225
|
+
parsedHour === null
|
|
226
|
+
? null
|
|
227
|
+
: buildDate(parseInt(year), monthNum, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
|
|
208
228
|
}
|
|
209
229
|
}
|
|
210
230
|
}
|
|
@@ -216,8 +236,8 @@ export function parseDateInput(input, options) {
|
|
|
216
236
|
error: 'Invalid date format. Try MM/DD/YYYY or YYYY-MM-DD',
|
|
217
237
|
};
|
|
218
238
|
}
|
|
219
|
-
//
|
|
220
|
-
if (options?.dateOnly
|
|
239
|
+
// In dateOnly mode a typed time is accepted but reset to midnight
|
|
240
|
+
if (options?.dateOnly) {
|
|
221
241
|
date.setHours(0, 0, 0, 0);
|
|
222
242
|
}
|
|
223
243
|
return { date, isValid: true };
|
package/dist/lib/utils/form.d.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
export declare function generateId(prefix?: string): string;
|
|
12
12
|
/**
|
|
13
13
|
* Computes the aria-describedby attribute value based on hint and error states.
|
|
14
|
-
* Returns undefined if neither hint nor error is present.
|
|
14
|
+
* Returns undefined if neither hint nor error is present. The hint is left out
|
|
15
|
+
* while an error shows, since form fields render the error in its place.
|
|
15
16
|
*
|
|
16
17
|
* @param hintId - The ID of the hint element
|
|
17
18
|
* @param errorId - The ID of the error element
|
package/dist/lib/utils/form.js
CHANGED
|
@@ -13,7 +13,8 @@ export function generateId(prefix = 'field') {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Computes the aria-describedby attribute value based on hint and error states.
|
|
16
|
-
* Returns undefined if neither hint nor error is present.
|
|
16
|
+
* Returns undefined if neither hint nor error is present. The hint is left out
|
|
17
|
+
* while an error shows, since form fields render the error in its place.
|
|
17
18
|
*
|
|
18
19
|
* @param hintId - The ID of the hint element
|
|
19
20
|
* @param errorId - The ID of the error element
|
|
@@ -23,7 +24,7 @@ export function generateId(prefix = 'field') {
|
|
|
23
24
|
*/
|
|
24
25
|
export function computeDescribedBy(hintId, errorId, hasHint, hasError) {
|
|
25
26
|
const ids = [];
|
|
26
|
-
if (hasHint && hintId)
|
|
27
|
+
if (hasHint && !hasError && hintId)
|
|
27
28
|
ids.push(hintId);
|
|
28
29
|
if (hasError && errorId)
|
|
29
30
|
ids.push(errorId);
|
package/dist/lib/utils/media.js
CHANGED
|
@@ -614,10 +614,11 @@ export function buildEmbedSrc(provider, videoId, options = {}) {
|
|
|
614
614
|
params.set('autoplay', '1');
|
|
615
615
|
if (privacy)
|
|
616
616
|
params.set('dnt', '1');
|
|
617
|
-
if (start !== undefined)
|
|
618
|
-
params.set('#t', `${Math.floor(start)}s`);
|
|
619
617
|
const query = params.toString();
|
|
620
|
-
|
|
618
|
+
// Vimeo reads the start time from the fragment (#t=30s), not the query;
|
|
619
|
+
// URLSearchParams would encode it as `%23t`, which Vimeo ignores.
|
|
620
|
+
const fragment = start !== undefined ? `#t=${Math.floor(start)}s` : '';
|
|
621
|
+
return `https://player.vimeo.com/video/${videoId}${query ? `?${query}` : ''}${fragment}`;
|
|
621
622
|
}
|
|
622
623
|
/**
|
|
623
624
|
* Poster image URL for an embedded video, when one can be derived without a
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL safety helpers for hrefs that come from config or API data.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Return `url` when it is relative or uses a safe scheme (http, https, mailto,
|
|
6
|
+
* tel), otherwise `null`. Rejects `javascript:`, `data:`, `vbscript:` and the
|
|
7
|
+
* like, including obfuscated forms (leading whitespace, mixed case, embedded
|
|
8
|
+
* tabs/newlines), because the URL parser normalises those before the check.
|
|
9
|
+
*/
|
|
10
|
+
export declare function safeHref(url: string | null | undefined): string | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL safety helpers for hrefs that come from config or API data.
|
|
3
|
+
*/
|
|
4
|
+
const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
5
|
+
/**
|
|
6
|
+
* Return `url` when it is relative or uses a safe scheme (http, https, mailto,
|
|
7
|
+
* tel), otherwise `null`. Rejects `javascript:`, `data:`, `vbscript:` and the
|
|
8
|
+
* like, including obfuscated forms (leading whitespace, mixed case, embedded
|
|
9
|
+
* tabs/newlines), because the URL parser normalises those before the check.
|
|
10
|
+
*/
|
|
11
|
+
export function safeHref(url) {
|
|
12
|
+
if (typeof url !== 'string' || url.trim() === '')
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(url, 'https://relative.invalid');
|
|
16
|
+
return SAFE_PROTOCOLS.has(parsed.protocol) ? url : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import {
|
|
2
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
3
|
+
/**
|
|
4
|
+
* tailwind-merge only knows Tailwind's default theme, so it reads the preset's
|
|
5
|
+
* custom font sizes (`text-h1`..`text-h6`, from `fontSize` in
|
|
6
|
+
* @classic-homes/theme-tailwind-preset) as text colours and drops a real colour
|
|
7
|
+
* such as `text-foreground` beside them. Register them as font sizes.
|
|
8
|
+
*/
|
|
9
|
+
const twMerge = extendTailwindMerge({
|
|
10
|
+
extend: {
|
|
11
|
+
classGroups: {
|
|
12
|
+
'font-size': [{ text: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
3
16
|
/**
|
|
4
17
|
* Utility function to merge Tailwind CSS classes
|
|
5
18
|
* Uses clsx for conditional classes and tailwind-merge to handle conflicts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/theme-svelte",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.2",
|
|
4
4
|
"description": "Svelte components for the Classic theme system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/lib/index.js",
|
|
@@ -15,14 +15,15 @@
|
|
|
15
15
|
"types": "./dist/lib/components/notifications/index.d.ts",
|
|
16
16
|
"svelte": "./dist/lib/components/notifications/index.js",
|
|
17
17
|
"default": "./dist/lib/components/notifications/index.js"
|
|
18
|
-
}
|
|
19
|
-
"./styles.css": "./dist/styles.css"
|
|
18
|
+
}
|
|
20
19
|
},
|
|
21
20
|
"sideEffects": [
|
|
22
21
|
"*.css"
|
|
23
22
|
],
|
|
24
23
|
"files": [
|
|
25
|
-
"dist"
|
|
24
|
+
"dist",
|
|
25
|
+
"!dist/__tests__",
|
|
26
|
+
"!dist/**/*.test.*"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
28
29
|
"build": "svelte-package -i src",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import FileUpload from '../lib/components/FileUpload.svelte';
|
|
4
|
-
describe('FileUpload accepted-types label', () => {
|
|
5
|
-
it('derives a friendly label from MIME types instead of showing raw values', () => {
|
|
6
|
-
render(FileUpload, {
|
|
7
|
-
props: {
|
|
8
|
-
label: 'Upload files',
|
|
9
|
-
acceptedTypes: [
|
|
10
|
-
'image/jpeg',
|
|
11
|
-
'image/png',
|
|
12
|
-
'application/pdf',
|
|
13
|
-
'video/mp4',
|
|
14
|
-
'video/quicktime',
|
|
15
|
-
],
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
expect(screen.getByText(/Images, PDF, Videos \(max/)).toBeInTheDocument();
|
|
19
|
-
// Raw MIME types should not be shown
|
|
20
|
-
expect(screen.queryByText(/image\/jpeg/)).not.toBeInTheDocument();
|
|
21
|
-
});
|
|
22
|
-
it('uses the default accepted types (Images, PDF) when none are provided', () => {
|
|
23
|
-
render(FileUpload, { props: { label: 'Upload files' } });
|
|
24
|
-
expect(screen.getByText(/^Images, PDF \(max/)).toBeInTheDocument();
|
|
25
|
-
});
|
|
26
|
-
it('respects an explicit typesLabel override', () => {
|
|
27
|
-
render(FileUpload, {
|
|
28
|
-
props: {
|
|
29
|
-
label: 'Upload files',
|
|
30
|
-
acceptedTypes: ['image/png', 'video/mp4'],
|
|
31
|
-
typesLabel: 'Photos and clips',
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
expect(screen.getByText(/^Photos and clips \(max/)).toBeInTheDocument();
|
|
35
|
-
});
|
|
36
|
-
it('falls back to the uppercased subtype for unmapped types', () => {
|
|
37
|
-
render(FileUpload, {
|
|
38
|
-
props: {
|
|
39
|
-
label: 'Upload files',
|
|
40
|
-
acceptedTypes: ['application/zip'],
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
expect(screen.getByText(/^ZIP \(max/)).toBeInTheDocument();
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { screen } from '@testing-library/svelte';
|
|
3
|
-
import { axe } from 'vitest-axe';
|
|
4
|
-
describe('Alert accessibility', () => {
|
|
5
|
-
it('should have no axe violations', async () => {
|
|
6
|
-
const container = document.createElement('div');
|
|
7
|
-
document.body.appendChild(container);
|
|
8
|
-
container.innerHTML = `
|
|
9
|
-
<div role="alert" aria-live="polite" aria-atomic="true" class="relative w-full rounded-lg border p-4">
|
|
10
|
-
<h5 class="mb-1 font-medium leading-none tracking-tight">Alert Title</h5>
|
|
11
|
-
<p class="text-sm">Alert description text</p>
|
|
12
|
-
</div>
|
|
13
|
-
`;
|
|
14
|
-
const results = await axe(container);
|
|
15
|
-
expect(results).toHaveNoViolations();
|
|
16
|
-
document.body.removeChild(container);
|
|
17
|
-
});
|
|
18
|
-
it('should have no axe violations with different variants', async () => {
|
|
19
|
-
const container = document.createElement('div');
|
|
20
|
-
document.body.appendChild(container);
|
|
21
|
-
// Test default variant
|
|
22
|
-
container.innerHTML = `
|
|
23
|
-
<div role="alert" aria-live="polite" aria-atomic="true" class="relative w-full rounded-lg border p-4 bg-gray-50">
|
|
24
|
-
<h5 class="mb-1 font-medium leading-none tracking-tight">Default Alert</h5>
|
|
25
|
-
</div>
|
|
26
|
-
`;
|
|
27
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
28
|
-
// Test destructive variant
|
|
29
|
-
container.innerHTML = `
|
|
30
|
-
<div role="alert" aria-live="assertive" aria-atomic="true" class="relative w-full rounded-lg border p-4 bg-red-50">
|
|
31
|
-
<h5 class="mb-1 font-medium leading-none tracking-tight">Destructive Alert</h5>
|
|
32
|
-
</div>
|
|
33
|
-
`;
|
|
34
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
35
|
-
document.body.removeChild(container);
|
|
36
|
-
});
|
|
37
|
-
it('should have role="alert"', () => {
|
|
38
|
-
const container = document.createElement('div');
|
|
39
|
-
document.body.appendChild(container);
|
|
40
|
-
container.innerHTML = `
|
|
41
|
-
<div role="alert" aria-live="polite" aria-atomic="true" class="relative w-full rounded-lg border p-4">
|
|
42
|
-
<h5 class="mb-1 font-medium leading-none tracking-tight">Alert Title</h5>
|
|
43
|
-
</div>
|
|
44
|
-
`;
|
|
45
|
-
const alert = screen.getByRole('alert');
|
|
46
|
-
expect(alert).toBeInTheDocument();
|
|
47
|
-
document.body.removeChild(container);
|
|
48
|
-
});
|
|
49
|
-
it('should have aria-atomic="true"', () => {
|
|
50
|
-
const container = document.createElement('div');
|
|
51
|
-
document.body.appendChild(container);
|
|
52
|
-
container.innerHTML = `
|
|
53
|
-
<div role="alert" aria-live="polite" aria-atomic="true" class="relative w-full rounded-lg border p-4">
|
|
54
|
-
<h5 class="mb-1 font-medium leading-none tracking-tight">Alert Title</h5>
|
|
55
|
-
</div>
|
|
56
|
-
`;
|
|
57
|
-
const alert = screen.getByRole('alert');
|
|
58
|
-
expect(alert).toHaveAttribute('aria-atomic', 'true');
|
|
59
|
-
document.body.removeChild(container);
|
|
60
|
-
});
|
|
61
|
-
it('should display title text', () => {
|
|
62
|
-
const container = document.createElement('div');
|
|
63
|
-
document.body.appendChild(container);
|
|
64
|
-
container.innerHTML = `
|
|
65
|
-
<div role="alert" aria-live="polite" aria-atomic="true" class="relative w-full rounded-lg border p-4">
|
|
66
|
-
<h5 class="mb-1 font-medium leading-none tracking-tight">Important Alert</h5>
|
|
67
|
-
</div>
|
|
68
|
-
`;
|
|
69
|
-
expect(screen.getByText('Important Alert')).toBeInTheDocument();
|
|
70
|
-
document.body.removeChild(container);
|
|
71
|
-
});
|
|
72
|
-
it('should display description text', () => {
|
|
73
|
-
const container = document.createElement('div');
|
|
74
|
-
document.body.appendChild(container);
|
|
75
|
-
container.innerHTML = `
|
|
76
|
-
<div role="alert" aria-live="polite" aria-atomic="true" class="relative w-full rounded-lg border p-4">
|
|
77
|
-
<h5 class="mb-1 font-medium leading-none tracking-tight">Alert</h5>
|
|
78
|
-
<p class="text-sm">This is the description</p>
|
|
79
|
-
</div>
|
|
80
|
-
`;
|
|
81
|
-
expect(screen.getByText('This is the description')).toBeInTheDocument();
|
|
82
|
-
document.body.removeChild(container);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
@@ -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
|
-
});
|