Skip to content

Commit

Permalink
feat: added footer component (#457)
Browse files Browse the repository at this point in the history
* chore: added BottomSheetFooter

* fix: added bottom inset to slide behavior

* chore: added footer example
  • Loading branch information
gorhom committed May 25, 2021
1 parent e6e6d6f commit 46fb883
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 4 deletions.
23 changes: 22 additions & 1 deletion example/src/Dev.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { StyleSheet, Text, View } from 'react-native';
import { DarkTheme, NavigationContainer } from '@react-navigation/native';
import {
createBottomTabNavigator,
useBottomTabBarHeight,
} from '@react-navigation/bottom-tabs';
import {
BottomSheetFlatList,
BottomSheetFooter,
BottomSheetModal,
BottomSheetModalProvider,
} from '@gorhom/bottom-sheet';
import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
import { RectButton } from 'react-native-gesture-handler';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { createContactListMockData } from './utilities';
import ContactItem from './components/contactItem';
Expand Down Expand Up @@ -97,6 +99,11 @@ const App = () => {
style={styles.flatlist}
contentContainerStyle={styles.flatlistContainer}
/>
<BottomSheetFooter appearanceBehavior={['fade', 'scale']}>
<RectButton style={styles.footer}>
<Text style={styles.footerText}>this is a footer!</Text>
</RectButton>
</BottomSheetFooter>
</BottomSheetModal>

{SNAP_POINTS.map(snapPoint => (
Expand Down Expand Up @@ -149,6 +156,20 @@ const styles = StyleSheet.create({
flatlistContainer: {
paddingHorizontal: 24,
},
footer: {
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 12,
padding: 12,
marginBottom: 12,
borderRadius: 24,
backgroundColor: '#80f',
},
footerText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
});

const Tab = createBottomTabNavigator();
Expand Down
106 changes: 106 additions & 0 deletions example/src/screens/advanced/FooterExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import BottomSheet, { BottomSheetFooter } from '@gorhom/bottom-sheet';
import SearchHandle from '../../components/searchHandle';
import Button from '../../components/button';
import ContactList from '../../components/contactList';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const FooterExample = () => {
// state
const [fadeBehavior, setFadeBehavior] = useState<'none' | 'fade'>('none');
const [slideBehavior, setSlideBehavior] = useState<'none' | 'slide'>('none');
const [scaleBehavior, setScaleBehavior] = useState<'none' | 'scale'>('none');

// hooks
const bottomSheetRef = useRef<BottomSheet>(null);
const { bottom: bottomSafeArea } = useSafeAreaInsets();

// variables
const snapPoints = useMemo(() => [80, 250], []);
const appearanceBehavior = useMemo(
() => [fadeBehavior, slideBehavior, scaleBehavior],
[fadeBehavior, slideBehavior, scaleBehavior]
);

// callbacks
const handleFadeBehavior = useCallback(() => {
setFadeBehavior(state => (state === 'none' ? 'fade' : 'none'));
}, []);
const handleScaleBehavior = useCallback(() => {
setScaleBehavior(state => (state === 'none' ? 'scale' : 'none'));
}, []);
const handleSlideBehavior = useCallback(() => {
setSlideBehavior(state => (state === 'none' ? 'slide' : 'none'));
}, []);
const handleExpandPress = useCallback(() => {
bottomSheetRef.current?.expand();
}, []);
const handleCollapsePress = useCallback(() => {
bottomSheetRef.current?.collapse();
}, []);
const handleClosePress = useCallback(() => {
bottomSheetRef.current?.close();
}, []);

// renders
return (
<View style={styles.container}>
<Button
label={`Toggle Fade Behavior: ${fadeBehavior}`}
onPress={handleFadeBehavior}
/>
<Button
label={`Toggle Scale Behavior: ${scaleBehavior}`}
onPress={handleScaleBehavior}
/>
<Button
label={`Toggle Slide Behavior: ${slideBehavior}`}
onPress={handleSlideBehavior}
/>
<Button label="Expand" onPress={handleExpandPress} />
<Button label="Collapse" onPress={handleCollapsePress} />
<Button label="Close" onPress={handleClosePress} />
<BottomSheet
ref={bottomSheetRef}
snapPoints={snapPoints}
keyboardBehavior="interactive"
keyboardBlurBehavior="restore"
handleComponent={SearchHandle}
>
<ContactList count={10} type="FlatList" />
<BottomSheetFooter
bottomInset={bottomSafeArea}
appearanceBehavior={appearanceBehavior}
>
<View style={styles.footer}>
<Text style={styles.footerText}>this is a footer!</Text>
</View>
</BottomSheetFooter>
</BottomSheet>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
},
footer: {
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 12,
padding: 12,
marginBottom: 12,
borderRadius: 24,
backgroundColor: '#80f',
},
footerText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
});

export default FooterExample;
5 changes: 5 additions & 0 deletions example/src/screens/screens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export const screens = [
slug: 'Advanced/ShadowExample',
getScreen: () => require('./advanced/ShadowExample').default,
},
{
name: 'Footer',
slug: 'Advanced/FooterExample',
getScreen: () => require('./advanced/FooterExample').default,
},
] as ShowcaseScreenType[],
},
{
Expand Down
15 changes: 15 additions & 0 deletions src/components/bottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
const animatedHandleHeight = useReactiveSharedValue(
_providedHandleHeight ?? INITIAL_HANDLE_HEIGHT
);
const animatedFooterHeight = useSharedValue(0);
const animatedSnapPoints = useNormalizedSnapPoints(
_providedSnapPoints,
animatedContainerHeight,
Expand Down Expand Up @@ -777,8 +778,14 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
animatedAnimationState,
animatedSheetState,
animatedScrollableState,
animatedKeyboardState: keyboardState,
animatedKeyboardHeight: keyboardHeight,
animatedIndex,
animatedPosition,
animatedContentHeight,
animatedHandleHeight,
animatedFooterHeight,
animatedContainerHeight,
scrollableContentOffsetY,
isInTemporaryPosition,
shouldHandleKeyboardEvents,
Expand All @@ -789,13 +796,21 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
failOffsetX: _providedFailOffsetX,
failOffsetY: _providedFailOffsetY,
contentPanGestureHandler,
getKeyboardHeightInContainer,
setScrollableRef: handleSettingScrollableRef,
removeScrollableRef,
}),
[
animatedIndex,
animatedPosition,
animatedContentHeight,
getKeyboardHeightInContainer,
animatedFooterHeight,
animatedContainerHeight,
animatedHandleHeight,
animatedAnimationState,
keyboardState,
keyboardHeight,
animatedSheetState,
contentPanGestureHandler,
handleSettingScrollableRef,
Expand Down
5 changes: 3 additions & 2 deletions src/components/bottomSheet/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ export interface BottomSheetProps
/**
* Points for the bottom sheet to snap to. It accepts array of number, string or mix.
* String values should be a percentage.
* @type Array<string | number>
* @example
* snapPoints={[200, 500]}
* snapPoints={[200, '%50']}
* snapPoints={[-1, '%100']}
* @type Array<string | number>
*/
snapPoints: Array<string | number>;
/**
Expand Down Expand Up @@ -124,11 +124,12 @@ export interface BottomSheetProps
//#region keyboard
/**
* Defines the keyboard appearance behavior.
* @enum
* - `none`: do nothing.
* - `extend`: extend the sheet to its maximum snap point.
* - `fillParent`: extend the sheet to fill parent.
* - `interactive`: offset the sheet by the size of the keyboard.
* @type `none` | `extend` | `interactive`
* @type `none` | `extend` | `fillParent` | `interactive`
* @default none
*/
keyboardBehavior?: keyof typeof KEYBOARD_BEHAVIOR;
Expand Down
130 changes: 130 additions & 0 deletions src/components/bottomSheetFooter/BottomSheetFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { memo, useCallback, useMemo } from 'react';
import { LayoutChangeEvent, StyleSheet } from 'react-native';
import Animated, {
Extrapolate,
interpolate,
useAnimatedStyle,
} from 'react-native-reanimated';
import { KEYBOARD_STATE } from '../../constants';
import { useBottomSheetInternal } from '../../hooks';
import { APPEARANCE_BEHAVIOR } from './constants';
import type { BottomSheetFooterProps } from './types';

function BottomSheetFooterComponent({
children,
appearanceBehavior = APPEARANCE_BEHAVIOR.fade,
bottomInset = 0,
}: BottomSheetFooterProps) {
//#region hooks
const {
animatedContainerHeight,
animatedHandleHeight,
animatedFooterHeight,
animatedPosition,
animatedKeyboardState,
getKeyboardHeightInContainer,
} = useBottomSheetInternal();
//#endregion

//#region styles
const containerAnimatedStyle = useAnimatedStyle(() => {
const keyboardHeight = getKeyboardHeightInContainer();
let footerTranslateY = Math.max(
0,
animatedContainerHeight.value - animatedPosition.value
);

if (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) {
footerTranslateY = footerTranslateY - keyboardHeight;
} else {
footerTranslateY = footerTranslateY - bottomInset;
}

footerTranslateY =
footerTranslateY -
animatedFooterHeight.value -
animatedHandleHeight.value;

const style: any = {
transform: [
{
translateY: footerTranslateY,
},
],
};

// merge appearance behavior styles
(typeof appearanceBehavior === 'string'
? [appearanceBehavior]
: appearanceBehavior
).map(behavior => {
if (behavior === APPEARANCE_BEHAVIOR.fade) {
style.opacity = interpolate(
footerTranslateY,
[5, 0],
[1, 0],
Extrapolate.CLAMP
);
} else if (behavior === APPEARANCE_BEHAVIOR.scale) {
style.transform.push({
scale: interpolate(
footerTranslateY,
[5, 0],
[1, 0],
Extrapolate.CLAMP
),
});
} else if (behavior === APPEARANCE_BEHAVIOR.slide) {
style.transform.push({
translateY: interpolate(
footerTranslateY,
[5, 0],
[0, animatedFooterHeight.value + bottomInset],
Extrapolate.CLAMP
),
});
}
});

return style;
}, [appearanceBehavior, bottomInset]);
const containerStyle = useMemo(
() => [styles.container, containerAnimatedStyle],
[containerAnimatedStyle]
);
//#endregion

//#region callbacks
const handleContainerLayout = useCallback(
({
nativeEvent: {
layout: { height },
},
}: LayoutChangeEvent) => {
animatedFooterHeight.value = height;
},
[animatedFooterHeight]
);
//#endregion

return children !== null ? (
<Animated.View onLayout={handleContainerLayout} style={containerStyle}>
{typeof children === 'function' ? children() : children}
</Animated.View>
) : null;
}

const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 9999,
},
});

const BottomSheetFooter = memo(BottomSheetFooterComponent);
BottomSheetFooter.displayName = 'BottomSheetFooter';

export default BottomSheetFooter;
8 changes: 8 additions & 0 deletions src/components/bottomSheetFooter/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
enum APPEARANCE_BEHAVIOR {
none = 'none',
fade = 'fade',
scale = 'scale',
slide = 'slide',
}

export { APPEARANCE_BEHAVIOR };
1 change: 1 addition & 0 deletions src/components/bottomSheetFooter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './BottomSheetFooter';

0 comments on commit 46fb883

Please sign in to comment.