diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js deleted file mode 100644 index a3aeaa597d7a..000000000000 --- a/src/components/Hoverable/hoverablePropTypes.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Whether to disable the hover action */ - disabled: PropTypes.bool, - - /** Children to wrap with Hoverable. */ - children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, - - /** Function that executes when the mouse moves over the children. */ - onHoverIn: PropTypes.func, - - /** Function that executes when the mouse leaves the children. */ - onHoverOut: PropTypes.func, - - /** Direct pass-through of React's onMouseEnter event. */ - onMouseEnter: PropTypes.func, - - /** Direct pass-through of React's onMouseLeave event. */ - onMouseLeave: 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.native.js b/src/components/Hoverable/index.native.js deleted file mode 100644 index 1c5df276baa6..000000000000 --- a/src/components/Hoverable/index.native.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import {defaultProps, propTypes} from './hoverablePropTypes'; - -/** - * On mobile, there is no concept of hovering, so we return a plain wrapper around the component's children, - * where the hover state is always false. - * - * @param {Object} props - * @returns {React.Component} - */ -function Hoverable(props) { - const childrenWithHoverState = _.isFunction(props.children) ? props.children(false) : props.children; - return {childrenWithHoverState}; -} - -Hoverable.propTypes = propTypes; -Hoverable.defaultProps = defaultProps; -Hoverable.displayName = 'Hoverable'; - -export default Hoverable; diff --git a/src/components/Hoverable/index.native.tsx b/src/components/Hoverable/index.native.tsx new file mode 100644 index 000000000000..b3d49db9d96e --- /dev/null +++ b/src/components/Hoverable/index.native.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {View} from 'react-native'; +import HoverableProps from './types'; + +/** + * On mobile, there is no concept of hovering, so we return a plain wrapper around the component's children, + * where the hover state is always false. + */ +function Hoverable({children}: HoverableProps) { + const childrenWithHoverState = typeof children === 'function' ? children(false) : children; + + return {childrenWithHoverState}; +} + +Hoverable.displayName = 'Hoverable'; + +export default Hoverable; diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.tsx similarity index 68% rename from src/components/Hoverable/index.js rename to src/components/Hoverable/index.tsx index db752b8845bc..a52dfa296925 100644 --- a/src/components/Hoverable/index.js +++ b/src/components/Hoverable/index.tsx @@ -1,24 +1,23 @@ -import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, MutableRefObject, ReactElement, RefAttributes, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter} from 'react-native'; -import _ from 'underscore'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import {defaultProps, propTypes} from './hoverablePropTypes'; +import HoverableProps from './types'; /** * Maps the children of a Hoverable component to * - a function that is called with the parameter * - the child itself if it is the only child - * @param {Array|Function|ReactNode} children - The children to map. - * @param {Object} callbackParam - The parameter to pass to the children function. - * @returns {ReactNode} The mapped children. + * @param children The children to map. + * @param callbackParam The parameter to pass to the children function. + * @returns The mapped children. */ -function mapChildren(children, callbackParam) { - if (_.isArray(children) && children.length === 1) { +function mapChildren(children: ((isHovered: boolean) => ReactElement) | ReactElement | ReactElement[], callbackParam: boolean): ReactElement & RefAttributes { + if (Array.isArray(children)) { return children[0]; } - if (_.isFunction(children)) { + if (typeof children === 'function') { return children(callbackParam); } @@ -27,21 +26,18 @@ function mapChildren(children, callbackParam) { /** * Assigns a ref to an element, either by setting the current property of the ref object or by calling the ref function - * @param {Object|Function} ref - The ref object or function. - * @param {HTMLElement} el - The element to assign the ref to. + * @param ref The ref object or function. + * @param element The element to assign the ref to. */ -function assignRef(ref, el) { +function assignRef(ref: ((instance: HTMLElement | null) => void) | MutableRefObject, element: HTMLElement) { if (!ref) { return; } - - if (_.has(ref, 'current')) { + if (typeof ref === 'function') { + ref(element); + } else if (ref?.current) { // eslint-disable-next-line no-param-reassign - ref.current = el; - } - - if (_.isFunction(ref)) { - ref(el); + ref.current = element; } } @@ -50,16 +46,18 @@ function assignRef(ref, el) { * because nesting Pressables causes issues where the hovered state of the child cannot be easily propagated to the * parent. https://github.com/necolas/react-native-web/issues/1875 */ - -const Hoverable = React.forwardRef(({disabled, onHoverIn, onHoverOut, onMouseEnter, onMouseLeave, children, shouldHandleScroll}, outerRef) => { +function Hoverable( + {disabled = false, onHoverIn = () => {}, onHoverOut = () => {}, onMouseEnter = () => {}, onMouseLeave = () => {}, children, shouldHandleScroll = false}: HoverableProps, + outerRef: ForwardedRef, +) { const [isHovered, setIsHovered] = useState(false); const isScrolling = useRef(false); const isHoveredRef = useRef(false); - const ref = useRef(null); + const ref = useRef(null); const updateIsHoveredOnScrolling = useCallback( - (hovered) => { + (hovered: boolean) => { if (disabled) { return; } @@ -106,14 +104,14 @@ const Hoverable = React.forwardRef(({disabled, onHoverIn, onHoverOut, onMouseEnt * Checks the hover state of a component and updates it based on the event target. * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger, * such as when an element is removed before the mouseleave event is triggered. - * @param {Event} e - The hover event object. + * @param event The hover event object. */ - const unsetHoveredIfOutside = (e) => { + const unsetHoveredIfOutside = (event: MouseEvent) => { if (!ref.current || !isHovered) { return; } - if (ref.current.contains(e.target)) { + if (ref.current.contains(event.target as Node)) { return; } @@ -145,50 +143,44 @@ const Hoverable = React.forwardRef(({disabled, onHoverIn, onHoverOut, onMouseEnt }, [disabled, isHovered, onHoverIn, onHoverOut]); // Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child. - useImperativeHandle(outerRef, () => ref.current, []); + useImperativeHandle(outerRef, () => ref.current, []); const child = useMemo(() => React.Children.only(mapChildren(children, isHovered)), [children, isHovered]); const enableHoveredOnMouseEnter = useCallback( - (el) => { + (event: MouseEvent) => { updateIsHoveredOnScrolling(true); + onMouseEnter(event); - if (_.isFunction(onMouseEnter)) { - onMouseEnter(el); - } - - if (_.isFunction(child.props.onMouseEnter)) { - child.props.onMouseEnter(el); + if (typeof child.props.onMouseEnter === 'function') { + child.props.onMouseEnter(event); } }, [child.props, onMouseEnter, updateIsHoveredOnScrolling], ); const disableHoveredOnMouseLeave = useCallback( - (el) => { + (event: MouseEvent) => { updateIsHoveredOnScrolling(false); + onMouseLeave(event); - if (_.isFunction(onMouseLeave)) { - onMouseLeave(el); - } - - if (_.isFunction(child.props.onMouseLeave)) { - child.props.onMouseLeave(el); + if (typeof child.props.onMouseLeave === 'function') { + child.props.onMouseLeave(event); } }, [child.props, onMouseLeave, updateIsHoveredOnScrolling], ); const disableHoveredOnBlur = useCallback( - (el) => { + (event: MouseEvent) => { // Check if the blur event occurred due to clicking outside the element // and the wrapperView contains the element that caused the blur and reset isHovered - if (!ref.current.contains(el.target) && !ref.current.contains(el.relatedTarget)) { + if (!ref.current?.contains(event.target as Node) && !ref.current?.contains(event.relatedTarget as Node)) { setIsHovered(false); } - if (_.isFunction(child.props.onBlur)) { - child.props.onBlur(el); + if (typeof child.props.onBlur === 'function') { + child.props.onBlur(event); } }, [child.props], @@ -196,9 +188,11 @@ const Hoverable = React.forwardRef(({disabled, onHoverIn, onHoverOut, onMouseEnt // We need to access the ref of a children from both parent and current component // So we pass it to current ref and assign it once again to the child ref prop - const hijackRef = (el) => { + const hijackRef = (el: HTMLElement) => { ref.current = el; - assignRef(child.ref, el); + if (child.ref) { + assignRef(child.ref, el); + } }; if (!DeviceCapabilities.hasHoverSupport()) { @@ -213,10 +207,6 @@ const Hoverable = React.forwardRef(({disabled, onHoverIn, onHoverOut, onMouseEnt onMouseLeave: disableHoveredOnMouseLeave, onBlur: disableHoveredOnBlur, }); -}); - -Hoverable.propTypes = propTypes; -Hoverable.defaultProps = defaultProps; -Hoverable.displayName = 'Hoverable'; +} -export default Hoverable; +export default forwardRef(Hoverable); diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts new file mode 100644 index 000000000000..430b865f50c5 --- /dev/null +++ b/src/components/Hoverable/types.ts @@ -0,0 +1,26 @@ +import {ReactElement} from 'react'; + +type HoverableProps = { + /** Children to wrap with Hoverable. */ + children: ((isHovered: boolean) => ReactElement) | ReactElement; + + /** Whether to disable the hover action */ + disabled?: boolean; + + /** Function that executes when the mouse moves over the children. */ + onHoverIn?: () => void; + + /** Function that executes when the mouse leaves the children. */ + onHoverOut?: () => void; + + /** Direct pass-through of React's onMouseEnter event. */ + onMouseEnter?: (event: MouseEvent) => void; + + /** Direct pass-through of React's onMouseLeave event. */ + onMouseLeave?: (event: MouseEvent) => void; + + /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ + shouldHandleScroll?: boolean; +}; + +export default HoverableProps;