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 {