mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 04:03:05 +00:00
fix(composer): isolate web hover handlers
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { useCallback, useState, type ReactElement, type ReactNode } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
|
||||
export interface ComposerTrackRowProps {
|
||||
/** `link` for a row that leaves the agent, `button` for one that acts in place. */
|
||||
accessibilityRole: "button" | "link";
|
||||
accessibilityLabel: string;
|
||||
testID: string;
|
||||
onPress: () => void;
|
||||
renderLeading?: () => ReactNode;
|
||||
label: string;
|
||||
/** Muted trailing context — a provider note, a cadence. */
|
||||
secondary?: string;
|
||||
actions?: () => ReactNode;
|
||||
}
|
||||
|
||||
interface ComposerTrackRowLayoutProps extends ComposerTrackRowProps {
|
||||
isHovered: boolean;
|
||||
showActions: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* One row in a track. The row control and its actions are siblings so an
|
||||
* action never activates the row's navigation.
|
||||
*/
|
||||
export function ComposerTrackRowLayout({
|
||||
accessibilityRole,
|
||||
accessibilityLabel,
|
||||
testID,
|
||||
onPress,
|
||||
renderLeading,
|
||||
label,
|
||||
secondary,
|
||||
actions,
|
||||
isHovered,
|
||||
showActions,
|
||||
}: ComposerTrackRowLayoutProps): ReactElement {
|
||||
const [pressed, setPressed] = useState(false);
|
||||
const handlePressIn = useCallback(() => setPressed(true), []);
|
||||
const handlePressOut = useCallback(() => setPressed(false), []);
|
||||
|
||||
return (
|
||||
<View style={isHovered || pressed ? styles.rowActive : styles.row}>
|
||||
<Pressable
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={styles.rowMain}
|
||||
>
|
||||
{renderLeading?.()}
|
||||
<Text style={styles.rowLabel} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
{secondary ? (
|
||||
<Text style={styles.rowSecondary} numberOfLines={1}>
|
||||
{secondary}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{actions ? (
|
||||
<View
|
||||
style={showActions ? styles.actionClusterVisible : styles.actionClusterHidden}
|
||||
pointerEvents={showActions ? "auto" : "none"}
|
||||
>
|
||||
{actions()}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
},
|
||||
rowActive: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
rowMain: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
rowLabel: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
// Keep secondary context bounded so the row's own name stays readable on
|
||||
// compact screens.
|
||||
rowSecondary: {
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
maxWidth: "45%",
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
actionClusterVisible: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
opacity: 1,
|
||||
},
|
||||
actionClusterHidden: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
opacity: 0,
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { ComposerTrackRowLayout, type ComposerTrackRowProps } from "./track-row-layout";
|
||||
|
||||
export type { ComposerTrackRowProps };
|
||||
|
||||
export function ComposerTrackRow(props: ComposerTrackRowProps): ReactElement {
|
||||
return <ComposerTrackRowLayout {...props} isHovered={false} showActions />;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useCallback, useState, type ReactElement } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { ComposerTrackRowLayout, type ComposerTrackRowProps } from "./track-row-layout";
|
||||
|
||||
export type { ComposerTrackRowProps };
|
||||
|
||||
export function ComposerTrackRow(props: ComposerTrackRowProps): ReactElement {
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handlePointerEnter = useCallback(() => setIsHovered(true), []);
|
||||
const handlePointerLeave = useCallback(() => setIsHovered(false), []);
|
||||
const showActions = isHovered || isCompact;
|
||||
|
||||
return (
|
||||
<View onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave}>
|
||||
<ComposerTrackRowLayout {...props} isHovered={isHovered} showActions={showActions} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
import { ChevronDown, ChevronRight } from "lucide-react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { ComposerTrackRow, type ComposerTrackRowProps } from "./track-row";
|
||||
|
||||
export { ComposerTrackRow, type ComposerTrackRowProps };
|
||||
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
const ThemedChevronRight = withUnistyles(ChevronRight);
|
||||
@@ -140,76 +142,6 @@ function expandedAccessibilityState(expanded: boolean) {
|
||||
return expanded ? EXPANDED_STATE : COLLAPSED_STATE;
|
||||
}
|
||||
|
||||
export interface ComposerTrackRowProps {
|
||||
/** `link` for a row that leaves the agent, `button` for one that acts in place. */
|
||||
accessibilityRole: "button" | "link";
|
||||
accessibilityLabel: string;
|
||||
testID: string;
|
||||
onPress: () => void;
|
||||
renderLeading?: () => ReactNode;
|
||||
label: string;
|
||||
/** Muted trailing context — a provider note, a cadence. */
|
||||
secondary?: string;
|
||||
actions?: () => ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* One row in a track. The row control and its actions are siblings so an
|
||||
* action never activates the row's navigation.
|
||||
*/
|
||||
export function ComposerTrackRow({
|
||||
accessibilityRole,
|
||||
accessibilityLabel,
|
||||
testID,
|
||||
onPress,
|
||||
renderLeading,
|
||||
label,
|
||||
secondary,
|
||||
actions,
|
||||
}: ComposerTrackRowProps): ReactElement {
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [pressed, setPressed] = useState(false);
|
||||
const handlePointerEnter = useCallback(() => setHovered(true), []);
|
||||
const handlePointerLeave = useCallback(() => setHovered(false), []);
|
||||
const handlePressIn = useCallback(() => setPressed(true), []);
|
||||
const handlePressOut = useCallback(() => setPressed(false), []);
|
||||
const showActions = hovered || isNative || isCompact;
|
||||
return (
|
||||
<View onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave}>
|
||||
<View style={hovered || pressed ? styles.rowActive : styles.row}>
|
||||
<Pressable
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={styles.rowMain}
|
||||
>
|
||||
{renderLeading?.()}
|
||||
<Text style={styles.rowLabel} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
{secondary ? (
|
||||
<Text style={styles.rowSecondary} numberOfLines={1}>
|
||||
{secondary}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{actions ? (
|
||||
<View
|
||||
style={showActions ? styles.actionClusterVisible : styles.actionClusterHidden}
|
||||
pointerEvents={showActions ? "auto" : "none"}
|
||||
>
|
||||
{actions()}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ComposerTrackActionButtonProps {
|
||||
accessibilityLabel: string;
|
||||
testID: string;
|
||||
@@ -321,55 +253,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
scrollContent: {
|
||||
paddingVertical: theme.spacing[1],
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
},
|
||||
rowActive: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
rowMain: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
rowLabel: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
// Keep secondary context bounded so the row's own name stays readable on
|
||||
// compact screens.
|
||||
rowSecondary: {
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
maxWidth: "45%",
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
actionClusterVisible: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
opacity: 1,
|
||||
},
|
||||
actionClusterHidden: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
opacity: 0,
|
||||
},
|
||||
actionButton: {
|
||||
padding: theme.spacing[1],
|
||||
alignItems: "center",
|
||||
|
||||
Reference in New Issue
Block a user