Skip to content

Commit de09508

Browse files
authored
Merge pull request #38546 from allroundexperts/fix-38372
feat: Add temporary focus to enabled option in workspace initial page
2 parents 93e9719 + 64c2a45 commit de09508

File tree

9 files changed

+131
-5
lines changed

9 files changed

+131
-5
lines changed

src/CONST.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const CONST = {
6363
// Note: Group and Self-DM excluded as these are not tied to a Workspace
6464
WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT],
6565
ANDROID_PACKAGE_NAME,
66+
ANIMATED_HIGHLIGHT_DELAY: 500,
67+
ANIMATED_HIGHLIGHT_DURATION: 500,
6668
ANIMATED_TRANSITION: 300,
6769
ANIMATED_TRANSITION_FROM_VALUE: 100,
6870
ANIMATION_IN_TIMING: 100,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type {ForwardedRef} from 'react';
2+
import React, {forwardRef} from 'react';
3+
import type {View} from 'react-native';
4+
import {StyleSheet} from 'react-native';
5+
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
6+
import useThemeStyles from '@hooks/useThemeStyles';
7+
import MenuItem from './MenuItem';
8+
import type {MenuItemProps} from './MenuItem';
9+
10+
type Props = MenuItemProps & {
11+
/** Should the menu item be highlighted? */
12+
highlighted?: boolean;
13+
};
14+
15+
function HighlightableMenuItem({wrapperStyle, highlighted, ...restOfProps}: Props, ref: ForwardedRef<View>) {
16+
const styles = useThemeStyles();
17+
18+
const flattenedWrapperStyles = StyleSheet.flatten(wrapperStyle);
19+
const animatedHighlightStyle = useAnimatedHighlightStyle({
20+
shouldHighlight: highlighted ?? false,
21+
height: flattenedWrapperStyles?.height ? Number(flattenedWrapperStyles.height) : styles.sectionMenuItem.height,
22+
borderRadius: flattenedWrapperStyles?.borderRadius ? Number(flattenedWrapperStyles.borderRadius) : styles.sectionMenuItem.borderRadius,
23+
});
24+
25+
return (
26+
<MenuItem
27+
// eslint-disable-next-line react/jsx-props-no-spreading
28+
{...restOfProps}
29+
outerWrapperStyle={animatedHighlightStyle}
30+
ref={ref}
31+
/>
32+
);
33+
}
34+
35+
HighlightableMenuItem.displayName = 'HighlightableMenuItem';
36+
37+
export default forwardRef(HighlightableMenuItem);

src/components/MenuItem.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ type MenuItemBaseProps = {
7171
/** Used to apply offline styles to child text components */
7272
style?: StyleProp<ViewStyle>;
7373

74+
/** Outer wrapper styles */
75+
outerWrapperStyle?: StyleProp<ViewStyle>;
76+
7477
/** Any additional styles to apply */
7578
wrapperStyle?: StyleProp<ViewStyle>;
7679

@@ -257,6 +260,7 @@ function MenuItem(
257260
badgeText,
258261
style,
259262
wrapperStyle,
263+
outerWrapperStyle,
260264
containerStyle,
261265
titleStyle,
262266
hoverAndPressStyle,
@@ -426,6 +430,7 @@ function MenuItem(
426430
onPressIn={() => shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
427431
onPressOut={ControlSelection.unblock}
428432
onSecondaryInteraction={onSecondaryInteraction}
433+
wrapperStyle={outerWrapperStyle}
429434
style={({pressed}) =>
430435
[
431436
containerStyle,

src/components/PressableWithSecondaryInteraction/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function PressableWithSecondaryInteraction(
1313
children,
1414
inline = false,
1515
style,
16+
wrapperStyle,
1617
enableLongPressWithHover = false,
1718
withoutFocusOnSecondaryInteraction = false,
1819
needsOffscreenAlphaCompositing = false,
@@ -96,7 +97,7 @@ function PressableWithSecondaryInteraction(
9697
// ESLint is disabled here to propagate all the props, enhancing PressableWithSecondaryInteraction's versatility across different use cases.
9798
// eslint-disable-next-line react/jsx-props-no-spreading
9899
{...rest}
99-
wrapperStyle={StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle)}
100+
wrapperStyle={[StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle), wrapperStyle]}
100101
onLongPress={onSecondaryInteraction ? executeSecondaryInteraction : undefined}
101102
pressDimmingValue={activeOpacity}
102103
ref={pressableRef}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const DELAY_FACTOR = 1.85;
2+
3+
export default {};
4+
5+
export {DELAY_FACTOR};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {isMobile} from '@libs/Browser';
2+
3+
// It takes varying amount of time to navigate to a new page on mobile and desktop
4+
// This variable takes that into account
5+
const DELAY_FACTOR = isMobile() ? 1 : 0.2;
6+
export default {};
7+
8+
export {DELAY_FACTOR};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import {InteractionManager} from 'react-native';
3+
import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated';
4+
import useTheme from '@hooks/useTheme';
5+
import CONST from '@src/CONST';
6+
import {DELAY_FACTOR} from './config';
7+
8+
type Props = {
9+
/** Border radius of the wrapper */
10+
borderRadius: number;
11+
12+
/** Height of the item that is to be faded */
13+
height: number;
14+
15+
/** Whether the item should be highlighted */
16+
shouldHighlight: boolean;
17+
18+
/** Duration of the highlight animation */
19+
highlightDuration?: number;
20+
21+
/** Delay before the highlight animation starts */
22+
delay?: number;
23+
};
24+
25+
/**
26+
* Returns a highlight style that interpolates the colour, height and opacity giving a fading effect.
27+
*/
28+
export default function useAnimatedHighlightStyle({
29+
borderRadius,
30+
shouldHighlight,
31+
highlightDuration = CONST.ANIMATED_HIGHLIGHT_DURATION,
32+
delay = CONST.ANIMATED_HIGHLIGHT_DELAY,
33+
height,
34+
}: Props) {
35+
const actualDelay = delay * DELAY_FACTOR;
36+
const repeatableProgress = useSharedValue(0);
37+
const nonRepeatableProgress = useSharedValue(shouldHighlight ? 0 : 1);
38+
const theme = useTheme();
39+
40+
const highlightBackgroundStyle = useAnimatedStyle(() => ({
41+
backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]),
42+
height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]),
43+
opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]),
44+
borderRadius,
45+
}));
46+
47+
React.useEffect(() => {
48+
if (!shouldHighlight) {
49+
return;
50+
}
51+
52+
InteractionManager.runAfterInteractions(() => {
53+
runOnJS(() => {
54+
nonRepeatableProgress.value = withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}));
55+
repeatableProgress.value = withSequence(
56+
withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})),
57+
withDelay(actualDelay, withTiming(0, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})),
58+
);
59+
})();
60+
});
61+
}, [shouldHighlight, highlightDuration, actualDelay, repeatableProgress, nonRepeatableProgress]);
62+
63+
return highlightBackgroundStyle;
64+
}

src/libs/actions/Policy.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3688,9 +3688,10 @@ function openPolicyDistanceRatesPage(policyID?: string) {
36883688

36893689
function navigateWhenEnableFeature(policyID: string, featureRoute: Route) {
36903690
const isNarrowLayout = getIsNarrowLayout();
3691-
36923691
if (isNarrowLayout) {
3693-
Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policyID));
3692+
setTimeout(() => {
3693+
Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID));
3694+
}, 1000);
36943695
return;
36953696
}
36963697

src/pages/workspace/WorkspaceInitialPage.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import type {ValueOf} from 'type-fest';
88
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
99
import ConfirmModal from '@components/ConfirmModal';
1010
import HeaderWithBackButton from '@components/HeaderWithBackButton';
11+
import HighlightableMenuItem from '@components/HighlightableMenuItem';
1112
import * as Expensicons from '@components/Icon/Expensicons';
12-
import MenuItem from '@components/MenuItem';
1313
import OfflineWithFeedback from '@components/OfflineWithFeedback';
1414
import ScreenWrapper from '@components/ScreenWrapper';
1515
import ScrollView from '@components/ScrollView';
@@ -225,6 +225,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
225225
];
226226

227227
const prevPolicy = usePrevious(policy);
228+
const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems);
229+
const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName));
228230

229231
// eslint-disable-next-line rulesdir/no-negated-variables
230232
const shouldShowNotFoundPage =
@@ -276,7 +278,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
276278
In this case where user can click on workspace avatar or menu items, we need to have a check for `isExecuting`. So, we are directly mapping menuItems.
277279
*/}
278280
{menuItems.map((item) => (
279-
<MenuItem
281+
<HighlightableMenuItem
280282
key={item.translationKey}
281283
disabled={hasPolicyCreationError || isExecuting}
282284
interactive={!hasPolicyCreationError}
@@ -285,6 +287,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
285287
onPress={item.action}
286288
brickRoadIndicator={item.brickRoadIndicator}
287289
wrapperStyle={styles.sectionMenuItem}
290+
highlighted={enabledItem?.routeName === item.routeName}
288291
focused={!!(item.routeName && activeRoute?.startsWith(item.routeName))}
289292
hoverAndPressStyle={styles.hoveredComponentBG}
290293
isPaneMenu

0 commit comments

Comments
 (0)