@lotics/ui 2.6.1 → 3.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.
- package/package.json +2 -15
- package/src/react_native.d.ts +2 -2
- package/src/segmented_control.tsx +201 -0
- package/src/cell_date.tsx +0 -30
- package/src/cell_date_format.test.ts +0 -32
- package/src/cell_date_format.ts +0 -73
- package/src/cell_number.test.ts +0 -42
- package/src/cell_number.tsx +0 -25
- package/src/cell_number_format.ts +0 -42
- package/src/cell_select.tsx +0 -68
- package/src/cell_text.tsx +0 -45
- package/src/grid/data_grid.tsx +0 -2003
- package/src/grid/data_grid_columns.test.ts +0 -72
- package/src/grid/data_grid_columns.ts +0 -30
- package/src/grid/data_grid_context.ts +0 -119
- package/src/grid/dispatch_safely.ts +0 -39
- package/src/grid/engine.module.css +0 -114
- package/src/grid/engine.tsx +0 -1042
- package/src/grid/helpers.ts +0 -205
- package/src/grid/layout.test.ts +0 -515
- package/src/grid/layout.ts +0 -425
- package/src/grid/recycling.test.ts +0 -236
- package/src/grid/recycling.ts +0 -172
- package/src/grid/row_cell.module.css +0 -105
- package/src/grid/row_cell.tsx +0 -313
- package/src/grid/search_highlight.ts +0 -71
- package/src/grid/select_cell.tsx +0 -58
- package/src/grid/select_group_summary_cell.tsx +0 -76
- package/src/grid/select_header_cell.tsx +0 -32
- package/src/grid/skeleton_row.module.css +0 -34
- package/src/grid/skeleton_row.tsx +0 -20
- package/src/grid/use_grid_groups.ts +0 -311
- package/src/grid/use_scroll_to_cell.ts +0 -135
- package/src/grid/use_virtual_grid.ts +0 -383
- package/src/grid/visibility.test.ts +0 -208
- package/src/grid/visibility.ts +0 -77
- package/src/kanban/constants.ts +0 -18
- package/src/kanban/default_renderers.tsx +0 -160
- package/src/kanban/drag_preview.tsx +0 -157
- package/src/kanban/index.ts +0 -13
- package/src/kanban/insert_card_zone.tsx +0 -135
- package/src/kanban/kanban_board.tsx +0 -635
- package/src/kanban/kanban_card.tsx +0 -321
- package/src/kanban/kanban_column.tsx +0 -499
- package/src/kanban/placeholders.tsx +0 -54
- package/src/kanban/types.ts +0 -116
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
GridGroup,
|
|
4
|
-
GridRow,
|
|
5
|
-
GroupPath,
|
|
6
|
-
GroupPathKey,
|
|
7
|
-
groupPathToKey,
|
|
8
|
-
RowPath,
|
|
9
|
-
useGridLayout,
|
|
10
|
-
} from "./layout";
|
|
11
|
-
import { RecycledColumn, RecycledRow, useColumnsRecycler, useRowsRecycler } from "./recycling";
|
|
12
|
-
import { ContentOffset, useScrollToCellOffset } from "./use_scroll_to_cell";
|
|
13
|
-
import { getVisibleIndexRange } from "./visibility";
|
|
14
|
-
|
|
15
|
-
export interface ScrollToOffsetParams extends Partial<ContentOffset> {
|
|
16
|
-
animated?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ScrollToCellParams {
|
|
20
|
-
rowPath?: RowPath;
|
|
21
|
-
column?: number;
|
|
22
|
-
animated?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface GridRef {
|
|
26
|
-
scrollToOffset: (params: ScrollToOffsetParams) => void;
|
|
27
|
-
scrollToCell: (params: ScrollToCellParams) => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface ColumnGroup {
|
|
31
|
-
width: number;
|
|
32
|
-
columns: RecycledColumn[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface UseVirtualGridProps {
|
|
36
|
-
columns: number[];
|
|
37
|
-
collapsedRows?: GroupPathKey[] | null;
|
|
38
|
-
frozenColumnCount: number;
|
|
39
|
-
footerHeight?: number;
|
|
40
|
-
groups: GridGroup[];
|
|
41
|
-
groupHeadingHeight?: number;
|
|
42
|
-
groupSummaryHeight?: number;
|
|
43
|
-
headerHeight?: number;
|
|
44
|
-
height: number;
|
|
45
|
-
leafRowHeight: number;
|
|
46
|
-
onEndReached?: () => void;
|
|
47
|
-
/** Fires when the visible row range changes. Used for viewport-driven page loading. */
|
|
48
|
-
onVisibleRangeChange?: (rowRange: [number, number]) => void;
|
|
49
|
-
rowOverscan?: number;
|
|
50
|
-
columnOverscan?: number;
|
|
51
|
-
spacerHeight?: number;
|
|
52
|
-
width: number;
|
|
53
|
-
/** Maximum width for frozen columns. If frozen columns exceed this, they will be scaled down proportionally. */
|
|
54
|
-
maxFrozenColumnsWidth?: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface UseVirtualGridResult {
|
|
58
|
-
api: GridRef;
|
|
59
|
-
layout: {
|
|
60
|
-
contentHeight: number;
|
|
61
|
-
contentWidth: number;
|
|
62
|
-
frozenColumnsWidth: number;
|
|
63
|
-
scrollableColumnsWidth: number;
|
|
64
|
-
scrollViewHeight: number;
|
|
65
|
-
scrollViewWidth: number;
|
|
66
|
-
totalHeight: number;
|
|
67
|
-
headerHeight: number;
|
|
68
|
-
footerHeight: number;
|
|
69
|
-
};
|
|
70
|
-
frozenColumns: ColumnGroup;
|
|
71
|
-
scrollableColumns: ColumnGroup;
|
|
72
|
-
recycledRows: RecycledRow[];
|
|
73
|
-
rows: GridRow[];
|
|
74
|
-
scrollViewRef: React.RefObject<HTMLDivElement | null>;
|
|
75
|
-
handleScroll: (event: React.UIEvent<HTMLDivElement, UIEvent>) => void;
|
|
76
|
-
isGroupCollapsed: (groupPathKey: GroupPathKey) => boolean;
|
|
77
|
-
/** Whether the grid is scrolled horizontally (scrollX > 0) */
|
|
78
|
-
isScrolledX: boolean;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function useVirtualGrid(props: UseVirtualGridProps): UseVirtualGridResult {
|
|
82
|
-
const {
|
|
83
|
-
collapsedRows = null,
|
|
84
|
-
columns,
|
|
85
|
-
frozenColumnCount,
|
|
86
|
-
footerHeight = 0,
|
|
87
|
-
groups,
|
|
88
|
-
groupHeadingHeight = 0,
|
|
89
|
-
groupSummaryHeight = 0,
|
|
90
|
-
headerHeight = 0,
|
|
91
|
-
height,
|
|
92
|
-
leafRowHeight,
|
|
93
|
-
onEndReached,
|
|
94
|
-
onVisibleRangeChange,
|
|
95
|
-
rowOverscan = 0,
|
|
96
|
-
columnOverscan = 0,
|
|
97
|
-
spacerHeight = 0,
|
|
98
|
-
width,
|
|
99
|
-
maxFrozenColumnsWidth,
|
|
100
|
-
} = props;
|
|
101
|
-
|
|
102
|
-
const scrollViewRef = useRef<HTMLDivElement | null>(null);
|
|
103
|
-
const scrollPositionRef = useRef<ContentOffset>({ x: 0, y: 0 });
|
|
104
|
-
const onEndReachedRef = useRef(onEndReached);
|
|
105
|
-
onEndReachedRef.current = onEndReached;
|
|
106
|
-
const onVisibleRangeChangeRef = useRef(onVisibleRangeChange);
|
|
107
|
-
onVisibleRangeChangeRef.current = onVisibleRangeChange;
|
|
108
|
-
const endReachedFiredRef = useRef(false);
|
|
109
|
-
const visibleRangeRef = useRef<{
|
|
110
|
-
rowRange: [number, number];
|
|
111
|
-
columnRange: [number, number];
|
|
112
|
-
} | null>(null);
|
|
113
|
-
const rafIdRef = useRef<number | null>(null);
|
|
114
|
-
|
|
115
|
-
const [visibleRange, setVisibleRange] = useState<{
|
|
116
|
-
rowRange: [number, number];
|
|
117
|
-
columnRange: [number, number];
|
|
118
|
-
} | null>(null);
|
|
119
|
-
|
|
120
|
-
const isScrolledXRef = useRef(false);
|
|
121
|
-
const [isScrolledX, setIsScrolledX] = useState(isScrolledXRef.current);
|
|
122
|
-
|
|
123
|
-
const {
|
|
124
|
-
contentHeight,
|
|
125
|
-
contentWidth,
|
|
126
|
-
frozenColumns: transformerfrozenColumns,
|
|
127
|
-
frozenColumnsWidth,
|
|
128
|
-
scrollableColumns: transformerScrollableColumns,
|
|
129
|
-
scrollableColumnsWidth,
|
|
130
|
-
rows,
|
|
131
|
-
} = useGridLayout({
|
|
132
|
-
columns,
|
|
133
|
-
frozenColumnCount,
|
|
134
|
-
groups,
|
|
135
|
-
groupHeadingHeight,
|
|
136
|
-
groupSummaryHeight,
|
|
137
|
-
leafRowHeight,
|
|
138
|
-
spacerHeight,
|
|
139
|
-
collapsedRows,
|
|
140
|
-
maxFrozenColumnsWidth,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const scrollViewHeight = useMemo(
|
|
144
|
-
() => height - headerHeight - footerHeight,
|
|
145
|
-
[height, headerHeight, footerHeight],
|
|
146
|
-
);
|
|
147
|
-
const scrollViewWidth = useMemo(() => width - frozenColumnsWidth, [width, frozenColumnsWidth]);
|
|
148
|
-
const totalHeight = useMemo(
|
|
149
|
-
() => contentHeight + headerHeight + footerHeight,
|
|
150
|
-
[contentHeight, headerHeight, footerHeight],
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const calculateVisibleRanges = useCallback(() => {
|
|
154
|
-
const scrollY = scrollPositionRef.current.y;
|
|
155
|
-
const scrollX = scrollPositionRef.current.x;
|
|
156
|
-
|
|
157
|
-
const rowRange = getVisibleIndexRange({
|
|
158
|
-
items: rows,
|
|
159
|
-
getItemOffset: (row) => row.y,
|
|
160
|
-
getItemSize: (row) => row.height,
|
|
161
|
-
scrollOffset: scrollY,
|
|
162
|
-
scrollViewSize: scrollViewHeight,
|
|
163
|
-
overscan: rowOverscan,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const columnRange = getVisibleIndexRange({
|
|
167
|
-
items: transformerScrollableColumns,
|
|
168
|
-
getItemOffset: (col) => col.x,
|
|
169
|
-
getItemSize: (col) => col.width,
|
|
170
|
-
scrollOffset: scrollX,
|
|
171
|
-
scrollViewSize: scrollViewWidth,
|
|
172
|
-
overscan: columnOverscan,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
return { rowRange, columnRange };
|
|
176
|
-
}, [
|
|
177
|
-
rows,
|
|
178
|
-
transformerScrollableColumns,
|
|
179
|
-
scrollViewHeight,
|
|
180
|
-
scrollViewWidth,
|
|
181
|
-
rowOverscan,
|
|
182
|
-
columnOverscan,
|
|
183
|
-
]);
|
|
184
|
-
|
|
185
|
-
const checkAndUpdateVisibleRange = useCallback(() => {
|
|
186
|
-
const newRange = calculateVisibleRanges();
|
|
187
|
-
const prevRange = visibleRangeRef.current;
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
!prevRange ||
|
|
191
|
-
prevRange.rowRange[0] !== newRange.rowRange[0] ||
|
|
192
|
-
prevRange.rowRange[1] !== newRange.rowRange[1] ||
|
|
193
|
-
prevRange.columnRange[0] !== newRange.columnRange[0] ||
|
|
194
|
-
prevRange.columnRange[1] !== newRange.columnRange[1]
|
|
195
|
-
) {
|
|
196
|
-
visibleRangeRef.current = newRange;
|
|
197
|
-
setVisibleRange(newRange);
|
|
198
|
-
onVisibleRangeChangeRef.current?.(newRange.rowRange);
|
|
199
|
-
}
|
|
200
|
-
}, [calculateVisibleRanges]);
|
|
201
|
-
|
|
202
|
-
const frozenColumnsEndIndex = useMemo(
|
|
203
|
-
() => Math.max(0, transformerfrozenColumns.length - 1),
|
|
204
|
-
[transformerfrozenColumns.length],
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const recycledRows = useRowsRecycler({
|
|
208
|
-
rows,
|
|
209
|
-
startIndex: visibleRange?.rowRange[0] ?? 0,
|
|
210
|
-
endIndex: visibleRange?.rowRange[1] ?? 0,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const recycledfrozenColumns = useColumnsRecycler({
|
|
214
|
-
columns: transformerfrozenColumns,
|
|
215
|
-
startIndex: 0,
|
|
216
|
-
endIndex: frozenColumnsEndIndex,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const recycledScrollableColumns = useColumnsRecycler({
|
|
220
|
-
columns: transformerScrollableColumns,
|
|
221
|
-
startIndex: visibleRange?.columnRange[0] ?? 0,
|
|
222
|
-
endIndex: visibleRange?.columnRange[1] ?? 0,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const scrollXForScrollToCellRef = useRef({ current: 0 });
|
|
226
|
-
const scrollYForScrollToCellRef = useRef({ current: 0 });
|
|
227
|
-
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
scrollXForScrollToCellRef.current.current = scrollPositionRef.current.x;
|
|
230
|
-
scrollYForScrollToCellRef.current.current = scrollPositionRef.current.y;
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const getScrollToCellOffset = useScrollToCellOffset({
|
|
234
|
-
rows,
|
|
235
|
-
frozenColumnCount,
|
|
236
|
-
columns: transformerScrollableColumns,
|
|
237
|
-
scrollViewHeight,
|
|
238
|
-
scrollViewWidth,
|
|
239
|
-
scrollX: scrollXForScrollToCellRef.current,
|
|
240
|
-
scrollY: scrollYForScrollToCellRef.current,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const handleScrollToOffset = useCallback((params: ScrollToOffsetParams) => {
|
|
244
|
-
if (scrollViewRef.current && (params.x !== undefined || params.y !== undefined)) {
|
|
245
|
-
scrollViewRef.current.scrollTo({
|
|
246
|
-
left: params.x,
|
|
247
|
-
top: params.y,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}, []);
|
|
251
|
-
|
|
252
|
-
const handleScroll = useCallback(
|
|
253
|
-
(event: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
|
254
|
-
const el = event.currentTarget;
|
|
255
|
-
const newY = el.scrollTop;
|
|
256
|
-
const newX = el.scrollLeft;
|
|
257
|
-
|
|
258
|
-
// Skip sub-pixel scrolling
|
|
259
|
-
const deltaY = Math.abs(newY - scrollPositionRef.current.y);
|
|
260
|
-
const deltaX = Math.abs(newX - scrollPositionRef.current.x);
|
|
261
|
-
if (deltaY < 1 && deltaX < 1) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
scrollPositionRef.current = { y: newY, x: newX };
|
|
266
|
-
|
|
267
|
-
// Update horizontal scroll state for frozen column shadow (only if changed)
|
|
268
|
-
const newIsScrolledX = newX > 0;
|
|
269
|
-
if (isScrolledXRef.current !== newIsScrolledX) {
|
|
270
|
-
isScrolledXRef.current = newIsScrolledX;
|
|
271
|
-
setIsScrolledX(newIsScrolledX);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Fire onEndReached when scroll nears the bottom (within 200px threshold)
|
|
275
|
-
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
276
|
-
if (distanceFromBottom < 200 && !endReachedFiredRef.current) {
|
|
277
|
-
endReachedFiredRef.current = true;
|
|
278
|
-
onEndReachedRef.current?.();
|
|
279
|
-
} else if (distanceFromBottom >= 200) {
|
|
280
|
-
endReachedFiredRef.current = false;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (rafIdRef.current === null) {
|
|
284
|
-
rafIdRef.current = requestAnimationFrame(() => {
|
|
285
|
-
rafIdRef.current = null;
|
|
286
|
-
checkAndUpdateVisibleRange();
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
},
|
|
290
|
-
[checkAndUpdateVisibleRange],
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
useEffect(() => {
|
|
294
|
-
checkAndUpdateVisibleRange();
|
|
295
|
-
}, [
|
|
296
|
-
checkAndUpdateVisibleRange,
|
|
297
|
-
rows,
|
|
298
|
-
transformerScrollableColumns,
|
|
299
|
-
scrollViewHeight,
|
|
300
|
-
scrollViewWidth,
|
|
301
|
-
]);
|
|
302
|
-
|
|
303
|
-
useEffect(() => {
|
|
304
|
-
return () => {
|
|
305
|
-
if (rafIdRef.current !== null) {
|
|
306
|
-
cancelAnimationFrame(rafIdRef.current);
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
}, []);
|
|
310
|
-
|
|
311
|
-
const api = useMemo<GridRef>(
|
|
312
|
-
() => ({
|
|
313
|
-
scrollToOffset: handleScrollToOffset,
|
|
314
|
-
scrollToCell: (params: ScrollToCellParams) =>
|
|
315
|
-
handleScrollToOffset(getScrollToCellOffset(params)),
|
|
316
|
-
}),
|
|
317
|
-
[getScrollToCellOffset, handleScrollToOffset],
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
const frozenColumns: ColumnGroup = useMemo(
|
|
321
|
-
() => ({
|
|
322
|
-
width: frozenColumnsWidth,
|
|
323
|
-
columns: recycledfrozenColumns,
|
|
324
|
-
}),
|
|
325
|
-
[frozenColumnsWidth, recycledfrozenColumns],
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
const scrollableColumns: ColumnGroup = useMemo(
|
|
329
|
-
() => ({
|
|
330
|
-
width: scrollableColumnsWidth,
|
|
331
|
-
columns: recycledScrollableColumns,
|
|
332
|
-
}),
|
|
333
|
-
[scrollableColumnsWidth, recycledScrollableColumns],
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
const collapsedGroupKeys = useMemo(() => {
|
|
337
|
-
if (!collapsedRows || collapsedRows.length === 0) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
return new Set(collapsedRows);
|
|
341
|
-
}, [collapsedRows]);
|
|
342
|
-
|
|
343
|
-
const isGroupCollapsed = useCallback(
|
|
344
|
-
(groupPathKey: GroupPathKey) => collapsedGroupKeys?.has(groupPathKey) ?? false,
|
|
345
|
-
[collapsedGroupKeys],
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
api,
|
|
350
|
-
frozenColumns,
|
|
351
|
-
scrollableColumns,
|
|
352
|
-
recycledRows,
|
|
353
|
-
rows,
|
|
354
|
-
scrollViewRef,
|
|
355
|
-
handleScroll,
|
|
356
|
-
isGroupCollapsed,
|
|
357
|
-
isScrolledX,
|
|
358
|
-
layout: useMemo(
|
|
359
|
-
() => ({
|
|
360
|
-
contentHeight,
|
|
361
|
-
contentWidth,
|
|
362
|
-
frozenColumnsWidth,
|
|
363
|
-
scrollableColumnsWidth,
|
|
364
|
-
scrollViewHeight,
|
|
365
|
-
scrollViewWidth,
|
|
366
|
-
totalHeight,
|
|
367
|
-
headerHeight,
|
|
368
|
-
footerHeight,
|
|
369
|
-
}),
|
|
370
|
-
[
|
|
371
|
-
contentHeight,
|
|
372
|
-
contentWidth,
|
|
373
|
-
frozenColumnsWidth,
|
|
374
|
-
scrollableColumnsWidth,
|
|
375
|
-
scrollViewHeight,
|
|
376
|
-
scrollViewWidth,
|
|
377
|
-
totalHeight,
|
|
378
|
-
headerHeight,
|
|
379
|
-
footerHeight,
|
|
380
|
-
],
|
|
381
|
-
),
|
|
382
|
-
};
|
|
383
|
-
}
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { getVisibleIndexRange } from "./visibility";
|
|
2
|
-
|
|
3
|
-
test("getVisibleIndexRange - basic visibility", () => {
|
|
4
|
-
const items = [
|
|
5
|
-
{ size: 100, offset: 0 },
|
|
6
|
-
{ size: 200, offset: 100 },
|
|
7
|
-
{ size: 100, offset: 300 },
|
|
8
|
-
{ size: 200, offset: 400 },
|
|
9
|
-
{ size: 100, offset: 600 },
|
|
10
|
-
{ size: 200, offset: 700 },
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
const scrollViewSize = 400;
|
|
14
|
-
|
|
15
|
-
const table = [
|
|
16
|
-
[0, 0, 2],
|
|
17
|
-
[100, 1, 3],
|
|
18
|
-
[350, 2, 5],
|
|
19
|
-
[500, 3, 5],
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
for (let i = 0; i < table.length; i++) {
|
|
23
|
-
const [scrollOffset, startIndex, endIndex] = table[i];
|
|
24
|
-
|
|
25
|
-
const result = getVisibleIndexRange({
|
|
26
|
-
items,
|
|
27
|
-
scrollOffset,
|
|
28
|
-
scrollViewSize,
|
|
29
|
-
getItemOffset: (column) => column.offset,
|
|
30
|
-
getItemSize: (column) => column.size,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
expect(result).toEqual([startIndex, endIndex]);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("getVisibleIndexRange - empty items", () => {
|
|
38
|
-
const result = getVisibleIndexRange<{ offset: number; size: number }>({
|
|
39
|
-
items: [],
|
|
40
|
-
scrollOffset: 0,
|
|
41
|
-
scrollViewSize: 400,
|
|
42
|
-
getItemOffset: (item) => item.offset,
|
|
43
|
-
getItemSize: (item) => item.size,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(result).toEqual([0, 0]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("getVisibleIndexRange - single item visible", () => {
|
|
50
|
-
const items = [
|
|
51
|
-
{ size: 100, offset: 0 },
|
|
52
|
-
{ size: 100, offset: 100 },
|
|
53
|
-
{ size: 100, offset: 200 },
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const result = getVisibleIndexRange({
|
|
57
|
-
items,
|
|
58
|
-
scrollOffset: 100,
|
|
59
|
-
scrollViewSize: 100,
|
|
60
|
-
getItemOffset: (item) => item.offset,
|
|
61
|
-
getItemSize: (item) => item.size,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(result).toEqual([1, 1]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("getVisibleIndexRange - with overscan in middle", () => {
|
|
68
|
-
const items = [
|
|
69
|
-
{ size: 100, offset: 0 },
|
|
70
|
-
{ size: 100, offset: 100 },
|
|
71
|
-
{ size: 100, offset: 200 },
|
|
72
|
-
{ size: 100, offset: 300 },
|
|
73
|
-
{ size: 100, offset: 400 },
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
// In the middle: visible item is index 2 (offset 200-300)
|
|
77
|
-
// With overscan=1: should overscan backward to [1] and forward to [3]
|
|
78
|
-
const result = getVisibleIndexRange({
|
|
79
|
-
items,
|
|
80
|
-
scrollOffset: 200,
|
|
81
|
-
scrollViewSize: 100,
|
|
82
|
-
getItemOffset: (item) => item.offset,
|
|
83
|
-
getItemSize: (item) => item.size,
|
|
84
|
-
overscan: 1,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Without overscan: [2, 2] (only item 2 visible)
|
|
88
|
-
// With overscan=1: [1, 3] (overscans 1 backward and 1 forward)
|
|
89
|
-
expect(result).toEqual([1, 3]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("getVisibleIndexRange - overscan in middle with larger overscan", () => {
|
|
93
|
-
const items = [
|
|
94
|
-
{ size: 100, offset: 0 },
|
|
95
|
-
{ size: 100, offset: 100 },
|
|
96
|
-
{ size: 100, offset: 200 },
|
|
97
|
-
{ size: 100, offset: 300 },
|
|
98
|
-
{ size: 100, offset: 400 },
|
|
99
|
-
{ size: 100, offset: 500 },
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
// In the middle: visible item is index 2 (offset 200-300)
|
|
103
|
-
// With overscan=2: should overscan backward to [0, 1] and forward to [3, 4]
|
|
104
|
-
const result = getVisibleIndexRange({
|
|
105
|
-
items,
|
|
106
|
-
scrollOffset: 200,
|
|
107
|
-
scrollViewSize: 100,
|
|
108
|
-
getItemOffset: (item) => item.offset,
|
|
109
|
-
getItemSize: (item) => item.size,
|
|
110
|
-
overscan: 2,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Visible: [2, 2]
|
|
114
|
-
// With overscan=2: [max(2-2, 0), min(2+2, 5)] = [0, 4]
|
|
115
|
-
expect(result).toEqual([0, 4]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("getVisibleIndexRange - overscan at boundaries", () => {
|
|
119
|
-
const items = [
|
|
120
|
-
{ size: 100, offset: 0 },
|
|
121
|
-
{ size: 100, offset: 100 },
|
|
122
|
-
{ size: 100, offset: 200 },
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
// At start - overscan forward is possible (items exist after visible range)
|
|
126
|
-
const resultStart = getVisibleIndexRange({
|
|
127
|
-
items,
|
|
128
|
-
scrollOffset: 0,
|
|
129
|
-
scrollViewSize: 100,
|
|
130
|
-
getItemOffset: (item) => item.offset,
|
|
131
|
-
getItemSize: (item) => item.size,
|
|
132
|
-
overscan: 5,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Can't overscan backward (already at 0), but can overscan forward
|
|
136
|
-
// Visible: [0], with overscan: [0, min(0+5, 2)] = [0, 2]
|
|
137
|
-
expect(resultStart).toEqual([0, 2]);
|
|
138
|
-
|
|
139
|
-
// At end - overscan backward is possible (items exist before visible range)
|
|
140
|
-
const resultEnd = getVisibleIndexRange({
|
|
141
|
-
items,
|
|
142
|
-
scrollOffset: 200,
|
|
143
|
-
scrollViewSize: 100,
|
|
144
|
-
getItemOffset: (item) => item.offset,
|
|
145
|
-
getItemSize: (item) => item.size,
|
|
146
|
-
overscan: 5,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Can't overscan forward (already at end), but can overscan backward
|
|
150
|
-
// Visible: [2], with overscan: [max(2-5, 0), 2] = [0, 2]
|
|
151
|
-
expect(resultEnd).toEqual([0, 2]);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("getVisibleIndexRange - item spans viewport boundary", () => {
|
|
155
|
-
const items = [
|
|
156
|
-
{ size: 100, offset: 0 },
|
|
157
|
-
{ size: 300, offset: 100 }, // Large item that spans multiple positions
|
|
158
|
-
{ size: 100, offset: 400 },
|
|
159
|
-
];
|
|
160
|
-
|
|
161
|
-
const result = getVisibleIndexRange({
|
|
162
|
-
items,
|
|
163
|
-
scrollOffset: 50,
|
|
164
|
-
scrollViewSize: 200,
|
|
165
|
-
getItemOffset: (item) => item.offset,
|
|
166
|
-
getItemSize: (item) => item.size,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Should include the large item that crosses the boundary
|
|
170
|
-
expect(result).toEqual([0, 1]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("getVisibleIndexRange - all items visible", () => {
|
|
174
|
-
const items = [
|
|
175
|
-
{ size: 50, offset: 0 },
|
|
176
|
-
{ size: 50, offset: 50 },
|
|
177
|
-
{ size: 50, offset: 100 },
|
|
178
|
-
];
|
|
179
|
-
|
|
180
|
-
const result = getVisibleIndexRange({
|
|
181
|
-
items,
|
|
182
|
-
scrollOffset: 0,
|
|
183
|
-
scrollViewSize: 200, // Larger than all items
|
|
184
|
-
getItemOffset: (item) => item.offset,
|
|
185
|
-
getItemSize: (item) => item.size,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(result).toEqual([0, 2]);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("getVisibleIndexRange - scrolled past all items", () => {
|
|
192
|
-
const items = [
|
|
193
|
-
{ size: 100, offset: 0 },
|
|
194
|
-
{ size: 100, offset: 100 },
|
|
195
|
-
{ size: 100, offset: 200 },
|
|
196
|
-
];
|
|
197
|
-
|
|
198
|
-
const result = getVisibleIndexRange({
|
|
199
|
-
items,
|
|
200
|
-
scrollOffset: 500, // Way past all items
|
|
201
|
-
scrollViewSize: 100,
|
|
202
|
-
getItemOffset: (item) => item.offset,
|
|
203
|
-
getItemSize: (item) => item.size,
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Should return last item
|
|
207
|
-
expect(result).toEqual([2, 2]);
|
|
208
|
-
});
|
package/src/grid/visibility.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { max, min } from "./helpers";
|
|
2
|
-
|
|
3
|
-
interface GetVisibleIndexRangeParams<T> {
|
|
4
|
-
items: T[];
|
|
5
|
-
scrollOffset: number;
|
|
6
|
-
scrollViewSize: number;
|
|
7
|
-
getItemOffset: (item: T) => number;
|
|
8
|
-
getItemSize: (item: T) => number;
|
|
9
|
-
overscan?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type VisibleIndexRange = [startIndex: number, endIndex: number];
|
|
13
|
-
|
|
14
|
-
export function getVisibleIndexRange<T>(params: GetVisibleIndexRangeParams<T>): VisibleIndexRange {
|
|
15
|
-
const { items, scrollOffset, scrollViewSize, getItemOffset, getItemSize, overscan = 0 } = params;
|
|
16
|
-
|
|
17
|
-
if (items.length === 0) {
|
|
18
|
-
return [0, 0];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Binary search for start index
|
|
22
|
-
let left = 0;
|
|
23
|
-
let right = items.length - 1;
|
|
24
|
-
let startIndex = 0;
|
|
25
|
-
|
|
26
|
-
while (left <= right) {
|
|
27
|
-
const mid = Math.floor((left + right) / 2);
|
|
28
|
-
const offset = getItemOffset(items[mid]);
|
|
29
|
-
|
|
30
|
-
if (offset <= scrollOffset) {
|
|
31
|
-
startIndex = mid;
|
|
32
|
-
left = mid + 1;
|
|
33
|
-
} else {
|
|
34
|
-
right = mid - 1;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Binary search for end index
|
|
39
|
-
const scrollEnd = scrollOffset + scrollViewSize;
|
|
40
|
-
left = startIndex;
|
|
41
|
-
right = items.length - 1;
|
|
42
|
-
let endIndex = startIndex;
|
|
43
|
-
|
|
44
|
-
while (left <= right) {
|
|
45
|
-
const mid = Math.floor((left + right) / 2);
|
|
46
|
-
const item = items[mid];
|
|
47
|
-
const itemEnd = getItemOffset(item) + getItemSize(item);
|
|
48
|
-
|
|
49
|
-
if (itemEnd < scrollEnd) {
|
|
50
|
-
endIndex = mid;
|
|
51
|
-
left = mid + 1;
|
|
52
|
-
} else {
|
|
53
|
-
right = mid - 1;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Ensure we include the item that crosses the boundary
|
|
58
|
-
if (endIndex < items.length - 1) {
|
|
59
|
-
const nextItem = items[endIndex + 1];
|
|
60
|
-
if (getItemOffset(nextItem) < scrollEnd) {
|
|
61
|
-
endIndex++;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Apply overscan in both directions when possible
|
|
66
|
-
if (overscan > 0) {
|
|
67
|
-
if (startIndex > 0) {
|
|
68
|
-
startIndex = max(startIndex - overscan, 0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (endIndex < items.length - 1) {
|
|
72
|
-
endIndex = min(endIndex + overscan, items.length - 1);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return [startIndex, endIndex];
|
|
77
|
-
}
|
package/src/kanban/constants.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Column layout
|
|
2
|
-
export const DEFAULT_COLUMN_WIDTH = 280;
|
|
3
|
-
export const MINIMIZED_COLUMN_WIDTH = 44;
|
|
4
|
-
export const DEFAULT_COLUMN_GAP = 16;
|
|
5
|
-
export const DEFAULT_ITEM_GAP = 8;
|
|
6
|
-
export const COLUMN_CONTENT_PADDING = 12;
|
|
7
|
-
|
|
8
|
-
// Auto-scroll behavior
|
|
9
|
-
export const AUTO_SCROLL_THRESHOLD = 60;
|
|
10
|
-
export const AUTO_SCROLL_SPEED = 8;
|
|
11
|
-
|
|
12
|
-
// Drag detection
|
|
13
|
-
export const DRAG_THRESHOLD = 5;
|
|
14
|
-
|
|
15
|
-
// Touch-specific drag activation (like dnd-kit's delay activation)
|
|
16
|
-
// On touch devices, require a hold before drag can start to distinguish from tap
|
|
17
|
-
export const TOUCH_DRAG_DELAY = 200; // ms - hold time before drag activates on touch
|
|
18
|
-
export const TOUCH_TOLERANCE = 10; // px - max movement allowed during delay period
|