diff --git a/docs/integration-test-helpers/masonry/ExampleGridItem.tsx b/docs/integration-test-helpers/masonry/ExampleGridItem.tsx
index 9e89277538..2bd3eb40f9 100644
--- a/docs/integration-test-helpers/masonry/ExampleGridItem.tsx
+++ b/docs/integration-test-helpers/masonry/ExampleGridItem.tsx
@@ -23,15 +23,12 @@ export default function ExampleGridItem({ data = {}, itemIdx, expanded }: Props)
const isTwoColItem = data.columnSpan === 2;
return (
-
+
>,
{mountGrid && (
{
const columnSpan = item.columnSpan as number | undefined;
return columnSpan ?? 1;
@@ -419,7 +421,7 @@ export default class MasonryContainer extends Component>,
: undefined
}
columnWidth={columnWidth}
- gutterWidth={0}
+ gutterWidth={14}
items={items}
layout={flexible ? 'flexible' : undefined}
measurementStore={externalCache ? measurementStore : undefined}
diff --git a/packages/gestalt/src/Masonry.tsx b/packages/gestalt/src/Masonry.tsx
index 48b22d69b6..a7d9ef4a04 100644
--- a/packages/gestalt/src/Masonry.tsx
+++ b/packages/gestalt/src/Masonry.tsx
@@ -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';
@@ -148,6 +149,12 @@ type Props = {
*/
_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
@@ -225,21 +232,34 @@ export default class Masonry extends ReactComponent, State> {
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) {
diff --git a/packages/gestalt/src/Masonry/dynamicHeightsUtils.test.ts b/packages/gestalt/src/Masonry/dynamicHeightsUtils.test.ts
index f0ce32e5b0..ea64cdebe9 100644
--- a/packages/gestalt/src/Masonry/dynamicHeightsUtils.test.ts
+++ b/packages/gestalt/src/Masonry/dynamicHeightsUtils.test.ts
@@ -1,5 +1,5 @@
import defaultLayout from './defaultLayout';
-import recalcHeights from './dynamicHeightsUtils';
+import recalcHeights from './dynamicHeightsV2Utils';
import MeasurementStore from './MeasurementStore';
import { Position } from './types';
diff --git a/packages/gestalt/src/Masonry/dynamicHeightsUtils.ts b/packages/gestalt/src/Masonry/dynamicHeightsUtils.ts
index ea2511a6c2..238e81921a 100644
--- a/packages/gestalt/src/Masonry/dynamicHeightsUtils.ts
+++ b/packages/gestalt/src/Masonry/dynamicHeightsUtils.ts
@@ -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(items: ReadonlyArray, positionStore: Cache): 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({
- 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({
items,
changedItem,
newHeight,
positionStore,
measurementStore,
- gutterWidth,
}: {
items: ReadonlyArray;
changedItem: T;
newHeight: number;
positionStore: Cache;
measurementStore: Cache;
- gutterWidth: number;
}): boolean {
const changedItemPosition = positionStore.get(changedItem);
@@ -103,18 +32,9 @@ function recalcHeights({
}
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
@@ -122,53 +42,24 @@ function recalcHeights({
: 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;
}
diff --git a/packages/gestalt/src/Masonry/dynamicHeightsV2Utils.ts b/packages/gestalt/src/Masonry/dynamicHeightsV2Utils.ts
new file mode 100644
index 0000000000..7551be3d5f
--- /dev/null
+++ b/packages/gestalt/src/Masonry/dynamicHeightsV2Utils.ts
@@ -0,0 +1,184 @@
+/**
+ * Util functions used to update positions when an item changes the height dynamically
+ */
+import { Cache } from './Cache';
+import { Position } from './types';
+
+function isBelowArea(area: { left: number; right: number }, position: Position) {
+ return position.left < area.right && position.left + position.width > area.left;
+}
+
+/*
+ * getColumnWidth
+ * This is a naive form of knowing the width of a column, so we can use it to know and item is
+ * multicolumn (has a bigger width than columnWidth). We can't use columnWidth prop because
+ * of flexible layouts (and that's an optional param)-
+ * TODO: We could standardize this by using _getColumnSpan, as in multicolumn modules
+ */
+function getColumnWidth(items: ReadonlyArray, positionStore: Cache): 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({
+ 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({
+ items,
+ changedItem,
+ newHeight,
+ positionStore,
+ measurementStore,
+ gutterWidth,
+}: {
+ items: ReadonlyArray;
+ changedItem: T;
+ newHeight: number;
+ positionStore: Cache;
+ measurementStore: Cache;
+ gutterWidth: number;
+}): boolean {
+ const changedItemPosition = positionStore.get(changedItem);
+
+ if (
+ !changedItemPosition ||
+ newHeight === 0 ||
+ Math.floor(changedItemPosition.height) === Math.floor(newHeight)
+ ) {
+ return false;
+ }
+
+ 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 itemsFilteredAndSorted = 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);
+
+ 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) {
+ // If it's a multicolumn module, we don't always use the same delta, because items above
+ // can limit the movement of the multicolumn module. We need to find the correct delta.
+ 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;
+}
+
+export default recalcHeights;
diff --git a/packages/gestalt/src/MasonryV2.tsx b/packages/gestalt/src/MasonryV2.tsx
index 599ff92317..ad9fb5d11e 100644
--- a/packages/gestalt/src/MasonryV2.tsx
+++ b/packages/gestalt/src/MasonryV2.tsx
@@ -16,6 +16,7 @@ import debounce from './debounce';
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';
@@ -161,6 +162,10 @@ type Props = {
* Experimental flag to enable dynamic heights on items. This only works if multi column items are enabled.
*/
_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
*
@@ -703,6 +708,7 @@ function Masonry(
_useRAF,
_getColumnSpanConfig,
_dynamicHeights,
+ _dynamicHeightsV2Experiment,
_loadingStateItems = [],
_renderLoadingStateItems,
_earlyBailout,
@@ -800,15 +806,26 @@ function Masonry(
defaultGutter = 0;
}
- triggerUpdate =
- recalcHeights({
- items,
- changedItem,
- newHeight,
- positionStore,
- measurementStore,
- gutterWidth: gutter ?? defaultGutter,
- }) || triggerUpdate;
+ if (_dynamicHeightsV2Experiment) {
+ triggerUpdate =
+ recalcHeightsV2({
+ items,
+ changedItem,
+ newHeight,
+ positionStore,
+ measurementStore,
+ gutterWidth: gutter ?? defaultGutter,
+ }) || triggerUpdate;
+ } else {
+ triggerUpdate =
+ recalcHeights({
+ items,
+ changedItem,
+ newHeight,
+ positionStore,
+ measurementStore,
+ }) || triggerUpdate;
+ }
}
});
if (triggerUpdate) {
@@ -816,7 +833,15 @@ function Masonry(
}
})
: undefined,
- [_dynamicHeights, items, measurementStore, positionStore, gutter, layout],
+ [
+ _dynamicHeights,
+ _dynamicHeightsV2Experiment,
+ items,
+ measurementStore,
+ positionStore,
+ gutter,
+ layout,
+ ],
);
const { hasPendingMeasurements, height, positions, renderLoadingState, updateMeasurement } =