react-native-gesture-handler
Version:
Declarative API exposing native platform touch and gesture system to React Native
393 lines (382 loc) • 17.7 kB
JavaScript
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
;