UNPKG

react-native-gesture-handler

Version:

Declarative API exposing native platform touch and gesture system to React Native

393 lines (382 loc) 17.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = createHandler; var React = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _customDirectEventTypes = require("./customDirectEventTypes"); var _RNGestureHandlerModule = _interopRequireDefault(require("../RNGestureHandlerModule")); var _State = require("../State"); var _handlersRegistry = require("./handlersRegistry"); var _getNextHandlerTag = require("./getNextHandlerTag"); var _utils = require("./utils"); var _findNodeHandle = _interopRequireDefault(require("../findNodeHandle")); var _utils2 = require("../utils"); var _ActionType = require("../ActionType"); var _PressabilityDebugView = require("./PressabilityDebugView"); var _GestureHandlerRootViewContext = _interopRequireDefault(require("../GestureHandlerRootViewContext")); var _ghQueueMicrotask = require("../ghQueueMicrotask"); var _mountRegistry = require("../mountRegistry"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } const UIManagerAny = _reactNative.UIManager; _customDirectEventTypes.customDirectEventTypes.topGestureHandlerEvent = { registrationName: 'onGestureHandlerEvent' }; const customGHEventsConfigFabricAndroid = { topOnGestureHandlerEvent: { registrationName: 'onGestureHandlerEvent' }, topOnGestureHandlerStateChange: { registrationName: 'onGestureHandlerStateChange' } }; const customGHEventsConfig = { onGestureHandlerEvent: { registrationName: 'onGestureHandlerEvent' }, onGestureHandlerStateChange: { registrationName: 'onGestureHandlerStateChange' }, // When using React Native Gesture Handler for Animated.event with useNativeDriver: true // on Android with Fabric enabled, the native part still sends the native events to JS // but prefixed with "top". We cannot simply rename the events above so they are prefixed // with "top" instead of "on" because in such case Animated.events would not be registered. // That's why we need to register another pair of event names. // The incoming events will be queued but never handled. // Without this piece of code below, you'll get the following JS error: // Unsupported top level event type "topOnGestureHandlerEvent" dispatched ...((0, _utils2.isFabric)() && _reactNative.Platform.OS === 'android' && customGHEventsConfigFabricAndroid) }; // Add gesture specific events to genericDirectEventTypes object exported from UIManager // native module. // Once new event types are registered with react it is possible to dispatch these // events to all kind of native views. UIManagerAny.genericDirectEventTypes = { ...UIManagerAny.genericDirectEventTypes, ...customGHEventsConfig }; const UIManagerConstants = UIManagerAny.getViewManagerConfig?.('getConstants'); if (UIManagerConstants) { UIManagerConstants.genericDirectEventTypes = { ...UIManagerConstants.genericDirectEventTypes, ...customGHEventsConfig }; } // Wrap JS responder calls and notify gesture handler manager const { setJSResponder: oldSetJSResponder = () => { // no-op }, clearJSResponder: oldClearJSResponder = () => { // no-op } } = UIManagerAny; UIManagerAny.setJSResponder = (tag, blockNativeResponder) => { _RNGestureHandlerModule.default.handleSetJSResponder(tag, blockNativeResponder); oldSetJSResponder(tag, blockNativeResponder); }; UIManagerAny.clearJSResponder = () => { _RNGestureHandlerModule.default.handleClearJSResponder(); oldClearJSResponder(); }; let allowTouches = true; const DEV_ON_ANDROID = __DEV__ && _reactNative.Platform.OS === 'android'; // Toggled inspector blocks touch events in order to allow inspecting on Android // This needs to be a global variable in order to set initial state for `allowTouches` property in Handler component if (DEV_ON_ANDROID) { _reactNative.DeviceEventEmitter.addListener('toggleElementInspector', () => { allowTouches = !allowTouches; }); } function hasUnresolvedRefs(props) { // TODO(TS) - add type for extract arg const extract = refs => { if (!Array.isArray(refs)) { return refs && refs.current === null; } return refs.some(r => r && r.current === null); }; return extract(props['simultaneousHandlers']) || extract(props['waitFor']); } const stateToPropMappings = { [_State.State.UNDETERMINED]: undefined, [_State.State.BEGAN]: 'onBegan', [_State.State.FAILED]: 'onFailed', [_State.State.CANCELLED]: 'onCancelled', [_State.State.ACTIVE]: 'onActivated', [_State.State.END]: 'onEnded' }; // TODO(TS) fix event types const UNRESOLVED_REFS_RETRY_LIMIT = 1; // TODO(TS) - make sure that BaseGestureHandlerProps doesn't need other generic parameter to work with custom properties. function createHandler({ name, allowedProps = [], config = {}, transformProps, customNativeProps = [] }) { class Handler extends React.Component { static displayName = name; static contextType = _GestureHandlerRootViewContext.default; handlerTag = -1; constructor(props) { super(props); this.config = {}; this.propsRef = /*#__PURE__*/React.createRef(); this.isMountedRef = /*#__PURE__*/React.createRef(); this.state = { allowTouches }; if (props.id) { if (_handlersRegistry.handlerIDToTag[props.id] !== undefined) { throw new Error(`Handler with ID "${props.id}" already registered`); } _handlersRegistry.handlerIDToTag[props.id] = this.handlerTag; } } componentDidMount() { const props = this.props; this.isMountedRef.current = true; if (DEV_ON_ANDROID) { this.inspectorToggleListener = _reactNative.DeviceEventEmitter.addListener('toggleElementInspector', () => { this.setState(_ => ({ allowTouches })); this.update(UNRESOLVED_REFS_RETRY_LIMIT); }); } if (hasUnresolvedRefs(props)) { // If there are unresolved refs (e.g. ".current" has not yet been set) // passed as `simultaneousHandlers` or `waitFor`, we enqueue a call to // _update method that will try to update native handler props using // queueMicrotask. This makes it so update() function gets called after all // react components are mounted and we expect the missing ref object to // be resolved by then. (0, _ghQueueMicrotask.ghQueueMicrotask)(() => { this.update(UNRESOLVED_REFS_RETRY_LIMIT); }); } this.createGestureHandler((0, _utils.filterConfig)(transformProps ? transformProps(this.props) : this.props, [...allowedProps, ...customNativeProps], config)); if (!this.viewNode) { throw new Error(`[Gesture Handler] Failed to obtain view for ${Handler.displayName}. Note that old API doesn't support functional components.`); } this.attachGestureHandler((0, _findNodeHandle.default)(this.viewNode)); // TODO(TS) - check if this can be null } componentDidUpdate() { const viewTag = (0, _findNodeHandle.default)(this.viewNode); if (this.viewTag !== viewTag) { this.attachGestureHandler(viewTag); // TODO(TS) - check interaction between _viewTag & findNodeHandle } this.update(UNRESOLVED_REFS_RETRY_LIMIT); } componentWillUnmount() { this.inspectorToggleListener?.remove(); this.isMountedRef.current = false; if (_reactNative.Platform.OS !== 'web') { (0, _handlersRegistry.unregisterOldGestureHandler)(this.handlerTag); } _RNGestureHandlerModule.default.dropGestureHandler(this.handlerTag); (0, _utils.scheduleFlushOperations)(); // We can't use this.props.id directly due to TS generic type narrowing bug, see https://github.com/microsoft/TypeScript/issues/13995 for more context const handlerID = this.props.id; if (handlerID) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete _handlersRegistry.handlerIDToTag[handlerID]; } _mountRegistry.MountRegistry.gestureHandlerWillUnmount(this); } onGestureHandlerEvent = event => { if (event.nativeEvent.handlerTag === this.handlerTag) { if (typeof this.props.onGestureEvent === 'function') { this.props.onGestureEvent?.(event); } } else { this.props.onGestureHandlerEvent?.(event); } }; // TODO(TS) - make sure this is right type for event onGestureHandlerStateChange = event => { if (event.nativeEvent.handlerTag === this.handlerTag) { if (typeof this.props.onHandlerStateChange === 'function') { this.props.onHandlerStateChange?.(event); } const state = event.nativeEvent.state; const stateEventName = stateToPropMappings[state]; const eventHandler = stateEventName && this.props[stateEventName]; if (eventHandler && typeof eventHandler === 'function') { eventHandler(event); } } else { this.props.onGestureHandlerStateChange?.(event); } }; refHandler = node => { this.viewNode = node; const child = React.Children.only(this.props.children); // @ts-ignore Since React 19 ref is accessible as standard prop // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#deprecated-element-ref const ref = (0, _utils2.isReact19)() ? child.props?.ref : child?.ref; if (!ref) { return; } if (typeof ref === 'function') { ref(node); } else { ref.current = node; } }; createGestureHandler = newConfig => { this.handlerTag = (0, _getNextHandlerTag.getNextHandlerTag)(); this.config = newConfig; _RNGestureHandlerModule.default.createGestureHandler(name, this.handlerTag, newConfig); }; attachGestureHandler = newViewTag => { this.viewTag = newViewTag; if (_reactNative.Platform.OS === 'web') { // Typecast due to dynamic resolution, attachGestureHandler should have web version signature in this branch _RNGestureHandlerModule.default.attachGestureHandler(this.handlerTag, newViewTag, _ActionType.ActionType.JS_FUNCTION_OLD_API, // ignored on web this.propsRef); } else { (0, _handlersRegistry.registerOldGestureHandler)(this.handlerTag, { onGestureEvent: this.onGestureHandlerEvent, onGestureStateChange: this.onGestureHandlerStateChange }); const actionType = (() => { const onGestureEvent = this.props?.onGestureEvent; const isGestureHandlerWorklet = onGestureEvent && ('current' in onGestureEvent || 'workletEventHandler' in onGestureEvent); const onHandlerStateChange = this.props?.onHandlerStateChange; const isStateChangeHandlerWorklet = onHandlerStateChange && ('current' in onHandlerStateChange || 'workletEventHandler' in onHandlerStateChange); const isReanimatedHandler = isGestureHandlerWorklet || isStateChangeHandlerWorklet; if (isReanimatedHandler) { // Reanimated worklet return _ActionType.ActionType.REANIMATED_WORKLET; } else if (onGestureEvent && '__isNative' in onGestureEvent) { // Animated.event with useNativeDriver: true return _ActionType.ActionType.NATIVE_ANIMATED_EVENT; } else { // JS callback or Animated.event with useNativeDriver: false return _ActionType.ActionType.JS_FUNCTION_OLD_API; } })(); _RNGestureHandlerModule.default.attachGestureHandler(this.handlerTag, newViewTag, actionType); } (0, _utils.scheduleFlushOperations)(); (0, _ghQueueMicrotask.ghQueueMicrotask)(() => { _mountRegistry.MountRegistry.gestureHandlerWillMount(this); }); }; updateGestureHandler = newConfig => { this.config = newConfig; _RNGestureHandlerModule.default.updateGestureHandler(this.handlerTag, newConfig); (0, _utils.scheduleFlushOperations)(); }; update(remainingTries) { if (!this.isMountedRef.current) { return; } const props = this.props; // When ref is set via a function i.e. `ref={(r) => refObject.current = r}` instead of // `ref={refObject}` it's possible that it won't be resolved in time. Seems like trying // again is easy enough fix. if (hasUnresolvedRefs(props) && remainingTries > 0) { (0, _ghQueueMicrotask.ghQueueMicrotask)(() => { this.update(remainingTries - 1); }); } else { const newConfig = (0, _utils.filterConfig)(transformProps ? transformProps(this.props) : this.props, [...allowedProps, ...customNativeProps], config); if (!(0, _utils2.deepEqual)(this.config, newConfig)) { this.updateGestureHandler(newConfig); } } } // eslint-disable-next-line @eslint-react/no-unused-class-component-members setNativeProps(updates) { const mergedProps = { ...this.props, ...updates }; const newConfig = (0, _utils.filterConfig)(transformProps ? transformProps(mergedProps) : mergedProps, [...allowedProps, ...customNativeProps], config); this.updateGestureHandler(newConfig); } render() { if (__DEV__ && !this.context && !(0, _utils2.isTestEnv)() && _reactNative.Platform.OS !== 'web') { throw new Error(name + ' must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/installation for more details.'); } let gestureEventHandler = this.onGestureHandlerEvent; // Another instance of https://github.com/microsoft/TypeScript/issues/13995 const { onGestureEvent, onGestureHandlerEvent } = this.props; if (onGestureEvent && typeof onGestureEvent !== 'function') { // If it's not a method it should be an native Animated.event // object. We set it directly as the handler for the view // In this case nested handlers are not going to be supported if (onGestureHandlerEvent) { throw new Error('Nesting touch handlers with native animated driver is not supported yet'); } gestureEventHandler = onGestureEvent; } else { if (onGestureHandlerEvent && typeof onGestureHandlerEvent !== 'function') { throw new Error('Nesting touch handlers with native animated driver is not supported yet'); } } let gestureStateEventHandler = this.onGestureHandlerStateChange; // Another instance of https://github.com/microsoft/TypeScript/issues/13995 const { onHandlerStateChange, onGestureHandlerStateChange } = this.props; if (onHandlerStateChange && typeof onHandlerStateChange !== 'function') { // If it's not a method it should be an native Animated.event // object. We set it directly as the handler for the view // In this case nested handlers are not going to be supported if (onGestureHandlerStateChange) { throw new Error('Nesting touch handlers with native animated driver is not supported yet'); } gestureStateEventHandler = onHandlerStateChange; } else { if (onGestureHandlerStateChange && typeof onGestureHandlerStateChange !== 'function') { throw new Error('Nesting touch handlers with native animated driver is not supported yet'); } } const events = { onGestureHandlerEvent: this.state.allowTouches ? gestureEventHandler : undefined, onGestureHandlerStateChange: this.state.allowTouches ? gestureStateEventHandler : undefined }; this.propsRef.current = events; let child = null; try { child = React.Children.only(this.props.children); } catch (e) { throw new Error((0, _utils2.tagMessage)(`${name} got more than one view as a child. If you want the gesture to work on multiple views, wrap them with a common parent and attach the gesture to that view.`)); } let grandChildren = child.props.children; if (__DEV__ && child.type && (child.type === 'RNGestureHandlerButton' || child.type.name === 'View' || child.type.displayName === 'View')) { grandChildren = React.Children.toArray(grandChildren); grandChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_PressabilityDebugView.PressabilityDebugView, { color: "mediumspringgreen", hitSlop: child.props.hitSlop }, "pressabilityDebugView")); } return /*#__PURE__*/React.cloneElement(child, { ref: this.refHandler, collapsable: false, ...((0, _utils2.isTestEnv)() ? { handlerType: name, handlerTag: this.handlerTag, enabled: this.props.enabled } : {}), testID: this.props.testID ?? child.props.testID, ...events }, grandChildren); } } return Handler; } //# sourceMappingURL=createHandler.js.map