mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(structure): 表结构编辑器补充可拖拽的纵向滚动条 (#9585)
编辑表结构的「字段 / 索引」页签之前只有自绘的横向滚动条,纵向只能靠滚轮, 字段较多时既没有“还有更多内容”的视觉提示,也无法拖拽定位。 - 复用 useVerticalOverlayScrollbar 组合式函数,为当前激活的表格页签 (字段 / 索引)渲染与横向滚动条同款的覆盖式纵向滚动条; - 页签面板改为三行两列的 grid:工具栏、滚动区、横向条在第一列, 纵向条独占第二列,仅在有溢出且处于表格页签时出现; - 隐藏该滚动区残留的原生纵向滚动条,避免与自绘条重叠; - 补充单测:纵向滚动条在有溢出时出现、拖拽可同步 scrollTop, 以及非表格页签(DDL)下不渲染。 Fixes #9408 Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
+49
@@ -837,6 +837,55 @@ describe("TableStructureEditor horizontal scrolling", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("TableStructureEditor vertical scrolling", () => {
|
||||
it("shows a fixed scrollbar for overflowing fields and syncs thumb dragging", async () => {
|
||||
const root = await mountEditor("postgres");
|
||||
const scroller = root.querySelector<HTMLElement>(".structure-table-scroller");
|
||||
if (!scroller) throw new Error("Missing structure table scroller");
|
||||
Object.defineProperties(scroller, {
|
||||
clientHeight: { configurable: true, value: 200 },
|
||||
scrollHeight: { configurable: true, value: 800 },
|
||||
scrollTop: { configurable: true, value: 0, writable: true },
|
||||
});
|
||||
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
const track = root.querySelector<HTMLElement>(".structure-vertical-scrollbar");
|
||||
const thumb = root.querySelector<HTMLElement>(".structure-vertical-scrollbar__thumb");
|
||||
if (!track || !thumb) throw new Error("Missing fixed vertical scrollbar");
|
||||
expect(Number.parseFloat(thumb.style.height)).toBeCloseTo(25);
|
||||
expect(Number.parseFloat(thumb.style.top)).toBeCloseTo(0);
|
||||
|
||||
track.getBoundingClientRect = () => DOMRect.fromRect({ width: 10, height: 200 });
|
||||
document.body.style.userSelect = "text";
|
||||
track.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, button: 0, clientY: 50, isPrimary: true }));
|
||||
window.dispatchEvent(new PointerEvent("pointermove", { clientY: 125, isPrimary: true }));
|
||||
|
||||
expect(scroller.scrollTop).toBeCloseTo(400);
|
||||
expect(document.body.style.userSelect).toBe("none");
|
||||
window.dispatchEvent(new PointerEvent("pointerup", { isPrimary: true }));
|
||||
expect(document.body.style.userSelect).toBe("");
|
||||
});
|
||||
|
||||
it("does not render the vertical scrollbar when the editor opens on the DDL tab", async () => {
|
||||
const root = await mountLoadingEditor("ddl");
|
||||
const scroller = root.querySelector<HTMLElement>(".structure-table-scroller");
|
||||
if (!scroller) throw new Error("Missing structure table scroller");
|
||||
Object.defineProperties(scroller, {
|
||||
clientHeight: { configurable: true, value: 200 },
|
||||
scrollHeight: { configurable: true, value: 800 },
|
||||
});
|
||||
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(root.querySelector(".structure-vertical-scrollbar")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TableStructureEditor metadata loading", () => {
|
||||
it("opens the initial DDL tab with its always-visible table comment", async () => {
|
||||
const root = await mountLoadingEditor("ddl", "app_user", "Application users");
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useTheme } from "@/composables/useTheme";
|
||||
import { editorFontTheme, loadEditorTheme } from "@/lib/editor/editorThemes";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useVerticalOverlayScrollbar } from "@/composables/useVerticalOverlayScrollbar";
|
||||
import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sql/sqlHighlighter";
|
||||
import { joinSqlStatementsForScript } from "@/lib/sql/sqlBatchScript";
|
||||
import { formatGeneratedDdlIdentifierQuotes, omitDdlDatabaseQualifier, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
|
||||
@@ -128,6 +129,9 @@ const constraintsScrollerRef = ref<StructureScrollerRef>();
|
||||
const triggersScrollerRef = ref<StructureScrollerRef>();
|
||||
const ddlScrollerRef = ref<StructureScrollerRef>();
|
||||
const structureHorizontalScrollbarTrackRef = ref<HTMLDivElement>();
|
||||
const columnsTableRef = ref<HTMLElement | null>(null);
|
||||
const indexesTableRef = ref<HTMLElement | null>(null);
|
||||
const structureVerticalScrollbarTrackRef = ref<HTMLElement | null>(null);
|
||||
const structureHorizontalScrollbarThumbRef = ref<HTMLDivElement>();
|
||||
const hasStructureHorizontalOverflow = ref(false);
|
||||
const dynamicDataTypeOptionsCache = new Map<string, string[]>();
|
||||
@@ -167,6 +171,21 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const activeTab = ref<TableInfoTab>("columns");
|
||||
|
||||
// DBX hides native scrollbars, so the columns/indexes tables only had the custom
|
||||
// horizontal bar. Render the same overlay affordance vertically for whichever
|
||||
// table tab is active; the bar occupies the tab panel's second grid column.
|
||||
const activeStructureTableScrollerRef = computed<HTMLElement | null>(() => structureScrollerElement(activeTab.value === "indexes" ? indexesScrollerRef.value : columnsScrollerRef.value) ?? null);
|
||||
const activeStructureTableContentRef = computed<HTMLElement | null>(() => (activeTab.value === "indexes" ? indexesTableRef.value : columnsTableRef.value) ?? null);
|
||||
const {
|
||||
hasOverflow: hasStructureVerticalOverflow,
|
||||
isScrolling: isStructureVerticalScrollbarScrolling,
|
||||
isDragging: isStructureVerticalScrollbarDragging,
|
||||
thumbStyle: structureVerticalScrollbarThumbStyle,
|
||||
onScroll: onStructureVerticalScrollerScroll,
|
||||
onTrackPointerDown: onStructureVerticalScrollbarTrackPointerDown,
|
||||
onThumbPointerDown: onStructureVerticalScrollbarThumbPointerDown,
|
||||
} = useVerticalOverlayScrollbar(activeStructureTableScrollerRef, activeStructureTableContentRef, structureVerticalScrollbarTrackRef);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const postSaveRefreshing = ref(false);
|
||||
@@ -1390,7 +1409,10 @@ function restoreStructureScrollPosition(tab = activeTab.value) {
|
||||
function onStructureContentScroll(tab: TableInfoTab, event: Event) {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
if (tab === "columns" || tab === "indexes") updateStructureHorizontalScrollbar(target);
|
||||
if (tab === "columns" || tab === "indexes") {
|
||||
updateStructureHorizontalScrollbar(target);
|
||||
if (activeTab.value === tab) onStructureVerticalScrollerScroll();
|
||||
}
|
||||
const position: TableStructureEditorViewport = {
|
||||
scrollTop: Math.max(0, Math.round(target.scrollTop)),
|
||||
scrollLeft: Math.max(0, Math.round(target.scrollLeft)),
|
||||
@@ -4337,8 +4359,8 @@ watch(
|
||||
|
||||
<div v-else class="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden">
|
||||
<div class="min-h-0 min-w-0 flex-1 overflow-hidden rounded-md border">
|
||||
<Tabs v-model="activeTab" class="flex h-full min-h-0 flex-col">
|
||||
<div class="flex shrink-0 items-center justify-between gap-2 border-b px-2 py-[var(--structure-header-py)]">
|
||||
<Tabs v-model="activeTab" class="structure-tabs h-full min-h-0">
|
||||
<div class="col-start-1 row-start-1 flex shrink-0 items-center justify-between gap-2 border-b px-2 py-[var(--structure-header-py)]">
|
||||
<TabsList>
|
||||
<TabsTrigger v-if="tableMetadataCapabilities.ddl && !isCreateMode" value="ddl">
|
||||
DDL
|
||||
@@ -4456,8 +4478,8 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent ref="columnsScrollerRef" v-if="tableMetadataCapabilities.columns" value="columns" class="structure-table-scroller m-0 min-h-0 flex-1 overflow-auto p-0" @scroll.passive="onStructureContentScroll('columns', $event)">
|
||||
<table class="structure-edit-grid border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: visibleColWidths.reduce((a, w) => a + w, 0) + 'px' }">
|
||||
<TabsContent ref="columnsScrollerRef" v-if="tableMetadataCapabilities.columns" value="columns" class="col-start-1 row-start-2 structure-table-scroller m-0 min-h-0 flex-1 overflow-auto p-0" @scroll.passive="onStructureContentScroll('columns', $event)">
|
||||
<table ref="columnsTableRef" class="structure-edit-grid border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: visibleColWidths.reduce((a, w) => a + w, 0) + 'px' }">
|
||||
<thead class="sticky top-0 z-10 bg-background">
|
||||
<tr>
|
||||
<th
|
||||
@@ -4914,7 +4936,7 @@ watch(
|
||||
</table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent ref="indexesScrollerRef" v-if="tableMetadataCapabilities.indexes" value="indexes" class="structure-table-scroller m-0 min-h-0 flex-1 overflow-auto p-0" @scroll.passive="onStructureContentScroll('indexes', $event)">
|
||||
<TabsContent ref="indexesScrollerRef" v-if="tableMetadataCapabilities.indexes" value="indexes" class="col-start-1 row-start-2 structure-table-scroller m-0 min-h-0 flex-1 overflow-auto p-0" @scroll.passive="onStructureContentScroll('indexes', $event)">
|
||||
<div v-if="indexesLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
@@ -4922,7 +4944,7 @@ watch(
|
||||
<div v-else-if="secondaryMetadataErrors.indexes" class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{{ secondaryMetadataErrors.indexes }}
|
||||
</div>
|
||||
<table v-else class="structure-edit-grid border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: indexColWidths.reduce((a, w) => a + w, 0) + 'px' }">
|
||||
<table v-else ref="indexesTableRef" class="structure-edit-grid border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: indexColWidths.reduce((a, w) => a + w, 0) + 'px' }">
|
||||
<thead class="sticky top-0 z-10 bg-background">
|
||||
<tr>
|
||||
<th
|
||||
@@ -5028,11 +5050,27 @@ watch(
|
||||
</table>
|
||||
</TabsContent>
|
||||
|
||||
<div v-if="hasStructureHorizontalOverflow && (activeTab === 'columns' || activeTab === 'indexes')" ref="structureHorizontalScrollbarTrackRef" class="structure-horizontal-scrollbar" @pointerdown="startStructureHorizontalScrollbarDrag">
|
||||
<div v-if="hasStructureHorizontalOverflow && (activeTab === 'columns' || activeTab === 'indexes')" ref="structureHorizontalScrollbarTrackRef" class="structure-horizontal-scrollbar col-start-1 row-start-3" @pointerdown="startStructureHorizontalScrollbarDrag">
|
||||
<div ref="structureHorizontalScrollbarThumbRef" class="structure-horizontal-scrollbar__thumb" />
|
||||
</div>
|
||||
|
||||
<TabsContent ref="foreignKeysScrollerRef" v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys" class="structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('foreignKeys', $event)">
|
||||
<div
|
||||
v-if="hasStructureVerticalOverflow && (activeTab === 'columns' || activeTab === 'indexes')"
|
||||
ref="structureVerticalScrollbarTrackRef"
|
||||
class="structure-vertical-scrollbar col-start-2 row-start-2"
|
||||
:class="{ 'structure-vertical-scrollbar--scrolling': isStructureVerticalScrollbarScrolling, 'structure-vertical-scrollbar--dragging': isStructureVerticalScrollbarDragging }"
|
||||
@pointerdown="onStructureVerticalScrollbarTrackPointerDown"
|
||||
>
|
||||
<div class="structure-vertical-scrollbar__thumb" :style="structureVerticalScrollbarThumbStyle" @pointerdown.stop="onStructureVerticalScrollbarThumbPointerDown" />
|
||||
</div>
|
||||
|
||||
<TabsContent
|
||||
ref="foreignKeysScrollerRef"
|
||||
v-if="tableMetadataCapabilities.foreignKeys"
|
||||
value="foreignKeys"
|
||||
class="col-start-1 row-start-2 structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]"
|
||||
@scroll.passive="onStructureContentScroll('foreignKeys', $event)"
|
||||
>
|
||||
<div v-if="foreignKeysLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
@@ -5085,7 +5123,13 @@ watch(
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent ref="constraintsScrollerRef" v-if="tableMetadataCapabilities.constraints" value="constraints" class="structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('constraints', $event)">
|
||||
<TabsContent
|
||||
ref="constraintsScrollerRef"
|
||||
v-if="tableMetadataCapabilities.constraints"
|
||||
value="constraints"
|
||||
class="col-start-1 row-start-2 structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]"
|
||||
@scroll.passive="onStructureContentScroll('constraints', $event)"
|
||||
>
|
||||
<div v-if="constraintsLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
@@ -5111,7 +5155,7 @@ watch(
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent ref="triggersScrollerRef" v-if="tableMetadataCapabilities.triggers" value="triggers" class="structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('triggers', $event)">
|
||||
<TabsContent ref="triggersScrollerRef" v-if="tableMetadataCapabilities.triggers" value="triggers" class="col-start-1 row-start-2 structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('triggers', $event)">
|
||||
<div v-if="triggersLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
@@ -5197,7 +5241,14 @@ watch(
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent ref="ddlScrollerRef" v-if="tableMetadataCapabilities.ddl" value="ddl" force-mount class="structure-card-scroller relative m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)] data-[state=inactive]:hidden" @scroll.passive="onStructureContentScroll('ddl', $event)">
|
||||
<TabsContent
|
||||
ref="ddlScrollerRef"
|
||||
v-if="tableMetadataCapabilities.ddl"
|
||||
value="ddl"
|
||||
force-mount
|
||||
class="col-start-1 row-start-2 structure-card-scroller relative m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)] data-[state=inactive]:hidden"
|
||||
@scroll.passive="onStructureContentScroll('ddl', $event)"
|
||||
>
|
||||
<div v-if="ddlLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
@@ -5436,8 +5487,25 @@ watch(
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.structure-table-scroller {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* Tab panel layout: toolbar, scrollable content and the custom horizontal bar
|
||||
stack in the first column, while the custom vertical bar occupies the second
|
||||
column of the content row. */
|
||||
.structure-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
/* Both native scrollbars are replaced: the horizontal one by
|
||||
`.structure-horizontal-scrollbar`, the vertical one by
|
||||
`.structure-vertical-scrollbar`. */
|
||||
.structure-table-scroller::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@@ -5485,6 +5553,35 @@ watch(
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.structure-vertical-scrollbar {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
cursor: default;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.structure-vertical-scrollbar__thumb {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
width: 4px;
|
||||
min-height: 24px;
|
||||
border-radius: 999px;
|
||||
background: rgba(82, 82, 82, 0.3);
|
||||
background: color-mix(in oklab, var(--foreground) 30%, transparent);
|
||||
transition:
|
||||
left 120ms ease,
|
||||
width 120ms ease,
|
||||
background-color 120ms ease;
|
||||
}
|
||||
|
||||
.structure-vertical-scrollbar:hover .structure-vertical-scrollbar__thumb,
|
||||
.structure-vertical-scrollbar--dragging .structure-vertical-scrollbar__thumb {
|
||||
left: 2px;
|
||||
width: 6px;
|
||||
background: rgba(82, 82, 82, 0.48);
|
||||
background: color-mix(in oklab, var(--foreground) 48%, transparent);
|
||||
}
|
||||
|
||||
.structure-horizontal-scrollbar {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
@@ -5595,7 +5692,8 @@ watch(
|
||||
html.dbx-legacy-webview.dark .structure-ddl-editor .cm-scroller::-webkit-scrollbar-thumb,
|
||||
html.dbx-legacy-webview.dark .structure-card-scroller::-webkit-scrollbar-thumb,
|
||||
html.dbx-legacy-webview.dark .structure-table-scroller::-webkit-scrollbar-thumb,
|
||||
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar__thumb {
|
||||
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar__thumb,
|
||||
html.dbx-legacy-webview.dark .structure-vertical-scrollbar__thumb {
|
||||
background: rgba(212, 212, 216, 0.3);
|
||||
}
|
||||
|
||||
@@ -5603,7 +5701,9 @@ html.dbx-legacy-webview.dark .structure-ddl-editor .cm-scroller::-webkit-scrollb
|
||||
html.dbx-legacy-webview.dark .structure-card-scroller::-webkit-scrollbar-thumb:hover,
|
||||
html.dbx-legacy-webview.dark .structure-table-scroller::-webkit-scrollbar-thumb:hover,
|
||||
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar:hover .structure-horizontal-scrollbar__thumb,
|
||||
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar--dragging .structure-horizontal-scrollbar__thumb {
|
||||
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar--dragging .structure-horizontal-scrollbar__thumb,
|
||||
html.dbx-legacy-webview.dark .structure-vertical-scrollbar:hover .structure-vertical-scrollbar__thumb,
|
||||
html.dbx-legacy-webview.dark .structure-vertical-scrollbar--dragging .structure-vertical-scrollbar__thumb {
|
||||
background: rgba(212, 212, 216, 0.48);
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Reference in New Issue
Block a user