From f360d946c662cbd270f4c53dae444d372a63ae71 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 31 Oct 2023 12:56:32 +0100 Subject: [PATCH 01/14] Rename files --- ...cPressable.js => BaseGenericPressable.tsx} | 0 .../{index.native.js => index.native.tsx} | 2 +- .../GenericPressable/{index.js => index.tsx} | 2 - .../Pressable/GenericPressable/types.ts | 111 ++++++++++++++++++ 4 files changed, 112 insertions(+), 3 deletions(-) rename src/components/Pressable/GenericPressable/{BaseGenericPressable.js => BaseGenericPressable.tsx} (100%) rename src/components/Pressable/GenericPressable/{index.native.js => index.native.tsx} (91%) rename src/components/Pressable/GenericPressable/{index.js => index.tsx} (85%) create mode 100644 src/components/Pressable/GenericPressable/types.ts diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.js b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx similarity index 100% rename from src/components/Pressable/GenericPressable/BaseGenericPressable.js rename to src/components/Pressable/GenericPressable/BaseGenericPressable.tsx diff --git a/src/components/Pressable/GenericPressable/index.native.js b/src/components/Pressable/GenericPressable/index.native.tsx similarity index 91% rename from src/components/Pressable/GenericPressable/index.native.js rename to src/components/Pressable/GenericPressable/index.native.tsx index 14a2c2bcbf82..8432faef24a8 100644 --- a/src/components/Pressable/GenericPressable/index.native.js +++ b/src/components/Pressable/GenericPressable/index.native.tsx @@ -15,6 +15,6 @@ const NativeGenericPressable = forwardRef((props, ref) => ( NativeGenericPressable.propTypes = GenericPressablePropTypes.pressablePropTypes; NativeGenericPressable.defaultProps = GenericPressablePropTypes.defaultProps; -NativeGenericPressable.displayName = 'WebGenericPressable'; +NativeGenericPressable.displayName = 'NativeGenericPressable'; export default NativeGenericPressable; diff --git a/src/components/Pressable/GenericPressable/index.js b/src/components/Pressable/GenericPressable/index.tsx similarity index 85% rename from src/components/Pressable/GenericPressable/index.js rename to src/components/Pressable/GenericPressable/index.tsx index 8247d0c35670..226c1035f5e0 100644 --- a/src/components/Pressable/GenericPressable/index.js +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -19,8 +19,6 @@ 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/types.ts b/src/components/Pressable/GenericPressable/types.ts new file mode 100644 index 000000000000..2f5397dbf53c --- /dev/null +++ b/src/components/Pressable/GenericPressable/types.ts @@ -0,0 +1,111 @@ +import {ValueOf} from 'type-fest'; +import {GestureResponderEvent, PressableProps as RNPressableProps, PressableStateCallbackType, StyleProp, ViewStyle, HostComponent} from 'react-native'; +import {ElementRef, RefObject} from 'react'; +import CONST from '@src/CONST'; + +type StylePropWithFunction = StyleProp | ((state: PressableStateCallbackType) => StyleProp); + +type PressableProps = RNPressableProps & { + /** + * onPress callback + */ + onPress: (event: GestureResponderEvent) => void; + + /** + * Specifies keyboard shortcut to trigger onPressHandler + * @example {shortcutKey: 'a', modifiers: ['ctrl', 'shift'], descriptionKey: 'keyboardShortcut.description'} + */ + keyboardShortcut: { + descriptionKey: string; + shortcutKey: string; + modifiers: string[]; + }; + + /** + * 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: requiredPropsCheck; + + /** + * 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; +}; + +export default PressableProps; From 4107a803afd0dfce844973172713663b942a0b18 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 10:16:14 +0100 Subject: [PATCH 02/14] Migrate general Pressables --- .../GenericPressable/BaseGenericPressable.tsx | 99 +++++++++---------- .../GenericPressable/index.native.tsx | 30 +++--- .../Pressable/GenericPressable/index.tsx | 44 +++++---- .../Pressable/GenericPressable/types.ts | 46 +++++---- .../Pressable/{index.js => index.ts} | 0 ...ngleExecution.js => useSingleExecution.ts} | 10 +- src/libs/Accessibility/index.ts | 2 +- src/libs/KeyboardShortcut/index.ts | 2 +- src/styles/StyleUtils.ts | 9 +- src/types/modules/react-native.d.ts | 7 +- 10 files changed, 127 insertions(+), 122 deletions(-) rename src/components/Pressable/{index.js => index.ts} (100%) rename src/hooks/{useSingleExecution.js => useSingleExecution.ts} (83%) diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index a3ce55003cdd..a64fd9909f0d 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ 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?.modifiers.map((modifier) => `${modifier}+${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/index.native.tsx b/src/components/Pressable/GenericPressable/index.native.tsx index 8432faef24a8..fbc53551f435 100644 --- a/src/components/Pressable/GenericPressable/index.native.tsx +++ b/src/components/Pressable/GenericPressable/index.native.tsx @@ -1,20 +1,20 @@ -import React, {forwardRef} from 'react'; +import React, {ForwardedRef, forwardRef} from 'react'; +import {View} from 'react-native'; import GenericPressable from './BaseGenericPressable'; -import GenericPressablePropTypes from './PropTypes'; +import PressableProps from './types'; -const NativeGenericPressable = forwardRef((props, ref) => ( - -)); +function NativeGenericPressable(props: PressableProps, ref: ForwardedRef) { + return ( + + ); +} -NativeGenericPressable.propTypes = GenericPressablePropTypes.pressablePropTypes; -NativeGenericPressable.defaultProps = GenericPressablePropTypes.defaultProps; NativeGenericPressable.displayName = 'NativeGenericPressable'; -export default NativeGenericPressable; +export default forwardRef(NativeGenericPressable); diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx index 226c1035f5e0..c8e9560062e0 100644 --- a/src/components/Pressable/GenericPressable/index.tsx +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -1,24 +1,30 @@ -import React, {forwardRef} from 'react'; +import React, {ForwardedRef, forwardRef} from 'react'; +import {Role, View} from 'react-native'; import GenericPressable from './BaseGenericPressable'; -import GenericPressablePropTypes from './PropTypes'; +import PressableProps from './types'; -const WebGenericPressable = forwardRef((props, ref) => ( - -)); +function WebGenericPressable(props: PressableProps, ref: ForwardedRef) { + return ( + + ); +} WebGenericPressable.displayName = 'WebGenericPressable'; -export default WebGenericPressable; +export default forwardRef(WebGenericPressable); diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index 2f5397dbf53c..2e8857224646 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -1,37 +1,40 @@ -import {ValueOf} from 'type-fest'; -import {GestureResponderEvent, PressableProps as RNPressableProps, PressableStateCallbackType, StyleProp, ViewStyle, HostComponent} from 'react-native'; 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 PressableProps = RNPressableProps & { /** * onPress callback */ - onPress: (event: GestureResponderEvent) => void; + onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; /** * Specifies keyboard shortcut to trigger onPressHandler * @example {shortcutKey: 'a', modifiers: ['ctrl', 'shift'], descriptionKey: 'keyboardShortcut.description'} */ - keyboardShortcut: { - descriptionKey: string; - shortcutKey: string; - modifiers: string[]; - }; + keyboardShortcut?: Shortcut; /** * Specifies if haptic feedback should be used on press * @default false */ - shouldUseHapticsOnPress: boolean; + shouldUseHapticsOnPress?: boolean; /** * Specifies if haptic feedback should be used on long press * @default false */ - shouldUseHapticsOnLongPress: boolean; + 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) @@ -39,7 +42,7 @@ type PressableProps = RNPressableProps & { * @example {backgroundColor: 'red'} * @example state => ({backgroundColor: state.isDisabled ? 'red' : 'blue'}) */ - disabledStyle: StylePropWithFunction; + 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) @@ -47,7 +50,7 @@ type PressableProps = RNPressableProps & { * @example {backgroundColor: 'red'} * @example state => ({backgroundColor: state.hovered ? 'red' : 'blue'}) */ - hoverStyle: StylePropWithFunction; + 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) @@ -55,7 +58,7 @@ type PressableProps = RNPressableProps & { * @example {backgroundColor: 'red'} * @example state => ({backgroundColor: state.focused ? 'red' : 'blue'}) */ - focusStyle: StylePropWithFunction; + 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) @@ -63,7 +66,7 @@ type PressableProps = RNPressableProps & { * @example {backgroundColor: 'red'} * @example state => ({backgroundColor: state.pressed ? 'red' : 'blue'}) */ - pressStyle: StylePropWithFunction; + pressStyle?: StylePropWithFunction; /** * style for when the component is active and the screen reader is on. @@ -72,7 +75,7 @@ type PressableProps = RNPressableProps & { * @example {backgroundColor: 'red'} * @example state => ({backgroundColor: state.isScreenReaderActive ? 'red' : 'blue'}) */ - screenReaderActiveStyle: StylePropWithFunction; + screenReaderActiveStyle?: StylePropWithFunction; /** * Specifies if the component should be accessible when the screen reader is on @@ -81,31 +84,34 @@ type PressableProps = RNPressableProps & { * @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; + enableInScreenReaderStates?: ValueOf; /** * Specifies which component should be focused after interacting with this component */ - nextFocusRef: ElementRef> & RefObject; + nextFocusRef?: ElementRef> & RefObject; /** * Specifies the accessibility label for the component * @example 'Search' * @example 'Close' */ - // accessibilityLabel: requiredPropsCheck; + accessibilityLabel: string; /** * Specifies the accessibility hint for the component * @example 'Double tap to open' */ - accessibilityHint: string; + accessibilityHint?: string; /** * Specifies if the component should calculate its hitSlop automatically * @default true */ - shouldUseAutoHitSlop: boolean; + shouldUseAutoHitSlop?: boolean; + + /** Turns off drag area for the component */ + noDragArea?: boolean; }; export default PressableProps; 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/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index faece4f44335..7a2135ead3cd 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; @@ -748,9 +748,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..0c9d7ec7ff91 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -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 { From 28c1d9f0041bcdb0587264588860f754f7ba7f70 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 10:20:50 +0100 Subject: [PATCH 03/14] Rename more files --- .../Pressable/GenericPressable/PropTypes.js | 142 ------------------ ...Toggle.js => PressableWithDelayToggle.tsx} | 0 ...hFeedback.js => PressableWithFeedback.tsx} | 0 ...edback.js => PressableWithoutFeedback.tsx} | 0 ...houtFocus.js => PressableWithoutFocus.tsx} | 0 5 files changed, 142 deletions(-) delete mode 100644 src/components/Pressable/GenericPressable/PropTypes.js rename src/components/Pressable/{PressableWithDelayToggle.js => PressableWithDelayToggle.tsx} (100%) rename src/components/Pressable/{PressableWithFeedback.js => PressableWithFeedback.tsx} (100%) rename src/components/Pressable/{PressableWithoutFeedback.js => PressableWithoutFeedback.tsx} (100%) rename src/components/Pressable/{PressableWithoutFocus.js => PressableWithoutFocus.tsx} (100%) 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/PressableWithDelayToggle.js b/src/components/Pressable/PressableWithDelayToggle.tsx similarity index 100% rename from src/components/Pressable/PressableWithDelayToggle.js rename to src/components/Pressable/PressableWithDelayToggle.tsx diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.tsx similarity index 100% rename from src/components/Pressable/PressableWithFeedback.js rename to src/components/Pressable/PressableWithFeedback.tsx diff --git a/src/components/Pressable/PressableWithoutFeedback.js b/src/components/Pressable/PressableWithoutFeedback.tsx similarity index 100% rename from src/components/Pressable/PressableWithoutFeedback.js rename to src/components/Pressable/PressableWithoutFeedback.tsx diff --git a/src/components/Pressable/PressableWithoutFocus.js b/src/components/Pressable/PressableWithoutFocus.tsx similarity index 100% rename from src/components/Pressable/PressableWithoutFocus.js rename to src/components/Pressable/PressableWithoutFocus.tsx From a63a8962ed3a1ba38de278b4331bbb655104ca7f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 10:28:38 +0100 Subject: [PATCH 04/14] Migrate PressableWithoutFocus --- .../Pressable/PressableWithoutFocus.tsx | 78 ++++++------------- 1 file changed, 23 insertions(+), 55 deletions(-) diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx index 641e695b1013..32cb1708baf0 100644 --- a/src/components/Pressable/PressableWithoutFocus.tsx +++ b/src/components/Pressable/PressableWithoutFocus.tsx @@ -1,31 +1,7 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import StylePropType from '@styles/stylePropTypes'; +import React, {useRef} from 'react'; +import {View} from 'react-native'; 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, -}; +import PressableProps from './GenericPressable/types'; /** * This component prevents the tapped element from capturing focus. @@ -34,35 +10,27 @@ const defaultProps = { * 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} - - ); - } +function PressableWithoutFocus({children, onPress, onLongPress, ...rest}: PressableProps) { + const ref = useRef(null); + + const pressAndBlur = () => { + ref?.current?.blur(); + onPress(); + }; + + return ( + + {children} + + ); } -PressableWithoutFocus.propTypes = propTypes; -PressableWithoutFocus.defaultProps = defaultProps; +PressableWithoutFocus.displayName = 'PressableWithoutFocus'; export default PressableWithoutFocus; From 56ca43fcccecf0e14cf38c497261ca8ae853ee7f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 10:31:01 +0100 Subject: [PATCH 05/14] Migrate PressableWithoutFeedback --- .../Pressable/PressableWithoutFeedback.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/Pressable/PressableWithoutFeedback.tsx b/src/components/Pressable/PressableWithoutFeedback.tsx index 92e704550dec..c3b780e63cfd 100644 --- a/src/components/Pressable/PressableWithoutFeedback.tsx +++ b/src/components/Pressable/PressableWithoutFeedback.tsx @@ -1,23 +1,21 @@ -import React from 'react'; -import _ from 'underscore'; +import React, {ForwardedRef} from 'react'; +import {View} from 'react-native'; import GenericPressable from './GenericPressable'; -import GenericPressableProps from './GenericPressable/PropTypes'; +import PressableProps from './GenericPressable/types'; -const omittedProps = ['pressStyle', 'hoverStyle', 'focusStyle', 'activeStyle', 'disabledStyle', 'screenReaderActiveStyle', 'shouldUseHapticsOnPress', 'shouldUseHapticsOnLongPress']; - -const PressableWithoutFeedback = React.forwardRef((props, ref) => { - const propsWithoutStyling = _.omit(props, omittedProps); +function PressableWithoutFeedback( + {pressStyle, hoverStyle, focusStyle, disabledStyle, screenReaderActiveStyle, shouldUseHapticsOnPress, shouldUseHapticsOnLongPress, ...rest}: PressableProps, + ref: ForwardedRef, +) { return ( ); -}); +} PressableWithoutFeedback.displayName = 'PressableWithoutFeedback'; -PressableWithoutFeedback.propTypes = _.omit(GenericPressableProps.pressablePropTypes, omittedProps); -PressableWithoutFeedback.defaultProps = _.omit(GenericPressableProps.defaultProps, omittedProps); -export default PressableWithoutFeedback; +export default React.forwardRef(PressableWithoutFeedback); From 384cb529e99973d9c32478a8292252c99be883a4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 10:40:12 +0100 Subject: [PATCH 06/14] Migrate PressableWithFeedback --- .../Pressable/PressableWithFeedback.tsx | 91 +++++++++---------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx index ad29204bb018..5d7f7c110ea7 100644 --- a/src/components/Pressable/PressableWithFeedback.tsx +++ b/src/components/Pressable/PressableWithFeedback.tsx @@ -1,95 +1,90 @@ -import propTypes from 'prop-types'; -import React, {forwardRef, useState} from 'react'; -import _ from 'underscore'; +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 GenericPressablePropTypes from './GenericPressable/PropTypes'; +import PressableProps from './GenericPressable/types'; -const omittedProps = ['wrapperStyle', 'needsOffscreenAlphaCompositing']; +type PressableWithFeedbackProps = PressableProps & { + /** Style for the wrapper view */ + wrapperStyle?: StyleProp>; -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, + 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: propTypes.number, - /** - * Used to locate this view from native classes. - */ - nativeID: propTypes.string, + hoverDimmingValue?: number; /** 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, + needsOffscreenAlphaCompositing?: boolean; }; -const PressableWithFeedback = forwardRef((props, ref) => { - const propsWithoutWrapperProps = _.omit(props, omittedProps); +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 ( { + {...rest} + disabled={rest.disabled} + onHoverIn={(event) => { setIsHovered(true); - if (props.onHoverIn) { - props.onHoverIn(); + if (rest.onHoverIn) { + rest.onHoverIn(event); } }} - onHoverOut={() => { + onHoverOut={(event) => { setIsHovered(false); - if (props.onHoverOut) { - props.onHoverOut(); + if (rest.onHoverOut) { + rest.onHoverOut(event); } }} - onPressIn={() => { + onPressIn={(event) => { setIsPressed(true); - if (props.onPressIn) { - props.onPressIn(); + if (rest.onPressIn) { + rest.onPressIn(event); } }} - onPressOut={() => { + onPressOut={(event) => { setIsPressed(false); - if (props.onPressOut) { - props.onPressOut(); + if (rest.onPressOut) { + rest.onPressOut(event); } }} > - {(state) => (_.isFunction(props.children) ? props.children(state) : props.children)} + {(state) => (typeof children === 'function' ? children(state) : children)} ); -}); +} PressableWithFeedback.displayName = 'PressableWithFeedback'; -PressableWithFeedback.propTypes = PressableWithFeedbackPropTypes; -PressableWithFeedback.defaultProps = PressableWithFeedbackDefaultProps; -export default PressableWithFeedback; +export default forwardRef(PressableWithFeedback); From 831c3f9586bf90ad97736d8d37422e7257132908 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 12:46:21 +0100 Subject: [PATCH 07/14] Migrate PressableWithDelayToggle --- src/components/OpacityView.js | 2 +- .../Pressable/PressableWithDelayToggle.tsx | 122 ++++++++---------- src/libs/getButtonState.ts | 6 +- 3 files changed, 57 insertions(+), 73 deletions(-) diff --git a/src/components/OpacityView.js b/src/components/OpacityView.js index ebd261916e65..582251d569c1 100644 --- a/src/components/OpacityView.js +++ b/src/components/OpacityView.js @@ -21,7 +21,7 @@ const propTypes = { * @default [] */ // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + style: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.bool]), /** * The value to use for the opacity when the view is dimmed diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index 7113afff8bdc..388c5b3660b6 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -1,8 +1,8 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +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 +10,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 +72,58 @@ 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,17 +135,6 @@ function PressableWithDelayToggle(props) { ); } -PressableWithDelayToggle.propTypes = propTypes; -PressableWithDelayToggle.defaultProps = defaultProps; - -const PressableWithDelayToggleWithRef = React.forwardRef((props, ref) => ( - -)); - -PressableWithDelayToggleWithRef.displayName = 'PressableWithDelayToggleWithRef'; +PressableWithDelayToggle.displayName = 'PressableWithDelayToggle'; -export default PressableWithDelayToggleWithRef; +export default forwardRef(PressableWithDelayToggle); 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; From 5e961ba6667201f5f2945c6728adb9c0103726ce Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 13:23:14 +0100 Subject: [PATCH 08/14] Remove excessive optional chaining --- .../Pressable/GenericPressable/BaseGenericPressable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index a64fd9909f0d..2da089b94661 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -83,7 +83,7 @@ function GenericPressable( HapticFeedback.longPress(); } if (ref && 'current' in ref) { - ref?.current?.blur(); + ref.current?.blur(); } onLongPress(event); @@ -104,7 +104,7 @@ function GenericPressable( HapticFeedback.press(); } if (ref && 'current' in ref) { - ref?.current?.blur(); + ref.current?.blur(); } onPress(event); From 7e6357ea60a2c708bddbb5df6073aa299eaeab0d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 13:35:17 +0100 Subject: [PATCH 09/14] Revert accessibilityHint change in index.native.tsx --- src/components/Pressable/GenericPressable/index.native.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Pressable/GenericPressable/index.native.tsx b/src/components/Pressable/GenericPressable/index.native.tsx index fbc53551f435..5bed0f488063 100644 --- a/src/components/Pressable/GenericPressable/index.native.tsx +++ b/src/components/Pressable/GenericPressable/index.native.tsx @@ -8,6 +8,7 @@ function NativeGenericPressable(props: PressableProps, ref: ForwardedRef) Date: Thu, 2 Nov 2023 13:51:06 +0100 Subject: [PATCH 10/14] Improve accessibilityLabel for pressable --- .../Pressable/GenericPressable/types.ts | 232 ++++++++++-------- .../Pressable/PressableWithDelayToggle.tsx | 2 +- 2 files changed, 132 insertions(+), 102 deletions(-) diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index 2e8857224646..35616cb600a3 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -12,106 +12,136 @@ type Shortcut = { modifiers: string[]; }; -type PressableProps = RNPressableProps & { - /** - * 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; -}; +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.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index 388c5b3660b6..316adc25076d 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -1,3 +1,4 @@ +/* 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'; @@ -109,7 +110,6 @@ function PressableWithDelayToggle( From edd4afa21f81cd4bd81bbbe1987c45181b090e4b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 14:01:47 +0100 Subject: [PATCH 11/14] Update stylePropTypes and use it in OpacityView --- src/components/FixedFooter.tsx | 2 +- src/components/OpacityView.js | 4 ++-- src/styles/stylePropTypes.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index afda41f16d06..61529f16b3bb 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -11,7 +11,7 @@ type FixedFooterProps = { }; function FixedFooter({style = [], children}: FixedFooterProps) { - return {children}; + return {children}; } FixedFooter.displayName = 'FixedFooter'; diff --git a/src/components/OpacityView.js b/src/components/OpacityView.js index 582251d569c1..be42c22feaa2 100644 --- a/src/components/OpacityView.js +++ b/src/components/OpacityView.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; +import stylePropTypes from '@styles/stylePropTypes'; import * as StyleUtils from '@styles/StyleUtils'; import variables from '@styles/variables'; @@ -20,8 +21,7 @@ const propTypes = { * Array of style objects * @default [] */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.bool]), + style: stylePropTypes, /** * The value to use for the opacity when the view is dimmed diff --git a/src/styles/stylePropTypes.js b/src/styles/stylePropTypes.js index b97decba621c..961846f8065a 100644 --- a/src/styles/stylePropTypes.js +++ b/src/styles/stylePropTypes.js @@ -1,3 +1,3 @@ import PropTypes from 'prop-types'; -export default PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object), PropTypes.func]); +export default PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object), PropTypes.func, PropTypes.bool]); From 9a5efebf00fdec759ec2549554c3f6ac8de0d634 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 14:04:28 +0100 Subject: [PATCH 12/14] Fix a typo --- src/components/FixedFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index 61529f16b3bb..afda41f16d06 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -11,7 +11,7 @@ type FixedFooterProps = { }; function FixedFooter({style = [], children}: FixedFooterProps) { - return {children}; + return {children}; } FixedFooter.displayName = 'FixedFooter'; From 1071d374614bbb578838ba27daeaf7d43fbd459a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Nov 2023 14:43:14 +0100 Subject: [PATCH 13/14] Fix prettier --- src/styles/StyleUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index 7a2135ead3cd..85a9bc7110bd 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -749,7 +749,7 @@ function parseStyleAsArray(styleParam: T | T[]): T[] { * Parse style function and return Styles object */ function parseStyleFromFunction(style: ParsableStyle, state: PressableStateCallbackType): StyleProp { - return typeof style === 'function' ? style(state) : style + return typeof style === 'function' ? style(state) : style; } /** From 28a5209a78b4f5d47efdfae797681f2d669f4a31 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 3 Nov 2023 11:51:05 +0100 Subject: [PATCH 14/14] Fix aria-keyshortcuts --- .../Pressable/GenericPressable/BaseGenericPressable.tsx | 2 +- src/types/modules/react-native.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index 2da089b94661..1576fe18da54 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -146,7 +146,7 @@ function GenericPressable( ...rest.accessibilityState, }} aria-disabled={isDisabled} - aria-keyshortcuts={keyboardShortcut?.modifiers.map((modifier) => `${modifier}+${keyboardShortcut.shortcutKey}`) ?? []} + aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers.join('')}+${keyboardShortcut.shortcutKey}`} // ios-only form of inputs onMagicTap={!isDisabled ? onPressHandler : undefined} onAccessibilityTap={!isDisabled ? onPressHandler : undefined} diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index 0c9d7ec7ff91..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;