From 95501b300d8ce02ad4cb8838927793fd9167f6d5 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 12:16:04 +0200 Subject: [PATCH 01/11] Fix type on useSyncFocus --- src/components/FocusableMenuItem.tsx | 2 +- src/components/SelectionList/BaseListItem.tsx | 2 +- src/hooks/useSyncFocus/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/FocusableMenuItem.tsx b/src/components/FocusableMenuItem.tsx index e3ec8394dfa0..7868e6e11b0e 100644 --- a/src/components/FocusableMenuItem.tsx +++ b/src/components/FocusableMenuItem.tsx @@ -5,7 +5,7 @@ import type {MenuItemProps} from './MenuItem'; import MenuItem from './MenuItem'; function FocusableMenuItem(props: MenuItemProps) { - const ref = useRef(null); + const ref = useRef(null); // Sync focus on an item useSyncFocus(ref, Boolean(props.focused)); diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 671823eb255b..d8074fb8ffc9 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -37,7 +37,7 @@ function BaseListItem({ const styles = useThemeStyles(); const {hovered, bind} = useHover(); - const pressableRef = useRef(null); + const pressableRef = useRef(null); // Sync focus on an item useSyncFocus(pressableRef, Boolean(isFocused && shouldSyncFocus)); diff --git a/src/hooks/useSyncFocus/index.ts b/src/hooks/useSyncFocus/index.ts index bdc4a6a876da..7ac45c89478a 100644 --- a/src/hooks/useSyncFocus/index.ts +++ b/src/hooks/useSyncFocus/index.ts @@ -7,7 +7,7 @@ import type {View} from 'react-native'; * When the user navigates through the app using the arrows and then the tab button, the focus on the element and the native focus of the browser differs. * To maintain consistency when an element is focused in the app, the focus() method is additionally called on the focused element to eliminate the difference between native browser focus and application focus. */ -const useSyncFocus = (ref: RefObject, isFocused: boolean) => { +const useSyncFocus = (ref: RefObject, isFocused: boolean) => { useLayoutEffect(() => { if (!isFocused) { return; From 1fdccab75cf07191c60af29f3af2c188bc62066d Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 12:17:32 +0200 Subject: [PATCH 02/11] Fix types --- .../EmojiPicker/EmojiPickerMenuItem/index.native.tsx | 3 ++- src/components/ThreeDotsMenu/index.tsx | 2 +- src/components/WorkspaceSwitcherButton.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx index 1336654cdf2f..0a7adabe009f 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx @@ -1,4 +1,5 @@ import React, {useEffect, useRef} from 'react'; +import type {View} from 'react-native'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -18,7 +19,7 @@ function EmojiPickerMenuItem({ isHighlighted = false, isUsingKeyboardMovement = false, }: EmojiPickerMenuItemProps) { - const ref = useRef(null); + const ref = useRef(null); const StyleUtils = useStyleUtils(); const themeStyles = useThemeStyles(); diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index f6b1f444a24b..72f7f31ded9d 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -41,7 +41,7 @@ function ThreeDotsMenu({ const theme = useTheme(); const styles = useThemeStyles(); const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); - const buttonRef = useRef(null); + const buttonRef = useRef(null); const {translate} = useLocalize(); const isBehindModal = modal?.willAlertModalBecomeVisible && !modal?.isPopover && !shouldOverlay; diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index 2b9b3480f96d..e612571a6c9c 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -24,7 +24,7 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { const {translate} = useLocalize(); const theme = useTheme(); - const pressableRef = useRef(null); + const pressableRef = useRef(null); const {source, name, type} = useMemo(() => { if (!policy) { From 3780b4c3e9ca738bd78a4b22edc9d737230b9d75 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 12:19:31 +0200 Subject: [PATCH 03/11] Get rid of refs on AutoCompleteSuggestions --- .../BaseAutoCompleteSuggestions.tsx | 44 +++++++++++-------- .../AutoCompleteSuggestions/index.tsx | 23 ---------- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index ccd0f21626a0..32ac018acb04 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,7 +1,6 @@ import {FlashList} from '@shopify/flash-list'; -import type {ForwardedRef, ReactElement} from 'react'; -import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react'; -import type {View} from 'react-native'; +import type {ReactElement} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; // We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another import {ScrollView} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; @@ -10,9 +9,9 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import viewForwardedRef from '@src/types/utils/viewForwardedRef'; import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; const measureHeightOfSuggestionRows = (numRows: number, isSuggestionPickerLarge: boolean): number => { @@ -30,18 +29,22 @@ const measureHeightOfSuggestionRows = (numRows: number, isSuggestionPickerLarge: return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; }; -function BaseAutoCompleteSuggestions( - { - highlightedSuggestionIndex, - onSelect, - accessibilityLabelExtractor, - renderSuggestionMenuItem, - suggestions, - isSuggestionPickerLarge, - keyExtractor, - }: AutoCompleteSuggestionsProps, - ref: ForwardedRef, -) { +/** + * On the mobile-web platform, when long-pressing on auto-complete suggestions, + * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). + * The desired pattern for all platforms is to do nothing on long-press. + * On the native platform, tapping on auto-complete suggestions will not blur the main input. + */ + +function BaseAutoCompleteSuggestions({ + highlightedSuggestionIndex, + onSelect, + accessibilityLabelExtractor, + renderSuggestionMenuItem, + suggestions, + isSuggestionPickerLarge, + keyExtractor, +}: AutoCompleteSuggestionsProps) { const {windowWidth, isLargeScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -92,9 +95,14 @@ function BaseAutoCompleteSuggestions( return ( { + if (DeviceCapabilities.hasHoverSupport()) { + return; + } + e.preventDefault(); + }} > ( BaseAutoCompleteSuggestions.displayName = 'BaseAutoCompleteSuggestions'; -export default forwardRef(BaseAutoCompleteSuggestions); +export default BaseAutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index baca4011a177..c7f2aaea4d82 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -3,39 +3,17 @@ import ReactDOM from 'react-dom'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; -/** - * On the mobile-web platform, when long-pressing on auto-complete suggestions, - * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). - * The desired pattern for all platforms is to do nothing on long-press. - * On the native platform, tapping on auto-complete suggestions will not blur the main input. - */ - function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { const StyleUtils = useStyleUtils(); - const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, left: 0, bottom: 0, }); - React.useEffect(() => { - const container = containerRef.current; - if (!container) { - return () => {}; - } - container.onpointerdown = (e) => { - if (DeviceCapabilities.hasHoverSupport()) { - return; - } - e.preventDefault(); - }; - return () => (container.onpointerdown = null); - }, []); React.useEffect(() => { if (!measureParentContainer) { @@ -48,7 +26,6 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} // eslint-disable-next-line react/jsx-props-no-spreading {...props} - ref={containerRef} /> ); From 69380142552f528559e539bf5ebe4c5dcde9e7ec Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 12:21:07 +0200 Subject: [PATCH 04/11] Improve types --- src/components/Button/index.tsx | 2 +- .../ButtonWithDropdownMenu/index.tsx | 2 +- .../DragAndDrop/NoDropZone/index.tsx | 8 ++++--- src/components/DragAndDrop/Provider/index.tsx | 8 ++++--- src/components/KYCWall/BaseKYCWall.tsx | 2 +- src/hooks/useDragAndDrop/index.native.ts | 3 +++ .../index.ts} | 23 +++++-------------- src/hooks/useDragAndDrop/types.ts | 15 ++++++++++++ src/pages/workspace/WorkspaceMembersPage.tsx | 2 -- .../categories/WorkspaceCategoriesPage.tsx | 4 +--- .../distanceRates/PolicyDistanceRatesPage.tsx | 4 +--- .../workspace/tags/WorkspaceTagsPage.tsx | 4 +--- .../workspace/taxes/WorkspaceTaxesPage.tsx | 4 +--- src/types/utils/htmlDivElementRef.ts | 5 ++++ 14 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 src/hooks/useDragAndDrop/index.native.ts rename src/hooks/{useDragAndDrop.ts => useDragAndDrop/index.ts} (87%) create mode 100644 src/hooks/useDragAndDrop/types.ts create mode 100644 src/types/utils/htmlDivElementRef.ts diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 68f1aac41a5b..5392f92e7af4 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -200,7 +200,7 @@ function Button( accessibilityLabel = '', ...rest }: ButtonProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index a4e6e2c87fec..78c51caf0657 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -40,7 +40,7 @@ function ButtonWithDropdownMenu({ const [isMenuVisible, setIsMenuVisible] = useState(false); const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); - const caretButton = useRef(null); + const caretButton = useRef(null); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; diff --git a/src/components/DragAndDrop/NoDropZone/index.tsx b/src/components/DragAndDrop/NoDropZone/index.tsx index 4760a16fd20b..ee3f2a6a34a1 100644 --- a/src/components/DragAndDrop/NoDropZone/index.tsx +++ b/src/components/DragAndDrop/NoDropZone/index.tsx @@ -2,20 +2,22 @@ import React, {useRef} from 'react'; import {View} from 'react-native'; import useDragAndDrop from '@hooks/useDragAndDrop'; import useThemeStyles from '@hooks/useThemeStyles'; +import htmlDivElementRef from '@src/types/utils/htmlDivElementRef'; +import viewRef from '@src/types/utils/viewRef'; import type NoDropZoneProps from './types'; function NoDropZone({children}: NoDropZoneProps) { const styles = useThemeStyles(); - const noDropZone = useRef(null); + const noDropZone = useRef(null); useDragAndDrop({ - dropZone: noDropZone, + dropZone: htmlDivElementRef(noDropZone), shouldAllowDrop: false, }); return ( {children} diff --git a/src/components/DragAndDrop/Provider/index.tsx b/src/components/DragAndDrop/Provider/index.tsx index a5da9cc45a36..dc02eea2b12c 100644 --- a/src/components/DragAndDrop/Provider/index.tsx +++ b/src/components/DragAndDrop/Provider/index.tsx @@ -4,6 +4,8 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import useDragAndDrop from '@hooks/useDragAndDrop'; import useThemeStyles from '@hooks/useThemeStyles'; +import htmlDivElementRef from '@src/types/utils/htmlDivElementRef'; +import viewRef from '@src/types/utils/viewRef'; import type {DragAndDropContextParams, DragAndDropProviderProps, SetOnDropHandlerCallback} from './types'; const DragAndDropContext = React.createContext({}); @@ -14,7 +16,7 @@ function shouldAcceptDrop(event: DragEvent): boolean { function DragAndDropProvider({children, isDisabled = false, setIsDraggingOver = () => {}}: DragAndDropProviderProps) { const styles = useThemeStyles(); - const dropZone = useRef(null); + const dropZone = useRef(null); const dropZoneID = useRef(Str.guid('drag-n-drop')); const onDropHandler = useRef(() => {}); @@ -23,7 +25,7 @@ function DragAndDropProvider({children, isDisabled = false, setIsDraggingOver = }, []); const {isDraggingOver} = useDragAndDrop({ - dropZone, + dropZone: htmlDivElementRef(dropZone), onDrop: onDropHandler.current, shouldAcceptDrop, isDisabled, @@ -38,7 +40,7 @@ function DragAndDropProvider({children, isDisabled = false, setIsDraggingOver = return ( {isDraggingOver && ( diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 7d2f99f49593..d37e00727fa6 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -69,7 +69,7 @@ function KYCWall({ walletTerms, shouldShowPersonalBankAccountOption = false, }: BaseKYCWallProps) { - const anchorRef = useRef(null); + const anchorRef = useRef(null); const transferBalanceButtonRef = useRef(null); const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); diff --git a/src/hooks/useDragAndDrop/index.native.ts b/src/hooks/useDragAndDrop/index.native.ts new file mode 100644 index 000000000000..4c1d0479aebb --- /dev/null +++ b/src/hooks/useDragAndDrop/index.native.ts @@ -0,0 +1,3 @@ +const useDragAndDrop = () => {}; + +export default useDragAndDrop; diff --git a/src/hooks/useDragAndDrop.ts b/src/hooks/useDragAndDrop/index.ts similarity index 87% rename from src/hooks/useDragAndDrop.ts rename to src/hooks/useDragAndDrop/index.ts index 7644d7bba5f0..b278039ee020 100644 --- a/src/hooks/useDragAndDrop.ts +++ b/src/hooks/useDragAndDrop/index.ts @@ -1,8 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; -import type React from 'react'; import {useCallback, useContext, useEffect, useRef, useState} from 'react'; -import type {View} from 'react-native'; import {PopoverContext} from '@components/PopoverProvider'; +import type UseDragAndDrop from './types'; const COPY_DROP_EFFECT = 'copy'; const NONE_DROP_EFFECT = 'none'; @@ -11,22 +10,10 @@ const DRAG_OVER_EVENT = 'dragover'; const DRAG_LEAVE_EVENT = 'dragleave'; const DROP_EVENT = 'drop'; -type DragAndDropParams = { - dropZone: React.MutableRefObject; - onDrop?: (event: DragEvent) => void; - shouldAllowDrop?: boolean; - isDisabled?: boolean; - shouldAcceptDrop?: (event: DragEvent) => boolean; -}; - -type DragAndDropOptions = { - isDraggingOver: boolean; -}; - /** * @param dropZone – ref to the dropZone component */ -export default function useDragAndDrop({dropZone, onDrop = () => {}, shouldAllowDrop = true, isDisabled = false, shouldAcceptDrop = () => true}: DragAndDropParams): DragAndDropOptions { +const useDragAndDrop: UseDragAndDrop = ({dropZone, onDrop = () => {}, shouldAllowDrop = true, isDisabled = false, shouldAcceptDrop = () => true}) => { const isFocused = useIsFocused(); const [isDraggingOver, setIsDraggingOver] = useState(false); const {close: closePopover} = useContext(PopoverContext); @@ -111,7 +98,7 @@ export default function useDragAndDrop({dropZone, onDrop = () => {}, shouldAllow return; } - const dropZoneRef = dropZone.current as HTMLDivElement; + const dropZoneRef = dropZone.current; // Note that the dragover event needs to be called with `event.preventDefault` in order for the drop event to be fired: // https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome @@ -133,4 +120,6 @@ export default function useDragAndDrop({dropZone, onDrop = () => {}, shouldAllow }, [dropZone, dropZoneDragHandler]); return {isDraggingOver}; -} +}; + +export default useDragAndDrop; diff --git a/src/hooks/useDragAndDrop/types.ts b/src/hooks/useDragAndDrop/types.ts new file mode 100644 index 000000000000..4600b43d97bc --- /dev/null +++ b/src/hooks/useDragAndDrop/types.ts @@ -0,0 +1,15 @@ +type DragAndDropParams = { + dropZone: React.MutableRefObject; + onDrop?: (event: DragEvent) => void; + shouldAllowDrop?: boolean; + isDisabled?: boolean; + shouldAcceptDrop?: (event: DragEvent) => boolean; +}; + +type DragAndDropOptions = { + isDraggingOver: boolean; +}; + +type UseDragAndDrop = (params: DragAndDropParams) => DragAndDropOptions; + +export default UseDragAndDrop; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 619dcc54a031..69f3223e46f7 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -87,7 +87,6 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, const prevPersonalDetails = usePrevious(personalDetails); const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const dropdownButtonRef = useRef(null); const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const isLoading = useMemo( () => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), @@ -515,7 +514,6 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} onPress={() => null} options={getBulkActionsButtonOptions()} - buttonRef={dropdownButtonRef} style={[isSmallScreenWidth && styles.flexGrow1]} /> ) : ( diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 80394623dba8..4a04e326e04b 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -1,6 +1,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -61,7 +61,6 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat const theme = useTheme(); const {translate} = useLocalize(); const [selectedCategories, setSelectedCategories] = useState>({}); - const dropdownButtonRef = useRef(null); const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); @@ -208,7 +207,6 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat return ( null} shouldAlwaysShowDropdownMenu pressOnEnter diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index ea35526bcdfa..556358b14619 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -1,6 +1,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -54,7 +54,6 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) const [selectedDistanceRates, setSelectedDistanceRates] = useState([]); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - const dropdownButtonRef = useRef(null); const policyID = route.params.policyID; const isFocused = useIsFocused(); @@ -257,7 +256,6 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} onPress={() => null} options={getBulkActionsButtonOptions()} - buttonRef={dropdownButtonRef} style={[isSmallScreenWidth && styles.flexGrow1]} wrapperStyle={styles.w100} /> diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index fafee7b3b74d..e56154fa7a2a 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -1,7 +1,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import lodashSortBy from 'lodash/sortBy'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -68,7 +68,6 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { const theme = useTheme(); const {translate} = useLocalize(); const [selectedTags, setSelectedTags] = useState>({}); - const dropdownButtonRef = useRef(null); const [deleteTagsConfirmModalVisible, setDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); @@ -221,7 +220,6 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { return ( null} shouldAlwaysShowDropdownMenu pressOnEnter diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index a61c8ae72734..f742336f606e 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -1,6 +1,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; @@ -53,7 +53,6 @@ function WorkspaceTaxesPage({ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const defaultExternalID = policy?.taxRates?.defaultExternalID; const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; - const dropdownButtonRef = useRef(null); const isFocused = useIsFocused(); const fetchTaxes = useCallback(() => { @@ -226,7 +225,6 @@ function WorkspaceTaxesPage({ ) : ( - buttonRef={dropdownButtonRef} onPress={() => {}} options={dropdownMenuOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} diff --git a/src/types/utils/htmlDivElementRef.ts b/src/types/utils/htmlDivElementRef.ts new file mode 100644 index 000000000000..9a20bb87e32e --- /dev/null +++ b/src/types/utils/htmlDivElementRef.ts @@ -0,0 +1,5 @@ +import type {View} from 'react-native'; + +const htmlDivElementRef = (ref: React.RefObject) => ref as React.RefObject; + +export default htmlDivElementRef; From bf3331f571ff4dc72e138fa20f7418db5ac5639d Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 12:50:27 +0200 Subject: [PATCH 05/11] Apply early feedback --- src/components/DragAndDrop/NoDropZone/index.tsx | 2 +- src/hooks/useDragAndDrop/index.native.ts | 4 +++- src/hooks/useDragAndDrop/types.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/DragAndDrop/NoDropZone/index.tsx b/src/components/DragAndDrop/NoDropZone/index.tsx index ee3f2a6a34a1..3438bfff7c05 100644 --- a/src/components/DragAndDrop/NoDropZone/index.tsx +++ b/src/components/DragAndDrop/NoDropZone/index.tsx @@ -8,7 +8,7 @@ import type NoDropZoneProps from './types'; function NoDropZone({children}: NoDropZoneProps) { const styles = useThemeStyles(); - const noDropZone = useRef(null); + const noDropZone = useRef(null); useDragAndDrop({ dropZone: htmlDivElementRef(noDropZone), diff --git a/src/hooks/useDragAndDrop/index.native.ts b/src/hooks/useDragAndDrop/index.native.ts index 4c1d0479aebb..6bdc85bf0723 100644 --- a/src/hooks/useDragAndDrop/index.native.ts +++ b/src/hooks/useDragAndDrop/index.native.ts @@ -1,3 +1,5 @@ -const useDragAndDrop = () => {}; +import type UseDragAndDrop from './types'; + +const useDragAndDrop: UseDragAndDrop = () => ({isDraggingOver: false}); export default useDragAndDrop; diff --git a/src/hooks/useDragAndDrop/types.ts b/src/hooks/useDragAndDrop/types.ts index 4600b43d97bc..78859783ee36 100644 --- a/src/hooks/useDragAndDrop/types.ts +++ b/src/hooks/useDragAndDrop/types.ts @@ -6,10 +6,10 @@ type DragAndDropParams = { shouldAcceptDrop?: (event: DragEvent) => boolean; }; -type DragAndDropOptions = { +type DragAndDropResult = { isDraggingOver: boolean; }; -type UseDragAndDrop = (params: DragAndDropParams) => DragAndDropOptions; +type UseDragAndDrop = (params: DragAndDropParams) => DragAndDropResult; export default UseDragAndDrop; From 865710cdabfe3d9d098a077fc7d11a18cb7f77b8 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 12:55:34 +0200 Subject: [PATCH 06/11] Remove used prop --- src/libs/ComposerFocusManager.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index d793c202d243..7dc2296a5e10 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -1,4 +1,3 @@ -import type {View} from 'react-native'; import {TextInput} from 'react-native'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -10,8 +9,6 @@ type InputElement = (TextInput & HTMLElement) | null; type RestoreFocusType = ValueOf | undefined; -type ModalContainer = (View & HTMLElement) | undefined | null; - /** * So far, modern browsers only support the file cancel event in some newer versions * (i.e., Chrome: 113+ / Firefox: 91+ / Safari 16.4+), and there is no standard feature detection method available. @@ -89,7 +86,7 @@ function getId() { /** * Save the focus state when opening the modal. */ -function saveFocusState(id: ModalId, isInUploadingContext = false, shouldClearFocusWithType = false, container: ModalContainer = undefined) { +function saveFocusState(id: ModalId, isInUploadingContext = false, shouldClearFocusWithType = false) { const activeInput = getActiveInput(); // For popoverWithoutOverlay, react calls autofocus before useEffect. @@ -108,9 +105,6 @@ function saveFocusState(id: ModalId, isInUploadingContext = false, shouldClearFo }); } - if (container?.contains(input)) { - return; - } focusMap.set(id, {input, isInUploadingContext}); input?.blur(); } From b031b0ad30033ba62a2087e311a957646ee68a2b Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 13:05:44 +0200 Subject: [PATCH 07/11] Further improvements --- src/components/TextInput/TextInputLabel/index.tsx | 7 ++++--- src/libs/focusTextInputAfterAnimation/types.ts | 2 +- src/pages/home/report/ReportActionItem.tsx | 2 +- src/pages/home/report/ReportActionItemMessageEdit.tsx | 2 +- src/types/utils/textRef.ts | 2 +- src/types/utils/viewForwardedRef.ts | 2 +- src/types/utils/viewRef.ts | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx index 8f6d3efdcd8d..e1083b88414b 100644 --- a/src/components/TextInput/TextInputLabel/index.tsx +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -4,14 +4,15 @@ import type {Text} from 'react-native'; import {Animated} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; +import textRef from '@src/types/utils/textRef'; import type TextInputLabelProps from './types'; function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); - const labelRef = useRef(null); + const labelRef = useRef(null); useEffect(() => { - if (!inputId || !labelRef.current) { + if (!inputId || !labelRef.current || !('setAttribute' in labelRef.current)) { return; } labelRef.current.setAttribute('for', inputId); @@ -20,7 +21,7 @@ function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: return ( diff --git a/src/libs/focusTextInputAfterAnimation/types.ts b/src/libs/focusTextInputAfterAnimation/types.ts index bfe29317c1ef..ce5086328b33 100644 --- a/src/libs/focusTextInputAfterAnimation/types.ts +++ b/src/libs/focusTextInputAfterAnimation/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement | undefined, animationLength: number) => void; +type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLElement | undefined, animationLength: number) => void; export default FocusTextInputAfterAnimation; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index efb2d8ba73fb..373409249f79 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -191,7 +191,7 @@ function ReportActionItem({ const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); - const textInputRef = useRef(); + const textInputRef = useRef(); const popoverAnchorRef = useRef>(null); const downloadedPreviews = useRef([]); const prevDraftMessage = usePrevious(draftMessage); diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index aeb870406adf..0632d9032a82 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -81,7 +81,7 @@ const shouldUseForcedSelectionRange = shouldUseEmojiPickerSelection(); function ReportActionItemMessageEdit( {action, draftMessage, reportID, isGroupPolicyReport, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, - forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>, + forwardedRef: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/types/utils/textRef.ts b/src/types/utils/textRef.ts index 668f54d59e36..ecc22c8b013a 100644 --- a/src/types/utils/textRef.ts +++ b/src/types/utils/textRef.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line no-restricted-imports import type {Text} from 'react-native'; -const textRef = (ref: React.RefObject) => ref as React.RefObject; +const textRef = (ref: React.RefObject) => ref as React.RefObject; export default textRef; diff --git a/src/types/utils/viewForwardedRef.ts b/src/types/utils/viewForwardedRef.ts index 87b26bfbab40..bd6201d4321e 100644 --- a/src/types/utils/viewForwardedRef.ts +++ b/src/types/utils/viewForwardedRef.ts @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; -const viewForwardedRef = (ref: ForwardedRef) => ref as ForwardedRef; +const viewForwardedRef = (ref: ForwardedRef) => ref as ForwardedRef; export default viewForwardedRef; diff --git a/src/types/utils/viewRef.ts b/src/types/utils/viewRef.ts index 1fd9d186ba1f..41d7cfac5b62 100644 --- a/src/types/utils/viewRef.ts +++ b/src/types/utils/viewRef.ts @@ -1,5 +1,5 @@ import type {View} from 'react-native'; -const viewRef = (ref: React.RefObject) => ref as React.RefObject; +const viewRef = (ref: React.RefObject) => ref as React.RefObject; export default viewRef; From b7f273e4b01f7b7dc1bfbd22e3f1abccc987e237 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 13:50:57 +0200 Subject: [PATCH 08/11] Update TS_STYLE --- contributingGuides/TS_STYLE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 72d2bdeefba2..3d405dabe46e 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -27,6 +27,7 @@ - [1.20 Hooks instead of HOCs](#hooks-instead-of-hocs) - [1.21 `compose` usage](#compose-usage) - [1.22 Type imports](#type-imports) + - [1.23 Ref types](#ref-types) - [Exception to Rules](#exception-to-rules) - [Communication Items](#communication-items) - [Migration Guidelines](#migration-guidelines) @@ -640,6 +641,21 @@ type Foo = { export someVariable ``` +- [1.23](#ref-types) **Ref types**: Avoid using HTML elements while declaring refs. Please use React Native components where possible. React Native Web handles the references on its own. It also extends React Native components [with Interaction API](https://necolas.github.io/react-native-web/docs/interactions/) which should be used to handle Pointers and Mouse events. If methods Exception of this rule is when we explicitly need to use functions available only in DOM and not in React Native, e.g. `getBoundingClientRect`. Then please declare ref type as `union` of React Native component and HTML element. When passing it to React Native component cast it as soon as possible using utility methods declared in `src/types/utils` + +```ts +import viewRef from '@src/types/utils/viewRef'; + +const ref = useRef(); + +if (ref.current && 'getBoundingClientRect' in ref.current ){ + ref.current.getBoundingClientRect(); +} + + + +``` + ## Exception to Rules Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily. From 3fcabdd6c9d5ce0a73f5d95bb15c1d25ed3ba807 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 24 Apr 2024 14:50:07 +0200 Subject: [PATCH 09/11] Improve docs --- contributingGuides/TS_STYLE.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 3d405dabe46e..e86c6ab43f45 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -641,8 +641,16 @@ type Foo = { export someVariable ``` -- [1.23](#ref-types) **Ref types**: Avoid using HTML elements while declaring refs. Please use React Native components where possible. React Native Web handles the references on its own. It also extends React Native components [with Interaction API](https://necolas.github.io/react-native-web/docs/interactions/) which should be used to handle Pointers and Mouse events. If methods Exception of this rule is when we explicitly need to use functions available only in DOM and not in React Native, e.g. `getBoundingClientRect`. Then please declare ref type as `union` of React Native component and HTML element. When passing it to React Native component cast it as soon as possible using utility methods declared in `src/types/utils` +- [1.23](#ref-types) **Ref types**: Avoid using HTML elements while declaring refs. Please use React Native components where possible. React Native Web handles the references on its own. It also extends React Native components [with Interaction API](https://necolas.github.io/react-native-web/docs/interactions/) which should be used to handle Pointers and Mouse events. Exception of this rule is when we explicitly need to use functions available only in DOM and not in React Native, e.g. `getBoundingClientRect`. Then please declare ref type as `union` of React Native component and HTML element. When passing it to React Native component cast it as soon as possible using utility methods declared in `src/types/utils` +Normal usage: +```ts +const ref = useRef(); + + {#DO SOMETHING}}> +``` + +Exceptional usage where DOM methods are necessary: ```ts import viewRef from '@src/types/utils/viewRef'; @@ -652,8 +660,7 @@ if (ref.current && 'getBoundingClientRect' in ref.current ){ ref.current.getBoundingClientRect(); } - - + {#DO SOMETHING}}> ``` ## Exception to Rules From 5284ca8cac6b4d1ae11b3d9bffd691ef202bfdc6 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Thu, 25 Apr 2024 08:39:56 +0200 Subject: [PATCH 10/11] Style improvements --- contributingGuides/TS_STYLE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index e86c6ab43f45..9b7326c90905 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -641,22 +641,22 @@ type Foo = { export someVariable ``` -- [1.23](#ref-types) **Ref types**: Avoid using HTML elements while declaring refs. Please use React Native components where possible. React Native Web handles the references on its own. It also extends React Native components [with Interaction API](https://necolas.github.io/react-native-web/docs/interactions/) which should be used to handle Pointers and Mouse events. Exception of this rule is when we explicitly need to use functions available only in DOM and not in React Native, e.g. `getBoundingClientRect`. Then please declare ref type as `union` of React Native component and HTML element. When passing it to React Native component cast it as soon as possible using utility methods declared in `src/types/utils` +- [1.23](#ref-types) **Ref types**: Avoid using HTML elements while declaring refs. Please use React Native components where possible. React Native Web handles the references on its own. It also extends React Native components with [Interaction API](https://necolas.github.io/react-native-web/docs/interactions/) which should be used to handle Pointer and Mouse events. Exception of this rule is when we explicitly need to use functions available only in DOM and not in React Native, e.g. `getBoundingClientRect`. Then please declare ref type as `union` of React Native component and HTML element. When passing it to React Native component assert it as soon as possible using utility methods declared in `src/types/utils`. Normal usage: -```ts +```tsx const ref = useRef(); {#DO SOMETHING}}> ``` Exceptional usage where DOM methods are necessary: -```ts +```tsx import viewRef from '@src/types/utils/viewRef'; const ref = useRef(); -if (ref.current && 'getBoundingClientRect' in ref.current ){ +if (ref.current && 'getBoundingClientRect' in ref.current ) { ref.current.getBoundingClientRect(); } From f26dae7c900132a734f3ed819fd3201f1f2b4e69 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski <56261019+jnowakow@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:09:28 +0200 Subject: [PATCH 11/11] Update contributingGuides/TS_STYLE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- contributingGuides/TS_STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 9b7326c90905..d407019cbed6 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -656,7 +656,7 @@ import viewRef from '@src/types/utils/viewRef'; const ref = useRef(); -if (ref.current && 'getBoundingClientRect' in ref.current ) { +if (ref.current && 'getBoundingClientRect' in ref.current) { ref.current.getBoundingClientRect(); }