diff --git a/src/CONST.ts b/src/CONST.ts index 099eeb2ae216..0a7af610bfb4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -205,6 +205,10 @@ const CONST = { IN: 'in', OUT: 'out', }, + POPOVER_ACCOUNT_SWITCHER_POSITION: { + horizontal: 12, + vertical: 80, + }, // Multiplier for gyroscope animation in order to make it a bit more subtle ANIMATION_GYROSCOPE_VALUE: 0.4, ANIMATION_PAID_DURATION: 200, diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 9b5d21743bef..8ccab44a2cb9 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -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'; @@ -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'; @@ -41,6 +40,7 @@ function AccountSwitcher() { const [session] = useOnyx(ONYXKEYS.SESSION); const [user] = useOnyx(ONYXKEYS.USER); const buttonRef = useRef(null); + const {windowHeight} = useWindowDimensions(); const [shouldShowDelegatorMenu, setShouldShowDelegatorMenu] = useState(false); const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); @@ -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> = {}, + ): PopoverMenuItem => { const error = Object.values(errors ?? {}).at(0) ?? ''; return { - title: personalDetails?.displayName ?? personalDetails?.login, + text: personalDetails?.displayName ?? personalDetails?.login ?? '', description: Str.removeSMSDomain(personalDetails?.login ?? ''), avatarID: personalDetails?.accountID ?? -1, icon: personalDetails?.avatar ?? '', @@ -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) { @@ -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}`, }); }); @@ -181,23 +181,27 @@ function AccountSwitcher() { {canSwitchAccounts && ( - { setShouldShowDelegatorMenu(false); clearDelegatorErrors(); }} anchorRef={buttonRef} - anchorPosition={styles.accountSwitcherAnchorPosition} - > - - {translate('delegate.switchAccount')} - - - + 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, shouldUseNarrowLayout ? {} : styles.wFitContent]} + headerStyles={styles.pt0} + innerContainerStyle={styles.pb0} + scrollContainerStyle={styles.pb4} + shouldUseScrollView + shouldUpdateFocusedIndex={false} + /> )} & { 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[]; @@ -107,6 +108,24 @@ type PopoverMenuProps = Partial & { /** Whether to show the selected option checkmark */ shouldShowSelectedItemCheck?: boolean; + + /** The style of content container which wraps all child views */ + containerStyles?: StyleProp; + + /** Used to apply styles specifically to the header text */ + headerStyles?: StyleProp; + + /** Modal container styles */ + innerContainerStyle?: ViewStyle; + + /** These styles will be applied to the scroll view content container which wraps all of the child views */ + scrollContainerStyle?: StyleProp; + + /** 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({ @@ -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(); @@ -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.at(index); @@ -155,7 +181,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?.(); @@ -164,7 +190,7 @@ function PopoverMenu({ selectedItem.shouldCloseAllModals, ); } else { - onItemSelected(selectedItem, index); + onItemSelected?.(selectedItem, index); selectedItem.onSelected?.(); } }; @@ -210,7 +236,7 @@ function PopoverMenu({ if (!headerText || enteredSubMenuIndexes.length !== 0) { return; } - return {headerText}; + return {headerText}; }; useKeyboardShortcut( @@ -263,61 +289,46 @@ function PopoverMenu({ shouldEnableNewFocusManagement={shouldEnableNewFocusManagement} useNativeDriver restoreFocusType={restoreFocusType} + innerContainerStyle={innerContainerStyle} > - + {renderHeaderText()} {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} - {currentMenuItems.map((item, menuIndex) => ( - - 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} - /> - - ))} - + {/** eslint-disable-next-line react/jsx-props-no-spreading */} + + {currentMenuItems.map((item, menuIndex) => { + const {text, onSelected, subMenuItems, shouldCallAfterModalHide, ...menuItemProps} = item; + return ( + + 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} + /> + + ); + })} + + ); @@ -328,7 +339,7 @@ PopoverMenu.displayName = 'PopoverMenu'; export default React.memo( PopoverMenu, (prevProps, nextProps) => - prevProps.menuItems.length === nextProps.menuItems.length && + lodashIsEqual(prevProps.menuItems, nextProps.menuItems) && prevProps.isVisible === nextProps.isVisible && lodashIsEqual(prevProps.anchorPosition, nextProps.anchorPosition) && prevProps.anchorRef === nextProps.anchorRef && diff --git a/src/pages/Search/SearchTypeMenuNarrow.tsx b/src/pages/Search/SearchTypeMenuNarrow.tsx index 0473c12c8482..8e204894e5be 100644 --- a/src/pages/Search/SearchTypeMenuNarrow.tsx +++ b/src/pages/Search/SearchTypeMenuNarrow.tsx @@ -208,6 +208,7 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title, onClose={closeMenu} onItemSelected={closeMenu} anchorRef={buttonRef} + shouldUseScrollView /> diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 005ca4df153a..08754a2f7e2c 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -23,6 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as IOU from '@userActions/IOU'; +import * as Modal from '@userActions/Modal'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import DelegateNoAccessModal from '@src/components/DelegateNoAccessModal'; @@ -229,13 +230,6 @@ function AttachmentPickerWithMenuItems({ { icon: Expensicons.Paperclip, text: translate('reportActionCompose.addAttachment'), - onSelected: () => { - if (Browser.isSafari()) { - return; - } - triggerAttachmentPicker(); - }, - shouldCallAfterModalHide: true, }, ]; return ( @@ -322,8 +316,14 @@ function AttachmentPickerWithMenuItems({ // In order for the file picker to open dynamically, the click // function must be called from within a event handler that was initiated // by the user on Safari. - if (index === menuItems.length - 1 && Browser.isSafari()) { - triggerAttachmentPicker(); + if (index === menuItems.length - 1) { + if (Browser.isSafari()) { + triggerAttachmentPicker(); + return; + } + Modal.close(() => { + triggerAttachmentPicker(); + }); } }} anchorPosition={styles.createMenuPositionReportActionCompose(shouldUseNarrowLayout, windowHeight, windowWidth)} diff --git a/src/styles/index.ts b/src/styles/index.ts index ef0d33a5d2a5..6998b7698c1d 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5270,6 +5270,7 @@ const styles = (theme: ThemeColors) => top: 80, left: 12, }, + qbdSetupLinkBox: { backgroundColor: theme.hoverComponentBG, borderRadius: variables.componentBorderRadiusMedium, diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts index ed4651bcf2e0..0cf3efd54d61 100644 --- a/src/styles/utils/sizing.ts +++ b/src/styles/utils/sizing.ts @@ -118,4 +118,7 @@ export default { wAuto: { width: 'auto', }, + wFitContent: { + width: 'fit-content', + }, } satisfies Record;