diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 0fed37ffea8b..d43f5ce67398 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -1,4 +1,4 @@ -import type {ViewStyle} from 'react-native'; +import type {View, ViewStyle} from 'react-native'; import type {ModalProps} from 'react-native-modal'; import type {ValueOf} from 'type-fest'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; @@ -23,7 +23,7 @@ type BaseModalProps = WindowDimensionsProps & shouldSetModalVisibility?: boolean; /** Callback method fired when the user requests to close the modal */ - onClose: (ref?: React.RefObject) => void; + onClose: (ref?: React.RefObject) => void; /** State that determines whether to display the modal or not */ isVisible: boolean; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 3d1f95822e6a..87a09895d50f 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -1,8 +1,11 @@ +import type {RefObject} from 'react'; +import type {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import type {PopoverAnchorPosition} from '@components/Modal/types'; import type BaseModalProps from '@components/Modal/types'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import type CONST from '@src/CONST'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; type AnchorAlignment = { /** The horizontal anchor alignment of the popover */ @@ -17,34 +20,32 @@ type PopoverDimensions = { height: number; }; -type PopoverProps = BaseModalProps & { - /** The anchor position of the popover */ - anchorPosition?: PopoverAnchorPosition; +type PopoverProps = BaseModalProps & + ChildrenProps & { + /** The anchor position of the popover */ + anchorPosition?: PopoverAnchorPosition; - /** The anchor alignment of the popover */ - anchorAlignment: AnchorAlignment; + /** The anchor alignment of the popover */ + anchorAlignment?: AnchorAlignment; - /** The anchor ref of the popover */ - anchorRef: React.RefObject; + /** The anchor ref of the popover */ + anchorRef: RefObject; - /** Whether disable the animations */ - disableAnimation: boolean; + /** Whether disable the animations */ + disableAnimation?: boolean; - /** Whether we don't want to show overlay */ - withoutOverlay: boolean; + /** Whether we don't want to show overlay */ + withoutOverlay: boolean; - /** The dimensions of the popover */ - popoverDimensions?: PopoverDimensions; + /** The dimensions of the popover */ + popoverDimensions?: PopoverDimensions; - /** The ref of the popover */ - withoutOverlayRef?: React.RefObject; + /** The ref of the popover */ + withoutOverlayRef?: RefObject; - /** Whether we want to show the popover on the right side of the screen */ - fromSidebarMediumScreen?: boolean; - - /** The popover children */ - children: React.ReactNode; -}; + /** Whether we want to show the popover on the right side of the screen */ + fromSidebarMediumScreen?: boolean; + }; type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps; diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx index b50b04289813..b1a6ebb0c5c0 100644 --- a/src/components/PopoverProvider/index.tsx +++ b/src/components/PopoverProvider/index.tsx @@ -1,18 +1,27 @@ -import React from 'react'; +import type {RefObject} from 'react'; +import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {View} from 'react-native'; import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types'; -const PopoverContext = React.createContext({ +const PopoverContext = createContext({ onOpen: () => {}, popover: {}, close: () => {}, isOpen: false, }); +function elementContains(ref: RefObject | undefined, target: EventTarget | null) { + if (ref?.current && 'contains' in ref?.current && ref?.current?.contains(target as Node)) { + return true; + } + return false; +} + function PopoverContextProvider(props: PopoverContextProps) { - const [isOpen, setIsOpen] = React.useState(false); - const activePopoverRef = React.useRef(null); + const [isOpen, setIsOpen] = useState(false); + const activePopoverRef = useRef(null); - const closePopover = React.useCallback((anchorRef?: React.RefObject) => { + const closePopover = useCallback((anchorRef?: RefObject) => { if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) { return; } @@ -25,10 +34,9 @@ function PopoverContextProvider(props: PopoverContextProps) { setIsOpen(false); }, []); - React.useEffect(() => { + useEffect(() => { const listener = (e: Event) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (activePopoverRef.current?.ref?.current?.contains(e.target as Node) || activePopoverRef.current?.anchorRef?.current?.contains(e.target as Node)) { + if (elementContains(activePopoverRef.current?.ref, e.target) || elementContains(activePopoverRef.current?.anchorRef, e.target)) { return; } const ref = activePopoverRef.current?.anchorRef; @@ -40,9 +48,9 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = (e: Event) => { - if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { + if (elementContains(activePopoverRef.current?.ref, e.target)) { return; } closePopover(); @@ -53,7 +61,7 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = (e: KeyboardEvent) => { if (e.key !== 'Escape') { return; @@ -66,7 +74,7 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = () => { if (document.hasFocus()) { return; @@ -79,9 +87,9 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = (e: Event) => { - if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { + if (elementContains(activePopoverRef.current?.ref, e.target)) { return; } @@ -93,7 +101,7 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - const onOpen = React.useCallback( + const onOpen = useCallback( (popoverParams: AnchorRef) => { if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) { closePopover(activePopoverRef.current.anchorRef); @@ -107,7 +115,7 @@ function PopoverContextProvider(props: PopoverContextProps) { [closePopover], ); - const contextValue = React.useMemo( + const contextValue = useMemo( () => ({ onOpen, close: closePopover, diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts index ffd0087cd5ff..49705d7ea7a8 100644 --- a/src/components/PopoverProvider/types.ts +++ b/src/components/PopoverProvider/types.ts @@ -1,18 +1,21 @@ +import type {ReactNode, RefObject} from 'react'; +import type {View} from 'react-native'; + type PopoverContextProps = { - children: React.ReactNode; + children: ReactNode; }; type PopoverContextValue = { onOpen?: (popoverParams: AnchorRef) => void; popover?: AnchorRef | Record | null; - close: (anchorRef?: React.RefObject) => void; + close: (anchorRef?: RefObject) => void; isOpen: boolean; }; type AnchorRef = { - ref: React.RefObject; - close: (anchorRef?: React.RefObject) => void; - anchorRef: React.RefObject; + ref: RefObject; + close: (anchorRef?: RefObject) => void; + anchorRef: RefObject; onOpenCallback?: () => void; onCloseCallback?: () => void; }; diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index 6aed275bd2dc..fe47a2e8cefe 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -8,6 +8,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Modal from '@userActions/Modal'; +import viewRef from '@src/types/utils/viewRef'; import type PopoverWithoutOverlayProps from './types'; function PopoverWithoutOverlay( @@ -119,7 +120,7 @@ function PopoverWithoutOverlay( return ( ; + anchorRef: RefObject; /** A react-native-animatable animation timing for the modal display animation */ animationInTiming?: number; @@ -22,7 +23,7 @@ type PopoverWithoutOverlayProps = ChildrenProps & disableAnimation?: boolean; /** The ref of the popover */ - withoutOverlayRef: React.RefObject; + withoutOverlayRef: RefObject; }; export default PopoverWithoutOverlayProps; diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx index 1b711633ed3b..5f32240aca9b 100644 --- a/src/components/ProcessMoneyRequestHoldMenu.tsx +++ b/src/components/ProcessMoneyRequestHoldMenu.tsx @@ -1,3 +1,4 @@ +import type {RefObject} from 'react'; import React from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -27,7 +28,7 @@ type ProcessMoneyRequestHoldMenuProps = { anchorAlignment: AnchorAlignment; /** The anchor ref of the popover menu */ - anchorRef: React.RefObject; + anchorRef: RefObject; }; function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorAlignment, anchorRef}: ProcessMoneyRequestHoldMenuProps) { diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx similarity index 71% rename from src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js rename to src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx index 54e7309ee48b..9f615cef525d 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx @@ -1,7 +1,5 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Dimensions, View} from 'react-native'; -import _ from 'underscore'; import GoogleMeetIcon from '@assets/images/google-meet.svg'; import ZoomIcon from '@assets/images/zoom-icon.svg'; import Icon from '@components/Icon'; @@ -10,37 +8,34 @@ import MenuItem from '@components/MenuItem'; import Popover from '@components/Popover'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Link from '@userActions/Link'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; -import {defaultProps, propTypes as videoChatButtonAndMenuPropTypes} from './videoChatButtonAndMenuPropTypes'; +import type VideoChatButtonAndMenuProps from './types'; -const propTypes = { +type BaseVideoChatButtonAndMenuProps = VideoChatButtonAndMenuProps & { /** Link to open when user wants to create a new google meet meeting */ - googleMeetURL: PropTypes.string.isRequired, - - ...videoChatButtonAndMenuPropTypes, - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, + googleMeetURL: string; }; -function BaseVideoChatButtonAndMenu(props) { +function BaseVideoChatButtonAndMenu({googleMeetURL, isConcierge = false, guideCalendarLink}: BaseVideoChatButtonAndMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); const [isVideoChatMenuActive, setIsVideoChatMenuActive] = useState(false); const [videoChatIconPosition, setVideoChatIconPosition] = useState({x: 0, y: 0}); - const videoChatIconWrapperRef = useRef(null); - const videoChatButtonRef = useRef(null); + const videoChatIconWrapperRef = useRef(null); + const videoChatButtonRef = useRef(null); const menuItemData = [ { icon: ZoomIcon, - text: props.translate('videoChatButtonAndMenu.zoom'), + text: translate('videoChatButtonAndMenu.zoom'), onPress: () => { setIsVideoChatMenuActive(false); Link.openExternalLink(CONST.NEW_ZOOM_MEETING_URL); @@ -48,10 +43,10 @@ function BaseVideoChatButtonAndMenu(props) { }, { icon: GoogleMeetIcon, - text: props.translate('videoChatButtonAndMenu.googleMeet'), + text: translate('videoChatButtonAndMenu.googleMeet'), onPress: () => { setIsVideoChatMenuActive(false); - Link.openExternalLink(props.googleMeetURL); + Link.openExternalLink(googleMeetURL); }, }, ]; @@ -87,22 +82,22 @@ function BaseVideoChatButtonAndMenu(props) { ref={videoChatIconWrapperRef} onLayout={measureVideoChatIconPosition} > - + { // Drop focus to avoid blue focus ring. - videoChatButtonRef.current.blur(); + videoChatButtonRef.current?.blur(); // If this is the Concierge chat, we'll open the modal for requesting a setup call instead - if (props.isConcierge && props.guideCalendarLink) { - Link.openExternalLink(props.guideCalendarLink); + if (isConcierge && guideCalendarLink) { + Link.openExternalLink(guideCalendarLink); return; } setIsVideoChatMenuActive((previousVal) => !previousVal); })} style={styles.touchableButtonImage} - accessibilityLabel={props.translate('videoChatButtonAndMenu.tooltip')} + accessibilityLabel={translate('videoChatButtonAndMenu.tooltip')} role={CONST.ROLE.BUTTON} > - - {_.map(menuItemData, ({icon, text, onPress}) => ( + + {menuItemData.map(({icon, text, onPress}) => (