Skip to content

Commit

Permalink
Merge pull request #34754 from esh-g/fix-overflow-context
Browse files Browse the repository at this point in the history
Fix Overflow menu from keyboard and show left-out options only
  • Loading branch information
mountiny authored Jan 26, 2024
2 parents 6ec827d + 51a4c64 commit e92ffbf
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 53 deletions.
6 changes: 3 additions & 3 deletions src/libs/calculateAnchorPosition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
import type {View} from 'react-native';
/* eslint-disable no-restricted-imports */
import type {Text as RNText, View} from 'react-native';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
Expand All @@ -13,7 +13,7 @@ type AnchorOrigin = {
/**
* Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
*/
export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise<AnchorPosition> {
export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise<AnchorPosition> {
return new Promise((resolve) => {
if (!anchorComponent) {
return resolve({horizontal: 0, vertical: 0});
Expand Down
58 changes: 48 additions & 10 deletions src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import lodashIsEqual from 'lodash/isEqual';
import type {MutableRefObject, RefObject} from 'react';
import React, {memo, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ContextMenuItemHandle} from '@components/ContextMenuItem';
Expand All @@ -12,15 +14,16 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ReportUtils from '@libs/ReportUtils';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, ReportAction, ReportActions} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {ContextMenuActionPayload} from './ContextMenuActions';
import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions';
import ContextMenuActions from './ContextMenuActions';
import type {ContextMenuType} from './ReportActionContextMenu';
import {hideContextMenu} from './ReportActionContextMenu';
import {hideContextMenu, showContextMenu} from './ReportActionContextMenu';

type BaseReportActionContextMenuOnyxProps = {
/** Beta features list */
Expand Down Expand Up @@ -78,7 +81,11 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & {
/** Content Ref */
contentRef?: RefObject<View>;

/** Function to check if context menu is active */
checkIfContextMenuActive?: () => void;

/** List of disabled actions */
disabledActions?: ContextMenuAction[];
};

type MenuItemRefs = Record<string, ContextMenuItemHandle | null>;
Expand All @@ -100,6 +107,7 @@ function BaseReportActionContextMenu({
betas,
reportActions,
checkIfContextMenuActive,
disabledActions = [],
}: BaseReportActionContextMenuProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
Expand All @@ -117,13 +125,22 @@ function BaseReportActionContextMenu({
}, [reportActions, reportActionID]);

const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen);
let filteredContextMenuActions = ContextMenuActions.filter((contextAction) =>
contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini),
let filteredContextMenuActions = ContextMenuActions.filter(
(contextAction) =>
!disabledActions.includes(contextAction) &&
contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini),
);
filteredContextMenuActions =
isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS
? ([...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)] as typeof filteredContextMenuActions)
: filteredContextMenuActions;

if (isMini) {
const menuAction = filteredContextMenuActions.at(-1);
const otherActions = filteredContextMenuActions.slice(0, -1);
if (otherActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && menuAction) {
filteredContextMenuActions = otherActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1);
filteredContextMenuActions.push(menuAction);
} else {
filteredContextMenuActions = otherActions;
}
}

// Context menu actions that are not rendered as menu items are excluded from arrow navigation
const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) =>
Expand Down Expand Up @@ -172,6 +189,28 @@ function BaseReportActionContextMenu({
{isActive: shouldEnableArrowNavigation},
);

const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => {
const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
const originalReport = ReportUtils.getReport(originalReportID);
showContextMenu(
CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
event,
selection,
anchor?.current as ViewType | RNText | null,
reportID,
reportAction?.reportActionID,
originalReportID,
draftMessage,
checkIfContextMenuActive,
checkIfContextMenuActive,
ReportUtils.isArchivedRoom(originalReport),
ReportUtils.chatIncludesChronos(originalReport),
undefined,
undefined,
filteredContextMenuActions,
);
};

return (
(isVisible || shouldKeepOpen) && (
<View
Expand All @@ -188,8 +227,7 @@ function BaseReportActionContextMenu({
close: () => setShouldKeepOpen(false),
openContextMenu: () => setShouldKeepOpen(true),
interceptAnonymousUser,
anchor,
checkIfContextMenuActive,
openOverflowMenu,
};

if ('renderContent' in contextAction) {
Expand Down
31 changes: 7 additions & 24 deletions src/pages/home/report/ContextMenu/ContextMenuActions.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import type {MutableRefObject} from 'react';
import React from 'react';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, View} from 'react-native';
import type {GestureResponderEvent} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {Emoji} from '@assets/emojis/types';
import * as Expensicons from '@components/Icon/Expensicons';
Expand All @@ -29,7 +28,7 @@ import type {TranslationPaths} from '@src/languages/types';
import ROUTES from '@src/ROUTES';
import type {Beta, ReportAction, ReportActionReactions} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
import {hideContextMenu, showContextMenu, showDeleteModal} from './ReportActionContextMenu';
import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu';

/** Gets the HTML version of the message in an action */
function getActionText(reportAction: OnyxEntry<ReportAction>): string {
Expand Down Expand Up @@ -70,8 +69,7 @@ type ContextMenuActionPayload = {
close: () => void;
openContextMenu: () => void;
interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void;
anchor?: MutableRefObject<HTMLElement | View | Text | null>;
checkIfContextMenuActive?: () => void;
openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void;
event?: GestureResponderEvent | MouseEvent | KeyboardEvent;
};

Expand Down Expand Up @@ -240,7 +238,7 @@ const ContextMenuActions: ContextMenuAction[] = [
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.markAsUnread',
icon: Expensicons.Mail,
icon: Expensicons.ChatBubbleUnread,
successIcon: Expensicons.Checkmark,
shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) =>
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat),
Expand Down Expand Up @@ -502,27 +500,12 @@ const ContextMenuActions: ContextMenuAction[] = [
textTranslateKey: 'reportActionContextMenu.menu',
icon: Expensicons.ThreeDots,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini,
onPress: (closePopover, {reportAction, reportID, event, anchor, selection, draftMessage, checkIfContextMenuActive}) => {
const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
const originalReport = ReportUtils.getReport(originalReportID);
showContextMenu(
CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
event as GestureResponderEvent | MouseEvent,
selection,
anchor?.current as View | RNText | null,
reportID,
reportAction.reportActionID,
originalReportID,
draftMessage,
checkIfContextMenuActive,
checkIfContextMenuActive,
ReportUtils.isArchivedRoom(originalReport),
ReportUtils.chatIncludesChronos(originalReport),
);
onPress: (closePopover, {openOverflowMenu, event}) => {
openOverflowMenu(event as GestureResponderEvent | MouseEvent);
},
getDescription: () => {},
},
];

export default ContextMenuActions;
export type {ContextMenuActionPayload};
export type {ContextMenuActionPayload, ContextMenuAction};
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native';

/* eslint-disable no-restricted-imports */
import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, Text as RNText, View} from 'react-native';
import {Dimensions} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import ConfirmModal from '@components/ConfirmModal';
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
import useLocalize from '@hooks/useLocalize';
import calculateAnchorPosition from '@libs/calculateAnchorPosition';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as IOU from '@userActions/IOU';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import type {ReportAction} from '@src/types/onyx';
import BaseReportActionContextMenu from './BaseReportActionContextMenu';
import type {ContextMenuAction} from './ContextMenuActions';
import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu';

type ContextMenuAnchorCallback = (x: number, y: number) => void;

type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void};

type Location = {
x: number;
y: number;
Expand Down Expand Up @@ -62,11 +62,12 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef<ReportA
const [isChronosReportEnabled, setIsChronosReportEnabled] = useState(false);
const [isChatPinned, setIsChatPinned] = useState(false);
const [hasUnreadMessages, setHasUnreadMessages] = useState(false);
const [disabledActions, setDisabledActions] = useState<ContextMenuAction[]>([]);

const contentRef = useRef<View>(null);
const anchorRef = useRef<View | HTMLDivElement>(null);
const dimensionsEventListener = useRef<EmitterSubscription | null>(null);
const contextMenuAnchorRef = useRef<ContextMenuAnchor | null>(null);
const contextMenuAnchorRef = useRef<View | RNText | null>(null);
const contextMenuTargetNode = useRef<HTMLElement | null>(null);

const onPopoverShow = useRef(() => {});
Expand Down Expand Up @@ -161,6 +162,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef<ReportA
isChronosReport = false,
isPinnedChat = false,
isUnreadChat = false,
disabledOptions = [],
) => {
const {pageX = 0, pageY = 0} = extractPointerEvent(event);
contextMenuAnchorRef.current = contextMenuAnchor;
Expand All @@ -171,16 +173,27 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef<ReportA
onPopoverShow.current = onShow;
onPopoverHide.current = onHide;

getContextMenuMeasuredLocation().then(({x, y}) => {
popoverAnchorPosition.current = {
horizontal: pageX - x,
vertical: pageY - y,
};

popoverAnchorPosition.current = {
horizontal: pageX,
vertical: pageY,
};
new Promise<void>((resolve) => {
if (!pageX && !pageY && contextMenuAnchorRef.current) {
calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => {
popoverAnchorPosition.current = position;
resolve();
});
} else {
getContextMenuMeasuredLocation().then(({x, y}) => {
cursorRelativePosition.current = {
horizontal: pageX - x,
vertical: pageY - y,
};
popoverAnchorPosition.current = {
horizontal: pageX,
vertical: pageY,
};
resolve();
});
}
}).then(() => {
setDisabledActions(disabledOptions);
typeRef.current = type;
reportIDRef.current = reportID ?? '0';
reportActionIDRef.current = reportActionID ?? '0';
Expand Down Expand Up @@ -310,6 +323,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef<ReportA
anchor={contextMenuTargetNode}
contentRef={contentRef}
originalReportID={originalReportIDRef.current}
disabledActions={disabledActions}
/>
</PopoverWithMeasuredContent>
<ConfirmModal
Expand Down
4 changes: 4 additions & 0 deletions src/pages/home/report/ContextMenu/ReportActionContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type {ReportAction} from '@src/types/onyx';
import type {ContextMenuAction} from './ContextMenuActions';

type OnHideCallback = () => void;

Expand All @@ -30,6 +31,7 @@ type ShowContextMenu = (
isChronosReport?: boolean,
isPinnedChat?: boolean,
isUnreadChat?: boolean,
disabledOptions?: ContextMenuAction[],
) => void;

type ReportActionContextMenu = {
Expand Down Expand Up @@ -108,6 +110,7 @@ function showContextMenu(
isChronosReport = false,
isPinnedChat = false,
isUnreadChat = false,
disabledActions: ContextMenuAction[] = [],
) {
if (!contextMenuRef.current) {
return;
Expand All @@ -134,6 +137,7 @@ function showContextMenu(
isChronosReport,
isPinnedChat,
isUnreadChat,
disabledActions,
);
}

Expand Down

0 comments on commit e92ffbf

Please sign in to comment.