Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function SortableGrid<I>(props: SortableGridProps<I>) {
columns,
data,
keyExtractor = defaultKeyExtractor,
masonry,
onActiveItemDropped,
onDragEnd: _onDragEnd,
onDragMove,
Expand Down Expand Up @@ -78,6 +79,7 @@ function SortableGrid<I>(props: SortableGridProps<I>) {
groups={groups}
isVertical={isVertical}
key={useStrategyKey(strategy)}
masonry={masonry}
rowHeight={rowHeight} // must be specified for horizontal grids
strategy={strategy}
onDragEnd={onDragEnd}
Expand Down Expand Up @@ -108,6 +110,7 @@ const SortableGridInner = typedMemo(function SortableGridInner<I>({
isVertical,
itemEntering,
itemExiting,
masonry,
overflow,
rowGap: _rowGap,
rowHeight,
Expand Down Expand Up @@ -141,6 +144,7 @@ const SortableGridInner = typedMemo(function SortableGridInner<I>({
controlledItemDimensions={controlledItemDimensions}
debug={debug}
isVertical={isVertical}
masonry={masonry}
numGroups={groups}
rowGap={rowGap}
rowHeight={rowHeight}
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native-sortables/src/constants/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export const DEFAULT_SORTABLE_GRID_PROPS = {
columns: 1,
keyExtractor: defaultKeyExtractor,
rowGap: 0,
strategy: 'insert'
strategy: 'insert',
masonry: false
} satisfies DefaultSortableGridProps;

export const DEFAULT_SORTABLE_FLEX_PROPS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ const { AutoOffsetAdjustmentProvider, useAutoOffsetAdjustmentContext } =
isVertical,
itemHeights,
itemWidths,
masonry,
numGroups
} = props;
const crossItemSizes = isVertical ? itemHeights : itemWidths;
Expand All @@ -226,6 +227,7 @@ const { AutoOffsetAdjustmentProvider, useAutoOffsetAdjustmentContext } =
crossGap: gaps.cross,
crossItemSizes,
indexToKey: indexToKey,
masonry,
numGroups
} as const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ export type GridLayoutProviderProps = PropsWithChildren<{
rowGap: SharedValue<number>;
columnGap: SharedValue<number>;
rowHeight?: number;
masonry?: boolean;
}>;

const { GridLayoutProvider, useGridLayoutContext } = createProvider(
'GridLayout'
)<GridLayoutProviderProps, GridLayoutContextType>(({
columnGap,
isVertical,
masonry,
numGroups,
rowGap,
rowHeight
Expand Down Expand Up @@ -170,6 +172,7 @@ const { GridLayoutProvider, useGridLayoutContext } = createProvider(
isVertical,
itemHeights: itemHeights.value,
itemWidths: itemWidths.value,
masonry,
numGroups,
requestId: layoutRequestId.value // Helper to force layout re-calculation
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,103 @@ import type {
import { resolveDimension } from '../../../../utils';
import { getCrossIndex, getMainIndex } from './helpers';

export const calculateLayout = ({
/**
* Calculates masonry-style layout where items stack within each column.
* Items maintain their sequential grid order (respecting columns). Vertical spacing
* between items in a column is controlled by gaps.cross (rowGap when vertical).
*/
const calculateMasonryLayout = ({
gaps,
indexToKey,
isVertical,
itemHeights,
itemWidths,
numGroups,
startCrossOffset
}: GridLayoutProps): GridLayout | null => {
'worklet';
const mainGroupSize = (isVertical ? itemWidths : itemHeights) as
| null
| number;

if (!mainGroupSize) {
return null;
}

const itemPositions: Record<string, Vector> = {};

let mainCoordinate: Coordinate;
let crossCoordinate: Coordinate;
let crossItemSizes;

if (isVertical) {
// grid with specified number of columns (vertical orientation)
mainCoordinate = 'x';
crossCoordinate = 'y';
crossItemSizes = itemHeights;
} else {
// grid with specified number of rows (horizontal orientation)
mainCoordinate = 'y';
crossCoordinate = 'x';
crossItemSizes = itemWidths;
}

// Track the current height/position of each column independently
// Each column stacks its items, separated by the configured cross gap
const columnHeights = new Array(numGroups).fill(startCrossOffset ?? 0);

for (const [itemIndex, itemKey] of indexToKey.entries()) {
const crossItemSize = resolveDimension(crossItemSizes, itemKey);

if (crossItemSize === null) {
return null;
}

// Determine which column this item belongs to based on grid order
const mainIndex = getMainIndex(itemIndex, numGroups);
const crossAxisOffset = columnHeights[mainIndex]!;

// Update item position - place it at the current column height
itemPositions[itemKey] = {
[crossCoordinate]: crossAxisOffset,
[mainCoordinate]: mainIndex * (mainGroupSize + gaps.main)
} as Vector;

// Update column height - advance by item size plus cross gap
columnHeights[mainIndex] = crossAxisOffset + crossItemSize + gaps.cross;
}

// Container size is determined by the tallest column
const rawMaxColumnHeight = Math.max(...columnHeights);
const baseCrossOffset = startCrossOffset ?? 0;
// Remove the trailing cross gap from the tallest column if at least one item exists
const maxColumnHeight =
rawMaxColumnHeight > baseCrossOffset
? Math.max(rawMaxColumnHeight - gaps.cross, baseCrossOffset)
: rawMaxColumnHeight;
const mainSize = (mainGroupSize + gaps.main) * numGroups - gaps.main;

return {
containerCrossSize: maxColumnHeight,
contentBounds: [
{
[crossCoordinate]: startCrossOffset ?? 0,
[mainCoordinate]: 0
} as Vector,
{
[crossCoordinate]: maxColumnHeight,
[mainCoordinate]: mainSize
} as Vector
],
crossAxisOffsets: columnHeights,
itemPositions
};
};

/**
* Calculates standard grid layout where items in the same row align vertically
*/
const calculateStandardLayout = ({
gaps,
indexToKey,
isVertical,
Expand Down Expand Up @@ -95,14 +191,59 @@ export const calculateLayout = ({
};
};

export const calculateLayout = (props: GridLayoutProps): GridLayout | null => {
'worklet';
return props.masonry
? calculateMasonryLayout(props)
: calculateStandardLayout(props);
};

export const calculateItemCrossOffset = ({
crossGap,
crossItemSizes,
indexToKey,
itemKey,
masonry,
numGroups
}: AutoOffsetAdjustmentProps): number => {
'worklet';

if (masonry) {
// Masonry layout: calculate offset within the same group (column for vertical, row for horizontal)
// Find the target item's index and group
let targetItemIndex = -1;
for (let i = 0; i < indexToKey.length; i++) {
if (indexToKey[i] === itemKey) {
targetItemIndex = i;
break;
}
}

if (targetItemIndex === -1) {
return 0;
}

const targetGroup = getMainIndex(targetItemIndex, numGroups);
let offset = 0;

// Sum cross-axis sizes of all items in the same group that come before the target item
// For vertical grids: sums heights of items in the same column
// For horizontal grids: sums widths of items in the same row
for (let i = 0; i < targetItemIndex; i++) {
const group = getMainIndex(i, numGroups);
if (group === targetGroup) {
const key = indexToKey[i]!;
const itemSize = resolveDimension(crossItemSizes, key);
if (itemSize !== null) {
offset += itemSize + crossGap;
}
}
}

return offset;
}

// Standard grid layout: calculate offset using row-based logic
let activeItemCrossOffset = 0;
let currentGroupCrossSize = 0;
let currentGroupCrossIndex = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function GridProvider({
children,
columnGap: columnGap_,
isVertical,
masonry,
numGroups,
rowGap: rowGap_,
rowHeight,
Expand All @@ -42,6 +43,7 @@ export default function GridProvider({
const sharedGridProviderProps = {
columnGap,
isVertical,
masonry,
numGroups,
rowGap
};
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-sortables/src/types/layout/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type GridLayoutProps = {
shouldAnimateLayout?: boolean;
requestNextLayout?: boolean;
startCrossOffset?: Maybe<number>;
masonry?: boolean;
};

export type GridLayout = {
Expand All @@ -29,4 +30,5 @@ export type AutoOffsetAdjustmentProps = {
crossItemSizes: ItemSizes;
indexToKey: Array<string>;
numGroups: number;
masonry?: boolean;
};
7 changes: 7 additions & 0 deletions packages/react-native-sortables/src/types/props/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ export type SortableGridProps<I> = Simplify<
* @important Works only for horizontal grids. Requires the rows property to be set.
*/
rowHeight?: number;
/** When true, renders the grid in masonry-style layout, allowing items of different sizes to stack without gaps, maintaining the sequential grid order.
*
* RowGap and columnGap still apply
*
* @default false
*/
masonry?: boolean;
}
>;

Expand Down
Loading