Skip to content

Commit

Permalink
Merge pull request #38546 from allroundexperts/fix-38372
Browse files Browse the repository at this point in the history
feat: Add temporary focus to enabled option in workspace initial page
  • Loading branch information
luacmartins authored Apr 4, 2024
2 parents 93e9719 + 64c2a45 commit de09508
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 5 deletions.
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const CONST = {
// Note: Group and Self-DM excluded as these are not tied to a Workspace
WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT],
ANDROID_PACKAGE_NAME,
ANIMATED_HIGHLIGHT_DELAY: 500,
ANIMATED_HIGHLIGHT_DURATION: 500,
ANIMATED_TRANSITION: 300,
ANIMATED_TRANSITION_FROM_VALUE: 100,
ANIMATION_IN_TIMING: 100,
Expand Down
37 changes: 37 additions & 0 deletions src/components/HighlightableMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef} from 'react';
import type {View} from 'react-native';
import {StyleSheet} from 'react-native';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useThemeStyles from '@hooks/useThemeStyles';
import MenuItem from './MenuItem';
import type {MenuItemProps} from './MenuItem';

type Props = MenuItemProps & {
/** Should the menu item be highlighted? */
highlighted?: boolean;
};

function HighlightableMenuItem({wrapperStyle, highlighted, ...restOfProps}: Props, ref: ForwardedRef<View>) {
const styles = useThemeStyles();

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,
});

return (
<MenuItem
// eslint-disable-next-line react/jsx-props-no-spreading
{...restOfProps}
outerWrapperStyle={animatedHighlightStyle}
ref={ref}
/>
);
}

HighlightableMenuItem.displayName = 'HighlightableMenuItem';

export default forwardRef(HighlightableMenuItem);
5 changes: 5 additions & 0 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ type MenuItemBaseProps = {
/** Used to apply offline styles to child text components */
style?: StyleProp<ViewStyle>;

/** Outer wrapper styles */
outerWrapperStyle?: StyleProp<ViewStyle>;

/** Any additional styles to apply */
wrapperStyle?: StyleProp<ViewStyle>;

Expand Down Expand Up @@ -257,6 +260,7 @@ function MenuItem(
badgeText,
style,
wrapperStyle,
outerWrapperStyle,
containerStyle,
titleStyle,
hoverAndPressStyle,
Expand Down Expand Up @@ -426,6 +430,7 @@ function MenuItem(
onPressIn={() => shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={ControlSelection.unblock}
onSecondaryInteraction={onSecondaryInteraction}
wrapperStyle={outerWrapperStyle}
style={({pressed}) =>
[
containerStyle,
Expand Down
3 changes: 2 additions & 1 deletion src/components/PressableWithSecondaryInteraction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function PressableWithSecondaryInteraction(
children,
inline = false,
style,
wrapperStyle,
enableLongPressWithHover = false,
withoutFocusOnSecondaryInteraction = false,
needsOffscreenAlphaCompositing = false,
Expand Down Expand Up @@ -96,7 +97,7 @@ function PressableWithSecondaryInteraction(
// ESLint is disabled here to propagate all the props, enhancing PressableWithSecondaryInteraction's versatility across different use cases.
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
wrapperStyle={StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle)}
wrapperStyle={[StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle), wrapperStyle]}
onLongPress={onSecondaryInteraction ? executeSecondaryInteraction : undefined}
pressDimmingValue={activeOpacity}
ref={pressableRef}
Expand Down
5 changes: 5 additions & 0 deletions src/hooks/useAnimatedHighlightStyle/config.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const DELAY_FACTOR = 1.85;

export default {};

export {DELAY_FACTOR};
8 changes: 8 additions & 0 deletions src/hooks/useAnimatedHighlightStyle/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {isMobile} from '@libs/Browser';

// It takes varying amount of time to navigate to a new page on mobile and desktop
// This variable takes that into account
const DELAY_FACTOR = isMobile() ? 1 : 0.2;
export default {};

export {DELAY_FACTOR};
64 changes: 64 additions & 0 deletions src/hooks/useAnimatedHighlightStyle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import {InteractionManager} from 'react-native';
import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated';
import useTheme from '@hooks/useTheme';
import CONST from '@src/CONST';
import {DELAY_FACTOR} from './config';

type Props = {
/** Border radius of the wrapper */
borderRadius: number;

/** Height of the item that is to be faded */
height: number;

/** Whether the item should be highlighted */
shouldHighlight: boolean;

/** Duration of the highlight animation */
highlightDuration?: number;

/** Delay before the highlight animation starts */
delay?: number;
};

/**
* Returns a highlight style that interpolates the colour, height and opacity giving a fading effect.
*/
export default function useAnimatedHighlightStyle({
borderRadius,
shouldHighlight,
highlightDuration = CONST.ANIMATED_HIGHLIGHT_DURATION,
delay = CONST.ANIMATED_HIGHLIGHT_DELAY,
height,
}: Props) {
const actualDelay = delay * DELAY_FACTOR;
const repeatableProgress = useSharedValue(0);
const nonRepeatableProgress = useSharedValue(shouldHighlight ? 0 : 1);
const theme = useTheme();

const highlightBackgroundStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]),
height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]),
opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]),
borderRadius,
}));

React.useEffect(() => {
if (!shouldHighlight) {
return;
}

InteractionManager.runAfterInteractions(() => {
runOnJS(() => {
nonRepeatableProgress.value = withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}));
repeatableProgress.value = withSequence(
withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})),
withDelay(actualDelay, withTiming(0, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})),
);
})();
});
}, [shouldHighlight, highlightDuration, actualDelay, repeatableProgress, nonRepeatableProgress]);

return highlightBackgroundStyle;
}
5 changes: 3 additions & 2 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3688,9 +3688,10 @@ function openPolicyDistanceRatesPage(policyID?: string) {

function navigateWhenEnableFeature(policyID: string, featureRoute: Route) {
const isNarrowLayout = getIsNarrowLayout();

if (isNarrowLayout) {
Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policyID));
setTimeout(() => {
Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID));
}, 1000);
return;
}

Expand Down
7 changes: 5 additions & 2 deletions src/pages/workspace/WorkspaceInitialPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type {ValueOf} from 'type-fest';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ConfirmModal from '@components/ConfirmModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import HighlightableMenuItem from '@components/HighlightableMenuItem';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
Expand Down Expand Up @@ -225,6 +225,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
];

const prevPolicy = usePrevious(policy);
const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems);
const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName));

// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage =
Expand Down Expand Up @@ -276,7 +278,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
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.
*/}
{menuItems.map((item) => (
<MenuItem
<HighlightableMenuItem
key={item.translationKey}
disabled={hasPolicyCreationError || isExecuting}
interactive={!hasPolicyCreationError}
Expand All @@ -285,6 +287,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
onPress={item.action}
brickRoadIndicator={item.brickRoadIndicator}
wrapperStyle={styles.sectionMenuItem}
highlighted={enabledItem?.routeName === item.routeName}
focused={!!(item.routeName && activeRoute?.startsWith(item.routeName))}
hoverAndPressStyle={styles.hoveredComponentBG}
isPaneMenu
Expand Down

0 comments on commit de09508

Please sign in to comment.