Skip to content
This repository was archived by the owner on Jun 24, 2022. It is now read-only.
Open
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
99 changes: 81 additions & 18 deletions packages/native/src/components/Carousel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export type Props = React.PropsWithChildren<{
* Called when the active carousel index is updated.
*/
onChange?: (index: number) => void;
/**
* Called when Carousel try to scroll before the first or after the last item (if restartAfterEnd is false).
*/
onOverflow?: (side: "start" | "end", fromAutoDelay: boolean) => void;
/**
* This number in milliseconds will enable automatic scrolling when defined.
*
Expand All @@ -27,6 +31,14 @@ export type Props = React.PropsWithChildren<{
* manually change the carousel item displayed.
*/
autoDelay?: number;
/**
* When the delay is elasped, if the Carousel is at the last item, it will scroll back to the beginning.
*/
restartAfterEnd?: boolean;
/**
* When the user tap on one side of an item, it will scroll to the next or precedent item. Same behavior as Instagram or Snapchat. Default: false.
*/
scrollOnSidePress?: boolean;
/**
* Additional props to pass to the outer container.
* This container is a Flex element.
Expand All @@ -42,15 +54,28 @@ export type Props = React.PropsWithChildren<{
* This container is a Flex element.
*/
slideIndicatorContainerProps?: FlexboxProps & ViewProps;

IndicatorComponent?:
| React.ComponentType<{
activeIndex: number;
slidesLength: number;
onChange?: (index: number) => void;
duration?: number;
}>
| React.ReactElement;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big fan of mixed types, it works but it requires a probably costly check on React.isValidElement where you could just have an extra prop IndicatorElement
and check what to show

{
 IndicatorElement ? (
          IndicatorElement
        ) : IndicatorComponent 
         ? (
          <IndicatorComponent
            ...
          />
        ) : null
 }

}>;

function Carousel({
activeIndex,
activeIndex = 0,
autoDelay,
restartAfterEnd = true,
scrollOnSidePress = false,
containerProps,
slideIndicatorContainerProps,
scrollViewProps,
onChange,
onOverflow,
IndicatorComponent = SlideIndicator,
children,
}: Props) {
const [init, setInit] = useState(false);
Expand Down Expand Up @@ -81,7 +106,9 @@ function Carousel({

useEffect(() => {
// On init scroll to the active index prop location - if specified.
if (init && activeIndex) scrollToIndex(activeIndex, false);
if (init && activeIndex) {
scrollToIndex(activeIndex, false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [init]);

Expand All @@ -97,6 +124,24 @@ function Carousel({
setInit(true);
};

const onTap = useCallback(
(event) => {
const tapPositionXPercent = event.nativeEvent.locationX / itemWidth;
if (tapPositionXPercent > 0.25) {
if (slidesLength > activeIndexState + 1) {
scrollToIndex(activeIndexState + 1, false);
} else {
onOverflow && onOverflow("end", false);
}
} else if (activeIndexState > 0) {
scrollToIndex(activeIndexState - 1, false);
} else {
onOverflow && onOverflow("start", false);
}
},
[slidesLength, activeIndexState, scrollToIndex, onOverflow, itemWidth],
);

const onScroll = ({
nativeEvent: { contentOffset, contentSize },
}: {
Expand All @@ -110,19 +155,31 @@ function Carousel({
useEffect(() => {
if (!autoDelay) return;

const interval = setInterval(() => {
const interval = setTimeout(() => {
if (!disableTimer.current) {
setActiveIndexState((index) => {
const newIndex = typeof index !== "undefined" ? (index + 1) % slidesLength : 0;
const newIndex =
typeof activeIndexState !== "undefined" ? (activeIndexState + 1) % slidesLength : 0;
if (restartAfterEnd || newIndex !== 0) {
scrollToIndex(newIndex);
onChange && onChange(newIndex);
return newIndex;
});
} else {
onOverflow && onOverflow("end", true);
}
}
}, autoDelay);

return () => clearInterval(interval);
}, [resetTimer, slidesLength, scrollToIndex, onChange, autoDelay]);
return () => {
return clearTimeout(interval);
};
}, [
resetTimer,
slidesLength,
scrollToIndex,
onChange,
autoDelay,
activeIndexState,
onOverflow,
restartAfterEnd,
]);

return (
<Flex flex={1} width="100%" alignItems="center" justifyContent="center" {...containerProps}>
Expand All @@ -141,6 +198,7 @@ function Carousel({
scrollEventThrottle={200}
contentContainerStyle={{ width: `${fullWidth}%` }}
decelerationRate="fast"
onTouchEnd={scrollOnSidePress ? onTap : undefined}
{...scrollViewProps}
>
{React.Children.map(children, (child, index) => (
Expand All @@ -150,14 +208,19 @@ function Carousel({
))}
</HorizontalScrollView>
<Flex my={8} {...slideIndicatorContainerProps}>
<SlideIndicator
activeIndex={activeIndexState || 0}
onChange={(index) => {
scrollToIndex(index);
setResetTimer({});
}}
slidesLength={slidesLength}
/>
{React.isValidElement(IndicatorComponent) ? (
IndicatorComponent
) : (
<IndicatorComponent
activeIndex={activeIndexState || 0}
onChange={(index: number) => {
scrollToIndex(index);
setResetTimer({});
}}
slidesLength={slidesLength}
duration={autoDelay}
/>
)}
</Flex>
</Flex>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Animated, {
type Props = {
slidesLength: number;
activeIndex: number;
onChange: (index: number) => void;
onChange?: (index: number) => void;
};

const Container = styled.View`
Expand Down Expand Up @@ -63,7 +63,7 @@ function SlideIndicator({ slidesLength, activeIndex = 0, onChange }: Props): Rea
return (
<Container>
{slidesArray.map((_, index) => (
<Bullet key={index} onPress={() => onChange(index)} />
<Bullet key={index} onPress={() => onChange && onChange(index)} />
))}
<AnimatedBullet style={[animatedStyles]} />
</Container>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useEffect, useMemo } from "react";
import styled from "styled-components/native";
import Animated, {
useAnimatedStyle,
withTiming,
useSharedValue,
Easing,
} from "react-native-reanimated";
import { FlexBoxProps } from "../../Layout/Flex";
import { Flex } from "../../Layout";

export interface StoryBarProps {
/**
* Is this step active.
*/
isActive?: boolean;
/**
* Has this step been completed.
*/
full?: boolean;
/**
* The duration of this step.
*/
duration?: number;
}

export interface StoriesIndicatorProps extends FlexBoxProps {
/**
* The index of the active step.
*/
activeIndex: number;
/**
* The total number of steps.
*/
slidesLength: number;
onChange?: (index: number) => void;
/**
* The duration of each step.
*/
duration?: number;
}

const ProgressBar = styled.View`
background-color: ${(p) => p.theme.colors.primary.c100};
height: 100%;
width: 100%;
border-radius: ${(p) => p.theme.radii[2]}px;
`;

const AnimatedProgressBar = Animated.createAnimatedComponent(ProgressBar);

function ActiveProgressBar({ duration }: StoryBarProps) {
const width = useSharedValue(0);

useEffect(() => {
width.value = 100;
}, [width]);

const animatedStyles = useAnimatedStyle(
() => ({
width: withTiming(`${width.value}%`, {
duration: duration || 200,
easing: Easing.linear,
}),
}),
[width, duration],
);

return <AnimatedProgressBar style={animatedStyles} />;
}

function StoryBar({ full = false, isActive, duration }: StoryBarProps) {
return (
<Flex height={4} backgroundColor="neutral.c50" margin={"auto"} borderRadius={2} flex={1} mx={1}>
{isActive ? <ActiveProgressBar duration={duration} /> : full ? <ProgressBar /> : null}
</Flex>
);
}

function StoriesIndicator({ activeIndex, slidesLength, duration }: StoriesIndicatorProps) {
const storiesArray = useMemo(() => new Array(slidesLength).fill(0), [slidesLength]);
return (
<Flex flexDirection={"row"} alignItems={"stretch"} width={"100%"}>
{storiesArray.map((_, storyIndex) => (
<StoryBar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably add a key so react doesn't warn you about this

key={storyIndex}
full={activeIndex > storyIndex}
isActive={activeIndex === storyIndex}
duration={duration}
/>
))}
</Flex>
);
}

export default React.memo(StoriesIndicator);
1 change: 1 addition & 0 deletions packages/native/src/components/Navigation/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as SlideIndicator } from "./SlideIndicator";
export { default as Stepper } from "./Stepper";
export { default as FlowStepper } from "./FlowStepper";
export { default as StoriesIndicator } from "./StoriesIndicator";
Loading