diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 38395dc55..aa2f157a9 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -117,6 +117,7 @@ import { useSidebarWorkspaceTrailing, } from "@/components/sidebar/workspace-trailing"; import { Button } from "@/components/ui/button"; +import { PressHighlight } from "@/components/ui/press-highlight"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Shortcut } from "@/components/ui/shortcut"; import type { ShortcutKey } from "@/utils/format-shortcut"; @@ -973,9 +974,10 @@ function ProjectHeaderRow({ onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave} > - {rowChildren} - + ); } @@ -1001,6 +1003,7 @@ function ProjectHeaderRow({ enabledOnMobile={false} accessibilityRole="button" style={projectRowStyle} + highlightStyle={styles.projectRowHovered} onPressIn={interaction.handlePressIn} onTouchMove={interaction.handleTouchMove} onPressOut={interaction.handlePressOut} @@ -1119,6 +1122,7 @@ function WorkspaceRowInner({ accessibilityRole="button" accessibilityState={accessibilityState} style={workspaceRowStyle} + highlightStyle={styles.workspaceRowHovered} onPressIn={interaction.handlePressIn} onTouchMove={interaction.handleTouchMove} onPressOut={interaction.handlePressOut} diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx index 596d14cb4..689ddcb35 100644 --- a/packages/app/src/components/sidebar/sidebar-status-list.tsx +++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx @@ -729,6 +729,7 @@ function StatusWorkspaceRowInner({ accessibilityRole="button" accessibilityState={accessibilityState} style={workspaceRowStyle} + highlightStyle={styles.workspaceRowHovered} onPress={onPress} testID={`sidebar-workspace-row-${workspace.workspaceKey}`} > diff --git a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx index 3f0b00c6b..f5234a62a 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx @@ -239,7 +239,7 @@ export function SidebarWorkspaceMenu({ type ContextTriggerProps = Omit< ComponentProps, - "children" | "enabledOnMobile" + "children" | "enabledOnMobile" | "highlightStyle" >; export function SidebarWorkspaceContextMenu({ @@ -264,6 +264,7 @@ export function SidebarWorkspaceContextMenu({ onTogglePin, openInFileManagerPath, accessibilityLabel, + highlightStyle, ...triggerProps }: PropsWithChildren< SidebarWorkspaceMenuItemsProps & @@ -274,6 +275,7 @@ export function SidebarWorkspaceContextMenu({ leadingProjectName?: string | null; hostBadgeLabel?: string | null; scriptSummary?: WorkspaceScriptSummary | null; + highlightStyle: ComponentProps["highlightStyle"]; } >) { const { @@ -301,6 +303,7 @@ export function SidebarWorkspaceContextMenu({ {...triggerProps} enabledOnMobile={false} accessibilityLabel={accessibilityLabel ?? rowAccessibilityLabel} + highlightStyle={highlightStyle} > {children} diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx index df4a27aba..41974e89e 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx @@ -324,6 +324,7 @@ function WorkspaceRowBody({ accessibilityRole="button" accessibilityState={accessibilityState} style={workspaceRowStyle} + highlightStyle={styles.workspaceRowHovered} onPressIn={draggable ? interaction.handlePressIn : undefined} onTouchMove={draggable ? interaction.handleTouchMove : undefined} onPressOut={draggable ? interaction.handlePressOut : undefined} diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx index b655c06e0..17a2aba75 100644 --- a/packages/app/src/components/ui/context-menu.tsx +++ b/packages/app/src/components/ui/context-menu.tsx @@ -7,7 +7,6 @@ import { } from "react"; import { Platform, - Pressable, StatusBar, View, type GestureResponderEvent, @@ -28,6 +27,7 @@ import { type MenuSurfaceProps, type MenuTriggerState, } from "@/components/ui/menu"; +import { PressHighlight } from "@/components/ui/press-highlight"; /** * A menu opened by a long press or a right click, anchored to the point of the gesture rather @@ -99,6 +99,7 @@ type TriggerStyleProp = StyleProp | ((state: MenuTriggerState) => Sty export function ContextMenuTrigger({ children, disabled, + highlightStyle, style, enabled = true, enabledOnMobile = true, @@ -108,6 +109,7 @@ export function ContextMenuTrigger({ ...props }: PropsWithChildren< Omit & { + highlightStyle?: StyleProp; style?: TriggerStyleProp; enabled?: boolean; enabledOnMobile?: boolean; @@ -184,7 +186,7 @@ export function ContextMenuTrigger({ ); return ( - {children} - + ); } diff --git a/packages/app/src/components/ui/press-highlight.native.tsx b/packages/app/src/components/ui/press-highlight.native.tsx new file mode 100644 index 000000000..b809534fc --- /dev/null +++ b/packages/app/src/components/ui/press-highlight.native.tsx @@ -0,0 +1,71 @@ +import { forwardRef, useMemo } from "react"; +import { Pressable, StyleSheet, View } from "react-native"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated"; +import type { PressHighlightProps } from "./press-highlight.types"; + +export const PressHighlight = forwardRef(function PressHighlight( + { children, disabled, highlightStyle, ...props }, + ref, +) { + const highlighted = useSharedValue(0); + const animatedHighlightStyle = useAnimatedStyle(() => ({ opacity: highlighted.value })); + // This gesture owns visual acknowledgement only. Pressable remains the sole owner of + // activation, cancellation, accessibility, and the caller's drag/long-press callbacks. + const pressGesture = useMemo( + () => + Gesture.Tap() + .enabled(disabled !== true && highlightStyle != null) + .maxDistance(8) + .shouldCancelWhenOutside(true) + .onBegin(() => { + highlighted.value = 1; + }) + .onFinalize(() => { + highlighted.value = 0; + }), + [disabled, highlightStyle, highlighted], + ); + + const highlight = highlightStyle ? ( + + {/* Keep the themed Unistyles node separate from the node Reanimated patches. */} + + + ) : null; + const renderedChildren = + typeof children === "function" ? ( + (state: Parameters[0]) => ( + <> + {highlight} + {children(state)} + + ) + ) : ( + <> + {highlight} + {children} + + ); + + const pressable = ( + + {renderedChildren} + + ); + + if (!highlightStyle) { + return pressable; + } + + return {pressable}; +}); + +const styles = StyleSheet.create({ + highlight: { + ...StyleSheet.absoluteFillObject, + }, + highlightFill: { + ...StyleSheet.absoluteFillObject, + }, +}); diff --git a/packages/app/src/components/ui/press-highlight.tsx b/packages/app/src/components/ui/press-highlight.tsx new file mode 100644 index 000000000..bcbb2957b --- /dev/null +++ b/packages/app/src/components/ui/press-highlight.tsx @@ -0,0 +1,11 @@ +import { forwardRef } from "react"; +import { Pressable } from "react-native"; +import type { View } from "react-native"; +import type { PressHighlightProps } from "./press-highlight.types"; + +export const PressHighlight = forwardRef(function PressHighlight( + { highlightStyle: _highlightStyle, ...props }, + ref, +) { + return ; +}); diff --git a/packages/app/src/components/ui/press-highlight.types.ts b/packages/app/src/components/ui/press-highlight.types.ts new file mode 100644 index 000000000..2f0e3896b --- /dev/null +++ b/packages/app/src/components/ui/press-highlight.types.ts @@ -0,0 +1,6 @@ +import type { PressableProps, StyleProp, ViewStyle } from "react-native"; + +export interface PressHighlightProps extends PressableProps { + /** Native finger-down surface. Desktop hover remains owned by the caller. */ + highlightStyle?: StyleProp; +}