diff --git a/.changeset/mosaic-user-button-trigger-hover.md b/.changeset/mosaic-user-button-trigger-hover.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/mosaic-user-button-trigger-hover.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/packages/swingset/src/stories/user-button.mdx b/packages/swingset/src/stories/user-button.mdx
index f32bbdc1671..9d0d40f5f59 100644
--- a/packages/swingset/src/stories/user-button.mdx
+++ b/packages/swingset/src/stories/user-button.mdx
@@ -83,6 +83,13 @@ trigger, no selection when personal is hidden and none is active, the account ot
storyModule={UserButtonStories}
/>
+A user's avatar on its own rounds the trigger fully to match it.
+
+
+
`renderTriggerBadge={false}` keeps the name. The badge is part of the label, so it needs both.
) {
);
}
+export function UserAvatarOnly(_args: Record) {
+ const prototype = usePrototype();
+
+ return (
+
+ );
+}
+
export function WithoutTriggerBadge(_args: Record) {
const prototype = usePrototype();
diff --git a/packages/ui/src/mosaic/user-button/__tests__/user-button.view.test.tsx b/packages/ui/src/mosaic/user-button/__tests__/user-button.view.test.tsx
index 8d0b3b87699..e2fbecf1ac4 100644
--- a/packages/ui/src/mosaic/user-button/__tests__/user-button.view.test.tsx
+++ b/packages/ui/src/mosaic/user-button/__tests__/user-button.view.test.tsx
@@ -865,7 +865,32 @@ describe('UserButtonTrigger', () => {
expect(screen.queryByText('Pro')).toBeNull();
});
- it('takes its corner from the workspace it names, labelled or not', () => {
+ it('ends the label with a caret, and carries none for the avatar alone', () => {
+ const caret = (props: Partial) => {
+ const { unmount } = renderTrigger(props);
+ const found = screen.getByRole('button', { name: /Open account menu/ }).querySelector('.cl-icon');
+ unmount();
+ return found;
+ };
+
+ expect(caret({ mode: 'organization' })).not.toBeNull();
+ expect(caret({ mode: 'organization', renderTriggerLabel: false })).toBeNull();
+ });
+
+ it('shrinks the avatar beside a label, and fills the trigger with it alone', () => {
+ const avatarSize = (props: Partial) => {
+ const { unmount } = renderTrigger(props);
+ const avatar = screen.getByRole('button', { name: /Open account menu/ }).querySelector('.cl-avatar');
+ const size = avatar?.getAttribute('data-size');
+ unmount();
+ return size;
+ };
+
+ expect(avatarSize({ mode: 'organization' })).toBe('xs');
+ expect(avatarSize({ mode: 'organization', renderTriggerLabel: false })).toBe('sm');
+ });
+
+ it('rounds fully only around a user avatar on its own', () => {
const corner = (props: Partial) => {
const { unmount } = renderTrigger(props);
const className = screen.getByRole('button', { name: /Open account menu/ }).className;
@@ -873,7 +898,7 @@ describe('UserButtonTrigger', () => {
return className;
};
- expect(corner({ mode: 'organization' })).not.toEqual(corner({ mode: 'user' }));
+ expect(corner({ mode: 'organization' })).toEqual(corner({ mode: 'user' }));
expect(corner({ mode: 'organization', renderTriggerLabel: false })).not.toEqual(
corner({ mode: 'user', renderTriggerLabel: false }),
);
diff --git a/packages/ui/src/mosaic/user-button/user-button.styles.ts b/packages/ui/src/mosaic/user-button/user-button.styles.ts
index 779142151f7..904eb7526fd 100644
--- a/packages/ui/src/mosaic/user-button/user-button.styles.ts
+++ b/packages/ui/src/mosaic/user-button/user-button.styles.ts
@@ -1,22 +1,33 @@
import * as stylex from '@stylexjs/stylex';
-import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../tokens.stylex';
+import { colorVars, durationVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../tokens.stylex';
export const styles = stylex.create({
- // The avatar is the trigger, so the button paints nothing of its own.
trigger: {
padding: 0,
+ borderRadius: radiusVars['--cl-radius-md'],
borderStyle: 'none',
- backgroundColor: 'transparent',
+ alignItems: 'center',
+ backgroundColor: {
+ default: 'transparent',
+ ':is([data-open])': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`,
+ '@media (hover: hover)': {
+ ':hover': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`,
+ },
+ },
cursor: 'pointer',
display: 'inline-flex',
+ transitionDuration: durationVars['--cl-duration-base'],
+ transitionProperty: 'background-color',
},
- // A labelled trigger sits in a host app's chrome, so it stays flush like the avatar-only form
- // and only spaces the avatar from its text.
triggerLabelled: {
- gap: space['2'],
- alignItems: 'center',
+ padding: space['1'],
+ gap: space['1.5'],
+ },
+
+ triggerRound: {
+ borderRadius: radiusVars['--cl-radius-full'],
},
// Matches `Item.Label`, so the trigger names a workspace the same way its row does. Capped,
@@ -29,6 +40,11 @@ export const styles = stylex.create({
maxWidth: '12rem',
},
+ triggerCaret: {
+ '--_cl-icon-color': colorVars['--cl-color-neutral-faded'],
+ marginInlineEnd: space['1'],
+ },
+
// The workspace list scrolls; the header and footer stay put. The scroll area carries the
// overflow, the edge fades, the scrollbar and the scroll padding they need, so only the cap
// is ours.
@@ -44,10 +60,3 @@ export const styles = stylex.create({
width: space['7'],
},
});
-
-// The trigger takes the corner of the workspace mark it carries: round for a person, squared for
-// an organization. Rounding it fully would draw a circle around a square mark, labelled or not.
-export const triggerShapes = stylex.create({
- circle: { borderRadius: radiusVars['--cl-radius-full'] },
- square: { borderRadius: radiusVars['--cl-radius-md'] },
-});
diff --git a/packages/ui/src/mosaic/user-button/user-button.view.tsx b/packages/ui/src/mosaic/user-button/user-button.view.tsx
index a2d34b53b1f..369e348d902 100644
--- a/packages/ui/src/mosaic/user-button/user-button.view.tsx
+++ b/packages/ui/src/mosaic/user-button/user-button.view.tsx
@@ -25,7 +25,7 @@ import { truncationStyles } from '../utils/typography.styles';
import type { UserButtonLayout } from './user-button.layout';
import { resolveUserButtonLayout } from './user-button.layout';
import { fill, plural, userButtonBase as m } from './user-button.messages';
-import { styles, triggerShapes } from './user-button.styles';
+import { styles } from './user-button.styles';
import type {
UserButtonBrandingProps,
UserButtonBusyState,
@@ -1086,19 +1086,25 @@ export function UserButtonTrigger({
focusOutline.visible,
styles.trigger,
renderTriggerLabel ? styles.triggerLabelled : null,
- triggerShapes[shape],
+ !renderTriggerLabel && shape === 'circle' ? styles.triggerRound : null,
)}
>
{renderTriggerLabel ? (
<>
{name}
{planLabel ? {planLabel} : null}
+
>
) : null}