From 028b505993267d023262aa547606e6e87ab84750 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 1 Sep 2026 12:47:36 +0200 Subject: [PATCH 1/3] fix: migrate MenuItems using `wrapperStyle` --- .../ImportOnyxState/BaseImportOnyxState.tsx | 15 +- src/pages/settings/Copilot/CopilotPage.tsx | 19 ++- .../settings/DynamicKeyboardShortcutsPage.tsx | 19 ++- .../CancelSubscriptionMenuItem/index.tsx | 22 ++- .../Subscription/CardSection/CardSection.tsx | 41 +++-- .../settings/Wallet/WalletPage/index.tsx | 151 +++++++++-------- .../workspace/WorkspaceOverviewSharePage.tsx | 24 +-- .../approvals/ApprovalWorkflowEditor.tsx | 15 +- ...aceWorkflowsApprovalsApprovalLimitPage.tsx | 154 +++++++++--------- 9 files changed, 254 insertions(+), 206 deletions(-) diff --git a/src/components/ImportOnyxState/BaseImportOnyxState.tsx b/src/components/ImportOnyxState/BaseImportOnyxState.tsx index b4fa1bea932d..053e41481bff 100644 --- a/src/components/ImportOnyxState/BaseImportOnyxState.tsx +++ b/src/components/ImportOnyxState/BaseImportOnyxState.tsx @@ -1,6 +1,7 @@ import AttachmentPicker from '@components/AttachmentPicker'; import DecisionModal from '@components/DecisionModal'; import MenuItem from '@components/MenuItem'; +import MenuItemSectionRow from '@components/MenuItem/presets/MenuItemSectionRow'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -37,16 +38,20 @@ function BaseImportOnyxState({ > {({openPicker}) => { return ( - { openPicker({ onPicked: (data) => onFileRead(data.at(0) ?? {}), }); }} - /> + > + + + + {translate('initialSettingsPage.troubleshoot.importOnyxState')} + + + ); }} diff --git a/src/pages/settings/Copilot/CopilotPage.tsx b/src/pages/settings/Copilot/CopilotPage.tsx index a9d8f459b1ed..fd1fa6655e17 100644 --- a/src/pages/settings/Copilot/CopilotPage.tsx +++ b/src/pages/settings/Copilot/CopilotPage.tsx @@ -5,6 +5,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; import MenuItem from '@components/MenuItem'; import type {MenuItemProps} from '@components/MenuItem'; +import MenuItemSectionRow from '@components/MenuItem/presets/MenuItemSectionRow'; import MenuItemList from '@components/MenuItemList'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import PopoverMenu from '@components/PopoverMenu'; @@ -491,9 +492,7 @@ function CopilotPage() { )} {isAgentAccount === false ? ( - { if (isActingAsDelegate) { @@ -510,9 +509,17 @@ function CopilotPage() { } Navigation.navigate(ROUTES.SETTINGS_ADD_DELEGATE); }} - shouldShowRightIcon - wrapperStyle={[styles.sectionMenuItemTopDescription]} - /> + > + + + + {translate('delegate.addCopilot')} + + + + + + ) : null} !!shortcut.descriptionKey); const renderShortcut = (shortcut: Shortcut) => ( - + + + + {shortcut.displayName} + {translate(`keyboardShortcutsPage.shortcuts.${shortcut.descriptionKey}`)} + + + ); return ( @@ -57,8 +58,8 @@ function DynamicKeyboardShortcutsPage() { onBackButtonPress={() => Navigation.goBack(backPath)} /> - - {translate('keyboardShortcutsPage.subtitle')} + + {translate('keyboardShortcutsPage.subtitle')} {shortcuts.map(renderShortcut)} diff --git a/src/pages/settings/Subscription/CardSection/CancelSubscriptionMenuItem/index.tsx b/src/pages/settings/Subscription/CardSection/CancelSubscriptionMenuItem/index.tsx index 4b459b6f14b8..6757ebba4587 100644 --- a/src/pages/settings/Subscription/CardSection/CancelSubscriptionMenuItem/index.tsx +++ b/src/pages/settings/Subscription/CardSection/CancelSubscriptionMenuItem/index.tsx @@ -1,9 +1,9 @@ import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import MenuItem from '@components/MenuItem'; +import MenuItemSectionRow from '@components/MenuItem/presets/MenuItemSectionRow'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -15,7 +15,6 @@ import React from 'react'; function CancelSubscriptionMenuItem() { const icons = useMemoizedLazyExpensifyIcons(['CircleSlash']); const {translate} = useLocalize(); - const styles = useThemeStyles(); const {isActingAsDelegate} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); @@ -27,15 +26,20 @@ function CancelSubscriptionMenuItem() { Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_CANCEL_SUBSCRIPTION); }; return ( - + > + + + + {translate('subscription.cancelSubscription.title')} + + + + + + ); } diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 60588d8c4e6d..2bd00f9b70d0 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,4 +1,5 @@ import MenuItem from '@components/MenuItem'; +import MenuItemSectionRow from '@components/MenuItem/presets/MenuItemSectionRow'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import PaymentCardDetails from '@components/PaymentCardDetails'; import RenderHTML from '@components/RenderHTML'; @@ -284,25 +285,25 @@ function CardSection() { )} {!!account?.hasPurchases && ( - + > + + + + {translate('subscription.cardSection.viewPaymentHistory')} + + + + + + )} {!!(subscriptionPlan && account?.isEligibleForRefund) && ( - { const result = await showRequestRefundModal(); if (result.action !== ModalActions.CONFIRM) { @@ -311,7 +312,17 @@ function CardSection() { requestRefund(); }} sentryLabel={CONST.SENTRY_LABEL.SETTINGS_SUBSCRIPTION.REQUEST_REFUND} - /> + > + + + + {translate('subscription.cardSection.requestRefund')} + + + + + + )} {!privateSubscription?.pendingFields?.type && canCancelSubscription(privateSubscription?.type, firstDayFreeTrial, lastDayFreeTrial, userBillingFundID, account?.hasPurchases) && ( diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 575d1f09bd0c..d4a78a63967e 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -6,6 +6,7 @@ import {KYCWallContext} from '@components/KYCWall/KYCWallContext'; import type {PaymentMethodType, Source} from '@components/KYCWall/types'; import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; import MenuItem from '@components/MenuItem'; +import MenuItemSectionRow from '@components/MenuItem/presets/MenuItemSectionRow'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -30,6 +31,7 @@ import usePaymentMethodState from '@hooks/usePaymentMethodState'; import type {FormattedSelectedPaymentMethod} from '@hooks/usePaymentMethodState/types'; import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -59,7 +61,7 @@ import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ForwardedRef, RefObject} from 'react'; +import type {RefObject} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {hasSeenTourSelector} from '@selectors/Onboarding'; @@ -119,6 +121,7 @@ function WalletPage() { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const network = useNetwork(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {paymentMethod, setPaymentMethod, resetSelectedPaymentMethodData} = usePaymentMethodState(); @@ -597,6 +600,19 @@ function WalletPage() { Navigation.navigate(ROUTES.SETTINGS_WALLET_PERSONAL_CARD_ADD_NEW); }; + const onEnableWalletPress = () => { + if (isAccountLocked) { + showLockedAccountModal(); + return; + } + + if (!isUserValidated) { + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.VERIFY_ACCOUNT.path)); + return; + } + Navigation.navigate(ROUTES.SETTINGS_ENABLE_PAYMENTS.getRoute()); + }; + const openCompanyCardFlow = () => { if (hasSinglePolicy) { const policy = activeAdminPolicies.at(0); @@ -731,41 +747,51 @@ function WalletPage() { listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} shouldShowConnectionStatus={shouldShowWalletConnectionStatus} /> - - - + + + + + {translate('personalCard.addPersonalCard')} + + + - - Navigation.navigate(ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS)} - wrapperStyle={[styles.paymentMethod, shouldUseNarrowLayout ? styles.ph5 : styles.ph8]} - sentryLabel={CONST.SENTRY_LABEL.SETTINGS_WALLET.IMPORT_TRANSACTIONS} - /> - + Navigation.navigate(ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS)} + sentryLabel={CONST.SENTRY_LABEL.SETTINGS_WALLET.IMPORT_TRANSACTIONS} + > + + + + {translate('workspace.companyCards.importTransactions.importButton')} + + + + + + {!hasAssignedCard && ( - - - + + + + + + + {translate('personalCard.lookingForCompanyCards')} + {translate('personalCard.lookingForCompanyCardsDescription')} + + + + + + )} {hasWallet && ( @@ -825,21 +851,23 @@ function WalletPage() { if (hasActivatedWallet) { return ( - } - title={translate('common.transferBalance')} - icon={icons.Transfer} + { triggerKYCFlow({event}); }} - shouldShowRightIcon - wrapperStyle={[ - styles.transferBalance, - shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8, - shouldUseNarrowLayout ? styles.ph5 : styles.ph8, - ]} sentryLabel={CONST.SENTRY_LABEL.SETTINGS_WALLET.TRANSFER_BALANCE} - /> + > + + + + {translate('common.transferBalance')} + + + + + + ); } @@ -870,29 +898,18 @@ function WalletPage() { } return ( - } - onPress={() => { - if (isAccountLocked) { - showLockedAccountModal(); - return; - } - - if (!isUserValidated) { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.VERIFY_ACCOUNT.path)); - return; - } - Navigation.navigate(ROUTES.SETTINGS_ENABLE_PAYMENTS.getRoute()); - }} - wrapperStyle={[ - styles.transferBalance, - shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8, - shouldUseNarrowLayout ? styles.ph5 : styles.ph8, - ]} + + > + + + + {translate('walletPage.enableWallet')} + + + ); }} diff --git a/src/pages/workspace/WorkspaceOverviewSharePage.tsx b/src/pages/workspace/WorkspaceOverviewSharePage.tsx index 4122efe3cc65..1720ff142f3f 100644 --- a/src/pages/workspace/WorkspaceOverviewSharePage.tsx +++ b/src/pages/workspace/WorkspaceOverviewSharePage.tsx @@ -24,6 +24,8 @@ import {getDefaultWorkspaceAvatar, getRoom} from '@libs/ReportUtils'; import shouldAllowDownloadQRCode from '@libs/shouldAllowDownloadQRCode'; import addTrailingForwardSlash from '@libs/UrlUtils'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; + import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -112,7 +114,7 @@ function WorkspaceOverviewSharePage({policy}: WithPolicyProps) { logoMarginRatio={CONST.QR.DEFAULT_LOGO_MARGIN_RATIO} /> - + Clipboard.setString(url)} shouldLimitWidth={false} - wrapperStyle={styles.sectionMenuItemTopDescription} /> {/* Remove this once https://github.com/Expensify/App/issues/19834 is done. We shouldn't introduce platform specific code in our codebase. This is a temporary solution while Web is not supported for the QR code download feature */} - {shouldAllowDownloadQRCode && ( - qrCodeRef.current?.download?.()} - wrapperStyle={styles.sectionMenuItemTopDescription} - /> - )} + {shouldAllowDownloadQRCode && ( + qrCodeRef.current?.download?.()}> + + + + {translate('common.download')} + + + + )} diff --git a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx index 6d34faf27ba2..0df80c6da218 100644 --- a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx +++ b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx @@ -1,4 +1,4 @@ -import MenuItem from '@components/MenuItem'; +import MenuItemAction from '@components/MenuItem/presets/MenuItemAction'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScrollView from '@components/ScrollView'; @@ -256,16 +256,17 @@ function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, polic errorText={approvalWorkflow?.errors?.additionalApprover ? translate(approvalWorkflow.errors.additionalApprover) : undefined} brickRoadIndicator={approvalWorkflow?.errors?.additionalApprover ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> + - {!!removeApprovalWorkflow && !approvalWorkflow.isDefault && ( - + - )} - + + )} ); } diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx index d0b6876d4911..ec80f30b876c 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx @@ -3,7 +3,7 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import Button from '@components/ButtonComposed'; import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItem from '@components/MenuItem'; +import MenuItemAction from '@components/MenuItem/presets/MenuItemAction'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -210,90 +210,90 @@ function WorkspaceWorkflowsApprovalsApprovalLimitPage({policy, isLoadingReportDa style={styles.flex1} contentContainerStyle={styles.flexGrow1} > - - {isEditFlow ? ( - <> - - - - ) : undefined - } - onPress={navigateToApproverChange} - shouldShowRightIcon - wrapperStyle={styles.sectionMenuItemTopDescription} + + + {isEditFlow ? ( + <> + + + + ) : undefined + } + onPress={navigateToApproverChange} + shouldShowRightIcon + wrapperStyle={styles.sectionMenuItemTopDescription} + /> + + + + + ) : ( + <> + {translate('workflowsApprovalLimitPage.header')} + + + + + )} + + + - - - - - ) : ( - <> - {translate('workflowsApprovalLimitPage.header')} - - - - - )} - - - + + + + + ) : undefined + } + onPress={navigateToApproverSelector} + shouldShowRightIcon + wrapperStyle={styles.sectionMenuItemTopDescription} + brickRoadIndicator={approverErrorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={approverErrorText} disabled={areLimitFieldsDisabled} - errorText={amountError} - onSubmitEditing={handleSubmit} + shouldGreyOutWhenDisabled /> - - - - - ) : undefined - } - onPress={navigateToApproverSelector} - shouldShowRightIcon - wrapperStyle={styles.sectionMenuItemTopDescription} - brickRoadIndicator={approverErrorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={approverErrorText} - disabled={areLimitFieldsDisabled} - shouldGreyOutWhenDisabled - /> - {shouldShowRemoveLimitRow && ( - )} From e2045e0ccc98d3ffb5133ac739768f2417741433 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 1 Sep 2026 12:54:19 +0200 Subject: [PATCH 2/3] feat: add `MenuItemSectionRow` preset --- .../MenuItem/layout/MenuItemRoot.tsx | 1 + .../MenuItem/presets/MenuItemSectionRow.tsx | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/components/MenuItem/presets/MenuItemSectionRow.tsx diff --git a/src/components/MenuItem/layout/MenuItemRoot.tsx b/src/components/MenuItem/layout/MenuItemRoot.tsx index 085809d9f233..f20e7883716d 100644 --- a/src/components/MenuItem/layout/MenuItemRoot.tsx +++ b/src/components/MenuItem/layout/MenuItemRoot.tsx @@ -110,3 +110,4 @@ function MenuItemRoot({children, onPress, isDisabled = false, sentryLabel, acces } export default MenuItemRoot; +export type {MenuItemRootProps}; diff --git a/src/components/MenuItem/presets/MenuItemSectionRow.tsx b/src/components/MenuItem/presets/MenuItemSectionRow.tsx new file mode 100644 index 000000000000..3db84f6e73c9 --- /dev/null +++ b/src/components/MenuItem/presets/MenuItemSectionRow.tsx @@ -0,0 +1,42 @@ +import MenuItemRoot from '@components/MenuItem/layout/MenuItemRoot'; +import type {MenuItemRootProps} from '@components/MenuItem/layout/MenuItemRoot'; + +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; + +import type {RefObject} from 'react'; + +import React from 'react'; +import {View} from 'react-native'; + +type MenuItemSectionRowProps = MenuItemRootProps & { + /** Ref to the bleed wrapper. Use it when a popover needs to anchor on the row. */ + ref?: RefObject; +}; + +/** A `MenuItem.Root` that spans the full width of a `Section`, ignoring the section's own horizontal padding */ +function MenuItemSectionRow({children, onPress, isDisabled = false, sentryLabel, accessibilityLabel, ref}: MenuItemSectionRowProps) { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + return ( + + + {children} + + + ); +} + +export default MenuItemSectionRow; +export type {MenuItemSectionRowProps}; From 7310badaef473fe31f93d539ea67786ce21b2fa7 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 1 Sep 2026 18:36:14 +0200 Subject: [PATCH 3/3] get rid of another two wrapperStyle usages --- src/components/MenuItem/index.ts | 10 +++- .../BaseMenuItemTitle.tsx} | 18 +++--- .../leaves/text/title/MenuItemTitle.tsx | 16 ++++++ .../leaves/text/title/MenuItemTitleBasic.tsx | 15 +++++ .../text/title/MenuItemTitlePlaceholder.tsx | 19 +++++++ .../MenuItem/leaves/text/title/types.ts | 7 +++ .../leaves/trailing/MenuItemTrailingIcon.tsx | 44 +++++++++++++++ .../MenuItem/presets/MenuItemAction.tsx | 2 +- .../MenuItem/presets/MenuItemNavigation.tsx | 2 +- .../IndividualExpenseRulesSectionRevamp.tsx | 56 ++++++++++++++++++- .../workspace/travel/BookOrManageYourTrip.tsx | 34 ++++++++--- 11 files changed, 203 insertions(+), 20 deletions(-) rename src/components/MenuItem/leaves/text/{MenuItemTitle.tsx => title/BaseMenuItemTitle.tsx} (58%) create mode 100644 src/components/MenuItem/leaves/text/title/MenuItemTitle.tsx create mode 100644 src/components/MenuItem/leaves/text/title/MenuItemTitleBasic.tsx create mode 100644 src/components/MenuItem/leaves/text/title/MenuItemTitlePlaceholder.tsx create mode 100644 src/components/MenuItem/leaves/text/title/types.ts create mode 100644 src/components/MenuItem/leaves/trailing/MenuItemTrailingIcon.tsx diff --git a/src/components/MenuItem/index.ts b/src/components/MenuItem/index.ts index 4c40cd6eadf0..6f39fca357c3 100644 --- a/src/components/MenuItem/index.ts +++ b/src/components/MenuItem/index.ts @@ -30,8 +30,12 @@ import MenuItemRow from './layout/MenuItemRow'; import MenuItemTrailing from './layout/MenuItemTrailing'; import MenuItemIcon from './leaves/leading/MenuItemIcon'; import MenuItemDescription from './leaves/text/MenuItemDescription'; -import MenuItemTitle from './leaves/text/MenuItemTitle'; +import MenuItemTitle from './leaves/text/title/MenuItemTitle'; +import MenuItemTitleBasic from './leaves/text/title/MenuItemTitleBasic'; +import MenuItemTitlePlaceholder from './leaves/text/title/MenuItemTitlePlaceholder'; import MenuItemChevron from './leaves/trailing/MenuItemChevron'; +import MenuItemErrorDot from './leaves/trailing/MenuItemErrorDot'; +import MenuItemTrailingIcon from './leaves/trailing/MenuItemTrailingIcon'; import LegacyMenuItem from './MenuItem'; const MenuItem = Object.assign(LegacyMenuItem, { @@ -41,8 +45,12 @@ const MenuItem = Object.assign(LegacyMenuItem, { Trailing: MenuItemTrailing, Icon: MenuItemIcon, Title: MenuItemTitle, + TitleBasic: MenuItemTitleBasic, + TitlePlaceholder: MenuItemTitlePlaceholder, Description: MenuItemDescription, Chevron: MenuItemChevron, + ErrorDot: MenuItemErrorDot, + TrailingIcon: MenuItemTrailingIcon, }); export default MenuItem; diff --git a/src/components/MenuItem/leaves/text/MenuItemTitle.tsx b/src/components/MenuItem/leaves/text/title/BaseMenuItemTitle.tsx similarity index 58% rename from src/components/MenuItem/leaves/text/MenuItemTitle.tsx rename to src/components/MenuItem/leaves/text/title/BaseMenuItemTitle.tsx index ccafead71111..7dc149a4f028 100644 --- a/src/components/MenuItem/leaves/text/MenuItemTitle.tsx +++ b/src/components/MenuItem/leaves/text/title/BaseMenuItemTitle.tsx @@ -8,15 +8,19 @@ import convertToLTR from '@libs/convertToLTR'; import CONST from '@src/CONST'; +import type {StyleProp, TextStyle} from 'react-native'; + import React from 'react'; -type MenuItemTitleProps = { - /** Text to render as the title */ - children: string | number; +import type MenuItemTitleProps from './types'; + +type BaseMenuItemTitleProps = MenuItemTitleProps & { + /** Typography layered on top of the shared title base — each leaf brings its own weight and colour */ + style?: StyleProp; }; -/** The title block of a `MenuItem.Content`. Bold, single line */ -function MenuItemTitle({children}: MenuItemTitleProps) { +/** Everything the title leaves have in common — the shared type face, LTR handling and label registration */ +function BaseMenuItemTitle({children, style}: BaseMenuItemTitleProps) { const styles = useThemeStyles(); const {isDisabled, isInteractive} = useMenuItemConfig(); @@ -24,7 +28,7 @@ function MenuItemTitle({children}: MenuItemTitleProps) { return ( @@ -33,4 +37,4 @@ function MenuItemTitle({children}: MenuItemTitleProps) { ); } -export default MenuItemTitle; +export default BaseMenuItemTitle; diff --git a/src/components/MenuItem/leaves/text/title/MenuItemTitle.tsx b/src/components/MenuItem/leaves/text/title/MenuItemTitle.tsx new file mode 100644 index 000000000000..6cf467d962c2 --- /dev/null +++ b/src/components/MenuItem/leaves/text/title/MenuItemTitle.tsx @@ -0,0 +1,16 @@ +import useThemeStyles from '@hooks/useThemeStyles'; + +import React from 'react'; + +import type MenuItemTitleProps from './types'; + +import BaseMenuItemTitle from './BaseMenuItemTitle'; + +/** The title block of a `MenuItem.Content`. Bold, single line */ +function MenuItemTitle({children}: MenuItemTitleProps) { + const styles = useThemeStyles(); + + return {children}; +} + +export default MenuItemTitle; diff --git a/src/components/MenuItem/leaves/text/title/MenuItemTitleBasic.tsx b/src/components/MenuItem/leaves/text/title/MenuItemTitleBasic.tsx new file mode 100644 index 000000000000..4a3bc9b8c9ba --- /dev/null +++ b/src/components/MenuItem/leaves/text/title/MenuItemTitleBasic.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import type MenuItemTitleProps from './types'; + +import BaseMenuItemTitle from './BaseMenuItemTitle'; + +/** + * The title of a `MenuItem.Content` at regular weight, for rows that pair a label with the value + * it holds — there the bold of `MenuItem.Title` would compete with the value for attention. + */ +function MenuItemTitleBasic({children}: MenuItemTitleProps) { + return {children}; +} + +export default MenuItemTitleBasic; diff --git a/src/components/MenuItem/leaves/text/title/MenuItemTitlePlaceholder.tsx b/src/components/MenuItem/leaves/text/title/MenuItemTitlePlaceholder.tsx new file mode 100644 index 000000000000..69e07dec35e3 --- /dev/null +++ b/src/components/MenuItem/leaves/text/title/MenuItemTitlePlaceholder.tsx @@ -0,0 +1,19 @@ +import useThemeStyles from '@hooks/useThemeStyles'; + +import React from 'react'; + +import type MenuItemTitleProps from './types'; + +import BaseMenuItemTitle from './BaseMenuItemTitle'; + +/** + * The title of a `MenuItem.Content` for a row whose value has not been set yet. Muted and regular + * weight, so it reads as a prompt rather than as content the way `MenuItem.Title` does. + */ +function MenuItemTitlePlaceholder({children}: MenuItemTitleProps) { + const styles = useThemeStyles(); + + return {children}; +} + +export default MenuItemTitlePlaceholder; diff --git a/src/components/MenuItem/leaves/text/title/types.ts b/src/components/MenuItem/leaves/text/title/types.ts new file mode 100644 index 000000000000..c78c50213b2a --- /dev/null +++ b/src/components/MenuItem/leaves/text/title/types.ts @@ -0,0 +1,7 @@ +/** Props shared by every title leaf of a `MenuItem.Content` */ +type MenuItemTitleProps = { + /** Text to render as the title */ + children: string | number; +}; + +export default MenuItemTitleProps; diff --git a/src/components/MenuItem/leaves/trailing/MenuItemTrailingIcon.tsx b/src/components/MenuItem/leaves/trailing/MenuItemTrailingIcon.tsx new file mode 100644 index 000000000000..7bec74c16721 --- /dev/null +++ b/src/components/MenuItem/leaves/trailing/MenuItemTrailingIcon.tsx @@ -0,0 +1,44 @@ +import Icon from '@components/Icon'; +import {useMenuItemConfig, useMenuItemInteraction} from '@components/MenuItem/MenuItemContext'; + +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import getButtonState from '@libs/getButtonState'; + +import variables from '@styles/variables'; + +import type IconAsset from '@src/types/utils/IconAsset'; + +import React from 'react'; +import {View} from 'react-native'; + +type MenuItemTrailingIconProps = { + /** Icon to display */ + src: IconAsset; +}; + +/** + * A trailing icon of a `MenuItem.Row` that is not a navigation indicator — an "opens in a new tab" + * arrow, for instance. Unlike `MenuItem.Chevron` it stays at full opacity when the row is not + * hovered, since it carries meaning on its own. + */ +function MenuItemTrailingIcon({src}: MenuItemTrailingIconProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {isDisabled, isInteractive} = useMenuItemConfig(); + const {isHovered, isPressed} = useMenuItemInteraction(); + + return ( + + + + ); +} + +export default MenuItemTrailingIcon; diff --git a/src/components/MenuItem/presets/MenuItemAction.tsx b/src/components/MenuItem/presets/MenuItemAction.tsx index 8b48e92da517..21dba80a41a1 100644 --- a/src/components/MenuItem/presets/MenuItemAction.tsx +++ b/src/components/MenuItem/presets/MenuItemAction.tsx @@ -2,7 +2,7 @@ import MenuItemContent from '@components/MenuItem/layout/MenuItemContent'; import MenuItemRoot from '@components/MenuItem/layout/MenuItemRoot'; import MenuItemRow from '@components/MenuItem/layout/MenuItemRow'; import MenuItemIcon from '@components/MenuItem/leaves/leading/MenuItemIcon'; -import MenuItemTitle from '@components/MenuItem/leaves/text/MenuItemTitle'; +import MenuItemTitle from '@components/MenuItem/leaves/text/title/MenuItemTitle'; import {callFunctionIfActionIsAllowed} from '@userActions/Session'; diff --git a/src/components/MenuItem/presets/MenuItemNavigation.tsx b/src/components/MenuItem/presets/MenuItemNavigation.tsx index 9a2770595b00..5a46ddf0db3b 100644 --- a/src/components/MenuItem/presets/MenuItemNavigation.tsx +++ b/src/components/MenuItem/presets/MenuItemNavigation.tsx @@ -3,7 +3,7 @@ import MenuItemRoot from '@components/MenuItem/layout/MenuItemRoot'; import MenuItemRow from '@components/MenuItem/layout/MenuItemRow'; import MenuItemTrailing from '@components/MenuItem/layout/MenuItemTrailing'; import MenuItemIcon from '@components/MenuItem/leaves/leading/MenuItemIcon'; -import MenuItemTitle from '@components/MenuItem/leaves/text/MenuItemTitle'; +import MenuItemTitle from '@components/MenuItem/leaves/text/title/MenuItemTitle'; import MenuItemChevron from '@components/MenuItem/leaves/trailing/MenuItemChevron'; import {callFunctionIfActionIsAllowed} from '@userActions/Session'; diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx index 73d8ea7ad532..b00b5170640e 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx @@ -1,4 +1,7 @@ +import Icon from '@components/Icon'; import MenuItem from '@components/MenuItem'; +import {useMenuItemConfig, useMenuItemInteraction} from '@components/MenuItem/MenuItemContext'; +import MenuItemSectionRow from '@components/MenuItem/presets/MenuItemSectionRow'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; @@ -7,10 +10,12 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {getBillableExpensesPendingAction, getCashExpenseReimbursableMode, setPolicyAttendeeTrackingEnabled, setWorkspaceEReceiptsEnabled} from '@libs/actions/Policy/Policy'; import {openPolicyTagsPage} from '@libs/actions/Policy/Tag'; +import getButtonState from '@libs/getButtonState'; import Navigation from '@libs/Navigation/Navigation'; import {getTagListLabel, getTagLists, hasPerTagListRequired, isAttendeeTrackingEnabled, isCollectPolicy, tryNavigateToControlPolicyUpgrade} from '@libs/PolicyUtils'; @@ -57,6 +62,35 @@ type BasicRuleMenuItem = { const COLLECT_ALLOWED_RULE_KEYS = new Set([RULE_MENU_ITEM_KEYS.REQUIRE_FIELDS, RULE_MENU_ITEM_KEYS.BILLABLE_EXPENSES]); +/** + * The leading icon cell of a rule row. Hugs its icon instead of reserving the avatar-width column + * `MenuItem.Icon` does, and carries the margin that lines the text up with the `ToggleSettingOptionRow` + * rows below it (a 20px icon plus `mr5`). The margin belongs on this cell rather than a wrapping view: + * outside the box `wAuto` shrinks, it would knock the icon off column. + */ +function MenuItemCompactIcon({src}: {src: IconAsset}) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {isDisabled, isInteractive} = useMenuItemConfig(); + const {isHovered, isPressed} = useMenuItemInteraction(); + + const isComplete = false; + const isMenuIcon = true; + const isPane = true; + + return ( + + + + ); +} + function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: IndividualExpenseRulesSectionRevampProps) { const {convertToDisplayString} = useCurrencyListActions(); const {translate} = useLocalize(); @@ -244,6 +278,27 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.INDIVIDUAL_EXPENSES_MENU_ITEM} numberOfLinesDescription={1} /> + {/* TODO: remove before merging — temporary side-by-side of the best the compound API can do today. + Remaining gaps vs the legacy row above: `Title` is always bold and never muted (legacy + `shouldShowBasicTitle` + `colorMuted`), and `Description` always wraps to 2 lines + (legacy `numberOfLinesDescription={1}`). */} + handleMenuItemPress(item) : undefined} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.INDIVIDUAL_EXPENSES_MENU_ITEM} + > + + + + {item.description ? {item.title} : {item.title}} + {!!item.description && {item.description}} + + {canWriteRules && ( + + + + )} + + )); @@ -255,7 +310,6 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu titleStyles={styles.accountSettingsSectionTitle} subtitleMuted subtitleStyles={styles.mt0} - containerStyles={styles.mh5} > {renderMenuItems(policyControlItems)} diff --git a/src/pages/workspace/travel/BookOrManageYourTrip.tsx b/src/pages/workspace/travel/BookOrManageYourTrip.tsx index 39cbf5316d37..b84777f2476c 100644 --- a/src/pages/workspace/travel/BookOrManageYourTrip.tsx +++ b/src/pages/workspace/travel/BookOrManageYourTrip.tsx @@ -1,4 +1,5 @@ import MenuItem from '@components/MenuItem'; +import MenuItemSectionRow from '@components/MenuItem/presets/MenuItemSectionRow'; import Section from '@components/Section'; import useConfirmModal from '@hooks/useConfirmModal'; @@ -22,6 +23,7 @@ import colors from '@styles/theme/colors'; import CONST from '@src/CONST'; import React from 'react'; +import {View} from 'react-native'; import WorkspaceTravelBillingSection from './WorkspaceTravelBillingSection'; @@ -44,6 +46,10 @@ function BookOrManageYourTrip({policyID}: BookOrManageYourTripProps) { const autoAddTripName = policy?.travelSettings?.autoAddTripName !== false; + const bookTravelText = translate('workspace.moreFeatures.travel.bookOrManageYourTrip.ctaText'); + // The row opens TravelDot in a new tab, so the label has to say so — but only when the icon announcing it is rendered + const bookTravelAccessibilityLabel = canWriteMoreFeatures ? `${bookTravelText}. ${translate('common.opensInNewTab')}` : bookTravelText; + const toggleAutoAddTripName = (enabled: boolean) => { setPolicyTravelSettings(policy, {autoAddTripName: enabled}); }; @@ -87,15 +93,25 @@ function BookOrManageYourTrip({policyID}: BookOrManageYourTripProps) { subtitleMuted isCentralPane > - + + + + + + {bookTravelText} + + {canWriteMoreFeatures && ( + + + + )} + + +