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}