Skip to content

Commit 726c3e8

Browse files
authored
Merge pull request #55454 from ishpaul777/make-tooltips-pressable
Add onTooltipPress callback to various tooltip components
2 parents 66dc2df + fdd058a commit 726c3e8

File tree

24 files changed

+199
-97
lines changed

24 files changed

+199
-97
lines changed

src/CONST.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4629,6 +4629,8 @@ const CONST = {
46294629
TOOLBAR: 'toolbar',
46304630
/** Use for navigation elements */
46314631
NAVIGATION: 'navigation',
4632+
/** Use for Tooltips */
4633+
TOOLTIP: 'tooltip',
46324634
},
46334635
TRANSLATION_KEYS: {
46344636
ATTACHMENT: 'common.attachment',
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Animated from 'react-native-reanimated';
2+
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
3+
4+
const AnimatedPressableWithoutFeedback = Animated.createAnimatedComponent(PressableWithoutFeedback);
5+
AnimatedPressableWithoutFeedback.displayName = 'AnimatedPressableWithoutFeedback';
6+
7+
export default AnimatedPressableWithoutFeedback;

src/components/FloatingActionButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
128128
renderTooltipContent={renderProductTrainingTooltip}
129129
wrapperStyle={styles.productTrainingTooltipWrapper}
130130
shouldHideOnNavigate={false}
131+
onTooltipPress={toggleFabAction}
131132
>
132133
<PressableWithoutFeedback
133134
ref={(el) => {

src/components/LHNOptionsList/OptionRowLHN.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
168168
const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar;
169169
const firstIcon = optionItem.icons?.at(0);
170170

171+
const onOptionPress = (event: GestureResponderEvent | KeyboardEvent | undefined) => {
172+
Performance.markStart(CONST.TIMING.OPEN_REPORT);
173+
Timing.start(CONST.TIMING.OPEN_REPORT);
174+
175+
event?.preventDefault();
176+
// Enable Composer to focus on clicking the same chat after opening the context menu.
177+
ReportActionComposeFocusManager.focus();
178+
hideProductTrainingTooltip();
179+
onSelectRow(optionItem, popoverAnchor);
180+
};
181+
171182
return (
172183
<OfflineWithFeedback
173184
pendingAction={optionItem.pendingAction}
@@ -186,22 +197,14 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
186197
shiftHorizontal={shouldShowWokspaceChatTooltip ? variables.workspaceLHNtooltipShiftHorizontal : variables.gbrTooltipShiftHorizontal}
187198
shiftVertical={shouldShowWokspaceChatTooltip ? 0 : variables.composerTooltipShiftVertical}
188199
wrapperStyle={styles.productTrainingTooltipWrapper}
200+
onTooltipPress={onOptionPress}
189201
>
190202
<View>
191203
<Hoverable>
192204
{(hovered) => (
193205
<PressableWithSecondaryInteraction
194206
ref={popoverAnchor}
195-
onPress={(event) => {
196-
Performance.markStart(CONST.TIMING.OPEN_REPORT);
197-
Timing.start(CONST.TIMING.OPEN_REPORT);
198-
199-
event?.preventDefault();
200-
// Enable Composer to focus on clicking the same chat after opening the context menu.
201-
ReportActionComposeFocusManager.focus();
202-
hideProductTrainingTooltip();
203-
onSelectRow(optionItem, popoverAnchor);
204-
}}
207+
onPress={onOptionPress}
205208
onMouseDown={(event) => {
206209
// Allow composer blur on right click
207210
if (!event) {

src/components/MenuItem.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import useTheme from '@hooks/useTheme';
1010
import useThemeStyles from '@hooks/useThemeStyles';
1111
import ControlSelection from '@libs/ControlSelection';
1212
import convertToLTR from '@libs/convertToLTR';
13-
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
13+
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
1414
import getButtonState from '@libs/getButtonState';
1515
import Parser from '@libs/Parser';
1616
import type {AvatarSource} from '@libs/UserUtils';
1717
import variables from '@styles/variables';
18-
import * as Session from '@userActions/Session';
18+
import {checkIfActionIsAllowed} from '@userActions/Session';
1919
import CONST from '@src/CONST';
2020
import type {Icon as IconType} from '@src/types/onyx/OnyxCommon';
2121
import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment';
@@ -335,6 +335,9 @@ type MenuItemBaseProps = {
335335
/** Render custom content inside the tooltip. */
336336
renderTooltipContent?: () => ReactNode;
337337

338+
/** Callback to fire when the education tooltip is pressed */
339+
onEducationTooltipPress?: () => void;
340+
338341
shouldShowLoadingSpinnerIcon?: boolean;
339342

340343
/** Should selected item be marked with checkmark */
@@ -459,6 +462,7 @@ function MenuItem(
459462
tooltipShiftHorizontal = 0,
460463
tooltipShiftVertical = 0,
461464
renderTooltipContent,
465+
onEducationTooltipPress,
462466
additionalIconStyles,
463467
shouldShowSelectedItemCheck = false,
464468
shouldIconUseAutoWidthStyle = false,
@@ -601,13 +605,14 @@ function MenuItem(
601605
shiftHorizontal={tooltipShiftHorizontal}
602606
shiftVertical={tooltipShiftVertical}
603607
shouldTeleportPortalToModalLayer={shouldTeleportPortalToModalLayer}
608+
onTooltipPress={onEducationTooltipPress}
604609
>
605610
<View>
606611
<Hoverable>
607612
{(isHovered) => (
608613
<PressableWithSecondaryInteraction
609-
onPress={shouldCheckActionAllowedOnPress ? Session.checkIfActionIsAllowed(onPressAction, isAnonymousAction) : onPressAction}
610-
onPressIn={() => shouldBlockSelection && shouldUseNarrowLayout && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
614+
onPress={shouldCheckActionAllowedOnPress ? checkIfActionIsAllowed(onPressAction, isAnonymousAction) : onPressAction}
615+
onPressIn={() => shouldBlockSelection && shouldUseNarrowLayout && canUseTouchScreen() && ControlSelection.block()}
611616
onPressOut={ControlSelection.unblock}
612617
onSecondaryInteraction={onSecondaryInteraction}
613618
wrapperStyle={outerWrapperStyle}

src/components/PopoverProvider/index.native.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const PopoverContext = React.createContext<PopoverContextValue>({
66
popover: null,
77
close: () => {},
88
isOpen: false,
9+
setActivePopoverExtraAnchorRef: () => {},
910
});
1011

1112
function PopoverContextProvider(props: PopoverContextProps) {
@@ -15,6 +16,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
1516
close: () => {},
1617
popover: null,
1718
isOpen: false,
19+
setActivePopoverExtraAnchorRef: () => {},
1820
}),
1921
[],
2022
);

src/components/PopoverProvider/index.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const PopoverContext = createContext<PopoverContextValue>({
1010
popoverAnchor: null,
1111
close: () => {},
1212
isOpen: false,
13+
setActivePopoverExtraAnchorRef: () => {},
1314
});
1415

1516
function elementContains(ref: RefObject<View | HTMLElement | Text> | undefined, target: EventTarget | null) {
@@ -23,6 +24,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
2324
const [isOpen, setIsOpen] = useState(false);
2425
const activePopoverRef = useRef<AnchorRef | null>(null);
2526
const [activePopoverAnchor, setActivePopoverAnchor] = useState<AnchorRef['anchorRef']['current']>(null);
27+
const [activePopoverExtraAnchorRefs, setActivePopoverExtraAnchorRefs] = useState<AnchorRef['extraAnchorRefs']>([]);
2628

2729
const closePopover = useCallback((anchorRef?: RefObject<View | HTMLElement | Text>): boolean => {
2830
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
@@ -41,14 +43,19 @@ function PopoverContextProvider(props: PopoverContextProps) {
4143
if (elementContains(activePopoverRef.current?.ref, e.target) || elementContains(activePopoverRef.current?.anchorRef, e.target)) {
4244
return;
4345
}
46+
// Incase there are any extra anchor refs where the popover should not close on click
47+
// for example, the case when the QAB tooltip is clicked it closes the popover this will prevent that
48+
if (activePopoverExtraAnchorRefs?.some((ref: RefObject<View | HTMLElement | Text>) => elementContains(ref, e.target))) {
49+
return;
50+
}
4451
const ref = activePopoverRef.current?.anchorRef;
4552
closePopover(ref);
4653
};
4754
document.addEventListener('click', listener, true);
4855
return () => {
4956
document.removeEventListener('click', listener, true);
5057
};
51-
}, [closePopover]);
58+
}, [closePopover, activePopoverExtraAnchorRefs]);
5259

5360
useEffect(() => {
5461
const listener = (e: Event) => {
@@ -117,16 +124,34 @@ function PopoverContextProvider(props: PopoverContextProps) {
117124
[closePopover],
118125
);
119126

127+
// To set the extra anchor refs for the popover when prop-drilling is not possible
128+
const setActivePopoverExtraAnchorRef = useCallback((extraAnchorRef?: RefObject<View | HTMLDivElement | Text>) => {
129+
if (!extraAnchorRef) {
130+
return;
131+
}
132+
setActivePopoverExtraAnchorRefs((prev: AnchorRef['extraAnchorRefs']) => {
133+
if (!prev) {
134+
return [extraAnchorRef];
135+
}
136+
137+
if (prev?.includes(extraAnchorRef)) {
138+
return prev;
139+
}
140+
return [...prev, extraAnchorRef];
141+
});
142+
}, []);
143+
120144
const contextValue = useMemo(
121145
() => ({
122146
onOpen,
147+
setActivePopoverExtraAnchorRef,
123148
close: closePopover,
124149
// eslint-disable-next-line react-compiler/react-compiler
125150
popover: activePopoverRef.current,
126151
popoverAnchor: activePopoverAnchor,
127152
isOpen,
128153
}),
129-
[onOpen, closePopover, isOpen, activePopoverAnchor],
154+
[onOpen, closePopover, isOpen, activePopoverAnchor, setActivePopoverExtraAnchorRef],
130155
);
131156

132157
return <PopoverContext.Provider value={contextValue}>{props.children}</PopoverContext.Provider>;

src/components/PopoverProvider/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ type PopoverContextValue = {
1212
popoverAnchor?: AnchorRef['anchorRef']['current'];
1313
close: (anchorRef?: RefObject<View | HTMLDivElement | Text>) => void;
1414
isOpen: boolean;
15+
setActivePopoverExtraAnchorRef: (ref?: RefObject<View | HTMLDivElement | Text>) => void;
1516
};
1617

1718
type AnchorRef = {
1819
ref: RefObject<View | HTMLDivElement | Text>;
1920
close: (anchorRef?: RefObject<View | HTMLDivElement | Text>) => void;
2021
anchorRef: RefObject<View | HTMLDivElement | Text>;
22+
extraAnchorRefs?: Array<RefObject<View | HTMLDivElement | Text>>;
2123
};
2224

2325
export type {PopoverContextProps, PopoverContextValue, AnchorRef};

src/components/Search/SearchPageHeader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) {
392392
shiftHorizontal={variables.searchFiltersTooltipShiftHorizontal}
393393
wrapperStyle={styles.productTrainingTooltipWrapper}
394394
renderTooltipContent={renderProductTrainingTooltip}
395+
onTooltipPress={onFiltersButtonPress}
395396
>
396397
<Button
397398
innerStyles={!isCannedQuery && [styles.searchRouterInputResults, styles.borderNone]}

src/components/ThreeDotsMenu/index.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import Icon from '@components/Icon';
55
import * as Expensicons from '@components/Icon/Expensicons';
66
import PopoverMenu from '@components/PopoverMenu';
77
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
8+
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
89
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
910
import useLocalize from '@hooks/useLocalize';
1011
import useTheme from '@hooks/useTheme';
1112
import useThemeStyles from '@hooks/useThemeStyles';
12-
import * as Browser from '@libs/Browser';
13+
import {isMobile} from '@libs/Browser';
14+
import variables from '@styles/variables';
1315
import CONST from '@src/CONST';
1416
import ONYXKEYS from '@src/ONYXKEYS';
1517
import type ThreeDotsMenuProps from './types';
@@ -30,6 +32,8 @@ function ThreeDotsMenu({
3032
shouldSetModalVisibility = true,
3133
disabled = false,
3234
hideProductTrainingTooltip,
35+
renderProductTrainingTooltipContent,
36+
shouldShowProductTrainingTooltip = false,
3337
}: ThreeDotsMenuProps) {
3438
const [modal] = useOnyx(ONYXKEYS.MODAL);
3539

@@ -55,31 +59,46 @@ function ThreeDotsMenu({
5559
hidePopoverMenu();
5660
}, [isBehindModal, isPopupMenuVisible]);
5761

62+
const onThreeDotsPress = () => {
63+
if (isPopupMenuVisible) {
64+
hidePopoverMenu();
65+
return;
66+
}
67+
hideProductTrainingTooltip?.();
68+
buttonRef.current?.blur();
69+
showPopoverMenu();
70+
if (onIconPress) {
71+
onIconPress();
72+
}
73+
};
74+
75+
const TooltipToRender = shouldShowProductTrainingTooltip ? EducationalTooltip : Tooltip;
76+
const tooltipProps = shouldShowProductTrainingTooltip
77+
? {
78+
renderTooltipContent: renderProductTrainingTooltipContent,
79+
shouldRender: shouldShowProductTrainingTooltip,
80+
anchorAlignment: {
81+
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
82+
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
83+
},
84+
shiftHorizontal: variables.savedSearchShiftHorizontal,
85+
shiftVertical: variables.savedSearchShiftVertical,
86+
wrapperStyle: [styles.mh4, styles.pv2, styles.productTrainingTooltipWrapper],
87+
onTooltipPress: onThreeDotsPress,
88+
}
89+
: {text: translate(iconTooltip), shouldRender: true};
90+
5891
return (
5992
<>
6093
<View>
61-
<Tooltip
62-
text={translate(iconTooltip)}
63-
// We need to hide the extra "More" tooltip when we have an educational tooltip
64-
shouldRender={!hideProductTrainingTooltip}
65-
>
94+
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
95+
<TooltipToRender {...tooltipProps}>
6696
<PressableWithoutFeedback
67-
onPress={() => {
68-
if (isPopupMenuVisible) {
69-
hidePopoverMenu();
70-
return;
71-
}
72-
hideProductTrainingTooltip?.();
73-
buttonRef.current?.blur();
74-
showPopoverMenu();
75-
if (onIconPress) {
76-
onIconPress();
77-
}
78-
}}
97+
onPress={onThreeDotsPress}
7998
disabled={disabled}
8099
onMouseDown={(e) => {
81100
/* Keep the focus state on mWeb like we did on the native apps. */
82-
if (!Browser.isMobile()) {
101+
if (!isMobile()) {
83102
return;
84103
}
85104
e.preventDefault();
@@ -94,7 +113,7 @@ function ThreeDotsMenu({
94113
fill={iconFill ?? isPopupMenuVisible ? theme.success : theme.icon}
95114
/>
96115
</PressableWithoutFeedback>
97-
</Tooltip>
116+
</TooltipToRender>
98117
</View>
99118
<PopoverMenu
100119
onClose={hidePopoverMenu}

src/components/ThreeDotsMenu/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ type ThreeDotsMenuProps = {
4141

4242
/** Function to hide the product training tooltip */
4343
hideProductTrainingTooltip?: () => void;
44+
45+
/** Tooltip content to render */
46+
renderProductTrainingTooltipContent?: () => React.JSX.Element;
47+
48+
/** Should we render the tooltip */
49+
shouldShowProductTrainingTooltip?: boolean;
4450
};
4551

4652
export default ThreeDotsMenuProps;

src/components/Tooltip/BaseGenericTooltip/index.native.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {View} from 'react-native';
44
// eslint-disable-next-line no-restricted-imports
55
import type {View as RNView} from 'react-native';
66
import Animated, {useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
7+
import AnimatedPressableWithoutFeedback from '@components/AnimatedPressableWithoutFeedback';
78
import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay';
89
import Text from '@components/Text';
910
import useStyleUtils from '@hooks/useStyleUtils';
@@ -38,6 +39,7 @@ function BaseGenericTooltip({
3839
onHideTooltip = () => {},
3940
shouldTeleportPortalToModalLayer = false,
4041
isEducationTooltip = false,
42+
onTooltipPress = () => {},
4143
}: BaseGenericTooltipProps) {
4244
// The width of tooltip's inner content. Has to be undefined in the beginning
4345
// as a width of 0 will cause the content to be rendered of a width of 0,
@@ -113,12 +115,17 @@ function BaseGenericTooltip({
113115
);
114116
}
115117

118+
const AnimatedWrapper = isEducationTooltip ? AnimatedPressableWithoutFeedback : Animated.View;
119+
116120
return (
117121
<Portal hostName={shouldTeleportPortalToModalLayer ? 'modal' : undefined}>
118122
{shouldUseOverlay && <TransparentOverlay onPress={onHideTooltip} />}
119-
<Animated.View
120-
ref={rootWrapper}
123+
<AnimatedWrapper
121124
style={[rootWrapperStyle, animationStyle]}
125+
ref={rootWrapper}
126+
onPress={isEducationTooltip ? onTooltipPress : undefined}
127+
role={isEducationTooltip ? CONST.ROLE.TOOLTIP : undefined}
128+
accessibilityLabel={isEducationTooltip ? CONST.ROLE.TOOLTIP : undefined}
122129
onLayout={(e) => {
123130
const {height, width} = e.nativeEvent.layout;
124131
if (height === wrapperMeasuredHeightAnimated.get()) {
@@ -137,7 +144,7 @@ function BaseGenericTooltip({
137144
<View style={pointerWrapperStyle}>
138145
<View style={pointerStyle} />
139146
</View>
140-
</Animated.View>
147+
</AnimatedWrapper>
141148
</Portal>
142149
);
143150
}

0 commit comments

Comments
 (0)