diff --git a/packages/app/src/hooks/use-container-width.test.ts b/packages/app/src/hooks/use-container-width.test.ts new file mode 100644 index 000000000..0d0670942 --- /dev/null +++ b/packages/app/src/hooks/use-container-width.test.ts @@ -0,0 +1,56 @@ +// @vitest-environment jsdom + +import { act, renderHook } from "@testing-library/react"; +import type { LayoutChangeEvent } from "react-native"; +import { describe, expect, it } from "vitest"; +import { useContainerWidthBelow } from "./use-container-width"; + +function layoutEvent(width: number): LayoutChangeEvent { + return { + nativeEvent: { + layout: { + width, + height: 48, + x: 0, + y: 0, + }, + }, + } as LayoutChangeEvent; +} + +describe("useContainerWidthBelow", () => { + it("does not re-render for width changes that stay in the same threshold bucket", () => { + let renderCount = 0; + const { result } = renderHook(() => { + renderCount += 1; + return useContainerWidthBelow(700); + }); + + expect(result.current.isBelow).toBe(true); + expect(renderCount).toBe(1); + + act(() => { + result.current.onLayout(layoutEvent(650)); + result.current.onLayout(layoutEvent(620)); + result.current.onLayout(layoutEvent(699)); + }); + + expect(result.current.isBelow).toBe(true); + expect(renderCount).toBe(1); + }); + + it("re-renders when the width crosses the threshold", () => { + let renderCount = 0; + const { result } = renderHook(() => { + renderCount += 1; + return useContainerWidthBelow(700); + }); + + act(() => { + result.current.onLayout(layoutEvent(760)); + }); + + expect(result.current.isBelow).toBe(false); + expect(renderCount).toBe(2); + }); +}); diff --git a/packages/app/src/hooks/use-container-width.ts b/packages/app/src/hooks/use-container-width.ts index 35bb1b7b6..86654c3f2 100644 --- a/packages/app/src/hooks/use-container-width.ts +++ b/packages/app/src/hooks/use-container-width.ts @@ -16,3 +16,25 @@ export function useContainerWidth(): { width, }; } + +/** + * Tracks only whether a container is narrower than a threshold. + */ +export function useContainerWidthBelow(threshold: number): { + onLayout: (e: LayoutChangeEvent) => void; + isBelow: boolean; +} { + const [isBelow, setIsBelow] = useState(true); + return { + onLayout: useCallback( + (e: LayoutChangeEvent) => { + const nextIsBelow = e.nativeEvent.layout.width < threshold; + setIsBelow((currentIsBelow) => + currentIsBelow === nextIsBelow ? currentIsBelow : nextIsBelow, + ); + }, + [threshold], + ), + isBelow, + }; +} diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 4bba0398b..10383e653 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -122,7 +122,7 @@ import { findAdjacentPane } from "@/utils/split-navigation"; import { isAbsolutePath } from "@/utils/path"; import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout"; import { isWeb, isNative } from "@/constants/platform"; -import { useContainerWidth } from "@/hooks/use-container-width"; +import { useContainerWidthBelow } from "@/hooks/use-container-width"; const TERMINALS_QUERY_STALE_TIME = 5_000; const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000; @@ -1051,8 +1051,8 @@ function WorkspaceScreenContent({ ); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); const { closingTabIds, closeTab } = useCloseTabs(); - const { onLayout: onHeaderLayout, width: headerWidth } = useContainerWidth(); - const showCompactButtonLabels = headerWidth < 700; + const { onLayout: onHeaderLayout, isBelow: showCompactButtonLabels } = + useContainerWidthBelow(700); const closeWorkspaceTabWithCleanup = useCallback( function closeWorkspaceTabWithCleanup(input: { tabId: string;