Skip to content

Commit

Permalink
feat: add simple animations (#30)
Browse files Browse the repository at this point in the history
* feat: add simple animations

* feat: create option list wrapper

* feat: update snapshot

* fix: apply requested changes, rename isAnimated prop to animated

* fix: change ReactElement type for ReactNode

* fix: change default animations prop in types

* feat: add animated example

* feat: add animation example with different animation durations

Co-authored-by: Agata Kosior <agata.kosior@tunego.com>
  • Loading branch information
koko57 and Agata Kosior authored Dec 13, 2021
1 parent 741cb32 commit 8519f08
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 81 deletions.
6 changes: 5 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ScrollToSelectedOption } from './examples/ScrollToSelectedOption';
import { Selects } from './examples/Selects';
import { Searchable } from './examples/Searchable';
import { SearchableInModal } from './examples/SearchableInModal';
import { Animated } from './examples/Animated';

export const DATA = [
{
Expand Down Expand Up @@ -55,7 +56,8 @@ const examples = [
'Custom Component',
'Scroll To Selected Option',
'Searchable',
'Searchable In Modal'
'Searchable In Modal',
'Animated',
];

export default function App() {
Expand Down Expand Up @@ -96,6 +98,8 @@ export default function App() {
return <Searchable />;
case 'Searchable In Modal':
return <SearchableInModal />;
case 'Animated':
return <Animated />;
default:
return null;
}
Expand Down
21 changes: 21 additions & 0 deletions example/src/examples/Animated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { SafeAreaView, Text } from 'react-native';
import { Select } from '@mobile-reality/react-native-select-pro';

import { DATA } from '../App';

export const Animated = () => {
return (
<SafeAreaView>
<Text style={{ marginBottom: 10 }}>With default animation duration (200 ms)</Text>
<Select animated options={DATA} selectControlStyle={{ width: 280, marginBottom: 20 }} />
<Text style={{ marginBottom: 10 }}>With custom animation duration (500 ms)</Text>
<Select
animated
animationDuration={500}
options={DATA}
selectControlStyle={{ width: 280 }}
/>
</SafeAreaView>
);
};
48 changes: 48 additions & 0 deletions src/components/options-list-wrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { ComponentProps, ReactNode, useEffect, useRef } from 'react';
import { Animated, StyleProp, View, ViewStyle } from 'react-native';

import type { OptionalToRequired } from '../../helpers';
import type { OptionsList } from '../options-list';

type FromOptionListProps = Pick<
ComponentProps<typeof OptionsList>,
'animated' | 'animationDuration' | 'isOpened'
>;

type WrapperStyles = {
wrapperStyles: StyleProp<ViewStyle>;
};

type OptionsListWrapperProps = OptionalToRequired<FromOptionListProps> & {
children: ReactNode;
} & WrapperStyles;

export const OptionsListWrapper = ({
children,
animated,
animationDuration,
isOpened,
wrapperStyles,
}: OptionsListWrapperProps) => {
const fadeAnimation = useRef(new Animated.Value(0)).current;

useEffect(() => {
if (animated) {
Animated.timing(fadeAnimation, {
toValue: isOpened ? 1 : 0,
duration: animationDuration,
useNativeDriver: true,
}).start();
}
}, [fadeAnimation, isOpened]);

return animated ? (
<Animated.View
pointerEvents={isOpened ? 'auto' : 'none'}
style={[wrapperStyles, { opacity: fadeAnimation }]}>
{children}
</Animated.View>
) : isOpened ? (
<View style={wrapperStyles}>{children}</View>
) : null;
};
142 changes: 73 additions & 69 deletions src/components/options-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Position, State } from '../../state/types';
import type { OnOutsidePress, OnPressOptionType } from '../../types';
import { NoOptions } from '../no-options';
import { Option } from '../option';
import { OptionsListWrapper } from '../options-list-wrapper';
import type { Select } from '../select';

type FromSelectComponentProps = Pick<
Expand All @@ -20,6 +21,8 @@ type FromSelectComponentProps = Pick<
| 'scrollToSelectedOption'
| 'noOptionsText'
| 'onSelect'
| 'animated'
| 'animationDuration'
| 'optionsListStyle'
| 'NoOptionsComponent'
| 'OptionComponent'
Expand Down Expand Up @@ -47,6 +50,8 @@ export const OptionsList = ({
flatListProps,
onPressOption,
selectedOption,
animated,
animationDuration,
searchedOptions,
searchValue,
searchable,
Expand Down Expand Up @@ -82,77 +87,76 @@ export const OptionsList = ({
return (
<>
{isOpened && (
<>
<Portal hostName={Portals.SelectOutsideWrapper}>
<TouchableWithoutFeedback
accessibilityLabel={'Close a dropdown from outside'}
onPress={onOutsidePress}>
<View style={styles.modalOverlay} />
</TouchableWithoutFeedback>
</Portal>
<Portal hostName={Portals.Select}>
<View
style={[
styles.options,
optionsListStyle,
{ top, left, width },
aboveSelectControl ? styles.overflown : styles.notOverflown,
]}>
<FlatList
accessibilityLabel={'Options list'}
bounces={false}
data={resolveData()}
getItemLayout={(_data, index) => {
const height = StyleSheet.flatten(optionStyle)?.height;
const isNumber = typeof height === 'number';
return {
length: isNumber ? height : ITEM_HEIGHT,
offset: isNumber ? height * index : ITEM_HEIGHT * index,
index,
};
}}
keyExtractor={({ value }) => value}
keyboardShouldPersistTaps="handled"
persistentScrollbar={true}
ref={ref}
renderItem={({ item, index }) => {
const { value } = item;
const isSelected = value === selectedOption?.value;
const isScrollToSelectedOption =
isSelected && ref.current && scrollToSelectedOption;
<Portal hostName={Portals.SelectOutsideWrapper}>
<TouchableWithoutFeedback
accessibilityLabel={'Close a dropdown from outside'}
onPress={onOutsidePress}>
<View style={styles.modalOverlay} />
</TouchableWithoutFeedback>
</Portal>
)}
<Portal hostName={Portals.Select}>
<OptionsListWrapper
animated={animated}
animationDuration={animationDuration}
isOpened={isOpened}
wrapperStyles={[
styles.options,
optionsListStyle,
{ top, left, width },
aboveSelectControl ? styles.overflown : styles.notOverflown,
]}>
<FlatList
accessibilityLabel={isOpened ? 'Options list' : ''}
bounces={false}
data={resolveData()}
getItemLayout={(_data, index) => {
const height = StyleSheet.flatten(optionStyle)?.height;
const isNumber = typeof height === 'number';
return {
length: isNumber ? height : ITEM_HEIGHT,
offset: isNumber ? height * index : ITEM_HEIGHT * index,
index,
};
}}
keyExtractor={({ value }) => value}
keyboardShouldPersistTaps="handled"
persistentScrollbar={true}
ref={ref}
renderItem={({ item, index }) => {
const { value } = item;
const isSelected = value === selectedOption?.value;
const isScrollToSelectedOption =
isSelected && ref.current && scrollToSelectedOption;

if (isScrollToSelectedOption) {
ref.current.scrollToIndex({
index,
animated: false,
});
}
if (isScrollToSelectedOption) {
ref.current.scrollToIndex({
index,
animated: false,
});
}

return (
<Option
OptionComponent={OptionComponent}
isSelected={isSelected}
key={value}
onPressOption={onPressOption}
onSelect={onSelect}
option={item}
optionSelectedStyle={optionSelectedStyle}
optionStyle={optionStyle}
optionTextStyle={optionTextStyle}
/>
);
}}
{...flatListProps}
ListEmptyComponent={
NoOptionsComponent || (
<NoOptions noOptionsText={noOptionsText} />
)
}
/>
</View>
</Portal>
</>
)}
return (
<Option
OptionComponent={OptionComponent}
isSelected={isSelected}
key={value}
onPressOption={onPressOption}
onSelect={onSelect}
option={item}
optionSelectedStyle={optionSelectedStyle}
optionStyle={optionStyle}
optionTextStyle={optionTextStyle}
/>
);
}}
{...flatListProps}
ListEmptyComponent={
NoOptionsComponent || <NoOptions noOptionsText={noOptionsText} />
}
/>
</OptionsListWrapper>
</Portal>
</>
);
};
Expand Down
52 changes: 42 additions & 10 deletions src/components/select-control/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { ComponentPropsWithRef, forwardRef } from 'react';
import React, { ComponentPropsWithRef, forwardRef, ReactElement, useEffect, useRef } from 'react';
import {
Animated,
Image,
ImageStyle,
Pressable,
Expand All @@ -22,6 +23,8 @@ type FromSelectComponentProps = Pick<
ComponentPropsWithRef<typeof Select>,
| 'selectControlStyle'
| 'clearable'
| 'animated'
| 'animationDuration'
| 'options'
| 'disabled'
| 'searchable'
Expand Down Expand Up @@ -49,10 +52,14 @@ type SelectControlProps = OptionalToRequired<
} & Pick<Position, 'aboveSelectControl'> & { setPosition: OnSetPosition }
>;

const arrowImage = require('./../../assets/icons/chevron-down.png');

export const SelectControl = forwardRef<View, SelectControlProps>(
(
{
isOpened,
animated,
animationDuration,
selectControlStyle,
selectedOption,
onPressSelectControl,
Expand Down Expand Up @@ -80,6 +87,23 @@ export const SelectControl = forwardRef<View, SelectControlProps>(
},
ref,
) => {
const rotateAnimation = useRef(new Animated.Value(0)).current;

useEffect(() => {
if (animated) {
Animated.timing(rotateAnimation, {
toValue: isOpened ? 1 : 0,
duration: animationDuration,
useNativeDriver: true,
}).start();
}
}, [rotateAnimation, isOpened, animated]);

const rotate = rotateAnimation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg'],
});

const onPressRemove = () => {
if (!disabled) {
dispatch({
Expand All @@ -102,6 +126,22 @@ export const SelectControl = forwardRef<View, SelectControlProps>(
}
};

const renderArrowImage = (): ReactElement =>
animated ? (
<Animated.Image
source={arrowImage}
style={[styles.arrowIcon, { transform: [{ rotate }] }]}
/>
) : (
<Image
source={arrowImage}
style={[
styles.arrowIcon,
isOpened ? styles.arrowIconOpened : styles.arrowIconClosed,
]}
/>
);

const renderSelection = () => {
if (searchable) {
return (
Expand Down Expand Up @@ -166,15 +206,7 @@ export const SelectControl = forwardRef<View, SelectControlProps>(
/>
</TouchableOpacity>
)}
{!hideSelectControlArrow && (
<Image
source={require('./../../assets/icons/chevron-down.png')}
style={[
styles.arrowIcon,
isOpened ? styles.arrowIconOpened : styles.arrowIconClosed,
]}
/>
)}
{!hideSelectControlArrow && renderArrowImage()}
</View>
</Pressable>
);
Expand Down
Loading

0 comments on commit 8519f08

Please sign in to comment.