From 64d9d6f2ddb54fc79044e49b708fc21bf61afdbf Mon Sep 17 00:00:00 2001 From: Ajetunmobi Isaac Date: Sun, 3 Nov 2024 15:25:26 +0100 Subject: [PATCH 1/3] implemented autoAdjustItemWidth for FlexGrid & ResponsiveGrid --- README.md | 16 +++ package.json | 2 +- src/flex-grid/calc-flex-grid.ts | 133 +++++++++++++++++--- src/flex-grid/index.tsx | 13 +- src/flex-grid/types.ts | 8 ++ src/responsive-grid/calc-responsive-grid.ts | 76 ++++++++--- src/responsive-grid/index.tsx | 6 +- src/responsive-grid/types.ts | 8 ++ 8 files changed, 218 insertions(+), 44 deletions(-) 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..52bf5a0 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; @@ -170,7 +176,8 @@ export const FlexGrid: React.FC = ({ style={[{ flexGrow: 1 }, style]} onLayout={(event) => { const { width, height } = event.nativeEvent.layout; - setContainerSize({ width, height }); + console.log(width, height); + setContainerSize({ width, height: 2000 }); }} > ReactNode; diff --git a/src/responsive-grid/calc-responsive-grid.ts b/src/responsive-grid/calc-responsive-grid.ts index e8f082b..3d2a030 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 = 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; From d1b41f0235dc0382b17fe1498c34689aeefebdac Mon Sep 17 00:00:00 2001 From: Ajetunmobi Isaac Date: Sun, 3 Nov 2024 15:42:42 +0100 Subject: [PATCH 2/3] removed console.log and fixed hardcoded container sizes --- .github/dependabot.yml | 2 +- src/flex-grid/index.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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/src/flex-grid/index.tsx b/src/flex-grid/index.tsx index 52bf5a0..e17ead8 100644 --- a/src/flex-grid/index.tsx +++ b/src/flex-grid/index.tsx @@ -176,8 +176,7 @@ export const FlexGrid: React.FC = ({ style={[{ flexGrow: 1 }, style]} onLayout={(event) => { const { width, height } = event.nativeEvent.layout; - console.log(width, height); - setContainerSize({ width, height: 2000 }); + setContainerSize({ width, height }); }} > Date: Sun, 3 Nov 2024 15:51:10 +0100 Subject: [PATCH 3/3] fix max-width concerns in responsive grid --- src/responsive-grid/calc-responsive-grid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/responsive-grid/calc-responsive-grid.ts b/src/responsive-grid/calc-responsive-grid.ts index 3d2a030..a5c4675 100644 --- a/src/responsive-grid/calc-responsive-grid.ts +++ b/src/responsive-grid/calc-responsive-grid.ts @@ -63,7 +63,7 @@ export const calcResponsiveGrid = ( // If widthRatio exceeds available space, adjust it if (widthRatio > availableWidth) { - widthRatio = availableWidth; + widthRatio = Math.max(1, availableWidth); } }