Skip to content

[General] Press buttons from hardware keys, accessibility services and a TV's Select press - #4614

Open
YevheniiKotyrlo wants to merge 11 commits into
software-mansion:mainfrom
YevheniiKotyrlo:fix/pressable-android-click-without-touch
Open

YevheniiKotyrlo wants to merge 11 commits into
software-mansion:mainfrom
YevheniiKotyrlo:fix/pressable-android-click-without-touch

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

On Android, an activation no pointer drives presses no gesture-handler button: a hardware Enter or Space on a focused button, a Switch Access or Voice Access click, and a TalkBack double tap on a default Pressable that no finger has touched. Enter and Space activate a focused button (WAI-ARIA APG, Button), and an activation by keyboard or voice fires the click a pointer would (HTML §6.5), as React Native's Pressable does, and gesture-handler on web for a key.

  • ButtonViewGroup.setPressed admits a press only when touchResponder === this || canBePressedAlongsideOther, so an exclusive button, the default, refuses the press View.onKeyDown makes for a confirm key and never clicks — the RectButton doesn't trigger onPress callback when pressed with keyboard Enter/Space key #2204 regression from Fix grabbing delegate in setPressed #3742 — and performClick activated the native gesture for a click no touch produced only while touch exploration was on or a key's up had set receivedKeyEvent. The button now records a click's source while View's own onKeyDown, onKeyUp or performAccessibilityAction runs, admits that press without taking the responder where its click would activate a native gesture, and activates the gesture from it, then calls View.performClick for the click sound and TYPE_VIEW_CLICKED once a handler took the press. receivedKeyEvent, the touch-exploration check and the root view's forwarding activateNativeHandlers are gone.
  • The activation leaves a gesture a pointer already drives, or one waiting on another's failure, to that gesture: ending it from the click, as main does for a key on a button a finger holds, cancels the finger's touches and leaves the button the touch responder.
  • [Android] Cancel button gesture when other handlers are extracted before it #4127's check, run wherever a handler is recorded, cancelled a key's or an accessibility click's activation against the root view's own handler, which a resting finger or a hovering mouse records; it now runs only where a pointer's touch records a handler.
  • On main an activation with no pointer reports the last pointer's count, type and position: (0, 0) on a button no pointer has reached, which PressableWithTouchable's isTouchWithinInset rejects. prepareActivationWithoutPointer reports no pointer, as Pointer Events requires of a click a non-pointing device generates (§4.2.12.1), at the view's center mapped to the screen, where View takes a key event to be (View.java); where that section asks for an empty type, it reports KEY for a key, as web does, and OTHER for an accessibility service. handle() now reads a down's pointer before the touch callbacks run, so a handler a UI-thread onTouchesDown activates, or a second finger begins, reports that touch rather than the last gesture's. makeActive no longer asks React Native to cancel a touch for an activation no pointer drives, which logged Can't cancel already finished gesture (JSTouchDispatcher).
  • Without a screen reader, LegacyPressable and StatefulPressable press in only on LONG_PRESS_TOUCHES_DOWN, which only a touch sends. A native gesture that begins reporting no pointer now goes around their state machine, pressing in as it begins and out as it ends at the position it reports, as StatefulPressable's TV branch did. Every engine, the default Pressable's included, presses it before its first layout and under a hit slop that leaves out its center, and isKeyboardDismissingTap no longer takes it, inside a gesture-handler ScrollView, for the tap dismissing the keyboard. NativeViewGestureHandlerPayload declares the x, y, absoluteX and absoluteY that Emit coordinates in Native gesture events #4302 emits, and the docs describe such a press.
  • Apple's native view handler reports a press no touch drives, a tvOS focus Select among them, at (0, 0) as TOUCH, so the default Pressable drops it. It now reports the view's center, as handleSwitch: does, with no pointer and KEY, so the same rule presses it. No CI job builds tvOS and no device here runs it, so that half rests on the iOS build and jest.

While a key is down the button shows its native press feedback (recording). Android clicks on the key's up, so onPressIn and onPress arrive together, and a held key presses once, on release, as React Native's Pressable does (Pressability.onClick).

Test plan

Tested on the following code, as the example app's Empty example:
import { useState } from 'react';
import {
  Pressable as ReactNativePressable,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import { Pressable, RectButton } from 'react-native-gesture-handler';

export default function EmptyExample() {
  const [presses, setPresses] = useState({
    pressable: 0,
    rectButton: 0,
    reactNativePressable: 0,
  });
  const count = (button: keyof typeof presses) => () =>
    setPresses((current) => ({ ...current, [button]: current[button] + 1 }));

  return (
    <View style={styles.container}>
      <Pressable style={styles.button} onPress={count('pressable')}>
        <Text>{`Pressable: ${presses.pressable}`}</Text>
      </Pressable>
      <RectButton style={styles.button} onPress={count('rectButton')}>
        <Text>{`RectButton: ${presses.rectButton}`}</Text>
      </RectButton>
      <ReactNativePressable
        style={styles.button}
        onPress={count('reactNativePressable')}>
        <Text>{`React Native Pressable: ${presses.reactNativePressable}`}</Text>
      </ReactNativePressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: 16,
  },
  button: {
    width: 240,
    padding: 16,
    alignItems: 'center',
    backgroundColor: '#e0e0e0',
  },
});
  • The code above, on an API 34 emulator: on main, Enter and Space on a Tab-focused button count only on React Native's Pressable; with this PR each key counts once on every button. With TalkBack on, a double tap on gesture-handler's Pressable before any touch counts nothing on main and once with this PR, while RectButton and React Native's Pressable count theirs on both.
  • The new Keyboard activation example's flow, .argent/flows/keyboard-activation-test.yaml, and .argent/flows/keyboard-touchable-test.yaml, which presses a Touchable with Enter, then Space, fail on main and pass here (before/after).
  • pressableClickWithoutTouch.test.tsx and api_v3.test.tsx: every engine presses once for a key, an accessibility click, an Android TV key, a tvOS focus press and an iOS press no touch drives, while a touch keeps the touch path. With main's engines every no-pointer case fails but a screen reader's click.
  • On the same emulator, an accessibility client's ACTION_CLICK with touch exploration off, as Switch Access and Voice Access click, presses nothing on main and each control once here.
  • Every Android and JavaScript job passes on Linux, emulator flows included; on a Mac with Xcode 27.1, for a device, so do the iOS build, every flow on an iPhone 12 Pro Max and the SPM build, with and without react-native-worklets.

Copilot AI balanced review requested due to automatic review settings October 19, 2026 12:50
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8734e4d9-0557-47d3-9963-198152c0e216
📥 Commits

Reviewing files that changed from the base of the PR and between 0c12342 and 849bf13.

📒 Files selected for processing: 23
  • .argent/flows/keyboard-activation-test.yaml
  • .argent/flows/keyboard-touchable-test.yaml
  • apps/common-app/src/new_api/index.tsx
  • apps/common-app/src/new_api/tests/keyboardActivation/index.tsx
  • packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx
  • packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt
  • packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm
  • packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
  • packages/react-native-gesture-handler/src/__tests__/pressableClickWithoutTouch.test.tsx
  • packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx
  • packages/react-native-gesture-handler/src/components/Pressable/utils.ts
  • packages/react-native-gesture-handler/src/handlers/GestureHandlerEventPayload.ts
  • packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts
  • packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx
  • packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx
  • packages/react-native-gesture-handler/src/v3/components/Touchable/Touchable.tsx
  • packages/react-native-gesture-handler/src/v3/scrollViewInterop.ts
💤 Files with no reviewable changes: 2
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Summary

Summary by CodeRabbit

  • New Features

    • Added Android support for activating pressable and gesture-handled controls with keyboard and accessibility inputs.
    • Added a keyboard activation example covering pressables, buttons, touchables, gestures, disabled controls, and event details.
    • Added support for accurate center-based coordinates and pointer metadata for pointerless activations.
  • Bug Fixes

    • Keyboard and accessibility activation now triggers expected press callbacks without incorrectly dismissing the soft keyboard.
    • Improved handling of native button activation and gesture coordination.
  • Documentation

    • Clarified pointerless activation behavior, coordinates, pointer counts, and responder cancellation.
  • Tests

    • Added comprehensive Android flows and regression coverage for keyboard, accessibility, touch, and focus-based activation.

The change adds pointerless activation support across native gesture handling and Pressable components. It updates keyboard-dismiss detection, documents event coordinates and pointer metadata, and adds automated coverage and an Android example.

Changes: Pointerless Press Activation

Layer / File(s) Summary
Native activation and event data
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/*, packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/*, packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm, packages/react-native-gesture-handler/src/handlers/GestureHandlerEventPayload.ts, packages/docs-gesture-handler/docs/gestures/*
Android gesture handlers and buttons now prepare and dispatch pointerless activations, with pointer type and center coordinates. iOS native view events now report key pointer types and center coordinates for touchless TV presses. Documentation describes pointerless event data and the Android responder behavior.
Pressable handling and keyboard dismissal
packages/react-native-gesture-handler/src/components/Pressable/*, packages/react-native-gesture-handler/src/v3/components/*, packages/react-native-gesture-handler/src/v3/scrollViewInterop.ts, packages/react-native-gesture-handler/src/__tests__/*, packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts
Pressable implementations now handle activations without pointers. Keyboard-dismiss detection receives pointer-count data and excludes pointerless activations. Tests cover native and JavaScript press handling across platforms and keyboard interactions.
Example screen and end-to-end flows
apps/common-app/src/new_api/index.tsx, apps/common-app/src/new_api/tests/keyboardActivation/index.tsx, .argent/flows/keyboard-activation-test.yaml, .argent/flows/keyboard-touchable-test.yaml
The common app now includes a keyboard activation example that logs activation events and coordinates. Android flows test keyboard activation and nested touchable behavior.

Sequence Diagram(s)

sequenceDiagram
  participant ButtonViewGroup
  participant GestureHandlerOrchestrator
  participant GestureHandler
  ButtonViewGroup->>GestureHandlerOrchestrator: Request native activation with pointer type
  GestureHandlerOrchestrator->>GestureHandler: Prepare and run activation lifecycle
  GestureHandlerOrchestrator-->>ButtonViewGroup: Return activation result
  ButtonViewGroup->>ButtonViewGroup: Perform click
Loading

Suggested reviewers: m-bert

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 849bf

This change makes keyboard, accessibility-service and TV Select activations press gesture-handler controls on Android and Apple platforms. No actionable merge-blocking risk is identified. Device-only accessibility behavior, such as TalkBack, is covered by the author's reported manual validation rather than automated tests.

Pre-merge checks: Passed 3
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: enabling button presses from hardware keys, accessibility services, and TV Select input.
Linked Issues check Passed Directly linked issue #3735 is closed and supplies historical context only. The PR states that no active directly linked issue objectives apply. Therefore, no linked-issue coding requirements apply to…
Out of Scope Changes check Passed The changes stay within the stated intent. Android key and accessibility activation, pointerless gesture metadata, responder handling, Pressable behavior, Apple and TV activation behavior, tests, exam…
Full details: Linked Issues check

Explanation

Directly linked issue #3735 is closed and supplies historical context only. The PR states that no active directly linked issue objectives apply. Therefore, no linked-issue coding requirements apply to this assessment.

Full details: Out of Scope Changes check

Explanation

The changes stay within the stated intent. Android key and accessibility activation, pointerless gesture metadata, responder handling, Pressable behavior, Apple and TV activation behavior, tests, examples, flows, and documentation all support the described activation changes.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx

Parsing error: ESLint was configured to run on <tsconfigRootDir>/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx using parserOptions.project: /tsconfig.json
The extension for the file (.mdx) is non-standard. You should add parserOptions.extraFileExtensions to your config.

packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx

Parsing error: ESLint was configured to run on <tsconfigRootDir>/packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx using parserOptions.project: /tsconfig.json
The extension for the file (.mdx) is non-standard. You should add parserOptions.extraFileExtensions to your config.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — main presses nothing for Enter or Space on a Tab-focused gesture-handler control, nor for an accessibility service's click.

FIXED — each activation presses its control once: Enter on a Pressable no pointer has touched, on the outer state-machine and legacy Pressables after a tap on their inner one, and on the Pressable beside an input holding the soft keyboard; a RectButton's gesture begins with KEY and 0 pointers; a key on a Touchable under a scale: 0.5 parent lands where a tap on it did; Enter and Space each press a Touchable once.

before — main after — with this PR
Keyboard activation
Nested touchables

API 34 x86_64 emulator (google_apis, 320×640 at 160 dpi), release builds of apps/expo-example from main at the base commit, with this PR's Keyboard activation screen added, and from this branch. Each image is the console after the screen's key presses; on Keyboard activation, lines 1–9 are the taps that come first, the same on both builds.

On Keyboard activation main this PR
ACTION_CLICK from an accessibility client with touch exploration off — the path Switch Access and Voice Access take — on four controls nothing, while each action reports performed each control pressed once; the RectButton reports OTHER with 0 pointers
the same on a disabled Pressable nothing nothing
TalkBack 14.2 double tap on the default Pressable no finger has touched nothing pressed once
Enter on the Pressable beside the input, the soft keyboard up nothing pressed once, the keyboard still up
Enter held 9 s on the RectButton nothing pressed once
uiautomator events across Enter on the RectButton no TYPE_VIEW_CLICKED one TYPE_VIEW_CLICKED
the screen's first tap on a RawButton its UI-thread onTouchesDown activates onActivate OTHER with 0 pointers onActivate TOUCH with 1 pointers at its touch

Press feedback — Enter held 1.5 s on the Tab-focused RectButton, on the same builds and emulator at full animation speed: main shows nothing and logs nothing; with this PR the button shows its ripple while the key is down, then logs onBegin KEY with 0 pointers and onPress. A screen recording holds a frame only when the screen changes: main's holds 5 frames over 6.5 s and the button's region never changes, while this PR's holds 91 frames through the hold, about 60 a second (before, after).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Stale touch tracking can suppress keyboard activation, and transformed buttons report incorrect absolute coordinates.

3 open issues
What changed in this PR

Enables Android hardware-key activation of focused gesture-handler buttons, extending existing accessibility-click handling.

Changes:

  • Tracks held confirm keys and activates native handlers.
  • Centers pointerless activations and handles them in both state-machine Pressable engines.
  • Adds keyboard flows and regression tests.
File Description
packages/​react-native-gesture-handler/​src/​v3/​components/​StatefulPressable.tsx Handles successful clicks without touches.
packages/​react-native-gesture-handler/​src/​components/​Pressable/​Pressable.tsx Adds equivalent legacy Pressable handling.
packages/​react-native-gesture-handler/​src/​__tests__/​pressableClickWithoutTouch.test.tsx Tests pointerless, touch, and accessibility activation.
packages/​react-native-gesture-handler/​android/​src/​main/​java/​com/​swmansion/​gesturehandler/​react/​RNGestureHandlerButtonViewManager.kt Tracks confirm keys and permits keyboard presses.
packages/​react-native-gesture-handler/​android/​src/​main/​java/​com/​swmansion/​gesturehandler/​core/​GestureHandlerOrchestrator.kt Centers pointerless native activations.
packages/​react-native-gesture-handler/​android/​src/​main/​java/​com/​swmansion/​gesturehandler/​core/​GestureHandler.kt Adds center-coordinate placement.
.argent/​flows/​keyboard-touchable-test.yaml Checks nested Touchable activation with Enter and Space.
.argent/​flows/​keyboard-pressable-test.yaml Checks focused nested Pressable activation with Enter.

Review effort: Balanced

Comment thread packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx Outdated
Comment thread packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx Outdated
@YevheniiKotyrlo YevheniiKotyrlo changed the title [Android] Press a focused button on a hardware Enter or Space [Android] Press buttons from hardware keys and accessibility services Oct 19, 2026
- ButtonViewGroup records a click's source while View's own onKeyDown, onKeyUp or performAccessibilityAction runs, admits that press while no child button is pressed, activates its native gesture as KEY or OTHER, and calls super.performClick(), which plays the click sound and sends TYPE_VIEW_CLICKED; receivedKeyEvent and the touch-exploration branch are gone.
- An activation no pointer drives reports no pointer and numberOfPointers 0, at the view's visible center mapped through every view's transform, the root view's included; makeActive asks React Native to cancel its touch only for an activation reporting a pointer, and activateNativeHandlersForView skips a disabled handler and takes POINTER_TYPE_OTHER by default.
- LegacyPressable and StatefulPressable take a native gesture reporting no pointer around their touch-based state machine, at the position it reports, while a tvOS focus press keeps the layout center; NativeViewGestureHandlerPayload declares the coordinates software-mansion#4302 emits.
- isKeyboardDismissingTap takes the press's pointer count, and StatefulPressable's long press clears a touch's keyboard verdict as it finalizes.
- The Keyboard activation example with keyboard-activation-test.yaml and keyboard-touchable-test.yaml, pressableClickWithoutTouch.test.tsx and the api_v3 cases.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/pressable-android-click-without-touch branch from 2c7700e to cd01505 Compare October 20, 2026 13:26
…sture's first touch

A key or an accessibility service's click pressed nothing while a finger
rested or a mouse hovered elsewhere in the root: the recorded handlers'
check from software-mansion#4127, which decides between handlers competing for the touch
being dispatched, cancelled the button's handler against the root's own.
The check now runs only inside touch dispatch, and `performClick` clicks
only for a press a handler took.

`makeActive` reads `hasTrackedPointers` instead of `numberOfPointers`,
which `handle()` sets after the touch-down callbacks, so a gesture the
state manager activates from its first `onTouchesDown` cancels React
Native's responder again. The Keyboard activation example and its flow
hold that case.

The native gesture's payload docs name each platform's position for a
press no pointer drives.
…e hit slop with none

A key or an accessibility service's click on a button a finger already
held ended the finger's gesture: both state-machine Pressables saw its
touches cancelled and pressed nothing, and the button kept the touch
responder, so every other exclusive button refused touches until it was
touched again. A native handler a pointer drives is now left to that
pointer, whose release presses, and the activation walks a snapshot of
the view's handlers taken under the registry's lock.

software-mansion#4127's check runs where a pointer's touch records a handler, rather
than wherever a handler is recorded during touch dispatch.

A handler takes its pointer type from the pointer that begins its
gesture, so one a second finger begins reports that finger's type
rather than the last gesture's, a key's included.

A key's press beside another button's touch moves the ripple to the
view's center, and LegacyPressable and the default Pressable press for
a press no pointer drives whatever their layout or hit slop, as
StatefulPressable does.

The native gesture's docs list its position fields with what each
platform reports with no pointer, and cancelsJSResponder's docs name the
activations that take no touch.
…and cancel the JS responder for every touch

A confirm key on a button a finger held unpressed it, since `View.onKeyUp`
unpresses a pressed view before it clicks: the ripple went out under the
finger, and the finger's release neither clicked, sounded nor reported
`TYPE_VIEW_CLICKED`. A press no pointer drives now shows only where its
click would activate a native gesture, and its release unpresses the
button only while no pointer presses it.

A key on a button whose touch waited on another gesture's failure
overwrote that touch's pointer count, type and position, which
`makeActive` dispatched once the other gesture failed. The activation now
leaves a handler in a gesture of its own to that gesture.

`makeActive` skipped the JS-responder cancel for every handler tracking
no pointer, so a touch handler activating after its pointers lifted left
React Native's Pressable beside it to press too. The handler now records
that an activation no pointer drives prepared it, and only that
activation skips the cancel.

The button reaches its root view's orchestrator directly, as
`requestDisallowInterceptTouchEvent` does, so the root view's and the
root helper's forwarding `activateNativeHandlers` go. The native
gesture's docs note the pointer count and type of a press no pointer
drives on Android and web.
…for a press its handler kept

A key's or an accessibility service's activation that waited on another
gesture dispatched its delayed ACTIVE and END with `pointerInside`
false, since the activation marked the handler in bounds only while it
ran, so a legacy BaseButton waiting on such a gesture never pressed. The
preparation now marks the pointer inside the view for the whole gesture,
and `withMarkedAsInBounds`, which nothing else called, goes.

The click sounded and reported `TYPE_VIEW_CLICKED` once the handler
began, even when a gesture it waits on cancelled it as it activated. A
press now counts as taken when its handler ends, waiting on another
gesture or not.

The press the button shows and the activation its click runs read one
selection of the view's native handlers.
…ouchless press at the view's center on Apple

On tvOS a focus Select press reaches the native gesture with no touch.
Apple's native view handler reported it at (0, 0) as a TOUCH pointer, so
the default Pressable dropped it at the hit-slop check and LegacyPressable
waited for a touch that never came; only StatefulPressable's TV branch
pressed, at the layout center.

isActivatedWithoutPointer now takes every press on a TV, as that branch
did, so all three engines go around the state machine and the hit-slop
check for it and press at the position the native gesture reports. Apple
reports a press with no touch at the view's center, as handleSwitch:
reports a switch's, and as KEY, as Android reports a key's press. The
docs and the payload type say a press no pointer drives reports the
view's center on every platform.

A click test case also pairs a screen reader with a hit slop that leaves
out the center, which fails if either state-machine engine lets the
screen reader's branch take a click with no pointer first.
…ort a touch's own pointers to an activation before it is handled

isActivatedWithoutPointer took every press on a TV around the touch state
machine and the hit-slop check, so on Android TV a mouse's press or a
touch-capable box's touch beginning in the press retention zone, or at a
center the hit slop leaves out, pressed in all three engines. On Android,
Android TV included, a press now goes around them only when it reports no
pointer; on tvOS, where nothing points at the screen, every press still
does.

The orchestrator dispatches a touch's down to the handler's touch
callbacks before handle() reads it, so a callback that activates the
handler from onTouchesDown on the UI thread dispatched the pointer count
and type the handler held from before: 0 and OTHER for its first touch,
0 and KEY after a key's press. updatePointerData now sets both from the
pointers the handler tracks before it dispatches the down. The Keyboard
activation example gains a RawButton whose touch activates it that way,
and the keyboard flow taps it after Enter.

The native gesture docs say a key's or an accessibility service's
activation reaches a Gesture Handler button's own native gesture only,
and what a tvOS Select press on a button reports.
… handled, and press an iOS press no touch drives in every engine

updatePointerData set the pointer count and type of a handler's down
before its touch callbacks ran, but not where the pointer is, so a
gesture its UI-thread onTouchesDown activates still reported the last
position and pointerInside the handler held: (0, 0) and outside on its
first touch, the view's center and inside after a key's press. handle()
reads the count, position, bounds, last position and pointer type
through helpers, and updatePointerData's down branch reads them through
the same helpers from the same adapted events before it dispatches the
touch. The example's touch-activated RawButton logs whether its
activation lands on its touch, and the keyboard flow taps it first on a
fresh screen.

Apple's native view handler reports a press no touch drives at the
view's center with no pointer on iPhone and iPad as well as on tvOS, and
the default Pressable pressed for it there while StatefulPressable and
LegacyPressable sent it into the touch state machine, which never
presses it. isActivatedWithoutPointer now takes such a press on iOS as
it does on Android; on tvOS every press still goes around the state
machine, and web, which reports no pointer once a press's pointers lift,
keeps its own path. The native gesture docs say iOS reports such a press
with no pointer and as a key.
Web drops a pointer from its tracker before its native gesture ends, so
every web press ends reporting no pointer, which isActivatedWithoutPointer
leaves to web's own path. A case pins it: LegacyPressable's web press
goes out where its press gesture saw the touch, not where its native
gesture ends.
A press's last event counts only the pointers still down: none once a
web press's pointers lift, and none on iOS for an action a button sends
with no event, as it sends a press's cancel when it is disabled under a
finger. LegacyPressable and StatefulPressable read
isActivatedWithoutPointer at their native gesture's onBegin and end the
press the way it began, as PressableWithTouchable keeps its press-in
verdict, so a touch press whose native gesture ends reporting no pointer
presses where the touch was. Read only at a press's first event, the
rule needs no platform but tvOS: web counts a key's press as a pointer,
so no web press begins without one.
…ycle a handler's adapted events on every exit

isKeyboardDismissingTap took a press with no pointer to be one counting
0 pointers, while the press path asks isActivatedWithoutPointer, which
takes every press on tvOS, so a tvOS press reporting a pointer went
around the state machine and was still dropped as the tap dismissing
the keyboard. It now takes the press and asks the same rule, in
StatefulPressable and in the Touchable under the default Pressable.

handle() returned from its shouldCancelWhenOutside failure without
recycling the events it adapted, as on main; it recycles them in a
finally, as the orchestrator releases its pooled lists. The
orchestrator's comment and the recording hook's KDoc say the
recorded-handler check runs where a pointer's touch records a handler,
and StatefulPressable's comment names iOS beside Android and tvOS.
@YevheniiKotyrlo YevheniiKotyrlo changed the title [Android] Press buttons from hardware keys and accessibility services [General] Press buttons from hardware keys, accessibility services and a TV's Select press Oct 21, 2026
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 21, 2026 23:20
@YevheniiKotyrlo
YevheniiKotyrlo requested a balanced review from Copilot October 23, 2026 19:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Shared native gesture arbitration and cross-platform press lifecycle changes warrant human review, particularly because tvOS lacks native build and device validation.

No open issues 🎉

3 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Shared native gesture-lifecycle changes and the acknowledged lack of tvOS build and device validation warrant final platform-owner review.

No open issues 🎉

Review effort: Balanced

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants