@slithy/react-grid-gallery 0.1.1 → 0.2.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/dist/index.d.ts +5 -0
- package/dist/index.js +77 -40
- package/package.json +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -22,8 +22,12 @@ type GridLayoutRow<T> = {
|
|
|
22
22
|
height: number;
|
|
23
23
|
};
|
|
24
24
|
type GridRow<T> = {
|
|
25
|
+
rowIndex: number;
|
|
26
|
+
startIndex: number;
|
|
25
27
|
items: Array<{
|
|
26
28
|
item: GalleryItem<T>;
|
|
29
|
+
itemIndex: number;
|
|
30
|
+
colIndex: number;
|
|
27
31
|
width: number;
|
|
28
32
|
height: number;
|
|
29
33
|
loaded: boolean;
|
|
@@ -42,6 +46,7 @@ type VirtualWindow = {
|
|
|
42
46
|
declare function useGridGallery<T>(items: GalleryItem<T>[], options: GridOptions, scrollContainerRef?: ScrollContainerRef): {
|
|
43
47
|
containerRef: RefObject<HTMLDivElement | null>;
|
|
44
48
|
rows: GridRow<T>[];
|
|
49
|
+
totalRows: number;
|
|
45
50
|
cellWidth: number;
|
|
46
51
|
cellHeight: number;
|
|
47
52
|
gap: number;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/computeGridLayout.ts
|
|
2
2
|
function computeGridLayout(items, columns, cellWidth, cellHeight) {
|
|
3
|
-
if (items.length === 0 || columns <= 0 || cellWidth <= 0) return [];
|
|
3
|
+
if (items.length === 0 || !Number.isFinite(columns) || !Number.isFinite(cellWidth) || !Number.isFinite(cellHeight) || columns <= 0 || cellWidth <= 0 || cellHeight < 0) return [];
|
|
4
|
+
const columnCount = Math.max(1, Math.floor(columns));
|
|
4
5
|
const rows = [];
|
|
5
|
-
for (let i = 0; i < items.length; i +=
|
|
6
|
+
for (let i = 0; i < items.length; i += columnCount) {
|
|
6
7
|
rows.push({
|
|
7
|
-
items: items.slice(i, i +
|
|
8
|
+
items: items.slice(i, i + columnCount),
|
|
8
9
|
width: cellWidth,
|
|
9
10
|
height: cellHeight
|
|
10
11
|
});
|
|
@@ -67,13 +68,19 @@ function useVirtualWindow(containerRef, enabled, scrollContainerRef) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// src/useGridGallery.ts
|
|
71
|
+
function finitePositive(value, fallback) {
|
|
72
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
73
|
+
}
|
|
74
|
+
function finiteNonNegative(value, fallback = 0) {
|
|
75
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
76
|
+
}
|
|
70
77
|
function useGridGallery(items, options, scrollContainerRef) {
|
|
71
78
|
const containerRef = useRef2(null);
|
|
72
79
|
const [containerWidth, setContainerWidth] = useState2(0);
|
|
73
80
|
const [focusedIndex, setFocusedIndex] = useState2(0);
|
|
74
81
|
const pendingFocusRef = useRef2(null);
|
|
75
82
|
const loadedSet = useRef2(/* @__PURE__ */ new Set());
|
|
76
|
-
const [
|
|
83
|
+
const [loadedTick, setLoadedTick] = useState2(0);
|
|
77
84
|
const virtualRange = useVirtualWindow(containerRef, options.virtualize === true, scrollContainerRef);
|
|
78
85
|
useEffect2(() => {
|
|
79
86
|
const observer = new ResizeObserver((entries) => {
|
|
@@ -87,49 +94,83 @@ function useGridGallery(items, options, scrollContainerRef) {
|
|
|
87
94
|
const onLoad = useCallback((key) => {
|
|
88
95
|
if (!loadedSet.current.has(key)) {
|
|
89
96
|
loadedSet.current.add(key);
|
|
90
|
-
|
|
97
|
+
setLoadedTick((v) => v + 1);
|
|
91
98
|
}
|
|
92
99
|
}, []);
|
|
93
100
|
const onError = useCallback((_key) => {
|
|
94
101
|
}, []);
|
|
102
|
+
const rawColumns = typeof options.columns === "function" ? options.columns(containerWidth) : options.columns;
|
|
95
103
|
const resolvedColumns = Math.max(
|
|
96
104
|
1,
|
|
97
|
-
Math.round(
|
|
105
|
+
Math.round(finitePositive(rawColumns, 1))
|
|
98
106
|
);
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
107
|
+
const rawGap = typeof options.gap === "function" ? options.gap(containerWidth) : options.gap ?? 0;
|
|
108
|
+
const resolvedGap = finiteNonNegative(rawGap);
|
|
109
|
+
const rawAspectRatio = typeof options.aspectRatio === "function" ? options.aspectRatio(containerWidth) : options.aspectRatio ?? 1;
|
|
110
|
+
const resolvedAspectRatio = finitePositive(rawAspectRatio, 1);
|
|
111
|
+
const cellWidth = containerWidth > 0 ? Math.max(0, Math.floor((containerWidth - resolvedGap * (resolvedColumns - 1)) / resolvedColumns)) : 0;
|
|
102
112
|
const cellHeight = cellWidth > 0 ? Math.round(cellWidth / resolvedAspectRatio) : 0;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const layoutRows = computeGridLayout(items, resolvedColumns, cellWidth, cellHeight);
|
|
106
|
-
return layoutRows.map((row) => ({
|
|
107
|
-
height: row.height,
|
|
108
|
-
items: row.items.map((item) => ({
|
|
109
|
-
item,
|
|
110
|
-
width: row.width,
|
|
111
|
-
height: row.height,
|
|
112
|
-
loaded: loadedSet.current.has(item.key)
|
|
113
|
-
}))
|
|
114
|
-
}));
|
|
115
|
-
}, [items, resolvedColumns, cellWidth, cellHeight, loadedVersion]);
|
|
113
|
+
const hasLayout = cellWidth > 0 && cellHeight >= 0 && items.length > 0;
|
|
114
|
+
const totalRows = hasLayout ? Math.ceil(items.length / resolvedColumns) : 0;
|
|
116
115
|
const rowStride = cellHeight + resolvedGap;
|
|
117
116
|
let virtualWindow = null;
|
|
118
|
-
if (options.virtualize && virtualRange !== null &&
|
|
119
|
-
const totalRows = rows.length;
|
|
117
|
+
if (options.virtualize && virtualRange !== null && totalRows > 0 && rowStride > 0) {
|
|
120
118
|
const overscan = options.overscan ?? cellHeight * 4;
|
|
121
119
|
const visibleTop = virtualRange.top - overscan;
|
|
122
120
|
const visibleBottom = virtualRange.bottom + overscan;
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
const maxRowIndex = totalRows - 1;
|
|
122
|
+
const firstIndex = Math.min(maxRowIndex, Math.max(0, Math.floor(visibleTop / rowStride)));
|
|
123
|
+
let lastIndex = Math.min(maxRowIndex, Math.max(0, Math.ceil(visibleBottom / rowStride) - 1));
|
|
125
124
|
if (firstIndex > lastIndex) {
|
|
126
|
-
|
|
127
|
-
lastIndex = totalRows - 1;
|
|
125
|
+
lastIndex = firstIndex;
|
|
128
126
|
}
|
|
129
127
|
const topSpacerHeight = firstIndex * rowStride;
|
|
130
128
|
const bottomSpacerHeight = (totalRows - 1 - lastIndex) * rowStride;
|
|
131
129
|
virtualWindow = { firstIndex, lastIndex, topSpacerHeight, bottomSpacerHeight };
|
|
132
130
|
}
|
|
131
|
+
const rows = useMemo(() => {
|
|
132
|
+
if (!hasLayout) return [];
|
|
133
|
+
if (!options.virtualize) {
|
|
134
|
+
const layoutRows = computeGridLayout(items, resolvedColumns, cellWidth, cellHeight);
|
|
135
|
+
return layoutRows.map((row, rowIndex) => {
|
|
136
|
+
const startIndex = rowIndex * resolvedColumns;
|
|
137
|
+
return {
|
|
138
|
+
rowIndex,
|
|
139
|
+
startIndex,
|
|
140
|
+
height: row.height,
|
|
141
|
+
items: row.items.map((item, colIndex) => ({
|
|
142
|
+
item,
|
|
143
|
+
itemIndex: startIndex + colIndex,
|
|
144
|
+
colIndex,
|
|
145
|
+
width: row.width,
|
|
146
|
+
height: row.height,
|
|
147
|
+
loaded: loadedSet.current.has(item.key)
|
|
148
|
+
}))
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (virtualWindow === null) return [];
|
|
153
|
+
const renderRows = [];
|
|
154
|
+
for (let rowIndex = virtualWindow.firstIndex; rowIndex <= virtualWindow.lastIndex; rowIndex++) {
|
|
155
|
+
const startIndex = rowIndex * resolvedColumns;
|
|
156
|
+
const endIndex = Math.min(startIndex + resolvedColumns, items.length);
|
|
157
|
+
const rowItems = items.slice(startIndex, endIndex);
|
|
158
|
+
renderRows.push({
|
|
159
|
+
rowIndex,
|
|
160
|
+
startIndex,
|
|
161
|
+
height: cellHeight,
|
|
162
|
+
items: rowItems.map((item, colIndex) => ({
|
|
163
|
+
item,
|
|
164
|
+
itemIndex: startIndex + colIndex,
|
|
165
|
+
colIndex,
|
|
166
|
+
width: cellWidth,
|
|
167
|
+
height: cellHeight,
|
|
168
|
+
loaded: loadedSet.current.has(item.key)
|
|
169
|
+
}))
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return renderRows;
|
|
173
|
+
}, [cellHeight, cellWidth, hasLayout, items, options.virtualize, resolvedColumns, virtualWindow, loadedTick]);
|
|
133
174
|
const isControlled = options.focusedIndex !== void 0;
|
|
134
175
|
const padding = options.padding ?? 0;
|
|
135
176
|
function scrollToRow(rowIndex) {
|
|
@@ -223,6 +264,7 @@ function useGridGallery(items, options, scrollContainerRef) {
|
|
|
223
264
|
return {
|
|
224
265
|
containerRef,
|
|
225
266
|
rows,
|
|
267
|
+
totalRows,
|
|
226
268
|
cellWidth,
|
|
227
269
|
cellHeight,
|
|
228
270
|
gap: resolvedGap,
|
|
@@ -239,14 +281,11 @@ function useGridGallery(items, options, scrollContainerRef) {
|
|
|
239
281
|
// src/GridGallery.tsx
|
|
240
282
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
241
283
|
function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
|
|
242
|
-
const { containerRef, rows, cellHeight, gap, columns, onLoad, onError, virtualWindow, focusedIndex, handleItemFocus, handleItemKeyDown } = useGridGallery(
|
|
284
|
+
const { containerRef, rows, totalRows, cellHeight, gap, columns, onLoad, onError, virtualWindow, focusedIndex, handleItemFocus, handleItemKeyDown } = useGridGallery(
|
|
243
285
|
items,
|
|
244
286
|
options,
|
|
245
287
|
scrollContainerRef
|
|
246
288
|
);
|
|
247
|
-
const firstIndex = virtualWindow?.firstIndex ?? 0;
|
|
248
|
-
const lastIndex = virtualWindow?.lastIndex ?? rows.length - 1;
|
|
249
|
-
const visibleRows = virtualWindow ? rows.slice(firstIndex, lastIndex + 1) : rows;
|
|
250
289
|
const padding = options.padding ?? 0;
|
|
251
290
|
const navigable = options.navigable === true;
|
|
252
291
|
return /* @__PURE__ */ jsxs(
|
|
@@ -254,18 +293,16 @@ function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
|
|
|
254
293
|
{
|
|
255
294
|
ref: containerRef,
|
|
256
295
|
style: { display: "flex", flexDirection: "column", gap: `${gap}px`, padding: padding > 0 ? `${padding}px` : void 0 },
|
|
257
|
-
...navigable ? { role: "grid", "aria-rowcount":
|
|
296
|
+
...navigable ? { role: "grid", "aria-rowcount": totalRows, "aria-colcount": columns } : {},
|
|
258
297
|
children: [
|
|
259
298
|
virtualWindow && virtualWindow.topSpacerHeight > 0 && /* @__PURE__ */ jsx("div", { style: { height: virtualWindow.topSpacerHeight, contain: "layout" } }),
|
|
260
|
-
|
|
261
|
-
const rowIndex = firstIndex + i;
|
|
299
|
+
rows.map((row) => {
|
|
262
300
|
return /* @__PURE__ */ jsx(
|
|
263
301
|
"div",
|
|
264
302
|
{
|
|
265
303
|
style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: `${gap}px`, contain: "layout" },
|
|
266
|
-
...navigable ? { role: "row", "aria-rowindex": rowIndex + 1 } : {},
|
|
267
|
-
children: row.items.map(({ item, loaded }
|
|
268
|
-
const itemIndex = rowIndex * columns + colIdx;
|
|
304
|
+
...navigable ? { role: "row", "aria-rowindex": row.rowIndex + 1 } : {},
|
|
305
|
+
children: row.items.map(({ item, itemIndex, colIndex, loaded }) => {
|
|
269
306
|
const focused = navigable && focusedIndex === itemIndex;
|
|
270
307
|
return /* @__PURE__ */ jsx(
|
|
271
308
|
"div",
|
|
@@ -273,7 +310,7 @@ function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
|
|
|
273
310
|
style: { height: `${cellHeight}px` },
|
|
274
311
|
...navigable ? {
|
|
275
312
|
role: "gridcell",
|
|
276
|
-
"aria-colindex":
|
|
313
|
+
"aria-colindex": colIndex + 1,
|
|
277
314
|
tabIndex: focused ? 0 : -1,
|
|
278
315
|
"data-grid-index": itemIndex,
|
|
279
316
|
onKeyDown: (e) => handleItemKeyDown(itemIndex, e),
|
|
@@ -292,7 +329,7 @@ function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
|
|
|
292
329
|
);
|
|
293
330
|
})
|
|
294
331
|
},
|
|
295
|
-
rowIndex
|
|
332
|
+
row.rowIndex
|
|
296
333
|
);
|
|
297
334
|
}),
|
|
298
335
|
virtualWindow && virtualWindow.bottomSpacerHeight > 0 && /* @__PURE__ */ jsx("div", { style: { height: virtualWindow.bottomSpacerHeight, contain: "layout" } })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slithy/react-grid-gallery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React grid gallery component.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,13 +28,16 @@
|
|
|
28
28
|
"tsup": "^8",
|
|
29
29
|
"typescript": "^5",
|
|
30
30
|
"vitest": "^4.1.2",
|
|
31
|
-
"@slithy/
|
|
32
|
-
"@slithy/
|
|
31
|
+
"@slithy/eslint-config": "0.0.0",
|
|
32
|
+
"@slithy/tsconfig": "0.0.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
36
36
|
"url": "https://github.com/mjcampagna/react-grid-gallery"
|
|
37
37
|
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
38
41
|
"author": "mjcampagna",
|
|
39
42
|
"license": "MIT",
|
|
40
43
|
"scripts": {
|