From 0fc146d7672f7ff0f1e433452cbb9a71dc2b6ac0 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 27 Oct 2025 09:28:36 +0700 Subject: [PATCH 1/3] fix: make More Features animation more obvious and add tap target --- src/CONST/index.ts | 4 +- src/components/HighlightableMenuItem.tsx | 3 + .../workspace/WorkspaceMoreFeaturesPage.tsx | 74 +++++++++++++++++++ .../workflows/ToggleSettingsOptionRow.tsx | 55 ++++++++++---- 4 files changed, 118 insertions(+), 18 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1f52babee56a..09bf10c33ba9 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -204,8 +204,8 @@ const CONST = { ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300, ANIMATED_HIGHLIGHT_START_DELAY: 10, ANIMATED_HIGHLIGHT_START_DURATION: 300, - ANIMATED_HIGHLIGHT_END_DELAY: 800, - ANIMATED_HIGHLIGHT_END_DURATION: 2000, + ANIMATED_HIGHLIGHT_END_DELAY: 7000, + ANIMATED_HIGHLIGHT_END_DURATION: 3000, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATED_PROGRESS_BAR_DELAY: 300, diff --git a/src/components/HighlightableMenuItem.tsx b/src/components/HighlightableMenuItem.tsx index 983acf6fb02b..590e55732d7b 100644 --- a/src/components/HighlightableMenuItem.tsx +++ b/src/components/HighlightableMenuItem.tsx @@ -3,6 +3,7 @@ import React, {forwardRef} from 'react'; import type {View} from 'react-native'; import {StyleSheet} from 'react-native'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import MenuItem from './MenuItem'; import type {MenuItemProps} from './MenuItem'; @@ -14,12 +15,14 @@ type Props = MenuItemProps & { function HighlightableMenuItem({wrapperStyle, highlighted, ...restOfProps}: Props, ref: ForwardedRef) { const styles = useThemeStyles(); + const theme = useTheme(); const flattenedWrapperStyles = StyleSheet.flatten(wrapperStyle); const animatedHighlightStyle = useAnimatedHighlightStyle({ shouldHighlight: highlighted ?? false, height: flattenedWrapperStyles?.height ? Number(flattenedWrapperStyles.height) : styles.sectionMenuItem.height, borderRadius: flattenedWrapperStyles?.borderRadius ? Number(flattenedWrapperStyles.borderRadius) : styles.sectionMenuItem.borderRadius, + highlightColor: theme.messageHighlightBG, }); return ( diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 9dc3ad6d5774..db5ca9c2b47f 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -67,6 +67,7 @@ type Item = { pendingAction: PendingAction | undefined; errors?: Errors; onCloseError?: () => void; + onPress?: () => void; }; type SectionObject = { @@ -145,6 +146,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } enablePolicyDistanceRates(policyID, isEnabled, distanceRateCustomUnit); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)); + }, }, { icon: illustrations.HandCard, @@ -162,6 +169,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabledAction: () => { setIsDisableExpensifyCardWarningModalOpen(true); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID)); + }, }, ]; @@ -181,6 +194,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabledAction: () => { setIsDisableCompanyCardsWarningModalOpen(true); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + }, }); spendItems.push({ @@ -199,6 +218,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } enablePerDiem(policyID, isEnabled, perDiemCustomUnit?.customUnitID, true); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM.getRoute(policyID)); + }, }); const manageItems: Item[] = [ @@ -216,6 +241,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }, disabled: isSmartLimitEnabled, disabledAction: onDisabledWorkflowPress, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)); + }, }, { icon: illustrations.Rules, @@ -234,6 +265,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } enablePolicyRules(policyID, isEnabled); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_RULES.getRoute(policyID)); + }, }, ]; @@ -250,6 +287,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } enablePolicyInvoicing(policyID, isEnabled); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)); + }, }, ]; @@ -268,6 +311,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } enablePolicyCategories(policyID, isEnabled, policyTagLists, policyCategories, allTransactionViolations, true); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); + }, }, { icon: illustrations.Tag, @@ -283,6 +332,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } enablePolicyTags({policyID, enabled: isEnabled, policyTags: policyTagLists}); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)); + }, }, { icon: illustrations.Coins, @@ -298,6 +353,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } enablePolicyTaxes(policyID, isEnabled); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)); + }, }, ]; @@ -328,6 +389,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } clearPolicyErrorField(policyID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + }, }, ]; @@ -358,6 +425,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro } clearPolicyErrorField(policyID, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED); }, + onPress: () => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_RECEIPT_PARTNERS.getRoute(policyID)); + }, }); } @@ -409,6 +482,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro showLockIcon={item.disabled} errors={item.errors} onCloseError={item.onCloseError} + onPress={item.onPress} /> ), diff --git a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx index bbe7b83510bf..4b9e95ff8c77 100644 --- a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx +++ b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx @@ -5,6 +5,7 @@ import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import Accordion from '@components/Accordion'; import Icon from '@components/Icon'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RenderHTML from '@components/RenderHTML'; import Switch from '@components/Switch'; import Text from '@components/Text'; @@ -77,6 +78,9 @@ type ToggleSettingOptionRowProps = { /** Callback to fire when the switch is toggled in disabled state */ disabledAction?: () => void; + + /** Callback to fire when the content area is pressed (only works when isActive is true) */ + onPress?: () => void; }; const ICON_SIZE = 48; @@ -102,6 +106,7 @@ function ToggleSettingOptionRow({ onCloseError, disabled = false, showLockIcon = false, + onPress, }: ToggleSettingOptionRowProps) { const styles = useThemeStyles(); const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(isActive); @@ -153,6 +158,27 @@ function ToggleSettingOptionRow({ processedSubtitle, ]); + const contentArea = ( + + {!!icon && ( + + )} + {customTitle ?? ( + + {title} + {!shouldPlaceSubtitleBelowSwitch && subtitle && subTitleView} + + )} + + ); + + const shouldMakeContentPressable = isActive && onPress; + return ( - - {!!icon && ( - - )} - {customTitle ?? ( - - {title} - {!shouldPlaceSubtitleBelowSwitch && subtitle && subTitleView} - - )} - + {shouldMakeContentPressable ? ( + + {contentArea} + + ) : ( + contentArea + )} Date: Tue, 28 Oct 2025 18:26:48 +0700 Subject: [PATCH 2/3] add new const --- src/CONST/index.ts | 6 ++++-- src/components/HighlightableMenuItem.tsx | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 77fd4ffb3b1c..e43af92d2760 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -204,8 +204,10 @@ const CONST = { ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300, ANIMATED_HIGHLIGHT_START_DELAY: 10, ANIMATED_HIGHLIGHT_START_DURATION: 300, - ANIMATED_HIGHLIGHT_END_DELAY: 7000, - ANIMATED_HIGHLIGHT_END_DURATION: 3000, + ANIMATED_HIGHLIGHT_WORKSPACE_FEATURE_ITEM_END_DELAY: 7000, + ANIMATED_HIGHLIGHT_WORKSPACE_FEATURE_ITEM_END_DURATION: 3000, + ANIMATED_HIGHLIGHT_END_DELAY: 800, + ANIMATED_HIGHLIGHT_END_DURATION: 2000, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATED_PROGRESS_BAR_DELAY: 300, diff --git a/src/components/HighlightableMenuItem.tsx b/src/components/HighlightableMenuItem.tsx index 590e55732d7b..cf4f65378210 100644 --- a/src/components/HighlightableMenuItem.tsx +++ b/src/components/HighlightableMenuItem.tsx @@ -5,6 +5,7 @@ import {StyleSheet} from 'react-native'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; import MenuItem from './MenuItem'; import type {MenuItemProps} from './MenuItem'; @@ -23,6 +24,8 @@ function HighlightableMenuItem({wrapperStyle, highlighted, ...restOfProps}: Prop height: flattenedWrapperStyles?.height ? Number(flattenedWrapperStyles.height) : styles.sectionMenuItem.height, borderRadius: flattenedWrapperStyles?.borderRadius ? Number(flattenedWrapperStyles.borderRadius) : styles.sectionMenuItem.borderRadius, highlightColor: theme.messageHighlightBG, + highlightEndDelay: CONST.ANIMATED_HIGHLIGHT_WORKSPACE_FEATURE_ITEM_END_DELAY, + highlightEndDuration: CONST.ANIMATED_HIGHLIGHT_WORKSPACE_FEATURE_ITEM_END_DURATION, }); return ( From c52b0be09b84c0aeee3e03e40788a099db052d99 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 31 Oct 2025 14:54:25 +0700 Subject: [PATCH 3/3] add hoverable item --- .../workspace/WorkspaceMoreFeaturesPage.tsx | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index db5ca9c2b47f..ac4e9914180e 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Hoverable from '@components/Hoverable'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -464,27 +465,36 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const renderItem = useCallback( (item: Item) => ( - - - + + {(hovered) => ( + + + + )} + ), [styles, StyleUtils, shouldUseNarrowLayout, translate], );