Skip to content

Commit

Permalink
Add prop to experiment with these changes
Browse files Browse the repository at this point in the history
  • Loading branch information
bryamrrr committed Nov 21, 2024
1 parent 89e826d commit 8a73f60
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 152 deletions.
9 changes: 3 additions & 6 deletions docs/integration-test-helpers/masonry/ExampleGridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,12 @@ export default function ExampleGridItem({ data = {}, itemIdx, expanded }: Props)
const isTwoColItem = data.columnSpan === 2;

return (
<div
style={{
padding: '0 7px 14px',
}}
>
<div>
<div
className="grid-item-test"
style={{
height: expanded ? data.height + 100 : data.height,
border: '1px solid #ff0000',
boxSizing: 'border-box',
background: isTwoColItem ? 'black' : data.color,
color: isTwoColItem ? 'white' : undefined,
}}
Expand Down
4 changes: 3 additions & 1 deletion docs/integration-test-helpers/masonry/MasonryContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ export default class MasonryContainer extends Component<Props<Record<any, any>>,
{mountGrid && (
<MasonryComponent
ref={this.gridRef}
_dynamicHeights
_dynamicHeightsV2Experiment
_getColumnSpanConfig={(item) => {
const columnSpan = item.columnSpan as number | undefined;
return columnSpan ?? 1;
Expand All @@ -419,7 +421,7 @@ export default class MasonryContainer extends Component<Props<Record<any, any>>,
: undefined
}
columnWidth={columnWidth}
gutterWidth={0}
gutterWidth={14}
items={items}
layout={flexible ? 'flexible' : undefined}
measurementStore={externalCache ? measurementStore : undefined}
Expand Down
38 changes: 29 additions & 9 deletions packages/gestalt/src/Masonry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import FetchItems from './FetchItems';
import styles from './Masonry.css';
import { Cache } from './Masonry/Cache';
import recalcHeights from './Masonry/dynamicHeightsUtils';
import recalcHeightsV2 from './Masonry/dynamicHeightsV2Utils';
import getLayoutAlgorithm from './Masonry/getLayoutAlgorithm';
import ItemResizeObserverWrapper from './Masonry/ItemResizeObserverWrapper';
import MeasurementStore from './Masonry/MeasurementStore';
Expand Down Expand Up @@ -148,6 +149,12 @@ type Props<T> = {
*/
_dynamicHeights?: boolean;
/**
* Experimental flag to enable an experiment to use a revamped version of dynamic heights (This needs _dynamicHeights enabled)
*/
_dynamicHeightsV2Experiment?: boolean;
/**
/**
*
* Experimental prop to enable early bailout when positioning multicolumn modules
*
* This is an experimental prop and may be removed or changed in the future
Expand Down Expand Up @@ -225,21 +232,34 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
const changedItem: T = this.state.items[idx]!;
const newHeight = contentRect.height;

// TODO: DefaultGutter comes from getLayoutAlgorithm and their utils, everything should be in one place (this.gutter?)
const { layout, gutterWidth } = this.props;
let defaultGutter = 14;
if ((layout && layout === 'flexible') || layout === 'serverRenderedFlexible') {
defaultGutter = 0;
}

triggerUpdate =
recalcHeights({
items: this.state.items,
changedItem,
newHeight,
positionStore: this.positionStore,
measurementStore: this.state.measurementStore,
gutterWidth: gutterWidth ?? defaultGutter,
}) || triggerUpdate;
/* eslint-disable-next-line no-underscore-dangle */
if (props._dynamicHeightsV2Experiment) {
triggerUpdate =
recalcHeightsV2({
items: this.state.items,
changedItem,
newHeight,
positionStore: this.positionStore,
measurementStore: this.state.measurementStore,
gutterWidth: gutterWidth ?? defaultGutter,
}) || triggerUpdate;
} else {
triggerUpdate =
recalcHeights({
items: this.state.items,
changedItem,
newHeight,
positionStore: this.positionStore,
measurementStore: this.state.measurementStore,
}) || triggerUpdate;
}
}
});
if (triggerUpdate) {
Expand Down
2 changes: 1 addition & 1 deletion packages/gestalt/src/Masonry/dynamicHeightsUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import defaultLayout from './defaultLayout';
import recalcHeights from './dynamicHeightsUtils';
import recalcHeights from './dynamicHeightsV2Utils';
import MeasurementStore from './MeasurementStore';
import { Position } from './types';

Expand Down
141 changes: 16 additions & 125 deletions packages/gestalt/src/Masonry/dynamicHeightsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,89 +8,18 @@ function isBelowArea(area: { left: number; right: number }, position: Position)
return position.left < area.right && position.left + position.width > area.left;
}

function getColumnWidth<T>(items: ReadonlyArray<T>, positionStore: Cache<T, Position>): number {
let columnWidth = Infinity;
items.forEach((item) => {
const position = positionStore.get(item);
if (position) {
columnWidth = Math.min(columnWidth, position.width);
}
});
return columnWidth;
}

function getDelta(
deltasStack: Array<{
left: number;
right: number;
delta: number;
}>,
position: Position,
): number {
for (let i = deltasStack.length - 1; i >= 0; i -= 1) {
const { left, right, delta } = deltasStack[i]!;
if (isBelowArea({ left, right }, position)) {
return delta;
}
}

return 0;
}

function getNewDelta<T>({
multicolumCurrentPosition,
allPreviousItems,
gutterWidth,
}: {
multicolumCurrentPosition: Position;
allPreviousItems: ReadonlyArray<{ item: T; position: Position }>;
gutterWidth: number;
}): number {
let closestItem: { item: T; position: Position };
allPreviousItems.forEach(({ item, position }) => {
const multiColumnLeftLimit = multicolumCurrentPosition.left;
const multiColumnRightLimit = multicolumCurrentPosition.left + multicolumCurrentPosition.width;
const currentItemLeftLimit = position.left;
const currentItemRightLimit = position.left + position.width;
const itemIsAboveMulticolumn =
multiColumnLeftLimit <= currentItemLeftLimit &&
multiColumnRightLimit >= currentItemRightLimit;

if (itemIsAboveMulticolumn) {
if (
(closestItem &&
position.top + position.height >
closestItem!.position.top + closestItem!.position.height) ||
!closestItem
) {
closestItem = { item, position };
}
}

return itemIsAboveMulticolumn;
});
const actualDelta =
closestItem!.position.top +
closestItem!.position.height -
multicolumCurrentPosition.top +
gutterWidth;
return actualDelta;
}

function recalcHeights<T>({
items,
changedItem,
newHeight,
positionStore,
measurementStore,
gutterWidth,
}: {
items: ReadonlyArray<T>;
changedItem: T;
newHeight: number;
positionStore: Cache<T, Position>;
measurementStore: Cache<T, number>;
gutterWidth: number;
}): boolean {
const changedItemPosition = positionStore.get(changedItem);

Expand All @@ -103,72 +32,34 @@ function recalcHeights<T>({
}

const { top, left, width, height } = changedItemPosition;
const oneColumnWidth = getColumnWidth(items.slice(0, 10), positionStore); // We don't need much items to know the column width

// We use a stack in case we found multicolumn items that changes the deltas for their columns below
const deltasStack = [
{
left,
right: left + width,
delta: newHeight - height,
},
];
const heightDelta = newHeight - height;

const itemsFilteredAndSorted = items
items
.map((item) => {
const position = positionStore.get(item);
return position && position.top >= changedItemPosition.top + changedItemPosition.height
? { item, position }
: undefined;
})
.filter((itemPosition) => !!itemPosition)
.sort((a, b) => a.position.top - b.position.top);
.sort((a, b) => a.position.top - b.position.top)
.reduce(
(area, { item, position }) => {
if (isBelowArea(area, position)) {
positionStore.set(item, { ...position, top: position.top + heightDelta });
return {
left: Math.min(area.left, position.left),
right: Math.max(area.right, position.left + position.width),
};
}
return area;
},
{ left, right: left + width } as { left: number; right: number },
);

measurementStore.set(changedItem, newHeight);
positionStore.set(changedItem, { top, left, width, height: newHeight });

itemsFilteredAndSorted.reduce(
(area, { item, position }) => {
if (isBelowArea(area, position)) {
const itemIsMulticolumn = position.width > oneColumnWidth;
if (itemIsMulticolumn) {
const multicolumCurrentPosition = position;

// Check all items above to check if movement is necessary
const allPreviousItems = items
.map((i) => {
const p = positionStore.get(i);
return p && p.top < multicolumCurrentPosition.top
? { item: i, position: p }
: undefined;
})
.filter((itemPosition) => !!itemPosition)
.sort((a, b) => a.position.top - b.position.top);

const newDelta = getNewDelta({
multicolumCurrentPosition,
allPreviousItems,
gutterWidth,
});
deltasStack.push({
left: position.left,
right: position.left + position.width,
delta: newDelta,
});
}

const currentDelta = getDelta(deltasStack, position);
positionStore.set(item, { ...position, top: position.top + currentDelta });
return {
left: Math.min(area.left, position.left),
right: Math.max(area.right, position.left + position.width),
};
}
return area;
},
{ left, right: left + width } as { left: number; right: number },
);

return true;
}

Expand Down
Loading

0 comments on commit 8a73f60

Please sign in to comment.