@proyecto-viviana/solidaria-components 0.2.5 → 0.2.9

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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ActionBar.d.ts +71 -0
  3. package/dist/ActionBar.d.ts.map +1 -0
  4. package/dist/ActionGroup.d.ts +74 -0
  5. package/dist/ActionGroup.d.ts.map +1 -0
  6. package/dist/Alert.d.ts +70 -0
  7. package/dist/Alert.d.ts.map +1 -0
  8. package/dist/Breadcrumbs.d.ts +10 -2
  9. package/dist/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/Button.d.ts +4 -0
  11. package/dist/Button.d.ts.map +1 -1
  12. package/dist/Calendar.d.ts +13 -0
  13. package/dist/Calendar.d.ts.map +1 -1
  14. package/dist/Checkbox.d.ts +2 -2
  15. package/dist/Checkbox.d.ts.map +1 -1
  16. package/dist/Collection.d.ts +125 -0
  17. package/dist/Collection.d.ts.map +1 -0
  18. package/dist/Color.d.ts +114 -2
  19. package/dist/Color.d.ts.map +1 -1
  20. package/dist/ColorEditor.d.ts +42 -0
  21. package/dist/ColorEditor.d.ts.map +1 -0
  22. package/dist/ComboBox.d.ts +64 -0
  23. package/dist/ComboBox.d.ts.map +1 -1
  24. package/dist/ContextualHelpTrigger.d.ts +40 -0
  25. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  26. package/dist/DateField.d.ts +27 -2
  27. package/dist/DateField.d.ts.map +1 -1
  28. package/dist/DatePicker.d.ts +67 -2
  29. package/dist/DatePicker.d.ts.map +1 -1
  30. package/dist/Dialog.d.ts.map +1 -1
  31. package/dist/Disclosure.d.ts +2 -0
  32. package/dist/Disclosure.d.ts.map +1 -1
  33. package/dist/DragAndDrop.d.ts +80 -0
  34. package/dist/DragAndDrop.d.ts.map +1 -0
  35. package/dist/DragPreview.d.ts +14 -0
  36. package/dist/DragPreview.d.ts.map +1 -0
  37. package/dist/DropZone.d.ts +27 -0
  38. package/dist/DropZone.d.ts.map +1 -0
  39. package/dist/FieldError.d.ts +23 -0
  40. package/dist/FieldError.d.ts.map +1 -0
  41. package/dist/FileTrigger.d.ts +26 -0
  42. package/dist/FileTrigger.d.ts.map +1 -0
  43. package/dist/Focusable.d.ts +27 -0
  44. package/dist/Focusable.d.ts.map +1 -0
  45. package/dist/Form.d.ts +27 -0
  46. package/dist/Form.d.ts.map +1 -0
  47. package/dist/GridList.d.ts +40 -1
  48. package/dist/GridList.d.ts.map +1 -1
  49. package/dist/Icon.d.ts +57 -0
  50. package/dist/Icon.d.ts.map +1 -0
  51. package/dist/Keyboard.d.ts +13 -0
  52. package/dist/Keyboard.d.ts.map +1 -0
  53. package/dist/Link.d.ts.map +1 -1
  54. package/dist/ListBox.d.ts +43 -1
  55. package/dist/ListBox.d.ts.map +1 -1
  56. package/dist/ListDropTargetDelegate.d.ts +38 -0
  57. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  58. package/dist/Menu.d.ts +20 -2
  59. package/dist/Menu.d.ts.map +1 -1
  60. package/dist/Meter.d.ts +2 -2
  61. package/dist/Meter.d.ts.map +1 -1
  62. package/dist/Modal.d.ts +2 -0
  63. package/dist/Modal.d.ts.map +1 -1
  64. package/dist/NumberField.d.ts +2 -0
  65. package/dist/NumberField.d.ts.map +1 -1
  66. package/dist/Popover.d.ts +4 -2
  67. package/dist/Popover.d.ts.map +1 -1
  68. package/dist/Pressable.d.ts +27 -0
  69. package/dist/Pressable.d.ts.map +1 -0
  70. package/dist/ProgressBar.d.ts +2 -2
  71. package/dist/ProgressBar.d.ts.map +1 -1
  72. package/dist/RadioGroup.d.ts.map +1 -1
  73. package/dist/RangeCalendar.d.ts +5 -0
  74. package/dist/RangeCalendar.d.ts.map +1 -1
  75. package/dist/RouterProvider.d.ts +75 -0
  76. package/dist/RouterProvider.d.ts.map +1 -0
  77. package/dist/SearchField.d.ts +2 -3
  78. package/dist/SearchField.d.ts.map +1 -1
  79. package/dist/Select.d.ts +11 -0
  80. package/dist/Select.d.ts.map +1 -1
  81. package/dist/SelectionIndicator.d.ts +30 -0
  82. package/dist/SelectionIndicator.d.ts.map +1 -0
  83. package/dist/SharedElementTransition.d.ts +39 -0
  84. package/dist/SharedElementTransition.d.ts.map +1 -0
  85. package/dist/Slider.d.ts +6 -3
  86. package/dist/Slider.d.ts.map +1 -1
  87. package/dist/Table.d.ts +39 -0
  88. package/dist/Table.d.ts.map +1 -1
  89. package/dist/Tabs.d.ts +4 -3
  90. package/dist/Tabs.d.ts.map +1 -1
  91. package/dist/TagGroup.d.ts +12 -2
  92. package/dist/TagGroup.d.ts.map +1 -1
  93. package/dist/Text.d.ts +10 -0
  94. package/dist/Text.d.ts.map +1 -0
  95. package/dist/TextField.d.ts +4 -0
  96. package/dist/TextField.d.ts.map +1 -1
  97. package/dist/TimeField.d.ts +26 -1
  98. package/dist/TimeField.d.ts.map +1 -1
  99. package/dist/Toast.d.ts.map +1 -1
  100. package/dist/ToggleButton.d.ts +30 -0
  101. package/dist/ToggleButton.d.ts.map +1 -0
  102. package/dist/ToggleButtonGroup.d.ts +33 -0
  103. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  104. package/dist/Toolbar.d.ts.map +1 -1
  105. package/dist/Tooltip.d.ts +9 -0
  106. package/dist/Tooltip.d.ts.map +1 -1
  107. package/dist/Tree.d.ts +44 -2
  108. package/dist/Tree.d.ts.map +1 -1
  109. package/dist/Virtualizer.d.ts +61 -0
  110. package/dist/Virtualizer.d.ts.map +1 -0
  111. package/dist/VirtualizerLayouts.d.ts +82 -0
  112. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  113. package/dist/VisuallyHidden.d.ts +3 -1
  114. package/dist/VisuallyHidden.d.ts.map +1 -1
  115. package/dist/contexts.d.ts +1 -0
  116. package/dist/contexts.d.ts.map +1 -1
  117. package/dist/index.d.ts +57 -25
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +13961 -5946
  120. package/dist/index.js.map +1 -7
  121. package/dist/index.ssr.js +9612 -2401
  122. package/dist/index.ssr.js.map +1 -7
  123. package/dist/useDragAndDrop.d.ts +93 -0
  124. package/dist/useDragAndDrop.d.ts.map +1 -0
  125. package/dist/utils.d.ts +7 -1
  126. package/dist/utils.d.ts.map +1 -1
  127. package/dist/virtualizer/Layout.d.ts +79 -0
  128. package/dist/virtualizer/Layout.d.ts.map +1 -0
  129. package/package.json +8 -6
  130. package/src/ActionBar.tsx +248 -0
  131. package/src/ActionGroup.tsx +285 -0
  132. package/src/Alert.tsx +177 -0
  133. package/src/Autocomplete.tsx +1 -1
  134. package/src/Breadcrumbs.tsx +103 -17
  135. package/src/Button.tsx +65 -21
  136. package/src/Calendar.tsx +179 -53
  137. package/src/Checkbox.tsx +1 -2
  138. package/src/Collection.tsx +341 -0
  139. package/src/Color.tsx +652 -34
  140. package/src/ColorEditor.tsx +231 -0
  141. package/src/ComboBox.tsx +315 -81
  142. package/src/ContextualHelpTrigger.tsx +183 -0
  143. package/src/DateField.tsx +93 -19
  144. package/src/DatePicker.tsx +495 -25
  145. package/src/Dialog.tsx +40 -9
  146. package/src/Disclosure.tsx +33 -27
  147. package/src/DragAndDrop.tsx +334 -0
  148. package/src/DragPreview.tsx +45 -0
  149. package/src/DropZone.tsx +213 -0
  150. package/src/FieldError.tsx +67 -0
  151. package/src/FileTrigger.tsx +83 -0
  152. package/src/Focusable.tsx +106 -0
  153. package/src/Form.tsx +85 -0
  154. package/src/GridList.tsx +379 -41
  155. package/src/Icon.tsx +154 -0
  156. package/src/Keyboard.tsx +26 -0
  157. package/src/Link.tsx +14 -1
  158. package/src/ListBox.tsx +484 -33
  159. package/src/ListDropTargetDelegate.ts +282 -0
  160. package/src/Menu.tsx +388 -35
  161. package/src/Meter.tsx +7 -3
  162. package/src/Modal.tsx +32 -4
  163. package/src/NumberField.tsx +163 -43
  164. package/src/Popover.tsx +136 -180
  165. package/src/Pressable.tsx +108 -0
  166. package/src/ProgressBar.tsx +7 -3
  167. package/src/RadioGroup.tsx +35 -25
  168. package/src/RangeCalendar.tsx +100 -68
  169. package/src/RouterProvider.tsx +240 -0
  170. package/src/SearchField.tsx +142 -34
  171. package/src/Select.tsx +221 -73
  172. package/src/SelectionIndicator.tsx +105 -0
  173. package/src/SharedElementTransition.tsx +258 -0
  174. package/src/Slider.tsx +16 -6
  175. package/src/Table.tsx +417 -57
  176. package/src/Tabs.tsx +68 -35
  177. package/src/TagGroup.tsx +121 -36
  178. package/src/Text.tsx +18 -0
  179. package/src/TextField.tsx +25 -8
  180. package/src/TimeField.tsx +101 -151
  181. package/src/Toast.tsx +108 -14
  182. package/src/ToggleButton.tsx +159 -0
  183. package/src/ToggleButtonGroup.tsx +136 -0
  184. package/src/Toolbar.tsx +14 -8
  185. package/src/Tooltip.tsx +108 -19
  186. package/src/Tree.tsx +1143 -87
  187. package/src/Virtualizer.tsx +702 -0
  188. package/src/VirtualizerLayouts.ts +265 -0
  189. package/src/VisuallyHidden.tsx +15 -21
  190. package/src/contexts.ts +1 -0
  191. package/src/index.ts +1057 -620
  192. package/src/useDragAndDrop.ts +351 -0
  193. package/src/utils.tsx +37 -3
  194. package/src/virtualizer/Layout.ts +200 -0
@@ -0,0 +1,702 @@
1
+ /**
2
+ * Virtualizer primitive for solidaria-components.
3
+ *
4
+ * Phase 1 parity: provides RAC-like API and context contract with
5
+ * a safe non-virtualized fallback render path.
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ createContext,
11
+ createMemo,
12
+ createSignal,
13
+ onCleanup,
14
+ onMount,
15
+ splitProps,
16
+ useContext,
17
+ } from 'solid-js';
18
+ import type { DragTypes, DropOperation, DropTarget, ItemDropTarget } from '@proyecto-viviana/solid-stately';
19
+ import { CollectionRendererContext, type CollectionRendererContextValue } from './Collection';
20
+ import { filterDOMProps } from './utils';
21
+ import {
22
+ GridLayout,
23
+ ListLayout,
24
+ TableLayout,
25
+ WaterfallLayout,
26
+ calculateLinearVisibleRange,
27
+ type DefaultVirtualizerLayoutOptions,
28
+ type GridLayoutOptions,
29
+ type LayoutInfo,
30
+ type Point,
31
+ type Rect,
32
+ type Size,
33
+ type VirtualizerRangeContext,
34
+ type VirtualizerLayoutInfoContext,
35
+ type VirtualizerDropTarget,
36
+ type VirtualizerVisibleRange,
37
+ type WaterfallLayoutOptions,
38
+ } from './VirtualizerLayouts';
39
+
40
+ export interface LayoutOptionsDelegate<O> {
41
+ useLayoutOptions?(): O;
42
+ }
43
+
44
+ export interface VirtualizerLayout<O = unknown> extends LayoutOptionsDelegate<O> {
45
+ getVisibleRange?(
46
+ context: VirtualizerRangeContext,
47
+ options?: O
48
+ ): VirtualizerVisibleRange;
49
+ getLayoutInfo?(
50
+ index: number,
51
+ context: VirtualizerLayoutInfoContext,
52
+ options?: O
53
+ ): LayoutInfo;
54
+ getDropTargetFromPoint?(
55
+ point: Point,
56
+ itemCount: number,
57
+ options?: O
58
+ ): VirtualizerDropTarget | null;
59
+ }
60
+
61
+ export interface VirtualizerLayoutClass<O> {
62
+ new (): VirtualizerLayout<O>;
63
+ }
64
+
65
+ export type VirtualizerKeyboardNavigationOverride = (
66
+ target: DropTarget | null,
67
+ direction: 'next' | 'previous',
68
+ isValidDropTarget: (target: DropTarget) => boolean
69
+ ) => DropTarget | null;
70
+
71
+ export interface VirtualizerContextValue<O = unknown> {
72
+ layout: VirtualizerLayout<O>;
73
+ layoutOptions?: O;
74
+ isVirtualized: boolean;
75
+ getVisibleRange: (itemCount: number) => VirtualizerVisibleRange;
76
+ getLayoutInfo: (index: number) => LayoutInfo;
77
+ getDropTargetFromPoint: (point: Point, itemCount: number) => VirtualizerDropTarget | null;
78
+ setDropTargetResolver: ((resolver: VirtualizerDropTargetResolver | undefined) => void);
79
+ setDropTargetItemCountResolver: ((resolver: (() => number) | undefined) => void);
80
+ setDropTargetIndexResolver: ((resolver: ((key: string | number) => number | null) | undefined) => void);
81
+ setDropOperationResolver: ((resolver: VirtualizerDropOperationResolver | undefined) => void);
82
+ setKeyboardNavigationOverride: ((override: VirtualizerKeyboardNavigationOverride | undefined) => void);
83
+ getBaseKeyboardNavigationTarget: VirtualizerKeyboardNavigationOverride;
84
+ }
85
+
86
+ export type VirtualizerDropTargetResolver = (target: VirtualizerDropTarget) => VirtualizerDropTarget;
87
+ export type VirtualizerDropOperationResolver = (
88
+ target: DropTarget,
89
+ types: DragTypes,
90
+ allowedOperations: DropOperation[]
91
+ ) => DropOperation;
92
+
93
+ export const VirtualizerContext = createContext<VirtualizerContextValue<unknown> | null>(null);
94
+
95
+ export function useVirtualizerContext<O>(): VirtualizerContextValue<O> | null {
96
+ return useContext(VirtualizerContext) as VirtualizerContextValue<O> | null;
97
+ }
98
+
99
+ export interface VirtualizerProps<O>
100
+ extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'style'> {
101
+ /** The child collection to virtualize (e.g. ListBox, GridList, or Table). */
102
+ children: JSX.Element;
103
+ /** Layout object or constructor for layout behavior and options delegation. */
104
+ layout: VirtualizerLayoutClass<O> | VirtualizerLayout<O>;
105
+ /** Layout options consumed by the layout implementation. */
106
+ layoutOptions?: O;
107
+ /** Optional renderer for collection drop indicators in virtualized flows. */
108
+ renderDropIndicator?: (index: number, position: 'before' | 'after' | 'on') => JSX.Element | undefined;
109
+ /** Optional operation resolver for collection drop target operations. */
110
+ getDropOperation?: VirtualizerDropOperationResolver;
111
+ class?: string;
112
+ style?: string | JSX.CSSProperties;
113
+ }
114
+
115
+ function getObjectValue<T extends object, K extends keyof T>(
116
+ value: T | undefined,
117
+ key: K
118
+ ): T[K] | undefined {
119
+ return value?.[key];
120
+ }
121
+
122
+ function isSameRange(a: VirtualizerVisibleRange, b: VirtualizerVisibleRange): boolean {
123
+ return (
124
+ a.start === b.start &&
125
+ a.end === b.end &&
126
+ a.offsetTop === b.offsetTop &&
127
+ a.offsetBottom === b.offsetBottom
128
+ );
129
+ }
130
+
131
+ function isSameLayoutInfo(a: LayoutInfo, b: LayoutInfo): boolean {
132
+ return (
133
+ a.key === b.key &&
134
+ a.index === b.index &&
135
+ a.rect.x === b.rect.x &&
136
+ a.rect.y === b.rect.y &&
137
+ a.rect.width === b.rect.width &&
138
+ a.rect.height === b.rect.height
139
+ );
140
+ }
141
+
142
+ /**
143
+ * A Virtualizer renders collection content under a virtualized layout contract.
144
+ * Current implementation supports fixed-size visible range virtualization.
145
+ */
146
+ export function Virtualizer<O>(props: VirtualizerProps<O>): JSX.Element {
147
+ const [local, domProps] = splitProps(props, [
148
+ 'children',
149
+ 'layout',
150
+ 'layoutOptions',
151
+ 'renderDropIndicator',
152
+ 'getDropOperation',
153
+ 'class',
154
+ 'style',
155
+ ]);
156
+ const [scrollOffset, setScrollOffset] = createSignal(0);
157
+ const [measuredViewportSize, setMeasuredViewportSize] = createSignal(0);
158
+ const [measuredViewportWidth, setMeasuredViewportWidth] = createSignal(0);
159
+ const [dropTargetResolver, setDropTargetResolver] = createSignal<VirtualizerDropTargetResolver | undefined>(
160
+ undefined
161
+ );
162
+ const [dropTargetItemCountResolver, setDropTargetItemCountResolver] = createSignal<(() => number) | undefined>(
163
+ undefined
164
+ );
165
+ const [dropTargetIndexResolver, setDropTargetIndexResolver] = createSignal<
166
+ ((key: string | number) => number | null) | undefined
167
+ >(undefined);
168
+ const [dropOperationResolver, setDropOperationResolver] = createSignal<VirtualizerDropOperationResolver | undefined>(
169
+ undefined
170
+ );
171
+ const [keyboardNavigationOverride, setKeyboardNavigationOverride] = createSignal<VirtualizerKeyboardNavigationOverride | undefined>(
172
+ undefined
173
+ );
174
+ let containerRef: HTMLDivElement | undefined;
175
+ const fallbackLayout = new ListLayout();
176
+ const visibleRangeCache = new Map<number, VirtualizerVisibleRange>();
177
+ const layoutInfoCache = new Map<number, LayoutInfo>();
178
+
179
+ const layout = createMemo<VirtualizerLayout<O>>(() => {
180
+ if (typeof local.layout === 'function') {
181
+ return new local.layout();
182
+ }
183
+ return local.layout;
184
+ });
185
+ const resolvedLayout = createMemo<VirtualizerLayout<O>>(() => {
186
+ return layout() ?? (new ListLayout() as VirtualizerLayout<O>);
187
+ });
188
+
189
+ const resolvedLayoutOptions = createMemo<O | undefined>(() => {
190
+ const fromLayout = resolvedLayout().useLayoutOptions?.();
191
+ if (local.layoutOptions && fromLayout) {
192
+ return { ...local.layoutOptions, ...fromLayout };
193
+ }
194
+ return local.layoutOptions ?? fromLayout;
195
+ });
196
+
197
+ const virtualOptions = createMemo(() => resolvedLayoutOptions() as DefaultVirtualizerLayoutOptions | undefined);
198
+ const layoutOptionsWithViewport = createMemo(() => {
199
+ const options = resolvedLayoutOptions();
200
+ if (options && typeof options === 'object') {
201
+ return {
202
+ ...(options as Record<string, unknown>),
203
+ viewportWidth: measuredViewportWidth(),
204
+ } as O;
205
+ }
206
+ return { viewportWidth: measuredViewportWidth() } as O;
207
+ });
208
+ const itemSize = createMemo(() => getObjectValue(virtualOptions(), 'itemSize') ?? 40);
209
+ const overscan = createMemo(() => getObjectValue(virtualOptions(), 'overscan') ?? 2);
210
+ const viewportSize = createMemo(
211
+ () => getObjectValue(virtualOptions(), 'viewportSize') ?? measuredViewportSize() ?? 0
212
+ );
213
+
214
+ const getVisibleRange = (itemCount: number): VirtualizerVisibleRange => {
215
+ const ctx: VirtualizerRangeContext = {
216
+ itemCount,
217
+ scrollOffset: scrollOffset(),
218
+ viewportSize: viewportSize(),
219
+ overscan: overscan(),
220
+ viewportWidth: measuredViewportWidth(),
221
+ };
222
+ const layoutResult = resolvedLayout().getVisibleRange?.(ctx, layoutOptionsWithViewport());
223
+ const nextRange =
224
+ layoutResult ??
225
+ calculateLinearVisibleRange(itemCount, ctx.scrollOffset, ctx.viewportSize, itemSize(), ctx.overscan);
226
+ const cachedRange = visibleRangeCache.get(itemCount);
227
+ if (cachedRange && isSameRange(cachedRange, nextRange)) {
228
+ return cachedRange;
229
+ }
230
+ visibleRangeCache.set(itemCount, nextRange);
231
+ return nextRange;
232
+ };
233
+ const getLayoutInfo = (index: number): LayoutInfo => {
234
+ const ctx: VirtualizerLayoutInfoContext = {
235
+ viewportWidth: measuredViewportWidth(),
236
+ };
237
+ const layoutResult = resolvedLayout().getLayoutInfo?.(index, ctx, layoutOptionsWithViewport());
238
+ const nextInfo = layoutResult ?? {
239
+ key: String(index),
240
+ index,
241
+ rect: {
242
+ x: 0,
243
+ y: index * itemSize(),
244
+ width: Math.max(0, measuredViewportWidth()),
245
+ height: itemSize(),
246
+ },
247
+ };
248
+ const cachedInfo = layoutInfoCache.get(index);
249
+ if (cachedInfo && isSameLayoutInfo(cachedInfo, nextInfo)) {
250
+ return cachedInfo;
251
+ }
252
+ layoutInfoCache.set(index, nextInfo);
253
+ return nextInfo;
254
+ };
255
+ const getDropTargetFromPoint = (point: Point, itemCount: number): VirtualizerDropTarget | null => {
256
+ const target = resolvedLayout().getDropTargetFromPoint?.(point, itemCount, layoutOptionsWithViewport()) ??
257
+ fallbackLayout.getDropTargetFromPoint(point, itemCount, virtualOptions());
258
+ if (!target) return null;
259
+ const resolver = dropTargetResolver();
260
+ return resolver ? resolver(target) : target;
261
+ };
262
+ const assignDropTargetResolver = (resolver: VirtualizerDropTargetResolver | undefined): void => {
263
+ setDropTargetResolver(() => resolver);
264
+ };
265
+ const assignDropTargetItemCountResolver = (resolver: (() => number) | undefined): void => {
266
+ setDropTargetItemCountResolver(() => resolver);
267
+ };
268
+ const assignDropTargetIndexResolver = (resolver: ((key: string | number) => number | null) | undefined): void => {
269
+ setDropTargetIndexResolver(() => resolver);
270
+ };
271
+ const assignDropOperationResolver = (resolver: VirtualizerDropOperationResolver | undefined): void => {
272
+ setDropOperationResolver(() => resolver);
273
+ };
274
+ const assignKeyboardNavigationOverride = (override: VirtualizerKeyboardNavigationOverride | undefined): void => {
275
+ setKeyboardNavigationOverride(() => override);
276
+ };
277
+ const toCollectionDropTarget = (target: VirtualizerDropTarget): DropTarget => {
278
+ if (target.type === 'root') return { type: 'root' };
279
+ const key = target.key ?? target.index;
280
+ return {
281
+ type: 'item',
282
+ key,
283
+ dropPosition: target.position,
284
+ } as ItemDropTarget;
285
+ };
286
+ const getCollectionDropTargetFromPoint = (
287
+ x: number,
288
+ y: number,
289
+ isValidDropTarget: (target: DropTarget) => boolean
290
+ ): DropTarget | null => {
291
+ const itemCount = dropTargetItemCountResolver()?.() ?? 0;
292
+ const virtualTarget = getDropTargetFromPoint({ x, y }, itemCount);
293
+ if (!virtualTarget) return null;
294
+ const mappedTarget = toCollectionDropTarget(virtualTarget);
295
+ if (isValidDropTarget(mappedTarget)) return mappedTarget;
296
+ if (mappedTarget.type === 'item') {
297
+ const alternatePositions: Array<'before' | 'after' | 'on'> = mappedTarget.dropPosition === 'on'
298
+ ? ['before', 'after']
299
+ : mappedTarget.dropPosition === 'before'
300
+ ? ['on', 'after']
301
+ : ['on', 'before'];
302
+ for (const position of alternatePositions) {
303
+ const alternateTarget: DropTarget = {
304
+ ...mappedTarget,
305
+ dropPosition: position,
306
+ };
307
+ if (isValidDropTarget(alternateTarget)) return alternateTarget;
308
+ }
309
+ const rootTarget: DropTarget = { type: 'root' };
310
+ if (isValidDropTarget(rootTarget)) return rootTarget;
311
+ }
312
+ return null;
313
+ };
314
+ const getCollectionDropOperation = (
315
+ target: DropTarget,
316
+ types: DragTypes,
317
+ allowedOperations: DropOperation[]
318
+ ): DropOperation => {
319
+ const resolver = dropOperationResolver() ?? local.getDropOperation;
320
+ if (resolver) return resolver(target, types, allowedOperations);
321
+ if (allowedOperations.length === 0) return 'cancel';
322
+ if (target.type === 'root') {
323
+ if (allowedOperations.includes('copy')) return 'copy';
324
+ if (allowedOperations.includes('move')) return 'move';
325
+ } else if (target.dropPosition === 'on') {
326
+ if (allowedOperations.includes('copy')) return 'copy';
327
+ if (allowedOperations.includes('move')) return 'move';
328
+ } else {
329
+ if (allowedOperations.includes('move')) return 'move';
330
+ if (allowedOperations.includes('copy')) return 'copy';
331
+ }
332
+ if (allowedOperations.includes('link')) return 'link';
333
+ return allowedOperations.find((operation) => operation !== 'cancel') ?? 'cancel';
334
+ };
335
+ const getBaseKeyboardNavigationTarget = (
336
+ target: DropTarget | null,
337
+ direction: 'next' | 'previous',
338
+ isValidDropTarget: (target: DropTarget) => boolean
339
+ ): DropTarget | null => {
340
+ const itemCount = dropTargetItemCountResolver()?.() ?? 0;
341
+ if (itemCount <= 0) {
342
+ const rootTarget: DropTarget = { type: 'root' };
343
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
344
+ }
345
+
346
+ const resolveCurrentIndex = (currentTarget: DropTarget | null): number | null => {
347
+ if (!currentTarget || currentTarget.type === 'root') return null;
348
+ const resolver = dropTargetIndexResolver();
349
+ if (resolver) {
350
+ const fromResolver = resolver(currentTarget.key);
351
+ if (fromResolver != null && fromResolver >= 0 && fromResolver < itemCount) return fromResolver;
352
+ return null;
353
+ }
354
+ if (typeof currentTarget.key === 'number' && currentTarget.key >= 0 && currentTarget.key < itemCount) {
355
+ return currentTarget.key;
356
+ }
357
+ return null;
358
+ };
359
+ const getCurrentIndex = (currentTarget: DropTarget | null): number => {
360
+ if (!currentTarget || currentTarget.type === 'root') {
361
+ return direction === 'next' ? -1 : itemCount;
362
+ }
363
+ const resolvedIndex = resolveCurrentIndex(currentTarget);
364
+ if (resolvedIndex != null) return resolvedIndex;
365
+ return direction === 'next' ? -1 : itemCount;
366
+ };
367
+ const tryCurrentItemTransition = (currentTarget: DropTarget | null): DropTarget | null => {
368
+ if (!currentTarget || currentTarget.type !== 'item') return null;
369
+ const tryPosition = (position: 'before' | 'on' | 'after'): DropTarget | null => {
370
+ if (currentTarget.dropPosition === position) return null;
371
+ const nextTarget: DropTarget = {
372
+ type: 'item',
373
+ key: currentTarget.key,
374
+ dropPosition: position,
375
+ };
376
+ return isValidDropTarget(nextTarget) ? nextTarget : null;
377
+ };
378
+
379
+ if (direction === 'next') {
380
+ if (currentTarget.dropPosition === 'before') {
381
+ return tryPosition('on') ?? tryPosition('after');
382
+ }
383
+ if (currentTarget.dropPosition === 'on') {
384
+ return tryPosition('after');
385
+ }
386
+ } else {
387
+ if (currentTarget.dropPosition === 'after') {
388
+ return tryPosition('on') ?? tryPosition('before');
389
+ }
390
+ if (currentTarget.dropPosition === 'on') {
391
+ return tryPosition('before');
392
+ }
393
+ }
394
+
395
+ return null;
396
+ };
397
+ const scanFromIndex = (
398
+ startIndex: number,
399
+ step: number,
400
+ directionForInsertion: 'next' | 'previous'
401
+ ): DropTarget | null => {
402
+ for (
403
+ let index = startIndex;
404
+ index >= 0 && index < itemCount;
405
+ index += step
406
+ ) {
407
+ const onTarget = tryTarget(index, 'on');
408
+ if (onTarget) return onTarget;
409
+
410
+ const insertionOrder: Array<'before' | 'after'> = directionForInsertion === 'next'
411
+ ? ['before', 'after']
412
+ : ['after', 'before'];
413
+ for (const position of insertionOrder) {
414
+ const insertionTarget = tryTarget(index, position);
415
+ if (insertionTarget) return insertionTarget;
416
+ }
417
+ }
418
+ return null;
419
+ };
420
+ const findNavigationTarget = (currentIndex: number, step = 1): DropTarget | null => {
421
+ const delta = direction === 'next' ? 1 : -1;
422
+ const stepSize = Math.max(1, step);
423
+ const nextStart = currentIndex + delta * stepSize;
424
+ const clampedStart = Math.max(0, Math.min(itemCount - 1, nextStart));
425
+ if (nextStart < 0 || nextStart >= itemCount) {
426
+ const rootTarget: DropTarget = { type: 'root' };
427
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
428
+ }
429
+ const primaryTarget = scanFromIndex(clampedStart, delta, direction);
430
+ if (primaryTarget) return primaryTarget;
431
+ const oppositeDirection: 'next' | 'previous' = direction === 'next' ? 'previous' : 'next';
432
+ const oppositeTarget = scanFromIndex(clampedStart - delta, -delta, oppositeDirection);
433
+ if (oppositeTarget) return oppositeTarget;
434
+
435
+ const rootTarget: DropTarget = { type: 'root' };
436
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
437
+ };
438
+ const tryTarget = (
439
+ index: number,
440
+ position: 'on' | 'before' | 'after'
441
+ ): DropTarget | null => {
442
+ const layoutInfo = getLayoutInfo(index);
443
+ const virtualTarget = getDropTargetFromPoint(
444
+ {
445
+ x: layoutInfo.rect.x + 1,
446
+ y: layoutInfo.rect.y + layoutInfo.rect.height / 2,
447
+ },
448
+ itemCount
449
+ );
450
+ if (!virtualTarget || virtualTarget.type === 'root') return null;
451
+ const nextTarget = toCollectionDropTarget({ ...virtualTarget, position });
452
+ return isValidDropTarget(nextTarget) ? nextTarget : null;
453
+ };
454
+ const tryBoundaryTarget = (boundaryDirection: 'next' | 'previous'): DropTarget | null => {
455
+ const boundaryIndex = boundaryDirection === 'next' ? 0 : itemCount - 1;
456
+ const boundaryOrder: Array<'before' | 'on' | 'after'> = boundaryDirection === 'next'
457
+ ? ['before', 'on', 'after']
458
+ : ['after', 'on', 'before'];
459
+ for (const position of boundaryOrder) {
460
+ const candidate = tryTarget(boundaryIndex, position);
461
+ if (candidate) return candidate;
462
+ }
463
+ return null;
464
+ };
465
+ const directTransition = resolveCurrentIndex(target) != null ? tryCurrentItemTransition(target) : null;
466
+ if (directTransition) return directTransition;
467
+ if (!target || target.type === 'root') {
468
+ const boundaryTarget = tryBoundaryTarget(direction);
469
+ if (boundaryTarget) return boundaryTarget;
470
+ const rootTarget: DropTarget = { type: 'root' };
471
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
472
+ }
473
+ const currentIndex = getCurrentIndex(target);
474
+ const nextStart = currentIndex + (direction === 'next' ? 1 : -1);
475
+ if (nextStart < 0 || nextStart >= itemCount) {
476
+ const rootTarget: DropTarget = { type: 'root' };
477
+ if (isValidDropTarget(rootTarget)) return rootTarget;
478
+ const wrappedBoundary = tryBoundaryTarget(direction);
479
+ if (wrappedBoundary) return wrappedBoundary;
480
+ return null;
481
+ }
482
+ return findNavigationTarget(currentIndex, 1);
483
+ };
484
+ const getKeyboardNavigationTarget = (
485
+ target: DropTarget | null,
486
+ direction: 'next' | 'previous',
487
+ isValidDropTarget: (target: DropTarget) => boolean
488
+ ): DropTarget | null => {
489
+ // If a collection component (e.g. Tree) has installed a keyboard navigation override,
490
+ // delegate to it. This enables collection-aware navigation (tree branch traversal, etc.).
491
+ const override = keyboardNavigationOverride();
492
+ if (override) {
493
+ return override(target, direction, isValidDropTarget);
494
+ }
495
+ return getBaseKeyboardNavigationTarget(target, direction, isValidDropTarget);
496
+ };
497
+ const getKeyboardPageNavigationTarget = (
498
+ target: DropTarget | null,
499
+ direction: 'next' | 'previous',
500
+ isValidDropTarget: (target: DropTarget) => boolean
501
+ ): DropTarget | null => {
502
+ const itemCount = dropTargetItemCountResolver()?.() ?? 0;
503
+ if (itemCount <= 0) {
504
+ const rootTarget: DropTarget = { type: 'root' };
505
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
506
+ }
507
+ if (!target || target.type === 'root') {
508
+ const startIndex = direction === 'next' ? 0 : itemCount - 1;
509
+ const delta = direction === 'next' ? 1 : -1;
510
+ const boundaryOrder: Array<'before' | 'on' | 'after'> = direction === 'next'
511
+ ? ['before', 'on', 'after']
512
+ : ['after', 'on', 'before'];
513
+ for (let index = startIndex; index >= 0 && index < itemCount; index += delta) {
514
+ const layoutInfo = getLayoutInfo(index);
515
+ const virtualTarget = getDropTargetFromPoint(
516
+ {
517
+ x: layoutInfo.rect.x + 1,
518
+ y: layoutInfo.rect.y + layoutInfo.rect.height / 2,
519
+ },
520
+ itemCount
521
+ );
522
+ if (!virtualTarget || virtualTarget.type === 'root') continue;
523
+ for (const position of boundaryOrder) {
524
+ const candidate = toCollectionDropTarget({ ...virtualTarget, position });
525
+ if (isValidDropTarget(candidate)) return candidate;
526
+ }
527
+ }
528
+ const rootTarget: DropTarget = { type: 'root' };
529
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
530
+ }
531
+ const resolver = dropTargetIndexResolver();
532
+ const resolvedIndex = resolver?.(target.key);
533
+ const currentIndex = resolvedIndex != null
534
+ ? resolvedIndex
535
+ : resolver
536
+ ? (direction === 'next' ? -1 : itemCount)
537
+ : (typeof target.key === 'number' ? target.key : (direction === 'next' ? -1 : itemCount));
538
+ const pageSize = Math.max(1, Math.floor(viewportSize() / Math.max(1, itemSize())));
539
+ const delta = direction === 'next' ? 1 : -1;
540
+ const nextStart = currentIndex + delta * pageSize;
541
+ const clampedStart = Math.max(0, Math.min(itemCount - 1, nextStart));
542
+ const tryTarget = (
543
+ index: number,
544
+ position: 'on' | 'before' | 'after'
545
+ ): DropTarget | null => {
546
+ const layoutInfo = getLayoutInfo(index);
547
+ const virtualTarget = getDropTargetFromPoint(
548
+ {
549
+ x: layoutInfo.rect.x + 1,
550
+ y: layoutInfo.rect.y + layoutInfo.rect.height / 2,
551
+ },
552
+ itemCount
553
+ );
554
+ if (!virtualTarget || virtualTarget.type === 'root') return null;
555
+ const nextTarget = toCollectionDropTarget({ ...virtualTarget, position });
556
+ return isValidDropTarget(nextTarget) ? nextTarget : null;
557
+ };
558
+ const scanFromIndex = (startIndex: number, step: number): DropTarget | null => {
559
+ const insertionOrder: Array<'before' | 'after'> = step > 0
560
+ ? ['before', 'after']
561
+ : ['after', 'before'];
562
+ for (let index = startIndex; index >= 0 && index < itemCount; index += step) {
563
+ const onTarget = tryTarget(index, 'on');
564
+ if (onTarget) return onTarget;
565
+ for (const position of insertionOrder) {
566
+ const insertionTarget = tryTarget(index, position);
567
+ if (insertionTarget) return insertionTarget;
568
+ }
569
+ }
570
+ return null;
571
+ };
572
+ if (nextStart < 0 || nextStart >= itemCount) {
573
+ if (direction === 'next') {
574
+ const endBoundaryTarget = tryTarget(itemCount - 1, 'after')
575
+ ?? tryTarget(itemCount - 1, 'on')
576
+ ?? tryTarget(itemCount - 1, 'before');
577
+ if (endBoundaryTarget) return endBoundaryTarget;
578
+ const backwardFallback = scanFromIndex(itemCount - 1, -1);
579
+ if (backwardFallback) return backwardFallback;
580
+ } else {
581
+ if (currentIndex <= 0) {
582
+ const rootTarget: DropTarget = { type: 'root' };
583
+ if (isValidDropTarget(rootTarget)) return rootTarget;
584
+ }
585
+ const startBoundaryTarget = tryTarget(0, 'before')
586
+ ?? tryTarget(0, 'on')
587
+ ?? tryTarget(0, 'after');
588
+ if (startBoundaryTarget) return startBoundaryTarget;
589
+ const forwardFallback = scanFromIndex(0, 1);
590
+ if (forwardFallback) return forwardFallback;
591
+ }
592
+
593
+ const rootTarget: DropTarget = { type: 'root' };
594
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
595
+ }
596
+ const primaryTarget = scanFromIndex(clampedStart, delta);
597
+ if (primaryTarget) return primaryTarget;
598
+ const oppositeTarget = scanFromIndex(clampedStart - delta, -delta);
599
+ if (oppositeTarget) return oppositeTarget;
600
+
601
+ const rootTarget: DropTarget = { type: 'root' };
602
+ return isValidDropTarget(rootTarget) ? rootTarget : null;
603
+ };
604
+
605
+ const contextValue = createMemo<VirtualizerContextValue<O>>(() => ({
606
+ layout: resolvedLayout(),
607
+ layoutOptions: resolvedLayoutOptions(),
608
+ isVirtualized: true,
609
+ getVisibleRange,
610
+ getLayoutInfo,
611
+ getDropTargetFromPoint,
612
+ setDropTargetResolver: assignDropTargetResolver,
613
+ setDropTargetItemCountResolver: assignDropTargetItemCountResolver,
614
+ setDropTargetIndexResolver: assignDropTargetIndexResolver,
615
+ setDropOperationResolver: assignDropOperationResolver,
616
+ setKeyboardNavigationOverride: assignKeyboardNavigationOverride,
617
+ getBaseKeyboardNavigationTarget,
618
+ }));
619
+ const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
620
+ renderItem: (item) => item as JSX.Element,
621
+ isVirtualized: true,
622
+ layoutDelegate: resolvedLayout(),
623
+ dropTargetDelegate: {
624
+ getDropTargetFromPoint: getCollectionDropTargetFromPoint,
625
+ getDropOperation: getCollectionDropOperation,
626
+ getKeyboardNavigationTarget,
627
+ getKeyboardPageNavigationTarget,
628
+ },
629
+ renderDropIndicator: local.renderDropIndicator,
630
+ }));
631
+
632
+ const filteredDomProps = createMemo(() => filterDOMProps(domProps, { global: true }));
633
+
634
+ const updateViewportSize = () => {
635
+ if (!containerRef) return;
636
+ const nextHeight = containerRef.clientHeight;
637
+ const nextWidth = containerRef.clientWidth;
638
+ if (nextHeight !== measuredViewportSize()) setMeasuredViewportSize(nextHeight);
639
+ if (nextWidth !== measuredViewportWidth()) setMeasuredViewportWidth(nextWidth);
640
+ };
641
+
642
+ onMount(() => {
643
+ updateViewportSize();
644
+ const handleResize = () => updateViewportSize();
645
+ window.addEventListener('resize', handleResize);
646
+ onCleanup(() => {
647
+ window.removeEventListener('resize', handleResize);
648
+ });
649
+ });
650
+
651
+ let scrollFrame: number | undefined;
652
+ onCleanup(() => {
653
+ if (scrollFrame != null) cancelAnimationFrame(scrollFrame);
654
+ });
655
+
656
+ return (
657
+ <CollectionRendererContext.Provider value={collectionRenderer()}>
658
+ <VirtualizerContext.Provider value={contextValue()}>
659
+ <div
660
+ {...filteredDomProps()}
661
+ ref={(el) => {
662
+ containerRef = el;
663
+ }}
664
+ class={local.class}
665
+ style={local.style}
666
+ data-virtualizer
667
+ onScroll={(e) => {
668
+ const target = e.currentTarget as HTMLDivElement;
669
+ if (scrollFrame != null) cancelAnimationFrame(scrollFrame);
670
+ scrollFrame = requestAnimationFrame(() => {
671
+ scrollFrame = undefined;
672
+ if (target.scrollTop !== scrollOffset()) setScrollOffset(target.scrollTop);
673
+ updateViewportSize();
674
+ });
675
+ }}
676
+ >
677
+ {local.children}
678
+ </div>
679
+ </VirtualizerContext.Provider>
680
+ </CollectionRendererContext.Provider>
681
+ );
682
+ }
683
+
684
+ export {
685
+ ListLayout,
686
+ GridLayout,
687
+ WaterfallLayout,
688
+ TableLayout,
689
+ };
690
+
691
+ export type {
692
+ VirtualizerVisibleRange,
693
+ VirtualizerRangeContext,
694
+ DefaultVirtualizerLayoutOptions,
695
+ GridLayoutOptions,
696
+ WaterfallLayoutOptions,
697
+ VirtualizerDropTarget,
698
+ LayoutInfo,
699
+ Rect,
700
+ Size,
701
+ Point,
702
+ };