@samitouri / QOS-React / commits / a10131304b

Renderers can schedule commit-time effects for initial mount

The finalizeInitialChildren HostConfig method now utilizes a boolean return type. Renderers can return true to indicate that custom effects should be processed at commit-time once host components have been mounted. This type of work is marked using the existing Update flag. A new HostConfig method, commitMount, has been added as well for performing this type of work. This change set is in support of the autoFocus prop.

Brian Vaughn committed Dec 28, 2016 at 15:03 UTC a10131304b59abf68a234f0bc75f38a7bb4013a7
11 files changed +113 -26
scripts/fiber/tests-passing.txt
+1
@@ -620,6 +620,7 @@ src/renderers/dom/shared/__tests__/ReactDOM-test.js
620 * should purge the DOM cache when removing nodes
621 * allow React.DOM factories to be called without warnings
622 * preserves focus
623 +* calls focus() on autoFocus elements after they have been mounted to the DOM
624
625 src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
626 * should handle className
src/renderers/art/ReactARTFiber.js
+5 -1
@@ -414,6 +414,10 @@ const ARTRenderer = ReactFiberReconciler({
414 // Noop
415 },
416
417 + commitMount(instance, type, newProps) {
418 + // Noop
419 + },
420 +
421 commitUpdate(instance, type, oldProps, newProps) {
422 instance._applyProps(instance, newProps, oldProps);
423 },
@@ -457,7 +461,7 @@ const ARTRenderer = ReactFiberReconciler({
461 },
462
463 finalizeInitialChildren(domElement, type, props) {
460 - // Noop
464 + return false;
465 },
466
467 insertBefore(parentInstance, child, beforeChild) {
src/renderers/dom/fiber/ReactDOMFiber.js
+28 -1
@@ -96,6 +96,20 @@ function validateContainer(container) {
96 }
97 }
98
99 +function shouldAutoFocusHostComponent(
100 + type : string,
101 + props : Props,
102 +) : boolean {
103 + switch (type) {
104 + case 'button':
105 + case 'input':
106 + case 'select':
107 + case 'textarea':
108 + return !!(props : any).autoFocus;
109 + }
110 + return false;
111 +}
112 +
113 var DOMRenderer = ReactFiberReconciler({
114
115 getRootHostContext(rootContainerInstance : Container) : HostContext {
@@ -173,8 +187,9 @@ var DOMRenderer = ReactFiberReconciler({
187 type : string,
188 props : Props,
189 rootContainerInstance : Container,
176 - ) : void {
190 + ) : boolean {
191 setInitialProperties(domElement, type, props, rootContainerInstance);
192 + return shouldAutoFocusHostComponent(type, props);
193 },
194
195 prepareUpdate(
@@ -197,6 +212,18 @@ var DOMRenderer = ReactFiberReconciler({
212 return true;
213 },
214
215 + commitMount(
216 + domElement : Instance,
217 + type : string,
218 + newProps : Props,
219 + rootContainerInstance : Container,
220 + internalInstanceHandle : Object,
221 + ) : void {
222 + if (shouldAutoFocusHostComponent(type, newProps)) {
223 + (domElement : any).focus();
224 + }
225 + },
226 +
227 commitUpdate(
228 domElement : Instance,
229 type : string,
src/renderers/dom/fiber/ReactDOMFiberComponent.js
-19
@@ -27,7 +27,6 @@ var ReactDOMFiberTextarea = require('ReactDOMFiberTextarea');
27 var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber');
28
29 var emptyFunction = require('emptyFunction');
30 -var focusNode = require('focusNode');
30 var invariant = require('invariant');
31 var isEventSupported = require('isEventSupported');
32 var setInnerHTML = require('setInnerHTML');
@@ -605,36 +604,18 @@ var ReactDOMFiberComponent = {
604 isCustomComponentTag
605 );
606
608 - // TODO: All these autoFocus won't work because the component is not in the
609 - // DOM yet. We need a special effect to handle this.
607 switch (tag) {
608 case 'input':
609 // TODO: Make sure we check if this is still unmounted or do any clean
610 // up necessary since we never stop tracking anymore.
611 inputValueTracking.trackNode((domElement : any));
612 ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
616 - if (props.autoFocus) {
617 - focusNode(domElement);
618 - }
613 break;
614 case 'textarea':
615 // TODO: Make sure we check if this is still unmounted or do any clean
616 // up necessary since we never stop tracking anymore.
617 inputValueTracking.trackNode((domElement : any));
618 ReactDOMFiberTextarea.postMountWrapper(domElement, rawProps);
625 - if (props.autoFocus) {
626 - focusNode(domElement);
627 - }
628 - break;
629 - case 'select':
630 - if (props.autoFocus) {
631 - focusNode(domElement);
632 - }
633 - break;
634 - case 'button':
635 - if (props.autoFocus) {
636 - focusNode(domElement);
637 - }
619 break;
620 case 'option':
621 ReactDOMFiberOption.postMountWrapper(domElement, rawProps);
src/renderers/dom/shared/__tests__/ReactDOM-test.js
+33
@@ -235,4 +235,37 @@ describe('ReactDOM', () => {
235 ]);
236 document.body.removeChild(container);
237 });
238 +
239 + it('calls focus() on autoFocus elements after they have been mounted to the DOM', () => {
240 + const originalFocus = HTMLElement.prototype.focus;
241 +
242 + try {
243 + let focusedElement;
244 + let inputFocusedAfterMount = false;
245 +
246 + // This test needs to determine that focus is called after mount.
247 + // Can't check document.activeElement because PhantomJS is too permissive;
248 + // It doesn't require element to be in the DOM to be focused.
249 + HTMLElement.prototype.focus = function() {
250 + focusedElement = this;
251 + inputFocusedAfterMount = !!this.parentNode;
252 + };
253 +
254 + const container = document.createElement('div');
255 + document.body.appendChild(container);
256 + ReactDOM.render(
257 + <div>
258 + <h1>Auto-focus Test</h1>
259 + <input autoFocus={true}/>
260 + <p>The above input should be focused after mount.</p>
261 + </div>,
262 + container,
263 + );
264 +
265 + expect(inputFocusedAfterMount).toBe(true);
266 + expect(focusedElement.tagName).toBe('INPUT');
267 + } finally {
268 + HTMLElement.prototype.focus = originalFocus;
269 + }
270 + });
271 });
src/renderers/native/ReactNativeFiber.js
+13 -1
@@ -110,6 +110,16 @@ const NativeRenderer = ReactFiberReconciler({
110 );
111 },
112
113 + commitMount(
114 + instance : Instance,
115 + type : string,
116 + newProps : Props,
117 + rootContainerInstance : Object,
118 + internalInstanceHandle : Object
119 + ) : void {
120 + // Noop
121 + },
122 +
123 commitUpdate(
124 instance : Instance,
125 type : string,
@@ -197,7 +207,7 @@ const NativeRenderer = ReactFiberReconciler({
207 type : string,
208 props : Props,
209 rootContainerInstance : Container,
200 - ) : void {
210 + ) : boolean {
211 // Map from child objects to native tags.
212 // Either way we need to pass a copy of the Array to prevent it from being frozen.
213 const nativeTags = parentInstance._children.map(
@@ -210,6 +220,8 @@ const NativeRenderer = ReactFiberReconciler({
220 parentInstance._nativeTag, // containerTag
221 nativeTags // reactTags
222 );
223 +
224 + return false;
225 },
226
227 getRootHostContext() {
src/renderers/noop/ReactNoop.js
+6 -2
@@ -70,14 +70,18 @@ var NoopRenderer = ReactFiberReconciler({
70 parentInstance.children.push(child);
71 },
72
73 - finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void {
74 - // Noop
73 + finalizeInitialChildren(domElement : Instance, type : string, props : Props) : boolean {
74 + return false;
75 },
76
77 prepareUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : boolean {
78 return true;
79 },
80
81 + commitMount(instance : Instance, type : string, newProps : Props) : void {
82 + // Noop
83 + },
84 +
85 commitUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : void {
86 instance.prop = newProps.prop;
87 },
src/renderers/shared/fiber/ReactFiberCommitWork.js
+16
@@ -40,6 +40,7 @@ module.exports = function<T, P, I, TI, C, CX>(
40 ) {
41
42 const {
43 + commitMount,
44 commitUpdate,
45 resetTextContent,
46 commitTextUpdate,
@@ -434,6 +435,21 @@ module.exports = function<T, P, I, TI, C, CX>(
435 case HostComponent: {
436 const instance : I = finishedWork.stateNode;
437 attachRef(current, finishedWork, instance);
438 +
439 + // Renderers may schedule work to be done after host components are mounted
440 + // (eg DOM renderer may schedule auto-focus for inputs and form controls).
441 + // These effects should only be committed when components are first mounted,
442 + // aka when there is no current/alternate.
443 + if (
444 + !current &&
445 + finishedWork.effectTag & Update
446 + ) {
447 + const type = finishedWork.type;
448 + const props = finishedWork.memoizedProps;
449 + const rootContainerInstance = getRootHostContainer();
450 + commitMount(instance, type, props, rootContainerInstance, finishedWork);
451 + }
452 +
453 return;
454 }
455 case HostText: {
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+8 -1
@@ -239,8 +239,15 @@ module.exports = function<T, P, I, TI, C, CX>(
239 currentHostContext,
240 workInProgress
241 );
242 +
243 appendAllChildren(instance, workInProgress);
243 - finalizeInitialChildren(instance, type, newProps, rootContainerInstance);
244 +
245 + // Certain renderers require commit-time effects for initial mount.
246 + // (eg DOM renderer supports auto-focus for certain elements).
247 + // Make sure such renderers get scheduled for later work.
248 + if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
249 + workInProgress.effectTag |= Update;
250 + }
251
252 workInProgress.stateNode = instance;
253 if (workInProgress.ref) {
src/renderers/shared/fiber/ReactFiberReconciler.js
+2 -1
@@ -50,10 +50,11 @@ export type HostConfig<T, P, I, TI, C, CX> = {
50
51 createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : I,
52 appendInitialChild(parentInstance : I, child : I | TI) : void,
53 - finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : void,
53 + finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : boolean,
54
55 prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, hostContext : CX) : boolean,
56 commitUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,
57 + commitMount(instance : I, type : T, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,
58
59 shouldSetTextContent(props : P) : boolean,
60 resetTextContent(instance : I) : void,
src/renderers/shared/fiber/ReactFiberScheduler.js
+1
@@ -371,6 +371,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
371 // In the second pass we'll perform all life-cycles and ref callbacks.
372 // Life-cycles happen as a separate pass so that all placements, updates,
373 // and deletions in the entire tree have already been invoked.
374 + // This pass also triggers any renderer-specific initial effects.
375 nextEffect = firstEffect;
376 while (nextEffect) {
377 try {