mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 18:35:17 +00:00
fix(app): keep tool badge file actions separate from disclosure buttons
Render the disclosure and open-file actions as sibling buttons so hovering a file-bearing badge does not produce invalid nested HTML buttons. Preserve independent mouse and keyboard activation.
This commit is contained in:
@@ -4,6 +4,8 @@ import { within } from "@testing-library/dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { i18n as testI18n } from "@/i18n/i18next";
|
||||
import { ExpandableBadge } from "@/components/message";
|
||||
import { page, userEvent } from "vitest/browser";
|
||||
import { PlanCard } from "@/components/plan-card";
|
||||
import { MessagePreview } from "@/components/user-message";
|
||||
import { DisclosureRow } from "./disclosure";
|
||||
@@ -145,3 +147,49 @@ describe("timeline disclosures hold their row", () => {
|
||||
expect(isExpanded()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function renderFileDetails() {
|
||||
return <div>File contents</div>;
|
||||
}
|
||||
|
||||
describe("tool badge file action", () => {
|
||||
it("keeps file opening separate from the accessible disclosure button", async () => {
|
||||
let opened = 0;
|
||||
function Badge() {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const toggle = React.useCallback(() => setExpanded(!expanded), [expanded]);
|
||||
const openFile = React.useCallback(() => {
|
||||
opened += 1;
|
||||
}, []);
|
||||
return (
|
||||
<ExpandableBadge
|
||||
label="Read"
|
||||
secondaryLabel="src/index.ts"
|
||||
isExpanded={expanded}
|
||||
onToggle={toggle}
|
||||
onOpenFile={openFile}
|
||||
renderDetails={renderFileDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
act(() => root.render(<Badge />));
|
||||
await page.getByRole("button", { name: "Read src/index.ts", exact: true }).hover();
|
||||
const file = view().getByRole("button", { name: /^Open file$/ });
|
||||
expect(file.parentElement?.closest("button")).toBeNull();
|
||||
await page.getByRole("button", { name: "Open file", exact: true }).click();
|
||||
expect(opened).toBe(1);
|
||||
const disclosure = view().getByRole("button", { name: /^Read src\/index\.ts$/ });
|
||||
expect(view().queryByText("File contents")).toBeNull();
|
||||
await page.getByRole("button", { name: "Read src/index.ts", exact: true }).click();
|
||||
|
||||
expect(view().getByText("File contents")).toBeTruthy();
|
||||
expect(opened).toBe(1);
|
||||
act(() => file.focus());
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(opened).toBe(2);
|
||||
expect(view().getByText("File contents")).toBeTruthy();
|
||||
act(() => disclosure.focus());
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(view().queryByText("File contents")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -929,12 +929,17 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
pressablePressed: {
|
||||
opacity: 0.9,
|
||||
},
|
||||
disclosureButton: {
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
labelRow: {
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
@@ -989,10 +994,6 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
borderRadius: theme.borderRadius.md,
|
||||
flexShrink: 0,
|
||||
},
|
||||
openFileButtonPlaceholderIcon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
detailWrapper: {
|
||||
borderBottomLeftRadius: theme.borderRadius.lg,
|
||||
borderBottomRightRadius: theme.borderRadius.lg,
|
||||
@@ -1598,7 +1599,6 @@ interface ExpandableBadgeWebShimmerOverlayProps {
|
||||
secondaryLabel?: string;
|
||||
shimmerLabelTextStyle: StyleProp<TextStyle>;
|
||||
shimmerSecondaryTextStyle: StyleProp<TextStyle>;
|
||||
showOpenFileButton: boolean;
|
||||
}
|
||||
|
||||
function ExpandableBadgeWebShimmerOverlay({
|
||||
@@ -1606,7 +1606,6 @@ function ExpandableBadgeWebShimmerOverlay({
|
||||
secondaryLabel,
|
||||
shimmerLabelTextStyle,
|
||||
shimmerSecondaryTextStyle,
|
||||
showOpenFileButton,
|
||||
}: ExpandableBadgeWebShimmerOverlayProps) {
|
||||
return (
|
||||
<View style={expandableBadgeStylesheet.shimmerOverlay} pointerEvents="none">
|
||||
@@ -1618,14 +1617,7 @@ function ExpandableBadgeWebShimmerOverlay({
|
||||
{secondaryLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
{showOpenFileButton ? (
|
||||
<View style={expandableBadgeStylesheet.openFileButton}>
|
||||
<View style={expandableBadgeStylesheet.openFileButtonPlaceholderIcon} />
|
||||
</View>
|
||||
) : null}
|
||||
{!secondaryLabel && !showOpenFileButton ? (
|
||||
<View style={expandableBadgeStylesheet.spacer} />
|
||||
) : null}
|
||||
{!secondaryLabel ? <View style={expandableBadgeStylesheet.spacer} /> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1649,11 +1641,6 @@ interface ExpandableBadgeLabelRowProps {
|
||||
onLabelRowLayout: (event: LayoutChangeEvent) => void;
|
||||
onLabelLayout: (event: LayoutChangeEvent) => void;
|
||||
onSecondaryLayout: (event: LayoutChangeEvent) => void;
|
||||
showOpenFileButton: boolean;
|
||||
isOpenFileHovered: boolean;
|
||||
onOpenFilePress: (event: GestureResponderEvent) => void;
|
||||
onOpenFileHoverIn: () => void;
|
||||
onOpenFileHoverOut: () => void;
|
||||
}
|
||||
|
||||
function ExpandableBadgeLabelRow({
|
||||
@@ -1675,13 +1662,7 @@ function ExpandableBadgeLabelRow({
|
||||
onLabelRowLayout,
|
||||
onLabelLayout,
|
||||
onSecondaryLayout,
|
||||
showOpenFileButton,
|
||||
isOpenFileHovered,
|
||||
onOpenFilePress,
|
||||
onOpenFileHoverIn,
|
||||
onOpenFileHoverOut,
|
||||
}: ExpandableBadgeLabelRowProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View
|
||||
style={expandableBadgeStylesheet.labelRow}
|
||||
@@ -1700,30 +1681,12 @@ function ExpandableBadgeLabelRow({
|
||||
shouldMeasureWebShimmer={shouldMeasureWebShimmer}
|
||||
onSecondaryLayout={onSecondaryLayout}
|
||||
/>
|
||||
{showOpenFileButton ? (
|
||||
<Pressable
|
||||
onPress={onOpenFilePress}
|
||||
onHoverIn={onOpenFileHoverIn}
|
||||
onHoverOut={onOpenFileHoverOut}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t("message.actions.openFile")}
|
||||
testID="tool-call-open-file"
|
||||
style={expandableBadgeStylesheet.openFileButton}
|
||||
hitSlop={6}
|
||||
>
|
||||
<ThemedFileSymlinkIcon
|
||||
size={14}
|
||||
uniProps={isOpenFileHovered ? foregroundColorMapping : foregroundMutedColorMapping}
|
||||
/>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{isWebShimmer ? (
|
||||
<ExpandableBadgeWebShimmerOverlay
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabel}
|
||||
shimmerLabelTextStyle={shimmerLabelTextStyle}
|
||||
shimmerSecondaryTextStyle={shimmerSecondaryTextStyle}
|
||||
showOpenFileButton={showOpenFileButton}
|
||||
/>
|
||||
) : null}
|
||||
{isNativeShimmer ? (
|
||||
@@ -1928,6 +1891,7 @@ export const ExpandableBadge = memo(function ExpandableBadge({
|
||||
borderlessWhenExpanded = false,
|
||||
testID,
|
||||
}: ExpandableBadgeProps) {
|
||||
const { t } = useTranslation();
|
||||
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isOpenFileHovered, setIsOpenFileHovered] = useState(false);
|
||||
@@ -2190,13 +2154,13 @@ export const ExpandableBadge = memo(function ExpandableBadge({
|
||||
onPointerEnter={isWeb ? handleHoverIn : undefined}
|
||||
onPointerLeave={isWeb ? handleHoverOut : undefined}
|
||||
>
|
||||
<Pressable
|
||||
{...pressHandlers}
|
||||
disabled={!isInteractive}
|
||||
accessibilityState={accessibilityState}
|
||||
style={pressableStyle}
|
||||
>
|
||||
<View style={expandableBadgeStylesheet.headerRow}>
|
||||
<View style={[pressableStyle, expandableBadgeStylesheet.headerRow]}>
|
||||
<Pressable
|
||||
{...pressHandlers}
|
||||
disabled={!isInteractive}
|
||||
accessibilityState={accessibilityState}
|
||||
style={expandableBadgeStylesheet.disclosureButton}
|
||||
>
|
||||
<View style={expandableBadgeStylesheet.iconBadge}>{iconSlotNode}</View>
|
||||
<ExpandableBadgeLabelRow
|
||||
label={label}
|
||||
@@ -2217,14 +2181,26 @@ export const ExpandableBadge = memo(function ExpandableBadge({
|
||||
onLabelRowLayout={handleLabelRowLayout}
|
||||
onLabelLayout={handleLabelLayout}
|
||||
onSecondaryLayout={handleSecondaryLayout}
|
||||
showOpenFileButton={Boolean(onOpenFile && isHovered)}
|
||||
isOpenFileHovered={isOpenFileHovered}
|
||||
onOpenFilePress={handleOpenFilePress}
|
||||
onOpenFileHoverIn={handleOpenFileHoverIn}
|
||||
onOpenFileHoverOut={handleOpenFileHoverOut}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
{onOpenFile && isHovered ? (
|
||||
<Pressable
|
||||
onPress={handleOpenFilePress}
|
||||
onHoverIn={handleOpenFileHoverIn}
|
||||
onHoverOut={handleOpenFileHoverOut}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t("message.actions.openFile")}
|
||||
testID="tool-call-open-file"
|
||||
style={expandableBadgeStylesheet.openFileButton}
|
||||
hitSlop={6}
|
||||
>
|
||||
<ThemedFileSymlinkIcon
|
||||
size={14}
|
||||
uniProps={isOpenFileHovered ? foregroundColorMapping : foregroundMutedColorMapping}
|
||||
/>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
{detailContent ? (
|
||||
<Pressable
|
||||
ref={detailWrapperRef}
|
||||
|
||||
Reference in New Issue
Block a user