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

Adding animation for children of switch components #53938

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
34 changes: 34 additions & 0 deletions src/components/Accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {ReactNode} from 'react';
import React from 'react';
import {View} from 'react-native';
import type {SharedValue} from 'react-native-reanimated';
import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
import useThemeStyles from '@hooks/useThemeStyles';

function Accordion({isExpanded, children, duration = 300}: {isExpanded: SharedValue<boolean>; children: ReactNode; duration?: number}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's extract props type to a separate type above, a good rule of thumb is to always do it for complex types like this

const height = useSharedValue(0);
const styles = useThemeStyles();
const derivedHeight = useDerivedValue(() =>
withTiming(height.get() * Number(isExpanded.get()), {
duration,
}),
);
const bodyStyle = useAnimatedStyle(() => ({
height: derivedHeight.get(),
}));

return (
<Animated.View style={[bodyStyle, styles.overflowHidden]}>
<View
onLayout={(e) => {
height.set(e.nativeEvent.layout.height);
}}
style={{position: 'absolute', left: 0, right: 0, top: 0}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use styling utilities here:

styles.pAbsolute, styles.t0 ...

>
{children}
</View>
</Animated.View>
);
}

export default Accordion;
13 changes: 11 additions & 2 deletions src/components/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,30 @@ type SwitchProps = {

/** Callback to fire when the switch is toggled in disabled state */
disabledAction?: () => void;

/**
* Callback function triggered only after a successful change
* in the external state of the switch, after the switch state change animation is triggered
*/
onStateChange?: (isOn: boolean) => void;
};

const OFFSET_X = {
OFF: 0,
ON: 20,
};

function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, disabledAction}: SwitchProps) {
function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, disabledAction, onStateChange}: SwitchProps) {
const styles = useThemeStyles();
const offsetX = useSharedValue(isOn ? OFFSET_X.ON : OFFSET_X.OFF);
const theme = useTheme();

useEffect(() => {
offsetX.set(withTiming(isOn ? OFFSET_X.ON : OFFSET_X.OFF, {duration: 300}));
}, [isOn, offsetX]);
if (onStateChange) {
onStateChange(isOn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use onToggle for this? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it seems to me that it does not, because doing it in onToggle called the animation trigger at the wrong time, but I will make sure that in the final version of the code it did not become possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to remove the use of this method and it is no longer needed.

}
}, [onStateChange, isOn, offsetX]);

const handleSwitchPress = () => {
InteractionManager.runAfterInteractions(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Str} from 'expensify-common';
import React from 'react';
import React, {useEffect, useState} from 'react';
import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
Expand Down Expand Up @@ -51,10 +51,22 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro
const policy = usePolicy(route.params.policyID);
const mappingName: SageIntacctMappingName = route.params.mapping;
const policyID: string = policy?.id ?? '-1';

const config = policy?.connections?.intacct?.config;
const translationKeys = getDisplayTypeTranslationKeys(config?.mappings?.[mappingName]);
const isImportMappingEnable = config?.mappings?.[mappingName] !== CONST.SAGE_INTACCT_MAPPING_VALUE.NONE;

// We are storing translation keys in the local state for animation purposes.
// Otherwise, the values change to undefined immediately after clicking, before the closing animation finishes,
// resulting in a janky animation effect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

const [translationKeys, setTranslationKey] = useState<DisplayTypeTranslationKeys | undefined>(undefined);

useEffect(() => {
if (!isImportMappingEnable) {
return;
}
setTranslationKey(getDisplayTypeTranslationKeys(config?.mappings?.[mappingName]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line will be confusing, since assigning translation keys to state is quite uncommon.
Perhaps we could add a short 1-line comment to at least say its done for animation purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that changing the switch changes the content of the object:
config.mappings
which resulted in changing the content of the translation itself, that's why I changed the assignment here to only non-empty values, I was afraid of operating on the config logic itself so I think it's safe to leave a comment here

}, [isImportMappingEnable, config?.mappings, mappingName]);

return (
<ConnectionLayout
displayName={SageIntacctToggleMappingsPage.displayName}
Expand Down Expand Up @@ -85,24 +97,24 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro
pendingAction={settingsPendingAction([mappingName], config?.pendingFields)}
errors={ErrorUtils.getLatestErrorField(config ?? {}, mappingName)}
onCloseError={() => clearSageIntacctErrorField(policyID, mappingName)}
subMenuItems={
<OfflineWithFeedback pendingAction={settingsPendingAction([mappingName], config?.pendingFields)}>
<MenuItemWithTopDescription
title={translationKeys?.titleKey ? translate(translationKeys?.titleKey) : undefined}
description={translate('workspace.common.displayedAs')}
shouldShowRightIcon
onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE.getRoute(policyID, mappingName))}
brickRoadIndicator={areSettingsInErrorFields([mappingName], config?.errorFields) ? 'error' : undefined}
/>
<Text
style={[styles.textLabelSupporting, styles.ph5]}
numberOfLines={2}
>
{translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined}
</Text>
</OfflineWithFeedback>
}
/>
{isImportMappingEnable && (
<OfflineWithFeedback pendingAction={settingsPendingAction([mappingName], config?.pendingFields)}>
<MenuItemWithTopDescription
title={translationKeys?.titleKey ? translate(translationKeys?.titleKey) : undefined}
description={translate('workspace.common.displayedAs')}
shouldShowRightIcon
onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE.getRoute(policyID, mappingName))}
brickRoadIndicator={areSettingsInErrorFields([mappingName], config?.errorFields) ? 'error' : undefined}
/>
<Text
style={[styles.textLabelSupporting, styles.ph5]}
numberOfLines={2}
>
{translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined}
</Text>
</OfflineWithFeedback>
)}
</ConnectionLayout>
);
}
Expand Down
9 changes: 8 additions & 1 deletion src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type {ReactNode} from 'react';
import React, {useMemo} from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import React, {useMemo} from 'react';
import React, {useMemo, useEffect} from 'react';

import {View} from 'react-native';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
import Accordion from '@components/Accordion';
import Icon from '@components/Icon';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import RenderHTML from '@components/RenderHTML';
Expand Down Expand Up @@ -98,6 +100,10 @@ function ToggleSettingOptionRow({
showLockIcon = false,
}: ToggleSettingOptionRowProps) {
const styles = useThemeStyles();
const isExpanded = useSharedValue(isActive);
const setIsExpanded = (value: boolean) => {
isExpanded.set(value);
};

const subtitleHtml = useMemo(() => {
if (!subtitle || !shouldParseSubtitle || typeof subtitle !== 'string') {
Expand Down Expand Up @@ -175,10 +181,11 @@ function ToggleSettingOptionRow({
isOn={isActive}
disabled={disabled}
showLockIcon={showLockIcon}
onStateChange={setIsExpanded}
/>
</View>
{shouldPlaceSubtitleBelowSwitch && subtitle && subTitleView}
{isActive && subMenuItems}
<Accordion isExpanded={isExpanded}>{subMenuItems}</Accordion>
</View>
</OfflineWithFeedback>
);
Expand Down
Loading