-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Adding animation for children of switch components #53938
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
Changes from 11 commits
dad3f55
457b549
eaa7d47
d852938
2b2b451
c2fb131
4473400
7443bc5
6c8771d
24cd3ca
8b7525a
23c3b57
408750f
be4015e
53fd209
314dca1
d99e589
20401d1
007eac8
65f680d
09794bf
b9aed8e
a23be73
67a6187
a65661f
d7e1334
a174547
8c39088
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,45 @@ | ||
| 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'; | ||
|
|
||
| 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; | ||
| }; | ||
|
|
||
| function Accordion({isExpanded, children, duration = 300}: AccordionProps) { | ||
| 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={[styles.pAbsolute, styles.l0, styles.r0, styles.t0]} | ||
| > | ||
| {children} | ||
| </View> | ||
| </Animated.View> | ||
| ); | ||
| } | ||
|
sumo-slonik marked this conversation as resolved.
|
||
|
|
||
| export default Accordion; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(() => { | ||
|
|
||
| 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 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro | |
|
|
||
| const policy = usePolicy(route.params.policyID); | ||
| const mappingName: SageIntacctMappingName = route.params.mapping; | ||
| const policyID: string = policy?.id ?? '-1'; | ||
|
|
||
| const policyID: string = policy?.id ?? CONST.DEFAULT_POLICY_ID; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please read through
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, let's avoid any defaulting values for string ids!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems to me that the example in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes please lets remove this
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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])); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
| }, [isImportMappingEnable, config?.mappings, mappingName]); | ||
|
|
||
| return ( | ||
| <ConnectionLayout | ||
| displayName={SageIntacctToggleMappingsPage.displayName} | ||
|
|
@@ -81,12 +95,13 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro | |
| 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}> | ||
| <OfflineWithFeedback pendingAction={settingsPendingAction([mappingName], config?.pendingFields)}> | ||
| <MenuItemWithTopDescription | ||
| title={translationKeys?.titleKey ? translate(translationKeys?.titleKey) : undefined} | ||
|
|
@@ -102,7 +117,7 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro | |
| {translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined} | ||
| </Text> | ||
| </OfflineWithFeedback> | ||
| )} | ||
| </Accordion> | ||
| </ConnectionLayout> | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,8 @@ import type {ReactNode} from 'react'; | |||||
| import React, {useMemo} from 'react'; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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'; | ||||||
|
|
@@ -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') { | ||||||
|
|
@@ -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> | ||||||
| ); | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.