Skip to content

Commit

Permalink
Renderers can schedule commit-time effects for initial mount
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Brian Vaughn committed Dec 28, 2016
1 parent 3baa47c commit a101313
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 26 deletions.
1 change: 1 addition & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ src/renderers/dom/shared/__tests__/ReactDOM-test.js
* should purge the DOM cache when removing nodes
* allow React.DOM factories to be called without warnings
* preserves focus
* calls focus() on autoFocus elements after they have been mounted to the DOM

src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* should handle className
Expand Down
6 changes: 5 additions & 1 deletion src/renderers/art/ReactARTFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,10 @@ const ARTRenderer = ReactFiberReconciler({
// Noop
},

commitMount(instance, type, newProps) {
// Noop
},

commitUpdate(instance, type, oldProps, newProps) {
instance._applyProps(instance, newProps, oldProps);
},
Expand Down Expand Up @@ -457,7 +461,7 @@ const ARTRenderer = ReactFiberReconciler({
},

finalizeInitialChildren(domElement, type, props) {
// Noop
return false;
},

insertBefore(parentInstance, child, beforeChild) {
Expand Down
29 changes: 28 additions & 1 deletion src/renderers/dom/fiber/ReactDOMFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ function validateContainer(container) {
}
}

function shouldAutoFocusHostComponent(
type : string,
props : Props,
) : boolean {
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
return !!(props : any).autoFocus;
}
return false;
}

var DOMRenderer = ReactFiberReconciler({

getRootHostContext(rootContainerInstance : Container) : HostContext {
Expand Down Expand Up @@ -173,8 +187,9 @@ var DOMRenderer = ReactFiberReconciler({
type : string,
props : Props,
rootContainerInstance : Container,
) : void {
) : boolean {
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
},

prepareUpdate(
Expand All @@ -197,6 +212,18 @@ var DOMRenderer = ReactFiberReconciler({
return true;
},

commitMount(
domElement : Instance,
type : string,
newProps : Props,
rootContainerInstance : Container,
internalInstanceHandle : Object,
) : void {
if (shouldAutoFocusHostComponent(type, newProps)) {
(domElement : any).focus();
}
},

commitUpdate(
domElement : Instance,
type : string,
Expand Down
19 changes: 0 additions & 19 deletions src/renderers/dom/fiber/ReactDOMFiberComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ var ReactDOMFiberTextarea = require('ReactDOMFiberTextarea');
var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber');

var emptyFunction = require('emptyFunction');
var focusNode = require('focusNode');
var invariant = require('invariant');
var isEventSupported = require('isEventSupported');
var setInnerHTML = require('setInnerHTML');
Expand Down Expand Up @@ -605,36 +604,18 @@ var ReactDOMFiberComponent = {
isCustomComponentTag
);

// TODO: All these autoFocus won't work because the component is not in the
// DOM yet. We need a special effect to handle this.
switch (tag) {
case 'input':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.trackNode((domElement : any));
ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
if (props.autoFocus) {
focusNode(domElement);
}
break;
case 'textarea':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.trackNode((domElement : any));
ReactDOMFiberTextarea.postMountWrapper(domElement, rawProps);
if (props.autoFocus) {
focusNode(domElement);
}
break;
case 'select':
if (props.autoFocus) {
focusNode(domElement);
}
break;
case 'button':
if (props.autoFocus) {
focusNode(domElement);
}
break;
case 'option':
ReactDOMFiberOption.postMountWrapper(domElement, rawProps);
Expand Down
33 changes: 33 additions & 0 deletions src/renderers/dom/shared/__tests__/ReactDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,37 @@ describe('ReactDOM', () => {
]);
document.body.removeChild(container);
});

it('calls focus() on autoFocus elements after they have been mounted to the DOM', () => {
const originalFocus = HTMLElement.prototype.focus;

try {
let focusedElement;
let inputFocusedAfterMount = false;

// This test needs to determine that focus is called after mount.
// Can't check document.activeElement because PhantomJS is too permissive;
// It doesn't require element to be in the DOM to be focused.
HTMLElement.prototype.focus = function() {
focusedElement = this;
inputFocusedAfterMount = !!this.parentNode;
};

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(
<div>
<h1>Auto-focus Test</h1>
<input autoFocus={true}/>
<p>The above input should be focused after mount.</p>
</div>,
container,
);

expect(inputFocusedAfterMount).toBe(true);
expect(focusedElement.tagName).toBe('INPUT');
} finally {
HTMLElement.prototype.focus = originalFocus;
}
});
});
14 changes: 13 additions & 1 deletion src/renderers/native/ReactNativeFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ const NativeRenderer = ReactFiberReconciler({
);
},

commitMount(
instance : Instance,
type : string,
newProps : Props,
rootContainerInstance : Object,
internalInstanceHandle : Object
) : void {
// Noop
},

commitUpdate(
instance : Instance,
type : string,
Expand Down Expand Up @@ -197,7 +207,7 @@ const NativeRenderer = ReactFiberReconciler({
type : string,
props : Props,
rootContainerInstance : Container,
) : void {
) : boolean {
// Map from child objects to native tags.
// Either way we need to pass a copy of the Array to prevent it from being frozen.
const nativeTags = parentInstance._children.map(
Expand All @@ -210,6 +220,8 @@ const NativeRenderer = ReactFiberReconciler({
parentInstance._nativeTag, // containerTag
nativeTags // reactTags
);

return false;
},

getRootHostContext() {
Expand Down
8 changes: 6 additions & 2 deletions src/renderers/noop/ReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,18 @@ var NoopRenderer = ReactFiberReconciler({
parentInstance.children.push(child);
},

finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void {
// Noop
finalizeInitialChildren(domElement : Instance, type : string, props : Props) : boolean {
return false;
},

prepareUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : boolean {
return true;
},

commitMount(instance : Instance, type : string, newProps : Props) : void {
// Noop
},

commitUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : void {
instance.prop = newProps.prop;
},
Expand Down
16 changes: 16 additions & 0 deletions src/renderers/shared/fiber/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module.exports = function<T, P, I, TI, C, CX>(
) {

const {
commitMount,
commitUpdate,
resetTextContent,
commitTextUpdate,
Expand Down Expand Up @@ -434,6 +435,21 @@ module.exports = function<T, P, I, TI, C, CX>(
case HostComponent: {
const instance : I = finishedWork.stateNode;
attachRef(current, finishedWork, instance);

// Renderers may schedule work to be done after host components are mounted
// (eg DOM renderer may schedule auto-focus for inputs and form controls).
// These effects should only be committed when components are first mounted,
// aka when there is no current/alternate.
if (
!current &&
finishedWork.effectTag & Update
) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
const rootContainerInstance = getRootHostContainer();
commitMount(instance, type, props, rootContainerInstance, finishedWork);
}

return;
}
case HostText: {
Expand Down
9 changes: 8 additions & 1 deletion src/renderers/shared/fiber/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,15 @@ module.exports = function<T, P, I, TI, C, CX>(
currentHostContext,
workInProgress
);

appendAllChildren(instance, workInProgress);
finalizeInitialChildren(instance, type, newProps, rootContainerInstance);

// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
workInProgress.effectTag |= Update;
}

workInProgress.stateNode = instance;
if (workInProgress.ref) {
Expand Down
3 changes: 2 additions & 1 deletion src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ export type HostConfig<T, P, I, TI, C, CX> = {

createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : I,
appendInitialChild(parentInstance : I, child : I | TI) : void,
finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : void,
finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : boolean,

prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, hostContext : CX) : boolean,
commitUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,
commitMount(instance : I, type : T, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,

shouldSetTextContent(props : P) : boolean,
resetTextContent(instance : I) : void,
Expand Down
1 change: 1 addition & 0 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// In the second pass we'll perform all life-cycles and ref callbacks.
// Life-cycles happen as a separate pass so that all placements, updates,
// and deletions in the entire tree have already been invoked.
// This pass also triggers any renderer-specific initial effects.
nextEffect = firstEffect;
while (nextEffect) {
try {
Expand Down

0 comments on commit a101313

Please sign in to comment.