fix(tabs): make vertical tab rail width follow mouse while dragging

This commit is contained in:
一颗红心 authored and GitHub committed 2026-09-22 17:18:19 +08:00
1 parent f652491198
commit 3c0cce8ce2
4 files changed
+91 -14

No files matched your search

@@ -145,4 +145,61 @@ describe("usePanelResize", () => {
restored.setTabBarCollapsed(false);
expect(localStorage.getItem("dbx-tab-bar-collapsed")).toBe("false");
});
it("updates the outer workspace tab rail width and flex while dragging (issue #9977)", () => {
localStorage.setItem("dbx-tab-bar-width", "240");
const rail = document.createElement("div");
rail.setAttribute("data-workspace-tab-navigation", "");
rail.style.width = "240px";
rail.style.flex = "0 0 240px";
const target = document.createElement("div");
const tabBar = document.createElement("div");
tabBar.className = "app-tab-bar";
const handle = document.createElement("div");
tabBar.append(handle);
target.append(tabBar);
rail.append(target);
document.body.append(rail);
vi.spyOn(rail, "getBoundingClientRect").mockReturnValue(rect(0, 240));
const { tabBarWidth, startLeftTabBarResize } = usePanelResize();
handle.addEventListener("pointerdown", startLeftTabBarResize);
handle.dispatchEvent(pointerEvent("pointerdown", 240));
document.dispatchEvent(pointerEvent("pointermove", 320));
expect(rail.style.width).toBe("320px");
expect(rail.style.flex).toBe("0 0 320px");
expect(tabBar.style.width).toBe("");
document.dispatchEvent(pointerEvent("pointerup", 320));
expect(tabBarWidth.value).toBe(320);
expect(localStorage.getItem("dbx-tab-bar-width")).toBe("320");
});
it("shrinks a right-side tab rail toward the left edge while dragging", () => {
localStorage.setItem("dbx-tab-bar-width", "300");
const rail = document.createElement("div");
rail.setAttribute("data-special-page-navigation", "");
const tabBar = document.createElement("div");
const handle = document.createElement("div");
tabBar.append(handle);
rail.append(tabBar);
document.body.append(rail);
vi.spyOn(rail, "getBoundingClientRect").mockReturnValue(rect(900, 300));
const { tabBarWidth, startRightTabBarResize } = usePanelResize();
handle.addEventListener("pointerdown", startRightTabBarResize);
handle.dispatchEvent(pointerEvent("pointerdown", 900));
document.dispatchEvent(pointerEvent("pointermove", 960));
expect(rail.style.width).toBe("240px");
expect(rail.style.flex).toBe("0 0 240px");
document.dispatchEvent(pointerEvent("pointerup", 960));
expect(tabBarWidth.value).toBe(240);
});
});
+27 -6
View File
@@ -6,12 +6,33 @@ const PANEL_MIN_WIDTH = 240;
const DEFAULT_PANEL_MAX_WIDTH = 800;
type PanelMaxWidth = number | ((handle: HTMLElement | null) => number);
/** Vertical tab rails size via width + flex-basis; sidebar/AI panels only need width. */
const TAB_NAV_PANEL_SELECTOR = "[data-workspace-tab-navigation], [data-special-page-navigation]";
const RESIZE_PANEL_SELECTOR = `${TAB_NAV_PANEL_SELECTOR}, [data-app-sidebar]`;
function restoredPanelWidth(storageKey: string, fallback: number): number {
return Math.max(PANEL_MIN_WIDTH, Number(safeLocalStorageGet(storageKey)) || fallback);
}
/**
* Prefer the layout-owning panel root over the handle's immediate parent.
* Vertical tab handles live inside a nested `.app-tab-bar`, while width is
* owned by `[data-workspace-tab-navigation]` / `[data-special-page-navigation]`.
*/
function resolveResizePanel(handle: HTMLElement | null): HTMLElement | null {
return handle?.closest(RESIZE_PANEL_SELECTOR) ?? handle?.parentElement ?? null;
}
function applyPanelWidth(panel: HTMLElement | null, width: number) {
if (!panel) return;
panel.style.setProperty("width", `${width}px`);
if (panel.matches(TAB_NAV_PANEL_SELECTOR)) {
panel.style.setProperty("flex", `0 0 ${width}px`);
}
}
function availableAiPanelMaxWidth(handle: HTMLElement | null) {
const panel = handle?.parentElement;
const panel = resolveResizePanel(handle);
const flexibleContent = panel?.previousElementSibling as HTMLElement | null;
if (!panel || !flexibleContent) return DEFAULT_PANEL_MAX_WIDTH;
@@ -49,11 +70,11 @@ export function usePanelResize() {
document.body.append(resizeOverlay);
const resolvedMaxWidth = typeof maxWidth === "function" ? maxWidth(resizeHandle) : maxWidth;
const upperBound = Number.isFinite(resolvedMaxWidth) ? Math.max(PANEL_MIN_WIDTH, resolvedMaxWidth) : DEFAULT_PANEL_MAX_WIDTH;
const renderedWidth = resizeHandle?.parentElement?.getBoundingClientRect().width;
const panelElement = resolveResizePanel(resizeHandle);
const renderedWidth = panelElement?.getBoundingClientRect().width;
const requestedStartWidth = typeof renderedWidth === "number" && Number.isFinite(renderedWidth) && renderedWidth > 0 ? renderedWidth : widthRef.value;
const startWidth = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, requestedStartWidth));
widthRef.value = startWidth;
const panelElement = resizeHandle?.parentElement;
let currentWidth = startWidth;
let rafId: number | null = null;
@@ -63,12 +84,12 @@ export function usePanelResize() {
// happy-dom unit tests assert styles synchronously (no frame advance),
// so apply immediately under the test mode instead of via rAF.
if (import.meta.env.MODE === "test") {
panelElement?.style.setProperty("width", `${currentWidth}px`);
applyPanelWidth(panelElement, currentWidth);
return;
}
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
panelElement?.style.setProperty("width", `${currentWidth}px`);
applyPanelWidth(panelElement, currentWidth);
rafId = null;
});
};
@@ -78,7 +99,7 @@ export function usePanelResize() {
cancelAnimationFrame(rafId);
rafId = null;
}
panelElement?.style.setProperty("width", `${currentWidth}px`);
applyPanelWidth(panelElement, currentWidth);
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", finishResize);
document.removeEventListener("pointercancel", finishResize);