diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5f0889c..f71d469 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,4 +8,4 @@ updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
- interval: "weekly"
+ interval: "monthly"
diff --git a/README.md b/README.md
index 0a77226..12f3685 100644
--- a/README.md
+++ b/README.md
@@ -306,6 +306,14 @@ A lower value results in more frequent updates, offering smoother visual updates
Defines the threshold for triggering onVerticalEndReached. 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. |
+
+ autoAdjustItemWidth |
+ boolean |
+ true |
+ false |
+ 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 |
+
+
HeaderComponent |
React.ComponentType<any> | React.ReactElement | null | undefined |
@@ -446,6 +454,14 @@ A lower value results in more frequent updates, offering smoother visual updates
Defines the distance from the end of the content at which onEndReached should be triggered, expressed as a proportion of the total content length. For example, a value of 0.1 triggers the callback when the user has scrolled to within 10% of the end of the content. |
+
+ autoAdjustItemWidth |
+ boolean |
+ true |
+ false |
+ 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 |
+
+
HeaderComponent |
React.ComponentType<any> | React.ReactElement | null | undefined |
diff --git a/package.json b/package.json
index ed832a1..4f46eb9 100644
--- a/package.json
+++ b/package.json
@@ -127,4 +127,4 @@
"directories": {
"example": "example"
}
-}
\ No newline at end of file
+}
diff --git a/src/flex-grid/calc-flex-grid.ts b/src/flex-grid/calc-flex-grid.ts
index 82b44a1..fc3bd47 100644
--- a/src/flex-grid/calc-flex-grid.ts
+++ b/src/flex-grid/calc-flex-grid.ts
@@ -3,48 +3,145 @@ import type { FlexGridItem, FlexGridTile } from './types';
export const calcFlexGrid = (
data: FlexGridTile[],
maxColumnRatioUnits: number,
- itemSizeUnit: number
+ itemSizeUnit: number,
+ autoAdjustItemWidth: boolean = true
): {
gridItems: FlexGridItem[];
totalHeight: number;
totalWidth: number;
} => {
const gridItems: FlexGridItem[] = [];
- let columnHeights = new Array(maxColumnRatioUnits).fill(0); // Track the height of each column.
+ let columnHeights = new Array(maxColumnRatioUnits).fill(0);
+
+ const findAvailableWidth = (
+ startColumn: number,
+ currentTop: number
+ ): number => {
+ let availableWidth = 0;
+ let column = startColumn;
+
+ while (column < maxColumnRatioUnits) {
+ // Check for protruding items at this column
+ const hasProtruding = gridItems.some((item) => {
+ const itemBottom = item.top + (item.heightRatio || 1) * itemSizeUnit;
+ const itemLeft = Math.floor(item.left / itemSizeUnit);
+ const itemRight = itemLeft + (item.widthRatio || 1);
+
+ return (
+ item.top < currentTop &&
+ itemBottom > currentTop &&
+ column >= itemLeft &&
+ column < itemRight
+ );
+ });
+
+ if (hasProtruding) {
+ break;
+ }
+
+ availableWidth++;
+ column++;
+ }
+
+ return availableWidth;
+ };
+
+ const findEndOfProtrudingItem = (
+ column: number,
+ currentTop: number
+ ): number => {
+ const protrudingItem = gridItems.find((item) => {
+ const itemBottom = item.top + (item.heightRatio || 1) * itemSizeUnit;
+ const itemLeft = Math.floor(item.left / itemSizeUnit);
+ const itemRight = itemLeft + (item.widthRatio || 1);
+
+ return (
+ item.top < currentTop &&
+ itemBottom > currentTop &&
+ column >= itemLeft &&
+ column < itemRight
+ );
+ });
+
+ if (protrudingItem) {
+ return (
+ Math.floor(protrudingItem.left / itemSizeUnit) +
+ (protrudingItem.widthRatio || 1)
+ );
+ }
+
+ return column;
+ };
+
+ const findNextColumnIndex = (currentTop: number): number => {
+ let nextColumn = 0;
+ let maxColumn = -1;
+
+ // Find the right most occupied column at this height
+ gridItems.forEach((item) => {
+ if (Math.abs(item.top - currentTop) < 0.1) {
+ maxColumn = Math.max(
+ maxColumn,
+ Math.floor(item.left / itemSizeUnit) + (item.widthRatio || 1)
+ );
+ }
+ });
+
+ // If we found items in this row, start after the last one
+ if (maxColumn !== -1) {
+ nextColumn = maxColumn;
+ }
+
+ // Check if there's a protruding item at the next position
+ const protrudingEnd = findEndOfProtrudingItem(nextColumn, currentTop);
+ if (protrudingEnd > nextColumn) {
+ nextColumn = protrudingEnd;
+ }
+
+ return nextColumn;
+ };
data.forEach((item) => {
- const widthRatio = item.widthRatio || 1;
+ let widthRatio = item.widthRatio || 1;
const heightRatio = item.heightRatio || 1;
- // Find the column with the minimum height to start placing the current item.
+ // Find shortest column for current row
let columnIndex = columnHeights.indexOf(Math.min(...columnHeights));
- // If the item doesn't fit in the remaining columns, reset the row.
- if (widthRatio + columnIndex > maxColumnRatioUnits) {
- columnIndex = 0;
- const maxHeight = Math.max(...columnHeights);
- columnHeights.fill(maxHeight); // Align all columns to the height of the tallest column.
+ const currentTop = columnHeights[columnIndex];
+
+ // Find where this item should be placed in the current row
+ columnIndex = findNextColumnIndex(currentTop);
+
+ if (autoAdjustItemWidth) {
+ // Get available width considering both row end and protruding items
+ const availableWidth = findAvailableWidth(columnIndex, currentTop);
+ const remainingWidth = maxColumnRatioUnits - columnIndex;
+
+ // Use the smaller of the two constraints
+ const maxWidth = Math.min(availableWidth, remainingWidth);
+
+ if (widthRatio > maxWidth) {
+ widthRatio = Math.max(1, maxWidth);
+ }
}
- // Push the item with calculated position into the gridItems array.
gridItems.push({
...item,
- top: columnHeights[columnIndex],
+ top: currentTop,
left: columnIndex * itemSizeUnit,
+ widthRatio,
+ heightRatio,
});
- // Update the heights of the columns spanned by this item.
+ // Update column heights
for (let i = columnIndex; i < columnIndex + widthRatio; i++) {
- columnHeights[i] += heightRatio * itemSizeUnit;
+ columnHeights[i] = currentTop + heightRatio * itemSizeUnit;
}
});
- // After positioning all data, calculate the total height of the grid.
- const totalHeight = Math.max(...columnHeights);
-
- // Return the positioned data and the total height of the grid.
return {
gridItems,
- totalHeight,
+ totalHeight: Math.max(...columnHeights),
totalWidth: maxColumnRatioUnits * itemSizeUnit,
};
};
diff --git a/src/flex-grid/index.tsx b/src/flex-grid/index.tsx
index 073c710..e17ead8 100644
--- a/src/flex-grid/index.tsx
+++ b/src/flex-grid/index.tsx
@@ -18,6 +18,7 @@ export const FlexGrid: React.FC = ({
virtualizedBufferFactor = 2,
showScrollIndicator = true,
renderItem = () => null,
+ autoAdjustItemWidth = true,
style = {},
itemContainerStyle = {},
keyExtractor = (_, index) => String(index), // default to item index if no keyExtractor is provided
@@ -43,8 +44,13 @@ export const FlexGrid: React.FC = ({
});
const { totalHeight, totalWidth, gridItems } = useMemo(() => {
- return calcFlexGrid(data, maxColumnRatioUnits, itemSizeUnit);
- }, [data, maxColumnRatioUnits, itemSizeUnit]);
+ return calcFlexGrid(
+ data,
+ maxColumnRatioUnits,
+ itemSizeUnit,
+ autoAdjustItemWidth
+ );
+ }, [data, maxColumnRatioUnits, itemSizeUnit, autoAdjustItemWidth]);
const renderedList = virtualization ? visibleItems : gridItems;
diff --git a/src/flex-grid/types.ts b/src/flex-grid/types.ts
index 1c30f49..14c6e28 100644
--- a/src/flex-grid/types.ts
+++ b/src/flex-grid/types.ts
@@ -20,6 +20,14 @@ export interface FlexGridProps {
/** Defines the base unit size for grid items. Actual item size is calculated by multiplying this with width and height ratios. */
itemSizeUnit: number;
+ /**
+ * 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
+ * @default true
+ */
+ autoAdjustItemWidth?: boolean;
+
/** Function to render each item in the grid. Receives the item and its index as parameters. */
renderItem: ({ item, index }: RenderItemProps) => ReactNode;
diff --git a/src/responsive-grid/calc-responsive-grid.ts b/src/responsive-grid/calc-responsive-grid.ts
index e8f082b..a5c4675 100644
--- a/src/responsive-grid/calc-responsive-grid.ts
+++ b/src/responsive-grid/calc-responsive-grid.ts
@@ -4,37 +4,77 @@ export const calcResponsiveGrid = (
data: TileItem[],
maxItemsPerColumn: number,
containerWidth: number,
- itemUnitHeight?: number
+ itemUnitHeight?: number,
+ autoAdjustItemWidth: boolean = true
): {
gridItems: GridItem[];
gridViewHeight: number;
} => {
const gridItems: GridItem[] = [];
- const itemSizeUnit = containerWidth / maxItemsPerColumn; // Determine TileSize based on container width and max number of columns
- let columnHeights: number[] = new Array(maxItemsPerColumn).fill(0); // Track the height of each column end.
+ const itemSizeUnit = containerWidth / maxItemsPerColumn;
+ let columnHeights: number[] = new Array(maxItemsPerColumn).fill(0);
- data.forEach((item) => {
- const widthRatio = item.widthRatio || 1;
- const heightRatio = item.heightRatio || 1;
+ const findAvailableWidth = (
+ startColumn: number,
+ currentTop: number
+ ): number => {
+ // Check each column from the start position
+ let availableWidth = 0;
- const itemWidth = widthRatio * itemSizeUnit;
+ for (let i = startColumn; i < maxItemsPerColumn; i++) {
+ // Check if there's any item from above rows protruding into this space
+ const hasProtrudingItem = gridItems.some((item) => {
+ const itemBottom = item.top + item.height;
+ const itemRight = item.left + item.width;
+ return (
+ item.top < currentTop && // Item starts above current row
+ itemBottom > currentTop && // Item extends into current row
+ item.left <= i * itemSizeUnit && // Item starts at or before this column
+ itemRight > i * itemSizeUnit // Item extends into this column
+ );
+ });
- const itemHeight = itemUnitHeight
- ? itemUnitHeight * heightRatio
- : heightRatio * itemSizeUnit; // Use itemUnitHeight if provided, else fallback to itemSizeUnit
+ if (hasProtrudingItem) {
+ break; // Stop counting available width when we hit a protruding item
+ }
+
+ availableWidth++;
+ }
+
+ return availableWidth;
+ };
+
+ data.forEach((item) => {
+ let widthRatio = item.widthRatio || 1;
+ const heightRatio = item.heightRatio || 1;
- // Find the column where the item should be placed.
let columnIndex = findColumnForItem(
columnHeights,
widthRatio,
maxItemsPerColumn
);
- // Calculate item's top and left positions.
+ if (autoAdjustItemWidth) {
+ // Get current row's height at the column index
+ const currentTop = columnHeights[columnIndex];
+
+ // Calculate available width considering both row end and protruding items
+ const availableWidth = findAvailableWidth(columnIndex, currentTop!);
+
+ // If widthRatio exceeds available space, adjust it
+ if (widthRatio > availableWidth) {
+ widthRatio = Math.max(1, availableWidth);
+ }
+ }
+
+ const itemWidth = widthRatio * itemSizeUnit;
+ const itemHeight = itemUnitHeight
+ ? itemUnitHeight * heightRatio
+ : heightRatio * itemSizeUnit;
+
const top = columnHeights[columnIndex]!;
const left = columnIndex * itemSizeUnit;
- // Place the item.
gridItems.push({
...item,
top,
@@ -43,19 +83,15 @@ export const calcResponsiveGrid = (
height: itemHeight,
});
- // Update the column heights for the columns that the item spans.
- // This needs to accommodate the actual height used (itemHeight).
+ // Update the column heights
for (let i = columnIndex; i < columnIndex + widthRatio; i++) {
- columnHeights[i] = top + itemHeight; // Update to use itemHeight
+ columnHeights[i] = top + itemHeight;
}
});
- // Calculate the total height of the grid.
- const gridViewHeight = Math.max(...columnHeights);
-
return {
gridItems,
- gridViewHeight,
+ gridViewHeight: Math.max(...columnHeights),
};
};
diff --git a/src/responsive-grid/index.tsx b/src/responsive-grid/index.tsx
index 5019c24..1beb781 100644
--- a/src/responsive-grid/index.tsx
+++ b/src/responsive-grid/index.tsx
@@ -14,6 +14,7 @@ export const ResponsiveGrid: React.FC = ({
maxItemsPerColumn = 3,
virtualizedBufferFactor = 5,
renderItem,
+ autoAdjustItemWidth = true,
scrollEventInterval = 200, // milliseconds
virtualization = true,
showScrollIndicator = true,
@@ -44,9 +45,10 @@ export const ResponsiveGrid: React.FC = ({
data,
maxItemsPerColumn,
containerSize.width,
- itemUnitHeight
+ itemUnitHeight,
+ autoAdjustItemWidth
),
- [data, maxItemsPerColumn, containerSize]
+ [data, maxItemsPerColumn, containerSize, autoAdjustItemWidth]
);
const renderedItems = virtualization ? visibleItems : gridItems;
diff --git a/src/responsive-grid/types.ts b/src/responsive-grid/types.ts
index 00fd338..9578d18 100644
--- a/src/responsive-grid/types.ts
+++ b/src/responsive-grid/types.ts
@@ -20,6 +20,14 @@ export interface ResponsiveGridProps {
/** Defines the maximum number of items that can be displayed within a single column of the grid. */
maxItemsPerColumn: number;
+ /**
+ * 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
+ * @default true
+ */
+ autoAdjustItemWidth?: boolean;
+
/** Interval in milliseconds at which scroll events are processed for virtualization. Default is 200ms. */
scrollEventInterval?: number;