diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index e78188ac61a67..d1095150b2d44 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -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 diff --git a/src/renderers/art/ReactARTFiber.js b/src/renderers/art/ReactARTFiber.js index 6244642a8432d..4c3f22ea056fa 100644 --- a/src/renderers/art/ReactARTFiber.js +++ b/src/renderers/art/ReactARTFiber.js @@ -414,6 +414,10 @@ const ARTRenderer = ReactFiberReconciler({ // Noop }, + commitMount(instance, type, newProps) { + // Noop + }, + commitUpdate(instance, type, oldProps, newProps) { instance._applyProps(instance, newProps, oldProps); }, @@ -457,7 +461,7 @@ const ARTRenderer = ReactFiberReconciler({ }, finalizeInitialChildren(domElement, type, props) { - // Noop + return false; }, insertBefore(parentInstance, child, beforeChild) { diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 3b6330db66562..20d851dcee681 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -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 { @@ -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( @@ -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, diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 49ed51cad9246..f583cfc5e0f49 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -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'); @@ -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); diff --git a/src/renderers/dom/shared/__tests__/ReactDOM-test.js b/src/renderers/dom/shared/__tests__/ReactDOM-test.js index d9bbe6c106151..a569abd37fc2e 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOM-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOM-test.js @@ -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( +
+

Auto-focus Test

+ +

The above input should be focused after mount.

+
, + container, + ); + + expect(inputFocusedAfterMount).toBe(true); + expect(focusedElement.tagName).toBe('INPUT'); + } finally { + HTMLElement.prototype.focus = originalFocus; + } + }); }); diff --git a/src/renderers/native/ReactNativeFiber.js b/src/renderers/native/ReactNativeFiber.js index 1ca5d6e4b19cc..2c0ce2573acaf 100644 --- a/src/renderers/native/ReactNativeFiber.js +++ b/src/renderers/native/ReactNativeFiber.js @@ -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, @@ -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( @@ -210,6 +220,8 @@ const NativeRenderer = ReactFiberReconciler({ parentInstance._nativeTag, // containerTag nativeTags // reactTags ); + + return false; }, getRootHostContext() { diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index b668cc9d882fd..804aba156fdac 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -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; }, diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 879fcf2485ee7..bb56a4c78f3bd 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -40,6 +40,7 @@ module.exports = function( ) { const { + commitMount, commitUpdate, resetTextContent, commitTextUpdate, @@ -434,6 +435,21 @@ module.exports = function( 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: { diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index c0f41ab4c872f..bffecb3c4690e 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -239,8 +239,15 @@ module.exports = function( 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) { diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index e05e6681348c3..ff55cbddaa008 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -50,10 +50,11 @@ export type HostConfig = { 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, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 5f203409f9e5e..63cea47ee4c1c 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -371,6 +371,7 @@ module.exports = function(config : HostConfig