From 407ea69425b445bd21136568b0bf0eec571fd58f Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 16 Jan 2026 21:03:06 +0100 Subject: [PATCH] fix(tv): add opening animations to bottom sheet option selectors --- components/ItemContent.tv.tsx | 151 +++++++++++------- .../video-player/controls/Controls.tv.tsx | 118 +++++++++----- 2 files changed, 172 insertions(+), 97 deletions(-) diff --git a/components/ItemContent.tv.tsx b/components/ItemContent.tv.tsx index 6f5189b0..12f8ad89 100644 --- a/components/ItemContent.tv.tsx +++ b/components/ItemContent.tv.tsx @@ -314,11 +314,39 @@ const TVOptionSelector = ({ const [isReady, setIsReady] = useState(false); const firstCardRef = useRef(null); + // Animation values + const overlayOpacity = useRef(new Animated.Value(0)).current; + const sheetTranslateY = useRef(new Animated.Value(200)).current; + const initialSelectedIndex = useMemo(() => { const idx = options.findIndex((o) => o.selected); return idx >= 0 ? idx : 0; }, [options]); + // Animate in when visible + useEffect(() => { + if (visible) { + // Reset values and animate in + overlayOpacity.setValue(0); + sheetTranslateY.setValue(200); + + Animated.parallel([ + Animated.timing(overlayOpacity, { + toValue: 1, + duration: 250, + easing: Easing.out(Easing.quad), + useNativeDriver: true, + }), + Animated.timing(sheetTranslateY, { + toValue: 0, + duration: 300, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, overlayOpacity, sheetTranslateY]); + // Delay rendering to work around hasTVPreferredFocus timing issue useEffect(() => { if (visible) { @@ -341,7 +369,7 @@ const TVOptionSelector = ({ if (!visible) return null; return ( - ({ backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "flex-end", zIndex: 1000, + opacity: overlayOpacity, }} > - - + - {/* Title */} - - {title} - - - {/* Horizontal options */} - {isReady && ( - - {options.map((option, index) => ( - { - onSelect(option.value); - onClose(); - }} - /> - ))} - - )} - - - + {title} + + + {/* Horizontal options */} + {isReady && ( + + {options.map((option, index) => ( + { + onSelect(option.value); + onClose(); + }} + /> + ))} + + )} + + + + ); }; diff --git a/components/video-player/controls/Controls.tv.tsx b/components/video-player/controls/Controls.tv.tsx index 433d8a52..e3362ea6 100644 --- a/components/video-player/controls/Controls.tv.tsx +++ b/components/video-player/controls/Controls.tv.tsx @@ -105,11 +105,39 @@ const TVOptionSelector = ({ const [isReady, setIsReady] = useState(false); const firstCardRef = useRef(null); + // Animation values + const overlayOpacity = useRef(new RNAnimated.Value(0)).current; + const sheetTranslateY = useRef(new RNAnimated.Value(200)).current; + const initialSelectedIndex = useMemo(() => { const idx = options.findIndex((o) => o.selected); return idx >= 0 ? idx : 0; }, [options]); + // Animate in when visible + useEffect(() => { + if (visible) { + // Reset values and animate in + overlayOpacity.setValue(0); + sheetTranslateY.setValue(200); + + RNAnimated.parallel([ + RNAnimated.timing(overlayOpacity, { + toValue: 1, + duration: 250, + easing: RNEasing.out(RNEasing.quad), + useNativeDriver: true, + }), + RNAnimated.timing(sheetTranslateY, { + toValue: 0, + duration: 300, + easing: RNEasing.out(RNEasing.cubic), + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, overlayOpacity, sheetTranslateY]); + // Delay rendering to work around hasTVPreferredFocus timing issue useEffect(() => { if (visible) { @@ -132,44 +160,57 @@ const TVOptionSelector = ({ if (!visible) return null; return ( - - - + + - {title} - {isReady && ( - - {options.map((option, index) => ( - { - onSelect(option.value); - onClose(); - }} - /> - ))} - - )} - - - + + {title} + {isReady && ( + + {options.map((option, index) => ( + { + onSelect(option.value); + onClose(); + }} + /> + ))} + + )} + + + + ); }; @@ -543,6 +584,9 @@ const selectorStyles = StyleSheet.create({ justifyContent: "flex-end", zIndex: 1000, }, + sheetContainer: { + // Container for the sheet to enable slide animation + }, blurContainer: { borderTopLeftRadius: 24, borderTopRightRadius: 24,