Skip to content

ui: Stepper warns about duplicate step values at registration time - #111567

Merged
bogiii merged 9 commits into
trunkfrom
fix/stepper-duplicate-step-warning
Jun 16, 2026
Merged

bogiii merged 9 commits into
trunkfrom
fix/stepper-duplicate-step-warning

Conversation

@bogiii

@bogiii bogiii commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Part of the follow-up work for #111036 review feedback (#111036 (comment), #111036 (comment), #111036 (comment)).

Proposed Changes

  • Move the duplicate-step-value dev warning from Stepper.Root into useStepRegistration, where it can actually fire. The previous warning loop in root.tsx was dead code: registration silently de-duped steps, so the steps array it inspected could never contain duplicates.
  • Track a per-value mount count in registerStep: only the first registration adds a state entry (later ones warn), and the entry is removed only when the last mounted instance for that value deregisters. Transient duplicates in either unmount order no longer drop a still-mounted step or skew indices and totalSteps.
  • Simplify the updateStep comparison to a plain shallow compare. The union-keys machinery was needed when registration meta carried ReactNode snapshots; meta is now flat scalars, and the key-count check still catches fields removed between renders (e.g. status).
  • Remove redundant children/className from StepperPanelProps; ComponentProps<'div'> already provides them.
  • Add tests: three DOM-level regression tests for duplicate handling, and five hook-level characterization tests for updateStep (written against the old comparison and kept green through the refactor).

Why are these changes being made?

  • Review feedback on feat(ui): add Stepper component to @automattic/ui #111036 pointed out that the duplicate-value check could never throw, that the updateStep comparison could be simplified after the registration-model refactor, and that the panel prop type duplicated ComponentProps<'div'> fields.
  • This keeps the de-duping (one entry per value protects index and count integrity) while making the dev warning real, and fixes a latent bug where unmounting one duplicate instance deregistered the step entirely.

Testing Instructions

  • Run yarn test-packages packages/ui/src/stepper and confirm all tests pass, including the "Stepper dev warnings" block and use-step-registration.tsx.
  • Run yarn typecheck-packages to confirm the StepperPanelProps change compiles.
  • Optionally, in Storybook, render a stepper with two steps sharing the same value and confirm a one-time console warning "Two steps share value ..." appears in dev mode.

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • For UI changes, have you tested the affected components in dark mode?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@bogiii bogiii self-assigned this Jun 11, 2026
@matticbot

matticbot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug fix/stepper-duplicate-step-warning on your sandbox.

bogiii and others added 3 commits June 11, 2026 22:49
The duplicate-value warning in Stepper.Root was dead code: registration
de-duped steps before the warning loop could ever see a duplicate. Move
the warning into useStepRegistration where it can actually fire, and
return a no-op deregister for rejected duplicates so unmounting one no
longer removes the original step registration.

Follow-up to #111036 review feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Stepper component is still unreleased, so a bug fix to it needs no changelog entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bogiii
bogiii force-pushed the fix/stepper-duplicate-step-warning branch from 60b235b to 02bc01c Compare June 11, 2026 20:58
@bogiii
bogiii requested a review from Copilot June 11, 2026 21:32
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jun 11, 2026
@bogiii
bogiii marked this pull request as ready for review June 11, 2026 21:32

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.

Pull request overview

This PR fixes Stepper duplicate-step-value handling by moving the duplicate-value dev warning to step registration (where duplicates are actually detected) and ensuring rejected duplicate registrations don’t deregister the original step on unmount. It also updates tests to cover the warning and the corrected unmount behavior.

Changes:

  • Move duplicate-step-value dev warning from Stepper.Root to useStepRegistration.
  • Return a no-op deregister function for rejected duplicate step registrations to avoid removing the original step.
  • Add unit tests for the registration-time warning and for preserving the original step when a duplicate unmounts.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/ui/src/stepper/use-step-registration.ts Adds registration-time duplicate detection and changes deregistration behavior for duplicates.
packages/ui/src/stepper/test/index.tsx Adds tests for the duplicate-value warning and for duplicate unmount not affecting the original.
packages/ui/src/stepper/root.tsx Removes now-dead duplicate scanning warning and documents that duplicates are handled at registration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/ui/src/stepper/use-step-registration.ts Outdated
Comment thread packages/ui/src/stepper/test/index.tsx
…p a step

A rejected duplicate previously got a no-op deregister, so if the originally-accepted instance unmounted while the duplicate stayed mounted, the value lost its registration entirely. Track a per-value mount count and remove the entry only when the last mounted instance deregisters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bogiii
bogiii marked this pull request as draft June 11, 2026 21:44
@bogiii
bogiii requested a review from Copilot June 11, 2026 21:49

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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@bogiii
bogiii requested a review from ciampo June 11, 2026 21:59
@bogiii
bogiii marked this pull request as ready for review June 11, 2026 22:00
The union-keys comparison in updateStep was needed when registration meta carried ReactNode snapshots; meta is now flat scalars, so a plain shallow compare (with a key-count check for removed fields) is enough. Also drop children/className from StepperPanelProps since ComponentProps<'div'> already provides them.

Follow-up to #111036 review feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bogiii
bogiii marked this pull request as draft June 12, 2026 10:15
@bogiii
bogiii marked this pull request as ready for review June 12, 2026 10:29
Comment thread packages/ui/src/stepper/use-step-registration.ts
Comment thread packages/ui/src/stepper/test/use-step-registration.tsx Outdated
bogiii and others added 2 commits June 15, 2026 23:24
- Document the stale-metadata limitation when duplicate step values overlap
- Rename use-step-registration test to .ts (no JSX)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…etadata

Instead of keying steps by value, each Stepper.Step now registers under a
unique useId. The public steps list is derived as one entry per value from
the first still-mounted instance, so a duplicate value can no longer override
or leave stale metadata on the surviving step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@bogiii
bogiii marked this pull request as draft June 15, 2026 22:19
Order derived steps by each value's stable first-appearance slot instead of
its current array position, so when an earlier duplicate instance unmounts the
surviving instance keeps the value's original slot rather than jumping to the
end. Adds tests for the slot invariant, the duplicate warning, and strengthens
the surviving-metadata assertion to cover both mounted and unmounted states.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@bogiii
bogiii requested a review from ciampo June 16, 2026 10:08
@bogiii
bogiii marked this pull request as ready for review June 16, 2026 10:09
@github-actions

Copy link
Copy Markdown

Looks like one of the E2E tests has failed.

You can fix them following these steps:

  1. Check out this branch locally:
    gh pr checkout 111567
  2. Start Claude Code in the repo:
    claude
  3. Run the /fix-e2e-tests skill, passing this PR number:
    /fix-e2e-tests 111567
    

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

Good to merge once remaining feedback is addressed

Comment on lines +88 to +99
const steps = useMemo( () => {
const order = slotOrder.current;
const canonical = new Map< string, T >();
for ( const inst of instances ) {
if ( ! canonical.has( inst.meta.value ) ) {
canonical.set( inst.meta.value, inst.meta );
}
}
return [ ...canonical.values() ].sort(
( a, b ) => ( order.get( a.value ) ?? 0 ) - ( order.get( b.value ) ?? 0 )
);
}, [ instances ] );

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.

Nit:

This memo reads slotOrder.current during render but only depends on [ instances ].

It happens to work because every slot mutation is paired with a setInstances, so the ref is always current by the time the memo reruns. Still, reading a mutable ref mid-render isn't pure and could bite us under concurrent rendering. Not worth blocking over — duplicates are already an error state — but if we want it airtight, folding the slot into the instances state ({ id, slot, meta }) and sorting on inst.slot drops the ref read entirely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call, done. Folded the slot into the instances state ({ id, slot, meta }) and the memo now sorts on inst.slot, so it no longer reads slotOrder.current mid-render. The ref is still used inside registerStep (outside render), so the derivation is now pure. Slot-pinning behavior is unchanged and still covered by the existing tests.

Fold the per-value slot into the instances state and sort on it, so the
steps memo no longer reads the slotOrder ref mid-render — keeping the
derivation pure under concurrent rendering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@bogiii
bogiii merged commit 282d0d5 into trunk Jun 16, 2026
19 checks passed
@bogiii
bogiii deleted the fix/stepper-duplicate-step-warning branch June 16, 2026 22:15
@github-actions github-actions Bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jun 16, 2026
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.

4 participants