diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.js b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx similarity index 65% rename from src/components/Pressable/GenericPressable/BaseGenericPressable.js rename to src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index a3ce55003cdd..1576fe18da54 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.js +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -1,7 +1,6 @@ -import React, {forwardRef, useCallback, useEffect, useMemo} from 'react'; +import React, {ForwardedRef, forwardRef, useCallback, useEffect, useMemo} from 'react'; // eslint-disable-next-line no-restricted-imports -import {Pressable} from 'react-native'; -import _ from 'underscore'; +import {GestureResponderEvent, Pressable, View, ViewStyle} from 'react-native'; import useSingleExecution from '@hooks/useSingleExecution'; import Accessibility from '@libs/Accessibility'; import HapticFeedback from '@libs/HapticFeedback'; @@ -9,15 +8,12 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; -import genericPressablePropTypes from './PropTypes'; +import PressableProps from './types'; /** * Returns the cursor style based on the state of Pressable - * @param {Boolean} isDisabled - * @param {Boolean} isText - * @returns {Object} */ -const getCursorStyle = (isDisabled, isText) => { +function getCursorStyle(isDisabled: boolean, isText: boolean): Pick { if (isDisabled) { return styles.cursorDisabled; } @@ -27,28 +23,34 @@ const getCursorStyle = (isDisabled, isText) => { } return styles.cursorPointer; -}; +} -const GenericPressable = forwardRef((props, ref) => { - const { +function GenericPressable( + { children, - onPress, + onPress = () => {}, onLongPress, - onKeyPress, onKeyDown, disabled, style, - shouldUseHapticsOnLongPress, - shouldUseHapticsOnPress, + disabledStyle = {}, + hoverStyle = {}, + focusStyle = {}, + pressStyle = {}, + screenReaderActiveStyle = {}, + shouldUseHapticsOnLongPress = false, + shouldUseHapticsOnPress = false, nextFocusRef, keyboardShortcut, - shouldUseAutoHitSlop, - enableInScreenReaderStates, + shouldUseAutoHitSlop = false, + enableInScreenReaderStates = CONST.SCREEN_READER_STATES.ALL, onPressIn, onPressOut, + accessible = true, ...rest - } = props; - + }: PressableProps, + ref: ForwardedRef, +) { const {isExecuting, singleExecution} = useSingleExecution(); const isScreenReaderActive = Accessibility.useScreenReaderStatus(); const [hitSlop, onLayout] = Accessibility.useAutoHitSlop(); @@ -63,13 +65,14 @@ const GenericPressable = forwardRef((props, ref) => { shouldBeDisabledByScreenReader = isScreenReaderActive; } - return props.disabled || shouldBeDisabledByScreenReader || isExecuting; - }, [isScreenReaderActive, enableInScreenReaderStates, props.disabled, isExecuting]); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return disabled || shouldBeDisabledByScreenReader || isExecuting; + }, [isScreenReaderActive, enableInScreenReaderStates, disabled, isExecuting]); const shouldUseDisabledCursor = useMemo(() => isDisabled && !isExecuting, [isDisabled, isExecuting]); const onLongPressHandler = useCallback( - (event) => { + (event: GestureResponderEvent) => { if (isDisabled) { return; } @@ -79,8 +82,8 @@ const GenericPressable = forwardRef((props, ref) => { if (shouldUseHapticsOnLongPress) { HapticFeedback.longPress(); } - if (ref && ref.current) { - ref.current.blur(); + if (ref && 'current' in ref) { + ref.current?.blur(); } onLongPress(event); @@ -90,7 +93,7 @@ const GenericPressable = forwardRef((props, ref) => { ); const onPressHandler = useCallback( - (event) => { + (event?: GestureResponderEvent | KeyboardEvent) => { if (isDisabled) { return; } @@ -100,8 +103,8 @@ const GenericPressable = forwardRef((props, ref) => { if (shouldUseHapticsOnPress) { HapticFeedback.press(); } - if (ref && ref.current) { - ref.current.blur(); + if (ref && 'current' in ref) { + ref.current?.blur(); } onPress(event); @@ -110,16 +113,6 @@ const GenericPressable = forwardRef((props, ref) => { [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled], ); - const onKeyPressHandler = useCallback( - (event) => { - if (event.key !== 'Enter') { - return; - } - onPressHandler(event); - }, - [onPressHandler], - ); - useEffect(() => { if (!keyboardShortcut) { return () => {}; @@ -135,39 +128,37 @@ const GenericPressable = forwardRef((props, ref) => { ref={ref} onPress={!isDisabled ? singleExecution(onPressHandler) : undefined} onLongPress={!isDisabled && onLongPress ? onLongPressHandler : undefined} - onKeyPress={!isDisabled ? onKeyPressHandler : undefined} onKeyDown={!isDisabled ? onKeyDown : undefined} onPressIn={!isDisabled ? onPressIn : undefined} onPressOut={!isDisabled ? onPressOut : undefined} style={(state) => [ - getCursorStyle(shouldUseDisabledCursor, [props.accessibilityRole, props.role].includes('text')), - StyleUtils.parseStyleFromFunction(props.style, state), - isScreenReaderActive && StyleUtils.parseStyleFromFunction(props.screenReaderActiveStyle, state), - state.focused && StyleUtils.parseStyleFromFunction(props.focusStyle, state), - state.hovered && StyleUtils.parseStyleFromFunction(props.hoverStyle, state), - state.pressed && StyleUtils.parseStyleFromFunction(props.pressStyle, state), - isDisabled && [...StyleUtils.parseStyleFromFunction(props.disabledStyle, state), styles.noSelect], + getCursorStyle(shouldUseDisabledCursor, [rest.accessibilityRole, rest.role].includes('text')), + StyleUtils.parseStyleFromFunction(style, state), + isScreenReaderActive && StyleUtils.parseStyleFromFunction(screenReaderActiveStyle, state), + state.focused && StyleUtils.parseStyleFromFunction(focusStyle, state), + state.hovered && StyleUtils.parseStyleFromFunction(hoverStyle, state), + state.pressed && StyleUtils.parseStyleFromFunction(pressStyle, state), + isDisabled && [StyleUtils.parseStyleFromFunction(disabledStyle, state), styles.noSelect], ]} // accessibility props accessibilityState={{ disabled: isDisabled, - ...props.accessibilityState, + ...rest.accessibilityState, }} aria-disabled={isDisabled} - aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers}+${keyboardShortcut.shortcutKey}`} + aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers.join('')}+${keyboardShortcut.shortcutKey}`} // ios-only form of inputs - onMagicTap={!isDisabled && onPressHandler} - onAccessibilityTap={!isDisabled && onPressHandler} + onMagicTap={!isDisabled ? onPressHandler : undefined} + onAccessibilityTap={!isDisabled ? onPressHandler : undefined} + accessible={accessible} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} > - {(state) => (_.isFunction(props.children) ? props.children({...state, isScreenReaderActive, isDisabled}) : props.children)} + {(state) => (typeof children === 'function' ? children({...state, isScreenReaderActive, isDisabled}) : children)} ); -}); +} GenericPressable.displayName = 'GenericPressable'; -GenericPressable.propTypes = genericPressablePropTypes.pressablePropTypes; -GenericPressable.defaultProps = genericPressablePropTypes.defaultProps; -export default GenericPressable; +export default forwardRef(GenericPressable); diff --git a/src/components/Pressable/GenericPressable/PropTypes.js b/src/components/Pressable/GenericPressable/PropTypes.js deleted file mode 100644 index 870c63301239..000000000000 --- a/src/components/Pressable/GenericPressable/PropTypes.js +++ /dev/null @@ -1,142 +0,0 @@ -import PropTypes from 'prop-types'; -import stylePropType from '@styles/stylePropTypes'; -import CONST from '@src/CONST'; - -const stylePropTypeWithFunction = PropTypes.oneOfType([stylePropType, PropTypes.func]); - -/** - * Custom test for required props - * + accessibilityLabel is required when accessible is true - * @param {Object} props - * @returns {Error} Error if prop is required - */ -function requiredPropsCheck(props) { - if (props.accessible !== true || (props.accessibilityLabel !== undefined && typeof props.accessibilityLabel === 'string')) { - return; - } - return new Error(`Provide a valid string for accessibilityLabel prop when accessible is true`); -} - -const pressablePropTypes = { - /** - * onPress callback - */ - onPress: PropTypes.func, - - /** - * Specifies keyboard shortcut to trigger onPressHandler - * @example {shortcutKey: 'a', modifiers: ['ctrl', 'shift'], descriptionKey: 'keyboardShortcut.description'} - */ - keyboardShortcut: PropTypes.shape({ - descriptionKey: PropTypes.string.isRequired, - shortcutKey: PropTypes.string.isRequired, - modifiers: PropTypes.arrayOf(PropTypes.string), - }), - - /** - * Specifies if haptic feedback should be used on press - * @default false - */ - shouldUseHapticsOnPress: PropTypes.bool, - - /** - * Specifies if haptic feedback should be used on long press - * @default false - */ - shouldUseHapticsOnLongPress: PropTypes.bool, - - /** - * style for when the component is disabled. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.isDisabled ? 'red' : 'blue'}) - */ - disabledStyle: stylePropTypeWithFunction, - - /** - * style for when the component is hovered. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.hovered ? 'red' : 'blue'}) - */ - hoverStyle: stylePropTypeWithFunction, - - /** - * style for when the component is focused. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.focused ? 'red' : 'blue'}) - */ - focusStyle: stylePropTypeWithFunction, - - /** - * style for when the component is pressed. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.pressed ? 'red' : 'blue'}) - */ - pressStyle: stylePropTypeWithFunction, - - /** - * style for when the component is active and the screen reader is on. - * Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.isScreenReaderActive ? 'red' : 'blue'}) - */ - screenReaderActiveStyle: stylePropTypeWithFunction, - - /** - * Specifies if the component should be accessible when the screen reader is on - * @default 'all' - * @example 'all' - the component is accessible regardless of screen reader state - * @example 'active' - the component is accessible only when the screen reader is on - * @example 'disabled' - the component is not accessible when the screen reader is on - */ - enableInScreenReaderStates: PropTypes.oneOf([CONST.SCREEN_READER_STATES.ALL, CONST.SCREEN_READER_STATES.ACTIVE, CONST.SCREEN_READER_STATES.DISABLED]), - - /** - * Specifies which component should be focused after interacting with this component - */ - nextFocusRef: PropTypes.func, - - /** - * Specifies the accessibility label for the component - * @example 'Search' - * @example 'Close' - */ - accessibilityLabel: requiredPropsCheck, - - /** - * Specifies the accessibility hint for the component - * @example 'Double tap to open' - */ - accessibilityHint: PropTypes.string, - - /** - * Specifies if the component should calculate its hitSlop automatically - * @default true - */ - shouldUseAutoHitSlop: PropTypes.bool, -}; - -const defaultProps = { - onPress: () => {}, - keyboardShortcut: undefined, - shouldUseHapticsOnPress: false, - shouldUseHapticsOnLongPress: false, - disabledStyle: {}, - hoverStyle: {}, - focusStyle: {}, - pressStyle: {}, - screenReaderActiveStyle: {}, - enableInScreenReaderStates: CONST.SCREEN_READER_STATES.ALL, - nextFocusRef: undefined, - shouldUseAutoHitSlop: false, - accessible: true, -}; - -export default { - pressablePropTypes, - defaultProps, -}; diff --git a/src/components/Pressable/GenericPressable/index.js b/src/components/Pressable/GenericPressable/index.js deleted file mode 100644 index 8247d0c35670..000000000000 --- a/src/components/Pressable/GenericPressable/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import React, {forwardRef} from 'react'; -import GenericPressable from './BaseGenericPressable'; -import GenericPressablePropTypes from './PropTypes'; - -const WebGenericPressable = forwardRef((props, ref) => ( - -)); - -WebGenericPressable.propTypes = GenericPressablePropTypes.pressablePropTypes; -WebGenericPressable.defaultProps = GenericPressablePropTypes.defaultProps; -WebGenericPressable.displayName = 'WebGenericPressable'; - -export default WebGenericPressable; diff --git a/src/components/Pressable/GenericPressable/index.native.js b/src/components/Pressable/GenericPressable/index.native.js deleted file mode 100644 index 14a2c2bcbf82..000000000000 --- a/src/components/Pressable/GenericPressable/index.native.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, {forwardRef} from 'react'; -import GenericPressable from './BaseGenericPressable'; -import GenericPressablePropTypes from './PropTypes'; - -const NativeGenericPressable = forwardRef((props, ref) => ( - -)); - -NativeGenericPressable.propTypes = GenericPressablePropTypes.pressablePropTypes; -NativeGenericPressable.defaultProps = GenericPressablePropTypes.defaultProps; -NativeGenericPressable.displayName = 'WebGenericPressable'; - -export default NativeGenericPressable; diff --git a/src/components/Pressable/GenericPressable/index.native.tsx b/src/components/Pressable/GenericPressable/index.native.tsx new file mode 100644 index 000000000000..5bed0f488063 --- /dev/null +++ b/src/components/Pressable/GenericPressable/index.native.tsx @@ -0,0 +1,21 @@ +import React, {ForwardedRef, forwardRef} from 'react'; +import {View} from 'react-native'; +import GenericPressable from './BaseGenericPressable'; +import PressableProps from './types'; + +function NativeGenericPressable(props: PressableProps, ref: ForwardedRef) { + return ( + + ); +} + +NativeGenericPressable.displayName = 'NativeGenericPressable'; + +export default forwardRef(NativeGenericPressable); diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx new file mode 100644 index 000000000000..c8e9560062e0 --- /dev/null +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -0,0 +1,30 @@ +import React, {ForwardedRef, forwardRef} from 'react'; +import {Role, View} from 'react-native'; +import GenericPressable from './BaseGenericPressable'; +import PressableProps from './types'; + +function WebGenericPressable(props: PressableProps, ref: ForwardedRef) { + return ( + + ); +} + +WebGenericPressable.displayName = 'WebGenericPressable'; + +export default forwardRef(WebGenericPressable); diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts new file mode 100644 index 000000000000..35616cb600a3 --- /dev/null +++ b/src/components/Pressable/GenericPressable/types.ts @@ -0,0 +1,147 @@ +import {ElementRef, RefObject} from 'react'; +import {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, ViewStyle} from 'react-native'; +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +type StylePropWithFunction = StyleProp | ((state: PressableStateCallbackType) => StyleProp); + +type Shortcut = { + displayName: string; + shortcutKey: string; + descriptionKey: string; + modifiers: string[]; +}; + +type RequiredAccessibilityLabel = + | { + /** + * When true, indicates that the view is an accessibility element. + * By default, all the touchable elements are accessible. + */ + accessible?: true | undefined; + + /** + * Specifies the accessibility label for the component + * @example 'Search' + * @example 'Close' + */ + accessibilityLabel: string; + } + | { + /** + * When false, indicates that the view is not an accessibility element. + */ + accessible: false; + + /** + * Specifies the accessibility label for the component + * @example 'Search' + * @example 'Close' + */ + accessibilityLabel?: string; + }; + +type PressableProps = RNPressableProps & + RequiredAccessibilityLabel & { + /** + * onPress callback + */ + onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; + + /** + * Specifies keyboard shortcut to trigger onPressHandler + * @example {shortcutKey: 'a', modifiers: ['ctrl', 'shift'], descriptionKey: 'keyboardShortcut.description'} + */ + keyboardShortcut?: Shortcut; + + /** + * Specifies if haptic feedback should be used on press + * @default false + */ + shouldUseHapticsOnPress?: boolean; + + /** + * Specifies if haptic feedback should be used on long press + * @default false + */ + shouldUseHapticsOnLongPress?: boolean; + + /** + * style for when the component is disabled. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.isDisabled ? 'red' : 'blue'}) + */ + disabledStyle?: StylePropWithFunction; + + /** + * style for when the component is hovered. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.hovered ? 'red' : 'blue'}) + */ + hoverStyle?: StylePropWithFunction; + + /** + * style for when the component is focused. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.focused ? 'red' : 'blue'}) + */ + focusStyle?: StylePropWithFunction; + + /** + * style for when the component is pressed. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.pressed ? 'red' : 'blue'}) + */ + pressStyle?: StylePropWithFunction; + + /** + * style for when the component is active and the screen reader is on. + * Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.isScreenReaderActive ? 'red' : 'blue'}) + */ + screenReaderActiveStyle?: StylePropWithFunction; + + /** + * Specifies if the component should be accessible when the screen reader is on + * @default 'all' + * @example 'all' - the component is accessible regardless of screen reader state + * @example 'active' - the component is accessible only when the screen reader is on + * @example 'disabled' - the component is not accessible when the screen reader is on + */ + enableInScreenReaderStates?: ValueOf; + + /** + * Specifies which component should be focused after interacting with this component + */ + nextFocusRef?: ElementRef> & RefObject; + + /** + * Specifies the accessibility label for the component + * @example 'Search' + * @example 'Close' + */ + accessibilityLabel?: string; + + /** + * Specifies the accessibility hint for the component + * @example 'Double tap to open' + */ + accessibilityHint?: string; + + /** + * Specifies if the component should calculate its hitSlop automatically + * @default true + */ + shouldUseAutoHitSlop?: boolean; + + /** Turns off drag area for the component */ + noDragArea?: boolean; + }; + +export default PressableProps; diff --git a/src/components/Pressable/PressableWithDelayToggle.js b/src/components/Pressable/PressableWithDelayToggle.tsx similarity index 51% rename from src/components/Pressable/PressableWithDelayToggle.js rename to src/components/Pressable/PressableWithDelayToggle.tsx index c9f05e5adfee..316adc25076d 100644 --- a/src/components/Pressable/PressableWithDelayToggle.js +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -1,8 +1,9 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +/* 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 {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import refPropTypes from '@components/refPropTypes'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; @@ -10,68 +11,61 @@ import getButtonState from '@libs/getButtonState'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import variables from '@styles/variables'; +import PressableProps from './GenericPressable/types'; import PressableWithoutFeedback from './PressableWithoutFeedback'; -const propTypes = { - /** Ref passed to the component by React.forwardRef (do not pass from parent) */ - innerRef: refPropTypes, - +type PressableWithDelayToggleProps = PressableProps & { /** The text to display */ - text: PropTypes.string, + text: string; /** The text to display once the pressable is pressed */ - textChecked: PropTypes.string, + textChecked: string; /** The tooltip text to display */ - tooltipText: PropTypes.string, + tooltipText: string; /** The tooltip text to display once the pressable is pressed */ - tooltipTextChecked: PropTypes.string, + tooltipTextChecked: string; /** Styles to apply to the container */ - // eslint-disable-next-line react/forbid-prop-types - styles: PropTypes.arrayOf(PropTypes.object), + styles?: StyleProp; - /** Styles to apply to the text */ - // eslint-disable-next-line react/forbid-prop-types - textStyles: PropTypes.arrayOf(PropTypes.object), + // /** Styles to apply to the text */ + textStyles?: StyleProp; /** Styles to apply to the icon */ - // eslint-disable-next-line react/forbid-prop-types - iconStyles: PropTypes.arrayOf(PropTypes.object), - - /** Callback to be called on onPress */ - onPress: PropTypes.func.isRequired, + iconStyles?: StyleProp; /** The icon to display */ - icon: PropTypes.func, + icon?: React.FC; /** The icon to display once the pressable is pressed */ - iconChecked: PropTypes.func, + iconChecked?: React.FC; /** * Should be set to `true` if this component is being rendered inline in * another `Text`. This is due to limitations in RN regarding the * vertical text alignment of non-Text elements */ - inline: PropTypes.bool, -}; - -const defaultProps = { - text: '', - textChecked: '', - tooltipText: '', - tooltipTextChecked: '', - styles: [], - textStyles: [], - iconStyles: [], - icon: null, - inline: true, - iconChecked: Expensicons.Checkmark, - innerRef: () => {}, + inline?: boolean; }; -function PressableWithDelayToggle(props) { +function PressableWithDelayToggle( + { + iconChecked = Expensicons.Checkmark, + inline = true, + onPress, + text, + textChecked, + tooltipText, + tooltipTextChecked, + styles: pressableStyle, + textStyles, + iconStyles, + icon, + }: PressableWithDelayToggleProps, + ref: ForwardedRef, +) { const [isActive, temporarilyDisableInteractions] = useThrottledButtonState(); const updatePressState = () => { @@ -79,54 +73,57 @@ function PressableWithDelayToggle(props) { return; } temporarilyDisableInteractions(); - props.onPress(); + onPress(); }; // Due to limitations in RN regarding the vertical text alignment of non-Text elements, // for elements that are supposed to be inline, we need to use a Text element instead // of a Pressable - const PressableView = props.inline ? Text : PressableWithoutFeedback; - const tooltipText = !isActive ? props.tooltipTextChecked : props.tooltipText; + const PressableView = inline ? Text : PressableWithoutFeedback; + const tooltipTexts = !isActive ? tooltipTextChecked : tooltipText; const labelText = ( - {!isActive && props.textChecked ? props.textChecked : props.text} + {!isActive && textChecked ? textChecked : text}   ); return ( <> - {props.inline && labelText} + {inline && labelText} {({hovered, pressed}) => ( <> - {!props.inline && labelText} - {props.icon && ( + {!inline && labelText} + {icon && ( )} @@ -138,18 +135,6 @@ function PressableWithDelayToggle(props) { ); } -PressableWithDelayToggle.propTypes = propTypes; -PressableWithDelayToggle.defaultProps = defaultProps; PressableWithDelayToggle.displayName = 'PressableWithDelayToggle'; -const PressableWithDelayToggleWithRef = React.forwardRef((props, ref) => ( - -)); - -PressableWithDelayToggleWithRef.displayName = 'PressableWithDelayToggleWithRef'; - -export default PressableWithDelayToggleWithRef; +export default forwardRef(PressableWithDelayToggle); diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.js deleted file mode 100644 index ad29204bb018..000000000000 --- a/src/components/Pressable/PressableWithFeedback.js +++ /dev/null @@ -1,95 +0,0 @@ -import propTypes from 'prop-types'; -import React, {forwardRef, useState} from 'react'; -import _ from 'underscore'; -import OpacityView from '@components/OpacityView'; -import variables from '@styles/variables'; -import GenericPressable from './GenericPressable'; -import GenericPressablePropTypes from './GenericPressable/PropTypes'; - -const omittedProps = ['wrapperStyle', 'needsOffscreenAlphaCompositing']; - -const PressableWithFeedbackPropTypes = { - ...GenericPressablePropTypes.pressablePropTypes, - /** - * Determines what opacity value should be applied to the underlaying view when Pressable is pressed. - * To disable dimming, pass 1 as pressDimmingValue - * @default variables.pressDimValue - */ - pressDimmingValue: propTypes.number, - /** - * Determines what opacity value should be applied to the underlaying view when pressable is hovered. - * To disable dimming, pass 1 as hoverDimmingValue - * @default variables.hoverDimValue - */ - hoverDimmingValue: propTypes.number, - /** - * Used to locate this view from native classes. - */ - nativeID: propTypes.string, - - /** Whether the view needs to be rendered offscreen (for Android only) */ - needsOffscreenAlphaCompositing: propTypes.bool, -}; - -const PressableWithFeedbackDefaultProps = { - ...GenericPressablePropTypes.defaultProps, - pressDimmingValue: variables.pressDimValue, - hoverDimmingValue: variables.hoverDimValue, - nativeID: '', - wrapperStyle: [], - needsOffscreenAlphaCompositing: false, -}; - -const PressableWithFeedback = forwardRef((props, ref) => { - const propsWithoutWrapperProps = _.omit(props, omittedProps); - const [isPressed, setIsPressed] = useState(false); - const [isHovered, setIsHovered] = useState(false); - - return ( - - { - setIsHovered(true); - if (props.onHoverIn) { - props.onHoverIn(); - } - }} - onHoverOut={() => { - setIsHovered(false); - if (props.onHoverOut) { - props.onHoverOut(); - } - }} - onPressIn={() => { - setIsPressed(true); - if (props.onPressIn) { - props.onPressIn(); - } - }} - onPressOut={() => { - setIsPressed(false); - if (props.onPressOut) { - props.onPressOut(); - } - }} - > - {(state) => (_.isFunction(props.children) ? props.children(state) : props.children)} - - - ); -}); - -PressableWithFeedback.displayName = 'PressableWithFeedback'; -PressableWithFeedback.propTypes = PressableWithFeedbackPropTypes; -PressableWithFeedback.defaultProps = PressableWithFeedbackDefaultProps; - -export default PressableWithFeedback; diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx new file mode 100644 index 000000000000..5d7f7c110ea7 --- /dev/null +++ b/src/components/Pressable/PressableWithFeedback.tsx @@ -0,0 +1,90 @@ +import React, {ForwardedRef, forwardRef, useState} from 'react'; +import {StyleProp, View, 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'; + +type PressableWithFeedbackProps = PressableProps & { + /** Style for the wrapper view */ + wrapperStyle?: StyleProp>; + + /** + * Determines what opacity value should be applied to the underlaying view when Pressable is pressed. + * To disable dimming, pass 1 as pressDimmingValue + * @default variables.pressDimValue + */ + pressDimmingValue?: number; + + /** + * Determines what opacity value should be applied to the underlaying view when pressable is hovered. + * To disable dimming, pass 1 as hoverDimmingValue + * @default variables.hoverDimValue + */ + hoverDimmingValue?: number; + + /** Whether the view needs to be rendered offscreen (for Android only) */ + needsOffscreenAlphaCompositing?: boolean; +}; + +function PressableWithFeedback( + { + children, + wrapperStyle = [], + needsOffscreenAlphaCompositing = false, + pressDimmingValue = variables.pressDimValue, + hoverDimmingValue = variables.hoverDimValue, + ...rest + }: PressableWithFeedbackProps, + ref: ForwardedRef, +) { + const [isPressed, setIsPressed] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + return ( + + { + setIsHovered(true); + if (rest.onHoverIn) { + rest.onHoverIn(event); + } + }} + onHoverOut={(event) => { + setIsHovered(false); + if (rest.onHoverOut) { + rest.onHoverOut(event); + } + }} + onPressIn={(event) => { + setIsPressed(true); + if (rest.onPressIn) { + rest.onPressIn(event); + } + }} + onPressOut={(event) => { + setIsPressed(false); + if (rest.onPressOut) { + rest.onPressOut(event); + } + }} + > + {(state) => (typeof children === 'function' ? children(state) : children)} + + + ); +} + +PressableWithFeedback.displayName = 'PressableWithFeedback'; + +export default forwardRef(PressableWithFeedback); diff --git a/src/components/Pressable/PressableWithoutFeedback.js b/src/components/Pressable/PressableWithoutFeedback.js deleted file mode 100644 index 92e704550dec..000000000000 --- a/src/components/Pressable/PressableWithoutFeedback.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import _ from 'underscore'; -import GenericPressable from './GenericPressable'; -import GenericPressableProps from './GenericPressable/PropTypes'; - -const omittedProps = ['pressStyle', 'hoverStyle', 'focusStyle', 'activeStyle', 'disabledStyle', 'screenReaderActiveStyle', 'shouldUseHapticsOnPress', 'shouldUseHapticsOnLongPress']; - -const PressableWithoutFeedback = React.forwardRef((props, ref) => { - const propsWithoutStyling = _.omit(props, omittedProps); - return ( - - ); -}); - -PressableWithoutFeedback.displayName = 'PressableWithoutFeedback'; -PressableWithoutFeedback.propTypes = _.omit(GenericPressableProps.pressablePropTypes, omittedProps); -PressableWithoutFeedback.defaultProps = _.omit(GenericPressableProps.defaultProps, omittedProps); - -export default PressableWithoutFeedback; diff --git a/src/components/Pressable/PressableWithoutFeedback.tsx b/src/components/Pressable/PressableWithoutFeedback.tsx new file mode 100644 index 000000000000..c3b780e63cfd --- /dev/null +++ b/src/components/Pressable/PressableWithoutFeedback.tsx @@ -0,0 +1,21 @@ +import React, {ForwardedRef} from 'react'; +import {View} from 'react-native'; +import GenericPressable from './GenericPressable'; +import PressableProps from './GenericPressable/types'; + +function PressableWithoutFeedback( + {pressStyle, hoverStyle, focusStyle, disabledStyle, screenReaderActiveStyle, shouldUseHapticsOnPress, shouldUseHapticsOnLongPress, ...rest}: PressableProps, + ref: ForwardedRef, +) { + return ( + + ); +} + +PressableWithoutFeedback.displayName = 'PressableWithoutFeedback'; + +export default React.forwardRef(PressableWithoutFeedback); diff --git a/src/components/Pressable/PressableWithoutFocus.js b/src/components/Pressable/PressableWithoutFocus.js deleted file mode 100644 index 641e695b1013..000000000000 --- a/src/components/Pressable/PressableWithoutFocus.js +++ /dev/null @@ -1,68 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import StylePropType from '@styles/stylePropTypes'; -import GenericPressable from './GenericPressable'; -import genericPressablePropTypes from './GenericPressable/PropTypes'; - -const propTypes = { - /** Element that should be clickable */ - children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, - - /** Callback for onPress event */ - onPress: PropTypes.func.isRequired, - - /** Callback for onLongPress event */ - onLongPress: PropTypes.func, - - /** Styles that should be passed to touchable container */ - style: StylePropType, - - /** Proptypes of pressable component used for implementation */ - ...genericPressablePropTypes.pressablePropTypes, -}; - -const defaultProps = { - style: [], - onLongPress: undefined, -}; - -/** - * This component prevents the tapped element from capturing focus. - * We need to blur this element when clicked as it opens modal that implements focus-trapping. - * When the modal is closed it focuses back to the last active element. - * Therefore it shifts the element to bring it back to focus. - * https://github.com/Expensify/App/issues/6806 - */ -class PressableWithoutFocus extends React.Component { - constructor(props) { - super(props); - this.pressAndBlur = this.pressAndBlur.bind(this); - } - - pressAndBlur() { - this.pressableRef.blur(); - this.props.onPress(); - } - - render() { - const restProps = _.omit(this.props, ['children', 'onPress', 'onLongPress', 'style']); - return ( - (this.pressableRef = el)} - style={this.props.style} - // eslint-disable-next-line react/jsx-props-no-spreading - {...restProps} - > - {this.props.children} - - ); - } -} - -PressableWithoutFocus.propTypes = propTypes; -PressableWithoutFocus.defaultProps = defaultProps; - -export default PressableWithoutFocus; diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx new file mode 100644 index 000000000000..32cb1708baf0 --- /dev/null +++ b/src/components/Pressable/PressableWithoutFocus.tsx @@ -0,0 +1,36 @@ +import React, {useRef} from 'react'; +import {View} from 'react-native'; +import GenericPressable from './GenericPressable'; +import PressableProps from './GenericPressable/types'; + +/** + * This component prevents the tapped element from capturing focus. + * We need to blur this element when clicked as it opens modal that implements focus-trapping. + * When the modal is closed it focuses back to the last active element. + * Therefore it shifts the element to bring it back to focus. + * https://github.com/Expensify/App/issues/6806 + */ +function PressableWithoutFocus({children, onPress, onLongPress, ...rest}: PressableProps) { + const ref = useRef(null); + + const pressAndBlur = () => { + ref?.current?.blur(); + onPress(); + }; + + return ( + + {children} + + ); +} + +PressableWithoutFocus.displayName = 'PressableWithoutFocus'; + +export default PressableWithoutFocus; diff --git a/src/components/Pressable/index.js b/src/components/Pressable/index.ts similarity index 100% rename from src/components/Pressable/index.js rename to src/components/Pressable/index.ts diff --git a/src/hooks/useSingleExecution.js b/src/hooks/useSingleExecution.ts similarity index 83% rename from src/hooks/useSingleExecution.js rename to src/hooks/useSingleExecution.ts index a2b4ccb4cd53..16a98152def1 100644 --- a/src/hooks/useSingleExecution.js +++ b/src/hooks/useSingleExecution.ts @@ -1,20 +1,20 @@ import {useCallback, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; +type Action = (...params: T) => void | Promise; + /** * With any action passed in, it will only allow 1 such action to occur at a time. - * - * @returns {Object} */ export default function useSingleExecution() { const [isExecuting, setIsExecuting] = useState(false); - const isExecutingRef = useRef(); + const isExecutingRef = useRef(); isExecutingRef.current = isExecuting; const singleExecution = useCallback( - (action) => - (...params) => { + (action: Action) => + (...params: T) => { if (isExecutingRef.current) { return; } diff --git a/src/libs/Accessibility/index.ts b/src/libs/Accessibility/index.ts index 5eceda8edcb1..aa167b1239b2 100644 --- a/src/libs/Accessibility/index.ts +++ b/src/libs/Accessibility/index.ts @@ -42,7 +42,7 @@ const useAutoHitSlop = () => { }, [frameSize], ); - return [getHitSlopForSize(frameSize), onLayout]; + return [getHitSlopForSize(frameSize), onLayout] as const; }; export default { diff --git a/src/libs/KeyboardShortcut/index.ts b/src/libs/KeyboardShortcut/index.ts index cfcf5d5ef535..1b684a7ab19f 100644 --- a/src/libs/KeyboardShortcut/index.ts +++ b/src/libs/KeyboardShortcut/index.ts @@ -128,7 +128,7 @@ function getPlatformEquivalentForKeys(keys: string[]): string[] { */ function subscribe( key: string, - callback: () => void, + callback: (event?: KeyboardEvent) => void, descriptionKey: string, modifiers: string[] = ['shift'], captureOnInputs = false, diff --git a/src/libs/getButtonState.ts b/src/libs/getButtonState.ts index 6b89e1b7d383..fe593b9f613e 100644 --- a/src/libs/getButtonState.ts +++ b/src/libs/getButtonState.ts @@ -1,12 +1,10 @@ import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; -type GetButtonState = (isActive: boolean, isPressed: boolean, isComplete: boolean, isDisabled: boolean, isInteractive: boolean) => ValueOf; - /** * Get the string representation of a button's state. */ -const getButtonState: GetButtonState = (isActive = false, isPressed = false, isComplete = false, isDisabled = false, isInteractive = true) => { +function getButtonState(isActive = false, isPressed = false, isComplete = false, isDisabled = false, isInteractive = true): ValueOf { if (!isInteractive) { return CONST.BUTTON_STATES.DEFAULT; } @@ -28,6 +26,6 @@ const getButtonState: GetButtonState = (isActive = false, isPressed = false, isC } return CONST.BUTTON_STATES.DEFAULT; -}; +} export default getButtonState; diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index eda4c3309cbf..e826142fc022 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -1,5 +1,5 @@ import {CSSProperties} from 'react'; -import {Animated, DimensionValue, ImageStyle, PressableStateCallbackType, TextStyle, ViewStyle} from 'react-native'; +import {Animated, DimensionValue, ImageStyle, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {EdgeInsets} from 'react-native-safe-area-context'; import {ValueOf} from 'type-fest'; import * as Browser from '@libs/Browser'; @@ -16,7 +16,7 @@ import spacing from './utilities/spacing'; import variables from './variables'; type AllStyles = ViewStyle | TextStyle | ImageStyle; -type ParsableStyle = AllStyles | ((state: PressableStateCallbackType) => AllStyles); +type ParsableStyle = StyleProp | ((state: PressableStateCallbackType) => StyleProp); type ColorValue = ValueOf; type AvatarSizeName = ValueOf; @@ -749,9 +749,8 @@ function parseStyleAsArray(styleParam: T | T[]): T[] { /** * Parse style function and return Styles object */ -function parseStyleFromFunction(style: ParsableStyle, state: PressableStateCallbackType): AllStyles[] { - const functionAppliedStyle = typeof style === 'function' ? style(state) : style; - return parseStyleAsArray(functionAppliedStyle); +function parseStyleFromFunction(style: ParsableStyle, state: PressableStateCallbackType): StyleProp { + return typeof style === 'function' ? style(state) : style; } /** diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index a816fc77625b..ec857af2eceb 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -35,7 +35,7 @@ declare module 'react-native' { 'aria-haspopup'?: 'dialog' | 'grid' | 'listbox' | 'menu' | 'tree' | false; 'aria-hidden'?: boolean; 'aria-invalid'?: boolean; - 'aria-keyshortcuts'?: string[]; + 'aria-keyshortcuts'?: string; 'aria-label'?: string; 'aria-labelledby'?: idRef; 'aria-level'?: number; @@ -85,7 +85,7 @@ declare module 'react-native' { accessibilityInvalid?: boolean; accessibilityKeyShortcuts?: string[]; accessibilityLabel?: string; - accessibilityLabelledBy?: idRefList; + accessibilityLabelledBy?: idRef; accessibilityLevel?: number; accessibilityLiveRegion?: 'assertive' | 'none' | 'polite'; accessibilityModal?: boolean; @@ -312,7 +312,10 @@ declare module 'react-native' { readonly hovered: boolean; readonly pressed: boolean; } - interface PressableStateCallbackType extends WebPressableStateCallbackType {} + interface PressableStateCallbackType extends WebPressableStateCallbackType { + readonly isScreenReaderActive: boolean; + readonly isDisabled: boolean; + } // Extracted from react-native-web, packages/react-native-web/src/exports/Pressable/index.js interface WebPressableProps extends WebSharedProps {