improve(grid): adapt split toolbar collapse to measured overflow

This commit is contained in:
zipg
2026-10-15 14:46:15 +08:00
committed by GitHub
parent 041eb78f4b
commit 74b825f109
7 changed files with 245 additions and 11 deletions
+59 -7
View File
@@ -386,6 +386,7 @@ import {
DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER,
dataGridDeleteRowToolbarState,
dataGridToolbarActionCollapseCount,
shouldCompactDataGridConditionInput,
isDataGridToolbarCompact,
type DataGridReloadIntent,
type DataGridToolbarActionCapability,
@@ -832,6 +833,7 @@ const dataGridViewportWidth = ref(0);
const dataGridTopbarOverflowCompact = ref(false);
const dataGridTopbarOverflowActionCount = ref(0);
const dataGridTopbarExpandedRequiredWidth = ref(0);
const dataGridFilterExpandedRequiredWidth = ref(0);
const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader);
const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader);
const showColumnHeaderTooltips = computed(() => settingsStore.editorSettings.showColumnHeaderTooltips !== false);
@@ -853,10 +855,15 @@ const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.c
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
const dataGridRowNumberMode = computed(() => settingsStore.editorSettings.dataGridRowNumberMode);
const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
const splitDataGridToolbar = computed(() => settingsStore.editorSettings.dataGridToolbarLayout === "split");
const responsiveDataGridToolbarActionCount = computed(() => dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || (!splitDataGridToolbar.value && isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)));
const responsiveDataGridToolbarActionCount = computed(() => (splitDataGridToolbar.value ? 0 : dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)));
const compactDataGridToolbarActionCount = computed(() => Math.max(responsiveDataGridToolbarActionCount.value, dataGridTopbarOverflowActionCount.value));
const compactDataGridToolbarActions = computed(() => (splitDataGridToolbar.value ? compactDataGridToolbarActionCount.value > 0 : compactDataGridToolbar.value));
watch(splitDataGridToolbar, () => {
resetDataGridTopbarOverflowCompact();
nextTick(updateDataGridTopbarWidth);
});
const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll);
const queryResultMaxRows = computed(() => effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows));
const paginationMaxRows = computed(() => (isResultsContext.value ? queryResultMaxRows.value : undefined));
@@ -3107,6 +3114,50 @@ function updateDataGridTopbarWidth() {
return;
}
if (splitDataGridToolbar.value) {
// Split rows each have the whole pane: the single-row breakpoint reserves
// space for controls that no longer share their row. Measure them separately.
const filters = currentTopbar.querySelector<HTMLElement>('[data-grid-topbar-row="filters"]');
const filterWidth = filters?.clientWidth ?? 0;
const conditionEditors = filters?.querySelectorAll<HTMLElement>("[data-grid-condition-editor]") ?? [];
let conditionInputsNeedCompact = false;
for (const editor of conditionEditors) {
const input = editor.querySelector<HTMLElement>("[data-grid-condition-input-viewport]");
if (!input) continue;
// Input widths change when the WHERE/ORDER BY divider moves even when
// the whole toolbar keeps the same width. Observe their own viewports.
dataGridTopbarResizeObserver?.observe(input);
const label = editor.querySelector<HTMLElement>(".data-grid-topbar-condition-label");
const hiddenLabelWidth = label ? Math.max(0, label.scrollWidth - label.clientWidth) : 0;
conditionInputsNeedCompact ||= shouldCompactDataGridConditionInput(input.clientWidth, hiddenLabelWidth, dataGridTopbarOverflowCompact.value);
}
const filtersOverflow = filterWidth > 0 && !!filters && filters.scrollWidth > filterWidth + 1;
let changed = false;
if (dataGridTopbarOverflowCompact.value && filterWidth >= dataGridFilterExpandedRequiredWidth.value && !conditionInputsNeedCompact) {
dataGridTopbarOverflowCompact.value = false;
dataGridFilterExpandedRequiredWidth.value = 0;
changed = true;
} else if (!dataGridTopbarOverflowCompact.value && filterWidth > 0 && (filtersOverflow || conditionInputsNeedCompact)) {
dataGridFilterExpandedRequiredWidth.value = filtersOverflow ? filters!.scrollWidth + DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX : 0;
dataGridTopbarOverflowCompact.value = true;
changed = true;
}
const actionRows = currentTopbar.querySelectorAll<HTMLElement>(':scope > [data-grid-topbar-row="actions"]');
const actionsWidth = Array.from(actionRows).reduce((width, row) => width + Math.max(row.clientWidth, row.scrollWidth), 0);
if (dataGridTopbarOverflowActionCount.value > 0 && dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) {
dataGridTopbarOverflowActionCount.value = 0;
dataGridTopbarExpandedRequiredWidth.value = 0;
changed = true;
} else if (dataGridTopbarWidth.value > 0 && actionsWidth > dataGridTopbarWidth.value + 1 && dataGridTopbarOverflowActionCount.value < DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.length) {
dataGridTopbarExpandedRequiredWidth.value = Math.max(dataGridTopbarExpandedRequiredWidth.value, actionsWidth + DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX);
dataGridTopbarOverflowActionCount.value += 1;
changed = true;
}
if (changed) scheduleDataGridTopbarRecheck();
return;
}
if (dataGridTopbarOverflowCompact.value || dataGridTopbarOverflowActionCount.value > 0) {
if (dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) {
dataGridTopbarOverflowCompact.value = false;
@@ -3139,6 +3190,7 @@ function resetDataGridTopbarOverflowCompact() {
dataGridTopbarOverflowCompact.value = false;
dataGridTopbarOverflowActionCount.value = 0;
dataGridTopbarExpandedRequiredWidth.value = 0;
dataGridFilterExpandedRequiredWidth.value = 0;
scheduleDataGridTopbarRecheck();
}
@@ -13921,7 +13973,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
:class="splitDataGridToolbar ? 'grid h-16 grid-cols-[auto_minmax(0,1fr)] grid-rows-2' : 'flex h-8 items-center'"
>
<div v-if="hasResultToolbarLeadingSlot" data-grid-topbar-row="actions" class="flex shrink-0 items-center border-r" :class="splitDataGridToolbar ? 'col-start-1 row-start-1' : ''">
<slot name="result-toolbar-leading" :compact="compactDataGridToolbar" />
<slot name="result-toolbar-leading" :compact="compactDataGridToolbarActions" />
</div>
<!-- Clip both axes instead of creating a hidden scroll container around the toolbar controls. -->
<div data-grid-topbar-row="filters" class="data-grid-topbar-scroll min-w-0 overflow-clip" :class="splitDataGridToolbar ? 'data-grid-topbar-scroll--row-divider col-span-2 row-start-2' : 'flex-1'">
@@ -14035,12 +14087,12 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@wheel="onDataGridTopbarFixedActionWheel"
>
<template #leading>
<slot v-if="hasResultToolbarActionsSlot" name="result-toolbar-actions" :compact="compactDataGridToolbar" />
<slot v-if="hasResultToolbarActionsSlot" name="result-toolbar-actions" :compact="compactDataGridToolbarActions" />
<DataGridVirtualRowIdentifier
v-if="showVirtualRowIdentifierControl"
:columns="props.tableMeta!.columns"
:selected-columns="virtualRowIdentifierColumns"
:compact="compactDataGridToolbar"
:compact="compactDataGridToolbarActions"
:disabled="virtualRowIdentifierDisabled"
@apply="applyVirtualRowIdentifier"
@clear="clearVirtualRowIdentifier"
@@ -14071,7 +14123,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
<Button
variant="ghost"
size="sm"
:class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '', mongoJsonPreviewOpen ? 'text-primary bg-primary/10 hover:bg-primary/15' : '']"
:class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbarActions ? 'data-grid-topbar-action-button--compact' : '', mongoJsonPreviewOpen ? 'text-primary bg-primary/10 hover:bg-primary/15' : '']"
:aria-pressed="mongoJsonPreviewOpen"
@click="toggleMongoJsonPreview"
>
@@ -14079,7 +14131,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
<span
class="data-grid-topbar-action-label"
:class="{
'data-grid-topbar-action-label--compact': compactDataGridToolbar,
'data-grid-topbar-action-label--compact': compactDataGridToolbarActions,
}"
>{{ t("grid.mongoJsonPreview") }}</span
>
@@ -642,12 +642,12 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
</script>
<template>
<div ref="controlRef" class="relative flex min-w-0 flex-1 items-stretch">
<div ref="controlRef" :data-grid-condition-editor="props.kind" class="relative flex min-w-0 flex-1 items-stretch">
<div class="flex min-w-0 flex-1 items-center gap-1">
<span class="data-grid-topbar-condition-label" :class="[props.kind === 'where' ? 'data-grid-topbar-condition-label--where' : 'data-grid-topbar-condition-label--order', { 'data-grid-topbar-condition-label--compact': props.compact }]">
{{ props.kind === "where" ? "WHERE" : "ORDER BY" }}
</span>
<div class="relative h-6 min-w-0 flex-1 overflow-hidden">
<div data-grid-condition-input-viewport class="relative h-6 min-w-0 flex-1 overflow-hidden">
<div v-if="!composing" aria-hidden="true" class="data-grid-condition-highlight pointer-events-none absolute left-0 top-0" :style="collapsedHighlightStyle">
<span v-for="(token, tokenIndex) in highlightTokens" :key="tokenIndex" :class="highlightTokenClass(token.type)">{{ token.text }}</span>
</div>
@@ -16,7 +16,7 @@ import type { DataGridDistinctValueSource, DataGridDistinctValueSuggestionState,
import type { DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
import type { DataGridFilterEditorView } from "@/stores/settingsStore";
import type { DatabaseType } from "@/types/database";
import { clampSearchSplitWidth } from "@/lib/dataGrid/dataGridSearchSplit";
import { clampSearchSplitWidth, MIN_SEARCH_SPLIT_PANE_WIDTH } from "@/lib/dataGrid/dataGridSearchSplit";
type LocalFilterSummary = {
columnIndex: number;
@@ -114,7 +114,15 @@ const resizing = ref(false);
let resizeStartX = 0;
let resizeStartWidth = 0;
const wherePaneStyle = computed(() => (whereWidth.value == null ? {} : { flex: `0 0 ${whereWidth.value}px` }));
const wherePaneStyle = computed(() =>
whereWidth.value == null
? {}
: {
// Retain the dragged preference while keeping both panes visible when
// their container later shrinks, without a reactive resize loop.
flex: `0 0 clamp(min(${MIN_SEARCH_SPLIT_PANE_WIDTH}px, 50%), ${whereWidth.value}px, max(50%, calc(100% - ${MIN_SEARCH_SPLIT_PANE_WIDTH}px)))`,
},
);
function containerWidth(): number {
return containerRef.value?.getBoundingClientRect().width ?? 0;
@@ -63,6 +63,7 @@ function mountGrid(
databaseType?: DatabaseType;
sourceDatabase?: string;
joinedWriteTargetCount?: number;
filterControls?: boolean;
} = {},
) {
const pinia = createPinia();
@@ -101,6 +102,7 @@ function mountGrid(
result,
databaseType: options.databaseType ?? "mysql",
context: options.context ?? "table-data",
onExecuteSql: options.filterControls ? async () => undefined : undefined,
queryMultiSource: options.queryMultiSource,
joinedWriteTargets: options.joinedWriteTargetCount ? Array.from({ length: options.joinedWriteTargetCount }, (_, index) => ({ tableMeta: { tableName: `table_${index}`, columns: [], primaryKeys: [] }, sourceColumns: ["id"] })) : undefined,
...(options.withTableMetadata
@@ -139,6 +141,58 @@ function gridRoot(host: HTMLElement): HTMLElement {
return root;
}
function mockToolbarGeometry(geometry: { width: number; filters: number; actions: number; compactActions?: number; leading?: number; conditionInputs?: { where: number; order: number } }) {
const observers: Array<{ callback: () => void; elements: Set<Element> }> = [];
vi.stubGlobal(
"ResizeObserver",
class {
elements = new Set<Element>();
constructor(callback: () => void) {
observers.push({ callback, elements: this.elements });
}
observe(element: Element) {
this.elements.add(element);
}
unobserve(element: Element) {
this.elements.delete(element);
}
disconnect() {
this.elements.clear();
}
},
);
const isLeading = (element: HTMLElement) => !!element.querySelector("[data-test-toolbar-leading]");
const conditionLabelWidth = (element: HTMLElement) => (element.closest("[data-grid-condition-editor]")?.getAttribute("data-grid-condition-editor") === "where" ? 42 : 67);
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockImplementation(function (this: HTMLElement) {
if (this.hasAttribute("data-grid-condition-input-viewport")) {
const kind = this.closest("[data-grid-condition-editor]")?.getAttribute("data-grid-condition-editor") === "where" ? "where" : "order";
const compact = !!this.closest("[data-grid-condition-editor]")?.querySelector(".data-grid-topbar-condition-label--compact");
return (geometry.conditionInputs?.[kind] ?? 250) + (compact ? conditionLabelWidth(this) : 0);
}
if (this.classList.contains("data-grid-topbar-condition-label")) return this.classList.contains("data-grid-topbar-condition-label--compact") ? 0 : conditionLabelWidth(this);
if (this.classList.contains("data-grid-topbar-shell") || this.dataset.gridTopbarRow === "filters") return geometry.width;
if (this.dataset.gridTopbarRow === "actions") return isLeading(this) ? (geometry.leading ?? 0) : Math.max(0, geometry.width - (geometry.leading ?? 0));
return 0;
});
vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockImplementation(function (this: HTMLElement) {
if (this.classList.contains("data-grid-topbar-condition-label")) return conditionLabelWidth(this);
if (this.classList.contains("data-grid-topbar-shell")) return geometry.width;
if (this.dataset.gridTopbarRow === "filters") return this.querySelector(".data-grid-topbar--compact") ? geometry.width : Math.max(geometry.width, geometry.filters);
if (this.dataset.gridTopbarRow === "actions") {
if (isLeading(this)) return geometry.leading ?? 0;
const actions = this.querySelector(".data-grid-topbar-action-label--compact") ? (geometry.compactActions ?? geometry.actions) : geometry.actions;
return Math.max(this.clientWidth, actions);
}
return 0;
});
return (selector = ".data-grid-topbar-shell") => {
for (const observer of observers) {
if ([...observer.elements].some((element) => element.matches(selector))) observer.callback();
}
};
}
function goToColumnItem(name: string, position: number): HTMLButtonElement {
const button = [...document.querySelectorAll<HTMLButtonElement>("button")].find((candidate) => candidate.textContent?.replaceAll(/\s/g, "") === `${name}#${position}`);
if (!button) throw new Error(`Go-to-column item ${name} not found`);
@@ -158,6 +212,7 @@ afterEach(() => {
app.unmount();
host.remove();
}
vi.unstubAllGlobals();
});
describe("DataGrid go-to-column shortcut", () => {
@@ -189,6 +244,101 @@ describe("DataGrid go-to-column shortcut", () => {
expect(host.querySelector('[data-grid-topbar-row="filters"].data-grid-topbar-scroll--row-divider')).not.toBeNull();
});
it("keeps split toolbar labels expanded below the single-row breakpoint and resets layout history", async () => {
mockToolbarGeometry({ width: 800, filters: 600, actions: 600 });
const { host, settingsStore } = mountGrid();
await settle();
const refresh = () => host.querySelector<HTMLElement>('[data-toolbar-action="refresh"] .data-grid-topbar-action-label');
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(true));
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(false));
expect(host.querySelector(".data-grid-topbar--compact")).toBeNull();
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "single" });
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(true));
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await vi.waitFor(() => expect(refresh()?.classList.contains("data-grid-topbar-action-label--compact")).toBe(false));
});
it("compacts split filter controls independently from action labels", async () => {
const geometry = { width: 800, filters: 900, actions: 500 };
const resize = mockToolbarGeometry(geometry);
const { host, settingsStore } = mountGrid();
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar--compact")).not.toBeNull());
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
geometry.width = 940;
resize();
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar--compact")).toBeNull());
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
});
it("measures both split action segments and restores their labels when the pane grows", async () => {
const geometry = { width: 700, filters: 500, actions: 650, compactActions: 450, leading: 100 };
const resize = mockToolbarGeometry(geometry);
const { host, settingsStore } = mountGrid({ slots: { "result-toolbar-leading": () => h("span", { "data-test-toolbar-leading": "" }, "Results") } });
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).not.toBeNull());
expect(host.querySelector(".data-grid-topbar--compact")).toBeNull();
geometry.width = 800;
resize();
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull());
expect(host.querySelector(".data-grid-topbar--compact")).toBeNull();
});
it("restores split action labels while the filter row still needs compact controls", async () => {
const geometry = { width: 700, filters: 900, actions: 750, compactActions: 450, leading: 100 };
const resize = mockToolbarGeometry(geometry);
const { host, settingsStore } = mountGrid({ slots: { "result-toolbar-leading": () => h("span", { "data-test-toolbar-leading": "" }, "Results") } });
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).not.toBeNull());
expect(host.querySelector(".data-grid-topbar--compact")).not.toBeNull();
geometry.width = 900;
resize();
await vi.waitFor(() => expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull());
expect(host.querySelector(".data-grid-topbar--compact")).not.toBeNull();
geometry.width = 940;
resize();
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar--compact")).toBeNull());
});
it("compacts WHERE and ORDER BY when inputs become too narrow without row overflow", async () => {
const geometry = { width: 500, filters: 500, actions: 300, conditionInputs: { where: 150, order: 120 } };
const resize = mockToolbarGeometry(geometry);
const { host, settingsStore } = mountGrid({ filterControls: true, withTableMetadata: true });
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await vi.waitFor(() => expect(host.querySelectorAll("[data-grid-condition-editor] > div > .data-grid-topbar-condition-label--compact")).toHaveLength(2));
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
geometry.width = 800;
geometry.conditionInputs = { where: 260, order: 230 };
resize();
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar-condition-label--compact")).toBeNull());
});
it("remeasures the WHERE/ORDER BY divider without resizing the pane and avoids premature label expansion", async () => {
const geometry = { width: 800, filters: 800, actions: 300, conditionInputs: { where: 260, order: 120 } };
const resize = mockToolbarGeometry(geometry);
const { host, settingsStore } = mountGrid({ filterControls: true, withTableMetadata: true });
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await vi.waitFor(() => expect(host.querySelectorAll("[data-grid-condition-editor] > div > .data-grid-topbar-condition-label--compact")).toHaveLength(2));
geometry.conditionInputs.order = 165;
resize("[data-grid-condition-input-viewport]");
await settle();
expect(host.querySelectorAll("[data-grid-condition-editor] > div > .data-grid-topbar-condition-label--compact")).toHaveLength(2);
geometry.conditionInputs.order = 190;
resize("[data-grid-condition-input-viewport]");
await vi.waitFor(() => expect(host.querySelector(".data-grid-topbar-condition-label--compact")).toBeNull());
expect(host.querySelector('[data-toolbar-action="refresh"] .data-grid-topbar-action-label--compact')).toBeNull();
});
it("does not duplicate the DDL action in the table-data toolbar", async () => {
const { host } = mountGrid({ withTableMetadata: true });
await settle();
@@ -8,6 +8,7 @@ import {
dataGridToolbarIntervalOptions,
isDataGridToolbarActionCompact,
isDataGridToolbarCompact,
shouldCompactDataGridConditionInput,
selectDataGridToolbarAddRowItem,
selectDataGridToolbarAutoRefreshInterval,
selectDataGridToolbarCopyItem,
@@ -67,6 +68,19 @@ describe("data grid toolbar capabilities", () => {
expect(isDataGridToolbarCompact(1000, 1100)).toBe(false);
});
it("reserves usable condition input width even when the flex row does not overflow", () => {
expect(shouldCompactDataGridConditionInput(159)).toBe(true);
expect(shouldCompactDataGridConditionInput(160)).toBe(false);
expect(shouldCompactDataGridConditionInput(250)).toBe(false);
});
it("accounts for the hidden prefix before restoring condition labels", () => {
expect(shouldCompactDataGridConditionInput(210, 60, true)).toBe(true);
expect(shouldCompactDataGridConditionInput(235, 60, true)).toBe(true);
expect(shouldCompactDataGridConditionInput(236, 60, true)).toBe(false);
expect(shouldCompactDataGridConditionInput(Number.NaN)).toBe(false);
});
it("progressively compacts toolbar actions as the available width shrinks", () => {
expect(dataGridToolbarActionCollapseCount(1050, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(0);
expect(dataGridToolbarActionCollapseCount(1049, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(1);
@@ -18,6 +18,16 @@ export function isDataGridToolbarCompact(toolbarWidth: number, viewportWidth: nu
return toolbarWidth > 0 && toolbarWidth < dataGridToolbarCompactBreakpoint(viewportWidth, minimumWidth);
}
// Reserve useful SQL input space before the flex row actually overflows.
const DATA_GRID_CONDITION_INPUT_MIN_WIDTH = 160;
const DATA_GRID_CONDITION_INPUT_EXPAND_SLACK = 16;
export function shouldCompactDataGridConditionInput(inputWidth: number, hiddenLabelWidth = 0, compact = false): boolean {
if (!Number.isFinite(inputWidth)) return false;
const expandedInputWidth = inputWidth - Math.max(0, hiddenLabelWidth);
return expandedInputWidth < DATA_GRID_CONDITION_INPUT_MIN_WIDTH + (compact ? DATA_GRID_CONDITION_INPUT_EXPAND_SLACK : 0);
}
export function dataGridToolbarActionCollapseCount(toolbarWidth: number, viewportWidth: number, minimumWidth?: number): number {
if (!Number.isFinite(toolbarWidth) || toolbarWidth <= 0) return 0;
const deficit = dataGridToolbarCompactBreakpoint(viewportWidth, minimumWidth) - toolbarWidth;
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB