Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add temporary focus to enabled option in workspace initial page #38546

Merged
merged 23 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
mountiny marked this conversation as resolved.
Show resolved Hide resolved
};

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
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;
mountiny marked this conversation as resolved.
Show resolved Hide resolved
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
Loading