From 1f65a1d7bf03c3420d8d82c539ad6603c35c83b1 Mon Sep 17 00:00:00 2001 From: Yen-Wei Liu Date: Wed, 6 Nov 2024 09:17:53 -0800 Subject: [PATCH] Masonry: Support flexible for uniformRow layout (#3857) --- packages/gestalt/src/Masonry.tsx | 63 ++----- .../gestalt/src/Masonry/getLayoutAlgorithm.ts | 5 +- packages/gestalt/src/Masonry/types.ts | 3 +- .../src/Masonry/uniformRowLayout.test.ts | 163 +++++++++++------- .../gestalt/src/Masonry/uniformRowLayout.ts | 52 +++++- 5 files changed, 168 insertions(+), 118 deletions(-) diff --git a/packages/gestalt/src/Masonry.tsx b/packages/gestalt/src/Masonry.tsx index fc176115c3..4a58881ddc 100644 --- a/packages/gestalt/src/Masonry.tsx +++ b/packages/gestalt/src/Masonry.tsx @@ -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; @@ -599,49 +597,22 @@ export default class Masonry extends ReactComponent, State> { items.length === 0 && _loadingStateItems && _renderLoadingStateItems, ); - let getPositions: ( - itemsToGetPosition: readonly T[] | readonly LoadingStateItem[], - ) => ReadonlyArray; - - 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; diff --git a/packages/gestalt/src/Masonry/getLayoutAlgorithm.ts b/packages/gestalt/src/Masonry/getLayoutAlgorithm.ts index 74886b2afe..09440dccf9 100644 --- a/packages/gestalt/src/Masonry/getLayoutAlgorithm.ts +++ b/packages/gestalt/src/Masonry/getLayoutAlgorithm.ts @@ -22,7 +22,7 @@ export default function getLayoutAlgorithm({ _earlyBailout, }: { align: Align; - columnWidth: number; + columnWidth: number | undefined; gutter?: number; items: ReadonlyArray; layout: Layout; @@ -54,11 +54,12 @@ export default function getLayoutAlgorithm({ earlyBailout: _earlyBailout, }); } - if (layout === 'uniformRow') { + if (layout.startsWith('uniformRow')) { return uniformRowLayout({ cache: measurementStore, columnWidth, gutter, + flexible: layout === 'uniformRowFlexible', minCols, width, renderLoadingState, diff --git a/packages/gestalt/src/Masonry/types.ts b/packages/gestalt/src/Masonry/types.ts index dd734e0c80..56f5a35ff0 100644 --- a/packages/gestalt/src/Masonry/types.ts +++ b/packages/gestalt/src/Masonry/types.ts @@ -26,6 +26,7 @@ export type Layout = | 'basicCentered' | 'flexible' | 'serverRenderedFlexible' - | 'uniformRow'; + | 'uniformRow' + | 'uniformRowFlexible'; export type LoadingStateItem = { height: number }; diff --git a/packages/gestalt/src/Masonry/uniformRowLayout.test.ts b/packages/gestalt/src/Masonry/uniformRowLayout.test.ts index 2a8ffefbd9..b65b4b8073 100644 --- a/packages/gestalt/src/Masonry/uniformRowLayout.test.ts +++ b/packages/gestalt/src/Masonry/uniformRowLayout.test.ts @@ -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', () => { diff --git a/packages/gestalt/src/Masonry/uniformRowLayout.ts b/packages/gestalt/src/Masonry/uniformRowLayout.ts index ed1f549552..563916e9f0 100644 --- a/packages/gestalt/src/Masonry/uniformRowLayout.ts +++ b/packages/gestalt/src/Masonry/uniformRowLayout.ts @@ -9,10 +9,48 @@ 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 = ({ cache, - columnWidth = 236, + columnWidth: idealColumnWidth = 236, + flexible = false, gutter = 14, width, minCols = 3, @@ -20,6 +58,7 @@ const uniformRowLayout = }: { cache: Cache; columnWidth?: number; + flexible?: boolean; gutter?: number; width?: number | null | undefined; minCols?: number; @@ -27,11 +66,16 @@ const uniformRowLayout = }): ((items: ReadonlyArray | ReadonlyArray) => ReadonlyArray) => (items: ReadonlyArray | ReadonlyArray): ReadonlyArray => { 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 = []; return items.map((item, i) => {