Skip to content

Commit

Permalink
Merge pull request Expensify#37641 from HezekielT/migrate/25155
Browse files Browse the repository at this point in the history
[TS migration] Migrate 'EmojiPicker' component to TypeScript
  • Loading branch information
tylerkaraszewski authored Apr 9, 2024
2 parents 0962a33 + dd6327f commit 0f4c845
Show file tree
Hide file tree
Showing 29 changed files with 455 additions and 599 deletions.
Original file line number Diff line number Diff line change
@@ -1,42 +1,33 @@
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 (
<View style={[styles.ph4, styles.flexRow]}>
{_.map(props.headerEmojis, (headerEmoji, i) => (
{headerEmojis.map((headerEmoji) => (
<CategoryShortcutButton
icon={headerEmoji.icon}
onPress={() => props.onPress(headerEmoji.index)}
key={`categoryShortcut${i}`}
onPress={() => onPress(headerEmoji.index)}
key={`categoryShortcut${headerEmoji.index}`}
code={headerEmoji.code}
/>
))}
</View>
);
}

CategoryShortcutBar.propTypes = propTypes;
CategoryShortcutBar.displayName = 'CategoryShortcutBar';

export default CategoryShortcutBar;
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
Expand All @@ -32,28 +32,28 @@ function CategoryShortcutButton(props) {

return (
<Tooltip
text={translate(`emojiPicker.headers.${props.code}`)}
text={translate(`emojiPicker.headers.${code}` as TranslationPaths)}
shiftVertical={-4}
>
<PressableWithoutFeedback
shouldUseAutoHitSlop={false}
onPress={props.onPress}
onPress={onPress}
onHoverIn={() => 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}
>
<Icon
fill={theme.icon}
src={props.icon}
src={icon}
height={variables.iconSizeNormal}
width={variables.iconSizeNormal}
/>
</PressableWithoutFeedback>
</Tooltip>
);
}
CategoryShortcutButton.propTypes = propTypes;

CategoryShortcutButton.displayName = 'CategoryShortcutButton';
export default React.memo(CategoryShortcutButton);
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,29 +20,29 @@ 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<EmojiPickerRef>) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [isEmojiPickerVisible, setIsEmojiPickerVisible] = useState(false);
const [emojiPopoverAnchorPosition, setEmojiPopoverAnchorPosition] = useState({
horizontal: 0,
vertical: 0,
});
const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN);
const [activeID, setActiveID] = useState();
const emojiPopoverAnchorRef = useRef(null);
const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState<AnchorOrigin>(DEFAULT_ANCHOR_ORIGIN);
const [activeID, setActiveID] = useState<string | null>();
const emojiPopoverAnchorRef = useRef<EmojiPopoverAnchor | null>(null);
const emojiAnchorDimension = useRef({
width: 0,
height: 0,
});
const onModalHide = useRef(() => {});
const onEmojiSelected = useRef(() => {});
const activeEmoji = useRef();
const emojiSearchInput = useRef();
const onEmojiSelected = useRef<OnEmojiSelected>(() => {});
const activeEmoji = useRef<string | undefined>();
const emojiSearchInput = useRef<BaseTextInputRef | null>();
const {isSmallScreenWidth, windowHeight} = useWindowDimensions();

/**
Expand All @@ -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({
Expand All @@ -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 = () => {};
}
Expand All @@ -124,30 +130,24 @@ 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) {
return;
}

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);

Expand All @@ -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,
Expand Down Expand Up @@ -201,14 +201,14 @@ const EmojiPicker = forwardRef((props, ref) => {
vertical: emojiPopoverAnchorPosition.vertical,
horizontal: emojiPopoverAnchorPosition.horizontal,
}}
anchorRef={getEmojiPopoverAnchor()}
anchorRef={getEmojiPopoverAnchor() as RefObject<View | HTMLDivElement>}
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
Expand All @@ -223,8 +223,7 @@ const EmojiPicker = forwardRef((props, ref) => {
/>
</PopoverWithMeasuredContent>
);
});
}

EmojiPicker.propTypes = propTypes;
EmojiPicker.displayName = 'EmojiPicker';
export default withViewportOffsetTop(EmojiPicker);
export default withViewportOffsetTop(forwardRef(EmojiPicker));
Loading

0 comments on commit 0f4c845

Please sign in to comment.