Skip to content

fix(native): don't make every container a Pressable - #482

Open
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/container-touch-observation
Open

YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/container-touch-observation

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Every element that declares a container — a container-name box, and every group a descendant selector names — renders as a Pressable, whatever the selector asks of it. updateRules wires the press family on each one:

// src/native/react/rules.ts
if (rule.c) {
  containers ??= inheritedContainers;
  activeFamily(state.ruleEffectGetter);
}

getStyledProps answers activeFamily with onPress / onPressIn / onPressOut, and useNativeCss swaps a View that has an onPress for a Pressable. So the container:

  • takes the JS responder for any touch no deeper view takes, and on Android JSResponderHandler then intercepts the rest of the touch, so a native ScrollView beneath it receives ACTION_CANCEL;
  • is announced as a control: Pressable defaults accessible and focusable to true, so a bare <View className="group/item"> dumps as clickable="true" focusable="true" and is a keyboard focus stop.

Any class that registers a container triggers it — @container/name, group, group/name — including a group that is only ever asked about an attribute (group-data-[state=on]:).

Solution

A container's :active, as its queries read it, gets its own family, containerActiveFamily, and nothing adds press handlers for it.

  • A container observes the touches its subtree receives through onTouchStart / onTouchEnd / onTouchCancel. They bubble to every ancestor without a responder claim, so nothing beneath it loses a touch, and a native scroller that takes the gesture ends the state through its touchcancel. It follows the touches that went down inside it and is active until the last of them ends; the touches still on screen prune that set on every event.
  • A container that has its own onPress answers through that press, as before, so its own active: and its descendants' group-active: change together.
  • testContainerPseudoCondition reads containerActiveFamily.

An element's own :active rule still makes it a Pressable. What changes for a plain group is that pressing a control inside it now activates the group, as a browser does: :active matches the flat-tree ancestors of the element being activated. Before, the control won the responder and the group never saw the press.

Tests

src/__tests__/native/container-activity.test.tsx renders containers and reads the host element's onStartShouldSetResponder — the prop a Pressable adds to take part in responder negotiation — then fires the touch events a container now answers from, carrying the touches / changedTouches React Native delivers. grouping.test.tsx's two :active cases fire touches instead of presses.

On main, 8 of those 13 cases fail: all three containers carry onStartShouldSetResponder, and no touch reaches a group's :active. The cases that pass on main are the ones that must not change: a group with its own onPress, and a touch that went down elsewhere, or whose end never arrived, keeping nothing active.

Verification

On Ubuntu with the .nvmrc Node, every pull-request job but the macOS build: yarn build · yarn lint 0 · yarn typecheck 0 · yarn test --maxWorkers=2 --coverage 1344 passed · node --test node/__tests__/*.mjs 42 passed · yarn example expo export --platform web · no unstaged files after either build · commitlint clean. I did not run the iOS dev build.

On an Android 16 emulator, the example app's ScrollView inside an @container view does not move under a drag on main and scrolls with this PR, landing on the same row as the same list inside a plain view; the pair is in this comment.

No existing issue tracks this — searched the tracker for group pressable, container pressable, group onPress, group clickable, group-active, container scroll, group scroll, responder, touch cancel and Pressable container, with no relevant hits.

Known limits

A touch whose view leaves the screen before the finger lifts never reaches JS — Fabric drops the event — so the containers it went down in stay active until the next touch reaches them.

Related

Base

Cut from a5002c5 (main); it merges cleanly.

…king it pressable

Registering a container — a size container from `container-name`, or a group
a descendant selector names — wired `activeFamily` on the element, and that
family's presence is what adds `onPress` and turns a `View` into a
`Pressable`. So every container became a touch target: it claimed the JS
responder, Android's `JSResponderHandler` then intercepted the touch and
cancelled the native gestures of everything beneath it, and the element was
announced as clickable and focusable.

A container now tracks its `:active` in `containerActiveFamily`, which its
queries read and which never adds press handlers. A container already pressed
for its own reasons answers through that press; any other observes the touch
events its subtree receives, which reach every ancestor without a responder
claim.
…ends

A container followed one boolean, so with two fingers inside a group,
lifting one ended its `:active` while the other still rested there.

Each container now follows the touches that went down inside it, and is
active until the last of them ends. The touches still on screen prune that
set on every event, so a touch that went down elsewhere never keeps it
active, and a touch whose end never arrived — its target left the screen
first — is dropped at the next touch event.
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — a ScrollView inside an @container view does not move under a drag, while the same ScrollView inside a plain view does.

FIXED — the same drag scrolls both lists to the same row.

before — main (a5002c5) after — with this PR

The example app as a release build on an Android 16 (API 36) emulator, 1140×2400 at density 480, with an identical 4-second drag on each list. The lower list is the control and is pixel-identical in both captures; uiautomator reports the @container view as clickable="true" focusable="true" on main and as neither with this PR.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 13, 2026 09:26

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.

1 participant