fix(desktop): polish status editor interactions

Keep status editing focused in the dialog while preserving the profile card hit area and simplifying workspace menu branches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-06-03 13:59:04 -04:00
co-authored by Cursor
parent 524142c05e
commit 180b1ccfe1
6 changed files with 65 additions and 73 deletions
-3
View File
@@ -797,9 +797,6 @@ export function AppShell() {
onSetUserStatus={(text, emoji) =>
setUserStatusMutation.mutate({ text, emoji })
}
onClearUserStatus={() =>
setUserStatusMutation.mutate({ text: "", emoji: "" })
}
profile={profileQuery.data}
selfUserStatus={
deferredPubkey
@@ -23,7 +23,6 @@ interface ProfilePopoverProps {
userStatusEmoji?: string;
onSetStatus: (status: PresenceStatus) => void;
onSetUserStatus: (text: string, emoji: string) => void;
onClearUserStatus: () => void;
onOpenSettings: (section?: "profile" | "appearance") => void;
children: React.ReactNode;
// Optional outer container whose clicks should NOT close the popover.
@@ -60,7 +59,6 @@ export function ProfilePopover({
userStatusEmoji,
onSetStatus,
onSetUserStatus,
onClearUserStatus,
onOpenSettings,
children,
triggerContainerRef,
@@ -292,7 +290,6 @@ export function ProfilePopover({
hasExistingStatus={hasUserStatus}
initialEmoji={userStatusEmoji}
initialText={userStatusText}
onClear={onClearUserStatus}
onOpenChange={setStatusDialogOpen}
onSave={onSetUserStatus}
open={statusDialogOpen}
@@ -138,7 +138,6 @@ type AppSidebarProps = {
onSelectSettings: (section?: "profile" | "appearance") => void;
onSetPresenceStatus?: (status: "online" | "away" | "offline") => void;
onSetUserStatus: (text: string, emoji: string) => void;
onClearUserStatus: () => void;
onSwitchWorkspace: (id: string) => void;
selfUserStatus?: UserStatus;
isPresencePending?: boolean;
@@ -193,7 +192,6 @@ export function AppSidebar({
onSelectSettings,
onSetPresenceStatus,
onSetUserStatus,
onClearUserStatus,
onSwitchWorkspace,
selfUserStatus,
isPresencePending,
@@ -662,7 +660,6 @@ export function AppSidebar({
<SidebarProfileCard
activeWorkspace={activeWorkspace}
isPresencePending={isPresencePending}
onClearUserStatus={onClearUserStatus}
onOpenAddWorkspace={onOpenAddWorkspace}
onOpenSettings={onSelectSettings}
onRemoveWorkspace={onRemoveWorkspace}
@@ -12,7 +12,6 @@ import { cn } from "@/shared/lib/cn";
type SidebarProfileCardProps = {
activeWorkspace: Workspace | null;
isPresencePending?: boolean;
onClearUserStatus: () => void;
onOpenAddWorkspace: () => void;
onOpenSettings: (section?: "profile" | "appearance") => void;
onRemoveWorkspace: (id: string) => void;
@@ -33,7 +32,6 @@ type SidebarProfileCardProps = {
export function SidebarProfileCard({
activeWorkspace,
isPresencePending,
onClearUserStatus,
onOpenAddWorkspace,
onOpenSettings,
onRemoveWorkspace,
@@ -53,10 +51,23 @@ export function SidebarProfileCard({
() => setProfilePopoverOpen((prev) => !prev),
[],
);
const handleCardClick = React.useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
const target = event.target;
if (
!(target instanceof Node) ||
!profileCardRef.current?.contains(target)
) {
return;
}
toggleProfilePopover();
},
[toggleProfilePopover],
);
const hasStatus = Boolean(selfUserStatus?.text || selfUserStatus?.emoji);
const workspaceLabel = activeWorkspace?.name ?? "No workspace";
const readonlyWorkspaceLabel = (
<span className="flex min-w-0 items-center gap-1 text-xs leading-snug text-sidebar-foreground/70">
<span className="flex min-w-0 cursor-pointer items-center gap-1 text-xs leading-snug text-sidebar-foreground/70">
<span aria-hidden="true" className="shrink-0 text-[10px] leading-none">
🌱
</span>
@@ -65,9 +76,11 @@ export function SidebarProfileCard({
);
return (
// biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: child buttons provide keyboard access; wrapper fills pointer gaps between them.
<div
className="group/profile-card rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-border/35 dark:hover:bg-sidebar-border/30"
className="group/profile-card cursor-pointer rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-border/35 dark:hover:bg-sidebar-border/30"
data-testid="sidebar-profile-card"
onClick={handleCardClick}
ref={profileCardRef}
>
<div className="flex min-w-0 items-center gap-3">
@@ -75,7 +88,10 @@ export function SidebarProfileCard({
aria-label={`Open profile menu for ${resolvedDisplayName}`}
className="relative shrink-0 rounded-xl outline-hidden focus:outline-none focus-visible:outline-none"
data-testid="sidebar-profile-avatar-button"
onClick={toggleProfilePopover}
onClick={(event) => {
event.stopPropagation();
toggleProfilePopover();
}}
type="button"
>
<ProfileAvatar
@@ -107,7 +123,6 @@ export function SidebarProfileCard({
userStatusEmoji={selfUserStatus?.emoji}
onSetStatus={onSetPresenceStatus ?? (() => {})}
onSetUserStatus={onSetUserStatus}
onClearUserStatus={onClearUserStatus}
onOpenSettings={onOpenSettings}
triggerContainerRef={profileCardRef}
workspaceSwitcherSlot={
@@ -123,6 +138,10 @@ export function SidebarProfileCard({
}
>
<button
onClick={(event) => {
event.stopPropagation();
toggleProfilePopover();
}}
className="block w-full min-w-0 rounded-sm text-left text-sidebar-foreground outline-hidden focus:outline-none focus-visible:outline-none"
data-testid="open-settings"
type="button"
@@ -145,7 +164,10 @@ export function SidebarProfileCard({
profilePopoverOpen && "opacity-100",
)}
data-testid="sidebar-profile-user-status"
onClick={toggleProfilePopover}
onClick={(event) => {
event.stopPropagation();
toggleProfilePopover();
}}
type="button"
>
{selfUserStatus?.emoji ? (
@@ -2,6 +2,7 @@ import * as React from "react";
import Picker from "@emoji-mart/react";
import data from "@emoji-mart/data";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { X } from "lucide-react";
import {
Dialog,
@@ -32,7 +33,6 @@ type SetStatusDialogProps = {
initialText?: string;
initialEmoji?: string;
onSave: (text: string, emoji: string) => void;
onClear: () => void;
hasExistingStatus: boolean;
};
@@ -46,7 +46,6 @@ export function SetStatusDialog({
initialText = "",
initialEmoji = "",
onSave,
onClear,
hasExistingStatus,
}: SetStatusDialogProps) {
const [text, setText] = React.useState(initialText);
@@ -75,9 +74,9 @@ export function SetStatusDialog({
onOpenChange(false);
}
function handleClear() {
onClear();
onOpenChange(false);
function handleClearDraft() {
setText("");
setEmoji("");
}
function handleKeyDown(event: React.KeyboardEvent) {
@@ -101,36 +100,21 @@ export function SetStatusDialog({
</DialogHeader>
<div className="flex flex-col gap-4 pt-2">
<div className="flex items-center gap-2">
<div className="relative">
<Popover onOpenChange={setPickerOpen} open={pickerOpen}>
<div className="relative shrink-0">
<PopoverTrigger asChild>
<button
aria-label="Choose status emoji"
className="flex h-9 w-9 items-center justify-center rounded-md border border-input text-lg transition-colors hover:bg-accent"
type="button"
>
{emoji || "\uD83D\uDCAC"}
</button>
</PopoverTrigger>
{emoji ? (
<button
aria-label="Clear status emoji"
className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full border border-background bg-muted text-[10px] leading-none text-muted-foreground hover:bg-accent hover:text-foreground"
onClick={(event) => {
event.stopPropagation();
setEmoji("");
}}
type="button"
>
×
</button>
) : null}
</div>
<PopoverTrigger asChild>
<button
aria-label="Choose status emoji"
className="absolute left-1.5 top-1/2 z-10 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-lg transition-colors hover:bg-accent"
type="button"
>
{emoji || "\uD83D\uDCAC"}
</button>
</PopoverTrigger>
<PopoverPrimitive.Content
align="start"
className="z-50 w-auto overflow-hidden rounded-2xl bg-transparent shadow-none outline-hidden"
sideOffset={4}
className="z-50 w-auto overflow-hidden rounded-2xl shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
>
<Picker
data={data}
@@ -146,12 +130,24 @@ export function SetStatusDialog({
</Popover>
<Input
autoFocus
className="pl-10 pr-9"
data-testid="set-status-input"
onChange={(event) => setText(event.target.value)}
onKeyDown={handleKeyDown}
placeholder="What's your status?"
value={text}
/>
{hasExistingStatus || text || emoji ? (
<button
aria-label="Clear status"
className="absolute right-1.5 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
data-testid="set-status-clear"
onClick={handleClearDraft}
type="button"
>
<X className="h-4 w-4" />
</button>
) : null}
</div>
<div className="flex flex-wrap gap-1.5">
@@ -168,20 +164,7 @@ export function SetStatusDialog({
))}
</div>
<div className="flex items-center justify-between gap-2 pt-1">
<div>
{hasExistingStatus ? (
<Button
data-testid="set-status-clear"
onClick={handleClear}
size="sm"
type="button"
variant="ghost"
>
Clear status
</Button>
) : null}
</div>
<div className="flex items-center justify-end gap-2 pt-1">
<div className="flex items-center gap-2">
<Button
data-testid="set-status-cancel"
@@ -68,7 +68,7 @@ export function WorkspaceSwitcher({
const connectionState = useRelayConnection();
const degraded = isRelayConnectionDegraded(connectionState);
const connectionLabel = CONNECTION_STATE_LABEL[connectionState];
const isProfileLike = variant === "profile" || variant === "profile-menu";
const isProfileVariant = variant === "profile";
function clearProfileMenuHoverTimer() {
if (profileMenuHoverTimer.current !== null) {
@@ -114,24 +114,24 @@ export function WorkspaceSwitcher({
<span
aria-hidden="false"
className={
isProfileLike
isProfileVariant
? "flex h-5 w-5 shrink-0 animate-pulse items-center justify-center rounded-md border border-sidebar-border/70 bg-sidebar-accent/40 text-destructive"
: "flex h-5 w-5 shrink-0 animate-pulse items-center justify-center text-destructive"
}
data-testid="relay-connection-warning"
role="img"
>
<WifiOff className={isProfileLike ? "h-3 w-3" : "h-4 w-4"} />
<WifiOff className={isProfileVariant ? "h-3 w-3" : "h-4 w-4"} />
</span>
</TooltipTrigger>
<TooltipContent side={isProfileLike ? "top" : "bottom"}>
<TooltipContent side={isProfileVariant ? "top" : "bottom"}>
{connectionLabel}
</TooltipContent>
</Tooltip>
) : (
<span
className={
isProfileLike
isProfileVariant
? "flex h-5 w-5 shrink-0 items-center justify-center rounded-md border border-sidebar-border/70 bg-sidebar-accent/40 text-[10px] leading-none"
: "flex h-5 w-5 shrink-0 items-center justify-center text-xs leading-none"
}
@@ -153,7 +153,7 @@ export function WorkspaceSwitcher({
) : (
<ChevronDown
className={
isProfileLike
isProfileVariant
? "h-3 w-3 shrink-0 text-sidebar-foreground/45"
: "h-3.5 w-3.5 shrink-0 text-sidebar-foreground/50"
}
@@ -251,18 +251,14 @@ export function WorkspaceSwitcher({
const switcherDropdown = (
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
{isProfileLike ? (
{variant === "profile" ? (
<button
aria-label={
degraded
? `${activeWorkspace?.name ?? "Workspace"} — ${connectionLabel}`
: "Switch workspace"
}
className={
variant === "profile-menu"
? "flex w-full min-w-0 items-center gap-3 py-0.5 text-left text-sm text-popover-foreground outline-hidden transition-colors hover:text-popover-foreground focus:outline-none focus-visible:outline-none data-[state=open]:text-popover-foreground"
: "flex min-w-0 max-w-full items-center gap-1.5 rounded-md py-0.5 text-left text-xs text-sidebar-foreground/50 outline-hidden transition-colors hover:text-sidebar-foreground focus:outline-none focus-visible:outline-none data-[state=open]:text-sidebar-foreground"
}
className="flex min-w-0 max-w-full items-center gap-1.5 rounded-md py-0.5 text-left text-xs text-sidebar-foreground/50 outline-hidden transition-colors hover:text-sidebar-foreground focus:outline-none focus-visible:outline-none data-[state=open]:text-sidebar-foreground"
data-testid="workspace-switcher"
type="button"
>