Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore DOM selection and suppress events #8353

Merged
merged 1 commit into from
Nov 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ src/renderers/dom/shared/eventPlugins/__tests__/SimpleEventPlugin-test.js
* should not forward clicks when it becomes disabled
* should work correctly if the listener is changed

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should control a value in reentrant events

src/renderers/dom/stack/client/__tests__/ReactDOM-test.js
* throws in render() if the mount callback is not a function
* throws in render() if the update callback is not a function
Expand Down
2 changes: 2 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMIframe-test.js

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should properly control a value even if no event listener exists
* should control a value in reentrant events
* should control values in reentrant events with different targets
* should display `defaultValue` of number 0
* only assigns defaultValue if it changes
Expand Down Expand Up @@ -898,6 +899,7 @@ src/renderers/dom/stack/client/__tests__/ReactDOM-test.js
* should overwrite props.children with children argument
* should purge the DOM cache when removing nodes
* allow React.DOM factories to be called without warnings
* preserves focus

src/renderers/dom/stack/client/__tests__/ReactMount-test.js
* should render different components in same root
Expand Down
23 changes: 18 additions & 5 deletions src/renderers/dom/fiber/ReactDOMFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,21 @@ import type { Fiber } from 'ReactFiber';
import type { HostChildren } from 'ReactFiberReconciler';
import type { ReactNodeList } from 'ReactTypes';

var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactControlledComponent = require('ReactControlledComponent');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var ReactDOMFiberComponent = require('ReactDOMFiberComponent');
var ReactDOMInjection = require('ReactDOMInjection');
var ReactFiberReconciler = require('ReactFiberReconciler');
var ReactInputSelection = require('ReactInputSelection');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactPortal = require('ReactPortal');

var findDOMNode = require('findDOMNode');
var invariant = require('invariant');
var warning = require('warning');

ReactDOMInjection.inject();
ReactControlledComponent.injection.injectFiberControlledHostComponent(
ReactDOMFiberComponent
);

var {
createElement,
setInitialProperties,
Expand Down Expand Up @@ -73,8 +70,24 @@ function recursivelyAppendChildren(parent : Element, child : HostChildren<Instan
}
}

let eventsEnabled : ?boolean = null;
let selectionInformation : ?mixed = null;

var DOMRenderer = ReactFiberReconciler({

prepareForCommit() : void {
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
ReactBrowserEventEmitter.setEnabled(false);
selectionInformation = ReactInputSelection.getSelectionInformation();
},

resetAfterCommit() : void {
ReactInputSelection.restoreSelection(selectionInformation);
selectionInformation = null;
ReactBrowserEventEmitter.setEnabled(eventsEnabled);
eventsEnabled = null;
},

updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
// TODO: Containers should update similarly to other parents.
container.innerHTML = '';
Expand Down
59 changes: 59 additions & 0 deletions src/renderers/dom/stack/client/__tests__/ReactDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,63 @@ describe('ReactDOM', () => {
'to be a function. Instead received: Foo (keys: a, b).'
);
});

it('preserves focus', () => {
let input;
let input2;
class A extends React.Component {
render() {
return (
<div>
<input id="one" ref={(r) => input = input || r} />
{this.props.showTwo &&
<input id="two" ref={(r) => input2 = input2 || r} />}
</div>
);
}

componentDidUpdate() {
// Focus should have been restored to the original input
expect(document.activeElement.id).toBe('one');
input2.focus();
expect(document.activeElement.id).toBe('two');
log.push('input2 focused');
}
}

var log = [];
var container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<A showTwo={false} />, container);
input.focus();

// When the second input is added, let's simulate losing focus, which is
// something that could happen when manipulating DOM nodes (but is hard to
// deterministically force without relying intensely on React DOM
// implementation details)
var div = container.firstChild;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caused a lint error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I turned off my precommit hook yesterday and forgot to reenable it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

['appendChild', 'insertBefore'].forEach((name) => {
var mutator = div[name];
div[name] = function() {
if (input) {
input.blur();
expect(document.activeElement.tagName).toBe('BODY');
log.push('input2 inserted');
}
return mutator.apply(this, arguments);
};
});

expect(document.activeElement.id).toBe('one');
ReactDOM.render(<A showTwo={true} />, container);
// input2 gets added, which causes input to get blurred. Then
// componentDidUpdate focuses input2 and that should make it down to here,
// not get overwritten by focus restoration.
expect(document.activeElement.id).toBe('two');
expect(log).toEqual([
'input2 inserted',
'input2 focused',
]);
document.body.removeChild(container);
});
});
17 changes: 17 additions & 0 deletions src/renderers/noop/ReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,23 @@ var NoopRenderer = ReactFiberReconciler({
scheduledDeferredCallback = callback;
},

prepareForCommit() : void {
if (isCommitting) {
throw new Error('Double prepare before commit');
}
isCommitting = true;
},

resetAfterCommit() : void {
if (!isCommitting) {
throw new Error('Double reset after commit');
}
isCommitting = false;
},

});

var isCommitting = false;
var rootContainers = new Map();
var roots = new Map();
var DEFAULT_ROOT_ID = '<default>';
Expand Down Expand Up @@ -255,6 +270,8 @@ var ReactNoop = {

syncUpdates: NoopRenderer.syncUpdates,

isCommitting: () => isCommitting,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can remove this now since we're not testing this.


// Logs the current state of the tree.
dumpTree(rootID : string = DEFAULT_ROOT_ID) {
const root = roots.get(rootID);
Expand Down
3 changes: 3 additions & 0 deletions src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export type HostConfig<T, P, I, TI, C> = {
scheduleAnimationCallback(callback : () => void) : void,
scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void,

prepareForCommit() : void,
resetAfterCommit() : void,

useSyncScheduling ?: boolean,
};

Expand Down
22 changes: 19 additions & 3 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
const hostScheduleDeferredCallback = config.scheduleDeferredCallback;
const useSyncScheduling = config.useSyncScheduling;

const prepareForCommit = config.prepareForCommit;
const resetAfterCommit = config.resetAfterCommit;

// The priority level to use when scheduling an update.
let priorityContext : PriorityLevel = useSyncScheduling ?
SynchronousPriority :
Expand All @@ -85,6 +88,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
// Need this to prevent recursion while in a Task loop.
let isPerformingTaskWork : boolean = false;

// We'll only prepare/reset on the outermost commit even when a setState
// callback causes another synchronous rerender
let isCommitting : boolean = false;

// The next work in progress fiber that we're currently working on.
let nextUnitOfWork : ?Fiber = null;
let nextPriorityLevel : PriorityLevel = NoWork;
Expand Down Expand Up @@ -161,6 +168,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}

function commitAllWork(finishedWork : Fiber) {
prepareForCommit();

// Commit all the side-effects within a tree.
// First, we'll perform all the host insertions, updates, deletions and
// ref unmounts.
Expand Down Expand Up @@ -202,6 +211,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
effectfulFiber = effectfulFiber.nextEffect;
}

// Finally if the root itself had an effect, we perform that since it is
// not part of the effect list.
if (finishedWork.effectTag !== NoEffect) {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
}

resetAfterCommit();

// Next, we'll perform all life-cycles and ref callbacks. Life-cycles
// happens as a separate pass so that all effects in the entire tree have
// already been invoked.
Expand All @@ -228,11 +246,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
effectfulFiber = next;
}

// Finally if the root itself had an effect, we perform that since it is not
// part of the effect list.
// Lifecycles on the root itself
if (finishedWork.effectTag !== NoEffect) {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
commitLifeCycles(current, finishedWork);
}

Expand Down