mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #8646 from bvaughn/focus-side-effect
Renderers can queue commit effects for initial mount
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user