Skip to content

Commit c443eed

Browse files
author
Brian Vaughn
committed
Focus side effect type added; auto-focus moved to commit phase
Auto-focus was previously managed by ReactDOMFiberComponent.setInitialProperties which was called during the complete phase. This did not work because host components are not yet present in the DOM until the entire tree has been mounted which happens during the commit phase. This changeset moves focus management to the latter part of commit and tracks it via a new side effect type, . I was able to reproduce the failing focus before in a browser and verify this fix. However it was a bit awkward to catch in a unit test because PhantomJS does not require DOM elements to be mounted in order to track their focused state. I've added a test that fails before and passes after, but it's kind of a hack. As a side note I see that we are still using the focusNode helper mehtod in a few places. It seems like this is no longer neceessary since we dropped IE8 support early 2016. I did not remove it in this commit though since it's done in a few places.
1 parent 3baa47c commit c443eed

File tree

6 files changed

+68
-28
lines changed

6 files changed

+68
-28
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ src/renderers/dom/shared/__tests__/ReactDOM-test.js
620620
* should purge the DOM cache when removing nodes
621621
* allow React.DOM factories to be called without warnings
622622
* preserves focus
623+
* calls focus() on autoFocus elements after they have been mounted to the DOM
623624

624625
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
625626
* should handle className

src/renderers/dom/fiber/ReactDOMFiberComponent.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ var ReactDOMFiberTextarea = require('ReactDOMFiberTextarea');
2727
var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber');
2828

2929
var emptyFunction = require('emptyFunction');
30-
var focusNode = require('focusNode');
3130
var invariant = require('invariant');
3231
var isEventSupported = require('isEventSupported');
3332
var setInnerHTML = require('setInnerHTML');
@@ -605,36 +604,18 @@ var ReactDOMFiberComponent = {
605604
isCustomComponentTag
606605
);
607606

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.
610607
switch (tag) {
611608
case 'input':
612609
// TODO: Make sure we check if this is still unmounted or do any clean
613610
// up necessary since we never stop tracking anymore.
614611
inputValueTracking.trackNode((domElement : any));
615612
ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
616-
if (props.autoFocus) {
617-
focusNode(domElement);
618-
}
619613
break;
620614
case 'textarea':
621615
// TODO: Make sure we check if this is still unmounted or do any clean
622616
// up necessary since we never stop tracking anymore.
623617
inputValueTracking.trackNode((domElement : any));
624618
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-
}
638619
break;
639620
case 'option':
640621
ReactDOMFiberOption.postMountWrapper(domElement, rawProps);

src/renderers/dom/shared/__tests__/ReactDOM-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,36 @@ describe('ReactDOM', () => {
235235
]);
236236
document.body.removeChild(container);
237237
});
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+
// We can't just check document.activeElement because PhantomJS is too permissive.
248+
HTMLElement.prototype.focus = function() {
249+
focusedElement = this;
250+
inputFocusedAfterMount = !!this.parentNode;
251+
};
252+
253+
const container = document.createElement('div');
254+
document.body.appendChild(container);
255+
ReactDOM.render(
256+
<div>
257+
<h1>Auto-focus Test</h1>
258+
<input autoFocus id='input'/>
259+
<p>The above input should be focused after mount.</p>
260+
</div>,
261+
container,
262+
);
263+
264+
expect(focusedElement.tagName).toBe('INPUT');
265+
expect(inputFocusedAfterMount).toBe(true);
266+
} finally {
267+
HTMLElement.prototype.focus = originalFocus;
268+
}
269+
});
238270
});

src/renderers/shared/fiber/ReactFiberCompleteWork.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var {
3939
Fragment,
4040
} = ReactTypeOfWork;
4141
var {
42+
Focus,
4243
Update,
4344
} = ReactTypeOfSideEffect;
4445

@@ -239,6 +240,20 @@ module.exports = function<T, P, I, TI, C, CX>(
239240
currentHostContext,
240241
workInProgress
241242
);
243+
244+
// A component cannot be focused until it's mounted.
245+
// Flag components with autoFocus for later work.
246+
switch (type) {
247+
case 'button':
248+
case 'input':
249+
case 'select':
250+
case 'textarea':
251+
if (newProps.autoFocus) {
252+
workInProgress.effectTag |= Focus;
253+
}
254+
break;
255+
}
256+
242257
appendAllChildren(instance, workInProgress);
243258
finalizeInitialChildren(instance, type, newProps, rootContainerInstance);
244259

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { FiberRoot } from 'ReactFiberRoot';
1717
import type { HostConfig, Deadline } from 'ReactFiberReconciler';
1818
import type { PriorityLevel } from 'ReactPriorityLevel';
1919

20+
var focusNode = require('focusNode');
2021
var {
2122
popContextProvider,
2223
} = require('ReactFiberContext');
@@ -41,6 +42,7 @@ var {
4142
} = require('ReactPriorityLevel');
4243

4344
var {
45+
Focus,
4446
NoEffect,
4547
Placement,
4648
Update,
@@ -286,6 +288,12 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
286288
function commitAllLifeCycles() {
287289
while (nextEffect) {
288290
const current = nextEffect.alternate;
291+
292+
// Auto-focus host components once they have been mounted.
293+
if (nextEffect.effectTag & Focus) {
294+
focusNode(nextEffect.stateNode);
295+
}
296+
289297
// Use Task priority for lifecycle updates
290298
if (nextEffect.effectTag & (Update | Callback)) {
291299
commitLifeCycles(current, nextEffect);
@@ -371,6 +379,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
371379
// In the second pass we'll perform all life-cycles and ref callbacks.
372380
// Life-cycles happen as a separate pass so that all placements, updates,
373381
// and deletions in the entire tree have already been invoked.
382+
// This pass also manages focus since components must be in the DOM first,
383+
// And this does not happen untilt he entire tree is mounted.
374384
nextEffect = firstEffect;
375385
while (nextEffect) {
376386
try {

src/renderers/shared/fiber/ReactTypeOfSideEffect.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@
1212

1313
'use strict';
1414

15-
export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 16 | 32;
15+
export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 16 | 32 | 64;
1616

1717
module.exports = {
18-
NoEffect: 0, // 0b000000
19-
Placement: 1, // 0b000001
20-
Update: 2, // 0b000010
21-
PlacementAndUpdate: 3, // 0b000011
22-
Deletion: 4, // 0b000100
23-
ContentReset: 8, // 0b001000
24-
Callback: 16, // 0b010000
25-
Err: 32, // 0b100000
18+
NoEffect: 0, // 0b0000000
19+
Placement: 1, // 0b0000001
20+
Update: 2, // 0b0000010
21+
PlacementAndUpdate: 3, // 0b0000011
22+
Deletion: 4, // 0b0000100
23+
ContentReset: 8, // 0b0001000
24+
Callback: 16, // 0b0010000
25+
Focus: 32, // 0b0100000
26+
Err: 64, // 0b1000000
2627
};

0 commit comments

Comments
 (0)