diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index 692733a7fe0c..c0bb531db007 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -8,7 +8,7 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import * as StyleUtils from '@styles/StyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import PressableProps from './types'; +import PressableProps, {PressableRef} from './types'; function GenericPressable( { @@ -34,7 +34,7 @@ function GenericPressable( accessible = true, ...rest }: PressableProps, - ref: ForwardedRef, + ref: PressableRef, ) { const styles = useThemeStyles(); const {isExecuting, singleExecution} = useSingleExecution(); @@ -124,7 +124,7 @@ function GenericPressable( } onPress={!isDisabled ? singleExecution(onPressHandler) : undefined} onLongPress={!isDisabled && onLongPress ? onLongPressHandler : undefined} onKeyDown={!isDisabled ? onKeyDown : undefined} diff --git a/src/components/Pressable/GenericPressable/index.native.tsx b/src/components/Pressable/GenericPressable/index.native.tsx index 5bed0f488063..968ee6063a95 100644 --- a/src/components/Pressable/GenericPressable/index.native.tsx +++ b/src/components/Pressable/GenericPressable/index.native.tsx @@ -1,9 +1,8 @@ -import React, {ForwardedRef, forwardRef} from 'react'; -import {View} from 'react-native'; +import React, {forwardRef} from 'react'; import GenericPressable from './BaseGenericPressable'; -import PressableProps from './types'; +import PressableProps, {PressableRef} from './types'; -function NativeGenericPressable(props: PressableProps, ref: ForwardedRef) { +function NativeGenericPressable(props: PressableProps, ref: PressableRef) { return ( ) { +function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: PressableRef) { const accessible = props.accessible ?? props.accessible === undefined ? true : props.accessible; return ( diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index cfe35641bffa..bff5f651ac9f 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -1,5 +1,5 @@ -import {ElementRef, RefObject} from 'react'; -import {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, ViewStyle} from 'react-native'; +import {ElementRef, ForwardedRef, RefObject} from 'react'; +import {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, View, ViewStyle} from 'react-native'; import {ValueOf} from 'type-fest'; import {Shortcut} from '@libs/KeyboardShortcut'; import CONST from '@src/CONST'; @@ -138,4 +138,7 @@ type PressableProps = RNPressableProps & noDragArea?: boolean; }; +type PressableRef = ForwardedRef; + export default PressableProps; +export type {PressableRef}; diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index 914b9dc674c3..9a5f1ecbbb8f 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native-a11y/has-valid-accessibility-descriptors */ -import React, {ForwardedRef, forwardRef} from 'react'; -import {Text as RNText, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import React, {forwardRef} from 'react'; +import {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -12,7 +12,7 @@ import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; -import PressableProps from './GenericPressable/types'; +import PressableProps, {PressableRef} from './GenericPressable/types'; import PressableWithoutFeedback from './PressableWithoutFeedback'; type PressableWithDelayToggleProps = PressableProps & { @@ -65,7 +65,7 @@ function PressableWithDelayToggle( iconStyles, icon, }: PressableWithDelayToggleProps, - ref: ForwardedRef, + ref: PressableRef, ) { const styles = useThemeStyles(); const theme = useTheme(); diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx index 5d7f7c110ea7..a4c439c4441c 100644 --- a/src/components/Pressable/PressableWithFeedback.tsx +++ b/src/components/Pressable/PressableWithFeedback.tsx @@ -1,10 +1,10 @@ -import React, {ForwardedRef, forwardRef, useState} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import React, {forwardRef, useState} from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; import {AnimatedStyle} from 'react-native-reanimated'; import OpacityView from '@components/OpacityView'; import variables from '@styles/variables'; import GenericPressable from './GenericPressable'; -import PressableProps from './GenericPressable/types'; +import PressableProps, {PressableRef} from './GenericPressable/types'; type PressableWithFeedbackProps = PressableProps & { /** Style for the wrapper view */ @@ -37,7 +37,7 @@ function PressableWithFeedback( hoverDimmingValue = variables.hoverDimValue, ...rest }: PressableWithFeedbackProps, - ref: ForwardedRef, + ref: PressableRef, ) { const [isPressed, setIsPressed] = useState(false); const [isHovered, setIsHovered] = useState(false); @@ -88,3 +88,4 @@ function PressableWithFeedback( PressableWithFeedback.displayName = 'PressableWithFeedback'; export default forwardRef(PressableWithFeedback); +export type {PressableWithFeedbackProps}; diff --git a/src/components/Pressable/PressableWithoutFeedback.tsx b/src/components/Pressable/PressableWithoutFeedback.tsx index c3b780e63cfd..fd9d695cc2ed 100644 --- a/src/components/Pressable/PressableWithoutFeedback.tsx +++ b/src/components/Pressable/PressableWithoutFeedback.tsx @@ -1,11 +1,10 @@ -import React, {ForwardedRef} from 'react'; -import {View} from 'react-native'; +import React from 'react'; import GenericPressable from './GenericPressable'; -import PressableProps from './GenericPressable/types'; +import PressableProps, {PressableRef} from './GenericPressable/types'; function PressableWithoutFeedback( {pressStyle, hoverStyle, focusStyle, disabledStyle, screenReaderActiveStyle, shouldUseHapticsOnPress, shouldUseHapticsOnLongPress, ...rest}: PressableProps, - ref: ForwardedRef, + ref: PressableRef, ) { return ( { - e.preventDefault(); - props.onSecondaryInteraction(e); - }; - - return ( - - {props.children} - - ); -} - -PressableWithSecondaryInteraction.propTypes = pressableWithSecondaryInteractionPropTypes.propTypes; -PressableWithSecondaryInteraction.defaultProps = pressableWithSecondaryInteractionPropTypes.defaultProps; -PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction'; - -const PressableWithSecondaryInteractionWithRef = forwardRef((props, ref) => ( - -)); - -PressableWithSecondaryInteractionWithRef.displayName = 'PressableWithSecondaryInteractionWithRef'; - -export default PressableWithSecondaryInteractionWithRef; diff --git a/src/components/PressableWithSecondaryInteraction/index.native.tsx b/src/components/PressableWithSecondaryInteraction/index.native.tsx new file mode 100644 index 000000000000..f3cef029aa65 --- /dev/null +++ b/src/components/PressableWithSecondaryInteraction/index.native.tsx @@ -0,0 +1,61 @@ +import React, {forwardRef} from 'react'; +import {GestureResponderEvent, TextProps} from 'react-native'; +import {PressableRef} from '@components/Pressable/GenericPressable/types'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import PressableWithSecondaryInteractionProps from './types'; + +/** This is a special Pressable that calls onSecondaryInteraction when LongPressed. */ +function PressableWithSecondaryInteraction( + { + children, + onSecondaryInteraction, + inline = false, + needsOffscreenAlphaCompositing = false, + suppressHighlighting = false, + activeOpacity = 1, + preventDefaultContextMenu, + withoutFocusOnSecondaryInteraction, + enableLongPressWithHover, + ...rest + }: PressableWithSecondaryInteractionProps, + ref: PressableRef, +) { + const executeSecondaryInteraction = (event: GestureResponderEvent) => { + event.preventDefault(); + onSecondaryInteraction?.(event); + }; + + // Use Text node for inline mode to prevent content overflow. + if (inline) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction'; + +export default forwardRef(PressableWithSecondaryInteraction); diff --git a/src/components/PressableWithSecondaryInteraction/index.js b/src/components/PressableWithSecondaryInteraction/index.tsx similarity index 55% rename from src/components/PressableWithSecondaryInteraction/index.js rename to src/components/PressableWithSecondaryInteraction/index.tsx index c56774638d6a..88aad38ad5a9 100644 --- a/src/components/PressableWithSecondaryInteraction/index.js +++ b/src/components/PressableWithSecondaryInteraction/index.tsx @@ -1,44 +1,39 @@ import React, {forwardRef, useEffect, useRef} from 'react'; -import _ from 'underscore'; +import {GestureResponderEvent} from 'react-native'; +import {PressableRef} from '@components/Pressable/GenericPressable/types'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as StyleUtils from '@styles/StyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; -import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes'; - -/** - * This is a special Pressable that calls onSecondaryInteraction when LongPressed, or right-clicked. - */ - -function PressableWithSecondaryInteraction({ - children, - inline, - style, - enableLongPressWithHover, - withoutFocusOnSecondaryInteraction, - preventDefaultContextMenu, - onSecondaryInteraction, - onPressIn, - onPress, - onPressOut, - activeOpacity, - forwardedRef, - ...rest -}) { +import PressableWithSecondaryInteractionProps from './types'; + +/** This is a special Pressable that calls onSecondaryInteraction when LongPressed, or right-clicked. */ +function PressableWithSecondaryInteraction( + { + children, + inline = false, + style, + enableLongPressWithHover = false, + withoutFocusOnSecondaryInteraction = false, + needsOffscreenAlphaCompositing = false, + preventDefaultContextMenu = true, + onSecondaryInteraction, + activeOpacity = 1, + ...rest + }: PressableWithSecondaryInteractionProps, + ref: PressableRef, +) { const styles = useThemeStyles(); - const pressableRef = useRef(null); + const pressableRef = useRef(null); - /** - * @param {Event} e - the secondary interaction event - */ - const executeSecondaryInteraction = (e) => { + const executeSecondaryInteraction = (event: GestureResponderEvent) => { if (DeviceCapabilities.hasHoverSupport() && !enableLongPressWithHover) { return; } if (withoutFocusOnSecondaryInteraction && pressableRef.current) { pressableRef.current.blur(); } - onSecondaryInteraction(e); + onSecondaryInteraction?.(event); }; useEffect(() => { @@ -46,32 +41,32 @@ function PressableWithSecondaryInteraction({ return; } - if (forwardedRef) { - if (_.isFunction(forwardedRef)) { - forwardedRef(pressableRef.current); - } else if (_.isObject(forwardedRef)) { + if (ref) { + if (typeof ref === 'function') { + ref(pressableRef.current); + } else if (typeof ref === 'object') { // eslint-disable-next-line no-param-reassign - forwardedRef.current = pressableRef.current; + ref.current = pressableRef.current; } } const element = pressableRef.current; /** - * @param {contextmenu} e - A right-click MouseEvent. + * @param event - A right-click MouseEvent. * https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event */ - const executeSecondaryInteractionOnContextMenu = (e) => { + const executeSecondaryInteractionOnContextMenu = (event: MouseEvent) => { if (!onSecondaryInteraction) { return; } - e.stopPropagation(); + event.stopPropagation(); if (preventDefaultContextMenu) { - e.preventDefault(); + event.preventDefault(); } - onSecondaryInteraction(e); + onSecondaryInteraction(event); /** * This component prevents the tapped element from capturing focus. @@ -90,42 +85,28 @@ function PressableWithSecondaryInteraction({ return () => { element.removeEventListener('contextmenu', executeSecondaryInteractionOnContextMenu); }; - }, [forwardedRef, onSecondaryInteraction, preventDefaultContextMenu, withoutFocusOnSecondaryInteraction]); + }, [ref, onSecondaryInteraction, preventDefaultContextMenu, withoutFocusOnSecondaryInteraction]); - const defaultPressableProps = _.omit(rest, ['onLongPress']); const inlineStyle = inline ? styles.dInline : {}; // On Web, Text does not support LongPress events thus manage inline mode with styling instead of using Text. return ( [StyleUtils.parseStyleFromFunction(style, state), inlineStyle]} + needsOffscreenAlphaCompositing={needsOffscreenAlphaCompositing} > {children} ); } -PressableWithSecondaryInteraction.propTypes = pressableWithSecondaryInteractionPropTypes.propTypes; -PressableWithSecondaryInteraction.defaultProps = pressableWithSecondaryInteractionPropTypes.defaultProps; PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction'; -const PressableWithSecondaryInteractionWithRef = forwardRef((props, ref) => ( - -)); - -PressableWithSecondaryInteractionWithRef.displayName = 'PressableWithSecondaryInteractionWithRef'; - -export default PressableWithSecondaryInteractionWithRef; +export default forwardRef(PressableWithSecondaryInteraction); diff --git a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js b/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js deleted file mode 100644 index 935b8ece5933..000000000000 --- a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js +++ /dev/null @@ -1,72 +0,0 @@ -import PropTypes from 'prop-types'; -import refPropTypes from '@components/refPropTypes'; -import stylePropTypes from '@styles/stylePropTypes'; - -const propTypes = { - /** The function that should be called when this pressable is pressed */ - onPress: PropTypes.func, - - /** The function that should be called when this pressable is pressedIn */ - onPressIn: PropTypes.func, - - /** The function that should be called when this pressable is pressedOut */ - onPressOut: PropTypes.func, - - /** - * The function that should be called when this pressable is LongPressed or right-clicked. - * - * This function should be stable, preferably wrapped in a `useCallback` so that it does not - * cause several re-renders. - */ - onSecondaryInteraction: PropTypes.func, - - /** The children which should be contained in this wrapper component. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /** The ref to the search input (may be null on small screen widths) */ - forwardedRef: refPropTypes, - - /** Prevent the default ContextMenu on web/Desktop */ - preventDefaultContextMenu: PropTypes.bool, - - /** Use Text instead of Pressable to create inline layout. - * It has few limitations in comparison to Pressable. - * - * - No support for delayLongPress. - * - No support for pressIn and pressOut events. - * - No support for opacity - * - * Note: Web uses styling instead of Text due to no support of LongPress. Thus above pointers are not valid for web. - */ - inline: PropTypes.bool, - - /** Disable focus trap for the element on secondary interaction */ - withoutFocusOnSecondaryInteraction: PropTypes.bool, - - /** Opacity to reduce to when active */ - activeOpacity: PropTypes.number, - - /** Used to apply styles to the Pressable */ - style: stylePropTypes, - - /** Whether the view needs to be rendered offscreen (for Android only) */ - needsOffscreenAlphaCompositing: PropTypes.bool, - - /** Whether the text has a gray highlights on press down (for IOS only) */ - suppressHighlighting: PropTypes.bool, -}; - -const defaultProps = { - forwardedRef: () => {}, - onPressIn: () => {}, - onPressOut: () => {}, - preventDefaultContextMenu: true, - inline: false, - withoutFocusOnSecondaryInteraction: false, - activeOpacity: 1, - enableLongPressWithHover: false, - needsOffscreenAlphaCompositing: false, - suppressHighlighting: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/PressableWithSecondaryInteraction/types.ts b/src/components/PressableWithSecondaryInteraction/types.ts new file mode 100644 index 000000000000..cf286afcb63a --- /dev/null +++ b/src/components/PressableWithSecondaryInteraction/types.ts @@ -0,0 +1,54 @@ +import {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; + +type PressableWithSecondaryInteractionProps = PressableWithFeedbackProps & + ChildrenProps & { + /** The function that should be called when this pressable is pressed */ + onPress: (event?: GestureResponderEvent) => void; + + /** The function that should be called when this pressable is pressedIn */ + onPressIn?: (event?: GestureResponderEvent) => void; + + /** The function that should be called when this pressable is pressedOut */ + onPressOut?: (event?: GestureResponderEvent) => void; + + /** + * The function that should be called when this pressable is LongPressed or right-clicked. + * + * This function should be stable, preferably wrapped in a `useCallback` so that it does not + * cause several re-renders. + */ + onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void; + + /** Prevent the default ContextMenu on web/Desktop */ + preventDefaultContextMenu?: boolean; + + /** Use Text instead of Pressable to create inline layout. + * It has few limitations in comparison to Pressable. + * + * - No support for delayLongPress. + * - No support for pressIn and pressOut events. + * - No support for opacity + * + * Note: Web uses styling instead of Text due to no support of LongPress. Thus above pointers are not valid for web. + */ + inline?: boolean; + + /** Disable focus trap for the element on secondary interaction */ + withoutFocusOnSecondaryInteraction?: boolean; + + /** Opacity to reduce to when active */ + activeOpacity?: number; + + /** Used to apply styles to the Pressable */ + style?: StyleProp; + + /** Whether the long press with hover behavior is enabled */ + enableLongPressWithHover?: boolean; + + /** Whether the text has a gray highlights on press down (for IOS only) */ + suppressHighlighting?: boolean; + }; + +export default PressableWithSecondaryInteractionProps; diff --git a/src/components/Reactions/EmojiReactionBubble.js b/src/components/Reactions/EmojiReactionBubble.js index 2487f9d0c73c..0b690cd5aeef 100644 --- a/src/components/Reactions/EmojiReactionBubble.js +++ b/src/components/Reactions/EmojiReactionBubble.js @@ -72,7 +72,6 @@ function EmojiReactionBubble(props) { props.onPress(); }} - onLongPress={props.onReactionListOpen} onSecondaryInteraction={props.onReactionListOpen} ref={props.forwardedRef} enableLongPressWithHover={props.isSmallScreenWidth}