@gorhom/bottom-sheet
Version:
A performant interactive bottom sheet with fully configurable options 🚀
174 lines (156 loc) • 4.28 kB
text/typescript
import { useCallback, useEffect } from 'react';
import {
Keyboard,
type KeyboardEvent,
type KeyboardEventEasing,
type KeyboardEventName,
Platform,
} from 'react-native';
import {
runOnUI,
useAnimatedReaction,
useSharedValue,
} from 'react-native-reanimated';
import { KEYBOARD_STATUS, SCREEN_HEIGHT } from '../constants';
import type { KeyboardState } from '../types';
const KEYBOARD_EVENT_MAPPER = {
KEYBOARD_SHOW: Platform.select({
ios: 'keyboardWillShow',
android: 'keyboardDidShow',
default: '',
}) as KeyboardEventName,
KEYBOARD_HIDE: Platform.select({
ios: 'keyboardWillHide',
android: 'keyboardDidHide',
default: '',
}) as KeyboardEventName,
};
const INITIAL_STATE: KeyboardState = {
status: KEYBOARD_STATUS.UNDETERMINED,
height: 0,
heightWithinContainer: 0,
easing: 'keyboard',
duration: 500,
};
export const useAnimatedKeyboard = () => {
//#region variables
const state = useSharedValue(INITIAL_STATE);
const temporaryCachedState = useSharedValue<Omit<
KeyboardState,
'heightWithinContainer' | 'target'
> | null>(null);
//#endregion
//#region worklets
const handleKeyboardEvent = useCallback(
(
status: KEYBOARD_STATUS,
height: number,
duration: number,
easing: KeyboardEventEasing,
bottomOffset?: number
) => {
'worklet';
const currentState = state.get();
/**
* if the keyboard event was fired before the `onFocus` on TextInput,
* then we cache the event, and wait till the `target` is been set
* to be updated then fire this function again.
*/
if (status === KEYBOARD_STATUS.SHOWN && !currentState.target) {
temporaryCachedState.set({
status,
height,
duration,
easing,
});
return;
}
/**
* clear temporary cached state.
*/
temporaryCachedState.set(null);
/**
* if keyboard status is hidden, then we keep old height.
*/
let adjustedHeight =
status === KEYBOARD_STATUS.SHOWN ? height : currentState.height;
/**
* if keyboard had an bottom offset -android bottom bar-, then
* we add that offset to the keyboard height.
*/
if (bottomOffset) {
adjustedHeight = adjustedHeight + bottomOffset;
}
state.set({
target: currentState.target,
status,
easing,
duration,
height: adjustedHeight,
heightWithinContainer: currentState.heightWithinContainer,
});
},
[state, temporaryCachedState]
);
//#endregion
//#region effects
useEffect(() => {
const handleOnKeyboardShow = (event: KeyboardEvent) => {
runOnUI(handleKeyboardEvent)(
KEYBOARD_STATUS.SHOWN,
event.endCoordinates.height,
event.duration,
event.easing,
SCREEN_HEIGHT -
event.endCoordinates.height -
event.endCoordinates.screenY
);
};
const handleOnKeyboardHide = (event: KeyboardEvent) => {
runOnUI(handleKeyboardEvent)(
KEYBOARD_STATUS.HIDDEN,
event.endCoordinates.height,
event.duration,
event.easing
);
};
const showSubscription = Keyboard.addListener(
KEYBOARD_EVENT_MAPPER.KEYBOARD_SHOW,
handleOnKeyboardShow
);
const hideSubscription = Keyboard.addListener(
KEYBOARD_EVENT_MAPPER.KEYBOARD_HIDE,
handleOnKeyboardHide
);
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, [handleKeyboardEvent]);
/**
* This reaction is needed to handle the issue with multiline text input.
*
* @link https://github.com/gorhom/react-native-bottom-sheet/issues/411
*/
useAnimatedReaction(
() => state.value.target,
(result, previous) => {
if (!result || result === previous) {
return;
}
const cachedState = temporaryCachedState.get();
if (!cachedState) {
return;
}
handleKeyboardEvent(
cachedState.status,
cachedState.height,
cachedState.duration,
cachedState.easing
);
},
[temporaryCachedState, handleKeyboardEvent]
);
//#endregion
return state;
};