From a41b217708fcf2a8a325c83f89a480b2ebd91ce6 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 29 Mar 2019 10:31:18 -0700 Subject: [PATCH] Add additional event API responder surfaces (#15248) * Add rest of event modules + small fixes --- packages/events/EventTypes.js | 9 +- .../src/events/DOMEventResponderSystem.js | 34 ++- packages/react-events/drag.js | 14 ++ packages/react-events/focus.js | 14 ++ packages/react-events/npm/drag.js | 7 + packages/react-events/npm/focus.js | 7 + packages/react-events/npm/swipe.js | 7 + packages/react-events/package.json | 5 + packages/react-events/src/Drag.js | 187 +++++++++++++++ packages/react-events/src/Focus.js | 101 ++++++++ packages/react-events/src/Press.js | 12 +- packages/react-events/src/Swipe.js | 217 ++++++++++++++++++ packages/react-events/swipe.js | 14 ++ scripts/rollup/bundles.js | 45 ++++ 14 files changed, 659 insertions(+), 14 deletions(-) create mode 100644 packages/react-events/drag.js create mode 100644 packages/react-events/focus.js create mode 100644 packages/react-events/npm/drag.js create mode 100644 packages/react-events/npm/focus.js create mode 100644 packages/react-events/npm/swipe.js create mode 100644 packages/react-events/src/Drag.js create mode 100644 packages/react-events/src/Focus.js create mode 100644 packages/react-events/src/Swipe.js create mode 100644 packages/react-events/swipe.js diff --git a/packages/events/EventTypes.js b/packages/events/EventTypes.js index bba4e239881bd..a03ef8f7405c1 100644 --- a/packages/events/EventTypes.js +++ b/packages/events/EventTypes.js @@ -9,6 +9,7 @@ import SyntheticEvent from 'events/SyntheticEvent'; import type {AnyNativeEvent} from 'events/PluginModuleType'; +import type {ReactEventResponderEventType} from 'shared/ReactTypes'; export type EventResponderContext = { event: AnyNativeEvent, @@ -30,8 +31,12 @@ export type EventResponderContext = { isTargetOwned: EventTarget => boolean, isTargetWithinEventComponent: EventTarget => boolean, isPositionWithinTouchHitTarget: (x: number, y: number) => boolean, - addRootEventTypes: (rootEventTypes: Array) => void, - removeRootEventTypes: (rootEventTypes: Array) => void, + addRootEventTypes: ( + rootEventTypes: Array, + ) => void, + removeRootEventTypes: ( + rootEventTypes: Array, + ) => void, requestOwnership: (target: EventTarget | null) => boolean, releaseOwnership: (target: EventTarget | null) => boolean, }; diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index ce23eb5ad9d77..9c52389654ffb 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -44,6 +44,7 @@ const targetEventTypeCached: Map< Array, Set, > = new Map(); +const targetOwnership: Map = new Map(); type EventListener = (event: SyntheticEvent) => void; @@ -204,16 +205,37 @@ DOMEventResponderContext.prototype.isPositionWithinTouchHitTarget = function() { // TODO }; -DOMEventResponderContext.prototype.isTargetOwned = function() { - // TODO +DOMEventResponderContext.prototype.isTargetOwned = function( + targetElement: Element | Node, +): boolean { + const targetDoc = targetElement.ownerDocument; + return targetOwnership.has(targetDoc); }; -DOMEventResponderContext.prototype.requestOwnership = function() { - // TODO +DOMEventResponderContext.prototype.requestOwnership = function( + targetElement: Element | Node, +): boolean { + const targetDoc = targetElement.ownerDocument; + if (targetOwnership.has(targetDoc)) { + return false; + } + targetOwnership.set(targetDoc, this._fiber); + return true; }; -DOMEventResponderContext.prototype.releaseOwnership = function() { - // TODO +DOMEventResponderContext.prototype.releaseOwnership = function( + targetElement: Element | Node, +): boolean { + const targetDoc = targetElement.ownerDocument; + if (!targetOwnership.has(targetDoc)) { + return false; + } + const owner = targetOwnership.get(targetDoc); + if (owner === this._fiber || owner === this._fiber.alternate) { + targetOwnership.delete(targetDoc); + return true; + } + return false; }; function getTargetEventTypes( diff --git a/packages/react-events/drag.js b/packages/react-events/drag.js new file mode 100644 index 0000000000000..f8148f1273cbf --- /dev/null +++ b/packages/react-events/drag.js @@ -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 Drag = require('./src/Drag'); + +module.exports = Drag.default || Drag; diff --git a/packages/react-events/focus.js b/packages/react-events/focus.js new file mode 100644 index 0000000000000..0b9288a790fef --- /dev/null +++ b/packages/react-events/focus.js @@ -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 Focus = require('./src/Focus'); + +module.exports = Focus.default || Focus; diff --git a/packages/react-events/npm/drag.js b/packages/react-events/npm/drag.js new file mode 100644 index 0000000000000..4b8838b9658c8 --- /dev/null +++ b/packages/react-events/npm/drag.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-events-drag.production.min.js'); +} else { + module.exports = require('./cjs/react-events-drag.development.js'); +} diff --git a/packages/react-events/npm/focus.js b/packages/react-events/npm/focus.js new file mode 100644 index 0000000000000..06b656a761ed3 --- /dev/null +++ b/packages/react-events/npm/focus.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-events-focus.production.min.js'); +} else { + module.exports = require('./cjs/react-events-focus.development.js'); +} diff --git a/packages/react-events/npm/swipe.js b/packages/react-events/npm/swipe.js new file mode 100644 index 0000000000000..aa2b1f2fe13f9 --- /dev/null +++ b/packages/react-events/npm/swipe.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-events-swipe.production.min.js'); +} else { + module.exports = require('./cjs/react-events-swipe.development.js'); +} diff --git a/packages/react-events/package.json b/packages/react-events/package.json index a61330b9ce455..0950a38579bcf 100644 --- a/packages/react-events/package.json +++ b/packages/react-events/package.json @@ -12,6 +12,11 @@ "LICENSE", "README.md", "press.js", + "hover.js", + "focus.js", + "swipe.js", + "drag.js", + "index.js", "build-info.json", "cjs/", "umd/" diff --git a/packages/react-events/src/Drag.js b/packages/react-events/src/Drag.js new file mode 100644 index 0000000000000..7181b4efa233b --- /dev/null +++ b/packages/react-events/src/Drag.js @@ -0,0 +1,187 @@ +/** + * 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 = ['pointerdown', 'pointercancel']; +const rootEventTypes = ['pointerup', {name: 'pointermove', passive: false}]; + +type DragState = { + dragTarget: null | EventTarget, + isPointerDown: boolean, + isDragging: boolean, + startX: number, + startY: number, + x: number, + y: number, +}; + +// 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', 'mousemove', { + name: 'touchmove', + passive: false, + }); +} + +function dispatchDragEvent( + context: EventResponderContext, + name: string, + listener: (e: Object) => void, + state: DragState, + discrete: boolean, + eventData?: { + diffX: number, + diffY: number, + }, +): void { + context.dispatchEvent(name, listener, state.dragTarget, discrete, eventData); +} + +const DragResponder = { + targetEventTypes, + createInitialState(): DragState { + return { + dragTarget: null, + isPointerDown: false, + isDragging: false, + startX: 0, + startY: 0, + x: 0, + y: 0, + }; + }, + handleEvent( + context: EventResponderContext, + props: Object, + state: DragState, + ): void { + const {eventTarget, eventType, event} = context; + + switch (eventType) { + case 'touchstart': + case 'mousedown': + case 'pointerdown': { + if (!state.isDragging) { + const obj = + eventType === 'touchstart' ? (event: any).changedTouches[0] : event; + const x = (state.startX = (obj: any).screenX); + const y = (state.startY = (obj: any).screenY); + state.x = x; + state.y = y; + state.dragTarget = eventTarget; + state.isPointerDown = true; + context.addRootEventTypes(rootEventTypes); + } + break; + } + case 'touchmove': + case 'mousemove': + case 'pointermove': { + if (context.isPassive()) { + return; + } + if (state.isPointerDown) { + const obj = + eventType === 'touchmove' ? (event: any).changedTouches[0] : event; + const x = (obj: any).screenX; + const y = (obj: any).screenY; + state.x = x; + state.y = y; + if (!state.isDragging && x !== state.startX && y !== state.startY) { + let shouldEnableDragging = true; + + if ( + props.onShouldClaimOwnership && + props.onShouldClaimOwnership() + ) { + shouldEnableDragging = context.requestOwnership(state.dragTarget); + } + if (shouldEnableDragging) { + state.isDragging = true; + if (props.onDragChange) { + const dragChangeEventListener = () => { + props.onDragChange(true); + }; + context.dispatchEvent( + 'dragchange', + dragChangeEventListener, + state.dragTarget, + true, + ); + } + } else { + state.dragTarget = null; + state.isPointerDown = false; + context.removeRootEventTypes(rootEventTypes); + } + } else { + if (props.onDragMove) { + const eventData = { + diffX: x - state.startX, + diffY: y - state.startY, + }; + dispatchDragEvent( + context, + 'dragmove', + props.onDragMove, + state, + false, + eventData, + ); + } + (event: any).preventDefault(); + } + } + break; + } + case 'pointercancel': + case 'touchcancel': + case 'touchend': + case 'mouseup': + case 'pointerup': { + if (state.isDragging) { + if (props.onShouldClaimOwnership) { + context.releaseOwnership(state.dragTarget); + } + if (props.onDragEnd) { + dispatchDragEvent(context, 'dragend', props.onDragEnd, state, true); + } + if (props.onDragChange) { + const dragChangeEventListener = () => { + props.onDragChange(false); + }; + context.dispatchEvent( + 'dragchange', + dragChangeEventListener, + state.dragTarget, + true, + ); + } + state.isDragging = false; + } + if (state.isPointerDown) { + state.dragTarget = null; + state.isPointerDown = false; + context.removeRootEventTypes(rootEventTypes); + } + break; + } + } + }, +}; + +export default { + $$typeof: REACT_EVENT_COMPONENT_TYPE, + props: null, + responder: DragResponder, +}; diff --git a/packages/react-events/src/Focus.js b/packages/react-events/src/Focus.js new file mode 100644 index 0000000000000..9c652f69d26be --- /dev/null +++ b/packages/react-events/src/Focus.js @@ -0,0 +1,101 @@ +/** + * 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: 'focus', passive: true, capture: true}, + {name: 'blur', passive: true, capture: true}, +]; + +type FocusState = { + isFocused: boolean, +}; + +function dispatchFocusInEvents(context: EventResponderContext, props: Object) { + const {event, eventTarget} = context; + if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { + return; + } + if (props.onFocus) { + context.dispatchEvent('focus', props.onFocus, eventTarget, true); + } + if (props.onFocusChange) { + const focusChangeEventListener = () => { + props.onFocusChange(true); + }; + context.dispatchEvent( + 'focuschange', + focusChangeEventListener, + eventTarget, + true, + ); + } +} + +function dispatchFocusOutEvents(context: EventResponderContext, props: Object) { + const {event, eventTarget} = context; + if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { + return; + } + if (props.onBlur) { + context.dispatchEvent('blur', props.onBlur, eventTarget, true); + } + if (props.onFocusChange) { + const focusChangeEventListener = () => { + props.onFocusChange(false); + }; + context.dispatchEvent( + 'focuschange', + focusChangeEventListener, + eventTarget, + true, + ); + } +} + +const FocusResponder = { + targetEventTypes, + createInitialState(): FocusState { + return { + isFocused: false, + }; + }, + handleEvent( + context: EventResponderContext, + props: Object, + state: FocusState, + ): void { + const {eventTarget, eventType} = context; + + switch (eventType) { + case 'focus': { + if (!state.isFocused && !context.isTargetOwned(eventTarget)) { + dispatchFocusInEvents(context, props); + state.isFocused = true; + } + break; + } + case 'blur': { + if (state.isFocused) { + dispatchFocusOutEvents(context, props); + state.isFocused = false; + } + break; + } + } + }, +}; + +export default { + $$typeof: REACT_EVENT_COMPONENT_TYPE, + props: null, + responder: FocusResponder, +}; diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index 8c46fd391d273..99becac03aef7 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -17,13 +17,13 @@ const targetEventTypes = [ 'pointercancel', 'contextmenu', ]; -const rootEventTypes = ['pointerup', 'scroll']; +const rootEventTypes = [{name: 'pointerup', passive: false}, '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'); + rootEventTypes.push({name: 'mouseup', passive: false}); } type PressState = { @@ -169,11 +169,11 @@ const PressResponder = { // 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) => { + keyPressEventListener = e => { if (!e.isDefaultPrevented() && !e.nativeEvent.defaultPrevented) { e.preventDefault(); state.defaultPrevented = true; - props.onPress(e, key); + props.onPress(e); } }; } @@ -279,8 +279,8 @@ const PressResponder = { props.onPress && !(state.isLongPressed && props.longPressCancelsPress) ) { - const pressEventListener = (e, key) => { - props.onPress(e, key); + const pressEventListener = e => { + props.onPress(e); if (e.nativeEvent.defaultPrevented) { state.defaultPrevented = true; } diff --git a/packages/react-events/src/Swipe.js b/packages/react-events/src/Swipe.js new file mode 100644 index 0000000000000..f265b2473242f --- /dev/null +++ b/packages/react-events/src/Swipe.js @@ -0,0 +1,217 @@ +/** + * 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 = ['pointerdown', 'pointercancel']; +const rootEventTypes = ['pointerup', {name: 'pointermove', passive: false}]; + +// 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', 'mousemove', { + name: 'touchmove', + passive: false, + }); +} + +function dispatchSwipeEvent( + context: EventResponderContext, + name: string, + listener: (e: Object) => void, + state: SwipeState, + discrete: boolean, + eventData?: { + diffX: number, + diffY: number, + }, +) { + context.dispatchEvent(name, listener, state.swipeTarget, discrete, eventData); +} + +type SwipeState = { + direction: number, + isSwiping: boolean, + lastDirection: number, + startX: number, + startY: number, + touchId: null | number, + swipeTarget: null | EventTarget, + x: number, + y: number, +}; + +const SwipeResponder = { + targetEventTypes, + createInitialState(): SwipeState { + return { + direction: 0, + isSwiping: false, + lastDirection: 0, + startX: 0, + startY: 0, + touchId: null, + swipeTarget: null, + x: 0, + y: 0, + }; + }, + handleEvent( + context: EventResponderContext, + props: Object, + state: SwipeState, + ): void { + const {eventTarget, eventType, event} = context; + + switch (eventType) { + case 'touchstart': + case 'mousedown': + case 'pointerdown': { + if (!state.isSwiping && !context.isTargetOwned(eventTarget)) { + let obj = event; + if (eventType === 'touchstart') { + obj = (event: any).targetTouches[0]; + state.touchId = obj.identifier; + } + const x = (obj: any).screenX; + const y = (obj: any).screenY; + + let shouldEnableSwiping = true; + + if (props.onShouldClaimOwnership && props.onShouldClaimOwnership()) { + shouldEnableSwiping = context.requestOwnership(eventTarget); + } + if (shouldEnableSwiping) { + state.isSwiping = true; + state.startX = x; + state.startY = y; + state.x = x; + state.y = y; + state.swipeTarget = eventTarget; + context.addRootEventTypes(rootEventTypes); + } else { + state.touchId = null; + } + } + break; + } + case 'touchmove': + case 'mousemove': + case 'pointermove': { + if (context.isPassive()) { + return; + } + if (state.isSwiping) { + let obj = null; + if (eventType === 'touchmove') { + const targetTouches = (event: any).targetTouches; + for (let i = 0; i < targetTouches.length; i++) { + if (state.touchId === targetTouches[i].identifier) { + obj = targetTouches[i]; + break; + } + } + } else { + obj = event; + } + if (obj === null) { + state.isSwiping = false; + state.swipeTarget = null; + state.touchId = null; + context.removeRootEventTypes(rootEventTypes); + return; + } + const x = (obj: any).screenX; + const y = (obj: any).screenY; + if (x < state.x && props.onSwipeLeft) { + state.direction = 3; + } else if (x > state.x && props.onSwipeRight) { + state.direction = 1; + } + state.x = x; + state.y = y; + if (props.onSwipeMove) { + const eventData = { + diffX: x - state.startX, + diffY: y - state.startY, + }; + dispatchSwipeEvent( + context, + 'swipemove', + props.onSwipeMove, + state, + false, + eventData, + ); + (event: any).preventDefault(); + } + } + break; + } + case 'pointercancel': + case 'touchcancel': + case 'touchend': + case 'mouseup': + case 'pointerup': { + if (state.isSwiping) { + if (state.x === state.startX && state.y === state.startY) { + return; + } + if (props.onShouldClaimOwnership) { + context.releaseOwnership(state.swipeTarget); + } + const direction = state.direction; + const lastDirection = state.lastDirection; + if (direction !== lastDirection) { + if (props.onSwipeLeft && direction === 3) { + dispatchSwipeEvent( + context, + 'swipeleft', + props.onSwipeLeft, + state, + true, + ); + } else if (props.onSwipeRight && direction === 1) { + dispatchSwipeEvent( + context, + 'swiperight', + props.onSwipeRight, + state, + true, + ); + } + } + if (props.onSwipeEnd) { + dispatchSwipeEvent( + context, + 'swipeend', + props.onSwipeEnd, + state, + true, + ); + } + state.lastDirection = direction; + state.isSwiping = false; + state.swipeTarget = null; + state.touchId = null; + context.removeRootEventTypes(rootEventTypes); + } + break; + } + } + }, +}; + +export default { + $$typeof: REACT_EVENT_COMPONENT_TYPE, + props: null, + responder: SwipeResponder, +}; diff --git a/packages/react-events/swipe.js b/packages/react-events/swipe.js new file mode 100644 index 0000000000000..3c2ad195ef4b2 --- /dev/null +++ b/packages/react-events/swipe.js @@ -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 Swipe = require('./src/Swipe'); + +module.exports = Swipe.default || Swipe; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 078a4697e52a3..98bfdadcd6a9d 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -507,6 +507,51 @@ const bundles = [ global: 'ReactEventsHover', externals: [], }, + + { + bundleTypes: [ + UMD_DEV, + UMD_PROD, + NODE_DEV, + NODE_PROD, + FB_WWW_DEV, + FB_WWW_PROD, + ], + moduleType: NON_FIBER_RENDERER, + entry: 'react-events/focus', + global: 'ReactEventsFocus', + externals: [], + }, + + { + bundleTypes: [ + UMD_DEV, + UMD_PROD, + NODE_DEV, + NODE_PROD, + FB_WWW_DEV, + FB_WWW_PROD, + ], + moduleType: NON_FIBER_RENDERER, + entry: 'react-events/swipe', + global: 'ReactEventsSwipe', + externals: [], + }, + + { + bundleTypes: [ + UMD_DEV, + UMD_PROD, + NODE_DEV, + NODE_PROD, + FB_WWW_DEV, + FB_WWW_PROD, + ], + moduleType: NON_FIBER_RENDERER, + entry: 'react-events/drag', + global: 'ReactEventsDrag', + externals: [], + }, ]; // Based on deep-freeze by substack (public domain)