diff --git a/apps/common-app/App.tsx b/apps/common-app/App.tsx index 2e4a95d279..e2a9cff07a 100644 --- a/apps/common-app/App.tsx +++ b/apps/common-app/App.tsx @@ -76,6 +76,10 @@ import LongPressExample from './src/simple/longPress'; import ManualExample from './src/simple/manual'; import SimpleFling from './src/simple/fling'; +import FlatListExample from './src/components/flatlist'; +import ScrollViewExample from './src/components/scrollview'; +import ButtonsExample from './src/components/buttons'; + import { Icon } from '@swmansion/icons'; interface Example { @@ -114,6 +118,14 @@ const EXAMPLES: ExamplesSection[] = [ }, ], }, + { + sectionTitle: 'Components', + data: [ + { name: 'FlatList example', component: FlatListExample }, + { name: 'ScrollView example', component: ScrollViewExample }, + { name: 'Buttons example', component: ButtonsExample }, + ], + }, { sectionTitle: 'Basic examples', data: [ diff --git a/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx b/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx index d7a467f834..39fa22d0e5 100644 --- a/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx +++ b/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { createNativeWrapper, - DrawerLayoutAndroid, + LegacyDrawerLayoutAndroid, } from 'react-native-gesture-handler'; const WrappedViewPagerAndroid = createNativeWrapper(ViewPagerAndroid, { @@ -35,22 +35,22 @@ export default class Example extends Component { return ( - navigationView}> - + - navigationView}> - + ); diff --git a/apps/common-app/src/components/buttons/index.tsx b/apps/common-app/src/components/buttons/index.tsx new file mode 100644 index 0000000000..d64cfc4cab --- /dev/null +++ b/apps/common-app/src/components/buttons/index.tsx @@ -0,0 +1,61 @@ +import { StyleSheet, Text } from 'react-native'; +import { + BaseButton, + BorderlessButton, + GestureHandlerRootView, + RectButton, +} from 'react-native-gesture-handler'; + +type ButtonWrapperProps = { + ButtonComponent: + | typeof BaseButton + | typeof RectButton + | typeof BorderlessButton; + + color: string; +}; + +function ButtonWrapper({ ButtonComponent, color }: ButtonWrapperProps) { + return ( + console.log(`[${ButtonComponent.name}] onPress`)} + onLongPress={() => { + console.log(`[${ButtonComponent.name}] onLongPress`); + }}> + {ButtonComponent.name} + + ); +} + +export default function ButtonsExample() { + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'space-around', + }, + button: { + width: 200, + height: 50, + borderRadius: 15, + + display: 'flex', + alignItems: 'center', + justifyContent: 'space-around', + }, + + buttonText: { + color: 'white', + fontSize: 16, + }, +}); diff --git a/apps/common-app/src/components/flatlist/index.tsx b/apps/common-app/src/components/flatlist/index.tsx new file mode 100644 index 0000000000..19482e5ea2 --- /dev/null +++ b/apps/common-app/src/components/flatlist/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { FlatList, GestureHandlerRootView } from 'react-native-gesture-handler'; + +const DATA = Array.from({ length: 20 }, (_, i) => ({ + id: i.toString(), + title: `Item ${i + 1}`, +})); + +const Item = ({ title }: { title: string }) => ( + + {title} + +); + +export default function FlatListExample() { + return ( + + item.id} + renderItem={({ item }) => } + contentContainerStyle={styles.container} + /> + ; + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingVertical: 16, + }, + item: { + backgroundColor: '#f9c2ff', + padding: 20, + marginVertical: 8, + marginHorizontal: 16, + borderRadius: 8, + }, + title: { + fontSize: 18, + }, +}); diff --git a/apps/common-app/src/components/scrollview/index.tsx b/apps/common-app/src/components/scrollview/index.tsx new file mode 100644 index 0000000000..72f5923723 --- /dev/null +++ b/apps/common-app/src/components/scrollview/index.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { Text, StyleSheet } from 'react-native'; +import { ScrollView, RefreshControl } from 'react-native-gesture-handler'; + +export default function ScrollViewExample() { + const [refreshing, setRefreshing] = useState(false); + + const onRefresh = () => { + setRefreshing(true); + setTimeout(() => { + setRefreshing(false); + }, 1500); + }; + + return ( + + }> + Pull down to refresh! + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + text: { + fontSize: 18, + }, +}); diff --git a/apps/common-app/src/new_api/calculator/index.tsx b/apps/common-app/src/new_api/calculator/index.tsx index 84b48fcb4f..07c92a6282 100644 --- a/apps/common-app/src/new_api/calculator/index.tsx +++ b/apps/common-app/src/new_api/calculator/index.tsx @@ -10,7 +10,7 @@ import { import { GestureDetector, Gesture, - ScrollView, + LegacyScrollView, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, @@ -60,7 +60,7 @@ interface OutputProps { function Output({ offset, expression, history }: OutputProps) { const layout = useRef({}); - const scrollView = useRef(null); + const scrollView = useRef(null); const drag = useSharedValue(0); const dragOffset = useSharedValue(0); const [opened, setOpened] = useState(false); @@ -130,8 +130,8 @@ function Output({ offset, expression, history }: OutputProps) { - { + { if (!opened) { ref?.scrollToEnd({ animated: false }); } @@ -146,7 +146,7 @@ function Output({ offset, expression, history }: OutputProps) { })} - + diff --git a/apps/common-app/src/recipes/panAndScroll/index.tsx b/apps/common-app/src/recipes/panAndScroll/index.tsx index 87dbe28caa..b31f52faeb 100644 --- a/apps/common-app/src/recipes/panAndScroll/index.tsx +++ b/apps/common-app/src/recipes/panAndScroll/index.tsx @@ -3,10 +3,10 @@ import { Animated, Dimensions, StyleSheet } from 'react-native'; import { PanGestureHandler, TapGestureHandler, - ScrollView, State, PanGestureHandlerGestureEvent, TapGestureHandlerStateChangeEvent, + LegacyScrollView, } from 'react-native-gesture-handler'; import { USE_NATIVE_DRIVER } from '../../config'; @@ -92,11 +92,11 @@ export default class Example extends Component { const tapRef = React.createRef(); const panRef = React.createRef(); return ( - + - + ); } } diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index 04409f7f2c..43ed5518a6 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -12,14 +12,17 @@ import { import type { NativeViewGestureHandlerPayload } from '../handlers/GestureHandlerEventPayload'; import type { BaseButtonWithRefProps, - BaseButtonProps, + LegacyBaseButtonProps, RectButtonWithRefProps, - RectButtonProps, + LegacyRectButtonProps, BorderlessButtonWithRefProps, - BorderlessButtonProps, + LegacyBorderlessButtonProps, } from './GestureButtonsProps'; -export const RawButton = createNativeWrapper(GestureHandlerButton, { +/** + * @deprecated use `RawButton` instead + */ +export const LegacyRawButton = createNativeWrapper(GestureHandlerButton, { shouldCancelWhenOutside: false, shouldActivateOnStart: false, }); @@ -123,7 +126,7 @@ class InnerBaseButton extends React.Component { const { rippleColor, style, ...rest } = this.props; return ( - { const AnimatedInnerBaseButton = Animated.createAnimatedComponent(InnerBaseButton); -export const BaseButton = React.forwardRef< +/** + * @deprecated use `BaseButton` instead + */ +export const LegacyBaseButton = React.forwardRef< React.ComponentType, - Omit + Omit >((props, ref) => ); const AnimatedBaseButton = React.forwardRef< @@ -185,7 +191,7 @@ class InnerRectButton extends React.Component { const resolvedStyle = StyleSheet.flatten(style) ?? {}; return ( - { ]} /> {children} - + ); } } -export const RectButton = React.forwardRef< +/** + * @deprecated use `RectButton` instead + */ +export const LegacyRectButton = React.forwardRef< React.ComponentType, - Omit + Omit >((props, ref) => ); class InnerBorderlessButton extends React.Component { @@ -251,9 +260,12 @@ class InnerBorderlessButton extends React.Component + Omit >((props, ref) => ); -export { default as PureNativeButton } from './GestureHandlerButton'; +export { default as LegacyPureNativeButton } from './GestureHandlerButton'; diff --git a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts index e9504bff32..fe4a97575f 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts +++ b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts @@ -8,7 +8,10 @@ import { } from 'react-native'; import type { NativeViewGestureHandlerProps } from '../handlers/NativeViewGestureHandler'; -export interface RawButtonProps +/** + * @deprecated use `RawButtonProps` with `RawButton` instead + */ +export interface LegacyRawButtonProps extends NativeViewGestureHandlerProps, AccessibilityProps { /** @@ -94,7 +97,10 @@ interface ButtonWithRefProps { innerRef?: React.ForwardedRef>; } -export interface BaseButtonProps extends RawButtonProps { +/** + * @deprecated use `BaseButtonProps` with `BaseButton` instead + */ +export interface LegacyBaseButtonProps extends LegacyRawButtonProps { /** * Called when the button gets pressed (analogous to `onPress` in * `TouchableHighlight` from RN core). @@ -123,10 +129,13 @@ export interface BaseButtonProps extends RawButtonProps { delayLongPress?: number; } export interface BaseButtonWithRefProps - extends BaseButtonProps, + extends LegacyBaseButtonProps, ButtonWithRefProps {} -export interface RectButtonProps extends BaseButtonProps { +/** + * @deprecated use `RectButtonProps` with `RectButton` instead + */ +export interface LegacyRectButtonProps extends LegacyBaseButtonProps { /** * Background color that will be dimmed when button is in active state. */ @@ -140,10 +149,14 @@ export interface RectButtonProps extends BaseButtonProps { activeOpacity?: number; } export interface RectButtonWithRefProps - extends RectButtonProps, + extends LegacyRectButtonProps, ButtonWithRefProps {} -export interface BorderlessButtonProps extends BaseButtonProps { +/** + * @deprecated use `BorderlessButtonProps` with `BorderlessButton` instead + */ + +export interface LegacyBorderlessButtonProps extends LegacyBaseButtonProps { /** * iOS only. * @@ -152,5 +165,5 @@ export interface BorderlessButtonProps extends BaseButtonProps { activeOpacity?: number; } export interface BorderlessButtonWithRefProps - extends BorderlessButtonProps, + extends LegacyBorderlessButtonProps, ButtonWithRefProps {} diff --git a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx index 8bdf73a35d..aac075a8d0 100644 --- a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx @@ -28,12 +28,17 @@ import { import { toArray } from '../utils'; -export const RefreshControl = createNativeWrapper(RNRefreshControl, { +/** + * @deprecated use `RefreshControl` instead + */ +export const LegacyRefreshControl = createNativeWrapper(RNRefreshControl, { disallowInterruption: true, shouldCancelWhenOutside: false, }); + // eslint-disable-next-line @typescript-eslint/no-redeclare -export type RefreshControl = typeof RefreshControl & RNRefreshControl; +export type LegacyRefreshControl = typeof LegacyRefreshControl & + RNRefreshControl; const GHScrollView = createNativeWrapper>( RNScrollView, @@ -42,11 +47,15 @@ const GHScrollView = createNativeWrapper>( shouldCancelWhenOutside: false, } ); -export const ScrollView = React.forwardRef< + +/** + * @deprecated use `ScrollView` instead + */ +export const LegacyScrollView = React.forwardRef< RNScrollView, RNScrollViewProps & NativeViewGestureHandlerProps >((props, ref) => { - const refreshControlGestureRef = React.useRef(null); + const refreshControlGestureRef = React.useRef(null); const { refreshControl, waitFor, ...rest } = props; return ( @@ -67,32 +76,46 @@ export const ScrollView = React.forwardRef< /> ); }); + // Backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457 // include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them. // eslint-disable-next-line @typescript-eslint/no-redeclare -export type ScrollView = typeof GHScrollView & RNScrollView; +export type LegacyScrollView = typeof GHScrollView & RNScrollView; -export const Switch = createNativeWrapper(RNSwitch, { +/** + * @deprecated use `Switch` instead + */ +export const LegacySwitch = createNativeWrapper(RNSwitch, { shouldCancelWhenOutside: false, shouldActivateOnStart: true, disallowInterruption: true, }); // eslint-disable-next-line @typescript-eslint/no-redeclare -export type Switch = typeof Switch & RNSwitch; +export type LegacySwitch = typeof LegacySwitch & RNSwitch; -export const TextInput = createNativeWrapper(RNTextInput); +/** + * @deprecated use `RefreshControl` instead + */ +export const LegacyTextInput = + createNativeWrapper(RNTextInput); // eslint-disable-next-line @typescript-eslint/no-redeclare -export type TextInput = typeof TextInput & RNTextInput; +export type LegacyTextInput = typeof LegacyTextInput & RNTextInput; -export const DrawerLayoutAndroid = createNativeWrapper< +/** + * @deprecated use `DrawerLayoutAndroid` instead + */ +export const LegacyDrawerLayoutAndroid = createNativeWrapper< PropsWithChildren >(RNDrawerLayoutAndroid, { disallowInterruption: true }); // eslint-disable-next-line @typescript-eslint/no-redeclare -export type DrawerLayoutAndroid = typeof DrawerLayoutAndroid & +export type LegacyDrawerLayoutAndroid = typeof LegacyDrawerLayoutAndroid & RNDrawerLayoutAndroid; -export const FlatList = React.forwardRef((props, ref) => { - const refreshControlGestureRef = React.useRef(null); +/** + * @deprecated use `FlatList` instead + */ +export const LegacyFlatList = React.forwardRef((props, ref) => { + const refreshControlGestureRef = React.useRef(null); const { waitFor, refreshControl, ...rest } = props; @@ -117,7 +140,7 @@ export const FlatList = React.forwardRef((props, ref) => { ref={ref} {...flatListProps} renderScrollComponent={(scrollProps) => ( - { }) as ( props: PropsWithChildren< Omit, 'renderScrollComponent'> & - RefAttributes> & + RefAttributes> & NativeViewGestureHandlerProps >, - ref?: ForwardedRef> + ref?: ForwardedRef> ) => ReactElement | null; // eslint-disable-next-line @typescript-eslint/no-redeclare -export type FlatList = typeof FlatList & RNFlatList; +export type LegacyFlatList = typeof LegacyFlatList & + RNFlatList; diff --git a/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx b/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx index ccc03eaf8d..6526532342 100644 --- a/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx @@ -10,32 +10,54 @@ import { import createNativeWrapper from '../handlers/createNativeWrapper'; -export const ScrollView = createNativeWrapper(RNScrollView, { +/** + * @deprecated use `ScrollView` instead + */ +export const LegacyScrollView = createNativeWrapper(RNScrollView, { disallowInterruption: false, }); -export const Switch = createNativeWrapper(RNSwitch, { +/** + * @deprecated use `Switch` instead + */ +export const LegacySwitch = createNativeWrapper(RNSwitch, { shouldCancelWhenOutside: false, shouldActivateOnStart: true, disallowInterruption: true, }); -export const TextInput = createNativeWrapper(RNTextInput); -export const DrawerLayoutAndroid = () => { + +/** + * @deprecated use `TextInput` instead + */ +export const LegacyTextInput = createNativeWrapper(RNTextInput); + +/** + * @deprecated use `DrawerLayoutAndroid` instead + */ +export const LegacyDrawerLayoutAndroid = () => { console.warn('DrawerLayoutAndroid is not supported on web!'); return ; }; +/** + * @deprecated use `RefreshControl` instead + */ // RefreshControl is implemented as a functional component, rendering a View // NativeViewGestureHandler needs to set a ref on its child, which cannot be done // on functional components -export const RefreshControl = createNativeWrapper(View); +export const LegacyRefreshControl = createNativeWrapper(View); -export const FlatList = React.forwardRef( +/** + * @deprecated use `FlatList` instead + */ +export const LegacyFlatList = React.forwardRef( (props: FlatListProps, ref: any) => ( } + renderScrollComponent={(scrollProps) => ( + + )} /> ) ); diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx index b6f0c391c0..8f7ddcd1fb 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx @@ -1,5 +1,5 @@ import { HostComponent } from 'react-native'; -import type { RawButtonProps } from './GestureButtonsProps'; +import type { LegacyRawButtonProps } from './GestureButtonsProps'; import RNGestureHandlerButtonNativeComponent from '../specs/RNGestureHandlerButtonNativeComponent'; -export default RNGestureHandlerButtonNativeComponent as HostComponent; +export default RNGestureHandlerButtonNativeComponent as HostComponent; diff --git a/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx b/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx index 6bbb344f49..01bff2671a 100644 --- a/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx +++ b/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx @@ -3,7 +3,7 @@ import { Component } from 'react'; import { Animated, Platform } from 'react-native'; import { State } from '../../State'; -import { BaseButton } from '../GestureButtons'; +import { LegacyBaseButton } from '../GestureButtons'; import { GestureEvent, @@ -251,7 +251,7 @@ export default class GenericTouchable extends Component< }; return ( - {this.props.children} - + ); } } diff --git a/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts b/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts index 050208ba2f..c4e4ff2506 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts @@ -1,8 +1,8 @@ import type { - BaseButtonProps, - BorderlessButtonProps, - RawButtonProps, - RectButtonProps, + LegacyBaseButtonProps, + LegacyBorderlessButtonProps, + LegacyRawButtonProps, + LegacyRectButtonProps, } from '../components/GestureButtonsProps'; import { GestureEvent, @@ -94,8 +94,22 @@ export type FlingGestureHandlerProperties = FlingGestureHandlerProps; * @deprecated ForceTouch gesture is deprecated and will be removed in the future. */ export type ForceTouchGestureHandlerProperties = ForceTouchGestureHandlerProps; + // Button props -export type RawButtonProperties = RawButtonProps; -export type BaseButtonProperties = BaseButtonProps; -export type RectButtonProperties = RectButtonProps; -export type BorderlessButtonProperties = BorderlessButtonProps; + +/** + * @deprecated Use RawButtonProperties instead + */ +export type LegacyRawButtonProperties = LegacyRawButtonProps; +/** + * @deprecated Use BaseButtonProperties instead + */ +export type LegacyBaseButtonProperties = LegacyBaseButtonProps; +/** + * @deprecated Use RectButtonProperties instead + */ +export type LegacyRectButtonProperties = LegacyRectButtonProps; +/** + * @deprecated Use BorderlessButtonProperties instead + */ +export type LegacyBorderlessButtonProperties = LegacyBorderlessButtonProps; diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index f1dfcea542..6e189eb528 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -70,19 +70,32 @@ export type { } from './handlers/gestures/gestureComposition'; export type { GestureStateManagerType as GestureStateManager } from './handlers/gestures/gestureStateManager'; export { NativeViewGestureHandler } from './handlers/NativeViewGestureHandler'; +export type { + LegacyRawButtonProps, + LegacyBaseButtonProps, + LegacyRectButtonProps, + LegacyBorderlessButtonProps, +} from './components/GestureButtonsProps'; export type { RawButtonProps, BaseButtonProps, RectButtonProps, BorderlessButtonProps, -} from './components/GestureButtonsProps'; +} from './v3/components/GestureButtonsProps'; +export { + LegacyRawButton, + LegacyBaseButton, + LegacyRectButton, + LegacyBorderlessButton, + LegacyPureNativeButton, +} from './components/GestureButtons'; export { RawButton, BaseButton, RectButton, BorderlessButton, PureNativeButton, -} from './components/GestureButtons'; +} from './v3/components/GestureButtons'; export type { TouchableHighlightProps, TouchableOpacityProps, @@ -94,14 +107,21 @@ export { TouchableOpacity, TouchableWithoutFeedback, } from './components/touchables'; +export { + LegacyScrollView, + LegacySwitch, + LegacyTextInput, + LegacyDrawerLayoutAndroid, + LegacyFlatList, + LegacyRefreshControl, +} from './components/GestureComponents'; export { ScrollView, Switch, TextInput, - DrawerLayoutAndroid, FlatList, RefreshControl, -} from './components/GestureComponents'; +} from './v3/components/GestureComponents'; export { Text } from './components/Text'; export { HoverEffect } from './handlers/gestures/hoverGesture'; export type { @@ -137,10 +157,10 @@ export type { FlingGestureHandlerProperties, ForceTouchGestureHandlerProperties, // Buttons props - RawButtonProperties, - BaseButtonProperties, - RectButtonProperties, - BorderlessButtonProperties, + LegacyRawButtonProperties, + LegacyBaseButtonProperties, + LegacyRectButtonProperties, + LegacyBorderlessButtonProperties, } from './handlers/gestureHandlerTypesCompat'; export type { diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx new file mode 100644 index 0000000000..f91e8930cb --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx @@ -0,0 +1,178 @@ +import React, { useRef } from 'react'; +import { Platform, StyleSheet, Animated } from 'react-native'; +import createNativeWrapper from '../createNativeWrapper'; +import GestureHandlerButton from '../../components/GestureHandlerButton'; +import type { + BaseButtonProps, + BorderlessButtonProps, + RectButtonProps, +} from './GestureButtonsProps'; + +import type { GestureStateChangeEvent } from '../types'; +import type { NativeViewHandlerData } from '../hooks/gestures/native/useNative'; + +type CallbackEventType = GestureStateChangeEvent; + +export const RawButton = createNativeWrapper(GestureHandlerButton, { + shouldCancelWhenOutside: false, + shouldActivateOnStart: false, +}); + +export const BaseButton = (props: BaseButtonProps) => { + const longPressDetected = useRef(false); + const longPressTimeout = useRef | undefined>( + undefined + ); + + const delayLongPress = props.delayLongPress ?? 600; + + const { + onLongPress, + onPress, + onActiveStateChange, + rippleColor, + style, + ...rest + } = props; + + const wrappedLongPress = () => { + longPressDetected.current = true; + onLongPress?.(); + }; + + const onBegin = (e: CallbackEventType) => { + if (Platform.OS === 'android' && e.handlerData.pointerInside) { + longPressDetected.current = false; + if (onLongPress) { + longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress); + } + } + }; + + const onStart = (e: CallbackEventType) => { + onActiveStateChange?.(true); + + if (Platform.OS !== 'android' && e.handlerData.pointerInside) { + longPressDetected.current = false; + if (onLongPress) { + longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress); + } + } + + if ( + !e.handlerData.pointerInside && + longPressTimeout.current !== undefined + ) { + clearTimeout(longPressTimeout.current); + longPressTimeout.current = undefined; + } + }; + + const onEnd = (e: CallbackEventType, success: boolean) => { + onActiveStateChange?.(false); + + if (success && !longPressDetected.current) { + onPress?.(e.handlerData.pointerInside); + } + }; + + const onFinalize = (_e: CallbackEventType) => { + if (longPressTimeout.current !== undefined) { + clearTimeout(longPressTimeout.current); + longPressTimeout.current = undefined; + } + }; + + return ( + + ); +}; + +const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton); + +const btnStyles = StyleSheet.create({ + underlay: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + top: 0, + }, +}); + +export const RectButton = (props: RectButtonProps) => { + const activeOpacity = props.activeOpacity ?? 0.105; + const underlayColor = props.underlayColor ?? 'black'; + + const opacity = useRef(new Animated.Value(0)).current; + + const onActiveStateChange = (active: boolean) => { + if (Platform.OS !== 'android') { + opacity.setValue(active ? activeOpacity : 0); + } + + props.onActiveStateChange?.(active); + }; + + const { children, style, ...rest } = props; + + const resolvedStyle = StyleSheet.flatten(style ?? {}); + + return ( + + + {children} + + ); +}; + +export const BorderlessButton = (props: BorderlessButtonProps) => { + const activeOpacity = props.activeOpacity ?? 0.3; + const opacity = useRef(new Animated.Value(1)).current; + + const onActiveStateChange = (active: boolean) => { + if (Platform.OS === 'ios') { + opacity.setValue(active ? activeOpacity : 1); + } + + props.onActiveStateChange?.(active); + }; + + const { children, style, ...rest } = props; + + return ( + + {children} + + ); +}; + +export { default as PureNativeButton } from '../../components/GestureHandlerButton'; diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts b/packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts new file mode 100644 index 0000000000..20122247b3 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts @@ -0,0 +1,156 @@ +import * as React from 'react'; +import { + AccessibilityProps, + ColorValue, + LayoutChangeEvent, + StyleProp, + ViewStyle, +} from 'react-native'; +import type { NativeViewGestureHandlerProps } from '../../handlers/NativeViewGestureHandler'; + +export interface RawButtonProps + extends NativeViewGestureHandlerProps, + AccessibilityProps { + /** + * Defines if more than one button could be pressed simultaneously. By default + * set true. + */ + exclusive?: boolean; + // TODO: we should transform props in `createNativeWrapper` + /** + * Android only. + * + * Defines color of native ripple animation used since API level 21. + */ + rippleColor?: number | ColorValue | null; + + /** + * Android only. + * + * Defines radius of native ripple animation used since API level 21. + */ + rippleRadius?: number | null; + + /** + * Android only. + * + * Set this to true if you want the ripple animation to render outside the view bounds. + */ + borderless?: boolean; + + /** + * Android only. + * + * Defines whether the ripple animation should be drawn on the foreground of the view. + */ + foreground?: boolean; + + /** + * Android only. + * + * Set this to true if you don't want the system to play sound when the button is pressed. + */ + touchSoundDisabled?: boolean; + + /** + * Style object, use it to set additional styles. + */ + style?: StyleProp; + + /** + * Invoked on mount and layout changes. + */ + onLayout?: (event: LayoutChangeEvent) => void; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onPress?: Function | null; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onPressIn?: Function | null; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onPressOut?: Function | null; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onLongPress?: Function | null; +} +interface ButtonWithRefProps { + ref?: React.RefObject; +} + +export interface BaseButtonProps extends RawButtonProps { + /** + * Called when the button gets pressed (analogous to `onPress` in + * `TouchableHighlight` from RN core). + */ + onPress?: (pointerInside: boolean) => void; + + /** + * Called when the button gets pressed and is held for `delayLongPress` + * milliseconds. + */ + onLongPress?: () => void; + + /** + * Called when button changes from inactive to active and vice versa. It + * passes active state as a boolean variable as a first parameter for that + * method. + */ + onActiveStateChange?: (active: boolean) => void; + style?: StyleProp; + testID?: string; + + /** + * Delay, in milliseconds, after which the `onLongPress` callback gets called. + * Defaults to 600. + */ + delayLongPress?: number; +} +export interface BaseButtonWithRefProps + extends BaseButtonProps, + ButtonWithRefProps {} + +export interface RectButtonProps extends BaseButtonProps { + /** + * Background color that will be dimmed when button is in active state. + */ + underlayColor?: string; + + /** + * iOS only. + * + * Opacity applied to the underlay when button is in active state. + */ + activeOpacity?: number; +} +export interface RectButtonWithRefProps + extends RectButtonProps, + ButtonWithRefProps {} + +export interface BorderlessButtonProps extends BaseButtonProps { + /** + * iOS only. + * + * Opacity applied to the button when it is in an active state. + */ + activeOpacity?: number; +} +export interface BorderlessButtonWithRefProps + extends BorderlessButtonProps, + ButtonWithRefProps {} diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx new file mode 100644 index 0000000000..2623e1ce2a --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx @@ -0,0 +1,164 @@ +import * as React from 'react'; +import { + PropsWithChildren, + ForwardedRef, + RefAttributes, + ReactElement, +} from 'react'; +import { + ScrollView as RNScrollView, + ScrollViewProps as RNScrollViewProps, + Switch as RNSwitch, + SwitchProps as RNSwitchProps, + TextInput as RNTextInput, + TextInputProps as RNTextInputProps, + FlatList as RNFlatList, + FlatListProps as RNFlatListProps, + RefreshControl as RNRefreshControl, +} from 'react-native'; + +import createNativeWrapper, { + ComponentWrapperRef, +} from '../createNativeWrapper'; + +import { NativeWrapperProperties } from '../types/NativeWrapperType'; +import { NativeWrapperProps } from '../hooks/utils'; +import { AnyGesture } from '../types'; + +export const RefreshControl = createNativeWrapper(RNRefreshControl, { + disallowInterruption: true, + shouldCancelWhenOutside: false, +}); + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type RefreshControl = typeof RefreshControl & RNRefreshControl; + +const GHScrollView = createNativeWrapper>( + RNScrollView, + { + disallowInterruption: true, + shouldCancelWhenOutside: false, + } +); +export const ScrollView = ( + props: RNScrollViewProps & NativeWrapperProperties +) => { + const refreshControlRef = + React.useRef>(null); + const { refreshControl, requireToFail, ...rest } = props; + + const waitFor = []; + + if (Array.isArray(requireToFail)) { + waitFor.push(...requireToFail); + } else if (requireToFail) { + waitFor.push(requireToFail); + } + + if (refreshControlRef.current?.gestureRef) { + waitFor.push(refreshControlRef.current.gestureRef); + } + + return ( + + ); +}; +// Backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457 +// include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them. +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ScrollView = typeof GHScrollView & RNScrollView; + +export const Switch = createNativeWrapper(RNSwitch, { + shouldCancelWhenOutside: false, + shouldActivateOnStart: true, + disallowInterruption: true, +}); +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type Switch = typeof Switch & RNSwitch; + +export const TextInput = createNativeWrapper(RNTextInput); +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type TextInput = typeof TextInput & RNTextInput; + +export const FlatList = ((props) => { + const refreshControlRef = + React.useRef>(null); + + const { requireToFail, refreshControl, ...rest } = props; + + const flatListProps = {}; + const scrollViewProps = {}; + for (const [propName, value] of Object.entries(rest)) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/26255 + if (NativeWrapperProps.has(propName)) { + // @ts-ignore - this function cannot have generic type so we have to ignore this error + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + scrollViewProps[propName] = value; + } else { + // @ts-ignore - this function cannot have generic type so we have to ignore this error + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + flatListProps[propName] = value; + } + } + + const waitFor: AnyGesture[] = []; + + if (Array.isArray(requireToFail)) { + waitFor.push(...requireToFail); + } else if (requireToFail) { + waitFor.push(requireToFail); + } + + if (refreshControlRef.current?.gestureRef) { + waitFor.push(refreshControlRef.current.gestureRef); + } + + return ( + // @ts-ignore - this function cannot have generic type so we have to ignore this error + ( + + )} + // @ts-ignore we don't pass `refreshing` prop as we only want to override the ref + refreshControl={ + refreshControl + ? React.cloneElement(refreshControl, { + // @ts-ignore for reasons unknown to me, `ref` doesn't exist on the type inferred by TS + ref: refreshControlRef, + }) + : undefined + } + /> + ); +}) as ( + props: PropsWithChildren< + Omit, 'renderScrollComponent'> & + RefAttributes> & + NativeWrapperProperties + >, + ref?: ForwardedRef> +) => ReactElement | null; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type FlatList = typeof FlatList & RNFlatList; diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.web.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.web.tsx new file mode 100644 index 0000000000..a9e69cc62d --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.web.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { + FlatList as RNFlatList, + Switch as RNSwitch, + TextInput as RNTextInput, + ScrollView as RNScrollView, + FlatListProps, + View, +} from 'react-native'; + +import createNativeWrapper from '../createNativeWrapper'; + +export const ScrollView = createNativeWrapper(RNScrollView, { + disallowInterruption: false, +}); + +export const Switch = createNativeWrapper(RNSwitch, { + shouldCancelWhenOutside: false, + shouldActivateOnStart: true, + disallowInterruption: true, +}); + +export const TextInput = createNativeWrapper(RNTextInput); + +export const DrawerLayoutAndroid = () => { + console.warn('DrawerLayoutAndroid is not supported on web!'); + return ; +}; + +// RefreshControl is implemented as a functional component, rendering a View +// NativeViewGestureHandler needs to set a ref on its child, which cannot be done +// on functional components +export const RefreshControl = createNativeWrapper(View); + +export const FlatList = React.forwardRef( + (props: FlatListProps, ref: any) => ( + } + /> + ) +); diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx new file mode 100644 index 0000000000..05f43c3ba5 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -0,0 +1,74 @@ +import React, { useImperativeHandle, useRef } from 'react'; + +import { NativeWrapperProps } from './hooks/utils'; +import { useNative } from './hooks/gestures'; +import { NativeDetector } from './detectors/NativeDetector'; +import type { NativeWrapperProperties } from './types/NativeWrapperType'; +import { + NativeViewGestureConfig, + NativeViewHandlerData, +} from './hooks/gestures/native/useNative'; +import { Gesture } from './types'; + +export type ComponentWrapperRef

= { + componentRef: React.ComponentType

; + gestureRef: Gesture; +}; + +export default function createNativeWrapper

( + Component: React.ComponentType

, + config: Readonly = {} +) { + const ComponentWrapper = ( + props: P & NativeWrapperProperties & { ref?: React.RefObject } + ) => { + // Filter out props that should be passed to gesture handler wrapper + const { gestureHandlerProps, childProps } = Object.keys(props).reduce( + (res, key) => { + // @ts-ignore TS being overly protective with it's types, see https://github.com/microsoft/TypeScript/issues/26255#issuecomment-458013731 for more info + if (NativeWrapperProps.has(key)) { + // @ts-ignore FIXME(TS) + res.gestureHandlerProps[key] = props[key]; + } else { + // @ts-ignore FIXME(TS) + res.childProps[key] = props[key]; + } + return res; + }, + { + gestureHandlerProps: { ...config }, // Watch out not to modify config + childProps: { + enabled: props.enabled, + hitSlop: props.hitSlop, + // testID: props.testID, + } as P, + } + ); + + const native = useNative(gestureHandlerProps); + + const componentRef = useRef>(null); + const gestureRef = useRef(native); + + useImperativeHandle(props.ref, () => ({ + componentRef: componentRef.current, + gestureRef: gestureRef.current, + })); + + return ( + + + + ); + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ComponentWrapper.displayName = + Component?.displayName || + // @ts-ignore if render doesn't exist it will return undefined and go further + Component?.render?.name || + (typeof Component === 'string' && Component) || + 'ComponentWrapper'; + + return ComponentWrapper; +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts b/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts index 85edd466d1..aafd30906a 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts @@ -11,7 +11,7 @@ import { useGesture } from '../../useGesture'; import { useClonedAndRemappedConfig } from '../../utils'; import { NativeGestureNativeProperties } from './NativeProperties'; -type NativeViewHandlerData = { +export type NativeViewHandlerData = { pointerInside: boolean; }; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts index 1091cbe8ce..fdb65c1f93 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts @@ -1,11 +1,13 @@ import { BaseGestureConfig, CommonGestureConfig, + ExternalRelations, GestureCallbacks, HandlersPropsWhiteList, InternalConfigProps, SingleGestureName, } from '../../types'; +import { NativeWrapperProperties } from '../../types/NativeWrapperType'; import { FlingNativeProperties } from '../gestures/fling/FlingProperties'; import { HoverNativeProperties } from '../gestures/hover/HoverProperties'; import { LongPressNativeProperties } from '../gestures/longPress/LongPressProperties'; @@ -13,10 +15,7 @@ import { NativeHandlerNativeProperties } from '../gestures/native/NativeProperti import { PanNativeProperties } from '../gestures/pan/PanProperties'; import { TapNativeProperties } from '../gestures/tap/TapProperties'; -export const allowedNativeProps = new Set< - keyof CommonGestureConfig | keyof InternalConfigProps ->([ - // CommonGestureConfig +const CommonConfig = new Set([ 'enabled', 'shouldCancelWhenOutside', 'hitSlop', @@ -25,6 +24,18 @@ export const allowedNativeProps = new Set< 'mouseButton', 'enableContextMenu', 'touchAction', +]); + +const ExternalRelationsConfig = new Set([ + 'simultaneousWith', + 'requireToFail', + 'block', +]); + +export const allowedNativeProps = new Set< + keyof CommonGestureConfig | keyof InternalConfigProps +>([ + ...CommonConfig, // InternalConfigProps 'dispatchesReanimatedEvents', @@ -48,16 +59,12 @@ export const HandlerCallbacks = new Set< export const PropsToFilter = new Set>([ ...HandlerCallbacks, + ...ExternalRelationsConfig, // Config props 'changeEventCalculator', 'disableReanimated', 'shouldUseReanimatedDetector', - - // Relations - 'simultaneousWithExternalGesture', - 'requireExternalGestureToFail', - 'blocksExternalGesture', ]); export const PropsWhiteLists = new Map< @@ -73,3 +80,11 @@ export const PropsWhiteLists = new Map< ]); export const EMPTY_WHITE_LIST = new Set(); + +export const NativeWrapperProps = new Set([ + ...CommonConfig, + ...HandlerCallbacks, + ...NativeHandlerNativeProperties, + ...ExternalRelationsConfig, + 'disableReanimated', +]); diff --git a/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts b/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts index 776ad7d694..5673c4c789 100644 --- a/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts @@ -10,9 +10,9 @@ import { FilterNeverProperties } from './UtilityTypes'; // Unfortunately, this type cannot be moved into ConfigTypes.ts because of circular dependency export type ExternalRelations = { - simultaneousWith?: Gesture | Gesture[]; - requireToFail?: Gesture | Gesture[]; - block?: Gesture | Gesture[]; + simultaneousWith?: AnyGesture | AnyGesture[]; + requireToFail?: AnyGesture | AnyGesture[]; + block?: AnyGesture | AnyGesture[]; }; // Similarly, this type cannot be moved into ConfigTypes.ts because it depends on `ExternalRelations` diff --git a/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts new file mode 100644 index 0000000000..0f38ab57c3 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts @@ -0,0 +1,8 @@ +import { CommonGestureConfig, ExternalRelations, GestureCallbacks } from '.'; +import { NativeGestureNativeProperties } from '../hooks/gestures/native/NativeProperties'; +import { NativeViewHandlerData } from '../hooks/gestures/native/useNative'; + +export type NativeWrapperProperties = CommonGestureConfig & + GestureCallbacks & + NativeGestureNativeProperties & + ExternalRelations;