Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
098e946
test(ui): cover the connected UserButton end to end
alexcarpenter Aug 3, 2026
ed05dd9
feat(ui): close the UserButton popover only when a workspace is picked
alexcarpenter Aug 3, 2026
0f5ddc6
test(ui): query the account menu trigger as a button
alexcarpenter Aug 3, 2026
dbeca18
refactor(ui): migrate the Mosaic Spinner to StyleX and give it a sm size
alexcarpenter Aug 3, 2026
5839448
feat(ui): name the active workspace in the UserButton trigger
alexcarpenter Aug 3, 2026
77ffb1a
feat(ui): let combined UserButton lead with the organization or the a…
alexcarpenter Aug 3, 2026
fefe87f
test(ui): follow the UserButton active-organization contract in the c…
alexcarpenter Aug 4, 2026
bec13ea
test(ui): carry organizationMemberships on the connected UserButton u…
alexcarpenter Aug 4, 2026
7105476
fix(ui): hold the Mosaic UserButton surface still while an action runs
alexcarpenter Aug 4, 2026
d1a94db
test(ui): cover the UserButton profile modals end to end
alexcarpenter Aug 5, 2026
acaadf9
feat(ui): close the UserButton popover behind whatever it opens
alexcarpenter Aug 5, 2026
5a41ad1
test(ui): cover the UserButton personal workspace row end to end
alexcarpenter Aug 5, 2026
5c049cd
test(ui): cover the named UserButton personal row end to end
alexcarpenter Aug 5, 2026
2ed0a04
test(ui): move UserButton rendering cases down to the view test
alexcarpenter Aug 4, 2026
3644bcb
feat(ui): forward mode to the connected UserButton
alexcarpenter Aug 4, 2026
1d00541
test(ui): cover the UserButton create-organization modal end to end
alexcarpenter Aug 5, 2026
b8d5d3b
feat(ui): forward mode to the UserButton loading placeholder
alexcarpenter Aug 5, 2026
7704762
test(ui): assert Invite opens the InviteMembers modal
alexcarpenter Aug 5, 2026
178fb88
test(ui): identify Alice's account row by her username
alexcarpenter Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-user-button-action-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
21 changes: 21 additions & 0 deletions packages/ui/src/mosaic/hooks/__tests__/useSpinDelay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ describe('useSpinDelay', () => {
expect(result.current).toBeNull();
});

// Direct feedback on a click has nothing to debounce, so a zero delay must not cost a timer's
// worth of render passes before the spinner appears.
it('surfaces the value in the same pass when there is no delay to wait out', async () => {
const { result, rerender } = render(null, { delay: 0, minDuration: 200 });
await act(() => rerender({ value: 'a' }));

expect(result.current).toBe('a');
});

it('still holds a zero-delay value for minDuration', async () => {
const { result, rerender } = render(null, { delay: 0, minDuration: 200 });
await act(() => rerender({ value: 'a' }));
await act(() => rerender({ value: null }));

await advance(199);
expect(result.current).toBe('a');

await advance(1);
expect(result.current).toBeNull();
});

it('swaps to a new value immediately when one replaces another mid-show', async () => {
const { result, rerender } = render(null, { delay: 500, minDuration: 200 });
await act(() => rerender({ value: 'a' }));
Expand Down
10 changes: 8 additions & 2 deletions packages/ui/src/mosaic/hooks/useSpinDelay.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';

export interface SpinDelayOptions {
/** Wait this long before showing the value, so quick actions never flash a spinner. */
/** Wait this long before showing the value, so quick actions never flash a spinner. `0` shows it straight away. */
delay?: number;
/** Once shown, keep the value up at least this long, so the spinner never flickers off. */
minDuration?: number;
Expand All @@ -25,11 +25,17 @@ export function useSpinDelay<T>(value: T | null, options: SpinDelayOptions = {})
const shownAt = useRef(0);

useEffect(() => {
// Nothing showing yet: arm a timer so the value only surfaces if it outlasts `delay`.
// Nothing showing yet: arm a timer so the value only surfaces if it outlasts `delay`. With no
// delay there is nothing to outlast, so it surfaces in this pass rather than a timer's.
if (shown === null) {
if (value === null) {
return;
}
if (delay <= 0) {
shownAt.current = Date.now();
setShown(value);
return;
}
const timer = setTimeout(() => {
shownAt.current = Date.now();
setShown(value);
Expand Down
Loading
Loading