@samitouri / QOS-React / commits / 0c885af183

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