Merge pull request #8646 from bvaughn/focus-side-effect

Renderers can queue commit effects for initial mount
This commit is contained in:
Brian Vaughn
2016-12-28 15:16:50 -05:00
committed by GitHub
11 changed files with 113 additions and 26 deletions
+1
View File
@@ -621,6 +621,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
+5 -1
View File
@@ -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) {
+28 -1
View File
@@ -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,
@@ -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);
@@ -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;
}
});
});
+13 -1
View File
@@ -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() {
+6 -2
View File
@@ -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;
},
@@ -40,6 +40,7 @@ module.exports = function<T, P, I, TI, C, CX>(
) {
const {
commitMount,
commitUpdate,
resetTextContent,
commitTextUpdate,
@@ -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: {
@@ -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) {
@@ -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,
@@ -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 {