-
Notifications
You must be signed in to change notification settings - Fork 3k
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
base: main
Are you sure you want to change the base?
Changes from all commits
dad3f55
457b549
eaa7d47
d852938
2b2b451
c2fb131
4473400
7443bc5
6c8771d
24cd3ca
8b7525a
23c3b57
408750f
be4015e
53fd209
314dca1
d99e589
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type {ReactNode} from 'react'; | ||
import React from 'react'; | ||
import type {StyleProp, ViewStyle} from 'react-native'; | ||
import {View} from 'react-native'; | ||
import type {SharedValue} from 'react-native-reanimated'; | ||
import Animated, {Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated'; | ||
import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
||
type AccordionProps = { | ||
/** Giving information whether the component is open */ | ||
isExpanded: SharedValue<boolean>; | ||
|
||
/** Element that is inside Accordion */ | ||
children: ReactNode; | ||
|
||
/** Duration of expansion animation */ | ||
duration?: number; | ||
|
||
/** Additional external style */ | ||
style?: StyleProp<ViewStyle>; | ||
}; | ||
|
||
function Accordion({isExpanded, children, duration = 300, style}: AccordionProps) { | ||
const height = useSharedValue(0); | ||
const styles = useThemeStyles(); | ||
|
||
const derivedHeight = useDerivedValue(() => | ||
withTiming(height.get() * Number(isExpanded.get()), { | ||
duration, | ||
easing: Easing.inOut(Easing.quad), | ||
}), | ||
); | ||
|
||
const derivedOpacity = useDerivedValue(() => | ||
withTiming(isExpanded.get() ? 1 : 0, { | ||
duration, | ||
easing: Easing.inOut(Easing.quad), | ||
}), | ||
); | ||
|
||
const bodyStyle = useAnimatedStyle(() => ({ | ||
height: derivedHeight.get(), | ||
opacity: derivedOpacity.get(), | ||
})); | ||
|
||
return ( | ||
<Animated.View style={[bodyStyle, style]}> | ||
<View | ||
onLayout={(e) => { | ||
height.set(e.nativeEvent.layout.height); | ||
}} | ||
style={[styles.pAbsolute, styles.l0, styles.r0, styles.t0]} | ||
> | ||
{children} | ||
</View> | ||
</Animated.View> | ||
); | ||
} | ||
|
||
export default Accordion; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import React, {useMemo} from 'react'; | ||
import React, {useEffect, useMemo} from 'react'; | ||
import {useSharedValue} from 'react-native-reanimated'; | ||
import Accordion from '@components/Accordion'; | ||
import ConnectionLayout from '@components/ConnectionLayout'; | ||
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; | ||
import OfflineWithFeedback from '@components/OfflineWithFeedback'; | ||
|
@@ -28,11 +30,16 @@ | |
|
||
function SageIntacctAdvancedPage({policy}: WithPolicyProps) { | ||
const {translate} = useLocalize(); | ||
const policyID = policy?.id ?? '-1'; | ||
Check failure on line 33 in src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx GitHub Actions / Changed files ESLint check
|
||
const styles = useThemeStyles(); | ||
|
||
const {importEmployees, autoSync, sync, pendingFields, errorFields} = policy?.connections?.intacct?.config ?? {}; | ||
const {data, config} = policy?.connections?.intacct ?? {}; | ||
const isAccordionExpanded = useSharedValue(!!sync?.syncReimbursedReports); | ||
|
||
useEffect(() => { | ||
isAccordionExpanded.set(!!sync?.syncReimbursedReports); | ||
}, [isAccordionExpanded, sync?.syncReimbursedReports]); | ||
|
||
const toggleSections = useMemo( | ||
() => [ | ||
|
@@ -70,7 +77,7 @@ | |
updateSageIntacctSyncReimbursedReports(policyID, enabled); | ||
|
||
if (enabled && !sync?.reimbursementAccountID) { | ||
const reimbursementAccountID = data?.bankAccounts[0]?.id ?? ''; | ||
Check failure on line 80 in src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx GitHub Actions / Changed files ESLint check
|
||
updateSageIntacctSyncReimbursementAccountID(policyID, reimbursementAccountID); | ||
} | ||
}, | ||
|
@@ -113,20 +120,23 @@ | |
/> | ||
))} | ||
|
||
{!!sync?.syncReimbursedReports && ( | ||
<Accordion | ||
isExpanded={isAccordionExpanded} | ||
style={styles.overflowHidden} | ||
> | ||
<OfflineWithFeedback | ||
key={translate('workspace.sageIntacct.paymentAccount')} | ||
pendingAction={settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID], pendingFields)} | ||
> | ||
<MenuItemWithTopDescription | ||
title={getReimbursedAccountName(data?.bankAccounts ?? [], sync.reimbursementAccountID) ?? translate('workspace.sageIntacct.notConfigured')} | ||
title={getReimbursedAccountName(data?.bankAccounts ?? [], sync?.reimbursementAccountID) ?? translate('workspace.sageIntacct.notConfigured')} | ||
description={translate('workspace.sageIntacct.paymentAccount')} | ||
shouldShowRightIcon | ||
onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.getRoute(policyID))} | ||
brickRoadIndicator={areSettingsInErrorFields([CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID], errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} | ||
/> | ||
</OfflineWithFeedback> | ||
)} | ||
</Accordion> | ||
</ConnectionLayout> | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import {Str} from 'expensify-common'; | ||
import React from 'react'; | ||
import React, {useEffect, useState} from 'react'; | ||
import {useSharedValue} from 'react-native-reanimated'; | ||
import Accordion from '@components/Accordion'; | ||
import ConnectionLayout from '@components/ConnectionLayout'; | ||
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; | ||
import OfflineWithFeedback from '@components/OfflineWithFeedback'; | ||
|
@@ -50,11 +52,23 @@ | |
|
||
const policy = usePolicy(route.params.policyID); | ||
const mappingName: SageIntacctMappingName = route.params.mapping; | ||
const policyID: string = policy?.id ?? '-1'; | ||
Check failure on line 55 in src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx GitHub Actions / Changed files ESLint check
|
||
|
||
const config = policy?.connections?.intacct?.config; | ||
const translationKeys = getDisplayTypeTranslationKeys(config?.mappings?.[mappingName]); | ||
const isImportMappingEnable = config?.mappings?.[mappingName] !== CONST.SAGE_INTACCT_MAPPING_VALUE.NONE; | ||
const isAccordionExpanded = useSharedValue(isImportMappingEnable); | ||
|
||
// 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. | ||
const [translationKeys, setTranslationKey] = useState<DisplayTypeTranslationKeys | undefined>(undefined); | ||
|
||
useEffect(() => { | ||
if (!isImportMappingEnable) { | ||
return; | ||
} | ||
setTranslationKey(getDisplayTypeTranslationKeys(config?.mappings?.[mappingName])); | ||
}, [isImportMappingEnable, config?.mappings, mappingName]); | ||
|
||
return ( | ||
<ConnectionLayout | ||
displayName={SageIntacctToggleMappingsPage.displayName} | ||
|
@@ -81,28 +95,27 @@ | |
onToggle={(enabled) => { | ||
const mappingValue = enabled ? CONST.SAGE_INTACCT_MAPPING_VALUE.TAG : CONST.SAGE_INTACCT_MAPPING_VALUE.NONE; | ||
updateSageIntacctMappingValue(policyID, mappingName, mappingValue, config?.mappings?.[mappingName]); | ||
isAccordionExpanded.set(enabled); | ||
}} | ||
pendingAction={settingsPendingAction([mappingName], config?.pendingFields)} | ||
errors={ErrorUtils.getLatestErrorField(config ?? {}, mappingName)} | ||
onCloseError={() => clearSageIntacctErrorField(policyID, mappingName)} | ||
/> | ||
{isImportMappingEnable && ( | ||
<Accordion | ||
isExpanded={isAccordionExpanded} | ||
style={styles.overflowHidden} | ||
> | ||
<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} | ||
hintText={translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined} | ||
/> | ||
<Text | ||
style={[styles.textLabelSupporting, styles.ph5]} | ||
numberOfLines={2} | ||
> | ||
{translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined} | ||
</Text> | ||
</OfflineWithFeedback> | ||
)} | ||
</Accordion> | ||
</ConnectionLayout> | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import {CONST as COMMON_CONST} from 'expensify-common'; | ||
import React from 'react'; | ||
import React, {useEffect} from 'react'; | ||
import {useSharedValue} from 'react-native-reanimated'; | ||
import Accordion from '@components/Accordion'; | ||
import HeaderWithBackButton from '@components/HeaderWithBackButton'; | ||
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; | ||
import OfflineWithFeedback from '@components/OfflineWithFeedback'; | ||
|
@@ -24,11 +26,17 @@ | |
const {translate} = useLocalize(); | ||
const config = policy?.connections?.netsuite?.options?.config; | ||
const autoSyncConfig = policy?.connections?.netsuite?.config; | ||
const policyID = route.params.policyID ?? '-1'; | ||
Check failure on line 29 in src/pages/workspace/accounting/netsuite/advanced/NetSuiteAutoSyncPage.tsx GitHub Actions / Changed files ESLint check
|
||
const accountingMethod = policy?.connections?.netsuite?.options?.config?.accountingMethod; | ||
const pendingAction = | ||
settingsPendingAction([CONST.NETSUITE_CONFIG.AUTO_SYNC], autoSyncConfig?.pendingFields) ?? settingsPendingAction([CONST.NETSUITE_CONFIG.ACCOUNTING_METHOD], config?.pendingFields); | ||
|
||
const isAccordionExpanded = useSharedValue(!!autoSyncConfig?.autoSync?.enabled); | ||
|
||
useEffect(() => { | ||
isAccordionExpanded.set(!!autoSyncConfig?.autoSync?.enabled); | ||
}, [isAccordionExpanded, autoSyncConfig?.autoSync?.enabled]); | ||
|
||
return ( | ||
<AccessOrNotFoundWrapper | ||
policyID={policyID} | ||
|
@@ -58,7 +66,11 @@ | |
pendingAction={pendingAction} | ||
errors={ErrorUtils.getLatestErrorField(autoSyncConfig, CONST.NETSUITE_CONFIG.AUTO_SYNC)} | ||
/> | ||
{!!autoSyncConfig?.autoSync?.enabled && ( | ||
|
||
<Accordion | ||
isExpanded={isAccordionExpanded} | ||
style={styles.overflowHidden} | ||
> | ||
<OfflineWithFeedback pendingAction={pendingAction}> | ||
<MenuItemWithTopDescription | ||
title={ | ||
|
@@ -71,7 +83,7 @@ | |
onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_ACCOUNTING_METHOD.getRoute(policyID))} | ||
/> | ||
</OfflineWithFeedback> | ||
)} | ||
</Accordion> | ||
</ScreenWrapper> | ||
</AccessOrNotFoundWrapper> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import React, {useMemo} from 'react'; | ||
import React, {useEffect, useMemo} from 'react'; | ||
import {useSharedValue} from 'react-native-reanimated'; | ||
import Accordion from '@components/Accordion'; | ||
import ConnectionLayout from '@components/ConnectionLayout'; | ||
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; | ||
import OfflineWithFeedback from '@components/OfflineWithFeedback'; | ||
|
@@ -20,7 +22,7 @@ | |
function QuickbooksDesktopCompanyCardExpenseAccountPage({policy}: WithPolicyConnectionsProps) { | ||
const {translate} = useLocalize(); | ||
const styles = useThemeStyles(); | ||
const policyID = policy?.id ?? '-1'; | ||
Check failure on line 25 in src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountPage.tsx GitHub Actions / Changed files ESLint check
|
||
const qbdConfig = policy?.connections?.quickbooksDesktop?.config; | ||
const {vendors} = policy?.connections?.quickbooksDesktop?.data ?? {}; | ||
const nonReimbursableBillDefaultVendorObject = vendors?.find((vendor) => vendor.id === qbdConfig?.export?.nonReimbursableBillDefaultVendor); | ||
|
@@ -34,6 +36,12 @@ | |
return qbdReimbursableAccounts.find(({id}) => nonReimbursableAccount === id)?.name || qbdReimbursableAccounts.at(0)?.name || translate('workspace.qbd.notConfigured'); | ||
}, [policy?.connections?.quickbooksDesktop, nonReimbursable, translate, nonReimbursableAccount]); | ||
|
||
const isAccordionExpanded = useSharedValue(!!qbdConfig?.shouldAutoCreateVendor); | ||
|
||
useEffect(() => { | ||
isAccordionExpanded.set(!!qbdConfig?.shouldAutoCreateVendor); | ||
}, [isAccordionExpanded, qbdConfig?.shouldAutoCreateVendor]); | ||
|
||
const sections = [ | ||
{ | ||
title: nonReimbursable ? translate(`workspace.qbd.accounts.${nonReimbursable}`) : undefined, | ||
|
@@ -96,7 +104,11 @@ | |
onToggle={(isOn) => QuickbooksDesktop.updateQuickbooksDesktopShouldAutoCreateVendor(policyID, isOn)} | ||
onCloseError={() => clearQBDErrorField(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR)} | ||
/> | ||
{!!qbdConfig?.shouldAutoCreateVendor && ( | ||
|
||
<Accordion | ||
isExpanded={isAccordionExpanded} | ||
style={styles.overflowHidden} | ||
> | ||
<OfflineWithFeedback | ||
pendingAction={PolicyUtils.settingsPendingAction( | ||
[CONST.QUICKBOOKS_DESKTOP_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR, CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR], | ||
|
@@ -115,7 +127,7 @@ | |
shouldShowRightIcon | ||
/> | ||
</OfflineWithFeedback> | ||
)} | ||
</Accordion> | ||
</> | ||
)} | ||
</ConnectionLayout> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import React from 'react'; | ||
import React, {useEffect} from 'react'; | ||
import {useSharedValue} from 'react-native-reanimated'; | ||
import Accordion from '@components/Accordion'; | ||
import ConnectionLayout from '@components/ConnectionLayout'; | ||
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; | ||
import OfflineWithFeedback from '@components/OfflineWithFeedback'; | ||
|
@@ -18,11 +20,17 @@ | |
function QuickbooksDesktopClassesPage({policy}: WithPolicyProps) { | ||
const {translate} = useLocalize(); | ||
const styles = useThemeStyles(); | ||
const policyID = policy?.id ?? '-1'; | ||
Check failure on line 23 in src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx GitHub Actions / Changed files ESLint check
|
||
const qbdConfig = policy?.connections?.quickbooksDesktop?.config; | ||
const isSwitchOn = !!(qbdConfig?.mappings?.classes && qbdConfig.mappings.classes !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); | ||
const isReportFieldsSelected = qbdConfig?.mappings?.classes === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; | ||
|
||
const isAccordionExpanded = useSharedValue(isSwitchOn); | ||
|
||
useEffect(() => { | ||
isAccordionExpanded.set(isSwitchOn); | ||
}, [isAccordionExpanded, isSwitchOn]); | ||
|
||
return ( | ||
<ConnectionLayout | ||
displayName={QuickbooksDesktopClassesPage.displayName} | ||
|
@@ -50,7 +58,10 @@ | |
errors={ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES)} | ||
onCloseError={() => clearQBDErrorField(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES)} | ||
/> | ||
{isSwitchOn && ( | ||
<Accordion | ||
isExpanded={isAccordionExpanded} | ||
style={styles.overflowHidden} | ||
> | ||
<OfflineWithFeedback pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES], qbdConfig?.pendingFields)}> | ||
<MenuItemWithTopDescription | ||
title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} | ||
|
@@ -65,7 +76,7 @@ | |
} | ||
/> | ||
</OfflineWithFeedback> | ||
)} | ||
</Accordion> | ||
</ConnectionLayout> | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import React from 'react'; | ||
import React, {useEffect} from 'react'; | ||
import {useSharedValue} from 'react-native-reanimated'; | ||
import Accordion from '@components/Accordion'; | ||
import ConnectionLayout from '@components/ConnectionLayout'; | ||
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; | ||
import OfflineWithFeedback from '@components/OfflineWithFeedback'; | ||
|
@@ -18,11 +20,17 @@ | |
function QuickbooksDesktopCustomersPage({policy}: WithPolicyProps) { | ||
const {translate} = useLocalize(); | ||
const styles = useThemeStyles(); | ||
const policyID = policy?.id ?? '-1'; | ||
Check failure on line 23 in src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx GitHub Actions / Changed files ESLint check
|
||
const qbdConfig = policy?.connections?.quickbooksDesktop?.config; | ||
const isSwitchOn = !!(qbdConfig?.mappings?.customers && qbdConfig.mappings.customers !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); | ||
const isReportFieldsSelected = qbdConfig?.mappings?.customers === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; | ||
|
||
const isAccordionExpanded = useSharedValue(isSwitchOn); | ||
|
||
useEffect(() => { | ||
isAccordionExpanded.set(isSwitchOn); | ||
}, [isAccordionExpanded, isSwitchOn]); | ||
|
||
return ( | ||
<ConnectionLayout | ||
displayName={QuickbooksDesktopCustomersPage.displayName} | ||
|
@@ -50,7 +58,10 @@ | |
errors={ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CUSTOMERS)} | ||
onCloseError={() => clearQBDErrorField(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CUSTOMERS)} | ||
/> | ||
{isSwitchOn && ( | ||
<Accordion | ||
isExpanded={isAccordionExpanded} | ||
style={styles.overflowHidden} | ||
> | ||
<OfflineWithFeedback pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CUSTOMERS], qbdConfig?.pendingFields)}> | ||
<MenuItemWithTopDescription | ||
title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} | ||
|
@@ -65,7 +76,7 @@ | |
} | ||
/> | ||
</OfflineWithFeedback> | ||
)} | ||
</Accordion> | ||
</ConnectionLayout> | ||
); | ||
} | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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