diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.tsx similarity index 83% rename from src/components/AvatarCropModal/AvatarCropModal.js rename to src/components/AvatarCropModal/AvatarCropModal.tsx index 2da3cf88c78c..3ac2e3e3d729 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.js +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -1,79 +1,71 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; import {ActivityIndicator, Image, View} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; import {Gesture, GestureHandlerRootView} from 'react-native-gesture-handler'; +import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import {interpolate, runOnUI, useSharedValue, useWorkletCallback} from 'react-native-reanimated'; import Button from '@components/Button'; import HeaderGap from '@components/HeaderGap'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; import Modal from '@components/Modal'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import cropOrRotateImage from '@libs/cropOrRotateImage'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import ImageCropView from './ImageCropView'; import Slider from './Slider'; -const propTypes = { +type AvatarCropModalProps = { /** Link to image for cropping */ - imageUri: PropTypes.string, + imageUri?: string; /** Name of the image */ - imageName: PropTypes.string, + imageName?: string; /** Type of the image file */ - imageType: PropTypes.string, + imageType?: string; /** Callback to be called when user closes the modal */ - onClose: PropTypes.func, + onClose?: () => void; /** Callback to be called when user saves the image */ - onSave: PropTypes.func, + onSave?: (newImage: File | CustomRNImageManipulatorResult) => void; /** Modal visibility */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** Image crop vector mask */ - maskImage: sourcePropTypes, - - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - imageUri: '', - imageName: '', - imageType: '', - onClose: () => {}, - onSave: () => {}, - maskImage: undefined, + maskImage?: IconAsset; }; // This component can't be written using class since reanimated API uses hooks. -function AvatarCropModal(props) { +function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose, onSave, isVisible, maskImage}: AvatarCropModalProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); - const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const translateY = useSharedValue(0); const translateX = useSharedValue(0); - const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE); + const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE); const rotation = useSharedValue(0); const translateSlider = useSharedValue(0); const isPressableEnabled = useSharedValue(true); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + // Check if image cropping, saving or uploading is in progress const isLoading = useSharedValue(false); @@ -82,13 +74,13 @@ function AvatarCropModal(props) { const prevMaxOffsetX = useSharedValue(0); const prevMaxOffsetY = useSharedValue(0); - const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); - const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const [isImageContainerInitialized, setIsImageContainerInitialized] = useState(false); const [isImageInitialized, setIsImageInitialized] = useState(false); // An onLayout callback, that initializes the image container, for proper render of an image - const initializeImageContainer = useCallback((event) => { + const initializeImageContainer = useCallback((event: LayoutChangeEvent) => { setIsImageContainerInitialized(true); const {height, width} = event.nativeEvent.layout; @@ -98,7 +90,7 @@ function AvatarCropModal(props) { }, []); // An onLayout callback, that initializes the slider container size, for proper render of a slider - const initializeSliderContainer = useCallback((event) => { + const initializeSliderContainer = useCallback((event: LayoutChangeEvent) => { setSliderContainerSize(event.nativeEvent.layout.width); }, []); @@ -122,7 +114,6 @@ function AvatarCropModal(props) { // In order to calculate proper image position/size/animation, we have to know its size. // And we have to update image size if image url changes. - const imageUri = props.imageUri; useEffect(() => { if (!imageUri) { return; @@ -143,17 +134,11 @@ function AvatarCropModal(props) { /** * Validates that value is within the provided mix/max range. - * - * @param {Number} value - * @param {Array} minMax - * @returns {Number} */ - const clamp = useWorkletCallback((value, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); + const clamp = useWorkletCallback((value: number, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); /** * Returns current image size taking into account scale and rotation. - * - * @returns {Object} */ const getDisplayedImageSize = useWorkletCallback(() => { let height = imageContainerSize * scale.value; @@ -172,12 +157,9 @@ function AvatarCropModal(props) { /** * Validates the offset to prevent overflow, and updates the image offset. - * - * @param {Number} newX - * @param {Number} newY */ const updateImageOffset = useWorkletCallback( - (offsetX, offsetY) => { + (offsetX: number, offsetY: number) => { const {height, width} = getDisplayedImageSize(); const maxOffsetX = (width - imageContainerSize) / 2; const maxOffsetY = (height - imageContainerSize) / 2; @@ -189,12 +171,7 @@ function AvatarCropModal(props) { [imageContainerSize, scale, clamp], ); - /** - * @param {Number} newSliderValue - * @param {Number} containerSize - * @returns {Number} - */ - const newScaleValue = useWorkletCallback((newSliderValue, containerSize) => { + const newScaleValue = useWorkletCallback((newSliderValue: number, containerSize: number) => { const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL; return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE; }); @@ -244,7 +221,7 @@ function AvatarCropModal(props) { isPressableEnabled.value = false; }, - onChange: (event) => { + onChange: (event: GestureUpdateEvent) => { 'worklet'; const newSliderValue = clamp(translateSlider.value + event.changeX, [0, sliderContainerSize]); @@ -311,24 +288,35 @@ function AvatarCropModal(props) { // Svg images are converted to a png blob to preserve transparency, so we need to update the // image name and type accordingly. - const isSvg = props.imageType.includes('image/svg'); - const imageName = isSvg ? 'fileName.png' : props.imageName; - const imageType = isSvg ? 'image/png' : props.imageType; + const isSvg = imageType.includes('image/svg'); + const name = isSvg ? 'fileName.png' : imageName; + const type = isSvg ? 'image/png' : imageType; - cropOrRotateImage(props.imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name: imageName, type: imageType}) + cropOrRotateImage(imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name, type}) .then((newImage) => { - props.onClose(); - props.onSave(newImage); + onClose?.(); + onSave?.(newImage); }) .catch(() => { isLoading.value = false; }); - }, [originalImageHeight.value, originalImageWidth.value, scale.value, translateX.value, imageContainerSize, translateY.value, props, rotation.value, isLoading]); - - /** - * @param {Number} locationX - */ - const sliderOnPress = (locationX) => { + }, [ + imageUri, + imageName, + imageType, + onClose, + onSave, + originalImageHeight.value, + originalImageWidth.value, + scale.value, + translateX.value, + imageContainerSize, + translateY.value, + rotation.value, + isLoading, + ]); + + const sliderOnPress = (locationX: number) => { // We are using the worklet directive here and running on the UI thread to ensure the Reanimated // shared values are updated synchronously, as they update asynchronously on the JS thread. @@ -349,8 +337,8 @@ function AvatarCropModal(props) { return ( onClose?.()} + isVisible={isVisible} type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED} onModalHide={resetState} > @@ -360,12 +348,12 @@ function AvatarCropModal(props) { includeSafeAreaPaddingBottom={false} testID={AvatarCropModal.displayName} > - {props.isSmallScreenWidth && } + {isSmallScreenWidth && } - {props.translate('avatarCropModal.description')} + {translate('avatarCropModal.description')} @@ -432,7 +420,7 @@ function AvatarCropModal(props) { style={[styles.m5]} onPress={cropAndSaveImage} pressOnEnter - text={props.translate('common.save')} + text={translate('common.save')} /> @@ -440,6 +428,5 @@ function AvatarCropModal(props) { } AvatarCropModal.displayName = 'AvatarCropModal'; -AvatarCropModal.propTypes = propTypes; -AvatarCropModal.defaultProps = defaultProps; -export default compose(withWindowDimensions, withLocalize)(AvatarCropModal); + +export default AvatarCropModal; diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.tsx similarity index 69% rename from src/components/AvatarCropModal/ImageCropView.js rename to src/components/AvatarCropModal/ImageCropView.tsx index 0790ffaca8e1..c79a209376b4 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -1,58 +1,52 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import type {PanGesture} from 'react-native-gesture-handler'; import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; +import type {SharedValue} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; +import type IconAsset from '@src/types/utils/IconAsset'; -const propTypes = { +type ImageCropViewProps = { /** Link to image for cropping */ - imageUri: PropTypes.string, + imageUri?: string; /** Size of the image container that will be rendered */ - containerSize: PropTypes.number, + containerSize?: number; /** The height of the selected image */ - originalImageHeight: PropTypes.shape({value: PropTypes.number}).isRequired, + originalImageHeight: SharedValue; /** The width of the selected image */ - originalImageWidth: PropTypes.shape({value: PropTypes.number}).isRequired, + originalImageWidth: SharedValue; /** The rotation value of the selected image */ - rotation: PropTypes.shape({value: PropTypes.number}).isRequired, + rotation: SharedValue; /** The relative image shift along X-axis */ - translateX: PropTypes.shape({value: PropTypes.number}).isRequired, + translateX: SharedValue; /** The relative image shift along Y-axis */ - translateY: PropTypes.shape({value: PropTypes.number}).isRequired, + translateY: SharedValue; /** The scale factor of the image */ - scale: PropTypes.shape({value: PropTypes.number}).isRequired, + scale: SharedValue; /** Configuration object for pan gesture for handling image panning */ - // eslint-disable-next-line react/forbid-prop-types - panGesture: PropTypes.object, + panGesture?: PanGesture; /** Image crop vector mask */ - maskImage: PropTypes.func, + maskImage?: IconAsset; }; -const defaultProps = { - imageUri: '', - containerSize: 0, - panGesture: Gesture.Pan(), - maskImage: Expensicons.ImageCropCircleMask, -}; - -function ImageCropView(props) { +function ImageCropView({imageUri = '', containerSize = 0, panGesture = Gesture.Pan(), maskImage = Expensicons.ImageCropCircleMask, ...props}: ImageCropViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize); + const containerStyle = StyleUtils.getWidthAndHeightStyle(containerSize, containerSize); const originalImageHeight = props.originalImageHeight; const originalImageWidth = props.originalImageWidth; @@ -75,23 +69,23 @@ function ImageCropView(props) { // We're preventing text selection with ControlSelection.blockElement to prevent safari // default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688 return ( - + ControlSelection.blockElement(el as HTMLElement | null)} style={[containerStyle, styles.imageCropContainer]} > @@ -100,8 +94,6 @@ function ImageCropView(props) { } ImageCropView.displayName = 'ImageCropView'; -ImageCropView.propTypes = propTypes; -ImageCropView.defaultProps = defaultProps; // React.memo is needed here to prevent styles recompilation // which sometimes may cause glitches during rerender of the modal diff --git a/src/components/AvatarCropModal/Slider.js b/src/components/AvatarCropModal/Slider.tsx similarity index 65% rename from src/components/AvatarCropModal/Slider.js rename to src/components/AvatarCropModal/Slider.tsx index 83c8577d2e55..9a9da65befa0 100644 --- a/src/components/AvatarCropModal/Slider.js +++ b/src/components/AvatarCropModal/Slider.tsx @@ -1,43 +1,31 @@ -import PropTypes from 'prop-types'; import React, {useState} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import Animated, {runOnJS, useAnimatedStyle} from 'react-native-reanimated'; +import type {SharedValue} from 'react-native-reanimated'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -const propTypes = { - /** Callbacks for react-native-gesture-handler to be executed when the user is panning slider */ - gestureCallbacks: PropTypes.shape({onBegin: PropTypes.func, onChange: PropTypes.func, onFinalize: PropTypes.func}), +type SliderProps = { + /** React-native-reanimated lib handler which executes when the user is panning slider */ + gestureCallbacks: { + onBegin: () => void; + onChange: (event: GestureUpdateEvent) => void; + onFinalize: () => void; + }; /** X position of the slider knob */ - sliderValue: PropTypes.shape({value: PropTypes.number}), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - gestureCallbacks: { - onBegin: () => { - 'worklet'; - }, - onChange: () => { - 'worklet'; - }, - onFinalize: () => { - 'worklet'; - }, - }, - sliderValue: {}, + sliderValue: SharedValue; }; // This component can't be written using class since reanimated API uses hooks. -function Slider(props) { +function Slider({sliderValue, gestureCallbacks}: SliderProps) { const styles = useThemeStyles(); - const sliderValue = props.sliderValue; const [tooltipIsVisible, setTooltipIsVisible] = useState(true); + const {translate} = useLocalize(); // A reanimated memoized style, which tracks // a translateX shared value and updates the slider position. @@ -49,28 +37,28 @@ function Slider(props) { .minDistance(5) .onBegin(() => { runOnJS(setTooltipIsVisible)(false); - props.gestureCallbacks.onBegin(); + gestureCallbacks.onBegin(); }) .onChange((event) => { - props.gestureCallbacks.onChange(event); + gestureCallbacks.onChange(event); }) .onFinalize(() => { runOnJS(setTooltipIsVisible)(true); - props.gestureCallbacks.onFinalize(); + gestureCallbacks.onFinalize(); }); // We're preventing text selection with ControlSelection.blockElement to prevent safari // default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688 return ( ControlSelection.blockElement(el as HTMLElement | null)} style={styles.sliderBar} > {tooltipIsVisible && ( {/* pointerEventsNone is a workaround to make sure the pan gesture works correctly on mobile safari */} @@ -84,6 +72,4 @@ function Slider(props) { } Slider.displayName = 'Slider'; -Slider.propTypes = propTypes; -Slider.defaultProps = defaultProps; -export default withLocalize(Slider); +export default Slider; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 5fb134648134..f4b6e8b23ecf 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -20,7 +20,7 @@ import validateSubmitShortcut from './validateSubmitShortcut'; type ButtonWithText = { /** The text for the button label */ - text: string; + text?: string; /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index dc04b6fcf329..2dd2e17e0454 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -40,7 +40,7 @@ type PressableProps = RNPressableProps & /** * onPress callback */ - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; /** * Specifies keyboard shortcut to trigger onPressHandler diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index ab1fa95efeb5..86f6c9d8aff8 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -78,7 +78,7 @@ function PressableWithDelayToggle( return; } temporarilyDisableInteractions(); - onPress(); + onPress?.(); }; // Due to limitations in RN regarding the vertical text alignment of non-Text elements, diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx index f887b0ea9b7d..240ef4a9873a 100644 --- a/src/components/Pressable/PressableWithoutFocus.tsx +++ b/src/components/Pressable/PressableWithoutFocus.tsx @@ -15,7 +15,7 @@ function PressableWithoutFocus({children, onPress, onLongPress, ...rest}: Pressa const pressAndBlur = () => { ref?.current?.blur(); - onPress(); + onPress?.(); }; return ( diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts index ab11e66bc369..44787dc77dbe 100644 --- a/src/libs/ControlSelection/index.ts +++ b/src/libs/ControlSelection/index.ts @@ -1,4 +1,3 @@ -import type CustomRefObject from '@src/types/utils/CustomRefObject'; import type ControlSelectionModule from './types'; /** @@ -20,25 +19,25 @@ function unblock() { /** * Block selection on particular element */ -function blockElement(ref?: CustomRefObject | null) { - if (!ref) { +function blockElement(element?: HTMLElement | null) { + if (!element) { return; } // eslint-disable-next-line no-param-reassign - ref.onselectstart = () => false; + element.onselectstart = () => false; } /** * Unblock selection on particular element */ -function unblockElement(ref?: CustomRefObject | null) { - if (!ref) { +function unblockElement(element?: HTMLElement | null) { + if (!element) { return; } // eslint-disable-next-line no-param-reassign - ref.onselectstart = () => true; + element.onselectstart = () => true; } const ControlSelection: ControlSelectionModule = { diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts index fc0b488577ec..c4ca4b713b9b 100644 --- a/src/libs/ControlSelection/types.ts +++ b/src/libs/ControlSelection/types.ts @@ -1,10 +1,8 @@ -import type CustomRefObject from '@src/types/utils/CustomRefObject'; - type ControlSelectionModule = { block: () => void; unblock: () => void; - blockElement: (ref?: CustomRefObject | null) => void; - unblockElement: (ref?: CustomRefObject | null) => void; + blockElement: (element?: HTMLElement | null) => void; + unblockElement: (element?: HTMLElement | null) => void; }; export default ControlSelectionModule; diff --git a/src/types/utils/CustomRefObject.ts b/src/types/utils/CustomRefObject.ts deleted file mode 100644 index 13bb0f27a42e..000000000000 --- a/src/types/utils/CustomRefObject.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {RefObject} from 'react'; - -type CustomRefObject = RefObject & {onselectstart: () => boolean}; - -export default CustomRefObject;