FN-6403: fix GitHub import pane resizing

Restores reliable GitHub import left-pane resizing while keeping it scoped to the two-pane layout.

- Unify resize availability with the side-by-side breakpoint so the handle and fixed pane width only render when dragging is supported.
- Enlarge the divider hit area without changing the visible divider treatment.
- Cover pointer dragging, clamping, responsive hiding, and list/PR tab resize surfaces.

Files changed:
 .../dashboard/app/components/GitHubImportModal.css | 20 ++++-
 .../dashboard/app/components/GitHubImportModal.tsx | 35 +++++----
 .../__tests__/GitHubImportModal.test.tsx           | 91 +++++++++++++++++++++-
 3 files changed, 127 insertions(+), 19 deletions(-)

Fusion-Task-Id: FN-6403

Fusion-Task-Lineage: 974903f5-63dd-4852-9edf-6b0ade7ca912
This commit is contained in:
gsxdsm
2026-06-13 16:13:40 -07:00
parent 1db4835d1d
commit 3387a9b298
3 changed files with 127 additions and 19 deletions

View File

@@ -146,22 +146,38 @@
}
.github-import-workspace__resize-handle {
flex: 0 0 var(--space-xs);
position: relative;
flex: 0 0 var(--space-sm);
align-self: stretch;
cursor: col-resize;
touch-action: none;
border-radius: var(--radius-sm);
}
.github-import-workspace__resize-handle::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: var(--space-xs);
transform: translateX(-50%);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--border) 35%, transparent);
transition: background var(--transition-fast);
}
.github-import-workspace__resize-handle:hover {
.github-import-workspace__resize-handle:hover::before,
.github-import-workspace__resize-handle:active::before {
background: color-mix(in srgb, var(--todo) 50%, transparent);
}
.github-import-workspace__resize-handle:focus-visible {
outline: var(--focus-ring-strong);
outline-offset: 0;
}
.github-import-workspace__resize-handle:focus-visible::before {
background: color-mix(in srgb, var(--todo) 60%, transparent);
}

View File

@@ -26,8 +26,9 @@ interface GitHubImportModalProps {
projectId?: string;
}
// Mobile breakpoint in pixels
// Mobile and two-pane breakpoints in pixels
const MOBILE_BREAKPOINT = 640;
const TWO_PANE_BREAKPOINT = 860;
const GITHUB_IMPORT_LIST_PANE_MIN_WIDTH = 240;
const GITHUB_IMPORT_LIST_PANE_MAX_WIDTH = 640;
const GITHUB_IMPORT_LIST_PANE_DEFAULT_WIDTH = 360;
@@ -82,8 +83,9 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
useModalResizePersist(modalRef, isOpen, "fusion:github-modal-size");
const overlayDismissProps = useOverlayDismiss(onClose);
// Mobile view state
// Responsive view state
const [isMobile, setIsMobile] = useState(false);
const [canResizePanes, setCanResizePanes] = useState(false);
const [mobileView, setMobileView] = useState<"list" | "preview">("list");
const [listPaneWidth, setListPaneWidth] = useState(() => {
if (typeof window === "undefined") {
@@ -288,20 +290,21 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
return () => document.removeEventListener("keydown", handleKey);
}, [isOpen, onClose]);
// Detect mobile viewport
// Detect responsive viewport bands
useEffect(() => {
if (!isOpen) return;
const checkMobile = () => {
const checkViewportBands = () => {
setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT);
setCanResizePanes(window.innerWidth > TWO_PANE_BREAKPOINT);
};
// Check initially
checkMobile();
checkViewportBands();
// Listen for resize
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
window.addEventListener("resize", checkViewportBands);
return () => window.removeEventListener("resize", checkViewportBands);
}, [isOpen]);
useEffect(() => {
@@ -316,7 +319,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
}, [listPaneWidth]);
const handleListPaneResizeStart = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
if (isMobile) {
if (!canResizePanes) {
return;
}
@@ -340,10 +343,10 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
document.addEventListener("pointermove", handlePointerMove);
document.addEventListener("pointerup", handlePointerUp);
}, [isMobile, listPaneWidth]);
}, [canResizePanes, listPaneWidth]);
const handleListPaneResizeKeyDown = useCallback((event: ReactKeyboardEvent<HTMLDivElement>) => {
if (isMobile) {
if (!canResizePanes) {
return;
}
@@ -371,7 +374,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
event.preventDefault();
setListPaneWidth(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH);
}
}, [isMobile]);
}, [canResizePanes]);
// Handle issue selection - switch to preview view on mobile
const handleIssueSelect = useCallback((issueNumber: number) => {
@@ -625,7 +628,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
{/* Left pane: Issue/PR list */}
<section
className={`github-import-list-pane ${isMobile ? 'mobile' : ''} ${mobileView === 'list' ? 'active' : ''}`}
style={!isMobile ? { flex: `0 0 ${listPaneWidth}px` } : undefined}
style={canResizePanes ? { flex: `0 0 ${listPaneWidth}px` } : undefined}
data-testid="github-import-list-pane"
aria-labelledby="github-import-results-heading"
>
@@ -763,7 +766,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
</div>
</section>
{!isMobile && (
{canResizePanes && (
<div
className="github-import-workspace__resize-handle"
role="separator"

View File

@@ -887,6 +887,50 @@ describe("GitHubImportModal", () => {
return rendered;
};
const renderWithEmptyIssues = async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([]);
const rendered = render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
await waitFor(() => {
expect(screen.getByText("No open issues found")).toBeTruthy();
});
return rendered;
};
const renderWithPulls = async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([]);
vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce([
{ number: 7, title: "Resize Test Pull", body: "Pull body", html_url: "https://github.com/owner/repo/pull/7", headBranch: "feature", baseBranch: "main" },
]);
const rendered = render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
fireEvent.click(screen.getByRole("tab", { name: /Pull Requests/i }));
await waitFor(() => {
expect(screen.getByText("Resize Test Pull")).toBeTruthy();
});
return rendered;
};
const stubPointerCapture = (handle: HTMLElement) => {
handle.setPointerCapture = vi.fn();
handle.releasePointerCapture = vi.fn();
handle.hasPointerCapture = vi.fn(() => true);
};
const dragHandle = (handle: HTMLElement, startX: number, endX: number) => {
stubPointerCapture(handle);
fireEvent.pointerDown(handle, { pointerId: 1, clientX: startX });
fireEvent.pointerMove(document, { pointerId: 1, clientX: endX });
fireEvent.pointerUp(document, { pointerId: 1, clientX: endX });
};
beforeEach(() => {
window.localStorage.removeItem("fusion:github-import-list-pane-width");
setViewportWidth(1200);
@@ -897,17 +941,62 @@ describe("GitHubImportModal", () => {
setViewportWidth(originalInnerWidth);
});
it("renders handle on desktop and hides it on mobile", async () => {
it("renders handle only in the side-by-side two-pane band", async () => {
await renderWithIssues();
expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy();
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px");
setViewportWidth(800);
await waitFor(() => {
expect(screen.queryByTestId("github-import-resize-handle")).toBeNull();
});
expect(screen.getByTestId("github-import-list-pane").getAttribute("style") ?? "").not.toContain("flex: 0 0");
setViewportWidth(480);
await waitFor(() => {
expect(screen.queryByTestId("github-import-resize-handle")).toBeNull();
});
expect(screen.getByTestId("github-import-list-pane").getAttribute("style") ?? "").not.toContain("flex: 0 0");
});
it("resizes the list pane with pointer drags and clamps to bounds", async () => {
await renderWithIssues();
const handle = screen.getByTestId("github-import-resize-handle");
const listPane = screen.getByTestId("github-import-list-pane");
dragHandle(handle, 100, 160);
expect(handle.getAttribute("aria-valuenow")).toBe("420");
expect(listPane.getAttribute("style")).toContain("flex: 0 0 420px");
dragHandle(handle, 160, 120);
expect(handle.getAttribute("aria-valuenow")).toBe("380");
expect(listPane.getAttribute("style")).toContain("flex: 0 0 380px");
dragHandle(handle, 120, -200);
expect(handle.getAttribute("aria-valuenow")).toBe("240");
expect(listPane.getAttribute("style")).toContain("flex: 0 0 240px");
dragHandle(handle, -200, 700);
expect(handle.getAttribute("aria-valuenow")).toBe("640");
expect(listPane.getAttribute("style")).toContain("flex: 0 0 640px");
});
it("renders the desktop handle regardless of list content or active tab", async () => {
const mounted = await renderWithEmptyIssues();
expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy();
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px");
mounted.unmount();
vi.clearAllMocks();
window.localStorage.removeItem("fusion:github-import-list-pane-width");
setViewportWidth(1200);
await renderWithPulls();
expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy();
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px");
});
it.each([
[{ key: "ArrowRight" }, 370],
[{ key: "ArrowLeft" }, 350],