From 2eda49d79f8a8df6134dbc27e66ff776dd8f4b02 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 6 Mar 2023 20:19:28 +0500 Subject: [PATCH 01/32] feat: refactored popover component to work without overlay --- src/components/PopoverWithMeasuredContent.js | 2 +- src/components/PopoverWithoutOverlay/index.js | 96 +++++++++++++++++++ .../PopoverWithoutOverlay/popoverPropTypes.js | 36 +++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/components/PopoverWithoutOverlay/index.js create mode 100644 src/components/PopoverWithoutOverlay/popoverPropTypes.js diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js index 2285f857968a..5cf045842551 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.js @@ -3,7 +3,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; -import Popover from './Popover'; +import Popover from './PopoverWithoutOverlay'; import {propTypes as popoverPropTypes, defaultProps as defaultPopoverProps} from './Popover/popoverPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import CONST from '../CONST'; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js new file mode 100644 index 000000000000..8f3c9868b968 --- /dev/null +++ b/src/components/PopoverWithoutOverlay/index.js @@ -0,0 +1,96 @@ +import React from 'react'; +import {Pressable, View} from 'react-native'; +import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {propTypes, defaultProps} from './popoverPropTypes'; +import styles from '../../styles/styles'; +import * as StyleUtils from '../../styles/StyleUtils'; +import getModalStyles from '../../styles/getModalStyles'; +import withWindowDimensions from '../withWindowDimensions'; + +/* + * This is a convenience wrapper around the Modal component for a responsive Popover. + * On small screen widths, it uses BottomDocked modal type, and a Popover type on wide screen widths. + */ +const Popover = (props) => { + const ref = React.useRef(null); + + const { + modalStyle, + modalContainerStyle, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaMargin, + shouldAddTopSafeAreaPadding, + shouldAddBottomSafeAreaPadding, + } = getModalStyles( + 'popover', + { + windowWidth: props.windowWidth, + windowHeight: props.windowHeight, + isSmallScreenWidth: props.isSmallScreenWidth, + }, + props.anchorPosition, + props.innerContainerStyle, + props.outerStyle, + ); + + React.useEffect(() => { + const listener = (e) => { + if (!ref.current || ref.current.contains(e.target)) { + return; + } + props.onClose(); + }; + document.addEventListener('click', listener, true); + return () => { + document.removeEventListener('click', listener, true); + }; + }, []); + + return props.isVisible ? ( + + + {(insets) => { + const { + paddingTop: safeAreaPaddingTop, + paddingBottom: safeAreaPaddingBottom, + paddingLeft: safeAreaPaddingLeft, + paddingRight: safeAreaPaddingRight, + } = StyleUtils.getSafeAreaPadding(insets); + + const modalPaddingStyles = StyleUtils.getModalPaddingStyles({ + safeAreaPaddingTop, + safeAreaPaddingBottom, + safeAreaPaddingLeft, + safeAreaPaddingRight, + shouldAddBottomSafeAreaMargin, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaPadding, + modalContainerStyleMarginTop: modalContainerStyle.marginTop, + modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, + modalContainerStylePaddingTop: modalContainerStyle.paddingTop, + modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + }); + return ( + + {props.children} + + ); + }} + + + ) : null; +}; + +Popover.propTypes = propTypes; +Popover.defaultProps = defaultProps; +Popover.displayName = 'Popover'; + +export default withWindowDimensions(Popover); diff --git a/src/components/PopoverWithoutOverlay/popoverPropTypes.js b/src/components/PopoverWithoutOverlay/popoverPropTypes.js new file mode 100644 index 000000000000..839ab68204d0 --- /dev/null +++ b/src/components/PopoverWithoutOverlay/popoverPropTypes.js @@ -0,0 +1,36 @@ +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import {propTypes as modalPropTypes, defaultProps as defaultModalProps} from '../Modal/modalPropTypes'; +import CONST from '../../CONST'; + +const propTypes = { + ...(_.omit(modalPropTypes, ['type', 'popoverAnchorPosition'])), + + /** The anchor position of the popover */ + anchorPosition: PropTypes.shape({ + top: PropTypes.number, + right: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + }), + + /** A react-native-animatable animation timing for the modal display animation. */ + animationInTiming: PropTypes.number, + + /** Whether disable the animations */ + disableAnimation: PropTypes.bool, +}; + +const defaultProps = { + ...(_.omit(defaultModalProps, ['type', 'popoverAnchorPosition'])), + + animationIn: 'fadeIn', + animationOut: 'fadeOut', + animationInTiming: CONST.ANIMATED_TRANSITION, + + // Anchor position is optional only because it is not relevant on mobile + anchorPosition: {}, + disableAnimation: true, +}; + +export {propTypes, defaultProps}; From 6c39e703385b85f6556f199e1263f871bd14cf4b Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 7 Mar 2023 01:37:43 +0500 Subject: [PATCH 02/32] fix: added onShowModal and onHideModal callbacks --- src/components/PopoverWithMeasuredContent.js | 10 +++++++--- src/components/PopoverWithoutOverlay/index.js | 19 +++++++++++++++++++ .../PopoverReportActionContextMenu.js | 1 + 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js index 5cf045842551..2eed88fbeb44 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.js @@ -3,7 +3,8 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; -import Popover from './PopoverWithoutOverlay'; +import PopoverWithoutOverlay from './PopoverWithoutOverlay'; +import Popover from './Popover'; import {propTypes as popoverPropTypes, defaultProps as defaultPopoverProps} from './Popover/popoverPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import CONST from '../CONST'; @@ -40,6 +41,7 @@ const propTypes = { }), ...windowDimensionsPropTypes, + withoutOverlay: PropTypes.bool, }; const defaultProps = { @@ -54,6 +56,7 @@ const defaultProps = { height: 0, width: 0, }, + withoutOverlay: false, }; /** @@ -181,15 +184,16 @@ class PopoverWithMeasuredContent extends Component { left: adjustedAnchorPosition.left + horizontalShift, top: adjustedAnchorPosition.top + verticalShift, }; + const PopoverComponentToUse = this.props.withoutOverlay ? PopoverWithoutOverlay : Popover; return this.state.isContentMeasured ? ( - {this.props.measureContent()} - + ) : ( /* diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 8f3c9868b968..019be3085ca9 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -1,6 +1,7 @@ import React from 'react'; import {Pressable, View} from 'react-native'; import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import * as Modal from '../../libs/actions/Modal'; import {propTypes, defaultProps} from './popoverPropTypes'; import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; @@ -46,6 +47,24 @@ const Popover = (props) => { }; }, []); + React.useEffect(() => { + Modal.setCloseModal(props.onClose); + + return () => { + Modal.setCloseModal(null); + }; + }, []); + + React.useEffect(() => { + if (props.isVisible) { + props.onModalShow(); + } else { + props.onModalHide(); + } + Modal.willAlertModalBecomeVisible(props.isVisible); + Modal.setCloseModal(props.isVisible ? props.onClose : null); + }, [props.isVisible]); + return props.isVisible ? ( diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index dd637193b0e9..64b2e2162572 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -304,6 +304,7 @@ class PopoverReportActionContextMenu extends React.Component { measureContent={this.measureContent} shouldSetModalVisibility={false} fullscreen + withoutOverlay > Date: Fri, 10 Mar 2023 13:41:16 +0500 Subject: [PATCH 03/32] feat: added more listeners --- src/components/PopoverWithoutOverlay/index.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 019be3085ca9..31deb7308e23 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -47,6 +47,42 @@ const Popover = (props) => { }; }, []); + React.useEffect(() => { + const listener = (e) => { + if (e.key !== 'Escape') { + return; + } + props.onClose(); + }; + document.addEventListener('keydown', listener); + return () => { + document.removeEventListener('keydown', listener); + }; + }, []); + + React.useEffect(() => { + const listener = () => { + if (document.hasFocus()) { + return; + } + props.onClose(); + }; + document.addEventListener('visibilitychange', listener); + return () => { + document.removeEventListener('visibilitychange', listener); + }; + }, []); + + React.useEffect(() => { + const listener = () => { + props.onClose(); + }; + document.addEventListener('contextmenu', listener); + return () => { + document.removeEventListener('contextmenu', listener); + }; + }, []); + React.useEffect(() => { Modal.setCloseModal(props.onClose); From 41226e3349d2298a058ddc3d65b88ccaf2f4d684 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 13 Apr 2023 12:37:05 +0500 Subject: [PATCH 04/32] feat: updated popover without overlay implementation --- src/App.js | 2 + src/components/PopoverProvider.js | 100 ++++++++++++++++++ src/components/PopoverWithoutOverlay/index.js | 63 ++--------- 3 files changed, 108 insertions(+), 57 deletions(-) create mode 100644 src/components/PopoverProvider.js diff --git a/src/App.js b/src/App.js index 581012838973..949b866dca21 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import Expensify from './Expensify'; import {LocaleContextProvider} from './components/withLocalize'; import OnyxProvider from './components/OnyxProvider'; import HTMLEngineProvider from './components/HTMLEngineProvider'; +import PopoverContextProvider from './components/PopoverProvider'; import ComposeProviders from './components/ComposeProviders'; import SafeArea from './components/SafeArea'; import * as Environment from './libs/Environment/Environment'; @@ -43,6 +44,7 @@ const App = () => ( HTMLEngineProvider, WindowDimensionsProvider, KeyboardStateProvider, + PopoverContextProvider, ]} > diff --git a/src/components/PopoverProvider.js b/src/components/PopoverProvider.js new file mode 100644 index 000000000000..c6b0ebb4a4a1 --- /dev/null +++ b/src/components/PopoverProvider.js @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + // eslint-disable-next-line + children: PropTypes.any, +}; + +const defaultProps = {}; + +const PopoverContext = React.createContext({ + onOpen: () => {}, + popover: {}, +}); + +const PopoverContextProvider = (props) => { + const [isOpen, setIsOpen] = React.useState(false); + const activePopoverRef = React.useRef(null); + + const closePopover = () => { + if (!activePopoverRef.current) { + return; + } + activePopoverRef.current.close(); + activePopoverRef.current = null; + setIsOpen(false); + }; + + React.useEffect(() => { + const listener = (e) => { + if ( + !activePopoverRef.current + || !activePopoverRef.current.ref + || activePopoverRef.current.ref.current.contains(e.target) + ) { + return; + } + closePopover(); + }; + document.addEventListener('click', listener, true); + return () => { + document.removeEventListener('click', listener, true); + }; + }, []); + + React.useEffect(() => { + const listener = (e) => { + if (e.key !== 'Escape') { + return; + } + closePopover(); + }; + document.addEventListener('keydown', listener); + return () => { + document.removeEventListener('keydown', listener); + }; + }, []); + + React.useEffect(() => { + const listener = () => { + if (document.hasFocus()) { + return; + } + closePopover(); + }; + document.addEventListener('visibilitychange', listener); + return () => { + document.removeEventListener('visibilitychange', listener); + }; + }, []); + + const onOpen = (popoverParams) => { + if (activePopoverRef.current) { + closePopover(); + } + activePopoverRef.current = popoverParams; + setIsOpen(true); + }; + return ( + + {props.children} + + ); +}; + +PopoverContextProvider.defaultProps = defaultProps; +PopoverContextProvider.propTypes = propTypes; +PopoverContextProvider.displayName = 'PopoverContextProvider'; + +export default PopoverContextProvider; + +export { + PopoverContext, +}; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 31deb7308e23..ed32edff380c 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -1,6 +1,7 @@ import React from 'react'; import {Pressable, View} from 'react-native'; import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {PopoverContext} from '../PopoverProvider'; import * as Modal from '../../libs/actions/Modal'; import {propTypes, defaultProps} from './popoverPropTypes'; import styles from '../../styles/styles'; @@ -14,6 +15,7 @@ import withWindowDimensions from '../withWindowDimensions'; */ const Popover = (props) => { const ref = React.useRef(null); + const {onOpen} = React.useContext(PopoverContext); const { modalStyle, @@ -34,66 +36,13 @@ const Popover = (props) => { props.outerStyle, ); - React.useEffect(() => { - const listener = (e) => { - if (!ref.current || ref.current.contains(e.target)) { - return; - } - props.onClose(); - }; - document.addEventListener('click', listener, true); - return () => { - document.removeEventListener('click', listener, true); - }; - }, []); - - React.useEffect(() => { - const listener = (e) => { - if (e.key !== 'Escape') { - return; - } - props.onClose(); - }; - document.addEventListener('keydown', listener); - return () => { - document.removeEventListener('keydown', listener); - }; - }, []); - - React.useEffect(() => { - const listener = () => { - if (document.hasFocus()) { - return; - } - props.onClose(); - }; - document.addEventListener('visibilitychange', listener); - return () => { - document.removeEventListener('visibilitychange', listener); - }; - }, []); - - React.useEffect(() => { - const listener = () => { - props.onClose(); - }; - document.addEventListener('contextmenu', listener); - return () => { - document.removeEventListener('contextmenu', listener); - }; - }, []); - - React.useEffect(() => { - Modal.setCloseModal(props.onClose); - - return () => { - Modal.setCloseModal(null); - }; - }, []); - React.useEffect(() => { if (props.isVisible) { props.onModalShow(); + onOpen({ + ref, + close: props.onClose, + }); } else { props.onModalHide(); } From 872ad78e0aba4f4431481cadc0b4af6c965c29d2 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 2 May 2023 02:06:46 +0500 Subject: [PATCH 05/32] fix: comments --- src/CONST.js | 1 + src/components/AddPaymentMethodMenu.js | 1 + src/components/AvatarWithImagePicker.js | 1 + src/components/ButtonWithMenu.js | 1 + src/components/Popover/index.js | 7 ++++ src/components/PopoverMenu/index.js | 1 + .../index.js} | 11 ++++++ .../PopoverProvider/index.native.js | 39 +++++++++++++++++++ src/components/PopoverWithMeasuredContent.js | 6 +-- src/components/PopoverWithoutOverlay/index.js | 15 ++++--- src/components/ThreeDotsMenu/index.js | 1 + .../BaseVideoChatButtonAndMenu.js | 1 + src/pages/home/report/ReportActionCompose.js | 1 + .../FloatingActionButtonAndPopover.js | 1 + .../Payments/PaymentsPage/BasePaymentsPage.js | 1 + .../getModalStyles/getBaseModalStyles.js | 24 ++++++++++++ 16 files changed, 100 insertions(+), 12 deletions(-) rename src/components/{PopoverProvider.js => PopoverProvider/index.js} (88%) create mode 100644 src/components/PopoverProvider/index.native.js diff --git a/src/CONST.js b/src/CONST.js index b021c0e85b8f..697ba3812972 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -535,6 +535,7 @@ const CONST = { CENTERED_SMALL: 'centered_small', BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', + POPOVER_WITHOUT_OVERLAY: 'popover_without_overlay', RIGHT_DOCKED: 'right_docked', }, ANCHOR_ORIGIN_VERTICAL: { diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 0a2117d1ca7c..eaf7be388e7b 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -65,6 +65,7 @@ const AddPaymentMethodMenu = props => ( }, ] : []), ]} + withoutOverlay /> ); diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index df19b6c1be44..2f01f4ea85bb 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -293,6 +293,7 @@ class AvatarWithImagePicker extends React.Component { onItemSelected={() => this.setState({isMenuVisible: false})} menuItems={this.createMenuItems(openPicker)} anchorPosition={this.props.anchorPosition} + withoutOverlay /> )} diff --git a/src/components/ButtonWithMenu.js b/src/components/ButtonWithMenu.js index eea6b5ffb835..2d2990e83557 100644 --- a/src/components/ButtonWithMenu.js +++ b/src/components/ButtonWithMenu.js @@ -90,6 +90,7 @@ class ButtonWithMenu extends PureComponent { this.setState({selectedItemIndex: index}); }, }))} + withoutOverlay /> )} diff --git a/src/components/Popover/index.js b/src/components/Popover/index.js index 031d9703d2c4..5858947e2c8e 100644 --- a/src/components/Popover/index.js +++ b/src/components/Popover/index.js @@ -4,6 +4,7 @@ import {propTypes, defaultProps} from './popoverPropTypes'; import CONST from '../../CONST'; import Modal from '../Modal'; import withWindowDimensions from '../withWindowDimensions'; +import PopoverWithoutOverlay from '../PopoverWithoutOverlay'; /* * This is a convenience wrapper around the Modal component for a responsive Popover. @@ -24,6 +25,12 @@ const Popover = (props) => { document.body, ); } + + if (props.withoutOverlay && !props.isSmallScreenWidth) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + } + return ( {!_.isEmpty(this.props.headerText) && ( diff --git a/src/components/PopoverProvider.js b/src/components/PopoverProvider/index.js similarity index 88% rename from src/components/PopoverProvider.js rename to src/components/PopoverProvider/index.js index c6b0ebb4a4a1..b30ac752c6c2 100644 --- a/src/components/PopoverProvider.js +++ b/src/components/PopoverProvider/index.js @@ -11,6 +11,8 @@ const defaultProps = {}; const PopoverContext = React.createContext({ onOpen: () => {}, popover: {}, + close: () => {}, + isOpen: false, }); const PopoverContextProvider = (props) => { @@ -31,6 +33,7 @@ const PopoverContextProvider = (props) => { if ( !activePopoverRef.current || !activePopoverRef.current.ref + || !activePopoverRef.current.ref.current || activePopoverRef.current.ref.current.contains(e.target) ) { return; @@ -69,6 +72,13 @@ const PopoverContextProvider = (props) => { }; }, []); + React.useEffect(() => { + document.addEventListener('scroll', closePopover, true); + return () => { + document.removeEventListener('scroll', closePopover, true); + }; + }, []); + const onOpen = (popoverParams) => { if (activePopoverRef.current) { closePopover(); @@ -80,6 +90,7 @@ const PopoverContextProvider = (props) => { {}, + popover: {}, + close: () => {}, + isOpen: false, +}); + +const PopoverContextProvider = ({children}) => ( + {}, + close: () => {}, + popover: {}, + isOpen: false, + }} + > + {children} + +); + +PopoverContextProvider.defaultProps = defaultProps; +PopoverContextProvider.propTypes = propTypes; +PopoverContextProvider.displayName = 'PopoverContextProvider'; + +export default PopoverContextProvider; + +export { + PopoverContext, +}; diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js index 2eed88fbeb44..85da80d3dbdf 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.js @@ -3,7 +3,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; -import PopoverWithoutOverlay from './PopoverWithoutOverlay'; import Popover from './Popover'; import {propTypes as popoverPropTypes, defaultProps as defaultPopoverProps} from './Popover/popoverPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; @@ -184,16 +183,15 @@ class PopoverWithMeasuredContent extends Component { left: adjustedAnchorPosition.left + horizontalShift, top: adjustedAnchorPosition.top + verticalShift, }; - const PopoverComponentToUse = this.props.withoutOverlay ? PopoverWithoutOverlay : Popover; return this.state.isContentMeasured ? ( - {this.props.measureContent()} - + ) : ( /* diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index ed32edff380c..ae8a513748b0 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -9,14 +9,9 @@ import * as StyleUtils from '../../styles/StyleUtils'; import getModalStyles from '../../styles/getModalStyles'; import withWindowDimensions from '../withWindowDimensions'; -/* - * This is a convenience wrapper around the Modal component for a responsive Popover. - * On small screen widths, it uses BottomDocked modal type, and a Popover type on wide screen widths. - */ const Popover = (props) => { const ref = React.useRef(null); - const {onOpen} = React.useContext(PopoverContext); - + const {onOpen, isOpen, close} = React.useContext(PopoverContext); const { modalStyle, modalContainerStyle, @@ -25,11 +20,11 @@ const Popover = (props) => { shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding, } = getModalStyles( - 'popover', + 'popover_without_overlay', { windowWidth: props.windowWidth, windowHeight: props.windowHeight, - isSmallScreenWidth: props.isSmallScreenWidth, + isSmallScreenWidth: false, }, props.anchorPosition, props.innerContainerStyle, @@ -45,6 +40,9 @@ const Popover = (props) => { }); } else { props.onModalHide(); + if (isOpen) { + close(); + } } Modal.willAlertModalBecomeVisible(props.isVisible); Modal.setCloseModal(props.isVisible ? props.onClose : null); @@ -74,6 +72,7 @@ const Popover = (props) => { modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, modalContainerStylePaddingTop: modalContainerStyle.paddingTop, modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + insets, }); return ( ); diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js index 3d56924125e9..0c78d59a28ee 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js @@ -130,6 +130,7 @@ class BaseVideoChatButtonAndMenu extends Component { left: this.state.videoChatIconPosition.x - 150, top: this.state.videoChatIconPosition.y + 40, }} + withoutOverlay > {_.map(this.menuItemData, ({icon, text, onPress}) => ( diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index e4c88999000b..cb7382f641eb 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -858,6 +858,7 @@ class ReportActionCompose extends React.Component { }, }, ]} + withoutOverlay /> )} diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index ebff5f4e6a51..41bfc90c9b97 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -229,6 +229,7 @@ class FloatingActionButtonAndPopover extends React.Component { }, ] : []), ]} + withoutOverlay /> {!this.state.showConfirmDeleteContent ? ( Date: Sat, 6 May 2023 17:44:44 +0500 Subject: [PATCH 06/32] feat: handled popover migration changes --- src/CONST.js | 14 ++++++ src/components/AddPaymentMethodMenu.js | 1 + src/components/AvatarWithImagePicker.js | 9 +++- src/components/ButtonWithMenu.js | 9 +++- src/components/EmojiPicker/EmojiPicker.js | 2 + .../EmojiPicker/EmojiPickerButton.js | 19 ++++++-- .../MoneyRequestConfirmationList.js | 1 + src/components/Popover/index.js | 2 +- src/components/PopoverMenu/index.js | 1 + src/components/PopoverProvider/index.js | 45 ++++++++++++++----- src/components/PopoverWithoutOverlay/index.js | 14 ++++-- .../PopoverWithoutOverlay/popoverPropTypes.js | 2 + src/components/Reactions/AddReactionBubble.js | 17 +++++-- .../Reactions/MiniQuickEmojiReactions.js | 12 ++++- src/components/SettlementButton.js | 1 + src/components/ThreeDotsMenu/index.js | 3 +- .../BaseVideoChatButtonAndMenu.js | 7 ++- .../PopoverReportActionContextMenu.js | 1 + .../ReactionList/PopoverReactionList.js | 3 ++ src/pages/home/report/ReportActionCompose.js | 5 ++- .../FloatingActionButtonAndPopover.js | 13 +++++- .../Payments/PaymentsPage/BasePaymentsPage.js | 10 +++-- 22 files changed, 153 insertions(+), 38 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 1c97b860fed8..563fe071f96e 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -651,6 +651,20 @@ const CONST = { }, }, + POPOVERS: { + ADD_PAYMENT_METHOD: 'add-payment-method', + IMAGE_PICKER: 'image-picker', + THREE_DOTS: 'three-dots', + ATTACHMENT: 'attachment', + LHN_FLOATING_ACTION_BUTTON: 'lhn-floating-action-button', + VIDEO_CHAT: 'video-chat', + DELETE_PAYMENT_METHOD: 'delete-payment-method', + EMOJI_PICKER: 'emoji-picker', + CONTEXT_MENU: 'context-menu', + EMOJI_REACTION_LIST: 'emoji-reaction-list', + MONEY_REQUEST_CONFIRMATION: 'money-request-confirmation', + SETTLEMENT: 'settlement', + }, PUSHER: { PRIVATE_USER_CHANNEL_PREFIX: 'private-encrypted-user-accountID-', PRIVATE_REPORT_CHANNEL_PREFIX: 'private-report-reportID-', diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index eaf7be388e7b..b8a09c251088 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -66,6 +66,7 @@ const AddPaymentMethodMenu = props => ( ] : []), ]} withoutOverlay + popoverId={CONST.POPOVERS.ADD_PAYMENT_METHOD} /> ); diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 2f01f4ea85bb..b4ed690df1de 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -255,7 +255,13 @@ class AvatarWithImagePicker extends React.Component { return ( this.setState({isMenuVisible: true})} + onPress={(e) => { + if (e.nativeEvent.closedPopoverId === CONST.POPOVERS.IMAGE_PICKER) { + return; + } + + this.setState(prev => ({isMenuVisible: !prev.isMenuVisible})); + }} > @@ -294,6 +300,7 @@ class AvatarWithImagePicker extends React.Component { menuItems={this.createMenuItems(openPicker)} anchorPosition={this.props.anchorPosition} withoutOverlay + popoverId={CONST.POPOVERS.IMAGE_PICKER} /> )} diff --git a/src/components/ButtonWithMenu.js b/src/components/ButtonWithMenu.js index 409252cda1c3..1009e97b6258 100644 --- a/src/components/ButtonWithMenu.js +++ b/src/components/ButtonWithMenu.js @@ -30,6 +30,7 @@ const propTypes = { iconHeight: PropTypes.number, iconDescription: PropTypes.string, })).isRequired, + popoverId: PropTypes.string.isRequired, }; const defaultProps = { @@ -62,8 +63,11 @@ class ButtonWithMenu extends PureComponent { isLoading={this.props.isLoading} isDisabled={this.props.isDisabled} onButtonPress={event => this.props.onPress(event, selectedItem.value)} - onDropdownPress={() => { - this.setMenuVisibility(true); + onDropdownPress={(e) => { + if (e.nativeEvent.closedPopoverId === this.props.popoverId) { + return; + } + this.setMenuVisibility(!this.state.isMenuVisible); }} /> ) : ( @@ -92,6 +96,7 @@ class ButtonWithMenu extends PureComponent { }, }))} withoutOverlay + popoverId={this.props.popoverId} /> )} diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index 369365a8db84..d7844ea29951 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -176,6 +176,8 @@ class EmojiPicker extends React.Component { }} anchorOrigin={this.state.emojiPopoverAnchorOrigin} measureContent={this.measureContent} + withoutOverlay + popoverId={CONST.POPOVERS.EMOJI_PICKER} > { - let emojiPopoverAnchor = null; + const emojiPopoverAnchor = useRef(null); return ( emojiPopoverAnchor = el} + ref={emojiPopoverAnchor} style={({hovered, pressed}) => ([ styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed)), ])} disabled={props.isDisabled} - onPress={() => EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor)} + onPress={(ev) => { + if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER) { + return; + } + + if (!EmojiPickerAction.emojiPickerRef.current.state.isEmojiPickerVisible) { + EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor.current); + } else { + EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); + } + }} nativeID={props.nativeID} > {({hovered, pressed}) => ( diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index a2da62051c5f..b62781179fd1 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -295,6 +295,7 @@ class MoneyRequestConfirmationList extends Component { isDisabled={shouldDisableButton} onPress={(_event, value) => this.confirm(value)} options={this.getSplitOrRequestOptions()} + popoverId={CONST.POPOVERS.MONEY_REQUEST_CONFIRMATION} /> )} > diff --git a/src/components/Popover/index.js b/src/components/Popover/index.js index 5858947e2c8e..7584434d2c06 100644 --- a/src/components/Popover/index.js +++ b/src/components/Popover/index.js @@ -28,7 +28,7 @@ const Popover = (props) => { if (props.withoutOverlay && !props.isSmallScreenWidth) { // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return createPortal(, document.body); } return ( diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js index 66dccf79dfa6..0cabd90ada31 100644 --- a/src/components/PopoverMenu/index.js +++ b/src/components/PopoverMenu/index.js @@ -96,6 +96,7 @@ class PopoverMenu extends PureComponent { disableAnimation={this.props.disableAnimation} fromSidebarMediumScreen={this.props.fromSidebarMediumScreen} withoutOverlay={this.props.withoutOverlay} + popoverId={this.props.popoverId} > {!_.isEmpty(this.props.headerText) && ( diff --git a/src/components/PopoverProvider/index.js b/src/components/PopoverProvider/index.js index b30ac752c6c2..3d70978fcf36 100644 --- a/src/components/PopoverProvider/index.js +++ b/src/components/PopoverProvider/index.js @@ -13,19 +13,27 @@ const PopoverContext = React.createContext({ popover: {}, close: () => {}, isOpen: false, + activePopoverId: '', }); const PopoverContextProvider = (props) => { const [isOpen, setIsOpen] = React.useState(false); + const [activePopoverId, setActivePopoverId] = React.useState(null); const activePopoverRef = React.useRef(null); - const closePopover = () => { - if (!activePopoverRef.current) { + const closePopover = (id) => { + if ( + !activePopoverRef.current + || !activePopoverId + || !id + || id !== activePopoverId + ) { return; } activePopoverRef.current.close(); activePopoverRef.current = null; setIsOpen(false); + setActivePopoverId(null); }; React.useEffect(() => { @@ -38,52 +46,64 @@ const PopoverContextProvider = (props) => { ) { return; } - closePopover(); + closePopover(activePopoverId); + e.closedPopoverId = activePopoverId; }; document.addEventListener('click', listener, true); return () => { document.removeEventListener('click', listener, true); }; - }, []); + }, [activePopoverId]); + + React.useEffect(() => { + const listener = () => { + closePopover(activePopoverId); + }; + document.addEventListener('contextmenu', listener); + return () => { + document.removeEventListener('contextmenu', listener); + }; + }, [activePopoverId]); React.useEffect(() => { const listener = (e) => { if (e.key !== 'Escape') { return; } - closePopover(); + closePopover(activePopoverId); }; document.addEventListener('keydown', listener); return () => { document.removeEventListener('keydown', listener); }; - }, []); + }, [activePopoverId]); React.useEffect(() => { const listener = () => { if (document.hasFocus()) { return; } - closePopover(); + closePopover(activePopoverId); }; document.addEventListener('visibilitychange', listener); return () => { document.removeEventListener('visibilitychange', listener); }; - }, []); + }, [activePopoverId]); React.useEffect(() => { - document.addEventListener('scroll', closePopover, true); + document.addEventListener('scroll', () => closePopover(activePopoverId), true); return () => { - document.removeEventListener('scroll', closePopover, true); + document.removeEventListener('scroll', () => closePopover(activePopoverId), true); }; - }, []); + }, [activePopoverId]); const onOpen = (popoverParams) => { if (activePopoverRef.current) { - closePopover(); + closePopover(activePopoverId); } activePopoverRef.current = popoverParams; + setActivePopoverId(popoverParams.popoverId); setIsOpen(true); }; return ( @@ -93,6 +113,7 @@ const PopoverContextProvider = (props) => { close: closePopover, popover: activePopoverRef.current, isOpen, + activePopoverId, }} > {props.children} diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index ae8a513748b0..27e4b7a3157c 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -11,7 +11,12 @@ import withWindowDimensions from '../withWindowDimensions'; const Popover = (props) => { const ref = React.useRef(null); - const {onOpen, isOpen, close} = React.useContext(PopoverContext); + const { + onOpen, + isOpen, + activePopoverId, + close, + } = React.useContext(PopoverContext); const { modalStyle, modalContainerStyle, @@ -37,15 +42,16 @@ const Popover = (props) => { onOpen({ ref, close: props.onClose, + popoverId: props.popoverId, }); } else { props.onModalHide(); - if (isOpen) { - close(); + if (isOpen && activePopoverId === props.popoverId) { + close(props.popoverId); } } Modal.willAlertModalBecomeVisible(props.isVisible); - Modal.setCloseModal(props.isVisible ? props.onClose : null); + Modal.setCloseModal(props.isVisible ? () => props.onClose(props.popoverId) : null); }, [props.isVisible]); return props.isVisible ? ( diff --git a/src/components/PopoverWithoutOverlay/popoverPropTypes.js b/src/components/PopoverWithoutOverlay/popoverPropTypes.js index 839ab68204d0..c20d0dd6d5ef 100644 --- a/src/components/PopoverWithoutOverlay/popoverPropTypes.js +++ b/src/components/PopoverWithoutOverlay/popoverPropTypes.js @@ -6,6 +6,8 @@ import CONST from '../../CONST'; const propTypes = { ...(_.omit(modalPropTypes, ['type', 'popoverAnchorPosition'])), + popoverId: PropTypes.string.isRequired, + /** The anchor position of the popover */ anchorPosition: PropTypes.shape({ top: PropTypes.number, diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.js index 4581ae3ff42e..069160ddadbd 100644 --- a/src/components/Reactions/AddReactionBubble.js +++ b/src/components/Reactions/AddReactionBubble.js @@ -11,6 +11,7 @@ import getButtonState from '../../libs/getButtonState'; import * as EmojiPickerAction from '../../libs/actions/EmojiPickerAction'; import variables from '../../styles/variables'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; +import CONST from '../../CONST'; const propTypes = { /** Whether it is for context menu so we can modify its style */ @@ -45,7 +46,7 @@ const defaultProps = { const AddReactionBubble = (props) => { const ref = useRef(); - const onPress = () => { + const onPress = (ev) => { const openPicker = (refParam, anchorOrigin) => { EmojiPickerAction.showEmojiPicker( () => {}, @@ -58,10 +59,18 @@ const AddReactionBubble = (props) => { ); }; - if (props.onPressOpenPicker) { - props.onPressOpenPicker(openPicker); + if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER) { + return; + } + + if (!EmojiPickerAction.emojiPickerRef.current.state.isEmojiPickerVisible) { + if (props.onPressOpenPicker) { + props.onPressOpenPicker(openPicker); + } else { + openPicker(); + } } else { - openPicker(); + EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); } }; diff --git a/src/components/Reactions/MiniQuickEmojiReactions.js b/src/components/Reactions/MiniQuickEmojiReactions.js index ca11824639d5..a20c783c0d15 100644 --- a/src/components/Reactions/MiniQuickEmojiReactions.js +++ b/src/components/Reactions/MiniQuickEmojiReactions.js @@ -81,7 +81,17 @@ const MiniQuickEmojiReactions = (props) => { ))} { + if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER) { + return; + } + + if (!EmojiPickerAction.emojiPickerRef.current.state.isEmojiPickerVisible) { + openEmojiPicker(); + } else { + EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); + } + }} isDelayButtonStateComplete={false} tooltipText={props.translate('emojiReactions.addReactionTooltip')} > diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js index 343d2fd6edba..0be5d8f97f4c 100644 --- a/src/components/SettlementButton.js +++ b/src/components/SettlementButton.js @@ -100,6 +100,7 @@ class SettlementButton extends React.Component { this.props.onPress(iouPaymentType); }} options={this.getButtonOptionsFromProps()} + popoverId={CONST.POPOVERS.SETTLEMENT} /> )} diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index 38e9969be67b..ba30de98c3ac 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -10,6 +10,7 @@ import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import Tooltip from '../Tooltip'; import * as Expensicons from '../Icon/Expensicons'; import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes'; +import CONST from '../../CONST'; const propTypes = { ...withLocalizePropTypes, @@ -98,7 +99,7 @@ class ThreeDotsMenu extends Component { anchorPosition={this.props.anchorPosition} onItemSelected={this.hidePopoverMenu} menuItems={this.props.menuItems} - withoutOverlay + popoverId={CONST.POPOVERS.THREE_DOTS} /> ); diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js index 0c78d59a28ee..64c67de12000 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js @@ -103,7 +103,7 @@ class BaseVideoChatButtonAndMenu extends Component { this.videoChatButton = el} - onPress={() => { + onPress={(ev) => { // Drop focus to avoid blue focus ring. this.videoChatButton.blur(); @@ -112,7 +112,9 @@ class BaseVideoChatButtonAndMenu extends Component { Linking.openURL(this.props.guideCalendarLink); return; } - this.setMenuVisibility(true); + if (ev.nativeEvent.closedPopoverId !== CONST.POPOVERS.VIDEO_CHAT) { + this.setMenuVisibility(!this.state.isVideoChatMenuActive); + } }} style={[styles.touchableButtonImage]} > @@ -131,6 +133,7 @@ class BaseVideoChatButtonAndMenu extends Component { top: this.state.videoChatIconPosition.y + 40, }} withoutOverlay + popoverId={CONST.POPOVERS.VIDEO_CHAT} > {_.map(this.menuItemData, ({icon, text, onPress}) => ( diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index 89098b0e7585..982c3baf0f9f 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -311,6 +311,7 @@ class PopoverReportActionContextMenu extends React.Component { shouldSetModalVisibility={false} fullscreen withoutOverlay + popoverId={CONST.POPOVERS.CONTEXT_MENU} > )} diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 41bfc90c9b97..ffb639c44e0b 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -230,12 +230,23 @@ class FloatingActionButtonAndPopover extends React.Component { ] : []), ]} withoutOverlay + popoverId={CONST.POPOVERS.LHN_FLOATING_ACTION_BUTTON} /> { + if (e.nativeEvent.closedPopoverId === CONST.POPOVERS.LHN_FLOATING_ACTION_BUTTON) { + return; + } + + if (this.state.isCreateMenuActive) { + this.hideCreateMenu(); + } else { + this.showCreateMenu(); + } + }} /> ); diff --git a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js index 07378b91a533..5c6edb53e22c 100644 --- a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js +++ b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js @@ -228,10 +228,11 @@ class BasePaymentsPage extends React.Component { this.setPositionAddPaymentMenu(position); return; } - this.setState({ - shouldShowAddPaymentMenu: true, - }); - + if (nativeEvent.closedPopoverId !== CONST.POPOVERS.ADD_PAYMENT_METHOD) { + this.setState(prev => ({ + shouldShowAddPaymentMenu: !prev.shouldShowAddPaymentMenu, + })); + } this.setPositionAddPaymentMenu(position); } @@ -438,6 +439,7 @@ class BasePaymentsPage extends React.Component { right: this.state.anchorPositionRight, }} withoutOverlay + popoverId={CONST.POPOVERS.DELETE_PAYMENT_METHOD} > {!this.state.showConfirmDeleteContent ? ( Date: Sat, 13 May 2023 15:41:44 +0500 Subject: [PATCH 07/32] feat: lint changes" --- src/components/AvatarWithImagePicker.js | 2 +- src/components/PopoverProvider/index.js | 18 +--- .../PopoverProvider/index.native.js | 4 +- src/components/PopoverWithoutOverlay/index.js | 21 ++--- .../PopoverWithoutOverlay/popoverPropTypes.js | 4 +- .../GenericPressable/BaseGenericPressable.js | 83 ++++++++++--------- .../BaseVideoChatButtonAndMenu.js | 2 +- 7 files changed, 60 insertions(+), 74 deletions(-) diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index b445f1d29e5c..a81860bbd508 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -262,7 +262,7 @@ class AvatarWithImagePicker extends React.Component { return; } - this.setState(prev => ({isMenuVisible: !prev.isMenuVisible})); + this.setState((prev) => ({isMenuVisible: !prev.isMenuVisible})); }} > diff --git a/src/components/PopoverProvider/index.js b/src/components/PopoverProvider/index.js index 3d70978fcf36..9e879365ba57 100644 --- a/src/components/PopoverProvider/index.js +++ b/src/components/PopoverProvider/index.js @@ -22,12 +22,7 @@ const PopoverContextProvider = (props) => { const activePopoverRef = React.useRef(null); const closePopover = (id) => { - if ( - !activePopoverRef.current - || !activePopoverId - || !id - || id !== activePopoverId - ) { + if (!activePopoverRef.current || !activePopoverId || !id || id !== activePopoverId) { return; } activePopoverRef.current.close(); @@ -38,12 +33,7 @@ const PopoverContextProvider = (props) => { React.useEffect(() => { const listener = (e) => { - if ( - !activePopoverRef.current - || !activePopoverRef.current.ref - || !activePopoverRef.current.ref.current - || activePopoverRef.current.ref.current.contains(e.target) - ) { + if (!activePopoverRef.current || !activePopoverRef.current.ref || !activePopoverRef.current.ref.current || activePopoverRef.current.ref.current.contains(e.target)) { return; } closePopover(activePopoverId); @@ -127,6 +117,4 @@ PopoverContextProvider.displayName = 'PopoverContextProvider'; export default PopoverContextProvider; -export { - PopoverContext, -}; +export {PopoverContext}; diff --git a/src/components/PopoverProvider/index.native.js b/src/components/PopoverProvider/index.native.js index 57abaa170597..472d32a088c5 100644 --- a/src/components/PopoverProvider/index.native.js +++ b/src/components/PopoverProvider/index.native.js @@ -34,6 +34,4 @@ PopoverContextProvider.displayName = 'PopoverContextProvider'; export default PopoverContextProvider; -export { - PopoverContext, -}; +export {PopoverContext}; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 27e4b7a3157c..ce36e8bf47d0 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -11,20 +11,8 @@ import withWindowDimensions from '../withWindowDimensions'; const Popover = (props) => { const ref = React.useRef(null); - const { - onOpen, - isOpen, - activePopoverId, - close, - } = React.useContext(PopoverContext); - const { - modalStyle, - modalContainerStyle, - shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaMargin, - shouldAddTopSafeAreaPadding, - shouldAddBottomSafeAreaPadding, - } = getModalStyles( + const {onOpen, isOpen, activePopoverId, close} = React.useContext(PopoverContext); + const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles( 'popover_without_overlay', { windowWidth: props.windowWidth, @@ -55,7 +43,10 @@ const Popover = (props) => { }, [props.isVisible]); return props.isVisible ? ( - + {(insets) => { const { diff --git a/src/components/PopoverWithoutOverlay/popoverPropTypes.js b/src/components/PopoverWithoutOverlay/popoverPropTypes.js index c20d0dd6d5ef..ab50c2e03a8c 100644 --- a/src/components/PopoverWithoutOverlay/popoverPropTypes.js +++ b/src/components/PopoverWithoutOverlay/popoverPropTypes.js @@ -4,7 +4,7 @@ import {propTypes as modalPropTypes, defaultProps as defaultModalProps} from '.. import CONST from '../../CONST'; const propTypes = { - ...(_.omit(modalPropTypes, ['type', 'popoverAnchorPosition'])), + ..._.omit(modalPropTypes, ['type', 'popoverAnchorPosition']), popoverId: PropTypes.string.isRequired, @@ -24,7 +24,7 @@ const propTypes = { }; const defaultProps = { - ...(_.omit(defaultModalProps, ['type', 'popoverAnchorPosition'])), + ..._.omit(defaultModalProps, ['type', 'popoverAnchorPosition']), animationIn: 'fadeIn', animationOut: 'fadeOut', diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.js b/src/components/Pressable/GenericPressable/BaseGenericPressable.js index 80664f2d3521..50c933823e63 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.js +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.js @@ -63,45 +63,54 @@ const GenericPressable = forwardRef((props, ref) => { return props.disabled || shouldBeDisabledByScreenReader; }, [isScreenReaderActive, enableInScreenReaderStates, props.disabled]); - const onLongPressHandler = useCallback((event) => { - if (isDisabled) { - return; - } - if (!onLongPress) { - return; - } - if (shouldUseHapticsOnLongPress) { - HapticFeedback.longPress(); - } - if (ref && ref.current) { - ref.current.blur(); - } - onLongPress(event); - - Accessibility.moveAccessibilityFocus(nextFocusRef); - }, [shouldUseHapticsOnLongPress, onLongPress, nextFocusRef, ref, isDisabled]); - - const onPressHandler = useCallback((event) => { - if (isDisabled) { - return; - } - if (shouldUseHapticsOnPress) { - HapticFeedback.press(); - } - if (ref && ref.current) { - ref.current.blur(); - } - onPress(event); + const onLongPressHandler = useCallback( + (event) => { + if (isDisabled) { + return; + } + if (!onLongPress) { + return; + } + if (shouldUseHapticsOnLongPress) { + HapticFeedback.longPress(); + } + if (ref && ref.current) { + ref.current.blur(); + } + onLongPress(event); + + Accessibility.moveAccessibilityFocus(nextFocusRef); + }, + [shouldUseHapticsOnLongPress, onLongPress, nextFocusRef, ref, isDisabled], + ); - Accessibility.moveAccessibilityFocus(nextFocusRef); - }, [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled]); + const onPressHandler = useCallback( + (event) => { + if (isDisabled) { + return; + } + if (shouldUseHapticsOnPress) { + HapticFeedback.press(); + } + if (ref && ref.current) { + ref.current.blur(); + } + onPress(event); + + Accessibility.moveAccessibilityFocus(nextFocusRef); + }, + [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled], + ); - const onKeyPressHandler = useCallback((event) => { - if (event.key !== 'Enter') { - return; - } - onPressHandler(event); - }, [onPressHandler]); + const onKeyPressHandler = useCallback( + (event) => { + if (event.key !== 'Enter') { + return; + } + onPressHandler(event); + }, + [onPressHandler], + ); useEffect(() => { if (!keyboardShortcut) { diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js index 46bd63c65176..e203f9507197 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js @@ -102,7 +102,7 @@ class BaseVideoChatButtonAndMenu extends Component { > this.videoChatButton = el} + ref={(el) => (this.videoChatButton = el)} onPress={(ev) => { // Drop focus to avoid blue focus ring. this.videoChatButton.blur(); From d5330f5eec6e6fb7c9d8f49936242fba4e9ced1b Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sat, 13 May 2023 19:38:50 +0500 Subject: [PATCH 08/32] feat: fix emoji popover consistency --- src/components/EmojiPicker/EmojiPickerButton.js | 7 ++----- src/components/Reactions/AddReactionBubble.js | 2 +- src/components/Reactions/MiniQuickEmojiReactions.js | 2 +- src/libs/actions/EmojiPickerAction.js | 2 +- .../settings/Payments/PaymentsPage/BasePaymentsPage.js | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index 8eb28e953ebc..1c8ffc306a73 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -35,13 +35,10 @@ const EmojiPickerButton = (props) => { > ([ - styles.chatItemEmojiButton, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed)), - ])} + style={({hovered, pressed}) => [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed))]} disabled={props.isDisabled} onPress={(ev) => { - if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER) { + if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER && EmojiPickerAction.emojiPickerRef.current.anchor === emojiPopoverAnchor.current) { return; } diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.js index 70ded656f76f..4ebeef8dab85 100644 --- a/src/components/Reactions/AddReactionBubble.js +++ b/src/components/Reactions/AddReactionBubble.js @@ -59,7 +59,7 @@ const AddReactionBubble = (props) => { ); }; - if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER) { + if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER && EmojiPickerAction.emojiPickerRef.current.anchor === ref.current) { return; } diff --git a/src/components/Reactions/MiniQuickEmojiReactions.js b/src/components/Reactions/MiniQuickEmojiReactions.js index c9ef5d004a48..4c05dbb1903b 100644 --- a/src/components/Reactions/MiniQuickEmojiReactions.js +++ b/src/components/Reactions/MiniQuickEmojiReactions.js @@ -73,7 +73,7 @@ const MiniQuickEmojiReactions = (props) => { { - if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER) { + if (ev.nativeEvent.closedPopoverId === CONST.POPOVERS.EMOJI_PICKER && EmojiPickerAction.emojiPickerRef.current.anchor === ref.current) { return; } diff --git a/src/libs/actions/EmojiPickerAction.js b/src/libs/actions/EmojiPickerAction.js index 6aba8e43bd23..3e15206534ad 100644 --- a/src/libs/actions/EmojiPickerAction.js +++ b/src/libs/actions/EmojiPickerAction.js @@ -15,7 +15,7 @@ function showEmojiPicker(onModalHide = () => {}, onEmojiSelected = () => {}, emo if (!emojiPickerRef.current) { return; } - + emojiPickerRef.current.anchor = emojiPopoverAnchor; emojiPickerRef.current.showEmojiPicker(onModalHide, onEmojiSelected, emojiPopoverAnchor, anchorOrigin, onWillShow); } diff --git a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js index 76b4f2912020..6d91b94f51d4 100644 --- a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js +++ b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js @@ -227,7 +227,7 @@ class BasePaymentsPage extends React.Component { return; } if (nativeEvent.closedPopoverId !== CONST.POPOVERS.ADD_PAYMENT_METHOD) { - this.setState(prev => ({ + this.setState((prev) => ({ shouldShowAddPaymentMenu: !prev.shouldShowAddPaymentMenu, })); } From 138dfcf9d3401091da9ec6e400563711cb8e1d05 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 16 May 2023 17:51:59 +0500 Subject: [PATCH 09/32] feat: use refs for popover anchor identification --- src/CONST.js | 15 -------- src/components/AddPaymentMethodMenu.js | 6 +++- src/components/AvatarWithImagePicker.js | 6 ++-- src/components/Button/index.js | 15 +++++++- src/components/ButtonWithDropdown.js | 5 +++ src/components/ButtonWithMenu.js | 8 +++-- src/components/EmojiPicker/EmojiPicker.js | 15 ++++---- .../EmojiPicker/EmojiPickerButton.js | 5 ++- .../MoneyRequestConfirmationList.js | 1 - src/components/Popover/popoverPropTypes.js | 4 +++ src/components/PopoverMenu/index.js | 2 +- .../PopoverMenu/popoverMenuPropTypes.js | 3 ++ src/components/PopoverProvider/index.js | 36 +++++++++---------- src/components/PopoverWithMeasuredContent.js | 3 ++ src/components/PopoverWithoutOverlay/index.js | 10 +++--- .../PopoverWithoutOverlay/popoverPropTypes.js | 6 ++-- src/components/Reactions/AddReactionBubble.js | 3 +- .../Reactions/MiniQuickEmojiReactions.js | 2 +- src/components/SettlementButton.js | 1 - src/components/ThreeDotsMenu/index.js | 7 ++-- .../BaseVideoChatButtonAndMenu.js | 9 ++--- src/libs/actions/EmojiPickerAction.js | 1 - .../PopoverReportActionContextMenu.js | 3 +- .../ReactionList/PopoverReactionList.js | 11 +++--- src/pages/home/report/ReportActionCompose.js | 10 +++--- .../FloatingActionButtonAndPopover.js | 6 ++-- .../settings/Payments/PaymentMethodList.js | 5 +++ .../Payments/PaymentsPage/BasePaymentsPage.js | 17 +++++---- 28 files changed, 122 insertions(+), 93 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index c858b94f840e..02f55de54324 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -665,21 +665,6 @@ const CONST = { VALIDATE_CODE_FAILED: 'Validate code failed', }, }, - - POPOVERS: { - ADD_PAYMENT_METHOD: 'add-payment-method', - IMAGE_PICKER: 'image-picker', - THREE_DOTS: 'three-dots', - ATTACHMENT: 'attachment', - LHN_FLOATING_ACTION_BUTTON: 'lhn-floating-action-button', - VIDEO_CHAT: 'video-chat', - DELETE_PAYMENT_METHOD: 'delete-payment-method', - EMOJI_PICKER: 'emoji-picker', - CONTEXT_MENU: 'context-menu', - EMOJI_REACTION_LIST: 'emoji-reaction-list', - MONEY_REQUEST_CONFIRMATION: 'money-request-confirmation', - SETTLEMENT: 'settlement', - }, PUSHER: { PRIVATE_USER_CHANNEL_PREFIX: 'private-encrypted-user-accountID-', PRIVATE_REPORT_CHANNEL_PREFIX: 'private-report-reportID-', diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 892e80741440..ca86271c0830 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -28,6 +28,9 @@ const propTypes = { /** List of betas available to current user */ betas: PropTypes.arrayOf(PropTypes.string), + /** Popover anchor ref */ + anchorRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + ...withLocalizePropTypes, }; @@ -36,6 +39,7 @@ const defaultProps = { payPalMeData: {}, shouldShowPaypal: true, betas: [], + anchorRef: () => {}, }; const AddPaymentMethodMenu = (props) => ( @@ -43,6 +47,7 @@ const AddPaymentMethodMenu = (props) => ( isVisible={props.isVisible} onClose={props.onClose} anchorPosition={props.anchorPosition} + anchorRef={props.anchorRef} onItemSelected={() => props.onClose()} menuItems={[ { @@ -72,7 +77,6 @@ const AddPaymentMethodMenu = (props) => ( : []), ]} withoutOverlay - popoverId={CONST.POPOVERS.ADD_PAYMENT_METHOD} /> ); diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index a81860bbd508..f534616a4170 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -99,6 +99,7 @@ class AvatarWithImagePicker extends React.Component { imageUri: '', imageType: '', }; + this.anchorRef = React.createRef(); } componentDidMount() { @@ -257,8 +258,9 @@ class AvatarWithImagePicker extends React.Component { return ( { - if (e.nativeEvent.closedPopoverId === CONST.POPOVERS.IMAGE_PICKER) { + if (e.nativeEvent.anchorRef && e.nativeEvent.anchorRef.current === this.anchorRef.current) { return; } @@ -303,7 +305,7 @@ class AvatarWithImagePicker extends React.Component { menuItems={this.createMenuItems(openPicker)} anchorPosition={this.props.anchorPosition} withoutOverlay - popoverId={CONST.POPOVERS.IMAGE_PICKER} + anchorRef={this.anchorRef} /> )} diff --git a/src/components/Button/index.js b/src/components/Button/index.js index 65107cfe9392..526d115730f9 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.js @@ -109,6 +109,9 @@ const propTypes = { /** Accessibility label for the component */ accessibilityLabel: PropTypes.string, + + /** React ref being forwarded to the Button */ + forwardedRef: PropTypes.func, }; const defaultProps = { @@ -141,6 +144,7 @@ const defaultProps = { shouldEnableHapticFeedback: false, nativeID: '', accessibilityLabel: '', + forwardedRef: () => {}, }; class Button extends Component { @@ -240,6 +244,7 @@ class Button extends Component { render() { return ( { if (e && e.type === 'click') { e.currentTarget.blur(); @@ -303,4 +308,12 @@ class Button extends Component { Button.propTypes = propTypes; Button.defaultProps = defaultProps; -export default compose(withNavigationFallback, withNavigationFocus)(Button); +const ComposedButton = compose(withNavigationFallback, withNavigationFocus)(Button); + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/components/ButtonWithDropdown.js b/src/components/ButtonWithDropdown.js index 61b578488e36..f82c443c4faf 100644 --- a/src/components/ButtonWithDropdown.js +++ b/src/components/ButtonWithDropdown.js @@ -22,6 +22,9 @@ const propTypes = { /** Should the button be disabled */ isDisabled: PropTypes.bool, + + /** Ref to the dropdown button */ + dropdownButtonRef: PropTypes.func, }; const defaultProps = { @@ -29,6 +32,7 @@ const defaultProps = { onDropdownPress: () => {}, isDisabled: false, isLoading: false, + dropdownButtonRef: () => {}, }; const ButtonWithDropdown = (props) => ( @@ -50,6 +54,7 @@ const ButtonWithDropdown = (props) => ( style={[styles.pl0]} onPress={props.onDropdownPress} shouldRemoveLeftBorderRadius + ref={props.dropdownButtonRef} > this.props.onPress(event, selectedItem.value)} onDropdownPress={(e) => { - if (e.nativeEvent.closedPopoverId === this.props.popoverId) { + if (e.nativeEvent.anchorRef && e.nativeEvent.anchorRef.current === this.anchorRef.current) { return; } this.setMenuVisibility(!this.state.isMenuVisible); }} + dropdownButtonRef={this.anchorRef} /> ) : (