Skip to content

feat(core): add composeEventHandlers; forward consumer onClick in SegmentedControlItem#3863

Merged
cixzhang merged 1 commit into
mainfrom
feat/compose-event-handlers
Jul 12, 2026
Merged

feat(core): add composeEventHandlers; forward consumer onClick in SegmentedControlItem#3863
cixzhang merged 1 commit into
mainfrom
feat/compose-event-handlers

Conversation

@cixzhang

Copy link
Copy Markdown
Contributor

Summary

Two related changes to how components handle event handlers passed through ...rest.

1. SegmentedControlItem silently dropped a consumer onClick. It spreads {...rest} onto the <button> but then sets its own onClick={handleClick} after the spread, so any consumer-supplied onClick was overwritten and never fired. This is the same class of bug as #3738 / #3852 (props that type-check via BaseProps but never reach the DOM), except here the prop reached the element and was then clobbered.

2. Add composeEventHandlers. When a component owns an interaction (a click that selects, a keydown that drives arrow navigation) and accepts a consumer handler for the same event, it must compose them, not clobber one. That logic — call handlers in order, stop if one calls preventDefault() — was copy-pasted across Button, ClickableCard, Toolbar, and TabList with subtly different semantics. This adds one shared utility for it, alongside mergeProps / mergeRefs.

Change

  • packages/core/src/utils/composeEventHandlers.tscomposeEventHandlers(...handlers) returns a single handler that calls each in argument order and stops when one prevents default. Order is the precedence knob: consumer-first (the default here) lets a consumer preventDefault() to opt out of built-in behavior; component-first runs built-in behavior unconditionally.
  • SegmentedControlItem.tsx — destructure onClick: onClickProp out of ...rest and compose it with selection (composeEventHandlers(onClickProp, select)), consumer-first.
  • Exported from utils/index.ts (flows to the package root via the existing export * from './utils').

Precedence policy

This PR encodes the per-category rule now documented in the API Conventions wiki:

Prop category Policy
className / style / xstyle combine (mergeProps)
contract props (role, owned aria-*, computed id) component wins (set after {...rest})
handlers the component also implements compose (composeEventHandlers)
neutral pass-throughs (data-testid, other data-*) forward ({...rest})

Follow-up

Scoped intentionally small. A follow-up will migrate the existing hand-rolled composition sites (Button, ClickableCard, SelectableCard, Toolbar, TabList, Lightbox, ResizeHandle, …) onto composeEventHandlers once this lands.

Test plan

  • New composeEventHandlers unit tests (7): argument order, skips undefined, same event to each, stops after preventDefault, consumer opt-out, runs when not prevented, all-undefined is safe.
  • New SegmentedControlItem tests (2): a consumer onClick fires alongside selection; a consumer onClick that calls preventDefault() cancels selection.
  • pnpm -F @astryxdesign/core typecheck clean; SegmentedControl suite (39) + utils suite green; pnpm -F @astryxdesign/core build green; sync-exports --check clean. @astryxdesign/core patch changeset added.

…mentedControlItem

SegmentedControlItem spread {...rest} onto the button but then set its own
onClick after it, silently dropping any consumer onClick. It now composes the
consumer handler with selection: the consumer's runs first and can call
preventDefault() to opt out.

Add composeEventHandlers(...handlers) — chains handlers in argument order and
stops when one prevents default. This is the shared home for the
consumer-first / respect-defaultPrevented composition that was copy-pasted
across Button, ClickableCard, Toolbar, and TabList.
@cixzhang
cixzhang requested a review from imdreamrunner as a code owner July 12, 2026 06:04
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 12, 2026 6:06am

Request Review

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

@thedjpetersen thedjpetersen 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.

Approved.

github-actions Bot added a commit that referenced this pull request Jul 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@cixzhang
cixzhang merged commit a3a4823 into main Jul 12, 2026
20 checks passed
@github-actions
github-actions Bot deleted the feat/compose-event-handlers branch July 12, 2026 06:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants