Skip to content

Commit df0daa6

Browse files
perf: decompose OptionRowLHN press/context-menu into a separate component
1 parent 4cfb6a6 commit df0daa6

2 files changed

Lines changed: 144 additions & 80 deletions

File tree

src/components/LHNOptionsList/OptionRowLHN.tsx

Lines changed: 18 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import React, {useMemo, useRef, useState} from 'react';
1+
import React, {useMemo, useRef} from 'react';
22
import type {GestureResponderEvent, ViewStyle} from 'react-native';
33
import {StyleSheet, View} from 'react-native';
44
import Badge from '@components/Badge';
55
import DisplayNames from '@components/DisplayNames';
6-
import Hoverable from '@components/Hoverable';
76
import Icon from '@components/Icon';
87
import OfflineWithFeedback from '@components/OfflineWithFeedback';
98
import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider';
10-
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
119
import {useProductTrainingContext} from '@components/ProductTrainingContext';
1210
import Text from '@components/Text';
1311
import Tooltip from '@components/Tooltip';
@@ -23,7 +21,6 @@ import useStyleUtils from '@hooks/useStyleUtils';
2321
import useTheme from '@hooks/useTheme';
2422
import useThemeStyles from '@hooks/useThemeStyles';
2523
import DateUtils from '@libs/DateUtils';
26-
import DomUtils from '@libs/DomUtils';
2724
import {containsCustomEmoji as containsCustomEmojiUtils, containsOnlyCustomEmoji} from '@libs/EmojiUtils';
2825
import FS from '@libs/Fullstory';
2926
import {shouldOptionShowTooltip, shouldUseBoldText} from '@libs/OptionsListUtils';
@@ -32,13 +29,13 @@ import {getDelegateAccountIDFromReportAction} from '@libs/ReportActionsUtils';
3229
import {isAdminRoom, isChatUsedForOnboarding as isChatUsedForOnboardingReportUtils, isConciergeChatReport, isGroupChat, isOneOnOneChat, isSystemChat} from '@libs/ReportUtils';
3330
import {startSpan} from '@libs/telemetry/activeSpans';
3431
import TextWithEmojiFragment from '@pages/inbox/report/comment/TextWithEmojiFragment';
35-
import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
3632
import FreeTrial from '@pages/settings/Subscription/FreeTrial';
3733
import variables from '@styles/variables';
3834
import CONST from '@src/CONST';
3935
import {isEmptyObject} from '@src/types/utils/EmptyObject';
4036
import LHNAvatar from './LHNAvatar';
4137
import {useLHNTooltipContext} from './LHNTooltipContext';
38+
import OptionRowPressable from './OptionRowPressable';
4239
import type {OptionRowLHNProps} from './types';
4340

4441
function OptionRowLHN({
@@ -87,7 +84,6 @@ function OptionRowLHN({
8784
const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip);
8885

8986
const {translate} = useLocalize();
90-
const [isContextMenuActive, setIsContextMenuActive] = useState(false);
9187
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
9288
const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT;
9389
const sidebarInnerRowStyle = StyleSheet.flatten<ViewStyle>(
@@ -179,37 +175,6 @@ function OptionRowLHN({
179175
const hoveredBackgroundColor = !!styles.sidebarLinkHover && 'backgroundColor' in styles.sidebarLinkHover ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
180176
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
181177

182-
/**
183-
* Show the ReportActionContextMenu modal popover.
184-
*
185-
* @param [event] - A press event.
186-
*/
187-
const showPopover = (event: MouseEvent | GestureResponderEvent) => {
188-
if (!isScreenFocused && shouldUseNarrowLayout) {
189-
return;
190-
}
191-
setIsContextMenuActive(true);
192-
showContextMenu({
193-
type: CONST.CONTEXT_MENU_TYPES.REPORT,
194-
event,
195-
selection: '',
196-
contextMenuAnchor: popoverAnchor.current,
197-
report: {
198-
reportID,
199-
originalReportID: reportID,
200-
isPinnedChat: optionItem.isPinned,
201-
isUnreadChat: !!optionItem.isUnread,
202-
},
203-
reportAction: {
204-
reportActionID: '-1',
205-
},
206-
callbacks: {
207-
onHide: () => setIsContextMenuActive(false),
208-
},
209-
withoutOverlay: false,
210-
});
211-
};
212-
213178
const emojiCode = optionItem.status?.emojiCode ?? '';
214179
const statusText = optionItem.status?.text ?? '';
215180
const statusClearAfterDate = optionItem.status?.clearAfter ?? '';
@@ -274,7 +239,19 @@ function OptionRowLHN({
274239
shouldHideOnScroll
275240
>
276241
<View>
277-
<Hoverable>
242+
<OptionRowPressable
243+
reportID={reportID}
244+
optionItem={optionItem}
245+
isOptionFocused={isOptionFocused}
246+
isScreenFocused={isScreenFocused}
247+
popoverAnchor={popoverAnchor}
248+
onPress={onOptionPress}
249+
onLayout={onLayout}
250+
accessibilityLabel={accessibilityLabelWithContextMenuHint}
251+
accessibilityHint={accessibilityHint}
252+
// reportID may be a number contrary to the type definition
253+
testID={typeof optionItem.reportID === 'number' ? String(optionItem.reportID) : optionItem.reportID}
254+
>
278255
{(hovered) => {
279256
let secondaryAvatarBgColor = theme.sidebar;
280257
if (isOptionFocused) {
@@ -283,46 +260,7 @@ function OptionRowLHN({
283260
secondaryAvatarBgColor = hoveredBackgroundColor;
284261
}
285262
return (
286-
<PressableWithSecondaryInteraction
287-
ref={popoverAnchor}
288-
onPress={onOptionPress}
289-
onMouseDown={(event) => {
290-
// Allow composer blur on right click
291-
if (!event) {
292-
return;
293-
}
294-
// Prevent composer blur on left click
295-
event.preventDefault();
296-
}}
297-
// reportID may be a number contrary to the type definition
298-
testID={typeof optionItem.reportID === 'number' ? String(optionItem.reportID) : optionItem.reportID}
299-
onSecondaryInteraction={(event) => {
300-
showPopover(event);
301-
// Ensure that we blur the composer when opening context menu, so that only one component is focused at a time
302-
if (DomUtils.getActiveElement()) {
303-
(DomUtils.getActiveElement() as HTMLElement | null)?.blur();
304-
}
305-
}}
306-
withoutFocusOnSecondaryInteraction
307-
activeOpacity={variables.pressDimValue}
308-
opacityAnimationDuration={0}
309-
style={[
310-
styles.flexRow,
311-
styles.alignItemsCenter,
312-
styles.justifyContentBetween,
313-
styles.sidebarLink,
314-
styles.sidebarLinkInnerLHN,
315-
StyleUtils.getBackgroundColorStyle(theme.sidebar),
316-
isOptionFocused ? styles.sidebarLinkActive : null,
317-
(hovered || isContextMenuActive) && !isOptionFocused ? styles.sidebarLinkHover : null,
318-
]}
319-
role={CONST.ROLE.BUTTON}
320-
accessibilityLabel={accessibilityLabelWithContextMenuHint}
321-
accessibilityHint={accessibilityHint}
322-
onLayout={onLayout}
323-
needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2}
324-
sentryLabel={CONST.SENTRY_LABEL.LHN.OPTION_ROW}
325-
>
263+
<>
326264
<View style={sidebarInnerRowStyle}>
327265
<View style={[styles.flexRow, styles.alignItemsCenter]}>
328266
{!!optionItem.icons?.length && !!firstIcon && (
@@ -464,10 +402,10 @@ function OptionRowLHN({
464402
</View>
465403
)}
466404
</View>
467-
</PressableWithSecondaryInteraction>
405+
</>
468406
);
469407
}}
470-
</Hoverable>
408+
</OptionRowPressable>
471409
</View>
472410
</EducationalTooltip>
473411
</OfflineWithFeedback>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type {ReactNode, RefObject} from 'react';
2+
import React, {useState} from 'react';
3+
import type {GestureResponderEvent, LayoutChangeEvent, View} from 'react-native';
4+
import Hoverable from '@components/Hoverable';
5+
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
6+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
7+
import useStyleUtils from '@hooks/useStyleUtils';
8+
import useTheme from '@hooks/useTheme';
9+
import useThemeStyles from '@hooks/useThemeStyles';
10+
import DomUtils from '@libs/DomUtils';
11+
import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
12+
import variables from '@styles/variables';
13+
import CONST from '@src/CONST';
14+
import type {OptionData} from '@src/libs/ReportUtils';
15+
16+
type OptionRowPressableProps = {
17+
reportID: string;
18+
optionItem: OptionData;
19+
isOptionFocused: boolean;
20+
isScreenFocused: boolean;
21+
popoverAnchor: RefObject<View | null>;
22+
onPress: (event: GestureResponderEvent | KeyboardEvent | undefined) => void;
23+
onLayout?: (event: LayoutChangeEvent) => void;
24+
accessibilityLabel: string;
25+
accessibilityHint?: string;
26+
testID?: string;
27+
children: (hovered: boolean) => ReactNode;
28+
};
29+
30+
function OptionRowPressable({
31+
reportID,
32+
optionItem,
33+
isOptionFocused,
34+
isScreenFocused,
35+
popoverAnchor,
36+
onPress,
37+
onLayout,
38+
accessibilityLabel,
39+
accessibilityHint,
40+
testID,
41+
children,
42+
}: OptionRowPressableProps) {
43+
const theme = useTheme();
44+
const styles = useThemeStyles();
45+
const StyleUtils = useStyleUtils();
46+
const {shouldUseNarrowLayout} = useResponsiveLayout();
47+
const [isContextMenuActive, setIsContextMenuActive] = useState(false);
48+
49+
const showPopover = (event: MouseEvent | GestureResponderEvent) => {
50+
if (!isScreenFocused && shouldUseNarrowLayout) {
51+
return;
52+
}
53+
setIsContextMenuActive(true);
54+
showContextMenu({
55+
type: CONST.CONTEXT_MENU_TYPES.REPORT,
56+
event,
57+
selection: '',
58+
contextMenuAnchor: popoverAnchor.current,
59+
report: {
60+
reportID,
61+
originalReportID: reportID,
62+
isPinnedChat: optionItem.isPinned,
63+
isUnreadChat: !!optionItem.isUnread,
64+
},
65+
reportAction: {
66+
reportActionID: '-1',
67+
},
68+
callbacks: {
69+
onHide: () => setIsContextMenuActive(false),
70+
},
71+
withoutOverlay: false,
72+
});
73+
};
74+
75+
return (
76+
<Hoverable>
77+
{(hovered) => (
78+
<PressableWithSecondaryInteraction
79+
ref={popoverAnchor}
80+
onPress={onPress}
81+
onMouseDown={(event) => {
82+
// Allow composer blur on right click
83+
if (!event) {
84+
return;
85+
}
86+
// Prevent composer blur on left click
87+
event.preventDefault();
88+
}}
89+
testID={testID}
90+
onSecondaryInteraction={(event) => {
91+
showPopover(event);
92+
// Ensure that we blur the composer when opening context menu, so that only one component is focused at a time
93+
if (DomUtils.getActiveElement()) {
94+
(DomUtils.getActiveElement() as HTMLElement | null)?.blur();
95+
}
96+
}}
97+
withoutFocusOnSecondaryInteraction
98+
activeOpacity={variables.pressDimValue}
99+
opacityAnimationDuration={0}
100+
style={[
101+
styles.flexRow,
102+
styles.alignItemsCenter,
103+
styles.justifyContentBetween,
104+
styles.sidebarLink,
105+
styles.sidebarLinkInnerLHN,
106+
StyleUtils.getBackgroundColorStyle(theme.sidebar),
107+
isOptionFocused ? styles.sidebarLinkActive : null,
108+
(hovered || isContextMenuActive) && !isOptionFocused ? styles.sidebarLinkHover : null,
109+
]}
110+
role={CONST.ROLE.BUTTON}
111+
accessibilityLabel={accessibilityLabel}
112+
accessibilityHint={accessibilityHint}
113+
onLayout={onLayout}
114+
needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2}
115+
sentryLabel={CONST.SENTRY_LABEL.LHN.OPTION_ROW}
116+
>
117+
{children(hovered)}
118+
</PressableWithSecondaryInteraction>
119+
)}
120+
</Hoverable>
121+
);
122+
}
123+
124+
OptionRowPressable.displayName = 'OptionRowPressable';
125+
126+
export default OptionRowPressable;

0 commit comments

Comments
 (0)