From 4f8fcc6edfde573d98bc506abdaa5242b88600ff Mon Sep 17 00:00:00 2001 From: Josh Black Date: Fri, 31 Jul 2026 13:17:04 -0500 Subject: [PATCH 1/2] Add merge props migration driver Refine the adoption report, add validated migration examples, and introduce the coordinator skill and bounded batch agent for ADR-025. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 00bfd295-6ed9-4119-8d63-103a0b3c63e8 --- .changeset/clean-radios-merge.md | 5 + .changeset/tidy-token-events.md | 5 + .changeset/visible-class-merging.md | 5 + .../migrate-to-merge-props-batch.agent.md | 68 ++++++ .../skills/migrate-to-merge-props/SKILL.md | 207 ++++++++++++++++++ packages/react/src/Radio/Radio.test.tsx | 1 + packages/react/src/Radio/Radio.tsx | 32 ++- packages/react/src/Radio/Radio.types.test.tsx | 6 + packages/react/src/Token/TokenBase.tsx | 54 +++-- .../react/src/Token/__tests__/Token.test.tsx | 11 + .../src/VisuallyHidden/VisuallyHidden.tsx | 10 +- 11 files changed, 362 insertions(+), 42 deletions(-) create mode 100644 .changeset/clean-radios-merge.md create mode 100644 .changeset/tidy-token-events.md create mode 100644 .changeset/visible-class-merging.md create mode 100644 .github/agents/migrate-to-merge-props-batch.agent.md create mode 100644 .github/skills/migrate-to-merge-props/SKILL.md create mode 100644 packages/react/src/Radio/Radio.types.test.tsx diff --git a/.changeset/clean-radios-merge.md b/.changeset/clean-radios-merge.md new file mode 100644 index 00000000000..5ddb7fd741d --- /dev/null +++ b/.changeset/clean-radios-merge.md @@ -0,0 +1,5 @@ +--- +'@primer/react': major +--- + +Radio: Prevent overriding the input type so the component always renders radio semantics. diff --git a/.changeset/tidy-token-events.md b/.changeset/tidy-token-events.md new file mode 100644 index 00000000000..7da0e9d015b --- /dev/null +++ b/.changeset/tidy-token-events.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Token: Run removal keyboard behavior before consumer keydown handlers. diff --git a/.changeset/visible-class-merging.md b/.changeset/visible-class-merging.md new file mode 100644 index 00000000000..4c07d423a18 --- /dev/null +++ b/.changeset/visible-class-merging.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +VisuallyHidden: Merge consumer class names with component styles consistently. diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md new file mode 100644 index 00000000000..375d777ce30 --- /dev/null +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -0,0 +1,68 @@ +--- +name: migrate-to-merge-props-batch +description: Performs one bounded ADR-025 mergeProps migration batch and returns validation and migration-report evidence to the coordinator. +tools: + - read + - search + - edit + - execute +skills: + - migrate-to-merge-props + - style-guide + - changesets + - storybook +--- + +You implement exactly one batch assigned by the `migrate-to-merge-props` +coordinator. Use that skill for the ADR, validated patterns, scope, stack +contract, and validation requirements. + +## Required input + +Do not begin without: + +- The current stack branch. +- Exact component names, file paths, and report locations. +- The maximum batch size. +- Total and affected-file counts before the batch. +- Targeted validation commands. + +Confirm the branch and findings still match the assignment. Stop if the batch +is too large, overlaps another entry, includes unassigned files, or requires an +unresolved architecture, accessibility, or public API decision. + +## Workflow + +1. Read ADR-025, the repository instructions, `mergeProps`, and the validated + production examples named by the coordinator skill. +2. Confirm every assigned finding still appears in the report. +3. Inspect public props, spread order, handlers, styles, class names, refs, + invariant attributes, tests, stories, and nearby prior art. +4. Implement only the assigned findings and tightly coupled tests, type tests, + or changesets. +5. Pass component props first and consumer props second. +6. Keep component class names as strings. +7. Omit absent optional consumer handlers from the second argument. +8. Compose refs separately. +9. Omit or gate invariant attributes in public types and apply them after the + merged spread. +10. Format, lint, test, type-check, and run any requested Storybook, + accessibility, visual, or build validation. +11. Regenerate the report and confirm the assigned findings disappeared with + no new findings. + +Do not disable enforcement, add blanket suppressions, manipulate report output, +expose `mergeProps` publicly, fix unrelated findings, select another batch, +create stack entries, commit, or change stack structure. + +## Result format + +| Field | Content | +| ------------- | -------------------------------------------------------------------- | +| Branch | Current stack branch | +| Assigned | Component names, paths, and original locations | +| Resolved | Findings removed from the regenerated report | +| Files changed | Source, tests, types, stories, and changesets edited | +| Remaining | Updated finding and affected-file counts | +| Validation | Failed commands and relevant summaries, or `Passed` | +| Blockers | Findings requiring an ADR, API, accessibility, or ownership decision | diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md new file mode 100644 index 00000000000..e7fb7b75593 --- /dev/null +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -0,0 +1,207 @@ +--- +name: migrate-to-merge-props +description: 'Use when: coordinating ADR-025 adoption through measured mergeProps batches in a stacked migration. Plans bounded batches, invokes migrate-to-merge-props-batch, verifies report progress, and removes temporary migration tooling at completion.' +--- + +# Migrate to mergeProps + +Coordinate adoption of +[`ADR-025: Prop merging conventions`](../../../contributor-docs/adrs/adr-025-prop-merging.md). + +ADR-025 is the source of truth for correctness. The ESLint rule and migration +report identify likely work, but a finding is not complete until the component +preserves its behavior, accessibility contract, public types, refs, handlers, +class names, and styles according to the ADR. + +## Sources of truth and migration artifacts + +| Artifact | Location | Role | Lifetime | +| ----------------- | ------------------------------------------------------------------------------------- | ----------------------------------------- | --------- | +| ADR | `contributor-docs/adrs/adr-025-prop-merging.md` | Defines the target prop-merging contract | Permanent | +| Style guidance | `contributor-docs/style.md`, "Merge shared props intentionally" | Shows the component-authoring convention | Permanent | +| Utility | `packages/react/src/utils/mergeProps.ts` | Implements shared merge behavior | Permanent | +| Adoption rule | `primer/prefer-merge-props` in `packages/eslint-config/src/rules/preferMergeProps.ts` | Detects unmerged component root props | Permanent | +| Migration report | `node script/merge-props-migration-status.mts` | Measures remaining findings | Temporary | +| Batch agent | `.github/agents/migrate-to-merge-props-batch.agent.md` | Implements one assigned migration batch | Temporary | +| Coordinator skill | `.github/skills/migrate-to-merge-props/SKILL.md` | Plans, delegates, measures, and cleans up | Temporary | + +## Scope + +- Included: findings reported in `packages/react/src/**/*.{ts,tsx}` by + `primer/prefer-merge-props`. +- Excluded: tests, stories, nested JSX, generated output, `.agents/**/*`, and + pure pass-through roots that have one spread and no component-authored prop + other than `ref` or `key`. +- Refs are always composed separately with `useMergedRefs`. +- Default maximum batch size: five findings. +- Default run size: two sequential batches per invocation. +- Never split one source file across separate batches. + +## Validated migration patterns + +Use these production examples as prior art: + +- `packages/react/src/VisuallyHidden/VisuallyHidden.tsx`: class-name-only + merging. +- `packages/react/src/Token/TokenBase.tsx`: internal behavior first, optional + consumer event handler second, and ref composition outside `mergeProps`. +- `packages/react/src/Radio/Radio.tsx`: a component-owned attribute omitted + from the public type and applied after merged props. + +Apply these rules: + +1. Pass component-authored props first and consumer-authored props second. +2. Keep the component-authored `className` as a string. Let `mergeProps` use + `clsx` to combine it with consumer class values. +3. Do not add an optional consumer handler to the second argument when it is + absent. An explicit `undefined` entry would replace the internal handler + instead of composing it. +4. Keep refs outside `mergeProps`. +5. Put invariant attributes after the merged spread and omit them from the + public prop type. Use a discriminated union when ownership depends on the + rendered element. +6. Preserve intentional consumer `undefined` precedence for ordinary props. +7. Add tests for event order, cancellation, controlled attributes, type + exclusions, or other behavior that is not already covered. + +## Stack contract + +Use one linear stack: + +1. `copilot/update-style-docs-prop-merging`: ADR, utility, adoption rule, and + report foundation. +2. `migrate/merge-props-migrator`: validated examples, tooling refinements, + coordinator skill, and batch agent. +3. `migrate/merge-props-batch-01` through + `migrate/merge-props-batch-NN`: one bounded migration batch per entry. +4. `migrate/merge-props-cleanup`: temporary artifact removal after the report + reaches zero. + +Use only non-interactive stack commands: + +```shell +gh stack view --json +gh stack add migrate/merge-props-batch-NN +gh stack submit --auto +``` + +Do not create the cleanup entry while in-scope findings remain. + +## Coordinator workflow + +### 1. Resume the stack + +Inspect: + +```shell +git status --short --branch +gh stack view --json +``` + +Confirm the current branch is the top active entry. If a lower entry changed, +rebase the upstack before selecting findings. Do not mix unrelated worktree +changes into a migration entry. + +### 2. Establish the baseline + +Run: + +```shell +node script/merge-props-migration-status.mts +``` + +Record the total finding count, affected-file count, exact paths and locations, +findings already assigned to stack entries, and blockers. Do not edit generated +output or exclusions to lower the count. + +### 3. Plan non-overlapping batches + +Group findings by component family and remediation pattern. Each batch must: + +- Contain at most the selected finding limit. +- Own complete files rather than splitting a file between entries. +- Be reviewable as one coherent change. +- Avoid files already assigned to an existing stack entry. +- Include tightly coupled tests and changesets. + +Calculate the remaining batch count using the selected limit. + +### 4. Ask how much work to run + +Use a structured prompt containing: + +| Field | Default | Constraint | +| ----------- | ------- | ---------------------------------------- | +| Batch size | `5` | Integer greater than or equal to `1` | +| Run mode | Limited | Limited batches or all remaining batches | +| Batch count | `2` | Integer greater than or equal to `1` | + +Show the baseline, calculated remaining batches, and exact assignments before +editing. Prefer a small set of sequential batches over running the full +migration at once. + +### 5. Run each selected batch + +For each batch: + +1. Create `migrate/merge-props-batch-NN` with `gh stack add`. +2. Invoke `migrate-to-merge-props-batch` with the branch, exact paths and + locations, maximum size, before counts, and targeted validation commands. +3. Review the returned diff and evidence. +4. Add or update changesets for consumer-facing behavior or type changes. +5. Commit only the assigned batch. +6. Regenerate the report. +7. Confirm assigned findings disappeared, no new findings appeared, and counts + changed by the expected amount. +8. Stop before another entry if validation or report evidence fails. + +Run code-editing batch agents sequentially because every branch depends on the +previous branch. + +### 6. Handle blockers + +Do not force a mechanical merge when a finding requires a decision about: + +- Consumer veto before internal behavior. +- Whether an ARIA, semantic, or stable identifier prop is component-owned. +- A breaking public type change. +- Polymorphic attribute ownership. +- Ref composition. + +Return the file, component, current behavior, ADR conflict, and smallest +decision needed. + +### 7. Finish or pause + +In limited mode, stop after the selected number of entries and leave the stack +resumable. + +When the report reaches zero, add `migrate/merge-props-cleanup` and remove: + +- `.github/skills/migrate-to-merge-props/` +- `.github/agents/migrate-to-merge-props-batch.agent.md` +- `script/merge-props-migration-status.mts` +- The `merge-props` job from `.github/workflows/migration-status.yml` + +Keep ADR-025, style guidance, `mergeProps`, the ESLint rule, and regression +tests. Mark ADR-025 adopted and implemented in the cleanup entry after final +validation. + +### 8. Submit and report + +Run: + +```shell +gh stack submit --auto +gh stack view --json +``` + +Return completed stack entries, requested/completed/remaining batches, resolved +paths, updated counts, validation failures, and blockers. Do not claim +completion before the report is zero and the cleanup entry exists. + +## Example invocation + +> Use `/migrate-to-merge-props` with at most five findings per entry. Run two +> batches, regenerate the report after each entry, and stop on unexpected +> results. diff --git a/packages/react/src/Radio/Radio.test.tsx b/packages/react/src/Radio/Radio.test.tsx index 1a7b9756dcd..d20071927db 100644 --- a/packages/react/src/Radio/Radio.test.tsx +++ b/packages/react/src/Radio/Radio.test.tsx @@ -21,6 +21,7 @@ describe('Radio', () => { const radio = getByRole('radio') expect(radio).toBeDefined() + expect(radio).toHaveAttribute('type', 'radio') }) it('renders data-component attribute', () => { diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx index 5ea79439a9f..d14e073f96e 100644 --- a/packages/react/src/Radio/Radio.tsx +++ b/packages/react/src/Radio/Radio.tsx @@ -5,6 +5,7 @@ import {clsx} from 'clsx' import sharedClasses from '../Checkbox/shared.module.css' import classes from './Radio.module.css' import type {WithSlotMarker} from '../utils/types' +import {mergeProps} from '../utils/mergeProps' export type RadioProps = { /** @@ -32,7 +33,7 @@ export type RadioProps = { * Indicates whether the radio button must be checked before the form can be submitted */ required?: boolean -} & InputHTMLAttributes +} & Omit, 'type'> /** * An accessible, native radio component for selecting one option from a list. @@ -56,7 +57,6 @@ const Radio = React.forwardRef( const radioGroupContext = useContext(RadioGroupContext) const handleOnChange: ChangeEventHandler = e => { radioGroupContext?.onChange && radioGroupContext.onChange(e) - onChange && onChange(e) } const name = nameProp || radioGroupContext?.name @@ -69,17 +69,25 @@ const Radio = React.forwardRef( return ( ) diff --git a/packages/react/src/Radio/Radio.types.test.tsx b/packages/react/src/Radio/Radio.types.test.tsx new file mode 100644 index 00000000000..a08856d1c1e --- /dev/null +++ b/packages/react/src/Radio/Radio.types.test.tsx @@ -0,0 +1,6 @@ +import Radio from '.' + +export function doesNotAcceptType() { + // @ts-expect-error Radio always renders an input with type="radio" + return +} diff --git a/packages/react/src/Token/TokenBase.tsx b/packages/react/src/Token/TokenBase.tsx index 03202c6f0cb..17025fc667f 100644 --- a/packages/react/src/Token/TokenBase.tsx +++ b/packages/react/src/Token/TokenBase.tsx @@ -1,10 +1,10 @@ import type {KeyboardEvent} from 'react' import React from 'react' -import {clsx} from 'clsx' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './TokenBase.module.css' import {defaultTokenSize, type TokenSizeKeys} from './constants' import {isTokenInteractive} from './utils' +import {mergeProps} from '../utils/mergeProps' export type {TokenSizeKeys} @@ -55,30 +55,38 @@ const TokenBase = React.forwardRef { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { + onRemove() + } + } + return ( ) => { - onKeyDown && onKeyDown(event) - - if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { - onRemove() - } - }} - className={clsx(classes.TokenBase, className)} - data-cursor-is-interactive={isTokenInteractive({ - as: Component, - onClick: rest.onClick, - onFocus: rest.onFocus, - tabIndex: rest.tabIndex, - disabled: rest.disabled, - })} - data-size={size} - id={id?.toString()} - {...(Component === 'button' - ? (rest as React.ButtonHTMLAttributes) - : Component === 'a' - ? (rest as React.AnchorHTMLAttributes) - : (rest as React.HTMLAttributes))} + {...mergeProps( + { + onKeyDown: handleKeyDown, + className: classes.TokenBase, + 'data-cursor-is-interactive': isTokenInteractive({ + as: Component, + onClick: rest.onClick, + onFocus: rest.onFocus, + tabIndex: rest.tabIndex, + disabled: rest.disabled, + }), + 'data-size': size, + }, + { + ...(Component === 'button' + ? (rest as React.ButtonHTMLAttributes) + : Component === 'a' + ? (rest as React.AnchorHTMLAttributes) + : (rest as React.HTMLAttributes)), + ...(onKeyDown ? {onKeyDown} : {}), + className, + id: id?.toString(), + }, + )} // TypeScript cannot resolve polymorphic ref types at compile time // This assertion is safe because the ref will match the actual rendered element // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/react/src/Token/__tests__/Token.test.tsx b/packages/react/src/Token/__tests__/Token.test.tsx index 73c01124c2a..ac07bbe22fe 100644 --- a/packages/react/src/Token/__tests__/Token.test.tsx +++ b/packages/react/src/Token/__tests__/Token.test.tsx @@ -63,6 +63,17 @@ const testTokenComponent = (Component: React.ComponentType { + const calls: string[] = [] + const {getByText} = HTMLRender( + calls.push('remove')} onKeyDown={() => calls.push('consumer')} />, + ) + + fireEvent.keyDown(getByText('token'), {key: 'Backspace'}) + + expect(calls).toEqual(['remove', 'consumer']) + }) + it('adds className to rendered component', () => { const {getByText} = HTMLRender() const domNode = getByText('token') diff --git a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx index d05c496352f..d2b8231d952 100644 --- a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx +++ b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx @@ -1,6 +1,6 @@ -import {clsx} from 'clsx' import type React from 'react' import {type HTMLAttributes} from 'react' +import {mergeProps} from '../utils/mergeProps' import classes from './VisuallyHidden.module.css' /** @@ -13,12 +13,8 @@ import classes from './VisuallyHidden.module.css' * * @see https://www.scottohara.me/blog/2023/03/21/visually-hidden-hack.html */ -export const VisuallyHidden = ({className, children, ...rest}: VisuallyHiddenProps) => { - return ( - - {children} - - ) +export const VisuallyHidden = ({children, ...rest}: VisuallyHiddenProps) => { + return {children} } export type VisuallyHiddenProps = React.PropsWithChildren< From 2dd85ec7a3e38eb2907141c0052b89988404d942 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Fri, 31 Jul 2026 13:28:21 -0500 Subject: [PATCH 2/2] Keep migration driver focused on coordination Move implementation and adoption-tool changes out of the driver entry and document stack ownership for foundation, driver, batch, and cleanup work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 00bfd295-6ed9-4119-8d63-103a0b3c63e8 --- .changeset/clean-radios-merge.md | 5 -- .changeset/tidy-token-events.md | 5 -- .changeset/visible-class-merging.md | 5 -- .../migrate-to-merge-props-batch.agent.md | 9 ++- .../skills/migrate-to-merge-props/SKILL.md | 67 ++++++++++++++----- packages/react/src/Radio/Radio.test.tsx | 1 - packages/react/src/Radio/Radio.tsx | 32 ++++----- packages/react/src/Radio/Radio.types.test.tsx | 6 -- packages/react/src/Token/TokenBase.tsx | 54 +++++++-------- .../react/src/Token/__tests__/Token.test.tsx | 11 --- .../src/VisuallyHidden/VisuallyHidden.tsx | 10 ++- 11 files changed, 98 insertions(+), 107 deletions(-) delete mode 100644 .changeset/clean-radios-merge.md delete mode 100644 .changeset/tidy-token-events.md delete mode 100644 .changeset/visible-class-merging.md delete mode 100644 packages/react/src/Radio/Radio.types.test.tsx diff --git a/.changeset/clean-radios-merge.md b/.changeset/clean-radios-merge.md deleted file mode 100644 index 5ddb7fd741d..00000000000 --- a/.changeset/clean-radios-merge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': major ---- - -Radio: Prevent overriding the input type so the component always renders radio semantics. diff --git a/.changeset/tidy-token-events.md b/.changeset/tidy-token-events.md deleted file mode 100644 index 7da0e9d015b..00000000000 --- a/.changeset/tidy-token-events.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': patch ---- - -Token: Run removal keyboard behavior before consumer keydown handlers. diff --git a/.changeset/visible-class-merging.md b/.changeset/visible-class-merging.md deleted file mode 100644 index 4c07d423a18..00000000000 --- a/.changeset/visible-class-merging.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': patch ---- - -VisuallyHidden: Merge consumer class names with component styles consistently. diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md index 375d777ce30..b093977be22 100644 --- a/.github/agents/migrate-to-merge-props-batch.agent.md +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -33,8 +33,8 @@ unresolved architecture, accessibility, or public API decision. ## Workflow -1. Read ADR-025, the repository instructions, `mergeProps`, and the validated - production examples named by the coordinator skill. +1. Read ADR-025, the repository instructions, `mergeProps`, its tests, and any + relevant migrated component from a lower batch entry. 2. Confirm every assigned finding still appears in the report. 3. Inspect public props, spread order, handlers, styles, class names, refs, invariant attributes, tests, stories, and nearby prior art. @@ -53,7 +53,10 @@ unresolved architecture, accessibility, or public API decision. Do not disable enforcement, add blanket suppressions, manipulate report output, expose `mergeProps` publicly, fix unrelated findings, select another batch, -create stack entries, commit, or change stack structure. +create stack entries, commit, or change stack structure. Do not refine the +adoption rule, report, utility, workflow, coordinator, or agent in a batch +entry. Return those issues as blockers so the coordinator can update the owning +lower stack entry and rebase. ## Result format diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md index e7fb7b75593..89773b78497 100644 --- a/.github/skills/migrate-to-merge-props/SKILL.md +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -37,16 +37,17 @@ class names, and styles according to the ADR. - Default run size: two sequential batches per invocation. - Never split one source file across separate batches. -## Validated migration patterns +## Migration rules and prior art -Use these production examples as prior art: +Before the first implementation batch, inspect: -- `packages/react/src/VisuallyHidden/VisuallyHidden.tsx`: class-name-only - merging. -- `packages/react/src/Token/TokenBase.tsx`: internal behavior first, optional - consumer event handler second, and ref composition outside `mergeProps`. -- `packages/react/src/Radio/Radio.tsx`: a component-owned attribute omitted - from the public type and applied after merged props. +- The component example in `contributor-docs/style.md`. +- `packages/react/src/utils/mergeProps.ts`. +- `packages/react/src/utils/__tests__/mergeProps.test.ts`. + +After implementation batches exist, inspect a relevant migrated component from +a lower stack entry as prior art. Do not add reference component migrations to +the driver entry. Apply these rules: @@ -64,16 +65,44 @@ Apply these rules: 7. Add tests for event order, cancellation, controlled attributes, type exclusions, or other behavior that is not already covered. +## Stack ownership contract + +Every change must live in the entry that owns its concern: + +- `copilot/update-style-docs-prop-merging` owns ADR-025, `mergeProps`, the + adoption rule, rule tests, report script, and migration-status workflow. + Refinements to any of those artifacts must be committed to this foundation + entry. +- `migrate/merge-props-migrator` owns only the coordinator skill and batch + agent. It must not contain component migrations, migration changesets, or + adoption-tool refinements. +- `migrate/merge-props-batch-NN` owns only its assigned component + implementation, tightly coupled tests or stories, and required changesets. +- `migrate/merge-props-cleanup` owns final temporary-artifact removal and the + ADR status update. + +If batch work exposes a problem in the adoption rule, report, utility, or +workflow: + +1. Stop the batch without working around the problem. +2. Navigate to `copilot/update-style-docs-prop-merging`. +3. Commit the refinement there. +4. Rebase every upstack entry. +5. Regenerate the report before resuming the batch. + +If the coordinator or agent instructions need refinement, update +`migrate/merge-props-migrator` and rebase the batch entries above it. + ## Stack contract Use one linear stack: -1. `copilot/update-style-docs-prop-merging`: ADR, utility, adoption rule, and - report foundation. -2. `migrate/merge-props-migrator`: validated examples, tooling refinements, - coordinator skill, and batch agent. +1. `copilot/update-style-docs-prop-merging`: ADR, utility, adoption mechanism, + report, workflow, and all refinements to those artifacts. +2. `migrate/merge-props-migrator`: coordinator skill and batch agent only. 3. `migrate/merge-props-batch-01` through - `migrate/merge-props-batch-NN`: one bounded migration batch per entry. + `migrate/merge-props-batch-NN`: one bounded component implementation batch + per entry. 4. `migrate/merge-props-cleanup`: temporary artifact removal after the report reaches zero. @@ -102,6 +131,9 @@ Confirm the current branch is the top active entry. If a lower entry changed, rebase the upstack before selecting findings. Do not mix unrelated worktree changes into a migration entry. +Inspect the diff for each existing stack entry and confirm it still follows the +stack ownership contract before creating another batch. + ### 2. Establish the baseline Run: @@ -149,11 +181,12 @@ For each batch: locations, maximum size, before counts, and targeted validation commands. 3. Review the returned diff and evidence. 4. Add or update changesets for consumer-facing behavior or type changes. -5. Commit only the assigned batch. -6. Regenerate the report. -7. Confirm assigned findings disappeared, no new findings appeared, and counts +5. Confirm the entry contains no adoption-tool or driver changes. +6. Commit only the assigned batch. +7. Regenerate the report. +8. Confirm assigned findings disappeared, no new findings appeared, and counts changed by the expected amount. -8. Stop before another entry if validation or report evidence fails. +9. Stop before another entry if validation or report evidence fails. Run code-editing batch agents sequentially because every branch depends on the previous branch. diff --git a/packages/react/src/Radio/Radio.test.tsx b/packages/react/src/Radio/Radio.test.tsx index d20071927db..1a7b9756dcd 100644 --- a/packages/react/src/Radio/Radio.test.tsx +++ b/packages/react/src/Radio/Radio.test.tsx @@ -21,7 +21,6 @@ describe('Radio', () => { const radio = getByRole('radio') expect(radio).toBeDefined() - expect(radio).toHaveAttribute('type', 'radio') }) it('renders data-component attribute', () => { diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx index d14e073f96e..5ea79439a9f 100644 --- a/packages/react/src/Radio/Radio.tsx +++ b/packages/react/src/Radio/Radio.tsx @@ -5,7 +5,6 @@ import {clsx} from 'clsx' import sharedClasses from '../Checkbox/shared.module.css' import classes from './Radio.module.css' import type {WithSlotMarker} from '../utils/types' -import {mergeProps} from '../utils/mergeProps' export type RadioProps = { /** @@ -33,7 +32,7 @@ export type RadioProps = { * Indicates whether the radio button must be checked before the form can be submitted */ required?: boolean -} & Omit, 'type'> +} & InputHTMLAttributes /** * An accessible, native radio component for selecting one option from a list. @@ -57,6 +56,7 @@ const Radio = React.forwardRef( const radioGroupContext = useContext(RadioGroupContext) const handleOnChange: ChangeEventHandler = e => { radioGroupContext?.onChange && radioGroupContext.onChange(e) + onChange && onChange(e) } const name = nameProp || radioGroupContext?.name @@ -69,25 +69,17 @@ const Radio = React.forwardRef( return ( ) diff --git a/packages/react/src/Radio/Radio.types.test.tsx b/packages/react/src/Radio/Radio.types.test.tsx deleted file mode 100644 index a08856d1c1e..00000000000 --- a/packages/react/src/Radio/Radio.types.test.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import Radio from '.' - -export function doesNotAcceptType() { - // @ts-expect-error Radio always renders an input with type="radio" - return -} diff --git a/packages/react/src/Token/TokenBase.tsx b/packages/react/src/Token/TokenBase.tsx index 17025fc667f..03202c6f0cb 100644 --- a/packages/react/src/Token/TokenBase.tsx +++ b/packages/react/src/Token/TokenBase.tsx @@ -1,10 +1,10 @@ import type {KeyboardEvent} from 'react' import React from 'react' +import {clsx} from 'clsx' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './TokenBase.module.css' import {defaultTokenSize, type TokenSizeKeys} from './constants' import {isTokenInteractive} from './utils' -import {mergeProps} from '../utils/mergeProps' export type {TokenSizeKeys} @@ -55,38 +55,30 @@ const TokenBase = React.forwardRef { - const handleKeyDown = (event: KeyboardEvent) => { - if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { - onRemove() - } - } - return ( ) - : Component === 'a' - ? (rest as React.AnchorHTMLAttributes) - : (rest as React.HTMLAttributes)), - ...(onKeyDown ? {onKeyDown} : {}), - className, - id: id?.toString(), - }, - )} + onKeyDown={(event: KeyboardEvent) => { + onKeyDown && onKeyDown(event) + + if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { + onRemove() + } + }} + className={clsx(classes.TokenBase, className)} + data-cursor-is-interactive={isTokenInteractive({ + as: Component, + onClick: rest.onClick, + onFocus: rest.onFocus, + tabIndex: rest.tabIndex, + disabled: rest.disabled, + })} + data-size={size} + id={id?.toString()} + {...(Component === 'button' + ? (rest as React.ButtonHTMLAttributes) + : Component === 'a' + ? (rest as React.AnchorHTMLAttributes) + : (rest as React.HTMLAttributes))} // TypeScript cannot resolve polymorphic ref types at compile time // This assertion is safe because the ref will match the actual rendered element // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/react/src/Token/__tests__/Token.test.tsx b/packages/react/src/Token/__tests__/Token.test.tsx index ac07bbe22fe..73c01124c2a 100644 --- a/packages/react/src/Token/__tests__/Token.test.tsx +++ b/packages/react/src/Token/__tests__/Token.test.tsx @@ -63,17 +63,6 @@ const testTokenComponent = (Component: React.ComponentType { - const calls: string[] = [] - const {getByText} = HTMLRender( - calls.push('remove')} onKeyDown={() => calls.push('consumer')} />, - ) - - fireEvent.keyDown(getByText('token'), {key: 'Backspace'}) - - expect(calls).toEqual(['remove', 'consumer']) - }) - it('adds className to rendered component', () => { const {getByText} = HTMLRender() const domNode = getByText('token') diff --git a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx index d2b8231d952..d05c496352f 100644 --- a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx +++ b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx @@ -1,6 +1,6 @@ +import {clsx} from 'clsx' import type React from 'react' import {type HTMLAttributes} from 'react' -import {mergeProps} from '../utils/mergeProps' import classes from './VisuallyHidden.module.css' /** @@ -13,8 +13,12 @@ import classes from './VisuallyHidden.module.css' * * @see https://www.scottohara.me/blog/2023/03/21/visually-hidden-hack.html */ -export const VisuallyHidden = ({children, ...rest}: VisuallyHiddenProps) => { - return {children} +export const VisuallyHidden = ({className, children, ...rest}: VisuallyHiddenProps) => { + return ( + + {children} + + ) } export type VisuallyHiddenProps = React.PropsWithChildren<