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.
This commit is contained in:
Mohamed Boudra
2026-10-16 20:46:29 +07:00
committed by GitHub
parent c1558d29ea
commit d7b3ff654d
5 changed files with 85 additions and 19 deletions
+11 -7
View File
@@ -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 }) => (
<DensityIcon
icon={icon}
size="md"
sizeAdjustment={iconSizeAdjustment}
color={hovered ? "foreground" : "foregroundMuted"}
/>
{({ hovered, pressed }) => (
<>
<SidebarFooterIconHighlight visible={pressed} />
<DensityIcon
icon={icon}
size="md"
sizeAdjustment={iconSizeAdjustment}
color={hovered ? "foreground" : "foregroundMuted"}
/>
</>
)}
</Pressable>
</TooltipTrigger>
@@ -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 <View pointerEvents="none" style={[styles.highlight, visible && styles.visible]} />;
}
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,
},
}));
@@ -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 (
<View ref={rowRef} collapsable={false} style={getContainerStyle(variant)}>
<View
style={[styles.row, isHighlighted && styles.rowHighlighted]}
style={[styles.row, isHighlighted && styles.rowHighlighted, isPressed && styles.rowPressed]}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
<Pressable
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
testID={testID}
nativeID={nativeID}
accessible
@@ -92,7 +98,14 @@ export function SidebarHeaderRow({
<Text style={[styles.label, isHighlighted && styles.labelHighlighted]}>{label}</Text>
</Pressable>
{right === null ? null : (
<Pressable onPress={onPress} accessible={false} focusable={false} style={styles.trailing}>
<Pressable
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
accessible={false}
focusable={false}
style={styles.trailing}
>
{right}
</Pressable>
)}
@@ -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,
@@ -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 }) => (
<DensityIcon
icon={CircleHelp}
size="md"
color={hovered ? "foreground" : "foregroundMuted"}
/>
{({ hovered, pressed, open: menuOpen }) => (
<>
<SidebarFooterIconHighlight visible={pressed || menuOpen} />
<DensityIcon
icon={CircleHelp}
size="md"
color={hovered ? "foreground" : "foregroundMuted"}
/>
</>
)}
</DropdownMenuTrigger>
</View>
+20 -4
View File
@@ -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,