From c75836e769a16fce2a465c94057059a34b64d79a Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 19 Jan 2026 15:53:00 +0100 Subject: [PATCH 1/5] Use pressIn & pressOut --- .../button/actionbutton/ActionButton.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ui/components/button/actionbutton/ActionButton.tsx b/src/ui/components/button/actionbutton/ActionButton.tsx index a5d15038..70d87ab0 100644 --- a/src/ui/components/button/actionbutton/ActionButton.tsx +++ b/src/ui/components/button/actionbutton/ActionButton.tsx @@ -1,5 +1,5 @@ import { Image, ImageSourcePropType, Platform, TouchableOpacity, View, ViewStyle } from 'react-native'; -import React, { ReactNode, useContext, useState } from 'react'; +import React, { ReactNode, useContext, useRef, useState } from 'react'; import { SvgContext } from '../svg/SvgUtils'; import { PlayerContext } from '../../util/PlayerContext'; import type { ButtonBaseProps } from '../ButtonBaseProps'; @@ -46,6 +46,7 @@ export const DEFAULT_ACTION_BUTTON_STYLE: ViewStyle = { export const ActionButton = (props: React.PropsWithChildren) => { const { activeOpacity, children, icon, style, svg, onPress, highlighted, testID } = props; const [focused, setFocused] = useState(false); + const isPressed = useRef(false); const context = useContext(PlayerContext); const shouldChangeTintColor = highlighted || (focused && Platform.isTV); const touchable = props.touchable != false; @@ -53,19 +54,26 @@ export const ActionButton = (props: React.PropsWithChildren) return {svg}; } - const onTouch = () => { + const onTouchIn = () => { if (context.ui.buttonsEnabled_) { - onPress?.(); + isPressed.current = true; } context.ui.onUserAction_(); }; + const onTouchOut = () => { + if (context.ui.buttonsEnabled_) { + onPress?.(); + } + isPressed.current = false; + }; return ( { context.ui.onUserAction_(); setFocused(true); From 36c9c292dd6e43c7c4a45e8ec2c2350ec27e289c Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 19 Jan 2026 16:13:19 +0100 Subject: [PATCH 2/5] Add comment --- src/ui/components/button/actionbutton/ActionButton.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/components/button/actionbutton/ActionButton.tsx b/src/ui/components/button/actionbutton/ActionButton.tsx index 70d87ab0..88bc14f0 100644 --- a/src/ui/components/button/actionbutton/ActionButton.tsx +++ b/src/ui/components/button/actionbutton/ActionButton.tsx @@ -54,6 +54,10 @@ export const ActionButton = (props: React.PropsWithChildren) return {svg}; } + /** + * Rely on onPressIn and onPressOut, as a workaround for onPress events sometimes being filtered by React Native + * in fullscreen presentation mode on Android & iOS. + */ const onTouchIn = () => { if (context.ui.buttonsEnabled_) { isPressed.current = true; From 8d52ad3a25fa0714cc082e70526d8e08ef1ba62e Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 19 Jan 2026 17:52:15 +0100 Subject: [PATCH 3/5] Check ifPressed value --- src/ui/components/button/actionbutton/ActionButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/components/button/actionbutton/ActionButton.tsx b/src/ui/components/button/actionbutton/ActionButton.tsx index 88bc14f0..fe137c33 100644 --- a/src/ui/components/button/actionbutton/ActionButton.tsx +++ b/src/ui/components/button/actionbutton/ActionButton.tsx @@ -65,7 +65,7 @@ export const ActionButton = (props: React.PropsWithChildren) context.ui.onUserAction_(); }; const onTouchOut = () => { - if (context.ui.buttonsEnabled_) { + if (isPressed.current && context.ui.buttonsEnabled_) { onPress?.(); } isPressed.current = false; From 2d19c7e8d8445778ee7b5c05d4bc1dfd2419fe92 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 20 Jan 2026 11:58:43 +0100 Subject: [PATCH 4/5] Use PanResponder --- .../button/actionbutton/ActionButton.tsx | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/ui/components/button/actionbutton/ActionButton.tsx b/src/ui/components/button/actionbutton/ActionButton.tsx index fe137c33..e1ed4efb 100644 --- a/src/ui/components/button/actionbutton/ActionButton.tsx +++ b/src/ui/components/button/actionbutton/ActionButton.tsx @@ -1,5 +1,5 @@ -import { Image, ImageSourcePropType, Platform, TouchableOpacity, View, ViewStyle } from 'react-native'; -import React, { ReactNode, useContext, useRef, useState } from 'react'; +import { Image, ImageSourcePropType, Platform, View, ViewStyle, PanResponder } from 'react-native'; +import React, { ReactNode, useContext, useState, useRef } from 'react'; import { SvgContext } from '../svg/SvgUtils'; import { PlayerContext } from '../../util/PlayerContext'; import type { ButtonBaseProps } from '../ButtonBaseProps'; @@ -46,38 +46,53 @@ export const DEFAULT_ACTION_BUTTON_STYLE: ViewStyle = { export const ActionButton = (props: React.PropsWithChildren) => { const { activeOpacity, children, icon, style, svg, onPress, highlighted, testID } = props; const [focused, setFocused] = useState(false); - const isPressed = useRef(false); + const [pressed, setPressed] = useState(false); const context = useContext(PlayerContext); const shouldChangeTintColor = highlighted || (focused && Platform.isTV); const touchable = props.touchable != false; - if (!touchable) { - return {svg}; - } + const pressedRef = useRef(false); - /** - * Rely on onPressIn and onPressOut, as a workaround for onPress events sometimes being filtered by React Native - * in fullscreen presentation mode on Android & iOS. - */ - const onTouchIn = () => { - if (context.ui.buttonsEnabled_) { - isPressed.current = true; - } + const handlePressIn = () => { + setPressed(true); + pressedRef.current = true; context.ui.onUserAction_(); }; - const onTouchOut = () => { - if (isPressed.current && context.ui.buttonsEnabled_) { - onPress?.(); - } - isPressed.current = false; + + const handlePressOut = () => { + setPressed(false); + pressedRef.current = false; }; + /** + * Use a PanResponder instead of Touchable component to fix the issue of onPress events sometimes being filtered by + * React Native in fullscreen presentation mode on Android & iOS. + */ + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponder: () => touchable, + onMoveShouldSetPanResponder: () => false, + onPanResponderGrant: () => handlePressIn(), + onPanResponderRelease: () => { + if (pressedRef.current) { + onPress?.(); + } + handlePressOut(); + }, + onPanResponderTerminate: () => handlePressOut(), + onPanResponderReject: () => handlePressOut(), + }), + ).current; + + if (!touchable) { + return {svg}; + } + return ( - { context.ui.onUserAction_(); setFocused(true); @@ -106,6 +121,6 @@ export const ActionButton = (props: React.PropsWithChildren) /> )} {children} - + ); }; From a3d44241337cc4abb399847d0117de45180746d6 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 21 Jan 2026 09:41:12 +0100 Subject: [PATCH 5/5] Add changeset --- .changeset/cozy-melons-rescue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cozy-melons-rescue.md diff --git a/.changeset/cozy-melons-rescue.md b/.changeset/cozy-melons-rescue.md new file mode 100644 index 00000000..63e826a7 --- /dev/null +++ b/.changeset/cozy-melons-rescue.md @@ -0,0 +1,5 @@ +--- +'@theoplayer/react-native-ui': patch +--- + +Resolved an issue where button presses occasionally failed to register in fullscreen presentation mode on iOS and Android.