@svgrid/grid 1.2.21 → 1.2.23
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/GridFooter.svelte +34 -20
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/SvAutoComplete.svelte +100 -0
- package/dist/SvAutoComplete.svelte.d.ts +16 -0
- package/dist/SvButton.svelte +97 -0
- package/dist/SvButton.svelte.d.ts +23 -0
- package/dist/SvCalendar.svelte +669 -0
- package/dist/SvCalendar.svelte.d.ts +70 -0
- package/dist/SvCheckBox.svelte +81 -0
- package/dist/SvCheckBox.svelte.d.ts +19 -0
- package/dist/SvColorInput.svelte +187 -0
- package/dist/SvColorInput.svelte.d.ts +15 -0
- package/dist/SvComboBox.svelte +130 -0
- package/dist/SvComboBox.svelte.d.ts +15 -0
- package/dist/SvCountryInput.svelte +123 -0
- package/dist/SvCountryInput.svelte.d.ts +14 -0
- package/dist/SvDateTimePicker.svelte +322 -0
- package/dist/SvDateTimePicker.svelte.d.ts +37 -0
- package/dist/SvDropDownList.svelte +143 -0
- package/dist/SvDropDownList.svelte.d.ts +15 -0
- package/dist/SvForm.svelte +147 -0
- package/dist/SvForm.svelte.d.ts +29 -0
- package/dist/SvGauge.svelte +99 -0
- package/dist/SvGauge.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +16 -2
- package/dist/SvGrid.controller.svelte.js +63 -8
- package/dist/SvGrid.css +84 -31
- package/dist/SvGrid.helpers.d.ts +4 -0
- package/dist/SvGrid.helpers.js +40 -12
- package/dist/SvGrid.svelte +84 -5
- package/dist/SvGrid.types.d.ts +36 -0
- package/dist/SvGridDropdown.svelte +16 -30
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvListBox.svelte +122 -0
- package/dist/SvListBox.svelte.d.ts +19 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvMaskedInput.svelte.d.ts +15 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +22 -0
- package/dist/SvPasswordInput.svelte +110 -0
- package/dist/SvPasswordInput.svelte.d.ts +22 -0
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvPhoneInput.svelte.d.ts +19 -0
- package/dist/SvRadioGroup.svelte +108 -0
- package/dist/SvRadioGroup.svelte.d.ts +23 -0
- package/dist/SvRating.svelte +111 -0
- package/dist/SvRating.svelte.d.ts +20 -0
- package/dist/SvRepeatButton.svelte +89 -0
- package/dist/SvRepeatButton.svelte.d.ts +21 -0
- package/dist/SvSlider.svelte +182 -0
- package/dist/SvSlider.svelte.d.ts +25 -0
- package/dist/SvSwitchButton.svelte +94 -0
- package/dist/SvSwitchButton.svelte.d.ts +19 -0
- package/dist/SvTabs.svelte +122 -0
- package/dist/SvTabs.svelte.d.ts +26 -0
- package/dist/SvTagsInput.svelte +81 -0
- package/dist/SvTagsInput.svelte.d.ts +19 -0
- package/dist/SvTimePicker.svelte +359 -0
- package/dist/SvTimePicker.svelte.d.ts +27 -0
- package/dist/SvToggleButton.svelte +57 -0
- package/dist/SvToggleButton.svelte.d.ts +16 -0
- package/dist/SvTree.svelte +210 -0
- package/dist/SvTree.svelte.d.ts +29 -0
- package/dist/build-api.js +78 -1
- package/dist/cdn/svgrid.js +10022 -5904
- package/dist/cdn/svgrid.svelte-external.js +8531 -4429
- package/dist/cell-render.d.ts +4 -0
- package/dist/cell-render.js +35 -1
- package/dist/clipboard.js +19 -1
- package/dist/collaboration.d.ts +2 -0
- package/dist/collaboration.js +4 -0
- package/dist/core.d.ts +22 -1
- package/dist/countries.d.ts +15 -0
- package/dist/countries.js +61 -0
- package/dist/datetime/date-core.d.ts +88 -0
- package/dist/datetime/date-core.js +165 -0
- package/dist/datetime/date-format.d.ts +39 -0
- package/dist/datetime/date-format.js +371 -0
- package/dist/datetime/date-restrict.d.ts +23 -0
- package/dist/datetime/date-restrict.js +50 -0
- package/dist/datetime/date-selection.d.ts +25 -0
- package/dist/datetime/date-selection.js +94 -0
- package/dist/datetime/mask.d.ts +21 -0
- package/dist/datetime/mask.js +90 -0
- package/dist/editing.d.ts +1 -1
- package/dist/editing.js +7 -0
- package/dist/editors/cell-editors.d.ts +2 -2
- package/dist/editors/cell-editors.js +3 -1
- package/dist/export-format.d.ts +178 -0
- package/dist/export-format.js +419 -0
- package/dist/hyperformula-adapter.d.ts +4 -0
- package/dist/hyperformula-adapter.js +15 -6
- package/dist/index.d.ts +36 -0
- package/dist/index.js +41 -0
- package/dist/list-option.d.ts +10 -0
- package/dist/list-option.js +7 -0
- package/dist/menus.js +18 -2
- package/dist/popover.d.ts +48 -0
- package/dist/popover.js +65 -0
- package/dist/row-resize.d.ts +4 -4
- package/dist/row-resize.js +8 -5
- package/dist/selection.d.ts +6 -0
- package/dist/selection.js +31 -5
- package/dist/server-data-source.d.ts +43 -0
- package/dist/server-data-source.js +100 -0
- package/dist/sparkline.js +8 -4
- package/dist/svgrid-wrapper.types.d.ts +26 -1
- package/package.json +6 -1
- package/src/GridFooter.svelte +34 -20
- package/src/SvAutoComplete.svelte +100 -0
- package/src/SvButton.svelte +97 -0
- package/src/SvCalendar.svelte +669 -0
- package/src/SvCalendar.test.ts +211 -0
- package/src/SvCheckBox.svelte +81 -0
- package/src/SvColorInput.svelte +187 -0
- package/src/SvComboBox.svelte +130 -0
- package/src/SvCountryInput.svelte +123 -0
- package/src/SvDateTimePicker.svelte +322 -0
- package/src/SvDateTimePicker.test.ts +136 -0
- package/src/SvDropDownList.svelte +143 -0
- package/src/SvForm.svelte +147 -0
- package/src/SvGauge.svelte +99 -0
- package/src/SvGrid.controller.svelte.ts +74 -8
- package/src/SvGrid.css +84 -31
- package/src/SvGrid.helpers.ts +41 -12
- package/src/SvGrid.svelte +84 -5
- package/src/SvGrid.types.ts +33 -0
- package/src/SvGridDropdown.svelte +16 -30
- package/src/SvListBox.svelte +122 -0
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +110 -0
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +108 -0
- package/src/SvRating.svelte +111 -0
- package/src/SvRepeatButton.svelte +89 -0
- package/src/SvSlider.svelte +182 -0
- package/src/SvSwitchButton.svelte +94 -0
- package/src/SvTabs.svelte +122 -0
- package/src/SvTagsInput.svelte +81 -0
- package/src/SvTimePicker.svelte +359 -0
- package/src/SvTimePicker.test.ts +98 -0
- package/src/SvToggleButton.svelte +57 -0
- package/src/SvTree.svelte +210 -0
- package/src/build-api.ts +115 -0
- package/src/cell-render.test.ts +38 -0
- package/src/cell-render.ts +43 -0
- package/src/clipboard.test.ts +18 -0
- package/src/clipboard.ts +24 -1
- package/src/collaboration.test.ts +30 -0
- package/src/collaboration.ts +6 -0
- package/src/core.ts +27 -3
- package/src/countries.ts +71 -0
- package/src/datetime/date-core.test.ts +217 -0
- package/src/datetime/date-core.ts +204 -0
- package/src/datetime/date-format.test.ts +121 -0
- package/src/datetime/date-format.ts +317 -0
- package/src/datetime/date-restrict.ts +60 -0
- package/src/datetime/date-selection.test.ts +129 -0
- package/src/datetime/date-selection.ts +137 -0
- package/src/datetime/mask.test.ts +36 -0
- package/src/datetime/mask.ts +84 -0
- package/src/editing.test.ts +22 -0
- package/src/editing.ts +7 -1
- package/src/editors/cell-editors.ts +7 -1
- package/src/export-data-api.test.ts +126 -0
- package/src/export-format.test.ts +107 -0
- package/src/export-format.ts +598 -0
- package/src/hyperformula-adapter.test.ts +35 -1
- package/src/hyperformula-adapter.ts +18 -6
- package/src/index.ts +71 -0
- package/src/list-option.ts +15 -0
- package/src/menus.ts +21 -2
- package/src/popover.ts +79 -0
- package/src/row-resize.test.ts +25 -0
- package/src/row-resize.ts +8 -5
- package/src/selection.test.ts +36 -4
- package/src/selection.ts +30 -9
- package/src/server-data-source.test.ts +201 -0
- package/src/server-data-source.ts +148 -0
- package/src/sparkline.test.ts +9 -0
- package/src/sparkline.ts +9 -4
- package/src/svgrid-wrapper.types.ts +28 -1
- package/src/svgrid.behavior.test.ts +27 -0
- package/src/svgrid.new-features.wrapper.test.ts +27 -1
- package/src/ui-buttons.test.ts +105 -0
- package/src/ui-composite.test.ts +89 -0
- package/src/ui-inputs.test.ts +92 -0
- package/src/ui-range.test.ts +61 -0
- package/src/ui-selection.test.ts +106 -0
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* It's headless and framework-agnostic on purpose: wire `setSort` /
|
|
9
9
|
* `setFilter` / `setPage` to the grid's controlled callbacks, and render the
|
|
10
10
|
* grid from the `{ rows, total, loading }` it hands you. See the demo.
|
|
11
|
+
*
|
|
12
|
+
* The write side (`createRow` / `updateRow` / `deleteRow`) is optional: a
|
|
13
|
+
* source that only implements `getRows` stays a pure read model, and the
|
|
14
|
+
* matching controller methods throw a clear error if called. Mutations are
|
|
15
|
+
* non-optimistic for now - the grid reflects a change only after the
|
|
16
|
+
* follow-up re-fetch of the current page lands.
|
|
11
17
|
*/
|
|
12
18
|
export type ServerSortModel = Array<{ id: string; desc: boolean }>
|
|
13
19
|
|
|
@@ -44,12 +50,23 @@ export type ServerResult<TData> = {
|
|
|
44
50
|
|
|
45
51
|
export type ServerDataSource<TData> = {
|
|
46
52
|
getRows(request: ServerRequest): Promise<ServerResult<TData>>
|
|
53
|
+
/**
|
|
54
|
+
* Optional write side. Implement whichever your backend supports; the
|
|
55
|
+
* controller exposes matching `createRow` / `updateRow` / `deleteRow`
|
|
56
|
+
* methods that call through and then `refresh()` the current page.
|
|
57
|
+
* Calling a controller method whose source counterpart is missing throws.
|
|
58
|
+
*/
|
|
59
|
+
createRow?(input: Partial<TData>): Promise<TData>
|
|
60
|
+
updateRow?(id: string, patch: Partial<TData>): Promise<TData>
|
|
61
|
+
deleteRow?(id: string): Promise<void>
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
export type ServerState<TData> = {
|
|
50
65
|
rows: ReadonlyArray<TData>
|
|
51
66
|
total: number
|
|
52
67
|
loading: boolean
|
|
68
|
+
/** True while a create / update / delete mutation is in flight. */
|
|
69
|
+
saving: boolean
|
|
53
70
|
error: unknown
|
|
54
71
|
pageIndex: number
|
|
55
72
|
pageSize: number
|
|
@@ -65,6 +82,23 @@ export type ServerController<TData> = {
|
|
|
65
82
|
setFilter(filterModel: ServerFilterModel): void
|
|
66
83
|
setPage(pageIndex: number): void
|
|
67
84
|
setPageSize(pageSize: number): void
|
|
85
|
+
/**
|
|
86
|
+
* Create a row through the source, then refresh the current page. Resolves
|
|
87
|
+
* with the created row. Rejects if the source has no `createRow` (or if the
|
|
88
|
+
* create itself fails - the read state is left untouched on failure).
|
|
89
|
+
*/
|
|
90
|
+
createRow(input: Partial<TData>): Promise<TData>
|
|
91
|
+
/**
|
|
92
|
+
* Update a row by id through the source. Non-optimistic: refreshes the page.
|
|
93
|
+
* Optimistic (see `optimistic` + `getRowId` options): patches the local row
|
|
94
|
+
* immediately, then reconciles with the server result, rolling back on error.
|
|
95
|
+
*/
|
|
96
|
+
updateRow(id: string, patch: Partial<TData>): Promise<TData>
|
|
97
|
+
/**
|
|
98
|
+
* Delete a row by id through the source. Non-optimistic: refreshes the page.
|
|
99
|
+
* Optimistic: removes the local row immediately, restoring it on error.
|
|
100
|
+
*/
|
|
101
|
+
deleteRow(id: string): Promise<void>
|
|
68
102
|
getState(): ServerState<TData>
|
|
69
103
|
/** Stop accepting in-flight responses (call on unmount). */
|
|
70
104
|
dispose(): void
|
|
@@ -74,6 +108,15 @@ export type ServerControllerOptions<TData> = {
|
|
|
74
108
|
pageSize?: number
|
|
75
109
|
/** Called whenever any of `rows` / `total` / `loading` / page changes. */
|
|
76
110
|
onChange: (state: ServerState<TData>) => void
|
|
111
|
+
/**
|
|
112
|
+
* Apply `updateRow` / `deleteRow` to the local rows immediately (before the
|
|
113
|
+
* server confirms) and roll back on error - so edits feel instant and no
|
|
114
|
+
* refetch is needed. Requires `getRowId` to locate rows; ignored without it.
|
|
115
|
+
* A subsequent `refresh()` reconciles ordering/filtering. Default false.
|
|
116
|
+
*/
|
|
117
|
+
optimistic?: boolean
|
|
118
|
+
/** Resolve a row's stable id, so optimistic update/delete can find it in `rows`. */
|
|
119
|
+
getRowId?: (row: TData) => string
|
|
77
120
|
}
|
|
78
121
|
|
|
79
122
|
export function createServerDataSource<TData>(
|
|
@@ -84,6 +127,7 @@ export function createServerDataSource<TData>(
|
|
|
84
127
|
rows: [],
|
|
85
128
|
total: 0,
|
|
86
129
|
loading: false,
|
|
130
|
+
saving: false,
|
|
87
131
|
error: null,
|
|
88
132
|
pageIndex: 0,
|
|
89
133
|
pageSize: options.pageSize ?? 50,
|
|
@@ -132,8 +176,105 @@ export function createServerDataSource<TData>(
|
|
|
132
176
|
}
|
|
133
177
|
}
|
|
134
178
|
|
|
179
|
+
// Shared mutation lifecycle: flip `saving`, run the write, refresh the
|
|
180
|
+
// current page on success, and always clear `saving`. A missing source
|
|
181
|
+
// method (or a disposed controller) rejects before anything is emitted.
|
|
182
|
+
function mutate<T>(name: string, thunk: (() => Promise<T>) | null): Promise<T> {
|
|
183
|
+
if (disposed) {
|
|
184
|
+
return Promise.reject(new Error('createServerDataSource: controller is disposed'))
|
|
185
|
+
}
|
|
186
|
+
if (!thunk) {
|
|
187
|
+
return Promise.reject(
|
|
188
|
+
new Error(`createServerDataSource: the datasource does not implement ${name}()`),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
state.saving = true
|
|
192
|
+
emit()
|
|
193
|
+
return (async () => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await thunk()
|
|
196
|
+
await fetchPage()
|
|
197
|
+
return result
|
|
198
|
+
} finally {
|
|
199
|
+
state.saving = false
|
|
200
|
+
emit()
|
|
201
|
+
}
|
|
202
|
+
})()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const optimistic = !!options.optimistic && !!options.getRowId
|
|
206
|
+
const getRowId = options.getRowId
|
|
207
|
+
|
|
208
|
+
// Optimistic update: patch the local row, reconcile with the server result,
|
|
209
|
+
// roll back on error. Falls back to the plain refresh path when the row
|
|
210
|
+
// isn't on the current page (nothing local to update).
|
|
211
|
+
async function optimisticUpdate(
|
|
212
|
+
id: string,
|
|
213
|
+
patch: Partial<TData>,
|
|
214
|
+
fn: (id: string, patch: Partial<TData>) => Promise<TData>,
|
|
215
|
+
): Promise<TData> {
|
|
216
|
+
if (disposed) throw new Error('createServerDataSource: controller is disposed')
|
|
217
|
+
const prevRows = state.rows
|
|
218
|
+
const idx = prevRows.findIndex((r) => getRowId!(r) === id)
|
|
219
|
+
if (idx < 0) return mutate('updateRow', () => fn(id, patch))
|
|
220
|
+
|
|
221
|
+
state.rows = [...prevRows.slice(0, idx), { ...prevRows[idx]!, ...patch }, ...prevRows.slice(idx + 1)]
|
|
222
|
+
state.saving = true
|
|
223
|
+
emit()
|
|
224
|
+
try {
|
|
225
|
+
const result = await fn(id, patch)
|
|
226
|
+
state.rows = state.rows.map((r) => (getRowId!(r) === id ? result : r))
|
|
227
|
+
return result
|
|
228
|
+
} catch (err) {
|
|
229
|
+
state.rows = prevRows
|
|
230
|
+
throw err
|
|
231
|
+
} finally {
|
|
232
|
+
state.saving = false
|
|
233
|
+
emit()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Optimistic delete: drop the local row + decrement total, restore on error.
|
|
238
|
+
async function optimisticDelete(
|
|
239
|
+
id: string,
|
|
240
|
+
fn: (id: string) => Promise<void>,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
if (disposed) throw new Error('createServerDataSource: controller is disposed')
|
|
243
|
+
const prevRows = state.rows
|
|
244
|
+
const prevTotal = state.total
|
|
245
|
+
const next = prevRows.filter((r) => getRowId!(r) !== id)
|
|
246
|
+
if (next.length === prevRows.length) return mutate('deleteRow', () => fn(id))
|
|
247
|
+
|
|
248
|
+
state.rows = next
|
|
249
|
+
state.total = Math.max(0, prevTotal - (prevRows.length - next.length))
|
|
250
|
+
state.saving = true
|
|
251
|
+
emit()
|
|
252
|
+
try {
|
|
253
|
+
await fn(id)
|
|
254
|
+
} catch (err) {
|
|
255
|
+
state.rows = prevRows
|
|
256
|
+
state.total = prevTotal
|
|
257
|
+
throw err
|
|
258
|
+
} finally {
|
|
259
|
+
state.saving = false
|
|
260
|
+
emit()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
135
264
|
return {
|
|
136
265
|
refresh: fetchPage,
|
|
266
|
+
createRow: (input) =>
|
|
267
|
+
mutate('createRow', source.createRow ? () => source.createRow!(input) : null),
|
|
268
|
+
updateRow: (id, patch) => {
|
|
269
|
+
if (!source.updateRow) return mutate('updateRow', null)
|
|
270
|
+
const fn = source.updateRow
|
|
271
|
+
return optimistic ? optimisticUpdate(id, patch, fn) : mutate('updateRow', () => fn(id, patch))
|
|
272
|
+
},
|
|
273
|
+
deleteRow: (id) => {
|
|
274
|
+
if (!source.deleteRow) return mutate('deleteRow', null)
|
|
275
|
+
const fn = source.deleteRow
|
|
276
|
+
return optimistic ? optimisticDelete(id, fn) : mutate('deleteRow', () => fn(id))
|
|
277
|
+
},
|
|
137
278
|
setSort(sortModel) {
|
|
138
279
|
state.sortModel = sortModel
|
|
139
280
|
state.pageIndex = 0
|
|
@@ -158,6 +299,13 @@ export function createServerDataSource<TData>(
|
|
|
158
299
|
getState: () => ({ ...state }),
|
|
159
300
|
dispose() {
|
|
160
301
|
disposed = true
|
|
302
|
+
// An in-flight fetch's resolution short-circuits on `disposed`, so it
|
|
303
|
+
// never clears `loading`. Clear it here (and emit) so a disposed
|
|
304
|
+
// controller doesn't report a permanent loading state.
|
|
305
|
+
if (state.loading) {
|
|
306
|
+
state.loading = false
|
|
307
|
+
emit()
|
|
308
|
+
}
|
|
161
309
|
},
|
|
162
310
|
}
|
|
163
311
|
}
|
package/src/sparkline.test.ts
CHANGED
|
@@ -34,6 +34,15 @@ describe('buildSparkline', () => {
|
|
|
34
34
|
expect(g.areaPath.endsWith('Z')).toBe(true)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
+
it('renders a visible mark for a single data point (#84)', () => {
|
|
38
|
+
// A lone point used to produce just "M x,y" (a moveto), which draws
|
|
39
|
+
// nothing. The fix centres it and emits a short segment.
|
|
40
|
+
const g = buildSparkline([5], { type: 'line', width: 100, height: 20 })!
|
|
41
|
+
expect(g.linePath).not.toBe('')
|
|
42
|
+
expect(g.linePath).toMatch(/M[\d.]+,[\d.]+ L[\d.]+,[\d.]+/)
|
|
43
|
+
expect(g.lastPoint).not.toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
37
46
|
it('emits one bar per value and flags negatives', () => {
|
|
38
47
|
const g = buildSparkline([-2, 4, -1], { type: 'bar' })!
|
|
39
48
|
expect(g.bars).toHaveLength(3)
|
package/src/sparkline.ts
CHANGED
|
@@ -137,12 +137,17 @@ export function buildSparkline(
|
|
|
137
137
|
const n = values.length
|
|
138
138
|
const stepX = n > 1 ? innerW / (n - 1) : 0
|
|
139
139
|
const pts = values.map((v, i) => ({
|
|
140
|
-
|
|
140
|
+
// A single point has no span to walk, so centre it rather than pinning it
|
|
141
|
+
// to the left edge.
|
|
142
|
+
x: n === 1 ? pad + innerW / 2 : pad + i * stepX,
|
|
141
143
|
y: pad + innerH - ((v - min) / span) * innerH,
|
|
142
144
|
}))
|
|
143
|
-
const linePath =
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
const linePath =
|
|
146
|
+
n === 1
|
|
147
|
+
? // One point produces only a moveto, which draws nothing. Emit a tiny
|
|
148
|
+
// horizontal segment so a single value still renders a visible mark.
|
|
149
|
+
`M${round(pts[0]!.x - 1)},${round(pts[0]!.y)} L${round(pts[0]!.x + 1)},${round(pts[0]!.y)}`
|
|
150
|
+
: pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ')
|
|
146
151
|
const first = pts[0]!
|
|
147
152
|
const last = pts[pts.length - 1]!
|
|
148
153
|
const baseY = pad + innerH
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ColumnDef, RowData, SvGridOptions, TableFeatures } from './core'
|
|
1
|
+
import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures } from './core'
|
|
2
|
+
import type { GridExportOptions, GridClipboardOptions } from './export-format'
|
|
2
3
|
|
|
3
4
|
export type SvGridFilterOperator =
|
|
4
5
|
| 'contains'
|
|
@@ -197,8 +198,34 @@ export type SvGridApi<
|
|
|
197
198
|
field?: string
|
|
198
199
|
header: string
|
|
199
200
|
visible: boolean
|
|
201
|
+
/** The column's `format` config, when set. Lets an exporter reproduce
|
|
202
|
+
* the on-screen display value (currency, date pattern, etc.). */
|
|
203
|
+
format?: CellFormatConfig
|
|
204
|
+
/** Effective horizontal alignment ('left' | 'center' | 'right'). */
|
|
205
|
+
align?: 'left' | 'center' | 'right'
|
|
200
206
|
}>
|
|
201
207
|
|
|
208
|
+
// ----- Free data export (CSV / TSV / JSON + clipboard) -----
|
|
209
|
+
/**
|
|
210
|
+
* Export the grid to a **CSV** file. Free in the community grid; the
|
|
211
|
+
* richer Excel / PDF / styled formats live in @svgrid/enterprise. Values
|
|
212
|
+
* are formatted as shown on screen (pass `rawValues: true` for raw). Rows
|
|
213
|
+
* default to the current view (`rows: 'selected' | 'all'` to change).
|
|
214
|
+
* Resolves with the serialized text; pass `download: false` to skip the
|
|
215
|
+
* browser download and just get the string.
|
|
216
|
+
*/
|
|
217
|
+
exportCsv(options?: GridExportOptions): Promise<string>
|
|
218
|
+
/** Export the grid to a **TSV** file (tab-separated). See `exportCsv`. */
|
|
219
|
+
exportTsv(options?: GridExportOptions): Promise<string>
|
|
220
|
+
/** Export the grid to a **JSON** file (array of `{ field: value }`). */
|
|
221
|
+
exportJson(options?: GridExportOptions): Promise<string>
|
|
222
|
+
/**
|
|
223
|
+
* Copy the grid to the system clipboard. `format: 'tsv'` (default) pastes
|
|
224
|
+
* straight into Excel / Sheets; `'csv'` / `'markdown'` also supported.
|
|
225
|
+
* Resolves with the copied text.
|
|
226
|
+
*/
|
|
227
|
+
copyToClipboard(options?: GridClipboardOptions): Promise<string>
|
|
228
|
+
|
|
202
229
|
/** Clear every checked row. Emits `onRowSelectionChange({}, [])`. */
|
|
203
230
|
clearRowSelection(): void
|
|
204
231
|
|
|
@@ -351,6 +351,33 @@ describe('SvGrid - capability shortcuts', () => {
|
|
|
351
351
|
}
|
|
352
352
|
})
|
|
353
353
|
|
|
354
|
+
it('paginationPosition mounts + gates the bottom pager', async () => {
|
|
355
|
+
// The top pager is gated on measurement (jsdom does not measure), so it's not
|
|
356
|
+
// asserted here; the visible top pager is covered by the browser/e2e check.
|
|
357
|
+
// This guards the mount path (a top footer rendered pre-measure used to crash).
|
|
358
|
+
for (const paginationPosition of ['top', 'both', 'bottom'] as const) {
|
|
359
|
+
const g = await mountGrid({ showPagination: true, paginationPosition })
|
|
360
|
+
try {
|
|
361
|
+
await tick()
|
|
362
|
+
expect(g.api).toBeTruthy()
|
|
363
|
+
const bottoms = [...g.target.querySelectorAll('.sv-grid-pagination')].filter((el) => !el.classList.contains('sv-grid-pagination-top'))
|
|
364
|
+
expect(bottoms.length).toBe(paginationPosition === 'top' ? 0 : 1) // no bottom pager when position is 'top'
|
|
365
|
+
} finally {
|
|
366
|
+
g.destroy()
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('pageSizeOptions is accepted by the pager', async () => {
|
|
372
|
+
const g = await mountGrid({ showPagination: true, pageSizeOptions: [5, 15, 45] })
|
|
373
|
+
try {
|
|
374
|
+
await tick()
|
|
375
|
+
expect(g.target.querySelector('.sv-grid-pagination')).toBeTruthy()
|
|
376
|
+
} finally {
|
|
377
|
+
g.destroy()
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
|
|
354
381
|
it('editable={false} blocks inline editing entry points', async () => {
|
|
355
382
|
const { api, target, destroy } = await mountGrid({ editable: false })
|
|
356
383
|
try {
|
|
@@ -75,11 +75,37 @@ describe('SvGrid wrapper - cellClass + rowClass', () => {
|
|
|
75
75
|
|
|
76
76
|
it('computes the cell-level class per column and threads it onto the <td>', () => {
|
|
77
77
|
expect(source).toMatch(/function computeCellClass/)
|
|
78
|
-
expect(source).toMatch(/userCellClass = computeCellClass\(row
|
|
78
|
+
expect(source).toMatch(/userCellClass = computeCellClass\(\s*row,\s*rendered\.column,?\s*\)/)
|
|
79
79
|
expect(source).toMatch(/class=\{`sv-grid-cell \$\{userCellClass\}`\}/)
|
|
80
80
|
})
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
+
describe('SvGrid wrapper - declarative cell validation (validate hook)', () => {
|
|
84
|
+
it('reads the column validate hook with the documented return union', () => {
|
|
85
|
+
expect(source).toMatch(/column\.columnDef\.validate/)
|
|
86
|
+
expect(source).toMatch(/=>\s*string\s*\|\s*boolean\s*\|\s*null\s*\|\s*undefined/)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('computes per-cell validity in the render layer (runs for every rendered cell)', () => {
|
|
90
|
+
expect(source).toMatch(/function computeCellValidity/)
|
|
91
|
+
expect(source).toMatch(/columnDef\.validate/)
|
|
92
|
+
// Valid when null / undefined / true; invalid on false or a message.
|
|
93
|
+
expect(source).toMatch(/if \(out == null \|\| out === true\)/)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('threads the invalid class + message tooltip onto both <td> render paths', () => {
|
|
97
|
+
const hits = source.match(/class:sv-grid-cell-invalid=\{cellValidity\.invalid\}/g) ?? []
|
|
98
|
+
expect(hits.length).toBeGreaterThanOrEqual(2)
|
|
99
|
+
// Message wins over the plain column tooltip when the cell is invalid.
|
|
100
|
+
expect(source).toMatch(/cellValidity\.invalid && cellValidity\.message/)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('forwards computeCellValidity through the controller so the wrapper can read it', () => {
|
|
104
|
+
expect(source).toMatch(/get computeCellValidity\(\)\s*\{\s*return computeCellValidity;/)
|
|
105
|
+
expect(source).toMatch(/computeCellValidity\s*=\s*\$derived\(ctrl\.computeCellValidity\)/)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
83
109
|
describe('SvGrid wrapper - column-layout API', () => {
|
|
84
110
|
it('declares setColumnWidth + getColumnWidths in the public type', () => {
|
|
85
111
|
expect(types).toContain('setColumnWidth(columnId: string, width: number): void')
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group B (buttons & toggles) component tests. Each asserts the emitted value /
|
|
3
|
+
* ARIA state a real interaction produces.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
7
|
+
import SvToggleButton from './SvToggleButton.svelte'
|
|
8
|
+
import SvSwitchButton from './SvSwitchButton.svelte'
|
|
9
|
+
import SvCheckBox from './SvCheckBox.svelte'
|
|
10
|
+
import SvRadioGroup from './SvRadioGroup.svelte'
|
|
11
|
+
import SvRating from './SvRating.svelte'
|
|
12
|
+
|
|
13
|
+
function mnt(Comp: any, props: Record<string, unknown>) {
|
|
14
|
+
const target = document.createElement('div')
|
|
15
|
+
document.body.appendChild(target)
|
|
16
|
+
const app = mount(Comp, { target, props: props as any })
|
|
17
|
+
flushSync()
|
|
18
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SvToggleButton', () => {
|
|
22
|
+
it('reflects pressed and emits the flipped value', () => {
|
|
23
|
+
let got: boolean | undefined
|
|
24
|
+
const { target, destroy } = mnt(SvToggleButton, { pressed: false, onChange: (v: boolean) => (got = v) })
|
|
25
|
+
try {
|
|
26
|
+
const btn = target.querySelector<HTMLButtonElement>('.sv-toggle')!
|
|
27
|
+
expect(btn.getAttribute('aria-pressed')).toBe('false')
|
|
28
|
+
btn.click(); flushSync()
|
|
29
|
+
expect(got).toBe(true)
|
|
30
|
+
} finally { destroy() }
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('SvSwitchButton', () => {
|
|
35
|
+
it('has role switch and toggles', () => {
|
|
36
|
+
let got: boolean | undefined
|
|
37
|
+
const { target, destroy } = mnt(SvSwitchButton, { checked: false, onChange: (v: boolean) => (got = v) })
|
|
38
|
+
try {
|
|
39
|
+
const sw = target.querySelector<HTMLButtonElement>('[role="switch"]')!
|
|
40
|
+
expect(sw.getAttribute('aria-checked')).toBe('false')
|
|
41
|
+
sw.click(); flushSync()
|
|
42
|
+
expect(got).toBe(true)
|
|
43
|
+
} finally { destroy() }
|
|
44
|
+
})
|
|
45
|
+
it('ArrowRight turns on, ArrowLeft off', () => {
|
|
46
|
+
const states: boolean[] = []
|
|
47
|
+
const { target, destroy } = mnt(SvSwitchButton, { checked: false, onChange: (v: boolean) => states.push(v) })
|
|
48
|
+
try {
|
|
49
|
+
const sw = target.querySelector<HTMLButtonElement>('[role="switch"]')!
|
|
50
|
+
sw.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
|
|
51
|
+
flushSync()
|
|
52
|
+
expect(states.at(-1)).toBe(true)
|
|
53
|
+
} finally { destroy() }
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('SvCheckBox', () => {
|
|
58
|
+
it('toggles and reports aria-checked mixed when indeterminate', () => {
|
|
59
|
+
let got: boolean | undefined
|
|
60
|
+
const a = mnt(SvCheckBox, { checked: false, onChange: (v: boolean) => (got = v) })
|
|
61
|
+
try {
|
|
62
|
+
const box = a.target.querySelector<HTMLButtonElement>('[role="checkbox"]')!
|
|
63
|
+
box.click(); flushSync()
|
|
64
|
+
expect(got).toBe(true)
|
|
65
|
+
} finally { a.destroy() }
|
|
66
|
+
const b = mnt(SvCheckBox, { indeterminate: true })
|
|
67
|
+
try {
|
|
68
|
+
expect(b.target.querySelector('[role="checkbox"]')!.getAttribute('aria-checked')).toBe('mixed')
|
|
69
|
+
} finally { b.destroy() }
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('SvRadioGroup', () => {
|
|
74
|
+
it('selects on click and arrow-navigates', () => {
|
|
75
|
+
const picks: unknown[] = []
|
|
76
|
+
const opts = [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }, { value: 'c', label: 'C' }]
|
|
77
|
+
const { target, destroy } = mnt(SvRadioGroup, { options: opts, value: 'a', onChange: (v: unknown) => picks.push(v) })
|
|
78
|
+
try {
|
|
79
|
+
const radios = target.querySelectorAll<HTMLButtonElement>('[role="radio"]')
|
|
80
|
+
expect(radios[0]!.getAttribute('aria-checked')).toBe('true')
|
|
81
|
+
radios[2]!.click(); flushSync()
|
|
82
|
+
expect(picks.at(-1)).toBe('c')
|
|
83
|
+
// roving tabindex: selected is 0, others -1
|
|
84
|
+
radios[0]!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
|
85
|
+
flushSync()
|
|
86
|
+
expect(picks.at(-1)).toBe('b')
|
|
87
|
+
} finally { destroy() }
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('SvRating', () => {
|
|
92
|
+
it('clicking a star emits its value; keyboard steps', () => {
|
|
93
|
+
const vals: number[] = []
|
|
94
|
+
const { target, destroy } = mnt(SvRating, { value: 2, onChange: (v: number) => vals.push(v) })
|
|
95
|
+
try {
|
|
96
|
+
const stars = target.querySelectorAll<HTMLButtonElement>('.sv-rating__star')
|
|
97
|
+
expect(stars.length).toBe(5)
|
|
98
|
+
stars[3]!.click(); flushSync()
|
|
99
|
+
expect(vals.at(-1)).toBe(4)
|
|
100
|
+
target.querySelector<HTMLElement>('.sv-rating')!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
|
|
101
|
+
flushSync()
|
|
102
|
+
expect(vals.at(-1)).toBe(3) // 2 + 1 step
|
|
103
|
+
} finally { destroy() }
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Group F (tabs / tree / form) component tests. */
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
4
|
+
import SvTabs from './SvTabs.svelte'
|
|
5
|
+
import SvTree from './SvTree.svelte'
|
|
6
|
+
import SvForm from './SvForm.svelte'
|
|
7
|
+
|
|
8
|
+
function mnt(Comp: any, props: Record<string, unknown>) {
|
|
9
|
+
const target = document.createElement('div')
|
|
10
|
+
document.body.appendChild(target)
|
|
11
|
+
const app = mount(Comp, { target, props: props as any })
|
|
12
|
+
flushSync()
|
|
13
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('SvTabs', () => {
|
|
17
|
+
const tabs = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, { id: 'c', label: 'C', disabled: true }]
|
|
18
|
+
it('selects on click and arrow-navigates skipping disabled', () => {
|
|
19
|
+
let got = ''
|
|
20
|
+
const { target, destroy } = mnt(SvTabs, { tabs, value: 'a', onChange: (id: string) => (got = id) })
|
|
21
|
+
try {
|
|
22
|
+
const els = target.querySelectorAll<HTMLButtonElement>('[role="tab"]')
|
|
23
|
+
expect(els[0]!.getAttribute('aria-selected')).toBe('true')
|
|
24
|
+
els[1]!.click(); flushSync()
|
|
25
|
+
expect(got).toBe('b')
|
|
26
|
+
// ArrowRight from b wraps past disabled c back to a
|
|
27
|
+
els[1]!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
|
|
28
|
+
flushSync()
|
|
29
|
+
expect(got).toBe('a')
|
|
30
|
+
} finally { destroy() }
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('SvTree', () => {
|
|
35
|
+
const nodes = [
|
|
36
|
+
{ id: '1', label: 'Root', children: [
|
|
37
|
+
{ id: '1a', label: 'Child A' },
|
|
38
|
+
{ id: '1b', label: 'Child B', children: [{ id: '1b1', label: 'Leaf' }] },
|
|
39
|
+
] },
|
|
40
|
+
{ id: '2', label: 'Sibling' },
|
|
41
|
+
]
|
|
42
|
+
it('expands to reveal children and selects', () => {
|
|
43
|
+
let sel = ''
|
|
44
|
+
const { target, destroy } = mnt(SvTree, { nodes, expandedIds: ['1'], onSelect: (id: string) => (sel = id) })
|
|
45
|
+
try {
|
|
46
|
+
// Root expanded -> shows 1, 1a, 1b, 2 (1b collapsed)
|
|
47
|
+
expect(target.querySelectorAll('[role="treeitem"]').length).toBe(4)
|
|
48
|
+
const childA = Array.from(target.querySelectorAll<HTMLElement>('[role="treeitem"]')).find((r) => r.textContent?.includes('Child A'))!
|
|
49
|
+
childA.click(); flushSync()
|
|
50
|
+
expect(sel).toBe('1a')
|
|
51
|
+
} finally { destroy() }
|
|
52
|
+
})
|
|
53
|
+
it('cascading checkbox checks all descendants', () => {
|
|
54
|
+
let checked: string[] = []
|
|
55
|
+
const { target, destroy } = mnt(SvTree, { nodes, expandedIds: ['1', '1b'], checkable: true, checked, onCheck: (ids: string[]) => (checked = ids) })
|
|
56
|
+
try {
|
|
57
|
+
const rootCheck = target.querySelector<HTMLButtonElement>('.sv-tree__check')!
|
|
58
|
+
rootCheck.click(); flushSync()
|
|
59
|
+
// Root + all descendants: 1,1a,1b,1b1
|
|
60
|
+
expect(new Set(checked)).toEqual(new Set(['1', '1a', '1b', '1b1']))
|
|
61
|
+
} finally { destroy() }
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('SvForm', () => {
|
|
66
|
+
const fields = [
|
|
67
|
+
{ name: 'name', label: 'Name', required: true },
|
|
68
|
+
{ name: 'age', label: 'Age', type: 'number' as const },
|
|
69
|
+
]
|
|
70
|
+
it('blocks submit and shows errors when required is empty', () => {
|
|
71
|
+
let submitted: any = null
|
|
72
|
+
const { target, destroy } = mnt(SvForm, { fields, onSubmit: (v: any) => (submitted = v) })
|
|
73
|
+
try {
|
|
74
|
+
target.querySelector<HTMLFormElement>('.sv-form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
75
|
+
flushSync()
|
|
76
|
+
expect(submitted).toBeNull()
|
|
77
|
+
expect(target.querySelector('.sv-form__error')!.textContent).toContain('required')
|
|
78
|
+
} finally { destroy() }
|
|
79
|
+
})
|
|
80
|
+
it('submits values when valid', () => {
|
|
81
|
+
let submitted: any = null
|
|
82
|
+
const { target, destroy } = mnt(SvForm, { fields, initial: { name: 'Ada' }, onSubmit: (v: any) => (submitted = v) })
|
|
83
|
+
try {
|
|
84
|
+
target.querySelector<HTMLFormElement>('.sv-form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
85
|
+
flushSync()
|
|
86
|
+
expect(submitted).toEqual({ name: 'Ada' })
|
|
87
|
+
} finally { destroy() }
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Group C (text inputs) component tests. */
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
4
|
+
import SvNumberInput from './SvNumberInput.svelte'
|
|
5
|
+
import SvPasswordInput from './SvPasswordInput.svelte'
|
|
6
|
+
import SvMaskedInput from './SvMaskedInput.svelte'
|
|
7
|
+
import SvPhoneInput from './SvPhoneInput.svelte'
|
|
8
|
+
|
|
9
|
+
function mnt(Comp: any, props: Record<string, unknown>) {
|
|
10
|
+
const target = document.createElement('div')
|
|
11
|
+
document.body.appendChild(target)
|
|
12
|
+
const app = mount(Comp, { target, props: props as any })
|
|
13
|
+
flushSync()
|
|
14
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('SvNumberInput', () => {
|
|
18
|
+
it('clamps to max on commit', () => {
|
|
19
|
+
let got: number | null | undefined
|
|
20
|
+
const { target, destroy } = mnt(SvNumberInput, { value: 5, min: 0, max: 10, step: 2, onChange: (v: number | null) => (got = v) })
|
|
21
|
+
try {
|
|
22
|
+
const input = target.querySelector<HTMLInputElement>('.sv-num__input')!
|
|
23
|
+
input.dispatchEvent(new FocusEvent('focus'))
|
|
24
|
+
input.value = '99'
|
|
25
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
26
|
+
input.dispatchEvent(new FocusEvent('blur'))
|
|
27
|
+
flushSync()
|
|
28
|
+
expect(got).toBe(10)
|
|
29
|
+
} finally { destroy() }
|
|
30
|
+
})
|
|
31
|
+
it('ArrowDown steps down by step from the current value', () => {
|
|
32
|
+
let got: number | null | undefined
|
|
33
|
+
const { target, destroy } = mnt(SvNumberInput, { value: 8, min: 0, max: 10, step: 2, onChange: (v: number | null) => (got = v) })
|
|
34
|
+
try {
|
|
35
|
+
target.querySelector<HTMLInputElement>('.sv-num__input')!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
|
36
|
+
flushSync()
|
|
37
|
+
expect(got).toBe(6)
|
|
38
|
+
} finally { destroy() }
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('SvPasswordInput', () => {
|
|
43
|
+
it('reveal toggle flips the input type', () => {
|
|
44
|
+
const { target, destroy } = mnt(SvPasswordInput, { value: 'secret' })
|
|
45
|
+
try {
|
|
46
|
+
const input = target.querySelector<HTMLInputElement>('.sv-pw__input')!
|
|
47
|
+
expect(input.type).toBe('password')
|
|
48
|
+
target.querySelector<HTMLButtonElement>('.sv-pw__eye')!.click()
|
|
49
|
+
flushSync()
|
|
50
|
+
expect(target.querySelector<HTMLInputElement>('.sv-pw__input')!.type).toBe('text')
|
|
51
|
+
} finally { destroy() }
|
|
52
|
+
})
|
|
53
|
+
it('computes a strength score', () => {
|
|
54
|
+
const { target, destroy } = mnt(SvPasswordInput, { value: 'Abcdef1!', showStrength: true })
|
|
55
|
+
try {
|
|
56
|
+
expect(target.querySelector('.sv-pw__strength')!.textContent).toBe('Strong')
|
|
57
|
+
} finally { destroy() }
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('SvMaskedInput', () => {
|
|
62
|
+
it('formats input to the mask and reports raw', () => {
|
|
63
|
+
let raw = ''
|
|
64
|
+
let masked = ''
|
|
65
|
+
const { target, destroy } = mnt(SvMaskedInput, {
|
|
66
|
+
mask: '(###) ###-####',
|
|
67
|
+
onChange: (m: string, r: string) => { masked = m; raw = r },
|
|
68
|
+
})
|
|
69
|
+
try {
|
|
70
|
+
const input = target.querySelector<HTMLInputElement>('.sv-masked')!
|
|
71
|
+
input.value = '1234567890'
|
|
72
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
73
|
+
flushSync()
|
|
74
|
+
expect(masked).toBe('(123) 456-7890')
|
|
75
|
+
expect(raw).toBe('1234567890')
|
|
76
|
+
} finally { destroy() }
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('SvPhoneInput', () => {
|
|
81
|
+
it('emits E.164 with dial code and national digits', () => {
|
|
82
|
+
let out = ''
|
|
83
|
+
const { target, destroy } = mnt(SvPhoneInput, { country: 'US', onChange: (v: string) => (out = v) })
|
|
84
|
+
try {
|
|
85
|
+
const input = target.querySelector<HTMLInputElement>('.sv-phone__number')!
|
|
86
|
+
input.value = '(415) 555-1234'
|
|
87
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
88
|
+
flushSync()
|
|
89
|
+
expect(out).toBe('+14155551234')
|
|
90
|
+
} finally { destroy() }
|
|
91
|
+
})
|
|
92
|
+
})
|