Skip to content

Commit ab26939

Browse files
authored
Merge pull request #42 from iNerdStack/feat/add-auto-adjust-item-width
implemented autoAdjustItemWidth for FlexGrid & ResponsiveGrid
2 parents d9bfa55 + be04ee2 commit ab26939

File tree

9 files changed

+217
-44
lines changed

9 files changed

+217
-44
lines changed

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ updates:
88
- package-ecosystem: "npm" # See documentation for possible values
99
directory: "/" # Location of package manifests
1010
schedule:
11-
interval: "weekly"
11+
interval: "monthly"

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,14 @@ A lower value results in more frequent updates, offering smoother visual updates
306306
<td>Defines the threshold for triggering <code>onVerticalEndReached</code>. Represented as a fraction of the total height of the scrollable grid, indicating how far from the end the vertical scroll must be to trigger the event.</td>
307307
</tr>
308308

309+
<tr>
310+
<td><code>autoAdjustItemWidth</code></td>
311+
<td><code>boolean</code></td>
312+
<td><code>true</code></td>
313+
<td><code>false</code></td>
314+
<td> Prevents width overflow by adjusting items with width ratios that exceed available columns in their row & width overlap by adjusting items that would overlap with items extending from previous rows</td>
315+
</tr>
316+
309317
<tr>
310318
<td><code>HeaderComponent</code></td>
311319
<td><code>React.ComponentType&lt;any&gt; | React.ReactElement | null | undefined</code></td>
@@ -446,6 +454,14 @@ A lower value results in more frequent updates, offering smoother visual updates
446454
<td> Defines the distance from the end of the content at which <code>onEndReached</code> should be triggered, expressed as a proportion of the total content length. For example, a value of <code>0.1</code> triggers the callback when the user has scrolled to within 10% of the end of the content. </td>
447455
</tr>
448456

457+
<tr>
458+
<td><code>autoAdjustItemWidth</code></td>
459+
<td><code>boolean</code></td>
460+
<td><code>true</code></td>
461+
<td><code>false</code></td>
462+
<td> Prevents width overflow by adjusting items with width ratios that exceed available columns in their row & width overlap by adjusting items that would overlap with items extending from previous rows</td>
463+
</tr>
464+
449465
<tr>
450466
<td><code>HeaderComponent</code></td>
451467
<td><code>React.ComponentType&lt;any&gt; | React.ReactElement | null | undefined</code></td>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@
127127
"directories": {
128128
"example": "example"
129129
}
130-
}
130+
}

src/flex-grid/calc-flex-grid.ts

Lines changed: 115 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,145 @@ import type { FlexGridItem, FlexGridTile } from './types';
33
export const calcFlexGrid = (
44
data: FlexGridTile[],
55
maxColumnRatioUnits: number,
6-
itemSizeUnit: number
6+
itemSizeUnit: number,
7+
autoAdjustItemWidth: boolean = true
78
): {
89
gridItems: FlexGridItem[];
910
totalHeight: number;
1011
totalWidth: number;
1112
} => {
1213
const gridItems: FlexGridItem[] = [];
13-
let columnHeights = new Array(maxColumnRatioUnits).fill(0); // Track the height of each column.
14+
let columnHeights = new Array(maxColumnRatioUnits).fill(0);
15+
16+
const findAvailableWidth = (
17+
startColumn: number,
18+
currentTop: number
19+
): number => {
20+
let availableWidth = 0;
21+
let column = startColumn;
22+
23+
while (column < maxColumnRatioUnits) {
24+
// Check for protruding items at this column
25+
const hasProtruding = gridItems.some((item) => {
26+
const itemBottom = item.top + (item.heightRatio || 1) * itemSizeUnit;
27+
const itemLeft = Math.floor(item.left / itemSizeUnit);
28+
const itemRight = itemLeft + (item.widthRatio || 1);
29+
30+
return (
31+
item.top < currentTop &&
32+
itemBottom > currentTop &&
33+
column >= itemLeft &&
34+
column < itemRight
35+
);
36+
});
37+
38+
if (hasProtruding) {
39+
break;
40+
}
41+
42+
availableWidth++;
43+
column++;
44+
}
45+
46+
return availableWidth;
47+
};
48+
49+
const findEndOfProtrudingItem = (
50+
column: number,
51+
currentTop: number
52+
): number => {
53+
const protrudingItem = gridItems.find((item) => {
54+
const itemBottom = item.top + (item.heightRatio || 1) * itemSizeUnit;
55+
const itemLeft = Math.floor(item.left / itemSizeUnit);
56+
const itemRight = itemLeft + (item.widthRatio || 1);
57+
58+
return (
59+
item.top < currentTop &&
60+
itemBottom > currentTop &&
61+
column >= itemLeft &&
62+
column < itemRight
63+
);
64+
});
65+
66+
if (protrudingItem) {
67+
return (
68+
Math.floor(protrudingItem.left / itemSizeUnit) +
69+
(protrudingItem.widthRatio || 1)
70+
);
71+
}
72+
73+
return column;
74+
};
75+
76+
const findNextColumnIndex = (currentTop: number): number => {
77+
let nextColumn = 0;
78+
let maxColumn = -1;
79+
80+
// Find the right most occupied column at this height
81+
gridItems.forEach((item) => {
82+
if (Math.abs(item.top - currentTop) < 0.1) {
83+
maxColumn = Math.max(
84+
maxColumn,
85+
Math.floor(item.left / itemSizeUnit) + (item.widthRatio || 1)
86+
);
87+
}
88+
});
89+
90+
// If we found items in this row, start after the last one
91+
if (maxColumn !== -1) {
92+
nextColumn = maxColumn;
93+
}
94+
95+
// Check if there's a protruding item at the next position
96+
const protrudingEnd = findEndOfProtrudingItem(nextColumn, currentTop);
97+
if (protrudingEnd > nextColumn) {
98+
nextColumn = protrudingEnd;
99+
}
100+
101+
return nextColumn;
102+
};
14103

15104
data.forEach((item) => {
16-
const widthRatio = item.widthRatio || 1;
105+
let widthRatio = item.widthRatio || 1;
17106
const heightRatio = item.heightRatio || 1;
18107

19-
// Find the column with the minimum height to start placing the current item.
108+
// Find shortest column for current row
20109
let columnIndex = columnHeights.indexOf(Math.min(...columnHeights));
21-
// If the item doesn't fit in the remaining columns, reset the row.
22-
if (widthRatio + columnIndex > maxColumnRatioUnits) {
23-
columnIndex = 0;
24-
const maxHeight = Math.max(...columnHeights);
25-
columnHeights.fill(maxHeight); // Align all columns to the height of the tallest column.
110+
const currentTop = columnHeights[columnIndex];
111+
112+
// Find where this item should be placed in the current row
113+
columnIndex = findNextColumnIndex(currentTop);
114+
115+
if (autoAdjustItemWidth) {
116+
// Get available width considering both row end and protruding items
117+
const availableWidth = findAvailableWidth(columnIndex, currentTop);
118+
const remainingWidth = maxColumnRatioUnits - columnIndex;
119+
120+
// Use the smaller of the two constraints
121+
const maxWidth = Math.min(availableWidth, remainingWidth);
122+
123+
if (widthRatio > maxWidth) {
124+
widthRatio = Math.max(1, maxWidth);
125+
}
26126
}
27127

28-
// Push the item with calculated position into the gridItems array.
29128
gridItems.push({
30129
...item,
31-
top: columnHeights[columnIndex],
130+
top: currentTop,
32131
left: columnIndex * itemSizeUnit,
132+
widthRatio,
133+
heightRatio,
33134
});
34135

35-
// Update the heights of the columns spanned by this item.
136+
// Update column heights
36137
for (let i = columnIndex; i < columnIndex + widthRatio; i++) {
37-
columnHeights[i] += heightRatio * itemSizeUnit;
138+
columnHeights[i] = currentTop + heightRatio * itemSizeUnit;
38139
}
39140
});
40141

41-
// After positioning all data, calculate the total height of the grid.
42-
const totalHeight = Math.max(...columnHeights);
43-
44-
// Return the positioned data and the total height of the grid.
45142
return {
46143
gridItems,
47-
totalHeight,
144+
totalHeight: Math.max(...columnHeights),
48145
totalWidth: maxColumnRatioUnits * itemSizeUnit,
49146
};
50147
};

src/flex-grid/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const FlexGrid: React.FC<FlexGridProps> = ({
1818
virtualizedBufferFactor = 2,
1919
showScrollIndicator = true,
2020
renderItem = () => null,
21+
autoAdjustItemWidth = true,
2122
style = {},
2223
itemContainerStyle = {},
2324
keyExtractor = (_, index) => String(index), // default to item index if no keyExtractor is provided
@@ -43,8 +44,13 @@ export const FlexGrid: React.FC<FlexGridProps> = ({
4344
});
4445

4546
const { totalHeight, totalWidth, gridItems } = useMemo(() => {
46-
return calcFlexGrid(data, maxColumnRatioUnits, itemSizeUnit);
47-
}, [data, maxColumnRatioUnits, itemSizeUnit]);
47+
return calcFlexGrid(
48+
data,
49+
maxColumnRatioUnits,
50+
itemSizeUnit,
51+
autoAdjustItemWidth
52+
);
53+
}, [data, maxColumnRatioUnits, itemSizeUnit, autoAdjustItemWidth]);
4854

4955
const renderedList = virtualization ? visibleItems : gridItems;
5056

src/flex-grid/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export interface FlexGridProps {
2020
/** Defines the base unit size for grid items. Actual item size is calculated by multiplying this with width and height ratios. */
2121
itemSizeUnit: number;
2222

23+
/**
24+
* Prevents width overflow by adjusting items with width ratios that exceed
25+
* available columns in their row & width overlap by adjusting items that would overlap with items
26+
* extending from previous rows
27+
* @default true
28+
*/
29+
autoAdjustItemWidth?: boolean;
30+
2331
/** Function to render each item in the grid. Receives the item and its index as parameters. */
2432
renderItem: ({ item, index }: RenderItemProps) => ReactNode;
2533

src/responsive-grid/calc-responsive-grid.ts

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,77 @@ export const calcResponsiveGrid = (
44
data: TileItem[],
55
maxItemsPerColumn: number,
66
containerWidth: number,
7-
itemUnitHeight?: number
7+
itemUnitHeight?: number,
8+
autoAdjustItemWidth: boolean = true
89
): {
910
gridItems: GridItem[];
1011
gridViewHeight: number;
1112
} => {
1213
const gridItems: GridItem[] = [];
13-
const itemSizeUnit = containerWidth / maxItemsPerColumn; // Determine TileSize based on container width and max number of columns
14-
let columnHeights: number[] = new Array(maxItemsPerColumn).fill(0); // Track the height of each column end.
14+
const itemSizeUnit = containerWidth / maxItemsPerColumn;
15+
let columnHeights: number[] = new Array(maxItemsPerColumn).fill(0);
1516

16-
data.forEach((item) => {
17-
const widthRatio = item.widthRatio || 1;
18-
const heightRatio = item.heightRatio || 1;
17+
const findAvailableWidth = (
18+
startColumn: number,
19+
currentTop: number
20+
): number => {
21+
// Check each column from the start position
22+
let availableWidth = 0;
1923

20-
const itemWidth = widthRatio * itemSizeUnit;
24+
for (let i = startColumn; i < maxItemsPerColumn; i++) {
25+
// Check if there's any item from above rows protruding into this space
26+
const hasProtrudingItem = gridItems.some((item) => {
27+
const itemBottom = item.top + item.height;
28+
const itemRight = item.left + item.width;
29+
return (
30+
item.top < currentTop && // Item starts above current row
31+
itemBottom > currentTop && // Item extends into current row
32+
item.left <= i * itemSizeUnit && // Item starts at or before this column
33+
itemRight > i * itemSizeUnit // Item extends into this column
34+
);
35+
});
2136

22-
const itemHeight = itemUnitHeight
23-
? itemUnitHeight * heightRatio
24-
: heightRatio * itemSizeUnit; // Use itemUnitHeight if provided, else fallback to itemSizeUnit
37+
if (hasProtrudingItem) {
38+
break; // Stop counting available width when we hit a protruding item
39+
}
40+
41+
availableWidth++;
42+
}
43+
44+
return availableWidth;
45+
};
46+
47+
data.forEach((item) => {
48+
let widthRatio = item.widthRatio || 1;
49+
const heightRatio = item.heightRatio || 1;
2550

26-
// Find the column where the item should be placed.
2751
let columnIndex = findColumnForItem(
2852
columnHeights,
2953
widthRatio,
3054
maxItemsPerColumn
3155
);
3256

33-
// Calculate item's top and left positions.
57+
if (autoAdjustItemWidth) {
58+
// Get current row's height at the column index
59+
const currentTop = columnHeights[columnIndex];
60+
61+
// Calculate available width considering both row end and protruding items
62+
const availableWidth = findAvailableWidth(columnIndex, currentTop!);
63+
64+
// If widthRatio exceeds available space, adjust it
65+
if (widthRatio > availableWidth) {
66+
widthRatio = Math.max(1, availableWidth);
67+
}
68+
}
69+
70+
const itemWidth = widthRatio * itemSizeUnit;
71+
const itemHeight = itemUnitHeight
72+
? itemUnitHeight * heightRatio
73+
: heightRatio * itemSizeUnit;
74+
3475
const top = columnHeights[columnIndex]!;
3576
const left = columnIndex * itemSizeUnit;
3677

37-
// Place the item.
3878
gridItems.push({
3979
...item,
4080
top,
@@ -43,19 +83,15 @@ export const calcResponsiveGrid = (
4383
height: itemHeight,
4484
});
4585

46-
// Update the column heights for the columns that the item spans.
47-
// This needs to accommodate the actual height used (itemHeight).
86+
// Update the column heights
4887
for (let i = columnIndex; i < columnIndex + widthRatio; i++) {
49-
columnHeights[i] = top + itemHeight; // Update to use itemHeight
88+
columnHeights[i] = top + itemHeight;
5089
}
5190
});
5291

53-
// Calculate the total height of the grid.
54-
const gridViewHeight = Math.max(...columnHeights);
55-
5692
return {
5793
gridItems,
58-
gridViewHeight,
94+
gridViewHeight: Math.max(...columnHeights),
5995
};
6096
};
6197

src/responsive-grid/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const ResponsiveGrid: React.FC<ResponsiveGridProps> = ({
1414
maxItemsPerColumn = 3,
1515
virtualizedBufferFactor = 5,
1616
renderItem,
17+
autoAdjustItemWidth = true,
1718
scrollEventInterval = 200, // milliseconds
1819
virtualization = true,
1920
showScrollIndicator = true,
@@ -44,9 +45,10 @@ export const ResponsiveGrid: React.FC<ResponsiveGridProps> = ({
4445
data,
4546
maxItemsPerColumn,
4647
containerSize.width,
47-
itemUnitHeight
48+
itemUnitHeight,
49+
autoAdjustItemWidth
4850
),
49-
[data, maxItemsPerColumn, containerSize]
51+
[data, maxItemsPerColumn, containerSize, autoAdjustItemWidth]
5052
);
5153

5254
const renderedItems = virtualization ? visibleItems : gridItems;

src/responsive-grid/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export interface ResponsiveGridProps {
2020
/** Defines the maximum number of items that can be displayed within a single column of the grid. */
2121
maxItemsPerColumn: number;
2222

23+
/**
24+
* Prevents width overflow by adjusting items with width ratios that exceed
25+
* available columns in their row & width overlap by adjusting items that would overlap with items
26+
* extending from previous rows
27+
* @default true
28+
*/
29+
autoAdjustItemWidth?: boolean;
30+
2331
/** Interval in milliseconds at which scroll events are processed for virtualization. Default is 200ms. */
2432
scrollEventInterval?: number;
2533

0 commit comments

Comments
 (0)