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

Feature/progress indicator #43

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions src/components/ProgressIndicator/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Bar, Props as BarProps } from './variants/Bar';
export { defaultStyles as progressIndicatorStyles } from './utils';
8 changes: 8 additions & 0 deletions src/components/ProgressIndicator/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const defaultStyles = {
variants: {
bar: {
color: 'colors.primary',
trackColor: 'colors.surfaceVariant',
},
},
};
159 changes: 159 additions & 0 deletions src/components/ProgressIndicator/variants/Bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {
Animated,
Easing,
I18nManager,
LayoutChangeEvent,
StyleProp,
ViewStyle,
} from 'react-native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useMolecules, useComponentStyles } from '../../../hooks';

const INDETERMINATE_WIDTH_FACTOR = 0.3;
const BAR_WIDTH_ZERO_POSITION = INDETERMINATE_WIDTH_FACTOR / (1 + INDETERMINATE_WIDTH_FACTOR);

export type Props = {
animated?: boolean;
children?: React.ReactNode;
color?: string;
indeterminate?: boolean;
indeterminateAnimationDuration?: number;
progress: number;
style?: StyleProp<ViewStyle>;
useNativeDriver?: boolean;
animationConfig?: object;
animationType?: 'decay' | 'timing' | 'spring';
};

const Bar = (props: Props) => {
const {
animated = true,
indeterminate = false,
indeterminateAnimationDuration = 1000,
progress = 0,
useNativeDriver = false,
animationConfig = { bounciness: 0 },
animationType = 'spring',
style: styleProp,
...rest
} = props;

const { View } = useMolecules();

const componentStyle = useComponentStyles(
'ProgressIndicator',
{ style: styleProp, color: rest.color },
{
variant: 'bar',
},
);

const [widthBar, setWidthBar] = useState(0);

const progressCalc = useRef(new Animated.Value(0)).current;

const animationValue = useRef(new Animated.Value(BAR_WIDTH_ZERO_POSITION)).current;

const { containerStyle, progressStyle } = useMemo(() => {
const innerWidth = Math.max(0, widthBar);
const { color, trackColor, ...restStyle } = componentStyle;

return {
containerStyle: {
width: '100%',
height: 6,
overflow: 'hidden' as 'hidden',
backgroundColor: trackColor,
...restStyle.style,
},

progressStyle: {
backgroundColor: restStyle.color ? restStyle.color : color,
height: '100%',
transform: [
{
translateX: animationValue.interpolate({
inputRange: [0, 1],
outputRange: [innerWidth * -INDETERMINATE_WIDTH_FACTOR, innerWidth],
}),
},
{
translateX: progressCalc.interpolate({
inputRange: [0, 1],
outputRange: [innerWidth / (I18nManager.isRTL ? 2 : -2), 0],
}),
},
{
scaleX: progressCalc.interpolate({
inputRange: [0, 1],
outputRange: [0.0001, 1],
}),
},
],
},
};
}, [widthBar, rest.color]);

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

const animate = useCallback(() => {
animationValue.setValue(0);

Animated.timing(animationValue, {
toValue: 1,
duration: indeterminateAnimationDuration,
easing: Easing.linear,
isInteraction: false,
useNativeDriver: useNativeDriver,
}).start(endState => {
if (endState.finished) {
animate();
}
});
}, [indeterminateAnimationDuration, useNativeDriver]);

useEffect(() => {
if (indeterminate) {
animate();
}
}, []);

useEffect(() => {
if (indeterminate) {
animate();
} else {
Animated.spring(animationValue, {
toValue: BAR_WIDTH_ZERO_POSITION,
useNativeDriver: useNativeDriver,
}).start();
}
}, [indeterminate]);

useEffect(() => {
const newProgress = indeterminate
? INDETERMINATE_WIDTH_FACTOR
: Math.min(Math.max(progress / 100, 0), 1);

if (animated) {
Animated[animationType](progressCalc, {
toValue: newProgress,
useNativeDriver: useNativeDriver,
velocity: 0,
...animationConfig,
}).start();
} else {
progressCalc.setValue(newProgress);
}
}, [indeterminate, progress]);

return (
<View style={containerStyle} {...rest} onLayout={handleLayout}>
<Animated.View style={progressStyle} />
{rest.children}
</View>
);
};

export default Bar;
2 changes: 2 additions & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ export * from './Dialog';
export * from './RadioButton';

export * from './NumberRangeInput';

export { Bar as ProgressBar, BarProps as ProgressBarProps ,progressIndicatorStyles} from './ProgressIndicator';
2 changes: 2 additions & 0 deletions src/core/components/ComponentsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
Dialog,
RadioButton,
NumberRangeInput,
ProgressBar
} from '../../components';
import type { DefaultComponents, ProvideComponentsProps } from './types';

Expand Down Expand Up @@ -98,6 +99,7 @@ const defaultComponents: DefaultComponents = {
Dialog,
RadioButton,
NumberRangeInput,
ProgressBar
};

export const ProvideComponents = ({ components = {}, children }: ProvideComponentsProps) => {
Expand Down
2 changes: 2 additions & 0 deletions src/core/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
Dialog,
RadioButton,
NumberRangeInputProps,
ProgressBarProps
} from '../../components';

export type ProvideComponentsProps = {
Expand Down Expand Up @@ -87,6 +88,7 @@ export interface DefaultComponents {
Dialog: typeof Dialog;
RadioButton: typeof RadioButton;
NumberRangeInput: ComponentType<NumberRangeInputProps>;
ProgressBar: ComponentType<ProgressBarProps>;
}

export type IComponentsProviderContext = IAtomsComponentsProviderContext & DefaultComponents & {};
3 changes: 3 additions & 0 deletions src/core/theme/ProvideTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
dialogScrollAreaStyles,
radioButtonStyles,
numberRangeInputStyles,
progressIndicatorStyles
} from '../../components';
import { MD3LightTheme, MD3DarkTheme } from '../../styles';
import type { DeepPartial } from '../../types';
Expand Down Expand Up @@ -147,6 +148,8 @@ const defaultThemeValue: Partial<ITheme> = {

RadioButton: radioButtonStyles,
NumberRangeInput: numberRangeInputStyles,

ProgressIndicator:progressIndicatorStyles
};

const defaultExtractStyles = memoize(
Expand Down
31 changes: 29 additions & 2 deletions test-cases/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
import Cases from './Cases';
import React, { useEffect, useState } from 'react';
import { ProvideMolecules, useMolecules } from 'bamboo-molecules';

export default () => <Cases />;
const App = () => {
return (
<ProvideMolecules>
<CheckComponent />
</ProvideMolecules>
);
};

export default App;

const CheckComponent = () => {
const { ProgressBar, View } = useMolecules();

const [progress, setProgress] = useState(0);

useEffect(() => {
setInterval(() => {
setProgress(Math.random() * 100);
}, 1000);
}, []);

return (
<View style={{ width: 500 }}>
<ProgressBar color="colors.primary" progress={progress} />
</View>
);
};