ajo-ui 0.1.0

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.
Files changed (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
@@ -0,0 +1,205 @@
1
+ import type { Children, IntrinsicElements, Stateful } from 'ajo'
2
+ import { statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import type { FixedArgs, OmitArg } from './utils'
4
+ import { stlx } from './utils'
5
+ import { virtual } from './virtual'
6
+
7
+ /** Identity accepted by a VirtualList item. */
8
+ export type VirtualListKey = number | string
9
+
10
+ /** Alignment used when bringing an item into view. */
11
+ export type VirtualListScrollOptions = {
12
+ align?: 'center' | 'end' | 'nearest' | 'start'
13
+ }
14
+
15
+ /** Current item addressed by exactly one logical coordinate. */
16
+ export type VirtualListTarget<Key extends VirtualListKey = VirtualListKey> =
17
+ | { index: number; key?: never }
18
+ | { index?: never; key: Key }
19
+
20
+ /** Small imperative controller for a mounted VirtualList. */
21
+ export type VirtualListApi<Key extends VirtualListKey = VirtualListKey> = {
22
+ /** Brings one current source item into view; false means unavailable. */
23
+ scrollTo(
24
+ target: VirtualListTarget<Key>,
25
+ options?: VirtualListScrollOptions,
26
+ ): boolean
27
+ }
28
+
29
+ /** Arguments for one vertical, data-driven virtual list. */
30
+ export type VirtualListArgs<
31
+ T = unknown,
32
+ Key extends VirtualListKey = VirtualListKey,
33
+ > = OmitArg<IntrinsicElements['ul'], 'children' | 'role'>
34
+ & FixedArgs<'children' | 'role'>
35
+ & {
36
+ /** Immutable ordered logical collection. */
37
+ items: readonly T[]
38
+ /** Stable unique identity across insert, delete, reorder, and refresh. */
39
+ getItemKey: (item: T, index: number) => Key
40
+ /** Positive initial border-box block size until an item is measured. */
41
+ estimateSize: number | ((item: T, index: number) => number)
42
+ /** Renders content inside the internally owned keyed list item. */
43
+ renderItem: (item: T, index: number) => Children
44
+ /** Extra items before and after the visible range. */
45
+ overscan?: number
46
+ /** Initial items emitted by SSR and the matching first client pass. */
47
+ prerender?: number
48
+ /** Receives one stable mounted-list controller. */
49
+ setApi?: (api: VirtualListApi<Key>) => void
50
+ }
51
+
52
+ type VirtualListRootArgs<T, Key extends VirtualListKey> = Pick<
53
+ VirtualListArgs<T, Key>,
54
+ 'estimateSize' | 'getItemKey' | 'items' | 'overscan' | 'prerender' | 'renderItem' | 'setApi'
55
+ >
56
+
57
+ const ROOT_STYLE = 'position:relative;overflow-x:hidden;overflow-y:auto;overflow-anchor:none;margin:0;padding:0;list-style:none'
58
+ const ITEM_STYLE = 'box-sizing:border-box;left:0;position:absolute;width:100%;top:'
59
+ const SIZER_KEY = '\0virtual-list-sizer'
60
+ const SNAPSHOT_ERROR = 'VirtualList mutated items'
61
+ const unavailableScroll: VirtualListApi['scrollTo'] = () => false
62
+
63
+ const vnodeKey = (key: VirtualListKey) => `\0virtual-list-item:${typeof key}:${key}`
64
+
65
+ const estimate = <T,>(value: VirtualListArgs<T>['estimateSize'], item: T, index: number) =>
66
+ typeof value === 'function' ? value(item, index) : value
67
+
68
+ const assertCountOption = (name: 'overscan' | 'prerender', value: number) => {
69
+ if (!Number.isInteger(value) || value < 0) {
70
+ throw new RangeError(`VirtualList invalid ${name}`)
71
+ }
72
+ }
73
+
74
+ const VirtualListRoot: Stateful<VirtualListRootArgs<any, VirtualListKey>, 'ul'> = function* () {
75
+ const geometry = virtual<VirtualListKey>(this)
76
+ let source: readonly unknown[] | undefined
77
+ let sourceLength = 0
78
+ let keys: readonly VirtualListKey[] = []
79
+ let apiReceiver: VirtualListRootArgs<unknown, VirtualListKey>['setApi']
80
+ let estimateInput: unknown
81
+ let estimateItems: readonly unknown[] | undefined
82
+ let estimateToken: object | undefined
83
+
84
+ const snapshotKeys = (
85
+ items: readonly unknown[],
86
+ getItemKey: (item: unknown, index: number) => VirtualListKey,
87
+ ) => {
88
+ if (items !== source) {
89
+ source = items
90
+ sourceLength = items.length
91
+ const next = items.map(getItemKey)
92
+ if (next.length === keys.length && next.every((key, index) => key === keys[index])) return keys
93
+ keys = next
94
+ return keys
95
+ }
96
+ if (items.length !== sourceLength) {
97
+ throw new TypeError(SNAPSHOT_ERROR)
98
+ }
99
+ if (items.length > 0) {
100
+ const last = items.length - 1
101
+ if (getItemKey(items[0], 0) !== keys[0] || getItemKey(items[last], last) !== keys[last]) {
102
+ throw new TypeError(SNAPSHOT_ERROR)
103
+ }
104
+ }
105
+ return keys
106
+ }
107
+
108
+ const api: VirtualListApi<VirtualListKey> = {
109
+ scrollTo: (target, options) => geometry.scrollTo(
110
+ target,
111
+ options?.align === 'nearest' ? 'auto' : options?.align,
112
+ ),
113
+ }
114
+ this.signal.addEventListener('abort', () => {
115
+ api.scrollTo = unavailableScroll
116
+ source = undefined
117
+ keys = []
118
+ apiReceiver = undefined
119
+ }, { once: true })
120
+
121
+ for (const {
122
+ estimateSize,
123
+ getItemKey,
124
+ items,
125
+ overscan = 4,
126
+ prerender = 20,
127
+ renderItem,
128
+ setApi,
129
+ } of this) {
130
+ assertCountOption('overscan', overscan)
131
+ assertCountOption('prerender', prerender)
132
+ if (estimateSize !== estimateInput || (typeof estimateSize === 'function' && items !== estimateItems)) {
133
+ estimateInput = estimateSize
134
+ estimateItems = items
135
+ estimateToken = {}
136
+ }
137
+ geometry.sync({
138
+ estimate: index => estimate(estimateSize, items[index], index),
139
+ estimateToken,
140
+ keys: snapshotKeys(items, getItemKey),
141
+ overscan,
142
+ prerender,
143
+ })
144
+ if (geometry.connected && setApi && setApi !== apiReceiver) {
145
+ apiReceiver = setApi
146
+ setApi(api)
147
+ }
148
+ yield (
149
+ <>
150
+ {geometry.rows.map(({ index, key, start }) => (
151
+ <li
152
+ aria-posinset={index + 1}
153
+ aria-setsize={items.length}
154
+ data-slot="virtual-list-item"
155
+ key={vnodeKey(key)}
156
+ ref={element => geometry.measure(key, element)}
157
+ style={`${ITEM_STYLE}${start}px`}
158
+ >
159
+ {renderItem(items[index], index)}
160
+ </li>
161
+ ))}
162
+ <li
163
+ aria-hidden="true"
164
+ data-slot="virtual-list-sizer"
165
+ key={SIZER_KEY}
166
+ role="none"
167
+ style={`height:${geometry.total}px;pointer-events:none;visibility:hidden`}
168
+ />
169
+ </>
170
+ )
171
+ }
172
+ }
173
+
174
+ VirtualListRoot.is = 'ul'
175
+
176
+ /** Virtualized native list with stable identity and bounded DOM work. */
177
+ const VirtualList = <T, Key extends VirtualListKey = VirtualListKey>({
178
+ estimateSize,
179
+ getItemKey,
180
+ items,
181
+ overscan,
182
+ prerender,
183
+ renderItem,
184
+ setApi,
185
+ style,
186
+ tabindex = 0,
187
+ ...attrs
188
+ }: VirtualListArgs<T, Key>) => (
189
+ <VirtualListRoot
190
+ {...rootAttrs(attrs as Record<string, unknown>)}
191
+ estimateSize={estimateSize}
192
+ getItemKey={getItemKey}
193
+ items={items}
194
+ overscan={overscan}
195
+ prerender={prerender}
196
+ renderItem={renderItem}
197
+ setApi={setApi}
198
+ attr:data-slot="virtual-list"
199
+ attr:style={stlx(style, ROOT_STYLE)}
200
+ attr:tabindex={tabindex}
201
+ />
202
+ )
203
+
204
+ export { VirtualList }
205
+ export default VirtualList
package/src/virtual.ts ADDED
@@ -0,0 +1,385 @@
1
+ import {
2
+ defaultRangeExtractor,
3
+ elementScroll,
4
+ measureElement,
5
+ observeElementOffset,
6
+ observeElementRect,
7
+ Virtualizer,
8
+ type Range,
9
+ type VirtualItem,
10
+ } from '@tanstack/virtual-core'
11
+ import { dom, frame, type Host } from 'ajo-cloves'
12
+
13
+ export type VirtualKey = number | string
14
+
15
+ export type VirtualInput<Key extends VirtualKey> = {
16
+ estimate: (index: number) => number
17
+ estimateToken: unknown
18
+ keys: readonly Key[]
19
+ overscan: number
20
+ prerender: number
21
+ }
22
+
23
+ export type VirtualRow<Key extends VirtualKey> = Readonly<{
24
+ index: number
25
+ key: Key
26
+ start: number
27
+ }>
28
+
29
+ export type VirtualTarget<Key extends VirtualKey> =
30
+ | { index: number; key?: never }
31
+ | { index?: never; key: Key }
32
+
33
+ export type VirtualView<Key extends VirtualKey> = {
34
+ readonly connected: boolean
35
+ readonly rows: readonly VirtualRow<Key>[]
36
+ readonly total: number
37
+ measure: (key: Key, node: HTMLElement | null) => void
38
+ scrollTo: (target: VirtualTarget<Key>, align?: 'auto' | 'center' | 'end' | 'start') => boolean
39
+ sync: (input: VirtualInput<Key>) => void
40
+ }
41
+
42
+ type Geometry<Key extends VirtualKey> = {
43
+ rows: ReadonlyArray<VirtualRow<Key> & Pick<VirtualItem, 'size'>>
44
+ total: number
45
+ }
46
+
47
+ const INDEX_ATTRIBUTE = 'data-ajo-virtual-index'
48
+ const TARGET_SHAPE_ERROR = 'VirtualList target needs key or index'
49
+
50
+ const sameGeometry = <Key extends VirtualKey>(left: Geometry<Key>, right: Geometry<Key>) => {
51
+ if (left.total !== right.total || left.rows.length !== right.rows.length) return false
52
+ for (let index = 0; index < left.rows.length; index++) {
53
+ const a = left.rows[index]!
54
+ const b = right.rows[index]!
55
+ if (a.key !== b.key || a.index !== b.index || a.start !== b.start || a.size !== b.size) return false
56
+ }
57
+ return true
58
+ }
59
+
60
+ const validateKeys = <Key extends VirtualKey>(keys: readonly Key[]) => {
61
+ const indexes = new Map<Key, number>()
62
+ for (let index = 0; index < keys.length; index++) {
63
+ const key = keys[index]!
64
+ if (typeof key !== 'string' && (typeof key !== 'number' || !Number.isFinite(key))) {
65
+ throw new TypeError(`VirtualList invalid key at ${index}`)
66
+ }
67
+ const previous = indexes.get(key)
68
+ if (previous !== undefined) {
69
+ throw new TypeError(`VirtualList duplicate key ${JSON.stringify(key)} ${previous}/${index}`)
70
+ }
71
+ indexes.set(key, index)
72
+ }
73
+ return indexes
74
+ }
75
+
76
+ export const virtual = <Key extends VirtualKey>(host: Host): VirtualView<Key> => {
77
+ let input: VirtualInput<Key> | undefined
78
+ let instance: Virtualizer<HTMLElement, HTMLElement> | undefined
79
+ let keyIndexes = new Map<Key, number>()
80
+ let validatedKeys: readonly Key[] | undefined
81
+ let mounted = false
82
+ let live = false
83
+ let mountCleanup: (() => void) | undefined
84
+ let postCommitQueued = false
85
+ let invalidateQueued = false
86
+ let syncing = false
87
+ const positiveSizes = new Map<Key, number>()
88
+ let focusedKey: Key | undefined
89
+ let liveRangeExtractor = defaultRangeExtractor
90
+ const elementsByKey = new Map<Key, HTMLElement>()
91
+ let geometry: Geometry<Key> = { rows: [], total: 0 }
92
+ let rendered = geometry
93
+
94
+ const estimate = (index: number) => {
95
+ const size = input!.estimate(index)
96
+ if (!Number.isFinite(size) || size <= 0) {
97
+ throw new RangeError(`VirtualList invalid estimate at ${index}`)
98
+ }
99
+ return size
100
+ }
101
+
102
+ let getItemKey = (index: number): Key | number => index
103
+
104
+ const collect = (): Geometry<Key> => {
105
+ if (!instance) return { rows: [], total: 0 }
106
+ return {
107
+ rows: instance.getVirtualItems().map(item => ({
108
+ index: item.index,
109
+ key: item.key as Key,
110
+ size: item.size,
111
+ start: item.start,
112
+ })),
113
+ total: instance.getTotalSize(),
114
+ }
115
+ }
116
+
117
+ const throwAsync = (error: unknown) => {
118
+ if (!host.signal.aborted) host.throw(error)
119
+ }
120
+
121
+ const renderNext = () => {
122
+ if (host.signal.aborted) return
123
+ try {
124
+ host.next()
125
+ } catch (error) {
126
+ throwAsync(error)
127
+ }
128
+ }
129
+
130
+ const schedule = frame(renderNext)
131
+ const invalidate = (immediate = false) => {
132
+ if (!immediate) return schedule()
133
+ schedule.cancel()
134
+ if (invalidateQueued || host.signal.aborted) return
135
+ invalidateQueued = true
136
+ queueMicrotask(() => {
137
+ invalidateQueued = false
138
+ renderNext()
139
+ })
140
+ }
141
+
142
+ const onChange = () => {
143
+ if (syncing || host.signal.aborted) return
144
+ try {
145
+ const next = collect()
146
+ geometry = next
147
+ if (sameGeometry(next, rendered)) {
148
+ schedule.cancel()
149
+ return
150
+ }
151
+ invalidate()
152
+ } catch (error) {
153
+ throwAsync(error)
154
+ }
155
+ }
156
+
157
+ const ssrRangeExtractor = (_range: Range) => {
158
+ const count = Math.min(input!.prerender, input!.keys.length)
159
+ return Array.from({ length: count }, (_, index) => index)
160
+ }
161
+
162
+ const refreshLiveRangeExtractor = () => {
163
+ const focusedIndex = focusedKey === undefined ? undefined : keyIndexes.get(focusedKey)
164
+ liveRangeExtractor = (range: Range) => {
165
+ const indexes = defaultRangeExtractor(range)
166
+ if (focusedIndex === undefined || indexes.includes(focusedIndex)) return indexes
167
+ indexes.push(focusedIndex)
168
+ indexes.sort((left, right) => left - right)
169
+ return indexes
170
+ }
171
+ }
172
+
173
+ const focusedRowKey = () => {
174
+ if (!dom(host)) return undefined
175
+ let node = host.ownerDocument.activeElement as HTMLElement | null
176
+ while (node && node !== host) {
177
+ const index = node.getAttribute(INDEX_ATTRIBUTE)
178
+ if (index !== null) return input?.keys[Number(index)]
179
+ node = node.parentElement
180
+ }
181
+ return undefined
182
+ }
183
+
184
+ const updateFocus = () => {
185
+ const next = focusedRowKey()
186
+ if (next === focusedKey) return
187
+ focusedKey = next
188
+ refreshLiveRangeExtractor()
189
+ if (!live || !instance || host.signal.aborted) return
190
+ try {
191
+ syncing = true
192
+ applyOptions()
193
+ geometry = collect()
194
+ } catch (error) {
195
+ throwAsync(error)
196
+ } finally {
197
+ syncing = false
198
+ }
199
+ if (!sameGeometry(geometry, rendered)) invalidate()
200
+ }
201
+
202
+ const measure = (
203
+ node: HTMLElement,
204
+ entry: ResizeObserverEntry | undefined,
205
+ current: Virtualizer<HTMLElement, HTMLElement>,
206
+ ) => {
207
+ const index = current.indexFromElement(node)
208
+ const key = input?.keys[index]
209
+ const cached = key === undefined ? undefined : positiveSizes.get(key)
210
+ if (!entry && cached !== undefined) return cached
211
+ const size = measureElement(node, entry, current)
212
+ if (Number.isFinite(size) && size > 0) {
213
+ if (key !== undefined) positiveSizes.set(key, size)
214
+ return size
215
+ }
216
+ return key === undefined ? estimate(index) : positiveSizes.get(key) ?? estimate(index)
217
+ }
218
+
219
+ const coreOptions = () => {
220
+ const prerender = Math.min(input!.prerender, input!.keys.length)
221
+ let initialHeight = 0
222
+ for (let index = 0; index < prerender; index++) initialHeight += estimate(index)
223
+ return {
224
+ count: input!.keys.length,
225
+ estimateSize: estimate,
226
+ getItemKey,
227
+ getScrollElement: () => dom(host) ? host as HTMLElement : null,
228
+ indexAttribute: INDEX_ATTRIBUTE,
229
+ initialRect: { height: initialHeight, width: 0 },
230
+ measureElement: measure,
231
+ observeElementOffset,
232
+ observeElementRect,
233
+ onChange,
234
+ overscan: live ? input!.overscan : 0,
235
+ rangeExtractor: live ? liveRangeExtractor : ssrRangeExtractor,
236
+ scrollToFn: elementScroll,
237
+ }
238
+ }
239
+
240
+ const applyOptions = () => {
241
+ if (!instance) instance = new Virtualizer<HTMLElement, HTMLElement>(coreOptions())
242
+ else instance.setOptions(coreOptions())
243
+ }
244
+
245
+ const postCommit = () => {
246
+ if (postCommitQueued || host.signal.aborted) return
247
+ postCommitQueued = true
248
+ queueMicrotask(() => {
249
+ postCommitQueued = false
250
+ if (host.signal.aborted || !dom(host) || !host.isConnected || !instance) return
251
+ try {
252
+ syncing = true
253
+ const firstMount = !mounted
254
+ if (firstMount) {
255
+ mountCleanup = instance._didMount()
256
+ mounted = true
257
+ focusedKey = focusedRowKey()
258
+ live = true
259
+ refreshLiveRangeExtractor()
260
+ host.addEventListener('focusin', updateFocus, { signal: host.signal })
261
+ host.addEventListener('focusout', () => queueMicrotask(updateFocus), { signal: host.signal })
262
+ }
263
+ applyOptions()
264
+ instance._willUpdate()
265
+ const next = collect()
266
+ const changed = !sameGeometry(next, rendered)
267
+ if (changed) geometry = next
268
+ if (firstMount || changed) invalidate(firstMount)
269
+ } catch (error) {
270
+ throwAsync(error)
271
+ } finally {
272
+ syncing = false
273
+ }
274
+ })
275
+ }
276
+
277
+ host.signal.addEventListener('abort', () => {
278
+ schedule.cancel()
279
+ mountCleanup?.()
280
+ mountCleanup = undefined
281
+ mounted = false
282
+ live = false
283
+ focusedKey = undefined
284
+ positiveSizes.clear()
285
+ elementsByKey.clear()
286
+ keyIndexes.clear()
287
+ validatedKeys = undefined
288
+ input = undefined
289
+ instance = undefined
290
+ geometry = rendered = { rows: [], total: 0 }
291
+ }, { once: true })
292
+
293
+ return {
294
+ get connected() {
295
+ return mounted && dom(host) && host.isConnected
296
+ },
297
+ get rows() {
298
+ return geometry.rows
299
+ },
300
+ get total() {
301
+ return geometry.total
302
+ },
303
+ measure(key, node) {
304
+ if (!instance) return
305
+ if (!node) {
306
+ instance.measureElement(null)
307
+ const previous = elementsByKey.get(key)
308
+ if (previous) queueMicrotask(() => {
309
+ if (host.signal.aborted || previous.isConnected || elementsByKey.get(key) !== previous) return
310
+ elementsByKey.delete(key)
311
+ })
312
+ return
313
+ }
314
+ const index = keyIndexes.get(key)
315
+ if (index === undefined || host.signal.aborted) return
316
+ elementsByKey.set(key, node)
317
+ node.setAttribute(INDEX_ATTRIBUTE, String(index))
318
+ instance.measureElement(node)
319
+ },
320
+ scrollTo(target, align = 'auto') {
321
+ if (!target || typeof target !== 'object') {
322
+ throw new TypeError(TARGET_SHAPE_ERROR)
323
+ }
324
+ const record = target as { index?: unknown; key?: unknown }
325
+ const hasIndex = Object.hasOwn(record, 'index')
326
+ const hasKey = Object.hasOwn(record, 'key')
327
+ if (hasIndex === hasKey) {
328
+ throw new TypeError(TARGET_SHAPE_ERROR)
329
+ }
330
+ let index: number | undefined
331
+ if (hasKey) {
332
+ if (typeof record.key !== 'string' && (typeof record.key !== 'number' || !Number.isFinite(record.key))) {
333
+ throw new TypeError('VirtualList invalid scroll key')
334
+ }
335
+ index = keyIndexes.get(record.key as Key)
336
+ } else {
337
+ if (typeof record.index !== 'number' || !Number.isInteger(record.index)) {
338
+ throw new TypeError('VirtualList invalid scroll index')
339
+ }
340
+ index = record.index
341
+ }
342
+ if (index === undefined || index < 0 || index >= (input?.keys.length ?? 0)) return false
343
+ if (!instance || !mounted || host.signal.aborted) return false
344
+ instance.scrollToIndex(index, { align, behavior: 'auto' })
345
+ return true
346
+ },
347
+ sync(next) {
348
+ const keysChanged = validatedKeys !== next.keys
349
+ if (keysChanged) {
350
+ const nextIndexes = validateKeys(next.keys)
351
+ if (focusedKey !== undefined && !nextIndexes.has(focusedKey)) {
352
+ const row = elementsByKey.get(focusedKey)
353
+ const active = dom(host) ? host.ownerDocument.activeElement : null
354
+ if (row && active && row.contains(active)) {
355
+ if (dom(host)) host.focus({ preventScroll: true })
356
+ }
357
+ focusedKey = undefined
358
+ }
359
+ for (const key of positiveSizes.keys()) {
360
+ if (!nextIndexes.has(key)) positiveSizes.delete(key)
361
+ }
362
+ if (instance) for (const key of instance.itemSizeCache.keys()) {
363
+ if (!nextIndexes.has(key as Key)) instance.itemSizeCache.delete(key)
364
+ }
365
+ keyIndexes = nextIndexes
366
+ validatedKeys = next.keys
367
+ refreshLiveRangeExtractor()
368
+ }
369
+ if (keysChanged || input?.estimateToken !== next.estimateToken) {
370
+ const currentKeys = next.keys
371
+ getItemKey = index => currentKeys[index] ?? index
372
+ }
373
+ input = next
374
+ syncing = true
375
+ try {
376
+ applyOptions()
377
+ geometry = collect()
378
+ rendered = geometry
379
+ } finally {
380
+ syncing = false
381
+ }
382
+ postCommit()
383
+ },
384
+ }
385
+ }