fix(layout): keep panel resizing smooth across plugin iframes
This commit is contained in:
8 files changed
+77
-31
No files matched your search
@@ -1009,7 +1009,7 @@ const appVersion = ref("");
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
|
||||
// Every pane's vertical strip writes back to this shared width/collapse state.
|
||||
function startTabBarResize(event: MouseEvent) {
|
||||
function startTabBarResize(event: PointerEvent) {
|
||||
if (settingsStore.editorSettings.tabPlacement === "right") {
|
||||
startRightTabBarResize(event);
|
||||
return;
|
||||
@@ -3918,7 +3918,7 @@ onUnmounted(() => {
|
||||
:class="[isClassicLayout ? 'h-full relative z-30 isolate bg-background' : 'h-full relative z-30 isolate rounded-md border border-border/80 bg-background', isAiPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[240px] max-w-full']"
|
||||
:style="isAiPanelMaximized ? {} : { width: aiPanelWidth + 'px' }"
|
||||
>
|
||||
<div v-if="!isAiPanelMaximized" class="panel-resize-handle panel-resize-handle--left" @mousedown="startAiPanelResize" />
|
||||
<div v-if="!isAiPanelMaximized" class="panel-resize-handle panel-resize-handle--left" @pointerdown="startAiPanelResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<AiAssistant
|
||||
v-if="aiPanelReady"
|
||||
@@ -3945,7 +3945,7 @@ onUnmounted(() => {
|
||||
:class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'"
|
||||
:style="{ width: historyWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startHistoryResize" />
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startHistoryResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<QueryHistory :current-connection-id="activeTab?.connectionId" :current-database="activeTab?.database" @restore="restoreHistorySql" @analyze-ai="analyzeHistoryWithAi" @close="closeRightSidebarPanel('history')" />
|
||||
</div>
|
||||
@@ -3957,7 +3957,7 @@ onUnmounted(() => {
|
||||
:class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'"
|
||||
:style="{ width: sqlLibraryWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startSqlLibraryResize" />
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlLibraryResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<SqlLibraryPanel @close="closeRightSidebarPanel('sqlLibrary')" />
|
||||
</div>
|
||||
@@ -3969,7 +3969,7 @@ onUnmounted(() => {
|
||||
:class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'"
|
||||
:style="{ width: sqlFilePanelWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startSqlFilePanelResize" />
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlFilePanelResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<SqlFilePanel @close="closeRightSidebarPanel('sqlFile')" />
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ defineProps<{
|
||||
const emit = defineEmits<{
|
||||
import: [source: "dbx" | "navicat" | "dbeaver" | "datagrip"];
|
||||
export: [];
|
||||
startResize: [event: MouseEvent];
|
||||
startResize: [event: PointerEvent];
|
||||
collapse: [];
|
||||
"open-settings": [initialTab: string];
|
||||
"add-to-ai": [nodes: TreeNode | TreeNode[]];
|
||||
@@ -296,7 +296,7 @@ defineExpose({ focusSearch, locateTabInSidebar });
|
||||
<ConnectionTree ref="connectionTreeRef" @open-settings="(initialTab) => emit('open-settings', initialTab)" @add-to-ai="(nodes) => emit('add-to-ai', nodes)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-resize-handle panel-resize-handle--right" @mousedown="emit('startResize', $event)" />
|
||||
<div class="panel-resize-handle panel-resize-handle--right" @pointerdown="emit('startResize', $event)" />
|
||||
<Dialog v-model:open="showDeleteSelectedConfirm">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -43,7 +43,7 @@ const emit = defineEmits<
|
||||
"activate-tab": [tabId: string];
|
||||
"locate-tab": [tab: QueryTab];
|
||||
"toggle-zen-mode": [];
|
||||
"start-resize": [event: MouseEvent];
|
||||
"start-resize": [event: PointerEvent];
|
||||
"toggle-collapse": [];
|
||||
"detach-tab": [tab: QueryTab];
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ const emit = defineEmits<{
|
||||
"activate-tab": [tabId: string];
|
||||
"locate-tab": [tab: QueryTab];
|
||||
"toggle-zen-mode": [];
|
||||
"start-resize": [event: MouseEvent];
|
||||
"start-resize": [event: PointerEvent];
|
||||
"toggle-collapse": [];
|
||||
"detach-tab": [tab: QueryTab];
|
||||
"activate-settings": [];
|
||||
@@ -1778,7 +1778,7 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
|
||||
</div>
|
||||
</div>
|
||||
<!-- Dragging any pane's handle resizes the shared vertical width; every pane follows. -->
|
||||
<div v-if="isVerticalLayout && !isTabBarCollapsed" class="panel-resize-handle" :class="settingsStore.editorSettings.tabPlacement === 'right' ? 'panel-resize-handle--left' : 'panel-resize-handle--right'" @mousedown="emit('start-resize', $event)" />
|
||||
<div v-if="isVerticalLayout && !isTabBarCollapsed" class="panel-resize-handle" :class="settingsStore.editorSettings.tabPlacement === 'right' ? 'panel-resize-handle--left' : 'panel-resize-handle--right'" @pointerdown="emit('start-resize', $event)" />
|
||||
<Dialog v-model:open="tabGroupEditorOpen">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -34,7 +34,7 @@ const emit = defineEmits<
|
||||
ContentAreaSurfaceEmits & {
|
||||
"locate-tab": [tab: QueryTab];
|
||||
"toggle-zen-mode": [];
|
||||
"start-resize": [event: MouseEvent];
|
||||
"start-resize": [event: PointerEvent];
|
||||
"toggle-collapse": [];
|
||||
"detach-tab": [tab: QueryTab];
|
||||
}
|
||||
|
||||
+35
-12
@@ -25,6 +25,12 @@ function rect(left: number, width: number): DOMRect {
|
||||
} as DOMRect;
|
||||
}
|
||||
|
||||
function pointerEvent(type: string, clientX: number, pointerId = 1): MouseEvent {
|
||||
const event = new MouseEvent(type, { bubbles: true, clientX });
|
||||
Object.defineProperty(event, "pointerId", { value: pointerId });
|
||||
return event;
|
||||
}
|
||||
|
||||
describe("usePanelResize", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("localStorage", localStorageMock);
|
||||
@@ -49,10 +55,10 @@ describe("usePanelResize", () => {
|
||||
vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(1000, 360));
|
||||
|
||||
const { aiPanelWidth, startAiPanelResize } = usePanelResize();
|
||||
handle.addEventListener("mousedown", startAiPanelResize);
|
||||
handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 1000 }));
|
||||
document.dispatchEvent(new MouseEvent("mousemove", { clientX: -1000 }));
|
||||
document.dispatchEvent(new MouseEvent("mouseup"));
|
||||
handle.addEventListener("pointerdown", startAiPanelResize);
|
||||
handle.dispatchEvent(pointerEvent("pointerdown", 1000));
|
||||
document.dispatchEvent(pointerEvent("pointermove", -1000));
|
||||
document.dispatchEvent(pointerEvent("pointerup", -1000));
|
||||
|
||||
expect(aiPanelWidth.value).toBe(1060);
|
||||
expect(localStorage.getItem("dbx-ai-panel-width")).toBe("1060");
|
||||
@@ -71,10 +77,10 @@ describe("usePanelResize", () => {
|
||||
vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(300, 700));
|
||||
|
||||
const { aiPanelWidth, startAiPanelResize } = usePanelResize();
|
||||
handle.addEventListener("mousedown", startAiPanelResize);
|
||||
handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 300 }));
|
||||
document.dispatchEvent(new MouseEvent("mousemove", { clientX: 400 }));
|
||||
document.dispatchEvent(new MouseEvent("mouseup"));
|
||||
handle.addEventListener("pointerdown", startAiPanelResize);
|
||||
handle.dispatchEvent(pointerEvent("pointerdown", 300));
|
||||
document.dispatchEvent(pointerEvent("pointermove", 400));
|
||||
document.dispatchEvent(pointerEvent("pointerup", 400));
|
||||
|
||||
expect(aiPanelWidth.value).toBe(600);
|
||||
expect(localStorage.getItem("dbx-ai-panel-width")).toBe("600");
|
||||
@@ -89,15 +95,32 @@ describe("usePanelResize", () => {
|
||||
vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(0, 260));
|
||||
|
||||
const { sidebarWidth, startSidebarResize } = usePanelResize();
|
||||
handle.addEventListener("mousedown", startSidebarResize);
|
||||
handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 260 }));
|
||||
document.dispatchEvent(new MouseEvent("mousemove", { clientX: 0 }));
|
||||
document.dispatchEvent(new MouseEvent("mouseup"));
|
||||
handle.addEventListener("pointerdown", startSidebarResize);
|
||||
handle.dispatchEvent(pointerEvent("pointerdown", 260));
|
||||
document.dispatchEvent(pointerEvent("pointermove", 0));
|
||||
|
||||
expect(panel.style.width).toBe("240px");
|
||||
document.dispatchEvent(pointerEvent("pointerup", 0));
|
||||
|
||||
expect(sidebarWidth.value).toBe(240);
|
||||
expect(localStorage.getItem("dbx-sidebar-width")).toBe("240");
|
||||
});
|
||||
|
||||
it("removes the drag overlay when the window loses focus", () => {
|
||||
const panel = document.createElement("div");
|
||||
const handle = document.createElement("div");
|
||||
panel.append(handle);
|
||||
document.body.append(panel);
|
||||
|
||||
const { startSidebarResize } = usePanelResize();
|
||||
handle.addEventListener("pointerdown", startSidebarResize);
|
||||
handle.dispatchEvent(pointerEvent("pointerdown", 260));
|
||||
|
||||
expect(document.body.querySelector('[aria-hidden="true"]')).not.toBeNull();
|
||||
window.dispatchEvent(new Event("blur"));
|
||||
expect(document.body.querySelector('[aria-hidden="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("raises persisted panel widths below the toolbar-safe minimum", () => {
|
||||
localStorage.setItem("dbx-sidebar-width", "180");
|
||||
localStorage.setItem("dbx-ai-panel-width", "200");
|
||||
|
||||
@@ -29,30 +29,52 @@ export function usePanelResize() {
|
||||
const tabBarCollapsed = ref(safeLocalStorageGet("dbx-tab-bar-collapsed") === "true");
|
||||
|
||||
function startPanelResize(widthRef: Ref<number>, storageKey: string, direction: "left" | "right", maxWidth: PanelMaxWidth = DEFAULT_PANEL_MAX_WIDTH) {
|
||||
return (e: MouseEvent) => {
|
||||
return (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const resizeHandle = e.currentTarget as HTMLElement | null;
|
||||
resizeHandle?.setPointerCapture(e.pointerId);
|
||||
const resizeOverlay = document.createElement("div");
|
||||
resizeOverlay.setAttribute("aria-hidden", "true");
|
||||
Object.assign(resizeOverlay.style, {
|
||||
position: "fixed",
|
||||
inset: "0",
|
||||
zIndex: "2147483647",
|
||||
cursor: "col-resize",
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
});
|
||||
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 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;
|
||||
|
||||
const onMouseMove = (ev: MouseEvent) => {
|
||||
const onPointerMove = (ev: PointerEvent) => {
|
||||
const delta = ev.clientX - startX;
|
||||
widthRef.value = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, startWidth + (direction === "right" ? delta : -delta)));
|
||||
currentWidth = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, startWidth + (direction === "right" ? delta : -delta)));
|
||||
panelElement?.style.setProperty("width", `${currentWidth}px`);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
const finishResize = () => {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", finishResize);
|
||||
document.removeEventListener("pointercancel", finishResize);
|
||||
window.removeEventListener("blur", finishResize);
|
||||
if (resizeHandle?.hasPointerCapture(e.pointerId)) resizeHandle.releasePointerCapture(e.pointerId);
|
||||
resizeOverlay.remove();
|
||||
widthRef.value = currentWidth;
|
||||
safeLocalStorageSet(storageKey, String(widthRef.value));
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", finishResize);
|
||||
document.addEventListener("pointercancel", finishResize);
|
||||
window.addEventListener("blur", finishResize, { once: true });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2522,6 +2522,7 @@ html.theme-vscode .app-tab-scroll.wrap-mode .app-tab-pill[data-active-tab="false
|
||||
bottom: 0;
|
||||
width: 7px;
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user