fix(diagram): stop search box swallowing keystrokes
This commit is contained in:
1 parent
49926f25f1
commit
47ddbf6bb4
4 files changed
+160
-2
No files matched your search
@@ -127,7 +127,7 @@ function connectionIconType(id: string) {
|
|||||||
|
|
||||||
<div class="relative min-w-40 flex-1">
|
<div class="relative min-w-40 flex-1">
|
||||||
<Search class="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
<Search class="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input :value="tableSearch" @input="(e: Event) => emit('update:table-search', (e.target as HTMLInputElement).value)" class="h-8 pl-7 pr-9 text-xs" :placeholder="t('diagram.searchTables')" />
|
<Input :model-value="tableSearch" @update:model-value="(value) => emit('update:table-search', String(value))" class="h-8 pl-7 pr-9 text-xs" :placeholder="t('diagram.searchTables')" />
|
||||||
<button v-if="tableSearch" type="button" class="absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 rounded-full bg-muted-foreground/20 hover:bg-muted-foreground/40 flex items-center justify-center transition-colors" @click="emit('update:table-search', '')">
|
<button v-if="tableSearch" type="button" class="absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 rounded-full bg-muted-foreground/20 hover:bg-muted-foreground/40 flex items-center justify-center transition-colors" @click="emit('update:table-search', '')">
|
||||||
<X class="h-3 w-3 text-muted-foreground" />
|
<X class="h-3 w-3 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1794,7 +1794,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.key === " " || e.key === "Spacebar") {
|
if (!typing && (e.key === " " || e.key === "Spacebar")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
isSpacePressed.value = true;
|
isSpacePressed.value = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("vue-i18n", () => ({
|
||||||
|
useI18n: () => ({ t: (key: string) => key }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@lucide/vue", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
const Icon = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup(_props, { attrs }) {
|
||||||
|
return () => h("span", attrs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return Object.fromEntries(["Copy", "Download", "Link2", "Loader2", "Maximize2", "Minimize2", "Network", "Plus", "RefreshCw", "Search", "Table2", "Upload", "X", "ZoomIn", "ZoomOut", "LayoutGrid"].map((name) => [name, Icon]));
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/input", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
return {
|
||||||
|
// Keep this stub on Input's public model API so the test catches accidental
|
||||||
|
// reliance on fallthrough value/input attributes.
|
||||||
|
Input: defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
modelValue: { type: [String, Number], default: "" },
|
||||||
|
},
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
return () =>
|
||||||
|
h("input", {
|
||||||
|
value: props.modelValue,
|
||||||
|
onInput: (event: Event) => emit("update:modelValue", (event.target as HTMLInputElement).value),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function passthrough(tag: string) {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
return defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup(_props, { attrs, slots }) {
|
||||||
|
return () => h(tag, attrs, slots.default?.());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/badge", async () => ({ Badge: await passthrough("span") }));
|
||||||
|
vi.mock("@/components/ui/button", async () => ({ Button: await passthrough("button") }));
|
||||||
|
vi.mock("@/components/ui/dropdown-menu", async () => ({
|
||||||
|
DropdownMenu: await passthrough("div"),
|
||||||
|
DropdownMenuContent: await passthrough("div"),
|
||||||
|
DropdownMenuItem: await passthrough("button"),
|
||||||
|
DropdownMenuTrigger: await passthrough("div"),
|
||||||
|
}));
|
||||||
|
vi.mock("@/components/ui/select", async () => ({
|
||||||
|
Select: await passthrough("div"),
|
||||||
|
SelectContent: await passthrough("div"),
|
||||||
|
SelectItem: await passthrough("div"),
|
||||||
|
SelectTrigger: await passthrough("button"),
|
||||||
|
SelectValue: await passthrough("span"),
|
||||||
|
}));
|
||||||
|
vi.mock("@/components/icons/DatabaseIcon.vue", async () => ({ default: await passthrough("span") }));
|
||||||
|
vi.mock("@/components/connection/ConnectionGroupBadge.vue", async () => ({ default: await passthrough("span") }));
|
||||||
|
|
||||||
|
import DiagramToolbar from "../DiagramToolbar.vue";
|
||||||
|
|
||||||
|
const mountedApps: Array<{ app: App; host: HTMLElement }> = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const { app, host } of mountedApps.splice(0)) {
|
||||||
|
app.unmount();
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountToolbar() {
|
||||||
|
const search = ref("");
|
||||||
|
const updates: string[] = [];
|
||||||
|
const host = document.createElement("div");
|
||||||
|
document.body.append(host);
|
||||||
|
const app = createApp(
|
||||||
|
defineComponent({
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(DiagramToolbar, {
|
||||||
|
connectionId: "",
|
||||||
|
database: "",
|
||||||
|
schema: "",
|
||||||
|
databases: [],
|
||||||
|
schemas: [],
|
||||||
|
sqlConnections: [],
|
||||||
|
selectedConnection: undefined,
|
||||||
|
isSchemaAware: false,
|
||||||
|
loadingDatabases: false,
|
||||||
|
loadingSchemas: false,
|
||||||
|
loadingDiagram: false,
|
||||||
|
diagramReady: false,
|
||||||
|
tablesCount: 0,
|
||||||
|
relationshipsCount: 0,
|
||||||
|
customRelationshipCount: 0,
|
||||||
|
matchRelationshipCount: 0,
|
||||||
|
diagramMode: "table",
|
||||||
|
tableSearch: search.value,
|
||||||
|
showMatchPanel: false,
|
||||||
|
showLayersPanel: false,
|
||||||
|
showAllTables: false,
|
||||||
|
focusTableName: "",
|
||||||
|
generatedJoinSql: "",
|
||||||
|
"onUpdate:table-search": (value: string) => {
|
||||||
|
updates.push(value);
|
||||||
|
search.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.mount(host);
|
||||||
|
mountedApps.push({ app, host });
|
||||||
|
return { host, search, updates };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("DiagramToolbar", () => {
|
||||||
|
it("forwards one search update per typed character through Input's model API", async () => {
|
||||||
|
const { host, search, updates } = mountToolbar();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const input = host.querySelector("input") as HTMLInputElement;
|
||||||
|
for (const character of "user") {
|
||||||
|
input.value += character;
|
||||||
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(updates).toEqual(["u", "us", "use", "user"]);
|
||||||
|
expect(search.value).toBe("user");
|
||||||
|
expect(input.value).toBe("user");
|
||||||
|
});
|
||||||
|
});
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const dialogSource = readFileSync(new URL("../SchemaDiagramDialog.vue", import.meta.url), "utf8");
|
||||||
|
|
||||||
|
describe("SchemaDiagramDialog keyboard boundaries", () => {
|
||||||
|
it("does not consume a space typed in an editable control for canvas panning", () => {
|
||||||
|
const keydownSource = dialogSource.slice(dialogSource.indexOf("function handleKeydown"), dialogSource.indexOf("function handleKeyup"));
|
||||||
|
|
||||||
|
expect(keydownSource).toContain('const typing = target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable);');
|
||||||
|
expect(keydownSource).toContain('if (!typing && (e.key === " " || e.key === "Spacebar"))');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user