Skip to content

fix(Item): the row's onClick ignores clicks from portals and role-based controls - #9393

Open
HelloOjasMutreja wants to merge 5 commits into
meta:mainfrom
HelloOjasMutreja:fix/item-click-foreign-targets
Open

HelloOjasMutreja wants to merge 5 commits into
meta:mainfrom
HelloOjasMutreja:fix/item-click-foreign-targets

Conversation

@HelloOjasMutreja

@HelloOjasMutreja HelloOjasMutreja commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

User impact

Anyone with a clickable Item or ListItem (one with onClick and no role) that holds another control in endContent. A role="tab", role="switch" or other role-based control in the row also runs the row's onClick, and so does a click in content the caller portals out of the row with its own createPortal.

Problem and solution fit

Why does it happen: handleContainerClick skipped only targets inside button, a, input, select, textarea, and never checked the target was inside the row. React bubbles a click through a portal to the component that rendered it, so a row in a caller-made portal reached the row's handler, and a div role="tab" matched none of the five selectors. Why does it harm: the row's primary action fires for a click meant for something else, which can navigate away or mutate data. Why does it matter: a row with a nested menu or switch is the ordinary shape of a list. The fix makes Item answer "does this click belong to something else" the way TreeListItem and the Table row plugins already do.

Expected behavior and authority

hasInteractiveAncestor and INTERACTIVE_SELECTORS in hooks/useClickableContainer.ts are the shared definition of "don't bubble this click to a clickable container", and TreeListItem uses them for the same job. The row's onClick should run only for a click whose target is inside the row's own DOM and not inside a nested control, native or role-based.

Smallest restoration

handleContainerClick in Item.tsx keeps its native check (closest('button, a, input, select, textarea')) and now also returns early when the target is outside e.currentTarget (a portal rendered from the row) or hasInteractiveAncestor(target, e.currentTarget) is true (a nested role-based control). No new API and no visual change.

Nothing else changes: a read-only input, and a row rooted on or nested in a link or button, stay inert exactly as before. Tooltip and HoverCard surface presses are stopped by the layer root now (#9531), so the row no longer reads the surface registry.

Evidence

  • Reproduction before the change: with the source reverted, the new portal test and the five role-based tests (tab, switch, menuitem, checkbox, option) fail because the row's onClick ran.
  • Result after the change: the same tests pass.
  • Representative unchanged path: a click on the row's label still runs onClick, and a click on a native button in endContent still does not (both tested).
  • Regression test or other mutation-sensitive proof: Item.test.tsx ("clicks that belong to something else").

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

npx vitest run over packages/core/src/Item, List, TreeList and Table, plus tsc --noEmit on packages/core, eslint and prettier via the pre-commit hook. The Item, List and TreeList suites pass. Table.perf.test.tsx fails on timing budgets in my environment with and without this change, and Item.swipeLayer.test.tsx failed once under parallel load and passed alone twice. Verified in jsdom only; the change is event-target logic with no layout or input-modality dependence.

Fixes #8977

@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 8, 2026 7:43am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 1, 2026
@astracat-bot

astracat-bot Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 8, 2026, 7:58 AM UTC

@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · b19b030

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
lab/TransferList 106.4 kB (+55 B) · 1.7 kB CSS (0) — — · — · —
lab/Comments 337.1 kB (+46 B) · 2.5 kB CSS (0) — — · — · —
Pagination 108.3 kB (+42 B) · 17.5 kB CSS (0) — — · — · —
23 more components
Component Size LoC Visual · A11y · RTL
CheckboxListItem 69.8 kB (+40 B) · 11.3 kB CSS (0) — — · — · —
ListItem 68.7 kB (+40 B) · 11.3 kB CSS (0) — — · — · —
Selector 98.2 kB (+37 B) · 17.1 kB CSS (0) — — · — · —
lab/TransferListSelector 113.1 kB (+37 B) · 1.9 kB CSS (0) — — · — · —
DropdownMenuSubMenu 69.0 kB (+35 B) · 9.7 kB CSS (0) — — · — · —
DropdownMenu 95.6 kB (+34 B) · 17.1 kB CSS (0) — — · — · —
Table 116.4 kB (+33 B) · 17.7 kB CSS (0) — — · — · —
richtext/RichTextEditorToolbar 250.8 kB (+33 B) · 454 B CSS (0) — — · — · —
PowerSearch 282.4 kB (+32 B) · 23.6 kB CSS (0) — — · — · —
MoreMenu 95.9 kB (+31 B) · 17.1 kB CSS (0) — — · — · —
RadioListItem 66.1 kB (+31 B) · 10.1 kB CSS (0) — — · — · —
DropdownMenuRadioItem 22.7 kB (+30 B) · 6.6 kB CSS (0) — — · — · —
lab/CommentThread 330.1 kB (+30 B) · 2.4 kB CSS (0) — — · — · —
ContextMenu 106.2 kB (+29 B) · 17.0 kB CSS (0) — — · — · —
TableHeaderCell 107.0 kB (+28 B) · 17.2 kB CSS (0) — — · — · —
Item · Storybook 17.4 kB (+24 B) · 5.5 kB CSS (0) 1,715 (+5) ✓ · ✓ · —
Markdown 179.5 kB (+24 B) · 20.0 kB CSS (0) — — · — · —
BreadcrumbItem 83.3 kB (+23 B) · 12.0 kB CSS (0) — — · — · —
TableCell 107.5 kB (+22 B) · 17.4 kB CSS (0) — — · — · —
SelectorOption 20.8 kB (+17 B) · 6.1 kB CSS (0) — — · — · —
ContextMenuItem 28.8 kB (+16 B) · 6.3 kB CSS (0) — — · — · —
DropdownMenuItem 21.9 kB (+16 B) · 6.3 kB CSS (0) — — · — · —
DropdownMenuCheckboxItem 22.6 kB (+15 B) · 6.6 kB CSS (0) — — · — · —

Package size: @astryxdesign/core 6.99 MB packed (+370 B); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 29 rows, core 35 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot 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.

Thanks: the fix works as described, so choosing Edit in a MoreMenu, or toggling a role="switch", inside a clickable Item or ListItem no longer also runs the row's onClick, and the new tests fail without it. Approval waits on an engineering owner, because no current spec says which clicks an Item row ignores: they need to approve or amend the four outcomes this PR ships (portal clicks and role-based controls no longer run onClick, while clicks on aria-readonly controls such as a read-only CheckboxInput, and clicks in a row rooted on or nested in <a>/<button>, now do). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Separately, please narrow the changeset: "or other row with onClick" also covers CheckboxListItem and RadioListItem, which this change does not touch.

[Automated review]

@HelloOjasMutreja

Copy link
Copy Markdown
Contributor Author

CI here stops at the ci:run gate (lint, test, build and docsite-test report the label message), so none of those checks has run on this head. Could a maintainer add the ci:run label so they do? Locally the Item, List, MoreMenu and TreeList suites, the typecheck, eslint and prettier pass.

@HelloOjasMutreja
HelloOjasMutreja force-pushed the fix/item-click-foreign-targets branch from 7be2487 to 6cb0f33 Compare November 2, 2026 16:42
@HelloOjasMutreja

Copy link
Copy Markdown
Contributor Author

Pushed 6cb0f33 (rebased onto main, which had a conflicting Item refactor in #9531).

  • Outcomes: the two you flagged as new are gone. The row keeps its native check (closest('button, a, input, select, textarea'), which still sees a read-only input and a row inside a link or button), and the portal and role-based checks sit on top of it. A new test covers a read-only checkbox input in endContent. What the change ships is now just the two things the issue asks for: a click inside a portal rendered from the row, and a click on a nested role-based control, no longer run the row's onClick.
  • Changeset: it names Item and ListItem only.

The Item, List, MoreMenu and Tooltip suites pass (287 tests). The spec decision on which clicks an Item row ignores is still for an engineering owner.

@astracat-bot astracat-bot Bot 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.

Thanks for narrowing this: the read-only input and link-row side effects are gone, and a click on a role-based control in a clickable Item or ListItem (such as a div role="tab") no longer also runs the row's onClick. Approval still waits on an engineering owner, because no current spec says which clicks an Item row ignores: they need to approve or amend the two outcomes this PR ships, clicks in content the caller portals from the row with its own createPortal and clicks on nested role-based controls. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Separately, please drop the MoreMenu example from the changeset, the description and the code comment: on main a click in a MoreMenu's menu already stops at the menu's layer root and never reaches the row (the new MoreMenu test passes without this change), so the changeset should name only caller-made portals and role-based controls.

[Automated review]

Comment thread packages/core/src/Item/Item.tsx Resolved
Comment on lines +774 to +775
// A native control owns the click, as it always has (this also keeps a
// read-only input and a row inside a link or button inert).

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.

Non-blocking: "as it always has" narrates history. Suggest keeping only the reason: "This check also looks above the row. It keeps a row inside a link or button inert, and a read-only input, which the shared walk skips."

@HelloOjasMutreja

Copy link
Copy Markdown
Contributor Author

Done in e474c97: the MoreMenu example is gone from the changeset, the description and the code comment, which now name only content the caller portals out of the row with its own createPortal and role-based controls. I also removed the MoreMenu.test.tsx case, since it passes on main and so proved nothing about this change. The /approve-spec decision on the two remaining outcomes is still the open item.

@astracat-bot astracat-bot Bot 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.

Thanks for dropping the MoreMenu example: the changeset, description and comment now name only caller-made portals and role-based controls. Approval still waits on an engineering owner, because no current spec says which clicks an Item row ignores: they need to approve or amend the two outcomes this PR ships, where clicks in content the caller portals from the row with its own createPortal, and clicks on nested role-based controls such as role="tab", no longer run the row's onClick. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner.

Please also fix:

  • The changeset and the description's User impact say "a clickable Item or ListItem (one with onClick)", but a row with a role (such as role="listitem" or "option") puts onClick straight on its root and still runs it for a nested role-based control or portal content, so narrow the scope to "(one with onClick and no role)".

[Automated review]

@HelloOjasMutreja

Copy link
Copy Markdown
Contributor Author

Narrowed in the changeset and the description's User impact to "(one with onClick and no role)". You are right: with a role, onClick goes straight on the root (hasParentRole ? onClick in Item.tsx), so this change does not filter those rows. The /approve-spec decision on the two outcomes is still the open item.

@astracat-bot astracat-bot Bot 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.

Thanks for narrowing the changeset to rows with no role. Approval still waits on an engineering owner, because no current spec says which clicks an Item row ignores: they need to approve or amend the two outcomes this PR ships, where clicks in content the caller portals from the row with its own createPortal, and clicks on nested role-based controls such as role="tab", no longer run the row's onClick. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner.

Please also fix:

  • The changeset and the description's User impact promise the fix for "the rest of the shared interactive list", but a pointer click on an Astryx Slider thumb in a clickable row still runs the row's onClick (the Slider captures the pointer on its track, so the click lands outside role="slider"), so name only the roles the tests prove, for example "…or on a nested element with role="tab", "switch", "menuitem", "checkbox" or "option" that receives the click".

[Automated review]

'@astryxdesign/core': patch
---

[fix] A clickable Item or ListItem (one with `onClick` and no `role`) no longer runs it for a click that belongs to something else: a click inside content the caller portals out of the row with its own `createPortal`, or on a role-based control (`role="tab"`, `"switch"`, `"menuitem"` and the rest of the shared interactive list) nested in the row. (#8977)

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.

"and the rest of the shared interactive list" also covers role="slider", but a pointer click on an Astryx Slider thumb in a clickable row still runs the row's onClick: the Slider captures the pointer on its track, so the click lands on the track, outside role="slider". Suggest naming only what the tests prove: "…or on a nested element with role="tab", "switch", "menuitem", "checkbox" or "option" that receives the click."

@HelloOjasMutreja
HelloOjasMutreja force-pushed the fix/item-click-foreign-targets branch from d0de0eb to 2f201b7 Compare November 4, 2026 17:29
@HelloOjasMutreja

Copy link
Copy Markdown
Contributor Author

Rebased onto current main, which conflicted in Item.tsx: #9959 added cancelNestedTouchPress to the same import block, so the import list keeps it beside hasInteractiveAncestor. Nothing else changed. The diff against main is still the changeset, the three-condition check in handleContainerClick and its test.

The Item and MoreMenu suites pass (138 tests), and typecheck and strict lint are clean. Approval still waits on an engineering owner for the Item click scope.

@astracat-bot astracat-bot Bot 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.

Thanks for the rebase; approval still waits on an engineering owner, because no current spec says which clicks an Item row's onClick ignores.

They need to approve or amend the two outcomes this PR ships: a click on a nested role-based control such as role="tab", and a click in content the caller portals from the row with its own createPortal, no longer run the row's onClick (component:Item FR9 covers only the touch press paint). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner.

The earlier request to narrow the changeset's Slider wording no longer applies: on current main a click on a Slider thumb lands on its role="slider" element, so the row ignores it as the changeset says.

[Automated review]

…ed controls

handleContainerClick skipped only button, a, input, select and textarea, and did
not check that the target was inside the row. A click on a MoreMenu row in
endContent bubbled through the portal, and a click on a role="tab" or
role="switch" control ran the row's onClick too. It now asks the shared
hasInteractiveAncestor walk (the rule TreeListItem and the Table row plugins
use) and ignores a target outside the row's own DOM.

Fixes meta#8977
…and ListItem

The row's click handler kept its native check (button, a, input, select,
textarea, looked up through the ancestors) and adds the portal and role-based
checks on top, so a read-only input, or a row inside a link or button, stays
inert as before. The changeset names Item and ListItem only.
@HelloOjasMutreja
HelloOjasMutreja force-pushed the fix/item-click-foreign-targets branch from 2f201b7 to 83ffa3a Compare November 5, 2026 08:36
@HelloOjasMutreja

Copy link
Copy Markdown
Contributor Author

Rebased onto current main, which conflicted again in Item.tsx and Item.test.tsx. #8085 moved the row's click wiring to useClickableContainerProxy and added PRESSABLE_CONTAINER_ATTRIBUTE to the same import block, and it added Activity to the test's react import. The imports keep both sides, with hasInteractiveAncestor and createPortal added back. The three-condition check in handleContainerClick is unchanged and still sits where main calls it. The diff against main is the same three files.

The Item, MoreMenu and List suites pass (234 tests), and typecheck and strict lint are clean. Approval still waits on an engineering owner for the Item click scope.

@astracat-bot astracat-bot Bot 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.

Thanks for the rebase; approval still waits on an engineering owner, because no current spec says which clicks an Item row's onClick ignores.

They need to approve or amend the two outcomes this PR ships for a row with onClick and no role, mouse included: a click on a nested role-based control such as role="tab", and a click in content the caller portals from the row with its own createPortal, no longer run the row's onClick (component:Item FR9 covers only the touch press paint). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so if they are an engineering owner); citing in the description the record line this restores, with mouse before-and-after results, lets that ruling cover the mouse change too.

Please also fix:

  • Regression: since the rebase onto main's List touch-target change, the containment check (Item.tsx:831) also stops content a List link row (ListItem with href) portals out from following the row's link, which restores component:List FR10 for a mouse but has no test that fails on main, so please add one beside the linkRow() cases in Item.test.tsx and name List link rows in the changeset.

[Automated review]

if (
// React bubbles a click through a portal the caller renders from the
// row (its own `createPortal`), but its target is not in the row.
!e.currentTarget.contains(target) ||

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.

Main now sends a List link row's surface click through this guard, so this line also stops content a ListItem with href portals out from following the row's link: a mouse click on such content below the row followed it on main. That restores component:List FR10 ("A fine pointer keeps today's hit target"), but no test fails on main for it. Please add one beside the linkRow() cases: a portaled endContent, a fireEvent.click on it outside the row's box with detail: 1, and a coarse-pointer tap, none of which should follow the anchor. Then name List link rows in the changeset.

@astracat-bot astracat-bot Bot 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.

Thanks for keeping this current with main; approval still waits on an engineering owner, because no current spec says which clicks an Item row's onClick ignores.

They need to approve or amend the two outcomes this PR ships for a row with onClick and no role, mouse included: a click on a nested role-based control such as role="tab", and a click in content the caller portals from the row with its own createPortal, no longer run the row's onClick (component:Item FR9 covers only the touch press paint). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so if they are an engineering owner); citing in the description the record line this restores, with mouse before-and-after results, lets that ruling also cover the mouse change.

Please also fix:

  • Regression: the containment check (Item.tsx:832) also stops content a List link row (ListItem with href) portals out from following the row's link, which restores component:List FR10 for a mouse but has no test that fails on main, so please add one beside the linkRow() cases in Item.test.tsx and name List link rows in the changeset.
  • Desktop behavior change needs an owner decision: a mouse click on content the caller portals from an onClick row, or on a nested role-based control in it, no longer runs the row's onClick (Item.tsx:832, Item.tsx:837); limit the change to touch input or cite the owner decision that covers it.

[Automated review]

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 8, 2026
@cixzhang cixzhang closed this Nov 8, 2026
@cixzhang cixzhang reopened this Nov 8, 2026

This branch was successfully deployed

1 active deployment
Preview — b19b0302 Deployed Nov 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] ListItem: the row's onClick runs for clicks in its portals and on role-based controls

2 participants