Skip to content

🔷 Anchor the popover without injecting a ref into the trigger - #3189

Draft
jandrade wants to merge 1 commit into
WB-2119.2from
WB-2119.3
Draft

🔷 Anchor the popover without injecting a ref into the trigger#3189
jandrade wants to merge 1 commit into
WB-2119.2from
WB-2119.3

Conversation

@jandrade

@jandrade jandrade commented Aug 28, 2026

Copy link
Copy Markdown
Member

Summary:

Now that Floating resolves the reference (anchor) element from the DOM, the
popover's anchor no longer needs any ref plumbing. This PR removes it.

This is the follow-up to #3163, which did the PopoverFloating migration.
That PR made the one change to PopoverAnchor needed for the popover to
position at all — spreading the FloatingReferenceAttributeName attribute
through to the trigger — and left the rest alone, so this PR is a small, focused
cleanup on top of it.

  • PopoverAnchor becomes a plain function component. It was a
    React.forwardRef component purely to receive the anchor ref, and after
    🚨 Refactor Popover to use wonder-blocks-floating #3163 nothing passes it a ref anymore.
  • The hand-rolled mergeRefs helper is deleted. It existed to merge the anchor
    ref with the trigger's own ref; there is no anchor ref to merge, so a
    trigger's ref is now left untouched.
  • The function-as-children and element-children branches are unified, since ref
    handling was the only thing that differed between them.

The user-visible effect is the reason this is a separate PR: a Popover trigger
can now be of any component type — host element, React.forwardRef
component, class component, or plain function component — and never has to
accept or forward a ref. It only has to spread the props it is given onto its
own element (which it needs to do anyway for the interaction and ARIA props).

Issue: WB-2119

Test plan:

Automated: pnpm typecheck, pnpm lint, and pnpm build all pass. The Popover
jest suite passes, with new coverage for a plain function component as the
trigger and for a trigger's own ref being left alone.

Manual (Storybook — Popover stories):

  1. Verify the popover still anchors and positions correctly for the
    element-children trigger pattern.
  2. Verify the same for the function-as-children trigger pattern.
  3. Verify the Tabs TabLabelRenderFunction story (popover anchored to a tab)
    still positions and behaves correctly.

Review plan:

Please review these risky changes

  1. 🔷 popover-anchor.tsx

Common patterns:

1 file: Drop forwardRef + mergeRefs; the trigger is identified by the
attribute Floating injects rather than by a ref.

// Before
const PopoverAnchor = React.forwardRef<HTMLElement, Props>(
    function PopoverAnchor(props, ref) {
        // ...
        return React.cloneElement(children, {
            ...sharedProps,
            ref: childrenRef ? mergeRefs(ref, childrenRef) : ref,
        });
    },
);

// After
export default function PopoverAnchor(props: Props) {
    // ...
    return React.cloneElement(renderedChildren, sharedProps);
}

…gger

Now that `Floating` resolves the reference (anchor) element from the DOM,
`PopoverAnchor` no longer needs any ref plumbing. This removes it:

- `PopoverAnchor` becomes a plain function component. It was a
  `React.forwardRef` component purely to receive the anchor ref, and nothing
  passes it a ref anymore after WB-2119.2.
- The hand-rolled `mergeRefs` helper is deleted. It existed to merge the anchor
  ref with the trigger's own ref; there is no anchor ref to merge, so a
  trigger's `ref` is now left untouched.
- The function-as-children and element-children branches are unified, since the
  only thing that differed between them was the ref handling.

The user-visible effect is that a `Popover` trigger can be of any component
type, including a plain function component, and never has to accept or forward a
ref. It only has to spread the props it is given onto its own element (which it
needs to do anyway for the interaction and ARIA props).

Issue: WB-2119

Automated: `pnpm typecheck`, `pnpm lint`, and `pnpm build` all pass. The Popover
jest suite passes, with new coverage for a plain function component as the
trigger and for a trigger's own ref being left alone.

Manual (Storybook — Popover stories): verify the popover still anchors and
positions correctly for the element-children and function-as-children trigger
patterns, and that the Tabs `TabLabelRenderFunction` story (popover anchored to
a tab) still positions and behaves correctly.

Please review these risky changes

1. 🔷 `packages/wonder-blocks-popover/src/components/popover-anchor.tsx`:
   Changes the contract between `Popover` and the trigger element it wraps.
@changeset-bot

changeset-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 98da780

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@khanacademy/wonder-blocks-popover Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

// the popover against it.
[FloatingReferenceAttributeName]: floatingReferenceId,
} as const;
export default function PopoverAnchor(props: Props) {

@jandrade jandrade Aug 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔷 This is the file to review in this PR — it changes the contract between Popover and the trigger element it wraps.

After #3163, Floating resolves the reference (anchor) element from the DOM via the attribute it injects, and nothing passes a ref into PopoverAnchor any more. So the ref plumbing that was left behind is removed here:

  • React.forwardRef → a plain function component. It was a forwardRef component purely to receive the anchor ref.
  • The hand-rolled mergeRefs helper is deleted. It existed to merge the anchor ref with the trigger's own ref; there is no anchor ref to merge, so a trigger's ref is now left untouched rather than being wrapped.
  • The function-as-children and element-children branches are unified, since ref handling was the only thing that differed between them.

The user-visible consequence, and the reason this is its own PR + changeset: a Popover trigger can now be any component type — host element, forwardRef component, class component, or plain function component — and never has to accept or forward a ref. It only has to spread the props it is given onto its own element, which it needs to do anyway for the interaction and ARIA props.

@khan-actions-bot
khan-actions-bot requested a review from a team August 28, 2026 16:19
@jandrade
jandrade removed the request for review from a team August 28, 2026 16:19
@jandrade
jandrade marked this pull request as draft August 28, 2026 16:19
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Size Change: -116 B (-0.09%)

Total Size: 133 kB

📦 View Changed
Filename Size Change
packages/wonder-blocks-popover/dist/es/index.js 2.71 kB -116 B (-4.11%)
ℹ️ View Unchanged
Filename Size
packages/eslint-plugin-wonder-blocks/dist/es/index.js 7.11 kB
packages/wonder-blocks-accordion/dist/es/index.js 3.02 kB
packages/wonder-blocks-announcer/dist/es/index.js 2.43 kB
packages/wonder-blocks-badge/dist/es/index.js 2.03 kB
packages/wonder-blocks-banner/dist/es/index.js 2.01 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.93 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 798 B
packages/wonder-blocks-button/dist/es/index.js 4.28 kB
packages/wonder-blocks-card/dist/es/index.js 1.09 kB
packages/wonder-blocks-cell/dist/es/index.js 2.19 kB
packages/wonder-blocks-clickable/dist/es/index.js 2.61 kB
packages/wonder-blocks-core/dist/es/index.js 2.6 kB
packages/wonder-blocks-data/dist/es/index.js 5.51 kB
packages/wonder-blocks-date-picker/dist/es/index.js 8.06 kB
packages/wonder-blocks-dropdown/dist/es/index.js 20.6 kB
packages/wonder-blocks-floating/dist/es/index.js 3.1 kB
packages/wonder-blocks-form/dist/es/index.js 6.39 kB
packages/wonder-blocks-grid/dist/es/index.js 1.25 kB
packages/wonder-blocks-icon-button/dist/es/index.js 4.06 kB
packages/wonder-blocks-icon/dist/es/index.js 1.89 kB
packages/wonder-blocks-labeled-field/dist/es/index.js 3.47 kB
packages/wonder-blocks-layout/dist/es/index.js 1.69 kB
packages/wonder-blocks-link/dist/es/index.js 1.54 kB
packages/wonder-blocks-modal/dist/es/index.js 7.36 kB
packages/wonder-blocks-pill/dist/es/index.js 1.32 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.49 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.12 kB
packages/wonder-blocks-styles/dist/es/index.js 464 B
packages/wonder-blocks-switch/dist/es/index.js 1.6 kB
packages/wonder-blocks-tabs/dist/es/index.js 5.62 kB
packages/wonder-blocks-testing-core/dist/es/index.js 4.12 kB
packages/wonder-blocks-testing/dist/es/index.js 978 B
packages/wonder-blocks-theming/dist/es/index.js 384 B
packages/wonder-blocks-timing/dist/es/index.js 1.53 kB
packages/wonder-blocks-tokens/dist/es/index.js 6.5 kB
packages/wonder-blocks-toolbar/dist/es/index.js 906 B
packages/wonder-blocks-tooltip/dist/es/index.js 6.19 kB
packages/wonder-blocks-typography/dist/es/index.js 1.04 kB

compressed-size-action

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (ac0b00b) and published all packages with changesets to npm.

You can install the packages in frontend by running:

./dev/tools/deploy_wonder_blocks.js --tag="PR3189"

Packages can also be installed manually by running:

pnpm add @khanacademy/wonder-blocks-<package-name>@PR3189

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-qowurnjpqv.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 546
Tests with visual changes 0
Total stories 880
Inherited (not captured) snapshots [TurboSnap] 0
Tests on the build 546

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