-
Notifications
You must be signed in to change notification settings - Fork 460
feat(ui): add UserButton view component #9184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alexcarpenter
wants to merge
17
commits into
main
Choose a base branch
from
carp/account-button-switcher
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
5e0563e
feat(ui): add UserButton account/org switcher
alexcarpenter c02ab84
feat(ui): give UserButton its own User section above Organization
alexcarpenter 5180c54
refactor(ui): move UserButton into its own user-button/ folder
alexcarpenter 8f7ffa2
fix(ui): resolve UserButton lint errors
alexcarpenter 75bd1fd
refactor(ui): compose UserButton view from Mosaic Popover, Card, Item…
alexcarpenter c6e0c03
fix(ui): show additional account emails on md rows in UserButton
alexcarpenter 7109b5f
fix(headless): only treat a menu as nested when it has a parent menu
alexcarpenter ec620bd
fix(ui): drop the popover positioner z-index
alexcarpenter f310bbe
feat(ui): render UserButton accounts as email rows with a menu
alexcarpenter 99be01e
fix(headless): stop menu Escape from bubbling to the parent floating …
alexcarpenter dd026e6
fix(ui): make the UserButton trigger follow the avatar's shape
alexcarpenter 338f356
feat(ui): give the Mosaic Popover trigger a themeable slot
alexcarpenter ddda67d
refactor(ui): name the Mosaic radius tokens by size
alexcarpenter 12459d8
avatar disable dragging
alexcarpenter 54a1e12
default item color to neutral faded
alexcarpenter 2378021
Update item.styles.ts
alexcarpenter 36f9c06
feat(headless): add alignOffset to Popover.Root
alexcarpenter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@clerk/ui': minor | ||
| --- | ||
|
|
||
| Add the `UserButton` Mosaic component: an account and organization switcher that combines multi-session account switching with organization selection, suggestions, and invitations behind a single popover. Exposes the all-in-one `UserButton` plus the composable `UserButtonRoot`, `UserButtonTrigger`, and `UserButtonPopup` parts. It owns no slots of its own: it is composed from `Popover`, `Card`, `Item`, `Avatar` and `Menu`, so `appearance.elements` overrides for those components theme it too. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import * as UserButtonStories from './user-button.stories'; | ||
|
|
||
| # UserButton | ||
|
|
||
| The account & organization switcher that sits behind the user avatar. The active workspace sits at the | ||
| top with **Manage account / Sign out**, its organizations (including any **suggested** workspaces you | ||
| can Join) are listed under the account they belong to, and **additional accounts** follow. Clicking any | ||
| row switches to it; the account's own `⋯` menu carries its per-account actions. The foot of the surface | ||
| exposes **Add account** and **Sign out of all accounts**. | ||
|
|
||
| It is a composition of lower-layer Mosaic components — `Popover` for the floating surface, `Card` for | ||
| the surface itself, `Item` for every row, `Avatar` for the workspace marks, and `Menu` for the | ||
| per-account actions — so it inherits their behavior, focus management, and ARIA wiring rather than | ||
| restyling any of it. This first pass is presentational: the parts render from the props on | ||
| `UserButtonRoot` (mock data in the examples). A `useUserButtonController()` that reads live Clerk | ||
| resources is a drop-in follow-up. | ||
|
|
||
| ## Example | ||
|
|
||
| <Story | ||
| name='Default' | ||
| storyModule={UserButtonStories} | ||
| /> | ||
|
|
||
| ## Usage | ||
|
|
||
| The all-in-one `UserButton` renders the trigger and popup from a single prop-driven call: | ||
|
|
||
| ```tsx | ||
| import { UserButton } from '@clerk/ui/mosaic/user-button/user-button.view'; | ||
|
|
||
| <UserButton | ||
| status='ready' | ||
| activeSession={{ sessionId: 'sess_colin', userId: 'user_colin', name: 'Colin', email: 'colin@clerk.dev' }} | ||
| activeOrganizationId='org_clerk_app' | ||
| hasOrganizations | ||
| memberships={[ | ||
| { kind: 'membership', organizationId: 'org_clerk_app', name: 'Clerk app', membersCount: 24, planLabel: 'Pro plan', upgradeable: true }, | ||
| { kind: 'membership', organizationId: 'org_clerk_cloud', name: 'Clerk Cloud' }, | ||
| ]} | ||
| suggestions={[{ kind: 'suggestion', id: 'sug_labs', organizationId: 'org_clerk_labs', name: 'Clerk Labs', status: 'pending' }]} | ||
| invitations={[]} | ||
| additionalSessions={[{ sessionId: 'sess_braden', userId: 'user_braden', name: 'Braden', email: 'braden@clerk.dev' }]} | ||
| onSelectOrganization={id => setActive({ organization: id })} | ||
| onSelectPersonal={() => setActive({ organization: null })} | ||
| onSwitchSession={sessionId => setActive({ session: sessionId })} | ||
| onSignOutAll={() => signOut()} | ||
| /> | ||
| ``` | ||
|
|
||
| For layouts that need to drop the popup into their own trigger, compose the parts directly. The data | ||
| and callbacks live on `UserButtonRoot` and are read from context by the leaves, so they take no props: | ||
|
|
||
| ```tsx | ||
| import { | ||
| UserButtonRoot, | ||
| UserButtonTrigger, | ||
| UserButtonPopup, | ||
| } from '@clerk/ui/mosaic/user-button/user-button.view'; | ||
|
|
||
| <UserButtonRoot {...data} {...callbacks}> | ||
| <UserButtonTrigger /> | ||
| <UserButtonPopup /> | ||
| </UserButtonRoot> | ||
| ``` | ||
|
|
||
| The exports are flat (not `UserButton.Trigger`) so each part can declare its own `'use client'` | ||
| boundary without forcing the consumer's file to become a client component. | ||
|
|
||
| ## States & scenarios | ||
|
|
||
| ### Personal (no organizations) | ||
|
|
||
| When the active account has no organizations, the header collapses to a personal layout — | ||
| **Manage account / Sign out** — and the organization list is omitted. | ||
|
|
||
| <Story | ||
| name='Personal' | ||
| storyModule={UserButtonStories} | ||
| /> | ||
|
|
||
| ### Personal & workspace | ||
|
|
||
| One account has workspaces (organizations); an additional account is a personal account (no orgs). | ||
| Switching to it flips the surface to the personal layout. | ||
|
|
||
| <Story | ||
| name='MultipleSessions' | ||
| storyModule={UserButtonStories} | ||
| /> | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ## Parts | ||
|
|
||
| | Part | Description | | ||
| | ------------------- | ---------------------------------------------------------------------------- | | ||
| | `UserButtonRoot` | Owns the data + callbacks and forwards popover open state to `Popover.Root`. | | ||
| | `UserButtonTrigger` | The trigger: the active workspace's avatar, and nothing else. | | ||
| | `UserButtonPopup` | The popover surface: header, workspace list, additional accounts, footer. | | ||
|
|
||
| ## Styling | ||
|
|
||
| The component owns no slots of its own — it is styled entirely by the components it composes, so | ||
| `appearance.elements` targets those. Overriding `item` re-themes every row here _and_ everywhere else | ||
| `Item` is used; that is the point of composing rather than restyling. | ||
|
|
||
| | Component | Slots it contributes | | ||
| | --------- | ------------------------------------------------------------------------------------------------------------------------------------ | | ||
| | `Popover` | `popover-positioner`, `popover-popup` | | ||
| | `Card` | `card-root` | | ||
| | `Item` | `item`, `item-media`, `item-content`, `item-title`, `item-description`, `item-label`, `item-actions`, `item-group`, `item-separator` | | ||
| | `Avatar` | `avatar`, `avatar-image`, `avatar-fallback` | | ||
| | `Button` | `button` | | ||
| | `Icon` | `icon` | | ||
| | `Menu` | `menu-trigger`, `menu-positioner`, `menu-popup`, `menu-item`, `menu-separator` | | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve
__sourcewhen constructinguserButtonModule.user-button.stories.tsxexports__sourcevia its?rawself-import, but this import list (Lines 108-113) omits it, anduserButtonModule(Lines 185-190) never sets it. Any consumer ofStoryModule.__sourcefor the code-footer feature won't have it for UserButton.This mirrors the same unresolved concern raised previously for
accountButtonModule.🛠️ Proposed fix
import { + __source as userButtonSource, Default as UserButtonDefault, meta as userButtonMeta, MultipleSessions as UserButtonMultipleAccounts, Personal as UserButtonPersonal, } from '../stories/user-button.stories'; ... const userButtonModule: StoryModule = { meta: userButtonMeta, + __source: userButtonSource, Default: UserButtonDefault, Personal: UserButtonPersonal, MultipleSessions: UserButtonMultipleAccounts, };Also applies to: 185-190
🤖 Prompt for AI Agents