From d7b3ff654d88f2c7dabd1840b4a66e2441ab059a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 16 Oct 2026 20:46:29 +0700 Subject: [PATCH] Show press feedback on sidebar rows, footer buttons, and settings items (#7150) * fix(app): show press feedback on sidebar rows, footer icons, and settings tiles Touch has no hover, so these controls gave no acknowledgement when pressed. Rows take the workspace row's pressed fill; the sidebar header row paints it on the row so the trailing slot lights up with the button. Footer icon buttons take the round header-control highlight. * style(app): format sidebar header row * fix(app): keep footer icon corners tappable on web Web hit-testing follows border-radius, so rounding the footer buttons turned their 44pt square targets into circles and taps near the corners fell through. Paint the round highlight as a layer inside the square button instead. --- packages/app/src/components/left-sidebar.tsx | 18 +++++++------ .../sidebar/sidebar-footer-icon-highlight.tsx | 25 +++++++++++++++++++ .../components/sidebar/sidebar-header-row.tsx | 21 ++++++++++++++-- .../components/sidebar/sidebar-help-menu.tsx | 16 +++++++----- packages/app/src/screens/settings-screen.tsx | 24 +++++++++++++++--- 5 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 packages/app/src/components/sidebar/sidebar-footer-icon-highlight.tsx diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index f054668347..943c1edd27 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -32,6 +32,7 @@ import { HostPicker } from "@/components/hosts/host-picker"; import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/display-preferences/menu"; import { SidebarSeparator } from "@/components/sidebar/sidebar-separator"; import { SidebarNavRows } from "@/components/sidebar/sidebar-nav-rows"; +import { SidebarFooterIconHighlight } from "@/components/sidebar/sidebar-footer-icon-highlight"; import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu"; import { SidebarResizeHandle } from "@/components/sidebar-resize-handle"; import { DensityIcon } from "@/components/ui/density-icon"; @@ -317,13 +318,16 @@ function FooterIconButton({ accessibilityRole="button" onPress={onPress} > - {({ hovered }) => ( - + {({ hovered, pressed }) => ( + <> + + + )} diff --git a/packages/app/src/components/sidebar/sidebar-footer-icon-highlight.tsx b/packages/app/src/components/sidebar/sidebar-footer-icon-highlight.tsx new file mode 100644 index 0000000000..7b901693fa --- /dev/null +++ b/packages/app/src/components/sidebar/sidebar-footer-icon-highlight.tsx @@ -0,0 +1,25 @@ +import { View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; + +/** + * The round highlight behind a sidebar footer icon. It is a layer inside the button rather than a + * radius on it: web hit-testing follows `border-radius`, so a round button would stop taking taps + * near the corners of its square finger-sized target. + */ +export function SidebarFooterIconHighlight({ visible }: { visible: boolean }) { + return ; +} + +const styles = StyleSheet.create((theme) => ({ + highlight: { + position: "absolute", + top: 0, + right: 0, + bottom: 0, + left: 0, + borderRadius: theme.borderRadius.full, + }, + visible: { + backgroundColor: theme.colors.interactionHighlight, + }, +})); diff --git a/packages/app/src/components/sidebar/sidebar-header-row.tsx b/packages/app/src/components/sidebar/sidebar-header-row.tsx index 2eeded947d..f38d27c65c 100644 --- a/packages/app/src/components/sidebar/sidebar-header-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-header-row.tsx @@ -54,6 +54,10 @@ export function SidebarHeaderRow({ const [isHovered, setIsHovered] = useState(false); const handlePointerEnter = useCallback(() => setIsHovered(true), []); const handlePointerLeave = useCallback(() => setIsHovered(false), []); + // The button and its trailing slot are siblings, so the row owns the pressed fill for both. + const [isPressed, setIsPressed] = useState(false); + const handlePressIn = useCallback(() => setIsPressed(true), []); + const handlePressOut = useCallback(() => setIsPressed(false), []); const isHighlighted = isHovered || isActive; const iconSize = variant === "header" ? "md" : "sm"; @@ -65,12 +69,14 @@ export function SidebarHeaderRow({ return ( {label} {right === null ? null : ( - + {right} )} @@ -136,6 +149,10 @@ const styles = StyleSheet.create((theme) => ({ rowHighlighted: { backgroundColor: theme.colors.surfaceSidebarHover, }, + // The workspace rows' pressed fill. + rowPressed: { + backgroundColor: theme.colors.surface2, + }, button: { flex: 1, minWidth: 0, diff --git a/packages/app/src/components/sidebar/sidebar-help-menu.tsx b/packages/app/src/components/sidebar/sidebar-help-menu.tsx index 97ff3d2772..0eefa77468 100644 --- a/packages/app/src/components/sidebar/sidebar-help-menu.tsx +++ b/packages/app/src/components/sidebar/sidebar-help-menu.tsx @@ -15,6 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { DensityIcon } from "@/components/ui/density-icon"; +import { SidebarFooterIconHighlight } from "./sidebar-footer-icon-highlight"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useAppDiagnosticStore } from "@/diagnostics/store"; import { useKeyboardShortcutsAvailable } from "@/keyboard/availability"; @@ -108,12 +109,15 @@ export function SidebarHelpMenu() { accessibilityRole="button" accessibilityLabel={t("sidebar.help.trigger")} > - {({ hovered }) => ( - + {({ hovered, pressed, open: menuOpen }) => ( + <> + + + )} diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 3950c0778e..31f5058940 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -275,15 +275,26 @@ function renderHostSettingsContent( // Trigger + sidebar style helpers // --------------------------------------------------------------------------- -function sidebarItemStyle({ hovered }: PressableStateCallbackType & { hovered?: boolean }) { - return [sidebarStyles.item, Boolean(hovered) && sidebarStyles.itemHovered]; +function sidebarItemStyle({ + hovered, + pressed, +}: PressableStateCallbackType & { hovered?: boolean }) { + return [ + sidebarStyles.item, + Boolean(hovered) && sidebarStyles.itemHovered, + pressed && sidebarStyles.itemPressed, + ]; } -function selectedSidebarItemStyle({ hovered }: PressableStateCallbackType & { hovered?: boolean }) { +function selectedSidebarItemStyle({ + hovered, + pressed, +}: PressableStateCallbackType & { hovered?: boolean }) { return [ sidebarStyles.item, Boolean(hovered) && sidebarStyles.itemHovered, sidebarStyles.itemSelected, + pressed && sidebarStyles.itemPressed, ]; } @@ -865,9 +876,10 @@ function HostPicker({ [], ); const triggerStyle = useCallback( - ({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [ + ({ hovered = false, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ sidebarStyles.pickerTrigger, hovered && sidebarStyles.pickerTriggerHovered, + pressed && sidebarStyles.itemPressed, ], [], ); @@ -1615,6 +1627,10 @@ const sidebarStyles = StyleSheet.create((theme) => ({ itemSelected: { backgroundColor: theme.colors.surfaceSidebarHover, }, + // The workspace rows' pressed fill. + itemPressed: { + backgroundColor: theme.colors.surface2, + }, label: { fontSize: { xs: theme.fontSize.lg, md: theme.fontSize.base }, color: theme.colors.foregroundMuted,