Skip to content

Instantly share code, notes, and snippets.

@sturmenta
Last active August 29, 2023 18:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sturmenta/af790331f6bd27322ecd73ee723c8c60 to your computer and use it in GitHub Desktop.
Save sturmenta/af790331f6bd27322ecd73ee723c8c60 to your computer and use it in GitHub Desktop.
react native collapse animation dynamic height
import React, {useEffect, useImperativeHandle, useState} from 'react';
import {Keyboard, View} from 'react-native';
import {useTheme} from '@react-navigation/native';
import {useSpring, animated} from '@react-spring/native';
import {MyThemeInterfaceColors} from '_styles';
import {GetDimensions} from '_atoms';
import {MultilineTextInput, Button, TextInputRef} from '_molecules';
import {getPercentageInHex, themedStyleSheet} from '_utils';
import {usePrevious} from '_hooks';
export interface FeedbackProps {
sectionName: string;
}
export interface FeedbackRef {
open(): void;
close(): void;
isOpen(): boolean;
}
export const Feedback = React.forwardRef<FeedbackRef, FeedbackProps>(
({sectionName}: FeedbackProps, ref) => {
const styles = useStyles();
const colors = useTheme().colors as MyThemeInterfaceColors;
const textInputRef = React.createRef<TextInputRef>();
const [isVisible, setIsVisible] = useState(false);
const previousVisible = usePrevious(isVisible);
const [feedbackText, setFeedbackText] = useState('');
const [dynamicHeight, setDynamicHeight] = useState<Number>(0);
const {height, y} = useSpring({
from: {height: 0, opacity: 0, y: 0},
to: {
height: isVisible ? dynamicHeight : 0,
opacity: isVisible ? 1 : 0,
y: isVisible ? 0 : 10,
},
});
useEffect(() => {
if (isVisible) textInputRef.current?.focus();
}, [isVisible, textInputRef, sectionName]);
const open = () => {
setFeedbackText('');
setIsVisible(true);
};
const close = () => {
Keyboard.dismiss();
setIsVisible(false);
};
const isOpen = () => isVisible;
useImperativeHandle(ref, () => ({open, close, isOpen})); // https://stackoverflow.com/a/64491870
// ─────────────────────────────────────────────────────────────────
const onSendFeedback = () => {
console.warn('send feedback -', sectionName);
close();
// TODO: fullscreen animation with background fade thanks for comments
};
// ─────────────────────────────────────────────────────────────────
const InsideAnimationContainer = React.memo(() => (
<>
<View style={styles.inputTextContainer}>
<MultilineTextInput
inputRef={textInputRef}
value={feedbackText}
onChangeText={setFeedbackText}
returnKeyType="send"
placeholder="¿Alguna idea sobre como mejorar esta parte de la app?"
onSubmitEditing={onSendFeedback}
/>
</View>
<View style={styles.buttonsContainer}>
<Button
text="Cancelar"
size="small"
onPress={close}
style={{
backgroundColor: `#ffffff${getPercentageInHex(10)}`,
...styles.button,
}}
textScale="caption"
/>
<Button
text="Enviar comentarios"
size="small"
onPress={onSendFeedback}
style={{
backgroundColor: colors.bg_green,
...styles.button,
}}
textScale="caption"
/>
</View>
</>
));
return (
<animated.View
style={{
...styles.touchableContainer,
...({
height:
isVisible && previousVisible === isVisible ? 'auto' : height,
top: y,
} || {}),
}}>
<GetDimensions
component={<InsideAnimationContainer />}
onDimensions={({height: _height}) =>
dynamicHeight !== _height ? setDynamicHeight(_height) : null
}
/>
<InsideAnimationContainer />
</animated.View>
);
},
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const useStyles = themedStyleSheet((colors: MyThemeInterfaceColors) => ({
touchableContainer: {
backgroundColor: `#000000${getPercentageInHex(20)}`,
},
inputTextContainer: {
marginTop: 15,
paddingHorizontal: 25,
},
button: {
borderRadius: 4,
height: 'auto',
padding: 5,
paddingHorizontal: 15,
},
buttonsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 25,
paddingTop: 6,
marginBottom: 10,
},
}));
// https://codesandbox.io/s/github/pmndrs/react-spring/tree/master/demo/src/sandboxes/tree
// https://react-spring-visualizer.com/
import React, {useEffect, useState} from 'react';
import {View, ViewProps} from 'react-native';
interface GetDimensionsProps extends ViewProps {
component: React.ReactChild[] | React.ReactChild;
onDimensions: ({width, height}: {width: Number; height: Number}) => void;
}
// render the component once (10 milliseconds) & return dimensions
export const GetDimensions: React.FC<GetDimensionsProps> = ({
component,
onDimensions,
...props
}: GetDimensionsProps) => {
const [isTimeToRenderNull, setIsTimeToRenderNull] = useState(false);
useEffect(() => {
setTimeout(() => setIsTimeToRenderNull(true), 10);
}, []);
if (isTimeToRenderNull) return null;
return (
<View
onLayout={event =>
onDimensions({
width: Math.round(event.nativeEvent.layout.width),
height: Math.round(event.nativeEvent.layout.height),
})
}
style={{position: 'absolute', width: '100%'}}
{...props}>
{component}
</View>
);
};
// ────────────────────────────────────────────────────────────────────────────────
// usage
// <GetDimensions
// component={<View />}
// onDimensions={({width, height}) => console.log(width, height)} />
/* eslint-disable no-void */
/* eslint-disable no-return-assign */
import {useEffect, useRef} from 'react';
export function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => void (ref.current = value), [value]);
return ref.current;
}
@kingkongqn4444
Copy link

look good. thank you for sharing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment