feat(ui): add UserButton view component - #9184
Conversation
🦋 Changeset detectedLatest commit: fd4475a The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types 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 latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a client-side Mosaic Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/electron
@clerk/electron-passkeys
@clerk/eslint-plugin
@clerk/expo
@clerk/expo-google-signin
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
API Changes Report
Summary
No API Changes DetectedAll packages have stable APIs with no detected changes. Report generated by Break Check Last ran on |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/swingset/src/lib/registry.ts`:
- Around line 3-8: Update the manually constructed AccountButton story module to
preserve the story’s raw __source export. Import __source from
account-button.stories and include it in the module object alongside
AccountButtonDefault, AccountButtonMultipleAccounts, AccountButtonPersonal, and
accountButtonMeta; apply the same change to the corresponding second
construction.
In `@packages/swingset/src/stories/account-button.mdx`:
- Around line 3-21: Restructure the AccountButton MDX documentation into
Archetype C format: replace the current descriptive sections with one short
paragraph covering owned state and the wired lower-layer pieces, then provide
exactly one Story. Add a composition array to that Story listing each direct
dependency, including the headless popover primitive and slot recipes, and
remove the current multi-story layout.
In `@packages/swingset/src/stories/account-button.stories.tsx`:
- Around line 10-14: Update the AccountButton story metadata to assign
accountButtonRecipe to meta.styles. In each story function, cast the knobs
Record<string, unknown> through knobsAsProps and spread the resulting props into
AccountButton so playground values are applied.
In `@packages/ui/src/mosaic/components/account-button.view.tsx`:
- Around line 446-808: Split the presentational leaves and sections from
account-button.view.tsx into focused React modules, extracting symbols such as
Avatar, Row, Header, WorkspaceList, AccountsSection, and Footer while preserving
their current behavior and context usage. Keep the public compound component
composition and exports in account-button.view.tsx, and update imports as needed
so the file remains focused and within the project’s preferred component size.
- Around line 21-85: Add comprehensive customer-facing JSDoc to the exported
AccountButtonAccount, AccountButtonMembership, AccountButtonSuggestion,
AccountButtonInvitation, AccountButtonData, AccountButtonCallbacks, and props
contracts, documenting field semantics including onUpgrade. Add explicit
React.JSX.Element return types to all four exported AccountButton components,
then request Docs-team review for the generated public API documentation.
- Around line 57-68: Remove the inert status field from AccountButtonData for
v1, since the current loading value still requires and renders ready-state data.
Update AccountButton consumers, including the account-button view, to rely on
the existing ready-state shape without status checks; only retain status if you
introduce a genuine discriminated union with a separate loading rendering path.
- Around line 488-535: Update the selection control rendered by Row to expose
its active state through the appropriate aria-current attribute, using the
active prop’s value. Apply the attribute to the interactive button and the
non-interactive div consistently, while leaving the visual check icon behavior
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Repository UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 05507a88-c836-4e0f-b43f-e0d914595582
📒 Files selected for processing (8)
.changeset/account-button-switcher.mdpackages/swingset/src/components/DocsViewer.tsxpackages/swingset/src/lib/registry.tspackages/swingset/src/stories/account-button.mdxpackages/swingset/src/stories/account-button.stories.tsxpackages/ui/src/mosaic/components/account-button.view.tsxpackages/ui/src/mosaic/icons/registry.tsxpackages/ui/src/mosaic/primitives/popover.tsx
dfae67d to
ef35f2d
Compare
ef35f2d to
8f7ffa2
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
packages/ui/src/mosaic/user-button/user-button.view.tsx (2)
477-538: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winExpose the active row to assistive technology.
The check icon at Lines 527-533 is purely visual. Add
aria-currentto the interactivebutton(Line 512) and the non-interactivediv(Line 520) inRow, driven by theactiveprop, so screen-reader users can identify the active account or organization.This mirrors the same unresolved concern raised previously on the sibling
account-button.view.tsxfile.As per coding guidelines, "Implement proper ARIA attributes for accessibility in React components."
♿ Proposed fix
{onSelect ? ( <button type='button' + aria-current={active ? 'true' : undefined} onClick={onSelect} {...select} > {inner} </button> ) : ( <div {...select} + aria-current={active ? 'true' : undefined} style={{ cursor: 'default' }} > {inner} </div> )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/mosaic/user-button/user-button.view.tsx` around lines 477 - 538, Update the interactive and non-interactive row elements in Row to expose active state through aria-current, deriving the attribute from the active prop. Apply the same value to both the button and fallback div while preserving their existing behavior and rendering.
447-813: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftSplit this file into focused modules.
This file still holds the data contracts, recipe, context, presentational leaves (
Avatar,Row,SuggestedBadge,InlineButton,HoverAction,AddRow), sections (Header,WorkspaceList,SessionsSection,Footer), and the public compound parts in one 912-line module. Extract the leaves and sections into separate files and keep the compound composition (UserButtonRoot,UserButtonTrigger,UserButtonPopup,UserButton) here.This mirrors the same unresolved concern raised previously on the sibling
account-button.view.tsxfile.As per coding guidelines, "Limit component size to 150-200 lines; extract logic into custom hooks."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/mosaic/user-button/user-button.view.tsx` around lines 447 - 813, Split the user-button view into focused modules: move the presentational leaves Avatar, Row, SuggestedBadge, InlineButton, HoverAction, and AddRow, plus Header, WorkspaceList, SessionsSection, and Footer, into separate files with their required shared contracts and imports. Keep UserButtonRoot, UserButtonTrigger, UserButtonPopup, and UserButton in the current module, preserving their existing composition and behavior while reducing the file to focused component logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/swingset/src/lib/registry.ts`:
- Around line 108-113: Update the UserButton story imports to include the
exported __source value, then assign it to the __source property when
constructing userButtonModule alongside the existing story metadata. Preserve
the current module construction behavior for all other fields.
In `@packages/swingset/src/stories/user-button.mdx`:
- Around line 5-14: Merge the interaction and implementation descriptions in the
user-button intro into one short, present-tense paragraph, preserving the key
behavior and composition details while removing redundant wording.
- Around line 68-90: Replace the nonstandard “States & scenarios” heading in the
user-button documentation with the established “Examples” section name, keeping
the Personal and MultipleSessions stories and their descriptions under that
section.
---
Duplicate comments:
In `@packages/ui/src/mosaic/user-button/user-button.view.tsx`:
- Around line 477-538: Update the interactive and non-interactive row elements
in Row to expose active state through aria-current, deriving the attribute from
the active prop. Apply the same value to both the button and fallback div while
preserving their existing behavior and rendering.
- Around line 447-813: Split the user-button view into focused modules: move the
presentational leaves Avatar, Row, SuggestedBadge, InlineButton, HoverAction,
and AddRow, plus Header, WorkspaceList, SessionsSection, and Footer, into
separate files with their required shared contracts and imports. Keep
UserButtonRoot, UserButtonTrigger, UserButtonPopup, and UserButton in the
current module, preserving their existing composition and behavior while
reducing the file to focused component logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 6df10ab6-9322-4890-89da-e4419eb949e3
📒 Files selected for processing (8)
.changeset/user-button-switcher.mdpackages/swingset/src/components/DocsViewer.tsxpackages/swingset/src/lib/registry.tspackages/swingset/src/stories/user-button.mdxpackages/swingset/src/stories/user-button.stories.tsxpackages/ui/src/mosaic/icons/registry.tsxpackages/ui/src/mosaic/primitives/popover.tsxpackages/ui/src/mosaic/user-button/user-button.view.tsx
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)
🚧 Files skipped from review as they are similar to previous changes (2)
- .changeset/user-button-switcher.md
- packages/ui/src/mosaic/primitives/popover.tsx
…ist at a time Memberships, invitations and suggestions are three separate requests, so rendering each as it landed walked the list in in stages. The placeholder now stands in for all three until the last one is in. The section itself no longer opens on loading alone: hasOrganizations is answered before the lists are fetched, so an account with none never opens a section that then disappears under it.
A labelled trigger was a pill regardless of what it named, so an organization got a fully rounded outline around a squared-off mark. The corner now comes from `triggerShapes` in both the labelled and avatar-only forms.
The labelled form padded itself while the avatar-only form sat flush. It now matches, keeping only the gap between the avatar and its label.
The shared `Skeleton` is Emotion-backed, and the Mosaic build has to stay free of that runtime to be mountable in a host app's own tree. The placeholder keeps its size and pulse, drawn with StyleX instead.
A row going busy swapped its host element from `<button>` to `<div>`, remounting the subtree and dropping the avatar back to its initials for the length of the action. A row that stands down now stays the button it was, disabled, and `Avatar.Image` resolves a browser-cached `src` in a layout effect so neither a remount nor a swap flashes the fallback.
…oll area
The list carried its own `overflow-y`. It now spreads `scrollAreaViewport('auto')`, so it picks up the scroll-driven edge fades and the scrollbar paint, and keeps only the 18rem cap of its own.
The default `auto` gutter rather than `stable`: a reserved gutter insets the rows whether or not the list overflows, so a short list would sit its avatars and icons off the edge the header and footer align to.
…zations The active account's row hung off the workspace list, so an account with no memberships, invitations or suggestions lost the row and every action on it: managing the account, creating an organization, and signing out of that account. Switching to a second account was the usual way in, since a second account rarely belongs to an organization. The row now stands on its own wherever the surface shows both accounts and organizations. The list it used to hang off is still withheld until there is something to list.
…n runs The account row emptied its action list while any other action was in flight, and an empty list renders no trigger at all, so the row dropped its trailing `...` for the length of a session switch and got it back after. It now stands the trigger down disabled, the way every other row on the surface already does.
The account's own workspace lists alongside the organizations and ahead of them, so `null` is a row you can click. Pending rows lead the list, invitations ahead of suggestions, matching the order the existing OrganizationSwitcher uses.
The row read as the account's own name, repeating what the trigger and header already say. It now reads "Personal account", the way the existing OrganizationSwitcher names it.
An instance with force organization selection enabled has no personal workspace to return to. The view drops the row rather than rendering one that does nothing when clicked.
Cases that only ever asserted what the popover renders belong here rather than in the connected test, which drives the real controller against a mocked Clerk.
…it leads with The placeholder was always a circle, so an organization-led trigger changed corner when the real avatar landed. The mode settles which workspace leads without waiting on data, so the placeholder can take the right corner up front.
…ts it The shape was set on the trigger wrapper, which is transparent and borderless, while the pulsing block inside it hardcoded a full radius. The placeholder stayed a circle whatever the surface led with.
…fade Tabbing to a row below the fold landed it flush against the edge the scroll area's mask fades out, so the row you just moved to was the one you could barely read. One fade's worth of `scroll-padding-block` on the group puts the destination clear of it.
`@stylexjs/valid-styles` rejects the `scrollPaddingBlock` shorthand, which failed the ui lint.
The check beside the active workspace or account is decorative, so on its own it left that row reading to a screen reader like the ones there is still somewhere to switch to.
An accept or join reported itself by dropping the button and putting a spinner on the row's trailing edge, so the affordance you pressed vanished under you. It is a labelled button, so it takes SubmitButton and keeps the spinner inside itself.
Custom actions and links join the foot of the popup ahead of Clerk's own rows. menuItemOrder arranges the foot by id, over createOrganization, addAccount, signOutAll, and each custom item's own id. Which of the built-ins the foot carries depends on the surface, so an id it has not got is ignored rather than an error.
The data contract, mode flags, and menu item shapes are what the view and a controller agree on, so they sit in a module neither one owns. Annotates the exported parts' return types: without one TS infers the global JSX.Element, which Emotion augments, so the signature published to apps read EmotionJSX.Element.
Standing in for the trigger means claiming a button is coming, and until Clerk loads there is no telling whether one is: a signed-out visitor is indistinguishable from a session still resolving, so the placeholder resolved to nothing for them. `<ClerkLoading>` is where an app that knows its own nav puts one.
`mode` took `'orgs'` while `modePriority` took `'organizations'`. Both are now `'organization'`, matching `ForPayerType` and the `for` prop on the billing components. Also corrects a stale comment in the view suite, which claimed the connected container does not expose `mode`.
The badge sits beside the trigger label and is governed by it, so it takes the same name. It stays a boolean: the plan comes off the active organization.
Description
The Mosaic
UserButtonsurface, presentation only. The controller that connects it to Clerk is a follow-up.user-button.view.tsxexports the parts (UserButtonRoot,UserButtonTrigger,UserButtonPopup) and the all-in-oneUserButtonView, which frees the bareUserButtonname for the connected container.user-button.types.tsholds the data contract that container has to satisfy, anduserButtonBusyKeysnames every affordance that can spin.What the surface offers:
modenarrows the surface to organizations or to accounts.modePrioritypicks which one a combined surface leads with, in its header and in the trigger.renderTriggerLabelnames the active workspace beside the avatar,renderPlanBadgeshows its plan.customMenuItemsandmenuItemOrderadd the app's own rows to the foot of the menu, each row either an action or a link.pendingKeyspins the affordance that owns the action in flight and stands the others down.@clerk/headlessgains what the surface needs from its popover: contents stay mounted while it closes,Popover.Roottakes analignOffset, and Escape inside a menu no longer bubbles to the floating element around it.Swingset documents the surface and renders it as a live prototype.
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change