-
Notifications
You must be signed in to change notification settings - Fork 4k
Animate submit button on Report Preview and Report Header #65997
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 all commits
5c04cf8
03af3b3
1489419
443a5db
2791bee
3273791
2e46b63
0b8d132
938d8b5
0389e5e
96ab6fa
56253f9
8f02af9
4f92a50
d3eca11
1bb0f74
fea44e0
891579d
926c016
f75d54a
4ce0c27
3d458d4
38513db
3fe37d7
a644db6
21499af
388c32b
fd540cc
3ca2d27
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,139 @@ | ||
| import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import Animated, {Keyframe, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; | ||
| import Button from '@components/Button'; | ||
| import * as Expensicons from '@components/Icon/Expensicons'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import variables from '@styles/variables'; | ||
| import CONST from '@src/CONST'; | ||
|
|
||
| type AnimatedSubmitButtonProps = { | ||
| // Whether to show the success state | ||
| success: boolean | undefined; | ||
|
|
||
| // Text to show on the button | ||
| text: string; | ||
|
|
||
| // Function to call when the button is pressed | ||
| onPress: () => void; | ||
|
|
||
| // Whether the animation is running | ||
| isSubmittingAnimationRunning: boolean; | ||
|
|
||
| // Function to call when the animation finishes | ||
| onAnimationFinish: () => void; | ||
| }; | ||
|
|
||
| function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunning, onAnimationFinish}: AnimatedSubmitButtonProps) { | ||
| const styles = useThemeStyles(); | ||
| const {translate} = useLocalize(); | ||
| const isAnimationRunning = isSubmittingAnimationRunning; | ||
| const buttonDuration = isSubmittingAnimationRunning ? CONST.ANIMATION_SUBMIT_DURATION : CONST.ANIMATION_SUBMITTED_DURATION; | ||
| const gap = styles.expenseAndReportPreviewTextButtonContainer.gap; | ||
| const buttonMarginTop = useSharedValue<number>(gap); | ||
| const height = useSharedValue<number>(variables.componentSizeNormal); | ||
| const [canShow, setCanShow] = useState(true); | ||
| const [minWidth, setMinWidth] = useState<number>(0); | ||
| const [isShowingLoading, setIsShowingLoading] = useState(false); | ||
| const viewRef = useRef<HTMLElement | null>(null); | ||
|
|
||
| const containerStyles = useAnimatedStyle(() => ({ | ||
| height: height.get(), | ||
| justifyContent: 'center', | ||
| })); | ||
|
|
||
| const stretchOutY = useCallback(() => { | ||
| 'worklet'; | ||
|
|
||
| if (canShow) { | ||
| runOnJS(onAnimationFinish)(); | ||
| return; | ||
| } | ||
| height.set(withTiming(0, {duration: buttonDuration}, () => runOnJS(onAnimationFinish)())); | ||
| }, [buttonDuration, height, onAnimationFinish, canShow]); | ||
|
|
||
| const buttonAnimation = useMemo( | ||
| () => | ||
| new Keyframe({ | ||
| from: { | ||
| opacity: 1, | ||
| transform: [{scale: 1}], | ||
| }, | ||
| to: { | ||
| opacity: 0, | ||
| transform: [{scale: 0}], | ||
| }, | ||
| }) | ||
| .duration(buttonDuration) | ||
| .withCallback(stretchOutY), | ||
| [buttonDuration, stretchOutY], | ||
| ); | ||
| const icon = isAnimationRunning ? Expensicons.Send : null; | ||
|
|
||
| useEffect(() => { | ||
| if (!isAnimationRunning) { | ||
| setMinWidth(0); | ||
| setCanShow(true); | ||
| setIsShowingLoading(false); | ||
| height.set(variables.componentSizeNormal); | ||
| buttonMarginTop.set(0); | ||
| return; | ||
| } | ||
|
|
||
| setMinWidth(viewRef.current?.getBoundingClientRect?.().width ?? 0); | ||
| setIsShowingLoading(true); | ||
|
|
||
| const timer = setTimeout(() => { | ||
| setIsShowingLoading(false); | ||
| }, CONST.ANIMATION_SUBMIT_LOADING_STATE_DURATION); | ||
|
allgandalf marked this conversation as resolved.
|
||
|
|
||
| return () => clearTimeout(timer); | ||
| }, [buttonMarginTop, gap, height, isAnimationRunning]); | ||
|
|
||
| useEffect(() => { | ||
| if (!isAnimationRunning || isShowingLoading) { | ||
| return; | ||
| } | ||
|
|
||
| const timer = setTimeout(() => setCanShow(false), CONST.ANIMATION_SUBMIT_SUBMITTED_STATE_VISIBLE_DURATION); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| }, [isAnimationRunning, isShowingLoading]); | ||
|
|
||
| // eslint-disable-next-line react-compiler/react-compiler | ||
| const showLoading = isShowingLoading || (!viewRef.current && isAnimationRunning); | ||
|
|
||
| return ( | ||
| <Animated.View style={[containerStyles, {minWidth}]}> | ||
| {isAnimationRunning && canShow && ( | ||
| <Animated.View | ||
| ref={(el) => { | ||
| viewRef.current = el as HTMLElement | null; | ||
| }} | ||
| exiting={buttonAnimation} | ||
| > | ||
| <Button | ||
| success={success} | ||
| text={showLoading ? text : translate('common.submitted')} | ||
| isLoading={showLoading} | ||
| icon={!showLoading ? icon : undefined} | ||
| isDisabled | ||
| shouldStayNormalOnDisable | ||
|
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. Honestly, I don't like this approach too much. Is it possible to 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.
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. what's wrong with this approach btw?, what do you suggest instead exactly ?
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. We leverage
We use
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. I agree with @hoangzinh. I think using |
||
| /> | ||
| </Animated.View> | ||
| )} | ||
| {!isAnimationRunning && ( | ||
| <Button | ||
| success={success} | ||
| text={text} | ||
| onPress={onPress} | ||
| icon={icon} | ||
| /> | ||
| )} | ||
| </Animated.View> | ||
| ); | ||
| } | ||
|
|
||
| AnimatedSubmitButton.displayName = 'AnimatedSubmitButton'; | ||
|
|
||
| export default AnimatedSubmitButton; | ||
Uh oh!
There was an error while loading. Please reload this page.