mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 05:20:02 +00:00
fix(tabs): make vertical tab rail width follow mouse while dragging
This commit is contained in:
@@ -175,13 +175,12 @@ const isWrapLayout = computed(() => !isVerticalLayout.value && settingsStore.edi
|
||||
// The icon-only collapse only exists in the vertical toolbar; horizontal
|
||||
// placements must ignore the persisted collapse state entirely.
|
||||
const isTabBarCollapsed = computed(() => isVerticalLayout.value && !!props.tabBarCollapsed);
|
||||
// Outer [data-workspace-tab-navigation] / [data-special-page-navigation] owns
|
||||
// the shared vertical rail width; fill that rail so nested pane bars stay in
|
||||
// sync while the resize handle updates the outer panel (issue #9977).
|
||||
const tabBarStyle = computed<CSSProperties | undefined>(() => {
|
||||
if (!isVerticalLayout.value) return undefined;
|
||||
if (props.tabBarCollapsed) {
|
||||
return { width: "100%", flex: "0 0 100%" };
|
||||
}
|
||||
const width = props.tabBarWidth ?? 240;
|
||||
return { width: `${width}px`, flex: `0 0 ${width}px` };
|
||||
return { width: "100%", flex: "0 0 100%" };
|
||||
});
|
||||
const tabBarCollapseIcon = computed(() => {
|
||||
const isLeft = settingsStore.editorSettings.tabPlacement === "left";
|
||||
|
||||
@@ -356,11 +356,11 @@ describe("AppTabBar group navigation portal", () => {
|
||||
const rail = element(host, "[data-workspace-tab-navigation]");
|
||||
const bar = element(rail, "[data-main-tab-bar]");
|
||||
expect(rail.style.width).toBe("260px");
|
||||
expect(bar.style.width).toBe("260px");
|
||||
expect(bar.style.width).toBe("100%");
|
||||
navigation.width = 320;
|
||||
await settle();
|
||||
expect(rail.style.width).toBe("320px");
|
||||
expect(bar.style.width).toBe("320px");
|
||||
expect(bar.style.width).toBe("100%");
|
||||
navigation.collapsed = true;
|
||||
await settle();
|
||||
expect(rail.style.width).toBe("var(--collapsed-tab-rail-width)");
|
||||
@@ -369,7 +369,7 @@ describe("AppTabBar group navigation portal", () => {
|
||||
navigation.collapsed = false;
|
||||
await settle();
|
||||
expect(rail.style.width).toBe("320px");
|
||||
expect(bar.style.width).toBe("320px");
|
||||
expect(bar.style.width).toBe("100%");
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user