Skip to content

Commit

Permalink
Merge pull request #32820 from software-mansion-labs/ts/VideoChatButt…
Browse files Browse the repository at this point in the history
…onAndMenu

[TS migration] Migrate 'VideoChatButtonAndMenu' component to TypeScript
  • Loading branch information
Hayata Suenaga authored Jan 16, 2024
2 parents e5bd1f1 + 10b93ae commit 32d5730
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 100 deletions.
4 changes: 2 additions & 2 deletions src/components/Modal/types.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -23,7 +23,7 @@ type BaseModalProps = WindowDimensionsProps &
shouldSetModalVisibility?: boolean;

/** Callback method fired when the user requests to close the modal */
onClose: (ref?: React.RefObject<HTMLElement>) => void;
onClose: (ref?: React.RefObject<View | HTMLDivElement>) => void;

/** State that determines whether to display the modal or not */
isVisible: boolean;
Expand Down
43 changes: 22 additions & 21 deletions src/components/Popover/types.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -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<HTMLElement>;
/** The anchor ref of the popover */
anchorRef: RefObject<View | HTMLDivElement>;

/** 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<HTMLElement>;
/** The ref of the popover */
withoutOverlayRef?: RefObject<View | HTMLDivElement>;

/** 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;

Expand Down
40 changes: 24 additions & 16 deletions src/components/PopoverProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -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<PopoverContextValue>({
const PopoverContext = createContext<PopoverContextValue>({
onOpen: () => {},
popover: {},
close: () => {},
isOpen: false,
});

function elementContains(ref: RefObject<View | HTMLElement> | 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<AnchorRef | null>(null);
const [isOpen, setIsOpen] = useState(false);
const activePopoverRef = useRef<AnchorRef | null>(null);

const closePopover = React.useCallback((anchorRef?: React.RefObject<HTMLElement>) => {
const closePopover = useCallback((anchorRef?: RefObject<View | HTMLElement>) => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return;
}
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -53,7 +61,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);

React.useEffect(() => {
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return;
Expand All @@ -66,7 +74,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);

React.useEffect(() => {
useEffect(() => {
const listener = () => {
if (document.hasFocus()) {
return;
Expand All @@ -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;
}

Expand All @@ -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);
Expand All @@ -107,7 +115,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
[closePopover],
);

const contextValue = React.useMemo(
const contextValue = useMemo(
() => ({
onOpen,
close: closePopover,
Expand Down
13 changes: 8 additions & 5 deletions src/components/PopoverProvider/types.ts
Original file line number Diff line number Diff line change
@@ -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<string, never> | null;
close: (anchorRef?: React.RefObject<HTMLElement>) => void;
close: (anchorRef?: RefObject<View | HTMLDivElement>) => void;
isOpen: boolean;
};

type AnchorRef = {
ref: React.RefObject<HTMLElement>;
close: (anchorRef?: React.RefObject<HTMLElement>) => void;
anchorRef: React.RefObject<HTMLElement>;
ref: RefObject<View | HTMLDivElement>;
close: (anchorRef?: RefObject<View | HTMLDivElement>) => void;
anchorRef: RefObject<View | HTMLDivElement>;
onOpenCallback?: () => void;
onCloseCallback?: () => void;
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/PopoverWithoutOverlay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -119,7 +120,7 @@ function PopoverWithoutOverlay(
return (
<View
style={[modalStyle, {zIndex: 1}]}
ref={withoutOverlayRef}
ref={viewRef(withoutOverlayRef)}
>
<View
style={{
Expand Down
5 changes: 3 additions & 2 deletions src/components/PopoverWithoutOverlay/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {RefObject} from 'react';
import type {View} from 'react-native';
import type BaseModalProps from '@components/Modal/types';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
Expand All @@ -13,7 +14,7 @@ type PopoverWithoutOverlayProps = ChildrenProps &
};

/** The anchor ref of the popover */
anchorRef: React.RefObject<HTMLElement>;
anchorRef: RefObject<View | HTMLDivElement>;

/** A react-native-animatable animation timing for the modal display animation */
animationInTiming?: number;
Expand All @@ -22,7 +23,7 @@ type PopoverWithoutOverlayProps = ChildrenProps &
disableAnimation?: boolean;

/** The ref of the popover */
withoutOverlayRef: React.RefObject<HTMLElement & View>;
withoutOverlayRef: RefObject<View | HTMLDivElement>;
};

export default PopoverWithoutOverlayProps;
3 changes: 2 additions & 1 deletion src/components/ProcessMoneyRequestHoldMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {RefObject} from 'react';
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
Expand Down Expand Up @@ -27,7 +28,7 @@ type ProcessMoneyRequestHoldMenuProps = {
anchorAlignment: AnchorAlignment;

/** The anchor ref of the popover menu */
anchorRef: React.RefObject<HTMLElement>;
anchorRef: RefObject<View | HTMLDivElement>;
};

function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorAlignment, anchorRef}: ProcessMoneyRequestHoldMenuProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,48 +8,45 @@ 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<View>(null);
const videoChatButtonRef = useRef<View>(null);

const menuItemData = [
{
icon: ZoomIcon,
text: props.translate('videoChatButtonAndMenu.zoom'),
text: translate('videoChatButtonAndMenu.zoom'),
onPress: () => {
setIsVideoChatMenuActive(false);
Link.openExternalLink(CONST.NEW_ZOOM_MEETING_URL);
},
},
{
icon: GoogleMeetIcon,
text: props.translate('videoChatButtonAndMenu.googleMeet'),
text: translate('videoChatButtonAndMenu.googleMeet'),
onPress: () => {
setIsVideoChatMenuActive(false);
Link.openExternalLink(props.googleMeetURL);
Link.openExternalLink(googleMeetURL);
},
},
];
Expand Down Expand Up @@ -87,22 +82,22 @@ function BaseVideoChatButtonAndMenu(props) {
ref={videoChatIconWrapperRef}
onLayout={measureVideoChatIconPosition}
>
<Tooltip text={props.translate('videoChatButtonAndMenu.tooltip')}>
<Tooltip text={translate('videoChatButtonAndMenu.tooltip')}>
<PressableWithoutFeedback
ref={videoChatButtonRef}
onPress={Session.checkIfActionIsAllowed(() => {
// 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}
>
<Icon
Expand All @@ -123,8 +118,8 @@ function BaseVideoChatButtonAndMenu(props) {
withoutOverlay
anchorRef={videoChatButtonRef}
>
<View style={props.isSmallScreenWidth ? {} : styles.pv3}>
{_.map(menuItemData, ({icon, text, onPress}) => (
<View style={isSmallScreenWidth ? {} : styles.pv3}>
{menuItemData.map(({icon, text, onPress}) => (
<MenuItem
wrapperStyle={styles.mr3}
key={text}
Expand All @@ -139,8 +134,6 @@ function BaseVideoChatButtonAndMenu(props) {
);
}

BaseVideoChatButtonAndMenu.propTypes = propTypes;
BaseVideoChatButtonAndMenu.defaultProps = defaultProps;
BaseVideoChatButtonAndMenu.displayName = 'BaseVideoChatButtonAndMenu';

export default compose(withWindowDimensions, withLocalize)(BaseVideoChatButtonAndMenu);
export default BaseVideoChatButtonAndMenu;
Loading

0 comments on commit 32d5730

Please sign in to comment.