-
Notifications
You must be signed in to change notification settings - Fork 464
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
Open
Changes from all commits
Commits
Show all changes
70 commits
Select commit
Hold shift + click to select a range
5854db7
feat(ui): add UserButton account/org switcher
alexcarpenter 46ebff2
feat(ui): give UserButton its own User section above Organization
alexcarpenter bfb25a1
refactor(ui): move UserButton into its own user-button/ folder
alexcarpenter 3403c8f
fix(ui): resolve UserButton lint errors
alexcarpenter ef7084d
refactor(ui): compose UserButton view from Mosaic Popover, Card, Item…
alexcarpenter f569458
fix(ui): show additional account emails on md rows in UserButton
alexcarpenter bca964e
fix(ui): drop the popover positioner z-index
alexcarpenter 4695ecb
feat(ui): render UserButton accounts as email rows with a menu
alexcarpenter 4ecb298
fix(headless): stop menu Escape from bubbling to the parent floating …
alexcarpenter 048f14e
fix(ui): make the UserButton trigger follow the avatar's shape
alexcarpenter 13513ef
feat(ui): give the Mosaic Popover trigger a themeable slot
alexcarpenter 6f9b525
refactor(ui): name the Mosaic radius tokens by size
alexcarpenter 3d7e1ef
avatar disable dragging
alexcarpenter 33fa922
default item color to neutral faded
alexcarpenter 3fae407
Update item.styles.ts
alexcarpenter 694bfcf
feat(headless): add alignOffset to Popover.Root
alexcarpenter 9b5ca15
feat(ui): add UserButton mode prop for combined, orgs, and user switc…
alexcarpenter b2f47ad
docs(ui): note UserButton mode in the changeset
alexcarpenter b509cfd
docs(repo): make the swingset UserButton examples live prototypes
alexcarpenter 348164b
fix(ui): stop rendering the active UserButton workspace as a button
alexcarpenter 6bcfd80
fix(ui): drop the separator above additional UserButton accounts
alexcarpenter 129e32f
fix(ui): put every UserButton switch row in one group
alexcarpenter d15bd3c
fix(ui): keep the account at the head of the combined UserButton surface
alexcarpenter 140374a
feat(ui): list every account's organizations in the UserButton
alexcarpenter 12a0482
fix(ui): give the Avatar fallback the image's radius
alexcarpenter 689c090
fix(ui): promote Item icons with the row, on interactive rows only
alexcarpenter 6f7ef1c
docs(swingset): render Item docs icons with the Icon component
alexcarpenter 7f2e343
style(ui): weight the UserButton account row email
alexcarpenter bc2a391
feat(ui): sign the UserButton footer with the Clerk logo
alexcarpenter c7f11f9
style(ui): give the UserButton branding row more breathing room
alexcarpenter e0c904c
feat(headless): hold Popover contents while it closes (#9310)
alexcarpenter db32919
refactor(ui): scope the UserButton switcher to fetchable data
alexcarpenter f1a4657
chore(ui): drop the UserButton changeset
alexcarpenter 0ede646
fix(ui): follow the StyleX Card migration
alexcarpenter 65c8cfc
docs(swingset): follow the Mosaic radius token rename
alexcarpenter a74d307
refactor(ui): rename the presentational UserButton to UserButtonView
alexcarpenter 87fcb8c
feat(ui): add Mosaic Spinner and a spin-delay hook
alexcarpenter a595062
feat(ui): add loading and busy states to UserButton
alexcarpenter db80b6a
fix(ui): keep pending workspaces reachable and avatars out of row names
alexcarpenter 6d3337c
feat(ui): close the UserButton popover only when a workspace is picked
alexcarpenter 0d8fd8e
refactor(ui): migrate the Mosaic Spinner to StyleX and give it a sm size
alexcarpenter 8ceeb57
feat(ui): rework the account-only UserButton surface
alexcarpenter bb43348
refactor(ui): sign out from a labelled header button and check the ac…
alexcarpenter 6b544e8
docs(swingset): trim the UserButton page
alexcarpenter 3bf207b
fix(ui): check the active account in the combined Accounts list, not …
alexcarpenter 0093193
feat(ui): name the active workspace in the UserButton trigger
alexcarpenter ba983c3
refactor(ui): split the UserButton trigger label into two props
alexcarpenter d71b3b8
feat(ui): let combined UserButton lead with the organization or the a…
alexcarpenter 4c60b70
docs(swingset): drop the Styling section from the UserButton page
alexcarpenter 92915c7
feat(ui): name the active organization from itself, and list accepted…
alexcarpenter 48d53ff
fix(ui): show the UserButton workspace list in one go rather than a l…
alexcarpenter c54c08e
fix(ui): take the user button trigger corner from its workspace
alexcarpenter 95d0034
fix(ui): drop the padding from the labelled UserButton trigger
alexcarpenter 7a97288
refactor(ui): render the UserButton trigger skeleton with StyleX
alexcarpenter 9a5fb7b
fix(ui): keep a UserButton row's avatar through its busy state
alexcarpenter 6a72f6b
feat(ui): scroll the UserButton workspace list through the shared scr…
alexcarpenter 56a7822
fix(ui): keep the UserButton account row on an account with no organi…
alexcarpenter b12b5c0
fix(ui): keep the UserButton account menu mounted while another actio…
alexcarpenter e82c73a
feat(ui): render the UserButton personal workspace row
alexcarpenter cd9a8a5
feat(ui): name the UserButton personal row "Personal account"
alexcarpenter 23fb11b
fix(ui): withhold the UserButton personal row when handed none
alexcarpenter 7fede63
test(ui): cover the UserButton rendering cases in the view test
alexcarpenter c435381
fix(ui): shape the UserButton loading placeholder like the workspace …
alexcarpenter 4f5d76b
fix(ui): apply the loading placeholder's shape to the block that pain…
alexcarpenter 35b52a9
fix(ui): keep a scrolled-into-view workspace row clear of the list's …
alexcarpenter e4e8c07
refactor(ui): name the UserButton account line by identifier, not email
alexcarpenter 698ad01
fix(ui): drop the UserButton identifier line when it repeats the title
alexcarpenter e242344
fix(repo): rename the swingset UserButton session fixture field to id…
alexcarpenter c9d10f3
fix(ui): use the scroll-padding longhands stylex accepts
alexcarpenter bb3a877
fix(ui): name the active UserButton row as the current one
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,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,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
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,72 @@ | ||
| import { cleanup, render, screen } from '@testing-library/react'; | ||
| import { afterEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { Freeze } from './freeze'; | ||
|
|
||
| afterEach(() => { | ||
| cleanup(); | ||
| }); | ||
|
|
||
| describe('Freeze', () => { | ||
| it('renders children while not frozen', () => { | ||
| render(<Freeze frozen={false}>Acme</Freeze>); | ||
|
|
||
| expect(screen.getByText('Acme')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('holds the committed DOM when children change while frozen', () => { | ||
| const { rerender } = render(<Freeze frozen={false}>Acme</Freeze>); | ||
|
|
||
| rerender(<Freeze frozen>Globex</Freeze>); | ||
|
|
||
| expect(screen.getByText('Acme')).toBeInTheDocument(); | ||
| expect(screen.queryByText('Globex')).toBeNull(); | ||
| }); | ||
|
|
||
| it('keeps the held DOM visible', () => { | ||
| const { rerender } = render(<Freeze frozen={false}>Acme</Freeze>); | ||
|
|
||
| rerender(<Freeze frozen>Globex</Freeze>); | ||
|
|
||
| expect(screen.getByText('Acme')).toBeVisible(); | ||
| }); | ||
|
|
||
| it('keeps the held DOM visible across further updates while frozen', () => { | ||
| const { rerender } = render(<Freeze frozen={false}>Acme</Freeze>); | ||
|
|
||
| rerender(<Freeze frozen>Globex</Freeze>); | ||
| rerender(<Freeze frozen>Initech</Freeze>); | ||
|
|
||
| expect(screen.getByText('Acme')).toBeVisible(); | ||
| }); | ||
|
|
||
| it('commits the pending children once unfrozen', () => { | ||
| const { rerender } = render(<Freeze frozen={false}>Acme</Freeze>); | ||
|
|
||
| rerender(<Freeze frozen>Globex</Freeze>); | ||
| rerender(<Freeze frozen={false}>Globex</Freeze>); | ||
|
|
||
| expect(screen.getByText('Globex')).toBeInTheDocument(); | ||
| expect(screen.queryByText('Acme')).toBeNull(); | ||
| }); | ||
|
|
||
| it('holds state updates raised from inside the frozen subtree', () => { | ||
| function Counter({ count }: { count: number }) { | ||
| return <span>count: {count}</span>; | ||
| } | ||
|
|
||
| const { rerender } = render( | ||
| <Freeze frozen={false}> | ||
| <Counter count={0} /> | ||
| </Freeze>, | ||
| ); | ||
|
|
||
| rerender( | ||
| <Freeze frozen> | ||
| <Counter count={1} /> | ||
| </Freeze>, | ||
| ); | ||
|
|
||
| expect(screen.getByText('count: 0')).toBeInTheDocument(); | ||
| }); | ||
| }); |
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,64 @@ | ||
| 'use client'; | ||
|
|
||
| import * as React from 'react'; | ||
|
|
||
| /** | ||
| * Never settles. Throwing it suspends the enclosing boundary indefinitely: React keeps | ||
| * rendering the subtree but holds the commit, so the DOM keeps painting its last frame. | ||
| */ | ||
| const never = new Promise<never>(() => {}); | ||
|
|
||
| function Suspend(): null { | ||
| // eslint-disable-next-line @typescript-eslint/only-throw-error -- Suspending is React's thrown-thenable protocol, not an error. `React.use()` would say this more plainly but needs React 19.2; this package supports React 18. | ||
| throw never; | ||
| } | ||
|
|
||
| export interface FreezeProps { | ||
| /** While `true`, the DOM below holds whatever it last committed. */ | ||
| frozen: boolean; | ||
| children?: React.ReactNode; | ||
| } | ||
|
|
||
| /** | ||
| * Holds its subtree's DOM at the last committed frame while `frozen`. Renders keep | ||
| * happening, they just don't reach the DOM; the pending one commits when `frozen` flips | ||
| * back to `false`. | ||
| * | ||
| * Use it to stop content from visibly changing under an exit animation — a popover that | ||
| * closes because the thing it was showing changed would otherwise swap its contents on the | ||
| * way out. | ||
| */ | ||
| export function Freeze({ frozen, children }: FreezeProps) { | ||
| const contentRef = React.useRef<HTMLDivElement | null>(null); | ||
|
|
||
| // Hold onto the node ourselves rather than reading a plain ref: hiding a boundary's children | ||
| // detaches their refs, so by the time the effect below runs a normal ref reads `null`. | ||
| const setContent = React.useCallback((node: HTMLDivElement | null) => { | ||
| if (node) { | ||
| contentRef.current = node; | ||
| } | ||
| }, []); | ||
|
|
||
| // React hides a suspended boundary's host children with `display: none !important`, which is | ||
| // the opposite of what this is for. Undo it on the commit that applies it: insertion effects | ||
| // run after the boundary's mutation and before paint, so the held frame never blinks out. | ||
| // `display: contents` is also what the wrapper renders with, so React puts it back on unfreeze | ||
| // and the wrapper stays out of the layout it is spliced into. | ||
| React.useInsertionEffect(() => { | ||
| if (frozen) { | ||
| contentRef.current?.style.setProperty('display', 'contents'); | ||
| } | ||
| }, [frozen]); | ||
|
|
||
| return ( | ||
| <React.Suspense fallback={null}> | ||
| {frozen ? <Suspend /> : null} | ||
| <div | ||
| ref={setContent} | ||
| style={{ display: 'contents' }} | ||
| > | ||
| {children} | ||
| </div> | ||
| </React.Suspense> | ||
| ); | ||
| } |
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
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