fix(app): acknowledge native sidebar presses immediately
Sidebar rows previously waited for JavaScript-driven selection or navigation before showing feedback. Keep the transient highlight on the UI worklet while leaving activation, cancellation, and drag ownership with the existing pressable.
This commit is contained in:
@@ -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}
|
||||
>
|
||||
<Pressable
|
||||
<PressHighlight
|
||||
accessibilityRole="button"
|
||||
style={projectRowStyle}
|
||||
highlightStyle={styles.projectRowHovered}
|
||||
onPressIn={interaction.handlePressIn}
|
||||
onTouchMove={interaction.handleTouchMove}
|
||||
onPressOut={interaction.handlePressOut}
|
||||
@@ -983,7 +985,7 @@ function ProjectHeaderRow({
|
||||
testID={`sidebar-project-row-${project.viewKey}`}
|
||||
>
|
||||
{rowChildren}
|
||||
</Pressable>
|
||||
</PressHighlight>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -729,6 +729,7 @@ function StatusWorkspaceRowInner({
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
style={workspaceRowStyle}
|
||||
highlightStyle={styles.workspaceRowHovered}
|
||||
onPress={onPress}
|
||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||
>
|
||||
|
||||
@@ -239,7 +239,7 @@ export function SidebarWorkspaceMenu({
|
||||
|
||||
type ContextTriggerProps = Omit<
|
||||
ComponentProps<typeof ContextMenuTrigger>,
|
||||
"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<typeof ContextMenuTrigger>["highlightStyle"];
|
||||
}
|
||||
>) {
|
||||
const {
|
||||
@@ -301,6 +303,7 @@ export function SidebarWorkspaceContextMenu({
|
||||
{...triggerProps}
|
||||
enabledOnMobile={false}
|
||||
accessibilityLabel={accessibilityLabel ?? rowAccessibilityLabel}
|
||||
highlightStyle={highlightStyle}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuTrigger>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<ViewStyle> | ((state: MenuTriggerState) => Sty
|
||||
export function ContextMenuTrigger({
|
||||
children,
|
||||
disabled,
|
||||
highlightStyle,
|
||||
style,
|
||||
enabled = true,
|
||||
enabledOnMobile = true,
|
||||
@@ -108,6 +109,7 @@ export function ContextMenuTrigger({
|
||||
...props
|
||||
}: PropsWithChildren<
|
||||
Omit<PressableProps, "style"> & {
|
||||
highlightStyle?: StyleProp<ViewStyle>;
|
||||
style?: TriggerStyleProp;
|
||||
enabled?: boolean;
|
||||
enabledOnMobile?: boolean;
|
||||
@@ -184,7 +186,7 @@ export function ContextMenuTrigger({
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<PressHighlight
|
||||
{...props}
|
||||
ref={handleRef}
|
||||
collapsable={false}
|
||||
@@ -194,8 +196,9 @@ export function ContextMenuTrigger({
|
||||
// @ts-ignore - onContextMenu is web-only and not in RN types.
|
||||
onContextMenu={handleContextMenu}
|
||||
style={pressableStyle}
|
||||
highlightStyle={highlightStyle}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
</PressHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<View, PressHighlightProps>(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 ? (
|
||||
<Animated.View pointerEvents="none" style={[styles.highlight, animatedHighlightStyle]}>
|
||||
{/* Keep the themed Unistyles node separate from the node Reanimated patches. */}
|
||||
<View style={[styles.highlightFill, highlightStyle]} />
|
||||
</Animated.View>
|
||||
) : null;
|
||||
const renderedChildren =
|
||||
typeof children === "function" ? (
|
||||
(state: Parameters<typeof children>[0]) => (
|
||||
<>
|
||||
{highlight}
|
||||
{children(state)}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{highlight}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
const pressable = (
|
||||
<Pressable ref={ref} {...props} collapsable={false} disabled={disabled}>
|
||||
{renderedChildren}
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
if (!highlightStyle) {
|
||||
return pressable;
|
||||
}
|
||||
|
||||
return <GestureDetector gesture={pressGesture}>{pressable}</GestureDetector>;
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
highlight: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
highlightFill: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
});
|
||||
@@ -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<View, PressHighlightProps>(function PressHighlight(
|
||||
{ highlightStyle: _highlightStyle, ...props },
|
||||
ref,
|
||||
) {
|
||||
return <Pressable ref={ref} {...props} />;
|
||||
});
|
||||
@@ -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<ViewStyle>;
|
||||
}
|
||||
Reference in New Issue
Block a user