diff --git a/src/components/EmojiPicker/CategoryShortcutBar.js b/src/components/EmojiPicker/CategoryShortcutBar.tsx similarity index 51% rename from src/components/EmojiPicker/CategoryShortcutBar.js rename to src/components/EmojiPicker/CategoryShortcutBar.tsx index d9d5e8f12465..25c1ff85f6a3 100644 --- a/src/components/EmojiPicker/CategoryShortcutBar.js +++ b/src/components/EmojiPicker/CategoryShortcutBar.tsx @@ -1,34 +1,26 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {HeaderIndice} from '@libs/EmojiUtils'; import CategoryShortcutButton from './CategoryShortcutButton'; -const propTypes = { +type CategoryShortcutBarProps = { /** The function to call when an emoji is selected */ - onPress: PropTypes.func.isRequired, + onPress: (index: number) => void; /** The emojis consisting emoji code and indices that the icons should link to */ - headerEmojis: PropTypes.arrayOf( - PropTypes.shape({ - code: PropTypes.string.isRequired, - index: PropTypes.number.isRequired, - icon: sourcePropTypes.isRequired, - }), - ).isRequired, + headerEmojis: HeaderIndice[]; }; -function CategoryShortcutBar(props) { +function CategoryShortcutBar({onPress, headerEmojis}: CategoryShortcutBarProps) { const styles = useThemeStyles(); return ( - {_.map(props.headerEmojis, (headerEmoji, i) => ( + {headerEmojis.map((headerEmoji) => ( props.onPress(headerEmoji.index)} - key={`categoryShortcut${i}`} + onPress={() => onPress(headerEmoji.index)} + key={`categoryShortcut${headerEmoji.index}`} code={headerEmoji.code} /> ))} @@ -36,7 +28,6 @@ function CategoryShortcutBar(props) { ); } -CategoryShortcutBar.propTypes = propTypes; CategoryShortcutBar.displayName = 'CategoryShortcutBar'; export default CategoryShortcutBar; diff --git a/src/components/EmojiPicker/CategoryShortcutButton.js b/src/components/EmojiPicker/CategoryShortcutButton.tsx similarity index 76% rename from src/components/EmojiPicker/CategoryShortcutButton.js rename to src/components/EmojiPicker/CategoryShortcutButton.tsx index e7db8161cea1..7ffff218d1fe 100644 --- a/src/components/EmojiPicker/CategoryShortcutButton.js +++ b/src/components/EmojiPicker/CategoryShortcutButton.tsx @@ -1,7 +1,5 @@ -import PropTypes from 'prop-types'; import React, {useState} from 'react'; import Icon from '@components/Icon'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; @@ -11,19 +9,21 @@ import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type IconAsset from '@src/types/utils/IconAsset'; -const propTypes = { +type CategoryShortcutButtonProps = { /** The emoji code of the category header */ - code: PropTypes.string.isRequired, + code: string; /** The icon representation of the category that this button links to */ - icon: sourcePropTypes.isRequired, + icon: IconAsset; /** The function to call when an emoji is selected */ - onPress: PropTypes.func.isRequired, + onPress: () => void; }; -function CategoryShortcutButton(props) { +function CategoryShortcutButton({code, icon, onPress}: CategoryShortcutButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -32,21 +32,21 @@ function CategoryShortcutButton(props) { return ( setIsHighlighted(true)} onHoverOut={() => setIsHighlighted(false)} style={({pressed}) => [StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), styles.categoryShortcutButton, isHighlighted && styles.emojiItemHighlighted]} - accessibilityLabel={`emojiPicker.headers.${props.code}`} + accessibilityLabel={`emojiPicker.headers.${code}`} role={CONST.ROLE.BUTTON} > @@ -54,6 +54,6 @@ function CategoryShortcutButton(props) { ); } -CategoryShortcutButton.propTypes = propTypes; + CategoryShortcutButton.displayName = 'CategoryShortcutButton'; export default React.memo(CategoryShortcutButton); diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.tsx similarity index 72% rename from src/components/EmojiPicker/EmojiPicker.js rename to src/components/EmojiPicker/EmojiPicker.tsx index 410d600dcce2..8c7b4df3264f 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.tsx @@ -1,12 +1,15 @@ -import PropTypes from 'prop-types'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {ForwardedRef, RefObject} from 'react'; import {Dimensions} from 'react-native'; -import _ from 'underscore'; +import type {View} from 'react-native'; +import type {Emoji} from '@assets/emojis/types'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import withViewportOffsetTop from '@components/withViewportOffsetTop'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {AnchorOrigin, EmojiPickerRef, EmojiPopoverAnchor, OnEmojiSelected, OnModalHideValue, OnWillShowPicker} from '@libs/actions/EmojiPickerAction'; import * as Browser from '@libs/Browser'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import CONST from '@src/CONST'; @@ -17,11 +20,11 @@ const DEFAULT_ANCHOR_ORIGIN = { vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }; -const propTypes = { - viewportOffsetTop: PropTypes.number.isRequired, +type EmojiPickerProps = { + viewportOffsetTop: number; }; -const EmojiPicker = forwardRef((props, ref) => { +function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isEmojiPickerVisible, setIsEmojiPickerVisible] = useState(false); @@ -29,17 +32,17 @@ const EmojiPicker = forwardRef((props, ref) => { horizontal: 0, vertical: 0, }); - const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN); - const [activeID, setActiveID] = useState(); - const emojiPopoverAnchorRef = useRef(null); + const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN); + const [activeID, setActiveID] = useState(); + const emojiPopoverAnchorRef = useRef(null); const emojiAnchorDimension = useRef({ width: 0, height: 0, }); const onModalHide = useRef(() => {}); - const onEmojiSelected = useRef(() => {}); - const activeEmoji = useRef(); - const emojiSearchInput = useRef(); + const onEmojiSelected = useRef(() => {}); + const activeEmoji = useRef(); + const emojiSearchInput = useRef(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); /** @@ -50,34 +53,39 @@ const EmojiPicker = forwardRef((props, ref) => { * * Don't directly get the ref from emojiPopoverAnchorRef, instead use getEmojiPopoverAnchor() */ - const getEmojiPopoverAnchor = useCallback(() => emojiPopoverAnchorRef.current || emojiPopoverAnchorRef, []); + const getEmojiPopoverAnchor = useCallback(() => emojiPopoverAnchorRef.current ?? emojiPopoverAnchorRef?.current, []); /** * Show the emoji picker menu. * - * @param {Function} [onModalHideValue=() => {}] - Run a callback when Modal hides. - * @param {Function} [onEmojiSelectedValue=() => {}] - Run a callback when Emoji selected. - * @param {React.MutableRefObject} emojiPopoverAnchorValue - Element to which Popover is anchored - * @param {Object} [anchorOrigin=DEFAULT_ANCHOR_ORIGIN] - Anchor origin for Popover - * @param {Function} [onWillShow] - Run a callback when Popover will show - * @param {String} id - Unique id for EmojiPicker - * @param {String} activeEmojiValue - Selected emoji to be highlighted + * @param [onModalHideValue=() => {}] - Run a callback when Modal hides. + * @param [onEmojiSelectedValue=() => {}] - Run a callback when Emoji selected. + * @param emojiPopoverAnchorValue - Element to which Popover is anchored + * @param [anchorOrigin=DEFAULT_ANCHOR_ORIGIN] - Anchor origin for Popover + * @param [onWillShow] - Run a callback when Popover will show + * @param id - Unique id for EmojiPicker + * @param activeEmojiValue - Selected emoji to be highlighted */ - const showEmojiPicker = (onModalHideValue, onEmojiSelectedValue, emojiPopoverAnchorValue, anchorOrigin, onWillShow, id, activeEmojiValue) => { + const showEmojiPicker = ( + onModalHideValue: OnModalHideValue, + onEmojiSelectedValue: OnEmojiSelected, + emojiPopoverAnchorValue: EmojiPopoverAnchor, + anchorOrigin?: AnchorOrigin, + onWillShow?: OnWillShowPicker, + id?: string, + activeEmojiValue?: string, + ) => { onModalHide.current = onModalHideValue; onEmojiSelected.current = onEmojiSelectedValue; activeEmoji.current = activeEmojiValue; emojiPopoverAnchorRef.current = emojiPopoverAnchorValue; const emojiPopoverAnchor = getEmojiPopoverAnchor(); - if (emojiPopoverAnchor.current && emojiPopoverAnchor.current.blur) { - // Drop focus to avoid blue focus ring. - emojiPopoverAnchor.current.blur(); - } + // Drop focus to avoid blue focus ring. + emojiPopoverAnchor?.current?.blur(); - const anchorOriginValue = anchorOrigin || DEFAULT_ANCHOR_ORIGIN; + const anchorOriginValue = anchorOrigin ?? DEFAULT_ANCHOR_ORIGIN; - calculateAnchorPosition(emojiPopoverAnchor.current, anchorOriginValue).then((value) => { - // eslint-disable-next-line es/no-optional-chaining + calculateAnchorPosition(emojiPopoverAnchor?.current, anchorOriginValue).then((value) => { onWillShow?.(); setIsEmojiPickerVisible(true); setEmojiPopoverAnchorPosition({ @@ -95,10 +103,8 @@ const EmojiPicker = forwardRef((props, ref) => { /** * Hide the emoji picker menu. - * - * @param {Boolean} isNavigating */ - const hideEmojiPicker = (isNavigating) => { + const hideEmojiPicker = (isNavigating?: boolean) => { if (isNavigating) { onModalHide.current = () => {}; } @@ -124,11 +130,8 @@ const EmojiPicker = forwardRef((props, ref) => { /** * Callback for the emoji picker to add whatever emoji is chosen into the main input - * - * @param {String} emoji - * @param {Object} emojiObject */ - const selectEmoji = (emoji, emojiObject) => { + const selectEmoji = (emoji: string, emojiObject: Emoji) => { // Prevent fast click / multiple emoji selection; // The first click will hide the emoji picker by calling the hideEmojiPicker() function if (!isEmojiPickerVisible) { @@ -136,18 +139,15 @@ const EmojiPicker = forwardRef((props, ref) => { } hideEmojiPicker(false); - if (_.isFunction(onEmojiSelected.current)) { + if (typeof onEmojiSelected.current === 'function') { onEmojiSelected.current(emoji, emojiObject); } }; /** * Whether emoji picker is active for the given id. - * - * @param {String} id - * @return {Boolean} */ - const isActive = (id) => Boolean(id) && id === activeID; + const isActive = (id: string) => !!id && id === activeID; const clearActive = () => setActiveID(null); @@ -158,14 +158,14 @@ const EmojiPicker = forwardRef((props, ref) => { useEffect(() => { const emojiPopoverDimensionListener = Dimensions.addEventListener('change', () => { const emojiPopoverAnchor = getEmojiPopoverAnchor(); - if (!emojiPopoverAnchor.current) { + if (!emojiPopoverAnchor?.current) { // In small screen width, the window size change might be due to keyboard open/hide, we should avoid hide EmojiPicker in those cases if (isEmojiPickerVisible && !isSmallScreenWidth) { hideEmojiPicker(); } return; } - calculateAnchorPosition(emojiPopoverAnchor.current, emojiPopoverAnchorOrigin).then((value) => { + calculateAnchorPosition(emojiPopoverAnchor?.current, emojiPopoverAnchorOrigin).then((value) => { setEmojiPopoverAnchorPosition({ horizontal: value.horizontal, vertical: value.vertical, @@ -201,14 +201,14 @@ const EmojiPicker = forwardRef((props, ref) => { vertical: emojiPopoverAnchorPosition.vertical, horizontal: emojiPopoverAnchorPosition.horizontal, }} - anchorRef={getEmojiPopoverAnchor()} + anchorRef={getEmojiPopoverAnchor() as RefObject} withoutOverlay popoverDimensions={{ width: CONST.EMOJI_PICKER_SIZE.WIDTH, height: CONST.EMOJI_PICKER_SIZE.HEIGHT, }} anchorAlignment={emojiPopoverAnchorOrigin} - outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} + outerStyle={StyleUtils.getOuterModalStyle(windowHeight, viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} anchorDimensions={emojiAnchorDimension.current} avoidKeyboard @@ -223,8 +223,7 @@ const EmojiPicker = forwardRef((props, ref) => { /> ); -}); +} -EmojiPicker.propTypes = propTypes; EmojiPicker.displayName = 'EmojiPicker'; -export default withViewportOffsetTop(EmojiPicker); +export default withViewportOffsetTop(forwardRef(EmojiPicker)); diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.tsx similarity index 63% rename from src/components/EmojiPicker/EmojiPickerButton.js rename to src/components/EmojiPicker/EmojiPickerButton.tsx index 438deb7e53d9..6e0944e5a913 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.tsx @@ -1,77 +1,72 @@ -import PropTypes from 'prop-types'; +import {useIsFocused} from '@react-navigation/native'; import React, {memo, useEffect, useRef} from 'react'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withNavigationFocus from '@components/withNavigationFocus'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import getButtonState from '@libs/getButtonState'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import CONST from '@src/CONST'; -const propTypes = { +type EmojiPickerButtonProps = { /** Flag to disable the emoji picker button */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** Id to use for the emoji picker button */ - id: PropTypes.string, + id?: string; /** Unique id for emoji picker */ - emojiPickerID: PropTypes.string, + emojiPickerID?: string; /** Emoji popup anchor offset shift vertical */ - shiftVertical: PropTypes.number, + shiftVertical?: number; - ...withLocalizePropTypes, -}; + onModalHide: EmojiPickerAction.OnModalHideValue; -const defaultProps = { - isDisabled: false, - id: '', - emojiPickerID: '', - shiftVertical: 0, + onEmojiSelected: EmojiPickerAction.OnEmojiSelected; }; -function EmojiPickerButton(props) { +function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shiftVertical = 0, onModalHide, onEmojiSelected}: EmojiPickerButtonProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); + const {translate} = useLocalize(); + const isFocused = useIsFocused(); useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); return ( - + [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed))]} - disabled={props.isDisabled} + disabled={isDisabled} onPress={() => { - if (!props.isFocused) { + if (!isFocused) { return; } - if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { + if (!EmojiPickerAction.emojiPickerRef?.current?.isEmojiPickerVisible) { EmojiPickerAction.showEmojiPicker( - props.onModalHide, - props.onEmojiSelected, + onModalHide, + onEmojiSelected, emojiPopoverAnchor, { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - shiftVertical: props.shiftVertical, + shiftVertical, }, () => {}, - props.emojiPickerID, + emojiPickerID, ); } else { EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); } }} - id={props.id} - accessibilityLabel={props.translate('reportActionCompose.emoji')} + id={id} + accessibilityLabel={translate('reportActionCompose.emoji')} > {({hovered, pressed}) => ( void; + value?: string; + disabled?: boolean; + style: StyleProp; }; -function EmojiPickerButtonDropdown(props) { +function EmojiPickerButtonDropdown( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + {isDisabled = false, onModalHide, onInputChange, value, disabled, style, ...otherProps}: EmojiPickerButtonDropdownProps, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ref: ForwardedRef, +) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); + const {translate} = useLocalize(); + useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); const onPress = () => { if (EmojiPickerAction.isEmojiPickerVisible()) { @@ -36,8 +46,8 @@ function EmojiPickerButtonDropdown(props) { } EmojiPickerAction.showEmojiPicker( - props.onModalHide, - (emoji) => props.onInputChange(emoji), + onModalHide, + (emoji) => onInputChange(emoji), emojiPopoverAnchor, { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, @@ -46,16 +56,16 @@ function EmojiPickerButtonDropdown(props) { }, () => {}, undefined, - props.value, + value, ); }; return ( - + - {props.value || ( - - )} + { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + value || ( + + ) + } - + ( - -)); - -EmojiPickerButtonDropdownWithRef.displayName = 'EmojiPickerButtonDropdownWithRef'; - -export default withLocalize(EmojiPickerButtonDropdownWithRef); +export default React.forwardRef(EmojiPickerButtonDropdown); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.tsx similarity index 62% rename from src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js rename to src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.tsx index a9bc1eeac8b6..8e1112e7844c 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.tsx @@ -1,87 +1,61 @@ import {FlashList} from '@shopify/flash-list'; -import PropTypes from 'prop-types'; +import type {ListRenderItem} from '@shopify/flash-list'; import React, {useMemo} from 'react'; +import type {ForwardedRef} from 'react'; import {StyleSheet, View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar'; import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList'; -import refPropTypes from '@components/refPropTypes'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import stylePropTypes from '@styles/stylePropTypes'; +import type {EmojiPickerList, EmojiPickerListItem, HeaderIndice} from '@libs/EmojiUtils'; import CONST from '@src/CONST'; -const emojiPropTypes = { - /** The code of the item */ - code: PropTypes.string.isRequired, - - /** Whether the item is a header or not */ - header: PropTypes.bool, - - /** Whether the item is a spacer or not */ - spacer: PropTypes.bool, - - /** Types of an emoji - e.g. different skin types */ - types: PropTypes.arrayOf(PropTypes.string), -}; - -const propTypes = { +type BaseEmojiPickerMenuProps = { /** Indicates if the emoji list is filtered or not */ - isFiltered: PropTypes.bool.isRequired, + isFiltered: boolean; /** Array of header emojis */ - headerEmojis: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired, + headerEmojis: HeaderIndice[]; /** Function to scroll to a specific header in the emoji list */ - scrollToHeader: PropTypes.func.isRequired, + scrollToHeader: (headerIndex: number) => void; /** Style to be applied to the list wrapper */ - listWrapperStyle: stylePropTypes, - - /** Reference to the emoji list */ - forwardedRef: refPropTypes, + listWrapperStyle?: StyleProp; /** The data for the emoji list */ - data: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired, + data: EmojiPickerList; /** Function to render each item in the list */ - renderItem: PropTypes.func.isRequired, + renderItem: ListRenderItem; /** Extra data to be passed to the list for re-rendering */ - // eslint-disable-next-line react/forbid-prop-types - extraData: PropTypes.any, + extraData?: Array | ((skinTone: number) => void)>; /** Array of indices for the sticky headers */ - stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), + stickyHeaderIndices?: number[]; /** Whether the list should always bounce vertically */ - alwaysBounceVertical: PropTypes.bool, -}; - -const defaultProps = { - listWrapperStyle: [], - forwardedRef: () => {}, - extraData: [], - stickyHeaderIndices: [], - alwaysBounceVertical: false, + alwaysBounceVertical?: boolean; }; /** * Improves FlashList's recycling when there are different types of items - * @param {Object} item - * @returns {String} */ -const getItemType = (item) => { +const getItemType = (item: EmojiPickerListItem): string | undefined => { // item is undefined only when list is empty if (!item) { return; } - if (item.name) { + if ('name' in item && item.name) { return CONST.EMOJI_PICKER_ITEM_TYPES.EMOJI; } - if (item.header) { + if ('header' in item && item.header) { return CONST.EMOJI_PICKER_ITEM_TYPES.HEADER; } @@ -91,15 +65,11 @@ const getItemType = (item) => { /** * Return a unique key for each emoji item * - * @param {Object} item - * @param {Number} index - * @returns {String} */ -const keyExtractor = (item, index) => `emoji_picker_${item.code}_${index}`; +const keyExtractor = (item: EmojiPickerListItem, index: number): string => `emoji_picker_${item.code}_${index}`; /** * Renders the list empty component - * @returns {React.Component} */ function ListEmptyComponent() { const styles = useThemeStyles(); @@ -108,7 +78,10 @@ function ListEmptyComponent() { return {translate('common.noResultsFound')}; } -function BaseEmojiPickerMenu({headerEmojis, scrollToHeader, isFiltered, listWrapperStyle, forwardedRef, data, renderItem, stickyHeaderIndices, extraData, alwaysBounceVertical}) { +function BaseEmojiPickerMenu( + {headerEmojis, scrollToHeader, isFiltered, listWrapperStyle = [], data, renderItem, stickyHeaderIndices = [], extraData = [], alwaysBounceVertical = false}: BaseEmojiPickerMenuProps, + ref: ForwardedRef>, +) { const styles = useThemeStyles(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -128,7 +101,7 @@ function BaseEmojiPickerMenu({headerEmojis, scrollToHeader, isFiltered, listWrap )} ( - -)); - -BaseEmojiPickerMenuWithRef.displayName = 'BaseEmojiPickerMenuWithRef'; - -export default BaseEmojiPickerMenuWithRef; +export default React.forwardRef(BaseEmojiPickerMenu); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js b/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js deleted file mode 100644 index ae345f6fcf56..000000000000 --- a/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js +++ /dev/null @@ -1,8 +0,0 @@ -import PropTypes from 'prop-types'; - -const emojiPickerMenuPropTypes = { - /** Function to add the selected emoji to the main compose text input */ - onEmojiSelected: PropTypes.func.isRequired, -}; - -export default emojiPickerMenuPropTypes; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx similarity index 72% rename from src/components/EmojiPicker/EmojiPickerMenu/index.native.js rename to src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx index 67f0542fb82c..5b7dda1c7ab1 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx @@ -1,10 +1,13 @@ +import type {ListRenderItem} from '@shopify/flash-list'; +import lodashDebounce from 'lodash/debounce'; import React, {useCallback} from 'react'; +import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import {runOnUI, scrollTo} from 'react-native-reanimated'; -import _ from 'underscore'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -12,13 +15,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as EmojiUtils from '@libs/EmojiUtils'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; -import emojiPickerMenuPropTypes from './emojiPickerMenuPropTypes'; +import type EmojiPickerMenuProps from './types'; import useEmojiPickerMenu from './useEmojiPickerMenu'; -const propTypes = emojiPickerMenuPropTypes; - -function EmojiPickerMenu({onEmojiSelected, activeEmoji}) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); @@ -41,10 +44,8 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}) { /** * Filter the entire list of emojis to only emojis that have the search term in their keywords - * - * @param {String} searchTerm */ - const filterEmojis = _.debounce((searchTerm) => { + const filterEmojis = lodashDebounce((searchTerm: string) => { const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm); if (emojiListRef.current) { @@ -58,11 +59,11 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}) { return; } - setFilteredEmojis(newFilteredEmojiList); + setFilteredEmojis(newFilteredEmojiList ?? []); setHeaderIndices([]); }, 300); - const scrollToHeader = (headerIndex) => { + const scrollToHeader = (headerIndex: number) => { const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; runOnUI(() => { 'worklet'; @@ -75,31 +76,35 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}) { * Given an emoji item object, render a component based on its type. * Items with the code "SPACER" return nothing and are used to fill rows up to 8 * so that the sticky headers function properly. - * - * @param {Object} item - * @returns {*} */ - const renderItem = useCallback( + const renderItem: ListRenderItem = useCallback( ({item, target}) => { - const {code, types} = item; - if (item.spacer) { + const code = item.code; + const types = 'types' in item ? item.types : undefined; + + if ('spacer' in item && item.spacer) { return null; } - if (item.header) { + if ('header' in item && item.header) { return ( - {translate(`emojiPicker.headers.${code}`)} + {translate(`emojiPicker.headers.${code}` as TranslationPaths)} ); } - const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; - const shouldEmojiBeHighlighted = Boolean(activeEmoji) && EmojiUtils.getRemovedSkinToneEmoji(emojiCode) === EmojiUtils.getRemovedSkinToneEmoji(activeEmoji); + const emojiCode = typeof preferredSkinTone === 'number' && types?.[preferredSkinTone] ? types?.[preferredSkinTone] : code; + const shouldEmojiBeHighlighted = !!activeEmoji && EmojiUtils.getRemovedSkinToneEmoji(emojiCode) === EmojiUtils.getRemovedSkinToneEmoji(activeEmoji); return ( onEmojiSelected(emoji, item))} + onPress={singleExecution((emoji) => { + if (!('name' in item)) { + return; + } + onEmojiSelected(emoji, item); + })} emoji={emojiCode} isHighlighted={shouldEmojiBeHighlighted} /> @@ -141,16 +146,4 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}) { } EmojiPickerMenu.displayName = 'EmojiPickerMenu'; -EmojiPickerMenu.propTypes = propTypes; - -const EmojiPickerMenuWithRef = React.forwardRef((props, ref) => ( - -)); - -EmojiPickerMenuWithRef.displayName = 'EmojiPickerMenuWithRef'; - -export default EmojiPickerMenuWithRef; +export default React.forwardRef(EmojiPickerMenu); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx similarity index 84% rename from src/components/EmojiPicker/EmojiPickerMenu/index.js rename to src/components/EmojiPicker/EmojiPickerMenu/index.tsx index 926903be18d1..b8c31a61ecbf 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx @@ -1,12 +1,13 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {ListRenderItem} from '@shopify/flash-list'; +import throttle from 'lodash/throttle'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import {scrollTo} from 'react-native-reanimated'; -import _ from 'underscore'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; @@ -19,23 +20,14 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; -import emojiPickerMenuPropTypes from './emojiPickerMenuPropTypes'; +import type EmojiPickerMenuProps from './types'; import useEmojiPickerMenu from './useEmojiPickerMenu'; -const propTypes = { - /** The ref to the search input (may be null on small screen widths) */ - forwardedRef: PropTypes.func, - ...emojiPickerMenuPropTypes, -}; - -const defaultProps = { - forwardedRef: () => {}, -}; - const throttleTime = Browser.isMobile() ? 200 : 50; -function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { +function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, ref: ForwardedRef) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -58,7 +50,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { } = useEmojiPickerMenu(); // Ref for the emoji search input - const searchInputRef = useRef(null); + const searchInputRef = useRef(null); // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus when open picker for mobile device @@ -78,7 +70,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { }, [arePointerEventsDisabled]); const onFocusedIndexChange = useCallback( - (newIndex) => { + (newIndex: number) => { if (filteredEmojis.length === 0) { return; } @@ -92,8 +84,8 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { } // If the input is not focused and the new index is out of range, focus the input - if (newIndex < 0 && !searchInputRef.current.isFocused() && shouldFocusInputOnScreenFocus) { - searchInputRef.current.focus(); + if (newIndex < 0 && !searchInputRef.current?.isFocused() && shouldFocusInputOnScreenFocus) { + searchInputRef.current?.focus(); } }, [filteredEmojis.length, highlightFirstEmoji, isUsingKeyboardMovement, shouldFocusInputOnScreenFocus], @@ -115,7 +107,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { allowNegativeIndexes: true, }); - const filterEmojis = _.throttle((searchTerm) => { + const filterEmojis = throttle((searchTerm: string) => { const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm); if (emojiListRef.current) { @@ -130,14 +122,14 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { return; } // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky - setFilteredEmojis(newFilteredEmojiList); + setFilteredEmojis(newFilteredEmojiList ?? []); setHeaderIndices([]); setHighlightFirstEmoji(true); setIsUsingKeyboardMovement(false); }, throttleTime); const keyDownHandler = useCallback( - (keyBoardEvent) => { + (keyBoardEvent: KeyboardEvent) => { if (keyBoardEvent.key.startsWith('Arrow')) { if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { keyBoardEvent.preventDefault(); @@ -157,8 +149,10 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { if (!item) { return; } - const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); - onEmojiSelected(emoji, item); + if ('types' in item || 'name' in item) { + const emoji = typeof preferredSkinTone === 'number' && item?.types?.[preferredSkinTone] ? item?.types?.[preferredSkinTone] : item.code; + onEmojiSelected(emoji, item); + } // On web, avoid this Enter default input action; otherwise, it will add a new line in the subsequently focused composer. keyBoardEvent.preventDefault(); // On mWeb, avoid propagating this Enter keystroke to Pressable child component; otherwise, it will trigger the onEmojiSelected callback again. @@ -214,8 +208,8 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default - if (shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { - forwardedRef(searchInputRef.current); + if (shouldFocusInputOnScreenFocus && ref && typeof ref === 'function') { + ref(searchInputRef.current); } setupEventHandlers(); @@ -223,10 +217,10 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { return () => { cleanupEventHandlers(); }; - }, [forwardedRef, shouldFocusInputOnScreenFocus, cleanupEventHandlers, setupEventHandlers]); + }, [ref, shouldFocusInputOnScreenFocus, cleanupEventHandlers, setupEventHandlers]); const scrollToHeader = useCallback( - (headerIndex) => { + (headerIndex: number) => { if (!emojiListRef.current) { return; } @@ -242,35 +236,39 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { * Items with the code "SPACER" return nothing and are used to fill rows up to 8 * so that the sticky headers function properly. * - * @param {Object} item - * @param {Number} index - * @returns {*} */ - const renderItem = useCallback( + const renderItem: ListRenderItem = useCallback( ({item, index, target}) => { - const {code, types} = item; - if (item.spacer) { + const code = item.code; + const types = 'types' in item ? item.types : undefined; + + if ('spacer' in item && item.spacer) { return null; } - if (item.header) { + if ('header' in item && item.header) { return ( - {translate(`emojiPicker.headers.${code}`)} + {translate(`emojiPicker.headers.${code}` as TranslationPaths)} ); } - const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; + const emojiCode = typeof preferredSkinTone === 'number' && types?.[preferredSkinTone] ? types[preferredSkinTone] : code; const isEmojiFocused = index === focusedIndex && isUsingKeyboardMovement; const shouldEmojiBeHighlighted = - (index === focusedIndex && highlightEmoji) || (Boolean(activeEmoji) && EmojiUtils.getRemovedSkinToneEmoji(emojiCode) === EmojiUtils.getRemovedSkinToneEmoji(activeEmoji)); + (index === focusedIndex && highlightEmoji) || (!!activeEmoji && EmojiUtils.getRemovedSkinToneEmoji(emojiCode) === EmojiUtils.getRemovedSkinToneEmoji(activeEmoji)); const shouldFirstEmojiBeHighlighted = index === 0 && highlightFirstEmoji; return ( onEmojiSelected(emoji, item))} + onPress={singleExecution((emoji) => { + if (!('name' in item)) { + return; + } + onEmojiSelected(emoji, item); + })} onHoverIn={() => { setHighlightEmoji(false); setHighlightFirstEmoji(false); @@ -347,17 +345,4 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected, activeEmoji}) { } EmojiPickerMenu.displayName = 'EmojiPickerMenu'; -EmojiPickerMenu.propTypes = propTypes; -EmojiPickerMenu.defaultProps = defaultProps; - -const EmojiPickerMenuWithRef = React.forwardRef((props, ref) => ( - -)); - -EmojiPickerMenuWithRef.displayName = 'EmojiPickerMenuWithRef'; - -export default EmojiPickerMenuWithRef; +export default React.forwardRef(EmojiPickerMenu); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/types.ts b/src/components/EmojiPicker/EmojiPickerMenu/types.ts new file mode 100644 index 000000000000..b821cc89aeea --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/types.ts @@ -0,0 +1,10 @@ +import type {Emoji} from '@assets/emojis/types'; + +type EmojiPickerMenuProps = { + /** Function to add the selected emoji to the main compose text input */ + onEmojiSelected: (emoji: string, emojiObject: Emoji) => void; + + activeEmoji?: string; +}; + +export default EmojiPickerMenuProps; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts similarity index 85% rename from src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js rename to src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts index c6f9f601f4df..7cbfc4f1e2bc 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts @@ -1,6 +1,6 @@ +import type {FlashList} from '@shopify/flash-list'; import {useCallback, useEffect, useMemo, useState} from 'react'; import {useAnimatedRef} from 'react-native-reanimated'; -import _ from 'underscore'; import emojis from '@assets/emojis'; import {useFrequentlyUsedEmojis} from '@components/OnyxProvider'; import useLocalize from '@hooks/useLocalize'; @@ -10,14 +10,14 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as EmojiUtils from '@libs/EmojiUtils'; const useEmojiPickerMenu = () => { - const emojiListRef = useAnimatedRef(); + const emojiListRef = useAnimatedRef>(); const frequentlyUsedEmojis = useFrequentlyUsedEmojis(); // eslint-disable-next-line react-hooks/exhaustive-deps const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]); const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]); - const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]); + const headerRowIndices = useMemo(() => headerEmojis.map((headerEmoji) => headerEmoji.index), [headerEmojis]); const spacersIndexes = useMemo(() => EmojiUtils.getSpacersIndexes(allEmojis), [allEmojis]); - const [filteredEmojis, setFilteredEmojis] = useState(allEmojis); + const [filteredEmojis, setFilteredEmojis] = useState(allEmojis); const [headerIndices, setHeaderIndices] = useState(headerRowIndices); const isListFiltered = allEmojis.length !== filteredEmojis.length; const {preferredLocale} = useLocalize(); @@ -41,15 +41,13 @@ const useEmojiPickerMenu = () => { /** * Suggest emojis based on the search term - * @param {String} searchTerm - * @returns {[String, Array]} */ const suggestEmojis = useCallback( - (searchTerm) => { + (searchTerm: string) => { const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); const emojisSuggestions = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length); - return [normalizedSearchTerm, emojisSuggestions]; + return [normalizedSearchTerm, emojisSuggestions] as const; }, [allEmojis, preferredLocale], ); diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js deleted file mode 100644 index 52d4a0db8812..000000000000 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js +++ /dev/null @@ -1,133 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {PureComponent} from 'react'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import Text from '@components/Text'; -import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import * as Browser from '@libs/Browser'; -import getButtonState from '@libs/getButtonState'; -import CONST from '@src/CONST'; - -const propTypes = { - /** The unicode that is used to display the emoji */ - emoji: PropTypes.string.isRequired, - - /** The function to call when an emoji is selected */ - onPress: PropTypes.func.isRequired, - - /** Handles what to do when we hover over this item with our cursor */ - onHoverIn: PropTypes.func, - - /** Handles what to do when the hover is out */ - onHoverOut: PropTypes.func, - - /** Handles what to do when the pressable is focused */ - onFocus: PropTypes.func, - - /** Handles what to do when the pressable is blurred */ - onBlur: PropTypes.func, - - /** Whether this menu item is currently focused or not */ - isFocused: PropTypes.bool, - - /** Whether the menu item should be highlighted or not */ - isHighlighted: PropTypes.bool, - - ...withThemeStylesPropTypes, - ...withStyleUtilsPropTypes, -}; - -class EmojiPickerMenuItem extends PureComponent { - constructor(props) { - super(props); - - this.ref = null; - this.focusAndScroll = this.focusAndScroll.bind(this); - this.state = { - isHovered: false, - }; - } - - componentDidMount() { - if (!this.props.isFocused) { - return; - } - this.focusAndScroll(); - } - - componentDidUpdate(prevProps) { - if (prevProps.isFocused === this.props.isFocused) { - return; - } - if (!this.props.isFocused) { - return; - } - - this.focusAndScroll(); - } - - focusAndScroll() { - this.ref.focus({preventScroll: true}); - this.ref.scrollIntoView({block: 'nearest'}); - } - - render() { - return ( - this.props.onPress(this.props.emoji)} - // In order to prevent haptic feedback, pass empty callback as onLongPress props. Please refer https://github.com/necolas/react-native-web/issues/2349#issuecomment-1195564240 - onLongPress={Browser.isMobileChrome() ? () => {} : undefined} - onPressOut={Browser.isMobile() ? this.props.onHoverOut : undefined} - onHoverIn={() => { - if (this.props.onHoverIn) { - this.props.onHoverIn(); - } - - this.setState({isHovered: true}); - }} - onHoverOut={() => { - if (this.props.onHoverOut) { - this.props.onHoverOut(); - } - - this.setState({isHovered: false}); - }} - onFocus={this.props.onFocus} - onBlur={this.props.onBlur} - ref={(ref) => (this.ref = ref)} - style={({pressed}) => [ - this.props.isFocused ? this.props.themeStyles.emojiItemKeyboardHighlighted : {}, - this.state.isHovered || this.props.isHighlighted ? this.props.themeStyles.emojiItemHighlighted : {}, - Browser.isMobile() && this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), - this.props.themeStyles.emojiItem, - ]} - accessibilityLabel={this.props.emoji} - role={CONST.ROLE.BUTTON} - > - {this.props.emoji} - - ); - } -} - -EmojiPickerMenuItem.propTypes = propTypes; -EmojiPickerMenuItem.defaultProps = { - isFocused: false, - isHighlighted: false, - onHoverIn: () => {}, - onHoverOut: () => {}, - onFocus: () => {}, - onBlur: () => {}, -}; - -// Significantly speeds up re-renders of the EmojiPickerMenu's FlatList -// by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. -export default withThemeStyles( - withStyleUtils( - React.memo( - EmojiPickerMenuItem, - (prevProps, nextProps) => prevProps.isFocused === nextProps.isFocused && prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji, - ), - ), -); diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js deleted file mode 100644 index 1726ff5b6543..000000000000 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js +++ /dev/null @@ -1,112 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {PureComponent} from 'react'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import Text from '@components/Text'; -import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import getButtonState from '@libs/getButtonState'; -import CONST from '@src/CONST'; - -const propTypes = { - /** The unicode that is used to display the emoji */ - emoji: PropTypes.string.isRequired, - - /** The function to call when an emoji is selected */ - onPress: PropTypes.func.isRequired, - - /** Handles what to do when we hover over this item with our cursor */ - onHoverIn: PropTypes.func, - - /** Handles what to do when the hover is out */ - onHoverOut: PropTypes.func, - - /** Handles what to do when the pressable is focused */ - onFocus: PropTypes.func, - - /** Handles what to do when the pressable is blurred */ - onBlur: PropTypes.func, - - /** Whether this menu item is currently highlighted or not */ - isHighlighted: PropTypes.bool, - - /** Whether this menu item is currently focused or not */ - isFocused: PropTypes.bool, - - /** Whether the emoji is highlighted by the keyboard/mouse */ - isUsingKeyboardMovement: PropTypes.bool, - - ...withThemeStylesPropTypes, - ...withStyleUtilsPropTypes, -}; - -class EmojiPickerMenuItem extends PureComponent { - constructor(props) { - super(props); - - this.ref = null; - } - - componentDidMount() { - if (!this.props.isFocused) { - return; - } - this.ref.focus(); - } - - componentDidUpdate(prevProps) { - if (prevProps.isFocused === this.props.isFocused) { - return; - } - if (!this.props.isFocused) { - return; - } - this.ref.focus(); - } - - render() { - return ( - this.props.onPress(this.props.emoji)} - onHoverIn={this.props.onHoverIn} - onHoverOut={this.props.onHoverOut} - onFocus={this.props.onFocus} - onBlur={this.props.onBlur} - ref={(ref) => (this.ref = ref)} - style={({pressed}) => [ - this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), - this.props.isHighlighted && this.props.isUsingKeyboardMovement ? this.props.themeStyles.emojiItemKeyboardHighlighted : {}, - this.props.isHighlighted && !this.props.isUsingKeyboardMovement ? this.props.themeStyles.emojiItemHighlighted : {}, - this.props.themeStyles.emojiItem, - ]} - accessibilityLabel={this.props.emoji} - role={CONST.ROLE.BUTTON} - > - {this.props.emoji} - - ); - } -} - -EmojiPickerMenuItem.propTypes = propTypes; -EmojiPickerMenuItem.defaultProps = { - isHighlighted: false, - isFocused: false, - isUsingKeyboardMovement: false, - onHoverIn: () => {}, - onHoverOut: () => {}, - onFocus: () => {}, - onBlur: () => {}, -}; - -// Significantly speeds up re-renders of the EmojiPickerMenu's FlatList -// by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. -export default withThemeStyles( - withStyleUtils( - React.memo( - EmojiPickerMenuItem, - (prevProps, nextProps) => - prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji && prevProps.isUsingKeyboardMovement === nextProps.isUsingKeyboardMovement, - ), - ), -); diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx new file mode 100644 index 000000000000..1336654cdf2f --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx @@ -0,0 +1,62 @@ +import React, {useEffect, useRef} from 'react'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import Text from '@components/Text'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getButtonState from '@libs/getButtonState'; +import CONST from '@src/CONST'; +import type EmojiPickerMenuItemProps from './types'; + +function EmojiPickerMenuItem({ + emoji, + onPress, + onHoverIn = () => {}, + onHoverOut = () => {}, + onFocus = () => {}, + onBlur = () => {}, + isFocused = false, + isHighlighted = false, + isUsingKeyboardMovement = false, +}: EmojiPickerMenuItemProps) { + const ref = useRef(null); + const StyleUtils = useStyleUtils(); + const themeStyles = useThemeStyles(); + + useEffect(() => { + if (!isFocused) { + return; + } + + ref?.current?.focus(); + }, [isFocused]); + + return ( + onPress(emoji)} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut} + onFocus={onFocus} + onBlur={onBlur} + ref={ref} + style={({pressed}) => [ + StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), + isHighlighted && isUsingKeyboardMovement && themeStyles.emojiItemKeyboardHighlighted, + isHighlighted && !isUsingKeyboardMovement && themeStyles.emojiItemHighlighted, + themeStyles.emojiItem, + ]} + accessibilityLabel={emoji} + role={CONST.ROLE.BUTTON} + > + {emoji} + + ); +} + +// Significantly speeds up re-renders of the EmojiPickerMenu's FlatList +// by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. +export default React.memo( + EmojiPickerMenuItem, + (prevProps, nextProps) => + prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji && prevProps.isUsingKeyboardMovement === nextProps.isUsingKeyboardMovement, +); diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.tsx b/src/components/EmojiPicker/EmojiPickerMenuItem/index.tsx new file mode 100644 index 000000000000..8aaf4a14e560 --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.tsx @@ -0,0 +1,89 @@ +import React, {useEffect, useRef, useState} from 'react'; +// eslint-disable-next-line no-restricted-imports +import type {Text as RNText, View} from 'react-native'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import Text from '@components/Text'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Browser from '@libs/Browser'; +import getButtonState from '@libs/getButtonState'; +import CONST from '@src/CONST'; +import type EmojiPickerMenuItemProps from './types'; + +function EmojiPickerMenuItem({ + emoji, + onPress, + onHoverIn = () => {}, + onHoverOut = () => {}, + onFocus = () => {}, + onBlur = () => {}, + isFocused = false, + isHighlighted = false, +}: EmojiPickerMenuItemProps) { + const [isHovered, setIsHovered] = useState(false); + const ref = useRef(null); + const StyleUtils = useStyleUtils(); + const themeStyles = useThemeStyles(); + + const focusAndScroll = () => { + if (ref.current && 'focus' in ref.current) { + ref.current.focus({preventScroll: true}); + } + if (ref.current && 'scrollIntoView' in ref.current) { + ref.current.scrollIntoView({block: 'nearest'}); + } + }; + + useEffect(() => { + if (!isFocused) { + return; + } + focusAndScroll(); + }, [isFocused]); + + return ( + onPress(emoji)} + // In order to prevent haptic feedback, pass empty callback as onLongPress Please refer https://github.com/necolas/react-native-web/issues/2349#issuecomment-1195564240 + onLongPress={Browser.isMobileChrome() ? () => {} : undefined} + onPressOut={Browser.isMobile() ? onHoverOut : undefined} + onHoverIn={() => { + if (onHoverIn) { + onHoverIn(); + } + + setIsHovered(true); + }} + onHoverOut={() => { + if (onHoverOut) { + onHoverOut(); + } + + setIsHovered(false); + }} + onFocus={onFocus} + onBlur={onBlur} + ref={(el) => { + ref.current = el ?? null; + }} + style={({pressed}) => [ + isFocused ? themeStyles.emojiItemKeyboardHighlighted : {}, + isHovered || isHighlighted ? themeStyles.emojiItemHighlighted : {}, + Browser.isMobile() && StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), + themeStyles.emojiItem, + ]} + accessibilityLabel={emoji} + role={CONST.ROLE.BUTTON} + > + {emoji} + + ); +} + +// Significantly speeds up re-renders of the EmojiPickerMenu's FlatList +// by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. +export default React.memo( + EmojiPickerMenuItem, + (prevProps, nextProps) => prevProps.isFocused === nextProps.isFocused && prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji, +); diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/types.ts b/src/components/EmojiPicker/EmojiPickerMenuItem/types.ts new file mode 100644 index 000000000000..ca5a4d12a988 --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/types.ts @@ -0,0 +1,30 @@ +type EmojiPickerMenuItemProps = { + /** The unicode that is used to display the emoji */ + emoji: string; + + /** The function to call when an emoji is selected */ + onPress: (emoji: string) => void; + + /** Handles what to do when we hover over this item with our cursor */ + onHoverIn?: () => void; + + /** Handles what to do when the hover is out */ + onHoverOut?: () => void; + + /** Handles what to do when the pressable is focused */ + onFocus?: () => void; + + /** Handles what to do when the pressable is blurred */ + onBlur?: () => void; + + /** Whether this menu item is currently focused or not */ + isFocused?: boolean; + + /** Whether the menu item should be highlighted or not */ + isHighlighted?: boolean; + + /** Whether the emoji is highlighted by the keyboard/mouse */ + isUsingKeyboardMovement?: boolean; +}; + +export default EmojiPickerMenuItemProps; diff --git a/src/components/EmojiPicker/EmojiSkinToneList.js b/src/components/EmojiPicker/EmojiSkinToneList.tsx similarity index 92% rename from src/components/EmojiPicker/EmojiSkinToneList.js rename to src/components/EmojiPicker/EmojiSkinToneList.tsx index 7f36d929025f..fb798f1c02c4 100644 --- a/src/components/EmojiPicker/EmojiSkinToneList.js +++ b/src/components/EmojiPicker/EmojiSkinToneList.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import * as Emojis from '@assets/emojis'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; @@ -11,9 +10,13 @@ import CONST from '@src/CONST'; import EmojiPickerMenuItem from './EmojiPickerMenuItem'; import getSkinToneEmojiFromIndex from './getSkinToneEmojiFromIndex'; +type SkinToneEmoji = { + skinTone: number; +}; + function EmojiSkinToneList() { const styles = useThemeStyles(); - const [highlightedIndex, setHighlightedIndex] = useState(null); + const [highlightedIndex, setHighlightedIndex] = useState(null); const [isSkinToneListVisible, setIsSkinToneListVisible] = useState(false); const {translate} = useLocalize(); const [preferredSkinTone, setPreferredSkinTone] = usePreferredEmojiSkinTone(); @@ -24,9 +27,8 @@ function EmojiSkinToneList() { /** * Set the preferred skin tone in Onyx and close the skin tone picker - * @param {object} skinToneEmoji */ - function updateSelectedSkinTone(skinToneEmoji) { + function updateSelectedSkinTone(skinToneEmoji: SkinToneEmoji) { setHighlightedIndex(skinToneEmoji.skinTone); setPreferredSkinTone(skinToneEmoji.skinTone); } @@ -57,7 +59,7 @@ function EmojiSkinToneList() { )} {isSkinToneListVisible && ( - {_.map(Emojis.skinTones, (skinToneEmoji) => ( + {Emojis.skinTones.map((skinToneEmoji) => ( updateSelectedSkinTone(skinToneEmoji)} onHoverIn={() => setHighlightedIndex(skinToneEmoji.skinTone)} diff --git a/src/components/EmojiPicker/getSkinToneEmojiFromIndex.js b/src/components/EmojiPicker/getSkinToneEmojiFromIndex.js deleted file mode 100644 index 48f9e2177a73..000000000000 --- a/src/components/EmojiPicker/getSkinToneEmojiFromIndex.js +++ /dev/null @@ -1,13 +0,0 @@ -import _ from 'underscore'; -import * as Emojis from '@assets/emojis'; - -/** - * Fetch the emoji code of selected skinTone - * @param {Number} skinToneIndex - * @returns {String} - */ -function getSkinToneEmojiFromIndex(skinToneIndex) { - return _.find(Emojis.skinTones, (emoji) => emoji.skinTone === skinToneIndex) || Emojis.skinTones[0]; -} - -export default getSkinToneEmojiFromIndex; diff --git a/src/components/EmojiPicker/getSkinToneEmojiFromIndex.ts b/src/components/EmojiPicker/getSkinToneEmojiFromIndex.ts new file mode 100644 index 000000000000..6bae46edca7b --- /dev/null +++ b/src/components/EmojiPicker/getSkinToneEmojiFromIndex.ts @@ -0,0 +1,11 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import * as Emojis from '@assets/emojis'; + +/** + * Fetch the emoji code of selected skinTone + */ +function getSkinToneEmojiFromIndex(skinToneIndex: OnyxEntry) { + return Emojis.skinTones.find((emoji) => emoji.skinTone === skinToneIndex) ?? Emojis.skinTones[0]; +} + +export default getSkinToneEmojiFromIndex; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 93da67331c9f..331f1c943b30 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -9,6 +9,7 @@ import type AmountTextInput from '@components/AmountTextInput'; import type CheckboxWithLabel from '@components/CheckboxWithLabel'; import type CountrySelector from '@components/CountrySelector'; import type DatePicker from '@components/DatePicker'; +import type EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown'; import type Picker from '@components/Picker'; import type RadioButtons from '@components/RadioButtons'; import type RoomNameInput from '@components/RoomNameInput'; @@ -26,9 +27,6 @@ import type {BaseForm} from '@src/types/form/Form'; /** * This type specifies all the inputs that can be used with `InputWrapper` component. Make sure to update it * when adding new inputs or removing old ones. - * - * TODO: Add remaining inputs here once these components are migrated to Typescript: - * EmojiPickerButtonDropdown */ type ValidInputs = | typeof TextInput @@ -47,7 +45,8 @@ type ValidInputs = | typeof RadioButtons | typeof AmountPicker | typeof TextPicker - | typeof AddPlaidBankAccount; + | typeof AddPlaidBankAccount + | typeof EmojiPickerButtonDropdown; type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country'; type ValueTypeMap = { diff --git a/src/components/Reactions/AddReactionBubble.tsx b/src/components/Reactions/AddReactionBubble.tsx index 6b7aa12b742e..8364a6658270 100644 --- a/src/components/Reactions/AddReactionBubble.tsx +++ b/src/components/Reactions/AddReactionBubble.tsx @@ -32,7 +32,7 @@ type AddReactionBubbleProps = { /** * Will get called the moment before the picker opens. */ - onWillShowPicker?: (callback: CloseContextMenuCallback) => void; + onWillShowPicker?: (callback?: CloseContextMenuCallback) => void; /** * Called when the user selects an emoji. diff --git a/src/components/Reactions/QuickEmojiReactions/types.ts b/src/components/Reactions/QuickEmojiReactions/types.ts index 7b6558364a55..0021f33ce2c0 100644 --- a/src/components/Reactions/QuickEmojiReactions/types.ts +++ b/src/components/Reactions/QuickEmojiReactions/types.ts @@ -18,7 +18,7 @@ type BaseReactionsProps = { /** * Will be called when the emoji picker is about to show. */ - onWillShowPicker?: (callback: CloseContextMenuCallback) => void; + onWillShowPicker?: (callback?: CloseContextMenuCallback) => void; /** * Callback to fire when the "open emoji picker" button is pressed. @@ -55,7 +55,7 @@ type QuickEmojiReactionsProps = BaseReactionsProps & { * Function that can be called to close the context menu * in which this component is rendered. */ - closeContextMenu: (callback: CloseContextMenuCallback) => void; + closeContextMenu: (callback?: CloseContextMenuCallback) => void; setIsEmojiPickerActive?: (state: boolean) => void; }; diff --git a/src/hooks/usePreferredEmojiSkinTone.ts b/src/hooks/usePreferredEmojiSkinTone.ts index 6eeecdb16617..3e536d3dc041 100644 --- a/src/hooks/usePreferredEmojiSkinTone.ts +++ b/src/hooks/usePreferredEmojiSkinTone.ts @@ -16,5 +16,5 @@ export default function usePreferredEmojiSkinTone() { [preferredSkinTone], ); - return [preferredSkinTone, updatePreferredSkinTone]; + return [preferredSkinTone, updatePreferredSkinTone] as const; } diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 05f6fbd17503..3b189dbb084f 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -13,7 +13,8 @@ import type IconAsset from '@src/types/utils/IconAsset'; type HeaderIndice = {code: string; index: number; icon: IconAsset}; type EmojiSpacer = {code: string; spacer: boolean}; -type EmojiPickerList = Array; +type EmojiPickerListItem = EmojiSpacer | Emoji | HeaderEmoji; +type EmojiPickerList = EmojiPickerListItem[]; type ReplacedEmoji = {text: string; emojis: Emoji[]; cursorPosition?: number}; let frequentlyUsedEmojis: FrequentlyUsedEmoji[] = []; @@ -155,7 +156,7 @@ function containsOnlyEmojis(message: string): boolean { /** * Get the header emojis with their code, icon and index */ -function getHeaderEmojis(emojis: PickerEmojis): HeaderIndice[] { +function getHeaderEmojis(emojis: EmojiPickerList): HeaderIndice[] { const headerIndices: HeaderIndice[] = []; emojis.forEach((emoji, index) => { if (!('header' in emoji)) { @@ -554,7 +555,7 @@ const getEmojiReactionDetails = (emojiName: string, reaction: ReportActionReacti /** * Given an emoji code, returns an base emoji code without skin tone */ -const getRemovedSkinToneEmoji = (emoji: string) => emoji.replace(CONST.REGEX.EMOJI_SKIN_TONES, ''); +const getRemovedSkinToneEmoji = (emoji?: string) => emoji?.replace(CONST.REGEX.EMOJI_SKIN_TONES, ''); function getSpacersIndexes(allEmojis: EmojiPickerList): number[] { const spacersIndexes: number[] = []; @@ -568,6 +569,8 @@ function getSpacersIndexes(allEmojis: EmojiPickerList): number[] { return spacersIndexes; } +export type {HeaderIndice, EmojiPickerList, EmojiSpacer, EmojiPickerListItem}; + export { findEmojiByName, findEmojiByCode, diff --git a/src/libs/actions/EmojiPickerAction.ts b/src/libs/actions/EmojiPickerAction.ts index a54095ad9c23..787f105e4939 100644 --- a/src/libs/actions/EmojiPickerAction.ts +++ b/src/libs/actions/EmojiPickerAction.ts @@ -9,15 +9,15 @@ import type CONST from '@src/CONST'; type AnchorOrigin = { horizontal: ValueOf; vertical: ValueOf; + shiftVertical?: number; }; type EmojiPopoverAnchor = MutableRefObject; -type OnWillShowPicker = (callback: CloseContextMenuCallback) => void; +type OnWillShowPicker = (callback?: CloseContextMenuCallback) => void; type OnModalHideValue = () => void; -// TODO: Move this type to src/components/EmojiPicker/EmojiPicker.js once it is converted to TS type EmojiPickerRef = { showEmojiPicker: ( onModalHideValue: OnModalHideValue, @@ -112,4 +112,4 @@ function resetEmojiPopoverAnchor() { } export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActive, clearActive, isEmojiPickerVisible, resetEmojiPopoverAnchor}; -export type {AnchorOrigin, EmojiPickerRef}; +export type {AnchorOrigin, OnModalHideValue, OnEmojiSelected, EmojiPopoverAnchor, OnWillShowPicker, EmojiPickerRef}; diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 365bd1ece28b..bee34c1c1e96 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -1,14 +1,7 @@ -/* eslint-disable no-restricted-imports */ -import type {ValueOf} from 'type-fest'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {AnchorDimensions, AnchorPosition} from '@src/styles'; - -type AnchorOrigin = { - horizontal: ValueOf; - vertical: ValueOf; - shiftVertical?: number; -}; +import type {AnchorOrigin} from './actions/EmojiPickerAction'; /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 04b4d5cc1850..f984e88b85b2 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -1,7 +1,7 @@ import React from 'react'; import type {RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {GestureResponderEvent, Text as RNText, View} from 'react-native'; +import type {GestureResponderEvent, Text as RNText, TextInput, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; @@ -16,7 +16,7 @@ type OnCancel = () => void; type ContextMenuType = ValueOf; -type ContextMenuAnchor = View | RNText | HTMLDivElement | null | undefined; +type ContextMenuAnchor = View | RNText | TextInput | HTMLDivElement | null | undefined; type ShowContextMenu = ( type: ContextMenuType, diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 02d7a14f4b0e..e3e32f398fb6 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -187,7 +187,7 @@ function ReportActionItem({ const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); const textInputRef = useRef(); - const popoverAnchorRef = useRef(null); + const popoverAnchorRef = useRef>(null); const downloadedPreviews = useRef([]); const prevDraftMessage = usePrevious(draftMessage); const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index bb7be4a6866c..5dc1c1832d39 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -162,11 +162,13 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) {}} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onInputChange={(emoji: string): void => {}} />