[fix] ResponderEvent touch identifier normalization

Move the touch identifier normalization into the ResponderEvent creation. This ensures that the identifier is consistent throughout. If application code reads an identifier from a touch object it can be used to find that touch in the `touchBank` array.

Fix #1716
This commit is contained in:
Nicolas Gallagher
2020-08-24 13:50:55 -07:00
parent 9ed0c407a9
commit de2a66c694
3 changed files with 72 additions and 14 deletions

View File

@@ -84,9 +84,17 @@ function getTouchIdentifier({ identifier }: Touch): number {
if (identifier == null) {
console.error('Touch object is missing identifier.');
}
// Safari produces very large identifiers that would cause the array length
// to be so large as to crash the browser, if not normalized like this.
return identifier > MAX_TOUCH_BANK ? identifier % 20 : identifier;
if (__DEV__) {
if (identifier > MAX_TOUCH_BANK) {
console.error(
'Touch identifier %s is greater than maximum supported %s which causes ' +
'performance issues backfilling array locations for all of the indices.',
identifier,
MAX_TOUCH_BANK
);
}
}
return identifier;
}
function recordTouchStart(touch: Touch): void {

View File

@@ -162,6 +162,33 @@ describe('useResponderEvents', () => {
expect(targetCallbacks.onResponderGrant).toBeCalledTimes(4);
});
// NOTE: this is only needed for performance reasons while the
// `touchBank` is an array.
test('normalizes touch identifiers', () => {
const targetRef = createRef();
let identifier;
const Component = () => {
useResponderEvents(targetRef, {
onStartShouldSetResponder: () => true,
onResponderStart: jest.fn(e => {
identifier = e.nativeEvent.identifier;
})
});
return <div ref={targetRef} />;
};
// render
act(() => {
render(<Component />);
});
const target = createEventTarget(targetRef.current);
// gesture
act(() => {
target.pointerdown({ pointerId: 123456, pointerType: 'touch' });
});
expect(identifier <= 20).toBe(true);
});
/**
* SET: onStartShouldSetResponderCapture
*/

View File

@@ -57,6 +57,15 @@ const emptyFunction = () => {};
const emptyObject = {};
const emptyArray = [];
/**
* Safari produces very large identifiers that would cause the `touchBank` array
* length to be so large as to crash the browser, if not normalized like this.
* In the future the `touchBank` should use an object/map instead.
*/
function normalizeIdentifier(identifier) {
return identifier > 20 ? identifier % 20 : identifier;
}
/**
* Converts a native DOM event to a ResponderEvent.
* Mouse events are transformed into fake touch events.
@@ -73,7 +82,9 @@ export default function createResponderEvent(domEvent: any): ResponderEvent {
const metaKey = domEvent.metaKey === true;
const shiftKey = domEvent.shiftKey === true;
const force = (domEventChangedTouches && domEventChangedTouches[0].force) || 0;
const identifier = (domEventChangedTouches && domEventChangedTouches[0].identifier) || 0;
const identifier = normalizeIdentifier(
(domEventChangedTouches && domEventChangedTouches[0].identifier) || 0
);
const clientX = (domEventChangedTouches && domEventChangedTouches[0].clientX) || domEvent.clientX;
const clientY = (domEventChangedTouches && domEventChangedTouches[0].clientY) || domEvent.clientY;
const pageX = (domEventChangedTouches && domEventChangedTouches[0].pageX) || domEvent.pageX;
@@ -86,8 +97,20 @@ export default function createResponderEvent(domEvent: any): ResponderEvent {
function normalizeTouches(touches) {
return Array.prototype.slice.call(touches).map(touch => {
touch.timestamp = timestamp;
return touch;
return {
force: touch.force,
identifier: normalizeIdentifier(touch.identifier),
get locationX() {
return locationX(touch.clientX);
},
get locationY() {
return locationY(touch.clientY);
},
pageX: touch.pageX,
pageY: touch.pageY,
target: touch.target,
timestamp
};
});
}
@@ -100,10 +123,10 @@ export default function createResponderEvent(domEvent: any): ResponderEvent {
force,
identifier,
get locationX() {
return locationX();
return locationX(clientX);
},
get locationY() {
return locationY();
return locationY(clientY);
},
pageX,
pageY,
@@ -140,10 +163,10 @@ export default function createResponderEvent(domEvent: any): ResponderEvent {
force,
identifier,
get locationX() {
return locationX();
return locationX(clientX);
},
get locationY() {
return locationY();
return locationY(clientY);
},
pageX,
pageY,
@@ -165,16 +188,16 @@ export default function createResponderEvent(domEvent: any): ResponderEvent {
// Using getters and functions serves two purposes:
// 1) The value of `currentTarget` is not initially available.
// 2) Measuring the clientRect may cause layout jank and should only be done on-demand.
function locationX() {
function locationX(x) {
rect = rect || getBoundingClientRect(responderEvent.currentTarget);
if (rect) {
return clientX - rect.left;
return x - rect.left;
}
}
function locationY() {
function locationY(y) {
rect = rect || getBoundingClientRect(responderEvent.currentTarget);
if (rect) {
return clientY - rect.top;
return y - rect.top;
}
}