fix(editor): stop toolbar flicker at resize threshold width
This commit is contained in:
3 files changed
+57
-21
No files matched your search
@@ -84,39 +84,56 @@ const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOpt
|
||||
const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas, isSchemaAware } = useSchemaOptions();
|
||||
|
||||
const toolbarRootRef = ref<HTMLElement | null>(null);
|
||||
const toolbarActionsRef = ref<HTMLElement | null>(null);
|
||||
const toolbarTier = ref<EditorToolbarTier>(0);
|
||||
// Available width when the current tier was condensed into; anchors the
|
||||
// step-down hysteresis so a static narrow layout cannot oscillate.
|
||||
const condensedAtWidth = ref(0);
|
||||
const expandedTierRequiredWidths: Partial<Record<EditorToolbarTier, number>> = {};
|
||||
let toolbarResizeObserver: ResizeObserver | undefined;
|
||||
let toolbarMeasureRaf = 0;
|
||||
|
||||
function measureToolbarTier() {
|
||||
const element = toolbarRootRef.value;
|
||||
if (!element) {
|
||||
const element = toolbarActionsRef.value;
|
||||
const root = toolbarRootRef.value;
|
||||
if (!element || !root) {
|
||||
return;
|
||||
}
|
||||
const availableWidth = root.clientWidth;
|
||||
// Use the stable full-row coordinate space, including the right controls.
|
||||
// Tier-dependent helpers must not appear to grow the pane when they hide.
|
||||
const contentWidth = element.scrollWidth + availableWidth - element.clientWidth;
|
||||
const next = resolveNextEditorToolbarTier({
|
||||
tier: toolbarTier.value,
|
||||
availableWidth: element.clientWidth,
|
||||
contentWidth: element.scrollWidth,
|
||||
availableWidth,
|
||||
contentWidth,
|
||||
condensedAtWidth: condensedAtWidth.value,
|
||||
expandedTierRequiredWidths,
|
||||
});
|
||||
if (next !== toolbarTier.value) {
|
||||
if (next > toolbarTier.value) {
|
||||
const currentTier = toolbarTier.value;
|
||||
expandedTierRequiredWidths[currentTier] = Math.max(expandedTierRequiredWidths[currentTier] ?? 0, element.scrollWidth);
|
||||
condensedAtWidth.value = element.clientWidth;
|
||||
expandedTierRequiredWidths[currentTier] = Math.max(expandedTierRequiredWidths[currentTier] ?? 0, contentWidth);
|
||||
condensedAtWidth.value = availableWidth;
|
||||
}
|
||||
toolbarTier.value = next;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleToolbarMeasurement() {
|
||||
if (toolbarMeasureRaf) {
|
||||
return;
|
||||
}
|
||||
toolbarMeasureRaf = requestAnimationFrame(() => {
|
||||
toolbarMeasureRaf = 0;
|
||||
measureToolbarTier();
|
||||
});
|
||||
}
|
||||
|
||||
// Hiding or restoring controls changes the row content without resizing the
|
||||
// toolbar box, so every tier change re-measures until the row settles.
|
||||
watch(toolbarTier, () => {
|
||||
void nextTick(measureToolbarTier);
|
||||
void nextTick(scheduleToolbarMeasurement);
|
||||
});
|
||||
|
||||
// The visible control set also changes with connection type and transaction
|
||||
@@ -124,7 +141,7 @@ watch(toolbarTier, () => {
|
||||
watch(
|
||||
() => [props.activeConnection?.id, props.activeConnection?.db_type, props.txnSessionId, props.activeTab.isExecuting, props.activeTab.isExplaining] as const,
|
||||
() => {
|
||||
void nextTick(measureToolbarTier);
|
||||
void nextTick(scheduleToolbarMeasurement);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -134,15 +151,19 @@ watch(
|
||||
toolbarResizeObserver?.disconnect();
|
||||
toolbarResizeObserver = undefined;
|
||||
if (element && typeof ResizeObserver !== "undefined") {
|
||||
toolbarResizeObserver = new ResizeObserver(measureToolbarTier);
|
||||
toolbarResizeObserver = new ResizeObserver(scheduleToolbarMeasurement);
|
||||
toolbarResizeObserver.observe(element);
|
||||
}
|
||||
void nextTick(measureToolbarTier);
|
||||
void nextTick(scheduleToolbarMeasurement);
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (toolbarMeasureRaf) {
|
||||
cancelAnimationFrame(toolbarMeasureRaf);
|
||||
toolbarMeasureRaf = 0;
|
||||
}
|
||||
toolbarResizeObserver?.disconnect();
|
||||
toolbarResizeObserver = undefined;
|
||||
});
|
||||
@@ -153,7 +174,10 @@ const activeCatalogs = computed(() => {
|
||||
});
|
||||
const activeCatalogNames = computed(() => activeCatalogs.value.map((catalog) => catalog.name));
|
||||
const catalogSelectorAvailable = computed(() => connectionIsDorisFamilyCatalogCapable(props.activeConnection) && queryCatalogSelectorVisible(activeCatalogs.value));
|
||||
const showCatalogSelector = computed(() => catalogSelectorAvailable.value && toolbarTier.value < 3);
|
||||
// Keep connection context controls mounted while the action group condenses.
|
||||
// Removing them changes the action group's available width and can make the
|
||||
// tier immediately expand again at the boundary, causing visible flicker.
|
||||
const showCatalogSelector = computed(() => catalogSelectorAvailable.value);
|
||||
const activeCatalogValue = computed(() => selectedQueryCatalogName(activeCatalogs.value, props.activeTab.catalog));
|
||||
const activeCatalogDatabaseKey = computed(() => (props.activeConnection && props.activeTab.catalog ? catalogDatabaseOptionsKey(props.activeConnection.id, props.activeTab.catalog) : ""));
|
||||
const activeDatabaseOptions = computed(() => {
|
||||
@@ -277,7 +301,7 @@ const schemaSelectorAvailable = computed(() => {
|
||||
const connection = props.activeConnection;
|
||||
return connection && isSchemaAware(connection.id) && (props.activeTab.database || isSingleDb.value || hasDefaultDatabaseOption.value);
|
||||
});
|
||||
const showSchemaSelector = computed(() => schemaSelectorAvailable.value && toolbarTier.value < 3);
|
||||
const showSchemaSelector = computed(() => schemaSelectorAvailable.value);
|
||||
|
||||
const activeSchemaOptions = computed(() => {
|
||||
const connection = props.activeConnection;
|
||||
@@ -389,8 +413,8 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="toolbarRootRef" class="app-editor-toolbar h-9 shrink-0 border-b bg-background/80 px-3 flex items-center gap-1 text-xs text-muted-foreground relative z-10 overflow-hidden" :style="toolbarStyle">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div ref="toolbarRootRef" class="app-editor-toolbar h-9 min-w-0 shrink-0 border-b bg-background/80 px-3 flex items-center gap-1 text-xs text-muted-foreground relative z-10 overflow-hidden" :style="toolbarStyle">
|
||||
<div ref="toolbarActionsRef" class="min-w-0 flex flex-1 items-center gap-0.5 overflow-hidden">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
@@ -677,9 +701,8 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0" />
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<span v-if="activeConnection?.color" class="h-4 w-1 rounded-full shrink-0" :style="{ backgroundColor: activeConnection.color }" />
|
||||
<ConnectionTreeSelect
|
||||
:model-value="activeConnectionValue"
|
||||
@@ -703,7 +726,7 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</template>
|
||||
</ConnectionTreeSelect>
|
||||
</div>
|
||||
<div v-if="showCatalogSelector" class="flex min-w-0 items-center gap-1">
|
||||
<div v-if="showCatalogSelector" class="flex shrink-0 items-center gap-1">
|
||||
<SearchableSelect
|
||||
:model-value="activeCatalogValue"
|
||||
:options="activeCatalogNames"
|
||||
@@ -741,7 +764,7 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
activeConnection?.db_type !== 'consul' &&
|
||||
!isSingleDb
|
||||
"
|
||||
class="flex items-center gap-1"
|
||||
class="flex shrink-0 items-center gap-1"
|
||||
:class="{ 'database-required-prompt': databaseRequiredVisible }"
|
||||
>
|
||||
<SearchableSelect
|
||||
@@ -790,7 +813,7 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
{{ isActiveDatabaseDefault ? t("editor.defaultDatabase") : t("editor.setDefaultDatabase") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="showSchemaSelector" class="flex min-w-0 items-center gap-1">
|
||||
<div v-if="showSchemaSelector" class="flex shrink-0 items-center gap-1">
|
||||
<SearchableSelect
|
||||
:model-value="activeSchemaValue"
|
||||
:options="activeSchemaOptions.length ? activeSchemaOptions : activeSchemaValue ? [activeSchemaValue] : []"
|
||||
|
||||
@@ -56,6 +56,18 @@ describe("editor toolbar measured condensation", () => {
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it("does not restore a measured tier before the pane has grown past the hysteresis gap", () => {
|
||||
expect(
|
||||
resolveNextEditorToolbarTier({
|
||||
tier: 2,
|
||||
availableWidth: 460,
|
||||
contentWidth: 460,
|
||||
condensedAtWidth: 420,
|
||||
expandedTierRequiredWidths: { 1: 400 },
|
||||
}),
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it("never oscillates: slack alone without pane growth keeps the condensed tier", () => {
|
||||
// The row fits with plenty of slack, but the pane barely grew since the
|
||||
// toolbar condensed at this width — stepping down would overflow again.
|
||||
|
||||
@@ -56,9 +56,10 @@ export function resolveNextEditorToolbarTier(input: EditorToolbarTierInput): Edi
|
||||
return (tier + 1) as EditorToolbarTier;
|
||||
}
|
||||
if (tier > 0 && expandedTierRequiredWidths) {
|
||||
const grewSinceCondensation = condensedAtWidth <= 0 || availableWidth - condensedAtWidth >= EDITOR_TOOLBAR_STEP_DOWN_MIN_GROWTH_PX;
|
||||
for (let candidate = 0; candidate < tier; candidate += 1) {
|
||||
const requiredWidth = expandedTierRequiredWidths[candidate as EditorToolbarTier];
|
||||
if (requiredWidth && availableWidth >= requiredWidth + EDITOR_TOOLBAR_STEP_DOWN_SLACK_PX) {
|
||||
if (grewSinceCondensation && requiredWidth && availableWidth >= requiredWidth + EDITOR_TOOLBAR_STEP_DOWN_SLACK_PX) {
|
||||
return candidate as EditorToolbarTier;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user