@slithy/react-grid-gallery 0.1.2 → 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 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
@@ -80,7 +80,7 @@ function useGridGallery(items, options, scrollContainerRef) {
80
80
  const [focusedIndex, setFocusedIndex] = useState2(0);
81
81
  const pendingFocusRef = useRef2(null);
82
82
  const loadedSet = useRef2(/* @__PURE__ */ new Set());
83
- const [loadedVersion, setLoadedVersion] = useState2(0);
83
+ const [loadedTick, setLoadedTick] = useState2(0);
84
84
  const virtualRange = useVirtualWindow(containerRef, options.virtualize === true, scrollContainerRef);
85
85
  useEffect2(() => {
86
86
  const observer = new ResizeObserver((entries) => {
@@ -94,7 +94,7 @@ function useGridGallery(items, options, scrollContainerRef) {
94
94
  const onLoad = useCallback((key) => {
95
95
  if (!loadedSet.current.has(key)) {
96
96
  loadedSet.current.add(key);
97
- setLoadedVersion((v) => v + 1);
97
+ setLoadedTick((v) => v + 1);
98
98
  }
99
99
  }, []);
100
100
  const onError = useCallback((_key) => {
@@ -110,36 +110,67 @@ function useGridGallery(items, options, scrollContainerRef) {
110
110
  const resolvedAspectRatio = finitePositive(rawAspectRatio, 1);
111
111
  const cellWidth = containerWidth > 0 ? Math.max(0, Math.floor((containerWidth - resolvedGap * (resolvedColumns - 1)) / resolvedColumns)) : 0;
112
112
  const cellHeight = cellWidth > 0 ? Math.round(cellWidth / resolvedAspectRatio) : 0;
113
- const rows = useMemo(() => {
114
- if (cellWidth === 0) return [];
115
- const layoutRows = computeGridLayout(items, resolvedColumns, cellWidth, cellHeight);
116
- return layoutRows.map((row) => ({
117
- height: row.height,
118
- items: row.items.map((item) => ({
119
- item,
120
- width: row.width,
121
- height: row.height,
122
- loaded: loadedSet.current.has(item.key)
123
- }))
124
- }));
125
- }, [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;
126
115
  const rowStride = cellHeight + resolvedGap;
127
116
  let virtualWindow = null;
128
- if (options.virtualize && virtualRange !== null && rows.length > 0) {
129
- const totalRows = rows.length;
117
+ if (options.virtualize && virtualRange !== null && totalRows > 0 && rowStride > 0) {
130
118
  const overscan = options.overscan ?? cellHeight * 4;
131
119
  const visibleTop = virtualRange.top - overscan;
132
120
  const visibleBottom = virtualRange.bottom + overscan;
133
- let firstIndex = Math.max(0, Math.floor(visibleTop / rowStride));
134
- let lastIndex = Math.min(totalRows - 1, Math.ceil(visibleBottom / rowStride) - 1);
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));
135
124
  if (firstIndex > lastIndex) {
136
- firstIndex = 0;
137
- lastIndex = totalRows - 1;
125
+ lastIndex = firstIndex;
138
126
  }
139
127
  const topSpacerHeight = firstIndex * rowStride;
140
128
  const bottomSpacerHeight = (totalRows - 1 - lastIndex) * rowStride;
141
129
  virtualWindow = { firstIndex, lastIndex, topSpacerHeight, bottomSpacerHeight };
142
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]);
143
174
  const isControlled = options.focusedIndex !== void 0;
144
175
  const padding = options.padding ?? 0;
145
176
  function scrollToRow(rowIndex) {
@@ -233,6 +264,7 @@ function useGridGallery(items, options, scrollContainerRef) {
233
264
  return {
234
265
  containerRef,
235
266
  rows,
267
+ totalRows,
236
268
  cellWidth,
237
269
  cellHeight,
238
270
  gap: resolvedGap,
@@ -249,14 +281,11 @@ function useGridGallery(items, options, scrollContainerRef) {
249
281
  // src/GridGallery.tsx
250
282
  import { jsx, jsxs } from "react/jsx-runtime";
251
283
  function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
252
- 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(
253
285
  items,
254
286
  options,
255
287
  scrollContainerRef
256
288
  );
257
- const firstIndex = virtualWindow?.firstIndex ?? 0;
258
- const lastIndex = virtualWindow?.lastIndex ?? rows.length - 1;
259
- const visibleRows = virtualWindow ? rows.slice(firstIndex, lastIndex + 1) : rows;
260
289
  const padding = options.padding ?? 0;
261
290
  const navigable = options.navigable === true;
262
291
  return /* @__PURE__ */ jsxs(
@@ -264,18 +293,16 @@ function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
264
293
  {
265
294
  ref: containerRef,
266
295
  style: { display: "flex", flexDirection: "column", gap: `${gap}px`, padding: padding > 0 ? `${padding}px` : void 0 },
267
- ...navigable ? { role: "grid", "aria-rowcount": rows.length, "aria-colcount": columns } : {},
296
+ ...navigable ? { role: "grid", "aria-rowcount": totalRows, "aria-colcount": columns } : {},
268
297
  children: [
269
298
  virtualWindow && virtualWindow.topSpacerHeight > 0 && /* @__PURE__ */ jsx("div", { style: { height: virtualWindow.topSpacerHeight, contain: "layout" } }),
270
- visibleRows.map((row, i) => {
271
- const rowIndex = firstIndex + i;
299
+ rows.map((row) => {
272
300
  return /* @__PURE__ */ jsx(
273
301
  "div",
274
302
  {
275
303
  style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: `${gap}px`, contain: "layout" },
276
- ...navigable ? { role: "row", "aria-rowindex": rowIndex + 1 } : {},
277
- children: row.items.map(({ item, loaded }, colIdx) => {
278
- const itemIndex = rowIndex * columns + colIdx;
304
+ ...navigable ? { role: "row", "aria-rowindex": row.rowIndex + 1 } : {},
305
+ children: row.items.map(({ item, itemIndex, colIndex, loaded }) => {
279
306
  const focused = navigable && focusedIndex === itemIndex;
280
307
  return /* @__PURE__ */ jsx(
281
308
  "div",
@@ -283,7 +310,7 @@ function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
283
310
  style: { height: `${cellHeight}px` },
284
311
  ...navigable ? {
285
312
  role: "gridcell",
286
- "aria-colindex": colIdx + 1,
313
+ "aria-colindex": colIndex + 1,
287
314
  tabIndex: focused ? 0 : -1,
288
315
  "data-grid-index": itemIndex,
289
316
  onKeyDown: (e) => handleItemKeyDown(itemIndex, e),
@@ -302,7 +329,7 @@ function GridGallery({ items, renderItem, scrollContainerRef, ...options }) {
302
329
  );
303
330
  })
304
331
  },
305
- rowIndex
332
+ row.rowIndex
306
333
  );
307
334
  }),
308
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.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "React grid gallery component.",
5
5
  "type": "module",
6
6
  "exports": {