@devvie/bottom-sheet
Version:
The 😎smart , 📦tiny , and 🎗flexible bottom sheet your app craves 🚀
472 lines (438 loc) • 14.2 kB
TypeScript
import React from 'react';
import { Animated, OpaqueColorValue, ViewProps, ViewStyle } from 'react-native';
import {
ANIMATIONS,
CUSTOM_BACKDROP_POSITIONS,
type BottomSheetMethods,
} from '../../types.d';
export type SheetStyleProp = Omit<
ViewStyle,
'height' | 'minHeight' | 'maxHeight'
>;
// short hand for toValue key of Animator methods
type ToValue = Animated.TimingAnimationConfig['toValue'];
// this is to accomodate static `ANIMATIONS` property of BottomSheet function below
type BOTTOMSHEET = React.ForwardRefExoticComponent<
BottomSheetProps & React.RefAttributes<BottomSheetMethods>
> & { ANIMATIONS: typeof ANIMATIONS };
type AnimationType = ANIMATIONS | Lowercase<keyof typeof ANIMATIONS>;
type AnimationEasingFunction = (x: number) => number;
/**
* Base props types for bottom sheet component
*/
interface BottomSheetBaseProps {
/**
* The snap point index the sheet opens to (when `snapPoints` is provided). Indexes are
* 0-based in ascending order, so `0` is the smallest point. The value is clamped to the
* valid range.
*
* `Note:` This is the initial open position only — it is not a controlled/reactive prop.
* Use the ref methods (`snapToIndex`, `expand`, `collapse`) to move the sheet afterwards.
* Ignored when `snapPoints` is not provided.
*
* `Default: 0`
* @type {number}
* @default 0
*/
index?: number;
/**
* Callback fired whenever the sheet settles on a snap point — both from a drag gesture
* and from the programmatic methods (`open`, `snapToIndex`, `expand`, `collapse`). The
* argument is the resting snap index, or `-1` when the sheet closes.
*
* Useful for syncing external/controlled UI to the sheet's current position.
*
* `Default: undefined`
* @type {(index: number) => void}
* @default undefined
* @example
* ```tsx
* <BottomSheet
* snapPoints={['30%', '60%', '90%']}
* onSnap={(index) => console.log('snapped to', index)} // 0..2, or -1 on close
* />
* ```
*/
onSnap?: (index: number) => void;
/**
* Extra styles to apply to bottom sheet (the `View` that wraps its children).
*
* `Note:` style properties `height`, `maxHeight`, `minHeight` will be ignored.
* If you want to set sheet's height, pass the `height` prop instead.
* @type {SheetStyleProp}
*/
style?: SheetStyleProp;
/**
* Height of the bottom sheet's overall container, this will be the height of
* the entire bottom sheet including the backdrop mask. height passed through the `height` prop
* will be relative to this.
*
* `Note:` By default this will be the height of the device's screen.
*
* `Default: DEVICE'S SCREEN HEIGHT`
* @type {number | string}
* @default {DEVICE SCREEN HEIGHT}
*/
containerHeight?: ViewStyle['height'];
/**
* Animation to use when opening and closing the bottom sheet.
* Use exported `ANIMATIONS` enum to pass value to this prop
*
* `Default: 'slide'`
* @type {AnimationType | ANIMATIONS}
* @default "slide" | ANIMATIONS.SLIDE
* @example
* ```tsx
* import BottomSheet, {ANIMATIONS} from '@devvie/bottom-sheet';
* ...
* <BottomSheet animationType={ANIMATIONS.SLIDE}>
* ...
* </BottomSheet>
* ```
*/
animationType?: AnimationType;
/**
* Color of the scrim or backdrop mask when `modal` is true.
*
* `Default: '#00000052'` (i.e black with 32% opacity)
* @type {string | OpaqueColorValue}
* @default '#00000052'
*/
backdropMaskColor?: string | OpaqueColorValue;
/**
* Determines whether the bottom sheet will close when the scrim or backdrop mask is pressed.
*
* `Default: true`
* @type {boolean}
* @default true
*/
closeOnBackdropPress?: boolean;
/**
* Determines whether bottom sheet will close when its dragged down
* below 1/3 (one quater) of its height.
*
* `Default:true`
* @type boolean
* @default true
*/
closeOnDragDown?: boolean;
/**
* When true, hides the sheet's drag handle. The drag handle is visible by default.
*
* `Note:` When true, custom drag handle component will also be hidden.
*
* `Default: false`
*
* @type {boolean}
* @default false
*/
hideDragHandle?: boolean;
/**
* Custom drag handle component to replace the default bottom sheet's drag handle.
*
* This component will be passed the animated `height` and `translateY` values of the bottom sheet,
* which can be used to interpolate or extended animations to its children.
*
* `Note:` Styles passed through `dragHandleStyle` prop won't be applied to it.
*
* @type {React.FC<{animatedHeight: Animated.Value, animatedYTranslation:Animated.Value}>}
* @example
* ```tsx
* // Below example will animate the custom drag handle's width as the
* // bottom sheet is being dragged/panned down
* <BottomSheet
customDragHandleComponent={(props) => (
<Animated.View
style={{
height: 5,
backgroundColor: 'orange',
width: props._animatedYTranslation.interpolate({
inputRange: [0, 25, 200],
outputRange: [20, 50, 100],
}),
}}
/>
)}>
...
</BottomSheet>
* ```
*/
customDragHandleComponent?: React.FC<{
/**
* Animated height of the bottom sheet when expanding
* @type {Animated.Value}
*/
_animatedHeight: Animated.Value;
}>;
/**
* Extra styles to apply to the drag handle.
*
* `Note:` These styles will be ignored when `customDragHandleComponent` is provided
* @type {ViewStyle}
*/
dragHandleStyle?: ViewStyle;
/**
* When true, prevents the bottom sheet from being panned down by dragging its drag handle.
* This prop also applies to custom drag handle component provided via `customDragHandleComponent` prop.
*
* The bottom sheet is draggable by it's drag handle by default
*
* `Default:false`;
* @type {boolean}
* @default false
*/
disableDragHandlePanning?: boolean;
/**
* When true, prevents the bottom sheet from being dragged/panned down on its body.
* The bottom sheet body is draggable by default.
*
* `Default:false`;
* @type boolean
* @default false
*/
disableBodyPanning?: boolean;
/**
* When `closeOnBackdropPress` is `true`, color of the ripple effect that occurs when scrim or backdrop mask is pressed.
*
* `Note:` Only works for Android
*
* `Default: none` (i.e no ripple effect);
* @platform Android
* @type (string | OpaqueColorValue)
* @default undefined
*/
android_backdropMaskRippleColor?: string | OpaqueColorValue;
/**
* Custom component for sheet's scrim or backdrop mask.\
* `Note:` The component will be passed animated height value
*
* @type {React.Component}
* @default null
*/
customBackdropComponent?: React.FunctionComponent<{
_animatedHeight: Animated.Value;
}>;
/**
* When `customBackdropComponent` is provided, determines its position within the sheet.
*
* **'top'** - positions the custom backdrop component directly above the sheet\
* **'behind'** - positions the custom backdrop component behind the sheet.
* This is the default behaviour
*
* `Note:` This prop only applies to custom backdrop component
*
* `Default: 'behind'`
* @type {"top" | "behind" | CUSTOM_BACKDROP_POSITIONS}
* @default "behind" / CUSTOM_BACKDROP_POSITIONS.BEHIND
*/
customBackdropPosition?:
| CUSTOM_BACKDROP_POSITIONS
| Lowercase<keyof typeof CUSTOM_BACKDROP_POSITIONS>;
/**
* Determines whether sheet is a modal.
*
* A modal sheet has a scrim or backdrop mask, while a standard (non-modal) sheet doesn't.
*
* `Note:` When false, this will also hide custom scrim/backdrop component supplied via `customBackdropComponent` prop.
*
* `Default: true`
* @type boolean
* @default true
*/
modal?: boolean;
/**
* Contents of the bottom sheet. Can be element(s) or a component.
*
* If this is a component, it will be passed animated
* height of bottom sheet via `_animatedHeight` prop
*
* `Default: null`
* @type {ViewProps['children'] | React.FunctionComponent<{_animatedHeight: Animated.Value}>}
* @default null
*/
children:
| ViewProps['children']
| React.FunctionComponent<{ _animatedHeight: Animated.Value }>;
/**
* Duration for sheet opening animation.
*
* `Default: 500`
* @type number
* @default 500
*/
openDuration?: number;
/**
* Duration for sheet closing animation.
*
* `Default: 500`
* @type number
* @default 500
*/
closeDuration?: number;
/**
* Custom easing function for driving sheet's animation.\
* If provided, easing function for passed `animationType` will be replaced with this.
*
* `Default: ANIMATIONS.SLIDE`
* @type {(AnimationEasingFunction)}
* @default {ANIMATIONS.SLIDE}
*/
customEasingFunction?: AnimationEasingFunction;
/**
* Determines whether sheet will close or not when device back button is pressed, on Android.
*
* __In a typical app screen (where back button takes user to the previous screen):__ \
* _If `true` and sheet is open, the user has to press the back button twice to exit the screen.
* The first press closes the sheet and the second exits the screen.\
* If `false` and sheet is open, the user has to close the sheet through another means like backdrop press
* and then press the back button after that to exit the screen_
*
* `Note:` Only applies for Android
*
* `Default: true`
* @platform Android
* @type {boolean}
* @default {true}
*/
android_closeOnBackPress?: boolean;
/**
* Сallback function that is called when the bottom sheet starts to close
*
* @type {Function}
* @default undefined
* @example
* ```tsx
* <BottomSheet
* onClose={() => {
* console.log('Bottom Sheet closing.');
* }}
* />
* ```
*/
onClose?: Function;
/**
* Сallback function that is called when the bottom sheet starts to open
*
* @type {Function}
* @default undefined
* @example
* ```tsx
* <BottomSheet
* onOpen={() => {
* console.log('Bottom Sheet opening.');
* }}
* />
* ```
*/
onOpen?: Function;
/**
* Function called with an animated number indicating the
* progress of the opening/closing animation when sheet is being opened or closed.
*
* When `animationType` prop is __not__ set to `ANIMATION.FADE` or `'fade'`,
* the number passed to this function will go from _`0`_ to _`sheet's height`_ (when sheet's opening)
* or _`sheet's height`_ to _`0`_ (when sheet's closing).
*
* When `animationType` prop is set to `ANIMATION.FADE` or `'fade'`,
* the number will go from `0.0` to `1.0` (when sheet's opening)
* or _`1.0`_ to _`0.0`_ (when sheet's closing).
*
* `Default: undefined`
* @default {undefined}
* @type {(animatedHeightOrOpacity: number) => void}
* ### Example with 'slide' or 'spring' animation
* @example
* ```tsx
* <BottomSheet
ref={sheetRef}
height={300}
animationType="slide"
onAnimate={(animatedValue) => {
console.log(animatedValue); // (0)...(150)...(300)
}}
>
...
</BottomSheet>
* ```
* ### Example with 'fade' animation
* @example
* ```tsx
* <BottomSheet
ref={sheetRef}
height={300}
animationType="fade"
onAnimate={(animatedValue) => {
console.log(animatedValue); // (0.0)...(0.5)...(1.0)
}}
>
...
</BottomSheet>
```
*/
onAnimate?: (animatedHeightOrOpacity: number) => void;
/**
* By default, the sheet handles keyboard pop-out automatically and smartly,
* setting this to `true` disables that behavior, essentially leaving
* the system to handle the keyboard the native way.
*
* `Default: false`
* @type {boolean}
* @default {false}
*/
disableKeyboardHandling?: boolean;
}
export type BottomSheetProps = BottomSheetBaseProps &
(
| {
/**
* Height of the bottom sheet when expanded. This value will be relative to `containerHeight`
* if it's supplied, or the screen's height otherwise.
* Value can be in pixel units (number) or percentage (string).
*
* `Default: '50%'`
*
* @type {number | string}
* @default '50%'
* @example
* height={300}
* // or
* height={'50%'}
*/
height?: number | string;
snapPoints?: never;
}
| {
height?: never;
/**
* Resting positions (detents) the bottom sheet can settle on. Accepts a mix of pixel
* numbers and percentage strings, e.g. `['25%', '50%', 600]`. Percentages are relative
* to `containerHeight` (the device screen by default), exactly like the `height` prop.
*
* When dragged, the sheet snaps to the nearest point on release (a fast flick carries it
* further); dragging below the smallest point closes it when `closeOnDragDown` is `true`.
* Move between points programmatically with the ref methods `snapToIndex`, `expand`, and
* `collapse`.
*
* `Note:` Points are sorted ascending internally, so a snap `index` always refers to
* smallest → largest. When provided, this prop takes precedence over `height`.
* Drag-to-snap applies to the `slide` and `spring` animations; with `fade` the sheet
* simply opens at the active snap point.
*
* `Default: undefined` (single-height behavior via `height`)
* @type {(number | string)[]}
* @default undefined
* @example
* snapPoints={['30%', '60%', '90%']}
*/
snapPoints: (number | string)[];
}
);
export {
AnimationEasingFunction,
ANIMATIONS,
BOTTOMSHEET,
BottomSheetMethods,
BottomSheetProps,
CUSTOM_BACKDROP_POSITIONS,
ToValue,
};