Adds experimental event component responder surfaces (#15228)

* Adds Press and Hover event modules + more features to the Event Responder System
This commit is contained in:
Dominic Gannaway
2019-03-27 16:42:17 -07:00
committed by GitHub
parent d8cb10f11f
commit 669cafb36f
15 changed files with 1049 additions and 170 deletions

View File

@@ -63,7 +63,7 @@ if (__DEV__) {
* @param {function} listener Application-level callback
* @param {*} inst Internal component instance
*/
function executeDispatch(event, listener, inst) {
export function executeDispatch(event, listener, inst) {
const type = event.type || 'unknown-event';
event.currentTarget = getNodeFromInstance(inst);
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import SyntheticEvent from 'events/SyntheticEvent';
import type {AnyNativeEvent} from 'events/PluginModuleType';
export type EventResponderContext = {
event: AnyNativeEvent,
eventTarget: EventTarget,
eventType: string,
isPassive: () => boolean,
isPassiveSupported: () => boolean,
dispatchEvent: (
name: string,
listener: (e: SyntheticEvent) => void | null,
pressTarget: EventTarget | null,
discrete: boolean,
extraProperties?: Object,
) => void,
isTargetWithinElement: (
childTarget: EventTarget,
parentTarget: EventTarget,
) => boolean,
isTargetOwned: EventTarget => boolean,
isTargetWithinEventComponent: EventTarget => boolean,
isPositionWithinTouchHitTarget: (x: number, y: number) => boolean,
addRootEventTypes: (rootEventTypes: Array<string>) => void,
removeRootEventTypes: (rootEventTypes: Array<string>) => void,
requestOwnership: (target: EventTarget | null) => boolean,
releaseOwnership: (target: EventTarget | null) => boolean,
};

View File

@@ -13,7 +13,7 @@ import {registrationNameModules} from 'events/EventPluginRegistry';
import warning from 'shared/warning';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import warningWithoutStack from 'shared/warningWithoutStack';
import type {ReactEventResponder} from 'shared/ReactTypes';
import type {ReactEventResponderEventType} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import {
@@ -1277,19 +1277,18 @@ export function restoreControlledState(
}
}
export function listenToEventResponderEvents(
eventResponder: ReactEventResponder,
export function listenToEventResponderEventTypes(
eventTypes: Array<ReactEventResponderEventType>,
element: Element | Document,
): void {
if (enableEventAPI) {
const {targetEventTypes} = eventResponder;
// Get the listening Set for this element. We use this to track
// what events we're listening to.
const listeningSet = getListeningSetForElement(element);
// Go through each target event type of the event responder
for (let i = 0, length = targetEventTypes.length; i < length; ++i) {
const targetEventType = targetEventTypes[i];
for (let i = 0, length = eventTypes.length; i < length; ++i) {
const targetEventType = eventTypes[i];
let topLevelType;
let capture = false;
let passive = true;
@@ -1323,7 +1322,7 @@ export function listenToEventResponderEvents(
// Create a unique name for this event, plus its properties. We'll
// use this to ensure we don't listen to the same event with the same
// properties again.
const passiveKey = passive ? '_passive' : '';
const passiveKey = passive ? '_passive' : '_active';
const captureKey = capture ? '_capture' : '';
const listeningName = `${topLevelType}${passiveKey}${captureKey}`;
if (!listeningSet.has(listeningName)) {

View File

@@ -24,7 +24,7 @@ import {
warnForDeletedHydratableText,
warnForInsertedHydratedElement,
warnForInsertedHydratedText,
listenToEventResponderEvents,
listenToEventResponderEventTypes,
} from './ReactDOMComponent';
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
import setTextContent from './setTextContent';
@@ -864,7 +864,10 @@ export function handleEventComponent(
): void {
if (enableEventAPI) {
const rootElement = rootContainerInstance.ownerDocument;
listenToEventResponderEvents(eventResponder, rootElement);
listenToEventResponderEventTypes(
eventResponder.targetEventTypes,
rootElement,
);
}
}

View File

@@ -13,29 +13,39 @@ import {
} from 'events/EventSystemFlags';
import type {AnyNativeEvent} from 'events/PluginModuleType';
import {EventComponent} from 'shared/ReactWorkTags';
import type {ReactEventResponder} from 'shared/ReactTypes';
import warning from 'shared/warning';
import type {
ReactEventResponder,
ReactEventResponderEventType,
} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import SyntheticEvent from 'events/SyntheticEvent';
import {runEventsInBatch} from 'events/EventBatching';
import {interactiveUpdates} from 'events/ReactGenericBatching';
import {executeDispatch} from 'events/EventPluginUtils';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import {listenToEventResponderEventTypes} from '../client/ReactDOMComponent';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
// Event responders provide us an array of target event types.
// To ensure we fire the right responders for given events, we check
// if the incoming event type is actually relevant for an event
// responder. Instead of doing an O(n) lookup on the event responder
// target event types array each time, we instead create a Set for
// faster O(1) lookups.
export const eventResponderValidEventTypes: Map<
ReactEventResponder,
import {enableEventAPI} from 'shared/ReactFeatureFlags';
const rootEventTypesToEventComponents: Map<
DOMTopLevelEventType | string,
Set<Fiber>,
> = new Map();
const targetEventTypeCached: Map<
Array<ReactEventResponderEventType>,
Set<DOMTopLevelEventType>,
> = new Map();
type EventListener = (event: SyntheticEvent) => void;
function copyEventProperties(eventData, syntheticEvent) {
for (let propName in eventData) {
syntheticEvent[propName] = eventData[propName];
}
}
// TODO add context methods for dispatching events
function DOMEventResponderContext(
topLevelType: DOMTopLevelEventType,
@@ -44,13 +54,14 @@ function DOMEventResponderContext(
eventSystemFlags: EventSystemFlags,
) {
this.event = nativeEvent;
this.eventType = topLevelType;
this.eventTarget = nativeEventTarget;
this.eventType = topLevelType;
this._flags = eventSystemFlags;
this._fiber = null;
this._responder = null;
this._discreteEvents = null;
this._nonDiscreteEvents = null;
this._isBatching = true;
}
DOMEventResponderContext.prototype.isPassive = function(): boolean {
@@ -61,12 +72,6 @@ DOMEventResponderContext.prototype.isPassiveSupported = function(): boolean {
return (this._flags & PASSIVE_NOT_SUPPORTED) === 0;
};
function copyEventProperties(eventData, syntheticEvent) {
for (let propName in eventData) {
syntheticEvent[propName] = eventData[propName];
}
}
DOMEventResponderContext.prototype.dispatchEvent = function(
eventName: string,
eventListener: EventListener,
@@ -88,80 +93,155 @@ DOMEventResponderContext.prototype.dispatchEvent = function(
syntheticEvent._dispatchInstances = [eventTargetFiber];
syntheticEvent._dispatchListeners = [eventListener];
let events;
if (discrete) {
events = this._discreteEvents;
if (events === null) {
events = this._discreteEvents = [];
}
} else {
events = this._nonDiscreteEvents;
if (events === null) {
events = this._nonDiscreteEvents = [];
}
}
events.push(syntheticEvent);
};
DOMEventResponderContext.prototype._runEventsInBatch = function(): void {
if (this._discreteEvents !== null) {
interactiveUpdates(() => {
runEventsInBatch(this._discreteEvents);
});
}
if (this._nonDiscreteEvents !== null) {
runEventsInBatch(this._nonDiscreteEvents);
}
};
function createValidEventTypeSet(targetEventTypes): Set<DOMTopLevelEventType> {
const eventTypeSet = new Set();
// Go through each target event type of the event responder
for (let i = 0, length = targetEventTypes.length; i < length; ++i) {
const targetEventType = targetEventTypes[i];
if (typeof targetEventType === 'string') {
eventTypeSet.add(((targetEventType: any): DOMTopLevelEventType));
} else {
if (__DEV__) {
warning(
typeof targetEventType === 'object' && targetEventType !== null,
'Event Responder: invalid entry in targetEventTypes array. ' +
'Entry must be string or an object. Instead, got %s.',
targetEventType,
);
if (this._isBatching) {
let events;
if (discrete) {
events = this._discreteEvents;
if (events === null) {
events = this._discreteEvents = [];
}
const targetEventConfigObject = ((targetEventType: any): {
name: DOMTopLevelEventType,
passive?: boolean,
capture?: boolean,
} else {
events = this._nonDiscreteEvents;
if (events === null) {
events = this._nonDiscreteEvents = [];
}
}
events.push(syntheticEvent);
} else {
if (discrete) {
interactiveUpdates(() => {
executeDispatch(syntheticEvent, eventListener, eventTargetFiber);
});
eventTypeSet.add(targetEventConfigObject.name);
} else {
executeDispatch(syntheticEvent, eventListener, eventTargetFiber);
}
}
return eventTypeSet;
};
DOMEventResponderContext.prototype.isTargetWithinEventComponent = function(
target: AnyNativeEvent,
): boolean {
const eventFiber = this._fiber;
if (target != null) {
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber === eventFiber || fiber === eventFiber.alternate) {
return true;
}
fiber = fiber.return;
}
}
return false;
};
DOMEventResponderContext.prototype.isTargetWithinElement = function(
childTarget: EventTarget,
parentTarget: EventTarget,
): boolean {
const childFiber = getClosestInstanceFromNode(childTarget);
const parentFiber = getClosestInstanceFromNode(parentTarget);
let currentFiber = childFiber;
while (currentFiber !== null) {
if (currentFiber === parentFiber) {
return true;
}
currentFiber = currentFiber.return;
}
return false;
};
DOMEventResponderContext.prototype.addRootEventTypes = function(
rootEventTypes: Array<ReactEventResponderEventType>,
) {
const element = this.eventTarget.ownerDocument;
listenToEventResponderEventTypes(rootEventTypes, element);
const eventComponent = this._fiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
topLevelEventType,
);
if (rootEventComponents === undefined) {
rootEventComponents = new Set();
rootEventTypesToEventComponents.set(
topLevelEventType,
rootEventComponents,
);
}
rootEventComponents.add(eventComponent);
}
};
DOMEventResponderContext.prototype.removeRootEventTypes = function(
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
const eventComponent = this._fiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
topLevelEventType,
);
if (rootEventComponents !== undefined) {
rootEventComponents.delete(eventComponent);
}
}
};
DOMEventResponderContext.prototype.isPositionWithinTouchHitTarget = function() {
// TODO
};
DOMEventResponderContext.prototype.isTargetOwned = function() {
// TODO
};
DOMEventResponderContext.prototype.requestOwnership = function() {
// TODO
};
DOMEventResponderContext.prototype.releaseOwnership = function() {
// TODO
};
function getTargetEventTypes(
eventTypes: Array<ReactEventResponderEventType>,
): Set<DOMTopLevelEventType> {
let cachedSet = targetEventTypeCached.get(eventTypes);
if (cachedSet === undefined) {
cachedSet = new Set();
for (let i = 0; i < eventTypes.length; i++) {
const eventType = eventTypes[i];
const topLevelEventType =
typeof eventType === 'string' ? eventType : eventType.name;
cachedSet.add(((topLevelEventType: any): DOMTopLevelEventType));
}
targetEventTypeCached.set(eventTypes, cachedSet);
}
return cachedSet;
}
function handleTopLevelType(
topLevelType: DOMTopLevelEventType,
fiber: Fiber,
context: Object,
isRootLevelEvent: boolean,
): void {
const responder: ReactEventResponder = fiber.type.responder;
if (!isRootLevelEvent) {
// Validate the target event type exists on the responder
const targetEventTypes = getTargetEventTypes(responder.targetEventTypes);
if (!targetEventTypes.has(topLevelType)) {
return;
}
}
let {props, state} = fiber.stateNode;
let validEventTypesForResponder = eventResponderValidEventTypes.get(
responder,
);
if (validEventTypesForResponder === undefined) {
validEventTypesForResponder = createValidEventTypeSet(
responder.targetEventTypes,
);
eventResponderValidEventTypes.set(responder, validEventTypesForResponder);
}
if (!validEventTypesForResponder.has(topLevelType)) {
return;
}
if (state === null && responder.createInitialState !== undefined) {
state = fiber.stateNode.state = responder.createInitialState(props);
}
@@ -177,19 +257,49 @@ export function runResponderEventsInBatch(
nativeEventTarget: EventTarget,
eventSystemFlags: EventSystemFlags,
): void {
const context = new DOMEventResponderContext(
topLevelType,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
);
let node = targetFiber;
// Traverse up the fiber tree till we find event component fibers.
while (node !== null) {
if (node.tag === EventComponent) {
handleTopLevelType(topLevelType, node, context);
if (enableEventAPI) {
const context = new DOMEventResponderContext(
topLevelType,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
);
let node = targetFiber;
// Traverse up the fiber tree till we find event component fibers.
while (node !== null) {
if (node.tag === EventComponent) {
handleTopLevelType(topLevelType, node, context, false);
}
node = node.return;
}
node = node.return;
// Handle root level events
const rootEventComponents = rootEventTypesToEventComponents.get(
topLevelType,
);
if (rootEventComponents !== undefined) {
const rootEventComponentFibers = Array.from(rootEventComponents);
for (let i = 0; i < rootEventComponentFibers.length; i++) {
const rootEventComponentFiber = rootEventComponentFibers[i];
handleTopLevelType(
topLevelType,
rootEventComponentFiber,
context,
true,
);
}
}
// Run batched events
const discreteEvents = context._discreteEvents;
if (discreteEvents !== null) {
interactiveUpdates(() => {
runEventsInBatch(discreteEvents);
});
}
const nonDiscreteEvents = context._nonDiscreteEvents;
if (nonDiscreteEvents !== null) {
runEventsInBatch(nonDiscreteEvents);
}
context._isBatching = false;
}
context._runEventsInBatch();
}

14
packages/react-events/hover.js vendored Normal file
View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
const Hover = require('./src/Hover');
module.exports = Hover.default || Hover;

7
packages/react-events/npm/hover.js vendored Normal file
View File

@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-hover.production.min.js');
} else {
module.exports = require('./cjs/react-events-hover.development.js');
}

7
packages/react-events/npm/press.js vendored Normal file
View File

@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-press.production.min.js');
} else {
module.exports = require('./cjs/react-events-press.development.js');
}

View File

@@ -11,6 +11,7 @@
"files": [
"LICENSE",
"README.md",
"press.js",
"build-info.json",
"cjs/",
"umd/"

14
packages/react-events/press.js vendored Normal file
View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
const Press = require('./src/Press');
module.exports = Press.default || Press;

179
packages/react-events/src/Hover.js vendored Normal file
View File

@@ -0,0 +1,179 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {EventResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
const targetEventTypes = [
'pointerover',
'pointermove',
'pointerout',
'pointercancel',
];
type HoverState = {
isHovered: boolean,
isInHitSlop: boolean,
isTouched: boolean,
};
// In the case we don't have PointerEvents (Safari), we listen to touch events
// too
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
targetEventTypes.push('touchstart', 'mouseover', 'mouseout');
}
function dispatchHoverInEvents(
context: EventResponderContext,
props: Object,
state: HoverState,
): void {
const {event, eventTarget} = context;
if (props.onHoverChange) {
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
return;
}
if (props.onHoverIn) {
context.dispatchEvent('hoverin', props.onHoverIn, eventTarget, true);
}
const hoverChangeEventListener = () => {
props.onHoverChange(true);
};
context.dispatchEvent(
'hoverchange',
hoverChangeEventListener,
eventTarget,
true,
);
}
}
function dispatchHoverOutEvents(context: EventResponderContext, props: Object) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
return;
}
if (props.onHoverOut) {
context.dispatchEvent('hoverout', props.onHoverOut, eventTarget, true);
}
if (props.onHoverChange) {
const hoverChangeEventListener = () => {
props.onHoverChange(false);
};
context.dispatchEvent(
'hoverchange',
hoverChangeEventListener,
eventTarget,
true,
);
}
}
const HoverResponder = {
targetEventTypes,
createInitialState() {
return {
isHovered: false,
isInHitSlop: false,
isTouched: false,
};
},
handleEvent(
context: EventResponderContext,
props: Object,
state: HoverState,
): void {
const {eventType, eventTarget, event} = context;
switch (eventType) {
case 'touchstart':
// Touch devices don't have hover support
if (!state.isTouched) {
state.isTouched = true;
}
break;
case 'pointerover':
case 'mouseover': {
if (
!state.isHovered &&
!state.isTouched &&
!context.isTargetOwned(eventTarget)
) {
if ((event: any).pointerType === 'touch') {
state.isTouched = true;
return;
}
if (
context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
)
) {
state.isInHitSlop = true;
return;
}
dispatchHoverInEvents(context, props, state);
state.isHovered = true;
}
break;
}
case 'pointerout':
case 'mouseout': {
if (state.isHovered && !state.isTouched) {
dispatchHoverOutEvents(context, props);
state.isHovered = false;
}
state.isInHitSlop = false;
state.isTouched = false;
break;
}
case 'pointermove': {
if (!state.isTouched) {
if (state.isInHitSlop) {
if (
!context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
)
) {
dispatchHoverInEvents(context, props, state);
state.isHovered = true;
state.isInHitSlop = false;
}
} else if (
state.isHovered &&
context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
)
) {
dispatchHoverOutEvents(context, props);
state.isHovered = false;
state.isInHitSlop = true;
}
}
break;
}
case 'pointercancel': {
if (state.isHovered && !state.isTouched) {
dispatchHoverOutEvents(context, props);
state.isHovered = false;
state.isTouched = false;
}
break;
}
}
},
};
export default {
$$typeof: REACT_EVENT_COMPONENT_TYPE,
props: null,
responder: HoverResponder,
};

342
packages/react-events/src/Press.js vendored Normal file
View File

@@ -0,0 +1,342 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {EventResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
const targetEventTypes = [
{name: 'click', passive: false},
{name: 'keydown', passive: false},
'pointerdown',
'pointercancel',
'contextmenu',
];
const rootEventTypes = ['pointerup', 'scroll'];
// In the case we don't have PointerEvents (Safari), we listen to touch events
// too
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
targetEventTypes.push('touchstart', 'touchend', 'mousedown', 'touchcancel');
rootEventTypes.push('mouseup');
}
type PressState = {
defaultPrevented: boolean,
isAnchorTouched: boolean,
isLongPressed: boolean,
isPressed: boolean,
longPressTimeout: null | TimeoutID,
pressTarget: null | EventTarget,
shouldSkipMouseAfterTouch: boolean,
};
function dispatchPressEvent(
context: EventResponderContext,
name: string,
state: PressState,
listener: (e: Object) => void,
): void {
context.dispatchEvent(name, listener, state.pressTarget, true);
}
function dispatchPressInEvents(
context: EventResponderContext,
props: Object,
state: PressState,
): void {
if (props.onPressIn) {
context.dispatchEvent('pressin', props.onPressIn, state.pressTarget, true);
}
if (props.onPressChange) {
const pressChangeEventListener = () => {
props.onPressChange(true);
};
context.dispatchEvent(
'presschange',
pressChangeEventListener,
state.pressTarget,
true,
);
}
if (!state.isLongPressed && (props.onLongPress || props.onLongPressChange)) {
const longPressDelay = props.longPressDelay || 1000;
state.longPressTimeout = setTimeout(() => {
state.isLongPressed = true;
state.longPressTimeout = null;
if (props.onLongPressChange) {
const longPressChangeEventListener = () => {
props.onLongPressChange(true);
};
context.dispatchEvent(
'longpresschange',
longPressChangeEventListener,
state.pressTarget,
true,
);
}
}, longPressDelay);
}
}
function dispatchPressOutEvents(
context: EventResponderContext,
props: Object,
state: PressState,
): void {
if (state.longPressTimeout !== null) {
clearTimeout(state.longPressTimeout);
state.longPressTimeout = null;
}
if (props.onPressOut) {
context.dispatchEvent(
'pressout',
props.onPressOut,
state.pressTarget,
true,
);
}
if (props.onPressChange) {
const pressChangeEventListener = () => {
props.onPressChange(false);
};
context.dispatchEvent(
'presschange',
pressChangeEventListener,
state.pressTarget,
true,
);
}
if (props.onLongPressChange && state.isLongPressed) {
const longPressChangeEventListener = () => {
props.onLongPressChange(false);
};
context.dispatchEvent(
'longpresschange',
longPressChangeEventListener,
state.pressTarget,
true,
);
}
}
function isAnchorTagElement(eventTarget: EventTarget): boolean {
return (eventTarget: any).nodeName === 'A';
}
const PressResponder = {
targetEventTypes,
createInitialState(): PressState {
return {
defaultPrevented: false,
isAnchorTouched: false,
isLongPressed: false,
isPressed: false,
longPressTimeout: null,
pressTarget: null,
shouldSkipMouseAfterTouch: false,
};
},
handleEvent(
context: EventResponderContext,
props: Object,
state: PressState,
): void {
const {eventTarget, eventType, event} = context;
switch (eventType) {
case 'keydown': {
if (!props.onPress || context.isTargetOwned(eventTarget)) {
return;
}
const isValidKeyPress =
(event: any).which === 13 ||
(event: any).which === 32 ||
(event: any).keyCode === 13;
if (!isValidKeyPress) {
return;
}
let keyPressEventListener = props.onPress;
// Wrap listener with prevent default behaviour, unless
// we are dealing with an anchor. Anchor tags are special beacuse
// we need to use the "click" event, to properly allow browser
// heuristics for cancelling link clicks. Furthermore, iOS and
// Android can show previous of anchor tags that requires working
// with click rather than touch events (and mouse down/up).
if (!isAnchorTagElement(eventTarget)) {
keyPressEventListener = (e, key) => {
if (!e.isDefaultPrevented() && !e.nativeEvent.defaultPrevented) {
e.preventDefault();
state.defaultPrevented = true;
props.onPress(e, key);
}
};
}
dispatchPressEvent(context, 'press', state, keyPressEventListener);
break;
}
case 'touchstart':
// Touch events are for Safari, which lack pointer event support.
if (!state.isPressed && !context.isTargetOwned(eventTarget)) {
// We bail out of polyfilling anchor tags, given the same heuristics
// explained above in regards to needing to use click events.
if (isAnchorTagElement(eventTarget)) {
state.isAnchorTouched = true;
return;
}
state.pressTarget = eventTarget;
dispatchPressInEvents(context, props, state);
state.isPressed = true;
context.addRootEventTypes(rootEventTypes);
}
break;
case 'touchend': {
// Touch events are for Safari, which lack pointer event support
if (state.isAnchorTouched) {
return;
}
if (state.isPressed) {
dispatchPressOutEvents(context, props, state);
if (
eventType !== 'touchcancel' &&
(props.onPress || props.onLongPress)
) {
// Find if the X/Y of the end touch is still that of the original target
const changedTouch = (event: any).changedTouches[0];
const doc = (eventTarget: any).ownerDocument;
const target = doc.elementFromPoint(
changedTouch.screenX,
changedTouch.screenY,
);
if (
target !== null &&
context.isTargetWithinEventComponent(target)
) {
if (state.isLongPressed && props.onLongPress) {
dispatchPressEvent(
context,
'longpress',
state,
props.onLongPress,
);
} else if (props.onPress) {
dispatchPressEvent(context, 'press', state, props.onPress);
}
}
}
state.isPressed = false;
state.isLongPressed = false;
state.shouldSkipMouseAfterTouch = true;
context.removeRootEventTypes(rootEventTypes);
}
break;
}
case 'pointerdown':
case 'mousedown': {
if (
!state.isPressed &&
!context.isTargetOwned(eventTarget) &&
!state.shouldSkipMouseAfterTouch
) {
if ((event: any).pointerType === 'mouse') {
// Ignore if we are pressing on hit slop area with mouse
if (
context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
)
) {
return;
}
// Ignore right-clicks
if (event.button === 2 || event.button === 1) {
return;
}
}
state.pressTarget = eventTarget;
dispatchPressInEvents(context, props, state);
state.isPressed = true;
context.addRootEventTypes(rootEventTypes);
}
break;
}
case 'mouseup':
case 'pointerup': {
if (state.isPressed) {
if (state.shouldSkipMouseAfterTouch) {
state.shouldSkipMouseAfterTouch = false;
return;
}
dispatchPressOutEvents(context, props, state);
if (
state.pressTarget !== null &&
(props.onPress || props.onLongPress)
) {
if (context.isTargetWithinElement(eventTarget, state.pressTarget)) {
if (state.isLongPressed && props.onLongPress) {
const longPressEventListener = e => {
props.onLongPress(e);
if (e.nativeEvent.defaultPrevented) {
state.defaultPrevented = true;
}
};
dispatchPressEvent(
context,
'longpress',
state,
longPressEventListener,
);
} else if (props.onPress) {
const pressEventListener = (e, key) => {
props.onPress(e, key);
if (e.nativeEvent.defaultPrevented) {
state.defaultPrevented = true;
}
};
dispatchPressEvent(context, 'press', state, pressEventListener);
}
}
}
state.isPressed = false;
state.isLongPressed = false;
context.removeRootEventTypes(rootEventTypes);
}
state.isAnchorTouched = false;
break;
}
case 'scroll':
case 'touchcancel':
case 'contextmenu':
case 'pointercancel': {
if (state.isPressed) {
state.shouldSkipMouseAfterTouch = false;
dispatchPressOutEvents(context, props, state);
state.isPressed = false;
state.isLongPressed = false;
context.removeRootEventTypes(rootEventTypes);
}
break;
}
case 'click': {
if (state.defaultPrevented) {
(event: any).preventDefault();
state.defaultPrevented = false;
}
}
}
},
};
export default {
$$typeof: REACT_EVENT_COMPONENT_TYPE,
props: null,
responder: PressResponder,
};

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactFeatureFlags;
let ReactDOM;
let Press;
describe('Press event responder', () => {
let container;
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableEventAPI = true;
React = require('react');
ReactDOM = require('react-dom');
Press = require('react-events/press');
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('should support onPress', () => {
let buttonRef = React.createRef();
let events = [];
function handleOnPress1() {
events.push('press 1');
}
function handleOnPress2() {
events.push('press 2');
}
function handleOnMouseDown() {
events.push('mousedown');
}
function handleKeyDown() {
events.push('keydown');
}
function Component() {
return (
<Press onPress={handleOnPress1}>
<Press onPress={handleOnPress2}>
<button
ref={buttonRef}
onMouseDown={handleOnMouseDown}
onKeyDown={handleKeyDown}>
Press me!
</button>
</Press>
</Press>
);
}
ReactDOM.render(<Component />, container);
const mouseDownEvent = document.createEvent('Event');
mouseDownEvent.initEvent('mousedown', true, true);
buttonRef.current.dispatchEvent(mouseDownEvent);
const mouseUpEvent = document.createEvent('Event');
mouseUpEvent.initEvent('mouseup', true, true);
buttonRef.current.dispatchEvent(mouseUpEvent);
expect(events).toEqual(['mousedown', 'press 2', 'press 1']);
events = [];
const keyDownEvent = new KeyboardEvent('keydown', {
which: 13,
keyCode: 13,
bubbles: true,
cancelable: true,
});
buttonRef.current.dispatchEvent(keyDownEvent);
// press 1 should not occur as press 2 will preventDefault
expect(events).toEqual(['keydown', 'press 2']);
});
it('should support onPressIn and onPressOut', () => {
let divRef = React.createRef();
let events = [];
function handleOnPressIn() {
events.push('onPressIn');
}
function handleOnPressOut() {
events.push('onPressOut');
}
function Component() {
return (
<Press onPressIn={handleOnPressIn} onPressOut={handleOnPressOut}>
<div ref={divRef}>Press me!</div>
</Press>
);
}
ReactDOM.render(<Component />, container);
const pointerEnterEvent = document.createEvent('Event');
pointerEnterEvent.initEvent('pointerdown', true, true);
divRef.current.dispatchEvent(pointerEnterEvent);
const pointerLeaveEvent = document.createEvent('Event');
pointerLeaveEvent.initEvent('pointerup', true, true);
divRef.current.dispatchEvent(pointerLeaveEvent);
expect(events).toEqual(['onPressIn', 'onPressOut']);
});
});

View File

@@ -464,12 +464,49 @@ const bundles = [
/******* React Events (experimental) *******/
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
bundleTypes: [
UMD_DEV,
UMD_PROD,
NODE_DEV,
NODE_PROD,
FB_WWW_DEV,
FB_WWW_PROD,
],
moduleType: ISOMORPHIC,
entry: 'react-events',
global: 'ReactEvents',
externals: [],
},
{
bundleTypes: [
UMD_DEV,
UMD_PROD,
NODE_DEV,
NODE_PROD,
FB_WWW_DEV,
FB_WWW_PROD,
],
moduleType: NON_FIBER_RENDERER,
entry: 'react-events/press',
global: 'ReactEventsPress',
externals: [],
},
{
bundleTypes: [
UMD_DEV,
UMD_PROD,
NODE_DEV,
NODE_PROD,
FB_WWW_DEV,
FB_WWW_PROD,
],
moduleType: NON_FIBER_RENDERER,
entry: 'react-events/hover',
global: 'ReactEventsHover',
externals: [],
},
];
// Based on deep-freeze by substack (public domain)

View File

@@ -46,29 +46,29 @@
"filename": "react-dom.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 803793,
"gzip": 183116
"size": 832345,
"gzip": 188603
},
{
"filename": "react-dom.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 107752,
"gzip": 34911
"size": 107683,
"gzip": 34867
},
{
"filename": "react-dom.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 813661,
"gzip": 184364
"size": 826372,
"gzip": 186963
},
{
"filename": "react-dom.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 108035,
"gzip": 34515
"size": 107664,
"gzip": 34301
},
{
"filename": "ReactDOM-dev.js",
@@ -88,15 +88,15 @@
"filename": "react-dom-test-utils.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 48274,
"gzip": 13318
"size": 48620,
"gzip": 13278
},
{
"filename": "react-dom-test-utils.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 10511,
"gzip": 3880
"size": 10184,
"gzip": 3732
},
{
"filename": "react-dom-test-utils.development.js",
@@ -123,22 +123,22 @@
"filename": "react-dom-unstable-native-dependencies.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 62061,
"gzip": 16285
"size": 62190,
"gzip": 16206
},
{
"filename": "react-dom-unstable-native-dependencies.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 11266,
"gzip": 3889
"size": 10936,
"gzip": 3741
},
{
"filename": "react-dom-unstable-native-dependencies.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 61643,
"gzip": 16033
"size": 61854,
"gzip": 16078
},
{
"filename": "react-dom-unstable-native-dependencies.production.min.js",
@@ -165,29 +165,29 @@
"filename": "react-dom-server.browser.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 133264,
"gzip": 35517
"size": 136840,
"gzip": 36205
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 19666,
"gzip": 7443
"size": 19363,
"gzip": 7290
},
{
"filename": "react-dom-server.browser.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 132758,
"gzip": 35195
"size": 132878,
"gzip": 35237
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 19756,
"gzip": 7540
"size": 19287,
"gzip": 7290
},
{
"filename": "ReactDOMServer-dev.js",
@@ -207,15 +207,15 @@
"filename": "react-dom-server.node.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 134747,
"gzip": 35752
"size": 134867,
"gzip": 35791
},
{
"filename": "react-dom-server.node.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 20639,
"gzip": 7850
"size": 20170,
"gzip": 7598
},
{
"filename": "react-art.development.js",
@@ -515,50 +515,50 @@
"filename": "ReactDOM-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 821918,
"gzip": 182691
"size": 851672,
"gzip": 188651
},
{
"filename": "ReactDOM-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 333267,
"gzip": 61003
"size": 339041,
"gzip": 62418
},
{
"filename": "ReactTestUtils-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 44870,
"gzip": 12188
"size": 46251,
"gzip": 12476
},
{
"filename": "ReactDOMUnstableNativeDependencies-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 59002,
"gzip": 14967
"size": 60296,
"gzip": 15251
},
{
"filename": "ReactDOMUnstableNativeDependencies-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 26900,
"gzip": 5426
"size": 26767,
"gzip": 5381
},
{
"filename": "ReactDOMServer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 130310,
"gzip": 33947
"size": 135272,
"gzip": 35002
},
{
"filename": "ReactDOMServer-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 46994,
"gzip": 10956
"size": 46877,
"gzip": 10879
},
{
"filename": "ReactART-dev.js",
@@ -718,8 +718,8 @@
"filename": "react-dom.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
"size": 111211,
"gzip": 35133
"size": 110839,
"gzip": 34944
},
{
"filename": "ReactNativeRenderer-profiling.js",
@@ -767,8 +767,8 @@
"filename": "ReactDOM-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
"size": 339339,
"gzip": 62379
"size": 345581,
"gzip": 63810
},
{
"filename": "ReactNativeRenderer-profiling.js",
@@ -795,8 +795,8 @@
"filename": "react-dom.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
"size": 110829,
"gzip": 35621
"size": 110730,
"gzip": 35485
},
{
"filename": "scheduler-tracing.development.js",
@@ -1033,64 +1033,64 @@
"filename": "react-dom-unstable-fire.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 804147,
"gzip": 183253
"size": 832471,
"gzip": 188731
},
{
"filename": "react-dom-unstable-fire.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 107767,
"gzip": 34920
"size": 107698,
"gzip": 34877
},
{
"filename": "react-dom-unstable-fire.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
"size": 110844,
"gzip": 35630
"size": 110745,
"gzip": 35493
},
{
"filename": "react-dom-unstable-fire.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 814014,
"gzip": 184503
"size": 826725,
"gzip": 187104
},
{
"filename": "react-dom-unstable-fire.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 108049,
"gzip": 34524
"size": 107678,
"gzip": 34310
},
{
"filename": "react-dom-unstable-fire.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
"size": 111225,
"gzip": 35142
"size": 110853,
"gzip": 34953
},
{
"filename": "ReactFire-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 821109,
"gzip": 182607
"size": 850863,
"gzip": 188551
},
{
"filename": "ReactFire-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 321531,
"gzip": 58583
"size": 327881,
"gzip": 60185
},
{
"filename": "ReactFire-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
"size": 327694,
"gzip": 59916
"size": 334366,
"gzip": 61586
},
{
"filename": "jest-mock-scheduler.development.js",