fix: restore wrapper View for sidebar hover stability

The wrapper View with onPointerEnter/onPointerLeave is needed to
keep hover state stable when the mouse moves from the row to the
kebab dropdown (which opens in an overlay outside the Pressable).
Without it, the Pressable fires onHoverOut and the kebab disappears.

On native, isNative makes controls always-visible so this is
web-only behavior which is correct (onPointerEnter works on web).
This commit is contained in:
Mohamed Boudra
2026-04-13 13:39:35 +07:00
parent 42aaa4e418
commit 8008ea69dd
@@ -851,8 +851,31 @@ function ProjectHeaderRow({
if (menuController) { if (menuController) {
return ( return (
<ContextMenuTrigger <View onPointerEnter={() => setIsHovered(true)} onPointerLeave={() => setIsHovered(false)}>
enabledOnMobile={false} <ContextMenuTrigger
enabledOnMobile={false}
style={({ pressed }) => [
styles.projectRow,
isDragging && styles.projectRowDragging,
selected && styles.sidebarRowSelected,
isHovered && styles.projectRowHovered,
pressed && styles.projectRowPressed,
]}
onPressIn={interaction.handlePressIn}
onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut}
onPress={handlePress}
testID={`sidebar-project-row-${project.projectKey}`}
>
{rowChildren}
</ContextMenuTrigger>
</View>
);
}
return (
<View onPointerEnter={() => setIsHovered(true)} onPointerLeave={() => setIsHovered(false)}>
<Pressable
style={({ pressed }) => [ style={({ pressed }) => [
styles.projectRow, styles.projectRow,
isDragging && styles.projectRowDragging, isDragging && styles.projectRowDragging,
@@ -860,8 +883,6 @@ function ProjectHeaderRow({
isHovered && styles.projectRowHovered, isHovered && styles.projectRowHovered,
pressed && styles.projectRowPressed, pressed && styles.projectRowPressed,
]} ]}
onHoverIn={() => setIsHovered(true)}
onHoverOut={() => setIsHovered(false)}
onPressIn={interaction.handlePressIn} onPressIn={interaction.handlePressIn}
onTouchMove={interaction.handleTouchMove} onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut} onPressOut={interaction.handlePressOut}
@@ -869,29 +890,8 @@ function ProjectHeaderRow({
testID={`sidebar-project-row-${project.projectKey}`} testID={`sidebar-project-row-${project.projectKey}`}
> >
{rowChildren} {rowChildren}
</ContextMenuTrigger> </Pressable>
); </View>
}
return (
<Pressable
style={({ pressed }) => [
styles.projectRow,
isDragging && styles.projectRowDragging,
selected && styles.sidebarRowSelected,
isHovered && styles.projectRowHovered,
pressed && styles.projectRowPressed,
]}
onHoverIn={() => setIsHovered(true)}
onHoverOut={() => setIsHovered(false)}
onPressIn={interaction.handlePressIn}
onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut}
onPress={handlePress}
testID={`sidebar-project-row-${project.projectKey}`}
>
{rowChildren}
</Pressable>
); );
} }
@@ -937,118 +937,121 @@ function WorkspaceRowInner({
}, [interaction.didLongPressRef, onPress]); }, [interaction.didLongPressRef, onPress]);
return ( return (
<Pressable <View
disabled={isArchiving} style={styles.workspaceRowContainer}
style={({ pressed }) => [ onPointerEnter={() => setIsHovered(true)}
styles.workspaceRowContainer, onPointerLeave={() => setIsHovered(false)}
styles.workspaceRow,
isDragging && styles.workspaceRowDragging,
selected && styles.sidebarRowSelected,
isHovered && styles.workspaceRowHovered,
pressed && styles.workspaceRowPressed,
]}
onHoverIn={() => setIsHovered(true)}
onHoverOut={() => setIsHovered(false)}
onPressIn={interaction.handlePressIn}
onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut}
onPress={handlePress}
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
> >
<View style={styles.workspaceRowMain}> <Pressable
<View disabled={isArchiving}
{...(dragHandleProps?.attributes as any)} style={({ pressed }) => [
{...(dragHandleProps?.listeners as any)} styles.workspaceRow,
ref={dragHandleProps?.setActivatorNodeRef as any} isDragging && styles.workspaceRowDragging,
style={styles.workspaceRowLeft} selected && styles.sidebarRowSelected,
> isHovered && styles.workspaceRowHovered,
<WorkspaceStatusIndicator pressed && styles.workspaceRowPressed,
bucket={workspace.statusBucket} ]}
workspaceKind={workspace.workspaceKind} onPressIn={interaction.handlePressIn}
loading={isArchiving || isCreating} onTouchMove={interaction.handleTouchMove}
/> onPressOut={interaction.handlePressOut}
<Text onPress={handlePress}
style={[ testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
styles.workspaceBranchText, >
isHovered && styles.workspaceBranchTextHovered, <View style={styles.workspaceRowMain}>
isCreating && styles.workspaceBranchTextCreating, <View
]} {...(dragHandleProps?.attributes as any)}
numberOfLines={1} {...(dragHandleProps?.listeners as any)}
ref={dragHandleProps?.setActivatorNodeRef as any}
style={styles.workspaceRowLeft}
> >
{workspace.name} <WorkspaceStatusIndicator
</Text> bucket={workspace.statusBucket}
</View> workspaceKind={workspace.workspaceKind}
<View style={styles.workspaceRowRight}> loading={isArchiving || isCreating}
{isCreating ? <Text style={styles.workspaceCreatingText}>Creating...</Text> : null} />
{onArchive && (isHovered || platformIsNative || isCompact) ? ( <Text
<DropdownMenu> style={[
<DropdownMenuTrigger styles.workspaceBranchText,
hitSlop={8} isHovered && styles.workspaceBranchTextHovered,
style={({ hovered = false }) => [ isCreating && styles.workspaceBranchTextCreating,
styles.kebabButton, ]}
hovered && styles.kebabButtonHovered, numberOfLines={1}
]} >
accessibilityRole="button" {workspace.name}
accessibilityLabel="Workspace actions" </Text>
testID={`sidebar-workspace-kebab-${workspace.workspaceKey}`} </View>
> <View style={styles.workspaceRowRight}>
{({ hovered }) => ( {isCreating ? <Text style={styles.workspaceCreatingText}>Creating...</Text> : null}
<MoreVertical {onArchive && (isHovered || platformIsNative || isCompact) ? (
size={14} <DropdownMenu>
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} <DropdownMenuTrigger
/> hitSlop={8}
)} style={({ hovered = false }) => [
</DropdownMenuTrigger> styles.kebabButton,
<DropdownMenuContent align="end" width={260}> hovered && styles.kebabButtonHovered,
{onCopyPath ? ( ]}
<DropdownMenuItem accessibilityRole="button"
testID={`sidebar-workspace-menu-copy-path-${workspace.workspaceKey}`} accessibilityLabel="Workspace actions"
leading={<Copy size={14} color={theme.colors.foregroundMuted} />} testID={`sidebar-workspace-kebab-${workspace.workspaceKey}`}
onSelect={onCopyPath}
>
Copy path
</DropdownMenuItem>
) : null}
{onCopyBranchName ? (
<DropdownMenuItem
testID={`sidebar-workspace-menu-copy-branch-name-${workspace.workspaceKey}`}
leading={<Copy size={14} color={theme.colors.foregroundMuted} />}
onSelect={onCopyBranchName}
>
Copy branch name
</DropdownMenuItem>
) : null}
<DropdownMenuItem
testID={`sidebar-workspace-menu-archive-${workspace.workspaceKey}`}
leading={<Archive size={14} color={theme.colors.foregroundMuted} />}
trailing={archiveShortcutKeys ? <Shortcut chord={archiveShortcutKeys} /> : null}
status={archiveStatus}
pendingLabel={archivePendingLabel}
onSelect={onArchive}
> >
{archiveLabel ?? "Archive"} {({ hovered }) => (
</DropdownMenuItem> <MoreVertical
</DropdownMenuContent> size={14}
</DropdownMenu> color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
) : workspace.diffStat ? ( />
<View style={styles.diffStatRow}> )}
<Text style={styles.diffStatAdditions}>+{workspace.diffStat.additions}</Text> </DropdownMenuTrigger>
<Text style={styles.diffStatDeletions}>-{workspace.diffStat.deletions}</Text> <DropdownMenuContent align="end" width={260}>
</View> {onCopyPath ? (
) : null} <DropdownMenuItem
{showShortcutBadge && shortcutNumber !== null ? ( testID={`sidebar-workspace-menu-copy-path-${workspace.workspaceKey}`}
<View style={styles.shortcutBadge}> leading={<Copy size={14} color={theme.colors.foregroundMuted} />}
<Text style={styles.shortcutBadgeText}>{shortcutNumber}</Text> onSelect={onCopyPath}
</View> >
) : null} Copy path
</DropdownMenuItem>
) : null}
{onCopyBranchName ? (
<DropdownMenuItem
testID={`sidebar-workspace-menu-copy-branch-name-${workspace.workspaceKey}`}
leading={<Copy size={14} color={theme.colors.foregroundMuted} />}
onSelect={onCopyBranchName}
>
Copy branch name
</DropdownMenuItem>
) : null}
<DropdownMenuItem
testID={`sidebar-workspace-menu-archive-${workspace.workspaceKey}`}
leading={<Archive size={14} color={theme.colors.foregroundMuted} />}
trailing={archiveShortcutKeys ? <Shortcut chord={archiveShortcutKeys} /> : null}
status={archiveStatus}
pendingLabel={archivePendingLabel}
onSelect={onArchive}
>
{archiveLabel ?? "Archive"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : workspace.diffStat ? (
<View style={styles.diffStatRow}>
<Text style={styles.diffStatAdditions}>+{workspace.diffStat.additions}</Text>
<Text style={styles.diffStatDeletions}>-{workspace.diffStat.deletions}</Text>
</View>
) : null}
{showShortcutBadge && shortcutNumber !== null ? (
<View style={styles.shortcutBadge}>
<Text style={styles.shortcutBadgeText}>{shortcutNumber}</Text>
</View>
) : null}
</View>
</View> </View>
</View> {prHint ? (
{prHint ? ( <View style={styles.workspacePrBadgeRow}>
<View style={styles.workspacePrBadgeRow}> <WorkspacePrBadge hint={prHint} />
<WorkspacePrBadge hint={prHint} /> </View>
</View> ) : null}
) : null} </Pressable>
</Pressable> </View>
); );
} }