-
Notifications
You must be signed in to change notification settings - Fork 0
feat: bottom sheet component #45
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
base: develop
Are you sure you want to change the base?
Changes from all commits
6b9ea07
6e2aa2a
7e55da2
dc7b8d7
0f6f660
bea67e8
6e6a61c
0d32cb3
85eb6a9
480c6f6
5e0b544
3713e84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import type {ComponentMeta, ComponentStory} from '@storybook/react' | ||
| import React, {useRef, useState} from 'react' | ||
| import {Text, TextInput} from 'react-native' | ||
| import {useSafeAreaInsets} from 'react-native-safe-area-context' | ||
| import {BottomSheet, Button} from 'rn-base-component' | ||
| import type {BottomSheetMethods} from 'src/components/BottomSheet/BottomSheet' | ||
|
|
||
| export default { | ||
| title: 'components/BottomSheet', | ||
| component: BottomSheet, | ||
| } as ComponentMeta<typeof BottomSheet> | ||
|
|
||
| export const Basic: ComponentStory<typeof BottomSheet> = rest => { | ||
| const ref = useRef<BottomSheetMethods>(null) | ||
| const insets = useSafeAreaInsets() | ||
|
|
||
| const [isVisible, setIsVisible] = useState(false) | ||
|
|
||
| return ( | ||
| <> | ||
| <Button text="Open Bottom Sheet" onPress={() => ref?.current?.open?.()} /> | ||
|
|
||
| <BottomSheet | ||
| {...rest} | ||
| ref={ref} | ||
| isVisible={isVisible} | ||
| onChangeValue={setIsVisible} | ||
| bottomInset={insets.bottom} | ||
| title="Header"> | ||
| <Text>Header</Text> | ||
| <TextInput placeholder="Search" /> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>footer</Text> | ||
| </BottomSheet> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| export const ContentHeight: ComponentStory<typeof BottomSheet> = rest => { | ||
| const ref = useRef<BottomSheetMethods>(null) | ||
| const insets = useSafeAreaInsets() | ||
|
|
||
| const [isVisible, setIsVisible] = useState(false) | ||
|
|
||
| return ( | ||
| <> | ||
| <Button text="Open Bottom Sheet" onPress={() => ref?.current?.open?.()} /> | ||
|
|
||
| <BottomSheet | ||
| {...rest} | ||
| ref={ref} | ||
| isVisible={isVisible} | ||
| onChangeValue={setIsVisible} | ||
| bottomInset={insets.bottom} | ||
| title="Header"> | ||
| <Text>Header</Text> | ||
| <TextInput placeholder="Search" /> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>Content</Text> | ||
| <Text>footer</Text> | ||
| </BottomSheet> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| Basic.args = {} | ||
|
|
||
| ContentHeight.args = { | ||
| contentHeight: 400, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import React, {forwardRef, memo, useCallback, useImperativeHandle, useMemo} from 'react' | ||
| import {runOnJS, useAnimatedKeyboard, useDerivedValue, useSharedValue} from 'react-native-reanimated' | ||
| import BottomSheetBackdrop from './components/BottomSheetBackdrop' | ||
| import BottomSheetContainer from './components/BottomSheetContainer' | ||
| import BottomSheetContent from './components/BottomSheetContent' | ||
| import BottomSheetContentContainer from './components/BottomSheetContentContainer' | ||
| import BottomSheetHeader from './components/BottomSheetHeader' | ||
| import {DEFAULT_CONTENT_HEIGHT, INITIAL_CONTENT_HEIGHT_POSITIVE} from './constants' | ||
| import {BottomSheetContextType, BottomSheetProvider} from './contexts' | ||
| import type {ViewStyle} from 'react-native' | ||
|
|
||
| export interface BottomSheetProps { | ||
| // configurations | ||
| isVisible: boolean | ||
| contentHeight?: number | ||
| title?: string | ||
| bottomInset: number | ||
| shouldPushContentWithKeyboardSize?: boolean | ||
| // callbacks | ||
| onChangeValue: (value: boolean) => void | ||
| onConfirm?: () => void | ||
| // styles | ||
| style?: ViewStyle | ||
| backdropStyle?: ViewStyle | ||
| contentContainerStyle?: ViewStyle | ||
| contentHeaderStyle?: ViewStyle | ||
| contentStyle?: ViewStyle | ||
| // components | ||
| children?: React.ReactNode | ||
| } | ||
|
|
||
| export type BottomSheetMethods = BottomSheetContextType | ||
|
|
||
| const BottomSheetComponent = forwardRef<BottomSheetMethods, BottomSheetProps>( | ||
| ( | ||
| { | ||
| // configurations | ||
| isVisible, | ||
| contentHeight = DEFAULT_CONTENT_HEIGHT, | ||
| title, | ||
| bottomInset, | ||
| shouldPushContentWithKeyboardSize, | ||
| // callbacks | ||
| onChangeValue: _onChangeValue, | ||
| onConfirm, | ||
| // styles | ||
| style, | ||
| backdropStyle, | ||
| contentContainerStyle, | ||
| contentHeaderStyle, | ||
| contentStyle, | ||
| // components | ||
| children, | ||
| }, | ||
| ref, | ||
| ) => { | ||
| // variables | ||
| const keyboard = useAnimatedKeyboard() | ||
| const animatedIsVisible = useSharedValue(isVisible) | ||
| const animatedCalculateContentHeight = useSharedValue(0) | ||
| const animatedContentHeight = useDerivedValue(() => { | ||
| if (contentHeight > 0) { | ||
| return contentHeight | ||
| } | ||
| return animatedCalculateContentHeight.value | ||
| }, []) | ||
| const animatedContentTranslateY = useDerivedValue(() => { | ||
| runOnJS(_onChangeValue)(animatedIsVisible.value) | ||
|
|
||
| if (animatedIsVisible.value) { | ||
| return shouldPushContentWithKeyboardSize ? -keyboard.height.value : 0 | ||
| } else { | ||
| return animatedContentHeight.value + bottomInset || INITIAL_CONTENT_HEIGHT_POSITIVE | ||
| } | ||
| }, []) | ||
|
|
||
| // callbacks | ||
| const handleOpen = useCallback(() => { | ||
| animatedIsVisible.value = true | ||
| }, [animatedIsVisible]) | ||
| const handleClose = useCallback(() => { | ||
| animatedIsVisible.value = false | ||
| }, [animatedIsVisible]) | ||
|
|
||
| // context | ||
| const contextValues: BottomSheetContextType = useMemo( | ||
| () => ({ | ||
| animatedIsVisible, | ||
| open: handleOpen, | ||
| close: handleClose, | ||
| }), | ||
| [animatedIsVisible, handleClose, handleOpen], | ||
| ) | ||
|
|
||
| useImperativeHandle(ref, () => contextValues) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why we need export
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because we can use this context inside and we can custom component outside
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't get what you mean. can you give some example? |
||
|
|
||
| return ( | ||
| <BottomSheetProvider value={contextValues}> | ||
| <BottomSheetContainer style={style}> | ||
| <BottomSheetBackdrop style={backdropStyle} /> | ||
| <BottomSheetContentContainer | ||
| style={contentContainerStyle} | ||
| {...{animatedCalculateContentHeight, animatedContentHeight, animatedContentTranslateY}}> | ||
| <BottomSheetHeader style={contentHeaderStyle} {...{title, onConfirm}} /> | ||
| <BottomSheetContent style={contentStyle}>{children}</BottomSheetContent> | ||
| </BottomSheetContentContainer> | ||
| </BottomSheetContainer> | ||
| </BottomSheetProvider> | ||
| ) | ||
| }, | ||
| ) | ||
|
|
||
| BottomSheetComponent.displayName = 'BottomSheetComponent' | ||
| const BottomSheet = memo(BottomSheetComponent) | ||
| BottomSheet.displayName = 'BottomSheet' | ||
|
|
||
| export default BottomSheet | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import React, {memo, useMemo} from 'react' | ||
| import {TapGestureHandler, TapGestureHandlerGestureEvent} from 'react-native-gesture-handler' | ||
| import Animated, { | ||
| runOnJS, | ||
| useAnimatedGestureHandler, | ||
| useAnimatedStyle, | ||
| withTiming, | ||
| } from 'react-native-reanimated' | ||
| import styled from 'styled-components' | ||
| import {useBottomSheet} from '../hooks' | ||
| import type {ITheme} from 'src/theme' | ||
| import {StyleSheet, type StyleProp, type ViewStyle} from 'react-native' | ||
|
|
||
| const Container = styled(Animated.View)<{theme: ITheme}>(({theme}) => ({ | ||
| ...StyleSheet.absoluteFillObject, | ||
| backgroundColor: theme?.colors?.black, | ||
| opacity: theme?.opacity?.partiallyOpaque, | ||
| })) | ||
|
|
||
| export interface BottomSheetBackdropProps { | ||
| style?: StyleProp<ViewStyle> | ||
| children?: React.ReactNode | ||
| } | ||
|
|
||
| const BottomSheetBackdropComponent: React.FC<BottomSheetBackdropProps> = ({style, children}) => { | ||
| const {animatedIsVisible, close} = useBottomSheet() | ||
|
|
||
| const animatedStyle = useAnimatedStyle( | ||
| () => ({ | ||
| opacity: withTiming(animatedIsVisible.value ? 0.5 : 0), | ||
| }), | ||
| [], | ||
| ) | ||
|
|
||
| const containerStyle = useMemo(() => StyleSheet.flatten([animatedStyle, style]), [animatedStyle, style]) | ||
|
|
||
| const gestureHandler = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>( | ||
| { | ||
| onFinish: () => { | ||
| runOnJS(close)() | ||
| }, | ||
| }, | ||
| [], | ||
| ) | ||
|
|
||
| return ( | ||
| <TapGestureHandler onGestureEvent={gestureHandler}> | ||
| <Container style={containerStyle}>{children}</Container> | ||
| </TapGestureHandler> | ||
| ) | ||
| } | ||
|
|
||
| const BottomSheetBackdrop = memo(BottomSheetBackdropComponent) | ||
| BottomSheetBackdrop.displayName = 'BottomSheetBackdrop' | ||
|
|
||
| export default BottomSheetBackdrop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we need this lib in dev dependencies?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because i'll get an error 'not found lib'. if i don't add 2 libs in dev