Skip to content
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
67 changes: 42 additions & 25 deletions packages/mobile/src/stepper/DefaultStepperHeaderHorizontal.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { memo, useEffect, useMemo } from 'react';
import { animated, useSpring } from '@react-spring/native';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { durations } from '@coinbase/cds-common/motion/tokens';

import { HStack } from '../layout/HStack';
import { mobileCurves } from '../motion/convertMotionConfig';
import { Text } from '../typography/Text';

import type { StepperHeaderComponent } from './Stepper';

const AnimatedHStack = animated(HStack);
const AnimatedHStack = Animated.createAnimatedComponent(HStack);

export const DefaultStepperHeaderHorizontal: StepperHeaderComponent = memo(
function DefaultStepperHeaderHorizontal({
activeStep,
complete,
disableAnimateOnMount,
flatStepIds,
style,
paddingBottom = 1.5,
Expand All @@ -20,27 +23,41 @@ export const DefaultStepperHeaderHorizontal: StepperHeaderComponent = memo(
fontFamily = font,
...props
}) {
const [spring, springApi] = useSpring(
{
from: { opacity: 0 },
to: { opacity: 1 },
reset: true,
},
[],
);
const opacity = useSharedValue(disableAnimateOnMount ? 1 : 0);
const disableAnimateOnMountRef = useRef(disableAnimateOnMount);
const isInitialRender = useRef(true);

const [displayedStep, setDisplayedStep] = useState(activeStep);
const [displayedComplete, setDisplayedComplete] = useState(complete);

// TO DO: resetting the spring doesn't work like it does in react-spring on web
// need to look into this deeper and understand why there is a difference in behavior
useEffect(() => {
springApi.start({
from: { opacity: 0 },
to: { opacity: 1 },
reset: true,
});
}, [springApi, activeStep]);
if (isInitialRender.current) {
isInitialRender.current = false;
setDisplayedStep(activeStep);
setDisplayedComplete(complete);
if (disableAnimateOnMountRef.current) return;
opacity.value = withTiming(1, { duration: durations.fast1, easing: mobileCurves.linear });
return;
}

// Fade out with old text, then swap text and fade in
opacity.value = withTiming(0, { duration: durations.fast1, easing: mobileCurves.linear });

const timeout = setTimeout(() => {
setDisplayedStep(activeStep);
setDisplayedComplete(complete);
opacity.value = withTiming(1, { duration: durations.fast1, easing: mobileCurves.linear });
}, durations.fast1 + durations.fast1);

return () => clearTimeout(timeout);
}, [activeStep, complete, opacity]);

const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));

const styles = useMemo(() => [style, spring] as any, [style, spring]);
const flatStepIndex = activeStep ? flatStepIds.indexOf(activeStep.id) : -1;
const styles = useMemo(() => [style, animatedStyle], [style, animatedStyle]);
const flatStepIndex = displayedStep ? flatStepIds.indexOf(displayedStep.id) : -1;
const emptyText = ' '; // Simple space for React Native

return (
Expand All @@ -52,7 +69,7 @@ export const DefaultStepperHeaderHorizontal: StepperHeaderComponent = memo(
{...props}
>
<Text alignItems="center" display="flex" font="caption" fontFamily={fontFamily}>
{!activeStep || complete ? (
{!displayedStep || displayedComplete ? (
emptyText
) : (
<HStack gap={1}>
Expand All @@ -65,12 +82,12 @@ export const DefaultStepperHeaderHorizontal: StepperHeaderComponent = memo(
>
{flatStepIndex + 1}/{flatStepIds.length}
</Text>
{activeStep.label && typeof activeStep.label === 'string' ? (
{displayedStep.label && typeof displayedStep.label === 'string' ? (
<Text font={font} fontFamily={fontFamily} numberOfLines={1}>
{activeStep.label}
{displayedStep.label}
</Text>
) : (
activeStep.label
displayedStep.label
)}
</HStack>
)}
Expand Down
30 changes: 25 additions & 5 deletions packages/mobile/src/stepper/DefaultStepperProgressHorizontal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { memo } from 'react';
import { animated, to } from '@react-spring/native';
import { memo, useCallback, useEffect } from 'react';
import type { LayoutChangeEvent } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';

import { Box } from '../layout/Box';

import type { StepperProgressComponent } from './Stepper';

const AnimatedBox = animated(Box);
const AnimatedBox = Animated.createAnimatedComponent(Box);

export const DefaultStepperProgressHorizontal: StepperProgressComponent = memo(
function DefaultStepperProgressHorizontal({
Expand All @@ -19,7 +20,7 @@ export const DefaultStepperProgressHorizontal: StepperProgressComponent = memo(
progress,
complete,
isDescendentActive,
progressSpringConfig,
progressTimingConfig,
animate,
disableAnimateOnMount,
style,
Expand All @@ -33,13 +34,32 @@ export const DefaultStepperProgressHorizontal: StepperProgressComponent = memo(
height = 4,
...props
}) {
const containerWidth = useSharedValue(0);
const animatedProgress = useSharedValue(progress);

const handleLayout = useCallback(
(event: LayoutChangeEvent) => {
containerWidth.value = event.nativeEvent.layout.width;
},
[containerWidth],
);

useEffect(() => {
animatedProgress.value = withTiming(progress, progressTimingConfig);
}, [progress, progressTimingConfig, animatedProgress]);

const animatedStyle = useAnimatedStyle(() => ({
width: animatedProgress.value * containerWidth.value,
}));

return (
<Box
accessibilityElementsHidden
background={background}
borderRadius={borderRadius}
flexGrow={1}
height={height}
onLayout={handleLayout}
style={style}
{...props}
>
Expand All @@ -57,7 +77,7 @@ export const DefaultStepperProgressHorizontal: StepperProgressComponent = memo(
}
borderRadius={borderRadius}
height="100%"
width={to([progress], (width) => `${width * 100}%`)}
style={animatedStyle}
/>
</Box>
);
Expand Down
52 changes: 30 additions & 22 deletions packages/mobile/src/stepper/DefaultStepperProgressVertical.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { memo, useCallback, useMemo } from 'react';
import { useHasMounted } from '@coinbase/cds-common/hooks/useHasMounted';
import { memo, useCallback, useEffect, useMemo } from 'react';
import type { LayoutChangeEvent } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { flattenSteps } from '@coinbase/cds-common/stepper/utils';
import { animated, to, useSpring } from '@react-spring/native';

import { Box } from '../layout/Box';

import type { StepperProgressComponent, StepperValue } from './Stepper';

const AnimatedBox = animated(Box);
const AnimatedBox = Animated.createAnimatedComponent(Box);

export const DefaultStepperProgressVertical: StepperProgressComponent = memo(
function DefaultStepperProgressVertical({
Expand All @@ -23,9 +23,7 @@ export const DefaultStepperProgressVertical: StepperProgressComponent = memo(
isDescendentActive,
style,
activeStepLabelElement,
progressSpringConfig,
animate = true,
disableAnimateOnMount,
progressTimingConfig,
background = 'bgLine',
defaultFill = 'bgLinePrimarySubtle',
activeFill = 'bgLinePrimarySubtle',
Expand All @@ -36,7 +34,6 @@ export const DefaultStepperProgressVertical: StepperProgressComponent = memo(
width = 2,
...props
}) {
const hasMounted = useHasMounted();
const isLastStep = flatStepIds[flatStepIds.length - 1] === step.id;

// Count the total number of sub-steps in the current step's tree
Expand All @@ -56,35 +53,45 @@ export const DefaultStepperProgressVertical: StepperProgressComponent = memo(
[],
);

// Fractional fill for steps with sub-steps. For all other cases, return 1
// and let the cascade's `progress` prop control whether the bar is filled.
const progressHeight = useMemo(() => {
const totalSubSteps = countAllSubSteps(step.subSteps ?? []);

if (complete) return 1;
if (active && totalSubSteps === 0) return 1;
if (active && !isDescendentActive) return 0;
if (isDescendentActive) {
if (active && totalSubSteps > 0 && !isDescendentActive) return 0;
if (isDescendentActive && totalSubSteps > 0) {
const activePosition = findSubStepPosition(step.subSteps ?? [], activeStepId);
return activePosition / totalSubSteps;
}
if (visited) return 1;

return 0;
return 1;
}, [
countAllSubSteps,
step.subSteps,
complete,
active,
isDescendentActive,
visited,
findSubStepPosition,
activeStepId,
]);

const fillHeightSpring = useSpring({
height: progressHeight,
immediate: !animate || (disableAnimateOnMount && !hasMounted),
config: progressSpringConfig,
});
const containerHeight = useSharedValue(0);
const targetHeight = progress * progressHeight;
const animatedHeight = useSharedValue(targetHeight);

const handleLayout = useCallback(
(event: LayoutChangeEvent) => {
containerHeight.value = event.nativeEvent.layout.height;
},
[containerHeight],
);

useEffect(() => {
animatedHeight.value = withTiming(targetHeight, progressTimingConfig);
}, [targetHeight, progressTimingConfig, animatedHeight]);

const animatedStyle = useAnimatedStyle(() => ({
height: animatedHeight.value * containerHeight.value,
}));

if (depth > 0 || isLastStep) return null;

Expand All @@ -93,6 +100,7 @@ export const DefaultStepperProgressVertical: StepperProgressComponent = memo(
background={background}
flexGrow={1}
minHeight={minHeight}
onLayout={handleLayout}
position="relative"
style={style}
width={width}
Expand All @@ -110,8 +118,8 @@ export const DefaultStepperProgressVertical: StepperProgressComponent = memo(
? visitedFill
: defaultFill
}
height={to([progress, fillHeightSpring.height], (p, h) => `${p * h * 100}%`)}
position="absolute"
style={animatedStyle}
width="100%"
/>
</Box>
Expand Down
6 changes: 3 additions & 3 deletions packages/mobile/src/stepper/DefaultStepperStepHorizontal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const DefaultStepperStepHorizontal: StepperStepComponent = memo(
styles,
activeStepLabelElement,
setActiveStepLabelElement,
progressSpringConfig,
progressTimingConfig,
animate,
disableAnimateOnMount,
StepperStepComponent = DefaultStepperStepHorizontal,
Expand Down Expand Up @@ -75,7 +75,7 @@ export const DefaultStepperStepHorizontal: StepperStepComponent = memo(
isDescendentActive={isDescendentActive}
parentStep={parentStep}
progress={progress}
progressSpringConfig={progressSpringConfig}
progressTimingConfig={progressTimingConfig}
step={step}
style={styles?.progress}
visited={visited}
Expand Down Expand Up @@ -140,7 +140,7 @@ export const DefaultStepperStepHorizontal: StepperStepComponent = memo(
isDescendentActive={isDescendentActive}
parentStep={step}
progress={progress}
progressSpringConfig={progressSpringConfig}
progressTimingConfig={progressTimingConfig}
setActiveStepLabelElement={setActiveStepLabelElement}
step={subStep}
styles={styles}
Expand Down
6 changes: 3 additions & 3 deletions packages/mobile/src/stepper/DefaultStepperStepVertical.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const DefaultStepperStepVertical: StepperStepComponent = memo(
styles,
activeStepLabelElement,
setActiveStepLabelElement,
progressSpringConfig,
progressTimingConfig,
animate,
disableAnimateOnMount,
StepperStepComponent = DefaultStepperStepVertical,
Expand Down Expand Up @@ -80,7 +80,7 @@ export const DefaultStepperStepVertical: StepperStepComponent = memo(
isDescendentActive={isDescendentActive}
parentStep={parentStep}
progress={progress}
progressSpringConfig={progressSpringConfig}
progressTimingConfig={progressTimingConfig}
step={step}
style={styles?.progress}
visited={visited}
Expand Down Expand Up @@ -146,7 +146,7 @@ export const DefaultStepperStepVertical: StepperStepComponent = memo(
isDescendentActive={isDescendentActive}
parentStep={step}
progress={progress}
progressSpringConfig={progressSpringConfig}
progressTimingConfig={progressTimingConfig}
setActiveStepLabelElement={setActiveStepLabelElement}
step={subStep}
styles={styles}
Expand Down
Loading