diff --git a/src/components/DatePicker/DatePickerModal.tsx b/src/components/DatePicker/DatePickerModal.tsx index f8ac807c5d82..ddadb8d46808 100644 --- a/src/components/DatePicker/DatePickerModal.tsx +++ b/src/components/DatePicker/DatePickerModal.tsx @@ -73,6 +73,7 @@ function DatePickerModal({ shouldSwitchPositionIfOverflow shouldEnableNewFocusManagement shouldMeasureAnchorPositionFromTop={shouldPositionFromTop} + shouldSkipRemeasurement > diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx index 6b1546b87056..a576c457f248 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx @@ -14,7 +14,8 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as EmojiUtils from '@libs/EmojiUtils'; +import type {EmojiPickerList, EmojiPickerListItem} from '@libs/EmojiUtils'; +import {getRemovedSkinToneEmoji} from '@libs/EmojiUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; @@ -44,7 +45,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r } = useEmojiPickerMenu(); const StyleUtils = useStyleUtils(); - const updateEmojiList = (emojiData: EmojiUtils.EmojiPickerList | Emoji[], headerData: number[] = []) => { + const updateEmojiList = (emojiData: EmojiPickerList | Emoji[], headerData: number[] = []) => { setFilteredEmojis(emojiData); setHeaderIndices(headerData); @@ -68,17 +69,20 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r } }, 300); - const scrollToHeader = (headerIndex: number) => { - const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; - emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true}); - }; + const scrollToHeader = useCallback( + (headerIndex: number) => { + const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; + emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true}); + }, + [emojiListRef], + ); /** * 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. */ - const renderItem: ListRenderItem = useCallback( + const renderItem: ListRenderItem = useCallback( ({item, target}) => { const code = item.code; const types = 'types' in item ? item.types : undefined; @@ -96,7 +100,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r } const emojiCode = typeof preferredSkinTone === 'number' && preferredSkinTone !== -1 && types?.at(preferredSkinTone) ? types.at(preferredSkinTone) : code; - const shouldEmojiBeHighlighted = !!activeEmoji && EmojiUtils.getRemovedSkinToneEmoji(emojiCode) === EmojiUtils.getRemovedSkinToneEmoji(activeEmoji); + const shouldEmojiBeHighlighted = !!activeEmoji && getRemovedSkinToneEmoji(emojiCode) === getRemovedSkinToneEmoji(activeEmoji); return ( & { /** Whether we should should use top side for the anchor positioning */ shouldMeasureAnchorPositionFromTop?: boolean; + + /** Whether to skip re-measurement when becoming visible (for components with static dimensions) */ + shouldSkipRemeasurement?: boolean; }; /** @@ -66,6 +69,7 @@ function PopoverWithMeasuredContent({ shouldHandleNavigationBack = false, shouldEnableNewFocusManagement, shouldMeasureAnchorPositionFromTop = false, + shouldSkipRemeasurement = false, ...props }: PopoverWithMeasuredContentProps) { const actionSheetAwareScrollViewContext = useContext(ActionSheetAwareScrollView.ActionSheetAwareScrollViewContext); @@ -75,6 +79,8 @@ function PopoverWithMeasuredContent({ const [popoverHeight, setPopoverHeight] = useState(popoverDimensions.height); const [isContentMeasured, setIsContentMeasured] = useState(popoverWidth > 0 && popoverHeight > 0); const prevIsVisible = usePrevious(isVisible); + const prevAnchorPosition = usePrevious(anchorPosition); + const prevWindowDimensions = usePrevious({windowWidth, windowHeight}); const modalId = useMemo(() => ComposerFocusManager.getId(), []); @@ -82,8 +88,19 @@ function PopoverWithMeasuredContent({ ComposerFocusManager.saveFocusState(modalId); } - if (!prevIsVisible && isVisible && isContentMeasured) { - setIsContentMeasured(false); + if (!prevIsVisible && isVisible && isContentMeasured && !shouldSkipRemeasurement) { + // Check if anything significant changed that would require re-measurement + const hasAnchorPositionChanged = !isEqual(prevAnchorPosition, anchorPosition); + const hasWindowSizeChanged = !isEqual(prevWindowDimensions, {windowWidth, windowHeight}); + const hasStaticDimensions = popoverDimensions.width > 0 && popoverDimensions.height > 0; + + // Only reset if: + // 1. We don't have static dimensions, OR + // 2. The anchor position changed significantly, OR + // 3. The window size changed significantly + if (!hasStaticDimensions || hasAnchorPositionChanged || hasWindowSizeChanged) { + setIsContentMeasured(false); + } } /** @@ -144,28 +161,39 @@ function PopoverWithMeasuredContent({ }; }, [anchorPosition, anchorAlignment, popoverWidth, popoverHeight]); - const horizontalShift = PopoverWithMeasuredContentUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth); - const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift( - adjustedAnchorPosition.top, - popoverHeight, - windowHeight, - anchorDimensions.height, - shouldSwitchPositionIfOverflow, - ); - const shiftedAnchorPosition: PopoverAnchorPosition = { - left: adjustedAnchorPosition.left + horizontalShift, - ...(shouldMeasureAnchorPositionFromTop ? {top: adjustedAnchorPosition.top + verticalShift} : {}), - }; + const positionCalculations = useMemo(() => { + const horizontalShift = PopoverWithMeasuredContentUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth); + const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift( + adjustedAnchorPosition.top, + popoverHeight, + windowHeight, + anchorDimensions.height, + shouldSwitchPositionIfOverflow, + ); + return {horizontalShift, verticalShift}; + }, [adjustedAnchorPosition.left, adjustedAnchorPosition.top, popoverWidth, popoverHeight, windowWidth, windowHeight, anchorDimensions.height, shouldSwitchPositionIfOverflow]); + + const shiftedAnchorPosition: PopoverAnchorPosition = useMemo(() => { + const result: PopoverAnchorPosition = { + left: adjustedAnchorPosition.left + positionCalculations.horizontalShift, + }; - if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { - const top = adjustedAnchorPosition.top + verticalShift; - const maxTop = windowHeight - popoverHeight - verticalShift; - shiftedAnchorPosition.top = Math.min(Math.max(verticalShift, top), maxTop); - } + if (shouldMeasureAnchorPositionFromTop) { + result.top = adjustedAnchorPosition.top + positionCalculations.verticalShift; + } - if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM) { - shiftedAnchorPosition.bottom = windowHeight - (adjustedAnchorPosition.top + popoverHeight) - verticalShift; - } + if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { + const top = adjustedAnchorPosition.top + positionCalculations.verticalShift; + const maxTop = windowHeight - popoverHeight - positionCalculations.verticalShift; + result.top = Math.min(Math.max(positionCalculations.verticalShift, top), maxTop); + } + + if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM) { + result.bottom = windowHeight - (adjustedAnchorPosition.top + popoverHeight) - positionCalculations.verticalShift; + } + + return result; + }, [adjustedAnchorPosition, positionCalculations, anchorAlignment.vertical, windowHeight, popoverHeight, shouldMeasureAnchorPositionFromTop]); return isContentMeasured ? ( ({ + secondaryRowHeight: styles.chatItemComposeSecondaryRow.height, + secondaryRowMarginTop: styles.chatItemComposeSecondaryRow.marginTop, + secondaryRowMarginBottom: styles.chatItemComposeSecondaryRow.marginBottom, + composeBoxMinHeight: styles.chatItemComposeBox.minHeight, + emojiButtonHeight: styles.chatItemEmojiButton.height, + }), + [ + styles.chatItemComposeSecondaryRow.height, + styles.chatItemComposeSecondaryRow.marginTop, + styles.chatItemComposeSecondaryRow.marginBottom, + styles.chatItemComposeBox.minHeight, + styles.chatItemEmojiButton.height, + ], + ); + const emojiShiftVertical = useMemo(() => { - const chatItemComposeSecondaryRowHeight = styles.chatItemComposeSecondaryRow.height + styles.chatItemComposeSecondaryRow.marginTop + styles.chatItemComposeSecondaryRow.marginBottom; - const reportActionComposeHeight = styles.chatItemComposeBox.minHeight + chatItemComposeSecondaryRowHeight; - const emojiOffsetWithComposeBox = (styles.chatItemComposeBox.minHeight - styles.chatItemEmojiButton.height) / 2; + const chatItemComposeSecondaryRowHeight = emojiPositionValues.secondaryRowHeight + emojiPositionValues.secondaryRowMarginTop + emojiPositionValues.secondaryRowMarginBottom; + const reportActionComposeHeight = emojiPositionValues.composeBoxMinHeight + chatItemComposeSecondaryRowHeight; + const emojiOffsetWithComposeBox = (emojiPositionValues.composeBoxMinHeight - emojiPositionValues.emojiButtonHeight) / 2; return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; - }, [styles]); + }, [emojiPositionValues]); const validateMaxLength = useCallback( (value: string) => {