Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use PopoverMenu for account switcher modal #49051

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e46b607
use PopoverMenu for account switcher modal
nkdengineer Sep 12, 2024
7069e8c
remove unnecessary change
nkdengineer Sep 12, 2024
eb9e18b
fix ts error
nkdengineer Sep 12, 2024
e894d78
Merge branch 'main' into fix/48292
nkdengineer Sep 13, 2024
de3bf0f
fix style bug and add shouldUseScrollView prop
nkdengineer Sep 13, 2024
1a5aaa7
move iconType to above
nkdengineer Sep 13, 2024
a49cada
merge main
nkdengineer Sep 16, 2024
5be7a19
resolve conflict
nkdengineer Sep 16, 2024
95f4224
Merge branch 'main' into fix/48292
nkdengineer Sep 16, 2024
dd32f45
Merge branch 'main' into fix/48292
nkdengineer Sep 17, 2024
70aa7f3
fix header styles
nkdengineer Sep 18, 2024
43e1ff1
merge main
nkdengineer Sep 18, 2024
bed23eb
Merge branch 'main' into fix/48292
nkdengineer Sep 18, 2024
4002bc1
Update src/components/PopoverMenu.tsx
nkdengineer Sep 18, 2024
2d5c678
add headerStyles
nkdengineer Sep 18, 2024
584469a
remove unused style
nkdengineer Sep 18, 2024
a9da05f
Merge branch 'main' into fix/48292
nkdengineer Sep 19, 2024
9b9e00f
Merge branch 'main' into fix/48292
nkdengineer Sep 20, 2024
7bc72ef
Merge branch 'main' into fix/48292
nkdengineer Sep 20, 2024
3ac507c
move the padding bottom to the scroll view
nkdengineer Sep 20, 2024
5d6c9db
fix error doesn't display
nkdengineer Sep 20, 2024
366bce2
Merge branch 'main' into fix/48292
nkdengineer Sep 23, 2024
e2ee3c7
fix padding bottom style in large screen
nkdengineer Sep 23, 2024
6556abd
remove hard code
nkdengineer Sep 23, 2024
907b6e0
Merge branch 'main' into fix/48292
nkdengineer Sep 24, 2024
84291a0
add shouldUpdateFocusedIndex prop
nkdengineer Sep 24, 2024
3a09746
Merge branch 'main' into fix/48292
nkdengineer Sep 25, 2024
6820937
fix lint
nkdengineer Sep 25, 2024
a58ff31
change onItemSelected as optional
nkdengineer Sep 25, 2024
4fde281
use lodashIsEqual
nkdengineer Sep 25, 2024
8692d40
merge main
nkdengineer Sep 27, 2024
1e8b7ce
resovle conflict
nkdengineer Sep 27, 2024
b326494
fix perf-test
nkdengineer Sep 27, 2024
c3ed169
Merge branch 'main' into fix/48292
nkdengineer Sep 30, 2024
92d3081
revert change
nkdengineer Sep 30, 2024
a6770f8
revert change
nkdengineer Sep 30, 2024
36b7519
merge main
nkdengineer Oct 2, 2024
25547dc
Merge branch 'main' into fix/48292
nkdengineer Oct 3, 2024
ab098d6
Merge branch 'main' into fix/48292
nkdengineer Oct 4, 2024
69fefcd
Merge branch 'main' into fix/48292
nkdengineer Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ const CONST = {
IN: 'in',
OUT: 'out',
},
POPOVER_ACCOUNT_SWITCHER_POSITION: {
horizontal: 12,
vertical: 80,
},
nkdengineer marked this conversation as resolved.
Show resolved Hide resolved
// Multiplier for gyroscope animation in order to make it a bit more subtle
ANIMATION_GYROSCOPE_VALUE: 0.4,
ANIMATION_PAID_DURATION: 200,
Expand Down
58 changes: 31 additions & 27 deletions src/components/AccountSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
Expand All @@ -22,10 +23,8 @@ import Avatar from './Avatar';
import ConfirmModal from './ConfirmModal';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import type {MenuItemProps} from './MenuItem';
import MenuItemList from './MenuItemList';
import type {MenuItemWithLink} from './MenuItemList';
import Popover from './Popover';
import type {PopoverMenuItem} from './PopoverMenu';
import PopoverMenu from './PopoverMenu';
import {PressableWithFeedback} from './Pressable';
import Text from './Text';

Expand All @@ -41,6 +40,7 @@ function AccountSwitcher() {
const [session] = useOnyx(ONYXKEYS.SESSION);
const [user] = useOnyx(ONYXKEYS.USER);
const buttonRef = useRef<HTMLDivElement>(null);
const {windowHeight} = useWindowDimensions();

const [shouldShowDelegatorMenu, setShouldShowDelegatorMenu] = useState(false);
const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false);
Expand All @@ -49,10 +49,14 @@ function AccountSwitcher() {
const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false;
const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate);

const createBaseMenuItem = (personalDetails: PersonalDetails | undefined, errors?: Errors, additionalProps: MenuItemWithLink = {}): MenuItemWithLink => {
const createBaseMenuItem = (
personalDetails: PersonalDetails | undefined,
errors?: Errors,
additionalProps: Partial<Omit<PopoverMenuItem, 'icon' | 'iconType'>> = {},
): PopoverMenuItem => {
const error = Object.values(errors ?? {})[0] ?? '';
return {
title: personalDetails?.displayName ?? personalDetails?.login,
text: personalDetails?.displayName ?? personalDetails?.login ?? '',
description: Str.removeSMSDomain(personalDetails?.login ?? ''),
avatarID: personalDetails?.accountID ?? -1,
icon: personalDetails?.avatar ?? '',
Expand All @@ -66,14 +70,12 @@ function AccountSwitcher() {
};
};

const menuItems = (): MenuItemProps[] => {
const menuItems = (): PopoverMenuItem[] => {
const currentUserMenuItem = createBaseMenuItem(currentUserPersonalDetails, undefined, {
wrapperStyle: [styles.buttonDefaultBG],
focused: true,
shouldShowRightIcon: true,
iconRight: Expensicons.Checkmark,
success: true,
key: `${currentUserPersonalDetails?.login}-current`,
isSelected: true,
});

if (isActingAsDelegate) {
Expand All @@ -89,34 +91,32 @@ function AccountSwitcher() {

return [
createBaseMenuItem(delegatePersonalDetails, error, {
onPress: () => {
onSelected: () => {
if (isOffline) {
Modal.close(() => setShouldShowOfflineModal(true));
return;
}
disconnect();
},
key: `${delegateEmail}-delegate`,
}),
currentUserMenuItem,
];
}

const delegatorMenuItems: MenuItemProps[] = delegators
const delegatorMenuItems: PopoverMenuItem[] = delegators
.filter(({email}) => email !== currentUserPersonalDetails.login)
.map(({email, role, errorFields}, index) => {
.map(({email, role, errorFields}) => {
const error = ErrorUtils.getLatestErrorField({errorFields}, 'connect');
const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email);
return createBaseMenuItem(personalDetails, error, {
badgeText: translate('delegate.role', {role}),
onPress: () => {
onSelected: () => {
if (isOffline) {
Modal.close(() => setShouldShowOfflineModal(true));
return;
}
connect(email);
},
key: `${email}-${index}`,
});
});

Expand Down Expand Up @@ -181,23 +181,27 @@ function AccountSwitcher() {
</View>
</PressableWithFeedback>
{canSwitchAccounts && (
<Popover
<PopoverMenu
isVisible={shouldShowDelegatorMenu}
onClose={() => {
setShouldShowDelegatorMenu(false);
clearDelegatorErrors();
}}
anchorRef={buttonRef}
anchorPosition={styles.accountSwitcherAnchorPosition}
>
<View style={styles.pb4}>
<Text style={[styles.createMenuHeaderText, styles.ph5, styles.pb3, !shouldUseNarrowLayout && styles.pt4]}>{translate('delegate.switchAccount')}</Text>
<MenuItemList
menuItems={menuItems()}
shouldUseSingleExecution
/>
</View>
</Popover>
anchorPosition={CONST.POPOVER_ACCOUNT_SWITCHER_POSITION}
anchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
}}
menuItems={menuItems()}
headerText={translate('delegate.switchAccount')}
containerStyles={[{maxHeight: windowHeight / 2}, styles.pb0, styles.mw100, styles.wFitContent]}
headerStyles={styles.pt0}
innerContainerStyle={styles.pb0}
scrollContainerStyle={styles.pb4}
shouldUseScrollView
shouldUpdateFocusedIndex={false}
/>
)}
<ConfirmModal
title={translate('common.youAppearToBeOffline')}
Expand Down
129 changes: 70 additions & 59 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable react/jsx-props-no-spreading */
import lodashIsEqual from 'lodash/isEqual';
import type {RefObject} from 'react';
import React, {useLayoutEffect, useState} from 'react';
import {StyleSheet} from 'react-native';
import type {View} from 'react-native';
import React, {Fragment, useLayoutEffect, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import type {ModalProps} from 'react-native-modal';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
Expand Down Expand Up @@ -67,7 +68,7 @@ type PopoverMenuProps = Partial<PopoverModalProps> & {
isVisible: boolean;

/** Callback to fire when a CreateMenu item is selected */
onItemSelected: (selectedItem: PopoverMenuItem, index: number) => void;
onItemSelected?: (selectedItem: PopoverMenuItem, index: number) => void;

/** Menu items to be rendered on the list */
menuItems: PopoverMenuItem[];
Expand Down Expand Up @@ -107,6 +108,24 @@ type PopoverMenuProps = Partial<PopoverModalProps> & {

/** Whether to show the selected option checkmark */
shouldShowSelectedItemCheck?: boolean;

/** The style of content container which wraps all child views */
containerStyles?: StyleProp<ViewStyle>;

/** Used to apply styles specifically to the header text */
headerStyles?: StyleProp<TextStyle>;

/** Modal container styles */
innerContainerStyle?: ViewStyle;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
innerContainerStyle?: ViewStyle;
innerContainerStyle?: StyleProp<ViewStyle>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

innerContainerStyle of BaseModal has style is ViewStyle

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another point, other style prop names don't have s at the end, should we rename headerStyles


/** These styles will be applied to the scroll view content container which wraps all of the child views */
scrollContainerStyle?: StyleProp<ViewStyle>;

/** Whether we should wrap the list item in a scroll view */
shouldUseScrollView?: boolean;

/** Whether to update the focused index on a row select */
shouldUpdateFocusedIndex?: boolean;
};

function PopoverMenu({
Expand All @@ -132,6 +151,12 @@ function PopoverMenu({
shouldEnableNewFocusManagement,
restoreFocusType,
shouldShowSelectedItemCheck = false,
containerStyles,
headerStyles,
innerContainerStyle,
scrollContainerStyle,
shouldUseScrollView = false,
shouldUpdateFocusedIndex = true,
}: PopoverMenuProps) {
const styles = useThemeStyles();
const theme = useTheme();
Expand All @@ -143,6 +168,7 @@ function PopoverMenu({
const {windowHeight} = useWindowDimensions();

const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: currentMenuItemsFocusedIndex, maxIndex: currentMenuItems.length - 1, isActive: isVisible});
const WrapComponent = shouldUseScrollView ? ScrollView : Fragment;

const selectItem = (index: number) => {
const selectedItem = currentMenuItems[index];
Expand All @@ -152,7 +178,7 @@ function PopoverMenu({
const selectedSubMenuItemIndex = selectedItem?.subMenuItems.findIndex((option) => option.isSelected);
setFocusedIndex(selectedSubMenuItemIndex);
} else if (selectedItem.shouldCallAfterModalHide && !Browser.isSafari()) {
onItemSelected(selectedItem, index);
onItemSelected?.(selectedItem, index);
Modal.close(
() => {
selectedItem.onSelected?.();
Expand All @@ -161,7 +187,7 @@ function PopoverMenu({
selectedItem.shouldCloseAllModals,
);
} else {
onItemSelected(selectedItem, index);
onItemSelected?.(selectedItem, index);
selectedItem.onSelected?.();
}
};
Expand Down Expand Up @@ -207,7 +233,7 @@ function PopoverMenu({
if (!headerText || enteredSubMenuIndexes.length !== 0) {
return;
}
return <Text style={[styles.createMenuHeaderText, styles.ph5, styles.pv3]}>{headerText}</Text>;
return <Text style={[styles.createMenuHeaderText, styles.ph5, styles.pv3, headerStyles]}>{headerText}</Text>;
};

useKeyboardShortcut(
Expand Down Expand Up @@ -260,61 +286,46 @@ function PopoverMenu({
shouldEnableNewFocusManagement={shouldEnableNewFocusManagement}
useNativeDriver
restoreFocusType={restoreFocusType}
innerContainerStyle={innerContainerStyle}
>
<FocusTrapForModal active={isVisible}>
<ScrollView style={isSmallScreenWidth ? {maxHeight: windowHeight - 250} : styles.createMenuContainer}>
<View style={[isSmallScreenWidth ? {maxHeight: windowHeight - 250} : styles.createMenuContainer, containerStyles]}>
{renderHeaderText()}
{enteredSubMenuIndexes.length > 0 && renderBackButtonItem()}
{currentMenuItems.map((item, menuIndex) => (
<OfflineWithFeedback
// eslint-disable-next-line react/no-array-index-key
key={`${item.text}_${menuIndex}`}
pendingAction={item.pendingAction}
>
<FocusableMenuItem
icon={item.icon}
iconWidth={item.iconWidth}
iconHeight={item.iconHeight}
iconFill={item.iconFill}
contentFit={item.contentFit}
title={item.text}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])}
shouldCheckActionAllowedOnPress={false}
description={item.description}
numberOfLinesDescription={item.numberOfLinesDescription}
onPress={() => selectItem(menuIndex)}
focused={focusedIndex === menuIndex}
displayInDefaultIconColor={item.displayInDefaultIconColor}
shouldShowRightIcon={item.shouldShowRightIcon}
shouldShowRightComponent={item.shouldShowRightComponent}
iconRight={item.iconRight}
rightComponent={item.rightComponent}
shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon}
label={item.label}
style={{backgroundColor: item.isSelected ? theme.activeComponentBG : undefined}}
isLabelHoverable={item.isLabelHoverable}
floatRightAvatars={item.floatRightAvatars}
floatRightAvatarSize={item.floatRightAvatarSize}
shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar}
disabled={item.disabled}
onFocus={() => setFocusedIndex(menuIndex)}
success={item.success}
containerStyle={item.containerStyle}
shouldRenderTooltip={item.shouldRenderTooltip}
tooltipAnchorAlignment={item.tooltipAnchorAlignment}
tooltipShiftHorizontal={item.tooltipShiftHorizontal}
tooltipShiftVertical={item.tooltipShiftVertical}
tooltipWrapperStyle={item.tooltipWrapperStyle}
renderTooltipContent={item.renderTooltipContent}
numberOfLinesTitle={item.numberOfLinesTitle}
interactive={item.interactive}
isSelected={item.isSelected}
badgeText={item.badgeText}
/>
</OfflineWithFeedback>
))}
</ScrollView>
{/** eslint-disable-next-line react/jsx-props-no-spreading */}
<WrapComponent {...(shouldUseScrollView && {contentContainerStyle: scrollContainerStyle})}>
{currentMenuItems.map((item, menuIndex) => {
const {text, onSelected, subMenuItems, shouldCallAfterModalHide, ...menuItemProps} = item;
return (
<OfflineWithFeedback
// eslint-disable-next-line react/no-array-index-key
key={`${item.text}_${menuIndex}`}
pendingAction={item.pendingAction}
>
<FocusableMenuItem
// eslint-disable-next-line react/no-array-index-key
key={`${item.text}_${menuIndex}`}
title={text}
onPress={() => selectItem(menuIndex)}
focused={focusedIndex === menuIndex}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
shouldCheckActionAllowedOnPress={false}
onFocus={() => {
if (!shouldUpdateFocusedIndex) {
return;
}
setFocusedIndex(menuIndex);
}}
style={{backgroundColor: item.isSelected ? theme.activeComponentBG : undefined}}
titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])}
// eslint-disable-next-line react/jsx-props-no-spreading
{...menuItemProps}
/>
</OfflineWithFeedback>
);
})}
</WrapComponent>
</View>
</FocusTrapForModal>
</PopoverWithMeasuredContent>
);
Expand All @@ -325,7 +336,7 @@ PopoverMenu.displayName = 'PopoverMenu';
export default React.memo(
PopoverMenu,
(prevProps, nextProps) =>
prevProps.menuItems.length === nextProps.menuItems.length &&
lodashIsEqual(prevProps.menuItems.length, nextProps.menuItems.length) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to compare the menu items and not just their lengths in order to show the error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s77rt Sorry, that was my mistake. I forgot to revert the change in PopoverMenu when running perf-test locally and mistakenly thought it was fixed. But the current fail test come from AttachmentPickerWithMenuItems

prevProps.isVisible === nextProps.isVisible &&
lodashIsEqual(prevProps.anchorPosition, nextProps.anchorPosition) &&
prevProps.anchorRef === nextProps.anchorRef &&
Expand Down
1 change: 1 addition & 0 deletions src/pages/Search/SearchTypeMenuNarrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title,
onClose={closeMenu}
onItemSelected={closeMenu}
anchorRef={buttonRef}
shouldUseScrollView
/>
<DeleteConfirmModal />
</View>
Expand Down
Loading
Loading