Skip to content

Commit 7435467

Browse files
authored
Fix shouldComponentUpdate interfering with droppability (react-grid-layout#1164)
* test(mock): ensure we're importing source files Otherwise we'll import from build/ * fix(droppable): ensure SCU doesn't short-circuit droppable We can also do a simpler equality check on activeDrag Added test to ensure we don't regress this Fixes react-grid-layout#1152 and supersedes react-grid-layout#1162
1 parent 20dac73 commit 7435467

File tree

7 files changed

+172
-38
lines changed

7 files changed

+172
-38
lines changed

__mocks__/react-grid-layout.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Used by jest as it requires some example files, which pull from 'react-grid-layout'
2+
module.exports = require('../index-dev');

examples/example-styles.css

+3-2
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ body {
9191
}
9292
.droppable-element {
9393
width: 150px;
94-
background: #ddd;
94+
text-align: center;
95+
background: #fdd;
9596
border: 1px solid black;
96-
margin-top: 10px;
97+
margin: 10px 0;
9798
padding: 10px;
9899
}

lib/GridItem.jsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export default class GridItem extends React.Component<Props, State> {
182182
// We can't deeply compare children. If the developer memoizes them, we can
183183
// use this optimization.
184184
if (this.props.children !== nextProps.children) return true;
185+
if (this.props.droppingPosition !== nextProps.droppingPosition) return true;
185186
// TODO memoize these calculations so they don't take so long?
186187
const oldPosition = calcGridItemPosition(
187188
this.getPositionParams(this.props),
@@ -205,6 +206,10 @@ export default class GridItem extends React.Component<Props, State> {
205206
);
206207
}
207208

209+
componentDidMount() {
210+
this.moveDroppingItem({});
211+
}
212+
208213
componentDidUpdate(prevProps: Props) {
209214
this.moveDroppingItem(prevProps);
210215
}
@@ -213,12 +218,13 @@ export default class GridItem extends React.Component<Props, State> {
213218
// this element by `x, y` pixels.
214219
moveDroppingItem(prevProps: Props) {
215220
const { droppingPosition } = this.props;
216-
const prevDroppingPosition = prevProps.droppingPosition;
217-
const { dragging } = this.state;
221+
if (!droppingPosition) return;
218222

219-
if (!droppingPosition || !prevDroppingPosition) {
220-
return;
221-
}
223+
const prevDroppingPosition = prevProps.droppingPosition || {
224+
left: 0,
225+
top: 0
226+
};
227+
const { dragging } = this.state;
222228

223229
if (!this.currentNode) {
224230
// eslint-disable-next-line react/no-find-dom-node

lib/ReactGridLayout.jsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ export default class ReactGridLayout extends React.Component<Props, State> {
202202
// handle changes properly, performance will increase.
203203
this.props.children !== nextProps.children ||
204204
!fastRGLPropsEqual(this.props, nextProps, isEqual) ||
205-
!isEqual(this.state.activeDrag, nextState.activeDrag)
205+
this.state.activeDrag !== nextState.activeDrag ||
206+
this.state.droppingPosition !== nextState.droppingPosition
206207
);
207208
}
208209

test/examples/15-drag-from-outside.jsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ export default class DragFromOutsideLayout extends React.Component {
99
rowHeight: 30,
1010
onLayoutChange: function() {},
1111
cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
12-
initialLayout: generateLayout()
1312
};
1413

1514
state = {
1615
currentBreakpoint: "lg",
1716
compactType: "vertical",
1817
mounted: false,
19-
layouts: { lg: this.props.initialLayout }
18+
layouts: { lg: generateLayout() }
2019
};
2120

2221
componentDidMount() {
@@ -98,7 +97,7 @@ export default class DragFromOutsideLayout extends React.Component {
9897
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=568313
9998
onDragStart={e => e.dataTransfer.setData("text/plain", "")}
10099
>
101-
Droppable Element
100+
Droppable Element (Drag me!)
102101
</div>
103102
<ResponsiveReactGridLayout
104103
{...this.props}
@@ -126,7 +125,7 @@ function generateLayout() {
126125
return _.map(_.range(0, 25), function(item, i) {
127126
var y = Math.ceil(Math.random() * 4) + 1;
128127
return {
129-
x: (_.random(0, 5) * 2) % 12,
128+
x: Math.round(Math.random() * 5) * 2,
130129
y: Math.floor(i / 6) * y,
131130
w: 2,
132131
h: y,

test/spec/lifecycle-test.js

+144-26
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
// @flow
22
/* eslint-env jest */
33

4-
import React from 'react';
5-
import _ from 'lodash';
6-
import ResponsiveReactGridLayout from '../../lib/ResponsiveReactGridLayout';
7-
import ReactGridLayout from '../../lib/ReactGridLayout';
8-
import BasicLayout from '../examples/1-basic';
9-
import ShowcaseLayout from '../examples/0-showcase';
10-
import deepFreeze from '../util/deepFreeze';
11-
import {shallow, mount} from 'enzyme';
12-
13-
describe('Lifecycle tests', function() {
4+
import React from "react";
5+
import _ from "lodash";
6+
import TestUtils from "react-dom/test-utils";
7+
import ResponsiveReactGridLayout from "../../lib/ResponsiveReactGridLayout";
8+
import ReactGridLayout from "../../lib/ReactGridLayout";
9+
import BasicLayout from "../examples/1-basic";
10+
import ShowcaseLayout from "../examples/0-showcase";
11+
import DroppableLayout from "../examples/15-drag-from-outside";
12+
import deepFreeze from "../util/deepFreeze";
13+
import { shallow, mount } from "enzyme";
1414

15+
describe("Lifecycle tests", function() {
1516
// Example layouts use randomness
1617
let randIdx = 0;
1718
beforeAll(() => {
1819
const randArr = [0.001, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.999];
19-
jest.spyOn(global.Math, 'random').mockImplementation(() => {
20+
jest.spyOn(global.Math, "random").mockImplementation(() => {
2021
randIdx = (randIdx + 1) % randArr.length;
2122
return randArr[randIdx];
2223
});
@@ -30,46 +31,163 @@ describe('Lifecycle tests', function() {
3031
global.Math.random.mockRestore();
3132
});
3233

33-
describe('<ReactGridLayout>', function() {
34-
it('Basic Render', async function() {
34+
describe("<ReactGridLayout>", function() {
35+
it("Basic Render", async function() {
3536
const wrapper = mount(<BasicLayout />);
3637
expect(wrapper).toMatchSnapshot();
3738
});
38-
});
3939

40-
describe('<ResponsiveReactGridLayout>', function() {
40+
describe("Droppability", function() {
41+
it("Updates when an item is dropped in", function() {
42+
const wrapper = mount(<DroppableLayout />);
43+
const gridLayout = wrapper.find("ReactGridLayout");
44+
expect(gridLayout).toHaveLength(1);
45+
46+
// Start: no dropping node.
47+
expect(gridLayout.state("droppingDOMNode")).toEqual(null);
48+
49+
// Find the droppable element and drag it over the grid layout.
50+
const droppable = wrapper.find(".droppable-element");
51+
TestUtils.Simulate.dragOver(gridLayout.getDOMNode(), {
52+
nativeEvent: {
53+
target: droppable.getDOMNode(),
54+
layerX: 200,
55+
layerY: 150
56+
}
57+
});
58+
59+
// We should have the position in our state.
60+
expect(gridLayout.state("droppingPosition")).toHaveProperty(
61+
"left",
62+
200
63+
);
64+
expect(gridLayout.state("droppingPosition")).toHaveProperty("top", 150);
65+
// We should now have the placeholder element in our state.
66+
expect(gridLayout.state("droppingDOMNode")).toHaveProperty(
67+
"type",
68+
"div"
69+
);
70+
expect(gridLayout.state("droppingDOMNode")).toHaveProperty(
71+
"key",
72+
"__dropping-elem__"
73+
);
74+
75+
// It should also have a layout item assigned to it.
76+
let layoutItem = gridLayout
77+
.state("layout")
78+
.find(item => item.i === "__dropping-elem__");
79+
expect(layoutItem).toEqual({
80+
i: "__dropping-elem__",
81+
h: 1,
82+
w: 1,
83+
x: 1,
84+
y: 4,
85+
static: false,
86+
isDraggable: true
87+
});
88+
89+
// Let's move it some more.
90+
TestUtils.Simulate.dragOver(gridLayout.getDOMNode(), {
91+
nativeEvent: {
92+
target: droppable.getDOMNode(),
93+
layerX: 0,
94+
layerY: 300
95+
}
96+
});
4197

42-
it('Basic Render', async function() {
98+
// State should change.
99+
expect(gridLayout.state("droppingPosition")).toHaveProperty("left", 0);
100+
expect(gridLayout.state("droppingPosition")).toHaveProperty("top", 300);
101+
102+
layoutItem = gridLayout
103+
.state("layout")
104+
.find(item => item.i === "__dropping-elem__");
105+
// Using toMatchObject() here as this will inherit some undefined properties from the cloning
106+
expect(layoutItem).toMatchObject({
107+
i: "__dropping-elem__",
108+
h: 1,
109+
w: 1,
110+
x: 0,
111+
y: 9,
112+
static: false,
113+
isDraggable: true
114+
});
115+
});
116+
});
117+
});
118+
119+
describe("<ResponsiveReactGridLayout>", function() {
120+
it("Basic Render", async function() {
43121
const wrapper = mount(<ShowcaseLayout />);
44122
expect(wrapper).toMatchSnapshot();
45123
});
46124

47-
it('Does not modify layout on movement', async function() {
125+
it("Does not modify layout on movement", async function() {
48126
const layouts = {
49127
lg: [
50-
..._.times(3, (i) => ({
128+
..._.times(3, i => ({
51129
i: String(i),
52130
x: i,
53131
y: 0,
54132
w: 1,
55-
h: 1,
133+
h: 1
56134
}))
57135
]
58136
};
59-
const frozenLayouts = deepFreeze(layouts, {set: true, get: false /* don't crash on unknown gets */});
137+
const frozenLayouts = deepFreeze(layouts, {
138+
set: true,
139+
get: false /* don't crash on unknown gets */
140+
});
60141
// Render the basic Responsive layout.
61142
const wrapper = mount(
62-
<ResponsiveReactGridLayout layouts={frozenLayouts} width={1280} breakpoint="lg">
63-
{_.times(3, (i) => <div key={i} />)}
143+
<ResponsiveReactGridLayout
144+
layouts={frozenLayouts}
145+
width={1280}
146+
breakpoint="lg"
147+
>
148+
{_.times(3, i => (
149+
<div key={i} />
150+
))}
64151
</ResponsiveReactGridLayout>
65152
);
66153

67154
// Set that layout as state and ensure it doesn't change.
68-
wrapper.setState({layouts: frozenLayouts});
69-
wrapper.setProps({width: 800, breakpoint: 'md'}); // will generate new layout
155+
wrapper.setState({ layouts: frozenLayouts });
156+
wrapper.setProps({ width: 800, breakpoint: "md" }); // will generate new layout
70157
wrapper.render();
71158

72-
expect(frozenLayouts).not.toContain('md');
159+
expect(frozenLayouts).not.toContain("md");
73160
});
74161
});
75-
});
162+
});
163+
164+
function simulateMovementFromTo(node, fromX, fromY, toX, toY) {
165+
TestUtils.Simulate.mouseDown(node, { clientX: fromX, clientY: fromX });
166+
mouseMove(node, toX, toY);
167+
TestUtils.Simulate.mouseUp(node);
168+
}
169+
170+
function mouseMove(node, x, y) {
171+
const doc = node ? node.ownerDocument : document;
172+
const evt = doc.createEvent("MouseEvents");
173+
// $FlowIgnore get with it, flow
174+
evt.initMouseEvent(
175+
"mousemove",
176+
true,
177+
true,
178+
window,
179+
0,
180+
0,
181+
0,
182+
x,
183+
y,
184+
false,
185+
false,
186+
false,
187+
false,
188+
0,
189+
null
190+
);
191+
doc.dispatchEvent(evt);
192+
return evt;
193+
}

test/util/setupTests.js

+7
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ Array.prototype.sort = function(comparator) {
1414
sort(this, comparator);
1515
return this;
1616
};
17+
18+
// Required in drag code, not working in JSDOM
19+
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
20+
get() {
21+
return this.parentNode;
22+
}
23+
});

0 commit comments

Comments
 (0)