Fix bugs that occur when event responder unmounts during a touch event sequence
> This PR adds a test (initially failing) for this case along with a fix for stack and fiber. The stack fix was copied from a Diff submitted by @sema. The fiber fix just required us to stop leaking properties for unmounted views. > Longer term we may want to explicitly invoke a release event listener for a responder just before unmounting it. This PR does not do that.
This commit is contained in:
parent
42c375b136
commit
a44a5d68d8
|
@ -1454,6 +1454,7 @@ src/renderers/native/__tests__/ReactNativeAttributePayload-test.js
|
|||
|
||||
src/renderers/native/__tests__/ReactNativeEvents-test.js
|
||||
* handles events
|
||||
* handles when a responder is unmounted while a touch sequence is in progress
|
||||
|
||||
src/renderers/native/__tests__/ReactNativeMount-test.js
|
||||
* should be able to create and render a native component
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
// Mock of the Native Hooks
|
||||
|
||||
var RCTUIManager = {
|
||||
clearJSResponder: jest.fn(),
|
||||
createView: jest.fn(),
|
||||
setJSResponder: jest.fn(),
|
||||
setChildren: jest.fn(),
|
||||
manageChildren: jest.fn(),
|
||||
updateView: jest.fn(),
|
||||
|
|
|
@ -53,6 +53,7 @@ function uncacheNode(inst) {
|
|||
|
||||
function uncacheFiberNode(tag) {
|
||||
delete instanceCache[tag];
|
||||
delete instanceProps[tag];
|
||||
}
|
||||
|
||||
function getInstanceFromTag(tag) {
|
||||
|
|
|
@ -15,6 +15,7 @@ var RCTEventEmitter;
|
|||
var React;
|
||||
var ReactErrorUtils;
|
||||
var ReactNative;
|
||||
var ResponderEventPlugin;
|
||||
var UIManager;
|
||||
var createReactNativeComponentClass;
|
||||
|
||||
|
@ -25,6 +26,7 @@ beforeEach(() => {
|
|||
React = require('React');
|
||||
ReactErrorUtils = require('ReactErrorUtils');
|
||||
ReactNative = require('ReactNative');
|
||||
ResponderEventPlugin = require('ResponderEventPlugin');
|
||||
UIManager = require('UIManager');
|
||||
createReactNativeComponentClass = require('createReactNativeComponentClass');
|
||||
|
||||
|
@ -91,3 +93,97 @@ it('handles events', () => {
|
|||
'outer touchend',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles when a responder is unmounted while a touch sequence is in progress', () => {
|
||||
var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
|
||||
var View = createReactNativeComponentClass({
|
||||
validAttributes: { id: true },
|
||||
uiViewClassName: 'View',
|
||||
});
|
||||
|
||||
function getViewById(id) {
|
||||
return UIManager.createView.mock.calls.find(
|
||||
args => args[3] && args[3].id === id
|
||||
)[0];
|
||||
}
|
||||
|
||||
function getResponderId() {
|
||||
const responder = ResponderEventPlugin._getResponder();
|
||||
if (responder === null) {
|
||||
return null;
|
||||
}
|
||||
const props = typeof responder.tag === 'number'
|
||||
? responder.memoizedProps
|
||||
: responder._currentElement.props;
|
||||
return props ? props.id : null;
|
||||
}
|
||||
|
||||
var log = [];
|
||||
ReactNative.render(
|
||||
<View id="parent">
|
||||
<View key={1}>
|
||||
<View
|
||||
id="one"
|
||||
onResponderEnd={() => log.push('one responder end')}
|
||||
onResponderStart={() => log.push('one responder start')}
|
||||
onStartShouldSetResponder={() => true}
|
||||
/>
|
||||
</View>
|
||||
<View key={2}>
|
||||
<View
|
||||
id="two"
|
||||
onResponderEnd={() => log.push('two responder end')}
|
||||
onResponderStart={() => log.push('two responder start')}
|
||||
onStartShouldSetResponder={() => true}
|
||||
/>
|
||||
</View>
|
||||
</View>,
|
||||
1
|
||||
);
|
||||
|
||||
EventEmitter.receiveTouches(
|
||||
'topTouchStart',
|
||||
[{target: getViewById('one'), identifier: 17}],
|
||||
[0]
|
||||
);
|
||||
|
||||
expect(getResponderId()).toBe('one');
|
||||
expect(log).toEqual(['one responder start']);
|
||||
log.splice(0);
|
||||
|
||||
ReactNative.render(
|
||||
<View id="parent">
|
||||
<View key={2}>
|
||||
<View
|
||||
id="two"
|
||||
onResponderEnd={() => log.push('two responder end')}
|
||||
onResponderStart={() => log.push('two responder start')}
|
||||
onStartShouldSetResponder={() => true}
|
||||
/>
|
||||
</View>
|
||||
</View>,
|
||||
1
|
||||
);
|
||||
|
||||
// TODO Verify the onResponderEnd listener has been called (before the unmount)
|
||||
// expect(log).toEqual(['one responder end']);
|
||||
// log.splice(0);
|
||||
|
||||
EventEmitter.receiveTouches(
|
||||
'topTouchEnd',
|
||||
[{target: getViewById('two'), identifier: 17}],
|
||||
[0]
|
||||
);
|
||||
|
||||
expect(getResponderId()).toBeNull();
|
||||
expect(log).toEqual([]);
|
||||
|
||||
EventEmitter.receiveTouches(
|
||||
'topTouchStart',
|
||||
[{target: getViewById('two'), identifier: 17}],
|
||||
[0]
|
||||
);
|
||||
|
||||
expect(getResponderId()).toBe('two');
|
||||
expect(log).toEqual(['two responder start']);
|
||||
});
|
||||
|
|
|
@ -142,6 +142,10 @@ var EventPluginHub = {
|
|||
// Text node, let it bubble through.
|
||||
return null;
|
||||
}
|
||||
if (!inst._rootNodeID) {
|
||||
// If the instance is already unmounted, we have no listeners.
|
||||
return null;
|
||||
}
|
||||
const props = inst._currentElement.props;
|
||||
listener = props[registrationName];
|
||||
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, props)) {
|
||||
|
|
Loading…
Reference in New Issue