Skip to content

Commit

Permalink
Masonry: Support flexible for uniformRow layout (#3857)
Browse files Browse the repository at this point in the history
  • Loading branch information
liuyenwei authored Nov 6, 2024
1 parent ce86350 commit 1f65a1d
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 118 deletions.
63 changes: 17 additions & 46 deletions packages/gestalt/src/Masonry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import debounce, { DebounceReturn } from './debounce';
import FetchItems from './FetchItems';
import styles from './Masonry.css';
import { Cache } from './Masonry/Cache';
import defaultLayout from './Masonry/defaultLayout';
import recalcHeights from './Masonry/dynamicHeightsUtils';
import fullWidthLayout from './Masonry/fullWidthLayout';
import getLayoutAlgorithm from './Masonry/getLayoutAlgorithm';
import ItemResizeObserverWrapper from './Masonry/ItemResizeObserverWrapper';
import MeasurementStore from './Masonry/MeasurementStore';
import { ColumnSpanConfig, MULTI_COL_ITEMS_MEASURE_BATCH_SIZE } from './Masonry/multiColumnLayout';
import ScrollContainer from './Masonry/ScrollContainer';
import { getElementHeight, getRelativeScrollTop, getScrollPos } from './Masonry/scrollUtils';
import { Align, Layout, LoadingStateItem, Position } from './Masonry/types';
import uniformRowLayout from './Masonry/uniformRowLayout';
import throttle, { ThrottleReturn } from './throttle';

const RESIZE_DEBOUNCE = 300;
Expand Down Expand Up @@ -599,49 +597,22 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
items.length === 0 && _loadingStateItems && _renderLoadingStateItems,
);

let getPositions: (
itemsToGetPosition: readonly T[] | readonly LoadingStateItem[],
) => ReadonlyArray<Position>;

if ((layout === 'flexible' || layout === 'serverRenderedFlexible') && width !== null) {
getPositions = fullWidthLayout({
gutter,
measurementCache: measurementStore,
positionCache: positionStore,
minCols,
idealColumnWidth: columnWidth,
width,
logWhitespace: _logTwoColWhitespace,
_getColumnSpanConfig,
renderLoadingState,
earlyBailout: _earlyBailout,
});
} else if (layout === 'uniformRow') {
getPositions = uniformRowLayout({
cache: measurementStore,
columnWidth,
gutter,
minCols,
width,
renderLoadingState,
});
} else {
getPositions = defaultLayout({
align,
measurementCache: measurementStore,
positionCache: positionStore,
columnWidth,
gutter,
layout,
minCols,
rawItemCount: renderLoadingState ? _loadingStateItems.length : items.length,
width,
logWhitespace: _logTwoColWhitespace,
_getColumnSpanConfig,
renderLoadingState,
earlyBailout: _earlyBailout,
});
}
const getPositions = getLayoutAlgorithm({
align,
columnWidth,
gutter,
items,
layout,
measurementStore,
positionStore,
minCols,
width,
_getColumnSpanConfig,
_logTwoColWhitespace,
_loadingStateItems,
renderLoadingState,
_earlyBailout,
});

let gridBody;

Expand Down
5 changes: 3 additions & 2 deletions packages/gestalt/src/Masonry/getLayoutAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function getLayoutAlgorithm<T>({
_earlyBailout,
}: {
align: Align;
columnWidth: number;
columnWidth: number | undefined;
gutter?: number;
items: ReadonlyArray<T>;
layout: Layout;
Expand Down Expand Up @@ -54,11 +54,12 @@ export default function getLayoutAlgorithm<T>({
earlyBailout: _earlyBailout,
});
}
if (layout === 'uniformRow') {
if (layout.startsWith('uniformRow')) {
return uniformRowLayout({
cache: measurementStore,
columnWidth,
gutter,
flexible: layout === 'uniformRowFlexible',
minCols,
width,
renderLoadingState,
Expand Down
3 changes: 2 additions & 1 deletion packages/gestalt/src/Masonry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type Layout =
| 'basicCentered'
| 'flexible'
| 'serverRenderedFlexible'
| 'uniformRow';
| 'uniformRow'
| 'uniformRowFlexible';

export type LoadingStateItem = { height: number };
163 changes: 98 additions & 65 deletions packages/gestalt/src/Masonry/uniformRowLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,84 +23,117 @@ const stubCache = (
};
};

test('empty', () => {
const layout = uniformRowLayout({
cache: stubCache(),
width: 500,
describe.each([false, true])('Uniform Row layout tests', (flexible) => {
test('empty', () => {
const layout = uniformRowLayout({
cache: stubCache(),
width: 500,
});

expect(layout([])).toEqual([]);
});

expect(layout([])).toEqual([]);
});
test('one row, equal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 100,
c: 100,
}),
flexible,
minCols: 2,
width: 900,
});

const expectedPositions = flexible
? [
{ top: 0, left: 0, width: 286, height: 100 },
{ top: 0, left: 300, width: 286, height: 100 },
{ top: 0, left: 600, width: 286, height: 100 },
]
: [
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 0, left: 250, width: 236, height: 100 },
{ top: 0, left: 500, width: 236, height: 100 },
];

test('one row, equal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 100,
c: 100,
}),
width: 500,
expect(layout(['a', 'b', 'c'])).toEqual(expectedPositions);
});

expect(layout(['a', 'b', 'c'])).toEqual([
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 0, left: 250, width: 236, height: 100 },
{ top: 0, left: 500, width: 236, height: 100 },
]);
});
test('one column, equal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 100,
c: 100,
}),
flexible,
width: 250,
minCols: 1,
});

test('one column, equal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 100,
c: 100,
}),
width: 250,
minCols: 1,
expect(layout(['a', 'b', 'c'])).toEqual([
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 114, left: 0, width: 236, height: 100 },
{ top: 228, left: 0, width: 236, height: 100 },
]);
});

expect(layout(['a', 'b', 'c'])).toEqual([
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 114, left: 0, width: 236, height: 100 },
{ top: 228, left: 0, width: 236, height: 100 },
]);
});
test('one row, unequal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 120,
c: 100,
}),
flexible,
minCols: 2,
width: 900,
});

const expectedPositions = flexible
? [
{ top: 0, left: 0, width: 286, height: 100 },
{ top: 0, left: 300, width: 286, height: 120 },
{ top: 0, left: 600, width: 286, height: 100 },
]
: [
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 0, left: 250, width: 236, height: 120 },
{ top: 0, left: 500, width: 236, height: 100 },
];

test('one row, unequal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 120,
c: 100,
}),
width: 500,
expect(layout(['a', 'b', 'c'])).toEqual(expectedPositions);
});

expect(layout(['a', 'b', 'c'])).toEqual([
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 0, left: 250, width: 236, height: 120 },
{ top: 0, left: 500, width: 236, height: 100 },
]);
});
test('multiple rows, unequal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 120,
c: 100,
d: 100,
}),
flexible,
width: 800,
});

test('multiple rows, unequal heights', () => {
const layout = uniformRowLayout({
cache: stubCache({
a: 100,
b: 120,
c: 100,
d: 100,
}),
width: 750,
});
const expectedPositions = flexible
? [
{ top: 0, left: 0, width: 252, height: 100 },
{ top: 0, left: 266, width: 252, height: 120 },
{ top: 0, left: 532, width: 252, height: 100 },
{ top: 134, left: 0, width: 252, height: 100 },
]
: [
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 0, left: 250, width: 236, height: 120 },
{ top: 0, left: 500, width: 236, height: 100 },
{ top: 134, left: 0, width: 236, height: 100 },
];

expect(layout(['a', 'b', 'c', 'd'])).toEqual([
{ top: 0, left: 0, width: 236, height: 100 },
{ top: 0, left: 250, width: 236, height: 120 },
{ top: 0, left: 500, width: 236, height: 100 },
{ top: 134, left: 0, width: 236, height: 100 },
]);
expect(layout(['a', 'b', 'c', 'd'])).toEqual(expectedPositions);
});
});

describe('loadingStateItems', () => {
Expand Down
52 changes: 48 additions & 4 deletions packages/gestalt/src/Masonry/uniformRowLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,73 @@ const offscreen = (width: number, height: number = Infinity) => ({
height,
});

function calculateColumnCountAndWidth({
columnWidth: idealColumnWidth,
flexible,
gutter,
minCols,
width,
}: {
columnWidth: number;
flexible: boolean;
gutter: number;
minCols: number;
width: number;
}) {
if (flexible) {
const colguess = Math.floor(width / idealColumnWidth);
const columnCount = Math.max(
Math.floor((width - colguess * gutter) / idealColumnWidth),
minCols,
);
const columnWidth = Math.floor(width / columnCount) - gutter;
const columnWidthAndGutter = columnWidth + gutter;
return {
columnCount,
columnWidth,
columnWidthAndGutter,
};
}

const columnWidthAndGutter = idealColumnWidth + gutter;
const columnCount = Math.max(Math.floor((width + gutter) / columnWidthAndGutter), minCols);
return {
columnCount,
columnWidth: idealColumnWidth,
columnWidthAndGutter,
};
}

const uniformRowLayout =
<T>({
cache,
columnWidth = 236,
columnWidth: idealColumnWidth = 236,
flexible = false,
gutter = 14,
width,
minCols = 3,
renderLoadingState,
}: {
cache: Cache<T, number>;
columnWidth?: number;
flexible?: boolean;
gutter?: number;
width?: number | null | undefined;
minCols?: number;
renderLoadingState?: boolean;
}): ((items: ReadonlyArray<T> | ReadonlyArray<LoadingStateItem>) => ReadonlyArray<Position>) =>
(items: ReadonlyArray<T> | ReadonlyArray<LoadingStateItem>): ReadonlyArray<Position> => {
if (width == null) {
return items.map(() => offscreen(columnWidth));
return items.map(() => offscreen(idealColumnWidth));
}

const columnWidthAndGutter = columnWidth + gutter;
const columnCount = Math.max(Math.floor((width + gutter) / columnWidthAndGutter), minCols);
const { columnWidth, columnWidthAndGutter, columnCount } = calculateColumnCountAndWidth({
columnWidth: idealColumnWidth,
flexible,
gutter,
minCols,
width,
});

const heights: Array<number> = [];
return items.map((item, i) => {
Expand Down

0 comments on commit 1f65a1d

Please sign in to comment.