Restore DOM selection and suppress events (#8353)
This makes Draft mostly work.
Ben Alpert committed
Nov 23, 2016 at 13:43 UTC
0c885af1839c97a33f7f67f81d3879f5b4433314
7 files changed
+118
-11
scripts/fiber/tests-failing.txt
-3
@@ -45,9 +45,6 @@ src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
45
src/renderers/dom/shared/__tests__/ReactEventListener-test.js
46
* should batch between handlers from different roots
47
48
-src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
49
-* should control a value in reentrant events
50
-
48
src/renderers/dom/stack/client/__tests__/ReactDOM-test.js
49
* throws in render() if the mount callback is not a function
50
* throws in render() if the update callback is not a function
scripts/fiber/tests-passing.txt
+2
@@ -811,6 +811,7 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMIframe-test.js
811
812
src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
813
* should properly control a value even if no event listener exists
814
+* should control a value in reentrant events
815
* should control values in reentrant events with different targets
816
* should display `defaultValue` of number 0
817
* only assigns defaultValue if it changes
@@ -930,6 +931,7 @@ src/renderers/dom/stack/client/__tests__/ReactDOM-test.js
931
* should overwrite props.children with children argument
932
* should purge the DOM cache when removing nodes
933
* allow React.DOM factories to be called without warnings
934
+* preserves focus
935
936
src/renderers/dom/stack/client/__tests__/ReactMount-test.js
937
* should render different components in same root
src/renderers/dom/fiber/ReactDOMFiber.js
+18
-5
@@ -16,12 +16,14 @@ import type { Fiber } from 'ReactFiber';
16
import type { HostChildren } from 'ReactFiberReconciler';
17
import type { ReactNodeList } from 'ReactTypes';
18
19
+var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
20
var ReactControlledComponent = require('ReactControlledComponent');
21
var ReactDOMComponentTree = require('ReactDOMComponentTree');
22
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
23
var ReactDOMFiberComponent = require('ReactDOMFiberComponent');
24
var ReactDOMInjection = require('ReactDOMInjection');
25
var ReactFiberReconciler = require('ReactFiberReconciler');
26
+var ReactInputSelection = require('ReactInputSelection');
27
var ReactInstanceMap = require('ReactInstanceMap');
28
var ReactPortal = require('ReactPortal');
29
@@ -29,11 +31,6 @@ var findDOMNode = require('findDOMNode');
31
var invariant = require('invariant');
32
var warning = require('warning');
33
32
-ReactDOMInjection.inject();
33
-ReactControlledComponent.injection.injectFiberControlledHostComponent(
34
- ReactDOMFiberComponent
35
-);
36
-
34
var {
35
createElement,
36
setInitialProperties,
@@ -73,8 +70,24 @@ function recursivelyAppendChildren(parent : Element, child : HostChildren<Instan
70
}
71
}
72
73
+let eventsEnabled : ?boolean = null;
74
+let selectionInformation : ?mixed = null;
75
+
76
var DOMRenderer = ReactFiberReconciler({
77
78
+ prepareForCommit() : void {
79
+ eventsEnabled = ReactBrowserEventEmitter.isEnabled();
80
+ ReactBrowserEventEmitter.setEnabled(false);
81
+ selectionInformation = ReactInputSelection.getSelectionInformation();
82
+ },
83
+
84
+ resetAfterCommit() : void {
85
+ ReactInputSelection.restoreSelection(selectionInformation);
86
+ selectionInformation = null;
87
+ ReactBrowserEventEmitter.setEnabled(eventsEnabled);
88
+ eventsEnabled = null;
89
+ },
90
+
91
updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
92
// TODO: Containers should update similarly to other parents.
93
container.innerHTML = '';
src/renderers/dom/stack/client/__tests__/ReactDOM-test.js
+59
@@ -177,4 +177,63 @@ describe('ReactDOM', () => {
177
'to be a function. Instead received: Foo (keys: a, b).'
178
);
179
});
180
+
181
+ it('preserves focus', () => {
182
+ let input;
183
+ let input2;
184
+ class A extends React.Component {
185
+ render() {
186
+ return (
187
+ <div>
188
+ <input id="one" ref={(r) => input = input || r} />
189
+ {this.props.showTwo &&
190
+ <input id="two" ref={(r) => input2 = input2 || r} />}
191
+ </div>
192
+ );
193
+ }
194
+
195
+ componentDidUpdate() {
196
+ // Focus should have been restored to the original input
197
+ expect(document.activeElement.id).toBe('one');
198
+ input2.focus();
199
+ expect(document.activeElement.id).toBe('two');
200
+ log.push('input2 focused');
201
+ }
202
+ }
203
+
204
+ var log = [];
205
+ var container = document.createElement('div');
206
+ document.body.appendChild(container);
207
+ ReactDOM.render(<A showTwo={false} />, container);
208
+ input.focus();
209
+
210
+ // When the second input is added, let's simulate losing focus, which is
211
+ // something that could happen when manipulating DOM nodes (but is hard to
212
+ // deterministically force without relying intensely on React DOM
213
+ // implementation details)
214
+ var div = container.firstChild;
215
+ ['appendChild', 'insertBefore'].forEach((name) => {
216
+ var mutator = div[name];
217
+ div[name] = function() {
218
+ if (input) {
219
+ input.blur();
220
+ expect(document.activeElement.tagName).toBe('BODY');
221
+ log.push('input2 inserted');
222
+ }
223
+ return mutator.apply(this, arguments);
224
+ };
225
+ });
226
+
227
+ expect(document.activeElement.id).toBe('one');
228
+ ReactDOM.render(<A showTwo={true} />, container);
229
+ // input2 gets added, which causes input to get blurred. Then
230
+ // componentDidUpdate focuses input2 and that should make it down to here,
231
+ // not get overwritten by focus restoration.
232
+ expect(document.activeElement.id).toBe('two');
233
+ expect(log).toEqual([
234
+ 'input2 inserted',
235
+ 'input2 focused',
236
+ ]);
237
+ document.body.removeChild(container);
238
+ });
239
});
src/renderers/noop/ReactNoop.js
+17
@@ -156,8 +156,23 @@ var NoopRenderer = ReactFiberReconciler({
156
scheduledDeferredCallback = callback;
157
},
158
159
+ prepareForCommit() : void {
160
+ if (isCommitting) {
161
+ throw new Error('Double prepare before commit');
162
+ }
163
+ isCommitting = true;
164
+ },
165
+
166
+ resetAfterCommit() : void {
167
+ if (!isCommitting) {
168
+ throw new Error('Double reset after commit');
169
+ }
170
+ isCommitting = false;
171
+ },
172
+
173
});
174
175
+var isCommitting = false;
176
var rootContainers = new Map();
177
var roots = new Map();
178
var DEFAULT_ROOT_ID = '<default>';
@@ -255,6 +270,8 @@ var ReactNoop = {
270
271
syncUpdates: NoopRenderer.syncUpdates,
272
273
+ isCommitting: () => isCommitting,
274
+
275
// Logs the current state of the tree.
276
dumpTree(rootID : string = DEFAULT_ROOT_ID) {
277
const root = roots.get(rootID);
src/renderers/shared/fiber/ReactFiberReconciler.js
+3
@@ -66,6 +66,9 @@ export type HostConfig<T, P, I, TI, C> = {
66
scheduleAnimationCallback(callback : () => void) : void,
67
scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void,
68
69
+ prepareForCommit() : void,
70
+ resetAfterCommit() : void,
71
+
72
useSyncScheduling ?: boolean,
73
};
74
src/renderers/shared/fiber/ReactFiberScheduler.js
+19
-3
@@ -74,6 +74,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
74
const hostScheduleDeferredCallback = config.scheduleDeferredCallback;
75
const useSyncScheduling = config.useSyncScheduling;
76
77
+ const prepareForCommit = config.prepareForCommit;
78
+ const resetAfterCommit = config.resetAfterCommit;
79
+
80
// The priority level to use when scheduling an update.
81
let priorityContext : PriorityLevel = useSyncScheduling ?
82
SynchronousPriority :
@@ -85,6 +88,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
88
// Need this to prevent recursion while in a Task loop.
89
let isPerformingTaskWork : boolean = false;
90
91
+ // We'll only prepare/reset on the outermost commit even when a setState
92
+ // callback causes another synchronous rerender
93
+ let isCommitting : boolean = false;
94
+
95
// The next work in progress fiber that we're currently working on.
96
let nextUnitOfWork : ?Fiber = null;
97
let nextPriorityLevel : PriorityLevel = NoWork;
@@ -161,6 +168,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
168
}
169
170
function commitAllWork(finishedWork : Fiber) {
171
+ prepareForCommit();
172
+
173
// Commit all the side-effects within a tree.
174
// First, we'll perform all the host insertions, updates, deletions and
175
// ref unmounts.
@@ -202,6 +211,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
211
effectfulFiber = effectfulFiber.nextEffect;
212
}
213
214
+ // Finally if the root itself had an effect, we perform that since it is
215
+ // not part of the effect list.
216
+ if (finishedWork.effectTag !== NoEffect) {
217
+ const current = finishedWork.alternate;
218
+ commitWork(current, finishedWork);
219
+ }
220
+
221
+ resetAfterCommit();
222
+
223
// Next, we'll perform all life-cycles and ref callbacks. Life-cycles
224
// happens as a separate pass so that all effects in the entire tree have
225
// already been invoked.
@@ -228,11 +246,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
246
effectfulFiber = next;
247
}
248
231
- // Finally if the root itself had an effect, we perform that since it is not
232
- // part of the effect list.
249
+ // Lifecycles on the root itself
250
if (finishedWork.effectTag !== NoEffect) {
251
const current = finishedWork.alternate;
235
- commitWork(current, finishedWork);
252
commitLifeCycles(current, finishedWork);
253
}
254