Skip to content
This repository was archived by the owner on Aug 18, 2025. It is now read-only.

Commit 9760dc7

Browse files
authored
Fixing timing of onDragStart (#676)
1 parent d905634 commit 9760dc7

File tree

3 files changed

+123
-4
lines changed

3 files changed

+123
-4
lines changed

src/state/middleware/hooks.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,15 +239,21 @@ export default (getHooks: () => Hooks, announce: Announce): Middleware => {
239239
return (store: MiddlewareStore) => (next: Dispatch) => (
240240
action: Action,
241241
): any => {
242-
// letting the reducer update first
243-
next(action);
244-
245242
if (action.type === 'INITIAL_PUBLISH') {
246243
const critical: Critical = action.payload.critical;
244+
// Need to fire the onDragStart hook before the connected components are rendered
245+
// This is so consumers can do work in their onDragStart function
246+
// before we have applied any inline styles to anything,
247+
// such as position: fixed to the dragging item.
248+
// This is important for use cases such as a table which uses dimension locking
247249
publisher.start(critical);
250+
next(action);
248251
return;
249252
}
250253

254+
// All other hooks can fire after we have updated our connected components
255+
next(action);
256+
251257
// Drag end
252258
if (action.type === 'DROP_COMPLETE') {
253259
const result: DropResult = action.payload;

stories/src/table/with-dimension-locking.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export default class TableApp extends Component<AppProps, AppState> {
233233
}
234234
})();
235235

236+
// eslint-disable-next-line no-console
236237
console.log('was copied?', wasCopied);
237238

238239
// clear selection
@@ -249,7 +250,9 @@ export default class TableApp extends Component<AppProps, AppState> {
249250
<Header>
250251
<LayoutControl>
251252
Current layout: <code>{this.state.layout}</code>
252-
<button onClick={this.toggleTableLayout}>Toggle</button>
253+
<button type="button" onClick={this.toggleTableLayout}>
254+
Toggle
255+
</button>
253256
</LayoutControl>
254257
<div>
255258
Copy table to clipboard:
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// @flow
2+
import React from 'react';
3+
import { getRect } from 'css-box-model';
4+
import invariant from 'tiny-invariant';
5+
import { mount, type ReactWrapper } from 'enzyme';
6+
import { DragDropContext, Draggable, Droppable } from '../../../src';
7+
import { getComputedSpacing } from '../../utils/dimension';
8+
import { withKeyboard } from '../../utils/user-input-util';
9+
import * as keyCodes from '../../../src/view/key-codes';
10+
import type { Provided as DraggableProvided } from '../../../src/view/draggable/draggable-types';
11+
import type { Provided as DroppableProvided } from '../../../src/view/droppable/droppable-types';
12+
import type { Hooks } from '../../../src/types';
13+
14+
const pressSpacebar = withKeyboard(keyCodes.space);
15+
16+
type ItemProps = {|
17+
provided: DraggableProvided,
18+
onRender: Function,
19+
|};
20+
21+
class Item extends React.Component<ItemProps> {
22+
render() {
23+
this.props.onRender();
24+
const provided: DraggableProvided = this.props.provided;
25+
return (
26+
<div
27+
className="drag-handle"
28+
ref={provided.innerRef}
29+
{...provided.draggableProps}
30+
{...provided.dragHandleProps}
31+
>
32+
<h4>Draggable</h4>
33+
</div>
34+
);
35+
}
36+
}
37+
38+
jest.useFakeTimers();
39+
40+
it('should call the onDragStart before any connected components are updated', () => {
41+
let onDragStartTime: ?DOMHighResTimeStamp = null;
42+
let renderTime: ?DOMHighResTimeStamp = null;
43+
const hooks: Hooks = {
44+
onDragStart: jest.fn().mockImplementation(() => {
45+
invariant(!onDragStartTime, 'onDragStartTime already set');
46+
onDragStartTime = performance.now();
47+
}),
48+
onDragEnd: jest.fn(),
49+
};
50+
const onItemRender = jest.fn().mockImplementation(() => {
51+
invariant(!renderTime, 'renderTime already set');
52+
renderTime = performance.now();
53+
});
54+
// Both list and item will have the same dimensions
55+
jest
56+
.spyOn(Element.prototype, 'getBoundingClientRect')
57+
.mockImplementation(() =>
58+
getRect({
59+
top: 0,
60+
left: 0,
61+
right: 100,
62+
bottom: 100,
63+
}),
64+
);
65+
66+
// Stubbing out totally - not including margins in this
67+
jest
68+
.spyOn(window, 'getComputedStyle')
69+
.mockImplementation(() => getComputedSpacing({}));
70+
const wrapper: ReactWrapper = mount(
71+
<DragDropContext {...hooks}>
72+
<Droppable droppableId="droppable">
73+
{(droppableProvided: DroppableProvided) => (
74+
<div
75+
ref={droppableProvided.innerRef}
76+
{...droppableProvided.droppableProps}
77+
>
78+
<h2>Droppable</h2>
79+
<Draggable draggableId="draggable" index={0}>
80+
{(draggableProvided: DraggableProvided) => (
81+
<Item onRender={onItemRender} provided={draggableProvided} />
82+
)}
83+
</Draggable>
84+
</div>
85+
)}
86+
</Droppable>
87+
</DragDropContext>,
88+
);
89+
90+
pressSpacebar(wrapper.find('.drag-handle'));
91+
92+
// clearing the first call
93+
expect(onItemRender).toHaveBeenCalledTimes(1);
94+
renderTime = null;
95+
onItemRender.mockClear();
96+
97+
// run out prepare phase
98+
jest.runOnlyPendingTimers();
99+
100+
// checking values are set
101+
invariant(onDragStartTime, 'onDragStartTime should be set');
102+
invariant(renderTime, 'renderTime should be set');
103+
104+
// core assertion
105+
expect(onDragStartTime).toBeLessThan(renderTime);
106+
107+
// validation
108+
expect(hooks.onDragStart).toHaveBeenCalledTimes(1);
109+
expect(onItemRender).toHaveBeenCalledTimes(1);
110+
});

0 commit comments

Comments
 (0)