diff --git a/src/CONST.ts b/src/CONST.ts index 93a17ac3c70d..dd676a9465e9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2643,6 +2643,9 @@ const CONST = { HTTPS: 'https', PUSHER: 'pusher', }, + EVENTS: { + SCROLLING: 'scrolling', + }, } as const; export default CONST; diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js index 9fb2e3bc7306..d483a06d6aaf 100644 --- a/src/components/Hoverable/hoverablePropTypes.js +++ b/src/components/Hoverable/hoverablePropTypes.js @@ -12,12 +12,16 @@ const propTypes = { /** Function that executes when the mouse leaves the children. */ onHoverOut: PropTypes.func, + + /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ + shouldHandleScroll: PropTypes.bool, }; const defaultProps = { disabled: false, onHoverIn: () => {}, onHoverOut: () => {}, + shouldHandleScroll: false, }; export {propTypes, defaultProps}; diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js index 0b560703a069..5da41f1388fb 100644 --- a/src/components/Hoverable/index.js +++ b/src/components/Hoverable/index.js @@ -1,7 +1,9 @@ import _ from 'underscore'; import React, {Component} from 'react'; +import {DeviceEventEmitter} from 'react-native'; import {propTypes, defaultProps} from './hoverablePropTypes'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; +import CONST from '../../CONST'; /** * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state, @@ -19,12 +21,37 @@ class Hoverable extends Component { isHovered: false, }; + this.isHoveredRef = false; + this.isScrollingRef = false; this.wrapperView = null; } componentDidMount() { document.addEventListener('visibilitychange', this.handleVisibilityChange); document.addEventListener('mouseover', this.checkHover); + + /** + * Only add the scrolling listener if the shouldHandleScroll prop is true + * and the scrollingListener is not already set. + */ + if (!this.scrollingListener && this.props.shouldHandleScroll) { + this.scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { + /** + * If user has stopped scrolling and the isHoveredRef is true, then we should update the hover state. + */ + if (!scrolling && this.isHoveredRef) { + this.setState({isHovered: this.isHoveredRef}, this.props.onHoverIn); + } else if (scrolling && this.isHoveredRef) { + /** + * If the user has started scrolling and the isHoveredRef is true, then we should set the hover state to false. + * This is to hide the existing hover and reaction bar. + */ + this.isHoveredRef = false; + this.setState({isHovered: false}, this.props.onHoverOut); + } + this.isScrollingRef = scrolling; + }); + } } componentDidUpdate(prevProps) { @@ -40,6 +67,9 @@ class Hoverable extends Component { componentWillUnmount() { document.removeEventListener('visibilitychange', this.handleVisibilityChange); document.removeEventListener('mouseover', this.checkHover); + if (this.scrollingListener) { + this.scrollingListener.remove(); + } } /** @@ -52,6 +82,17 @@ class Hoverable extends Component { return; } + /** + * Capture whther or not the user is hovering over the component. + * We will use this to determine if we should update the hover state when the user has stopped scrolling. + */ + this.isHoveredRef = isHovered; + + /** + * If the isScrollingRef is true, then the user is scrolling and we should not update the hover state. + */ + if (this.isScrollingRef && this.props.shouldHandleScroll && !this.state.isHovered) return; + if (isHovered !== this.state.isHovered) { this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut); } diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index e8e546385207..74409e9a0fe0 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -1,9 +1,10 @@ -import React, {forwardRef, useEffect} from 'react'; +import React, {forwardRef, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; -import {FlatList, StyleSheet} from 'react-native'; +import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; import _ from 'underscore'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import styles from '../../styles/styles'; +import CONST from '../../CONST'; const propTypes = { /** Passed via forwardRef so we can access the FlatList ref */ @@ -14,6 +15,9 @@ const propTypes = { /** Any additional styles to apply */ // eslint-disable-next-line react/forbid-prop-types contentContainerStyle: PropTypes.any, + + /** Same as for FlatList */ + onScroll: PropTypes.func, }; // This is adapted from https://codesandbox.io/s/react-native-dsyse @@ -22,6 +26,11 @@ function InvertedFlatList(props) { const {innerRef, contentContainerStyle} = props; const listRef = React.createRef(); + const lastScrollEvent = useRef(null); + const scrollEndTimeout = useRef(null); + const updateInProgress = useRef(false); + const eventHandler = useRef(null); + useEffect(() => { if (!_.isFunction(innerRef)) { // eslint-disable-next-line no-param-reassign @@ -29,8 +38,78 @@ function InvertedFlatList(props) { } else { innerRef(listRef); } + + return () => { + if (scrollEndTimeout.current) { + clearTimeout(scrollEndTimeout.current); + } + + if (eventHandler.current) { + eventHandler.current.remove(); + } + }; }, [innerRef, listRef]); + /** + * Emits when the scrolling is in progress. Also, + * invokes the onScroll callback function from props. + * + * @param {Event} event - The onScroll event from the FlatList + */ + const onScroll = (event) => { + props.onScroll(event); + + if (!updateInProgress.current) { + updateInProgress.current = true; + eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, true); + } + }; + + /** + * Emits when the scrolling has ended. + */ + const onScrollEnd = () => { + eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, false); + updateInProgress.current = false; + }; + + /** + * Decides whether the scrolling has ended or not. If it has ended, + * then it calls the onScrollEnd function. Otherwise, it calls the + * onScroll function and pass the event to it. + * + * This is a temporary work around, since react-native-web doesn't + * support onScrollBeginDrag and onScrollEndDrag props for FlatList. + * More info: + * https://github.com/necolas/react-native-web/pull/1305 + * + * This workaround is taken from below and refactored to fit our needs: + * https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185 + * + * @param {Event} event - The onScroll event from the FlatList + */ + const handleScroll = (event) => { + onScroll(event); + const timestamp = Date.now(); + + if (scrollEndTimeout.current) { + clearTimeout(scrollEndTimeout.current); + } + + if (lastScrollEvent.current) { + scrollEndTimeout.current = setTimeout(() => { + if (lastScrollEvent.current !== timestamp) { + return; + } + // Scroll has ended + lastScrollEvent.current = null; + onScrollEnd(); + }, 250); + } + + lastScrollEvent.current = timestamp; + }; + return ( ); } @@ -46,6 +126,7 @@ function InvertedFlatList(props) { InvertedFlatList.propTypes = propTypes; InvertedFlatList.defaultProps = { contentContainerStyle: {}, + onScroll: () => {}, }; export default forwardRef((props, ref) => ( diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 398df07649cf..f60982f52dd4 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -154,6 +154,7 @@ function Tooltip(props) { {children} diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js index af18c4cfa412..2ddf8120d58c 100644 --- a/src/components/Tooltip/tooltipPropTypes.js +++ b/src/components/Tooltip/tooltipPropTypes.js @@ -28,6 +28,9 @@ const propTypes = { /** Unique key of renderTooltipContent to rerender the tooltip when one of the key changes */ renderTooltipContentKey: PropTypes.arrayOf(PropTypes.string), + + /** passes this down to Hoverable component to decide whether to handle the scroll behaviour to show hover once the scroll ends */ + shouldHandleScroll: PropTypes.bool, }; const defaultProps = { @@ -38,6 +41,7 @@ const defaultProps = { numberOfLines: CONST.TOOLTIP_MAX_LINES, renderTooltipContent: undefined, renderTooltipContentKey: [], + shouldHandleScroll: false, }; export {propTypes, defaultProps}; diff --git a/src/components/UserDetailsTooltip/index.web.js b/src/components/UserDetailsTooltip/index.web.js index 5fdae15184ac..1a78459d30a6 100644 --- a/src/components/UserDetailsTooltip/index.web.js +++ b/src/components/UserDetailsTooltip/index.web.js @@ -66,6 +66,7 @@ function UserDetailsTooltip(props) { shiftHorizontal={props.shiftHorizontal} renderTooltipContent={renderTooltipContent} renderTooltipContentKey={[userDisplayName, userLogin]} + shouldHandleScroll > {props.children} diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 7440b28a8b3b..8cf8fd78371d 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -593,7 +593,10 @@ function ReportActionItem(props) { withoutFocusOnSecondaryInteraction accessibilityLabel={props.translate('accessibilityHints.chatMessage')} > - + {(hovered) => ( {props.shouldDisplayNewMarker && }