Skip to content

Commit

Permalink
Restore DOM selection and suppress events (facebook#8353)
Browse files Browse the repository at this point in the history
This makes Draft mostly work.
  • Loading branch information
sophiebits authored and acusti committed Mar 15, 2017
1 parent 7a42323 commit 6e8ac00
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 11 deletions.
3 changes: 0 additions & 3 deletions scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
src/renderers/dom/shared/__tests__/ReactEventListener-test.js
* should batch between handlers from different roots

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 @@ -811,6 +811,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 @@ -930,6 +931,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;
['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,

// 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

0 comments on commit 6e8ac00

Please sign in to comment.