fix(editor): measure shortcut capture input width to fit CJK
This commit is contained in:
1 parent
b06e952ba0
commit
0437f952e0
4 files changed
+288
-1
No files matched your search
@@ -239,6 +239,7 @@ import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, normalizeCustomF
|
||||
import { buildFontFamilyOptions, displayFontFamily, isPresetFontFamily, loadSystemFontNames } from "@/lib/app/fontFamilyOptions";
|
||||
import { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, type AppSupportInfoLabels } from "@/lib/app/supportInfo";
|
||||
import { useUiFontFamilyPreview } from "@/composables/useUiFontFamilyPreview";
|
||||
import { useMeasuredWidth } from "@/composables/useMeasuredWidth";
|
||||
import { DateTimePatterns, normalizeSupportedDateTimePattern } from "@/lib/dataGrid/columnFormatter";
|
||||
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { MAX_QUERY_RESULT_MAX_ROWS } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
@@ -2187,7 +2188,21 @@ function formatShortcutPill(shortcut: string): string {
|
||||
}
|
||||
|
||||
const shortcutPressShortcutLabel = computed(() => t("settings.shortcutPressShortcut"));
|
||||
const shortcutPressShortcutInputWidth = computed(() => `${shortcutPressShortcutLabel.value.length + 2}em`);
|
||||
// #9144: `label.length + 2em` under-sizes CJK placeholders wherever the
|
||||
// environment's fallback font advances wider than 1em (user report: 15px/char
|
||||
// at a 13px font → the last glyph clipped mid-stroke). Measure a hidden mirror
|
||||
// span carrying the same box + typography classes as the capture inputs
|
||||
// instead; the char-count formula only survives as the pre-measurement
|
||||
// fallback. The +2px cushion absorbs sub-pixel rounding differences between
|
||||
// the mirror span and the real input.
|
||||
const shortcutPressShortcutInputWidth = ref(`${shortcutPressShortcutLabel.value.length + 2}em`);
|
||||
const shortcutPlaceholderMirrorRef = ref<HTMLElement | null>(null);
|
||||
const shortcutPlaceholderMirrorWidth = useMeasuredWidth(shortcutPlaceholderMirrorRef, 0, [shortcutPressShortcutLabel]);
|
||||
watch(shortcutPlaceholderMirrorWidth, (width) => {
|
||||
if (width > 0) {
|
||||
shortcutPressShortcutInputWidth.value = `${Math.ceil(width) + 2}px`;
|
||||
}
|
||||
});
|
||||
|
||||
function focusShortcutInput(actionId: ShortcutActionId) {
|
||||
editingShortcutId.value = actionId;
|
||||
@@ -9724,6 +9739,13 @@ LIMIT 100;</pre
|
||||
:confirm-label="t('common.delete')"
|
||||
@confirm="templateDeleteConfirm && confirmDeleteTemplate(templateDeleteConfirm)"
|
||||
/>
|
||||
|
||||
<!-- Hidden mirror of the shortcut-capture placeholder: measured to size
|
||||
the capture inputs (#9144). Carries the same box + typography classes
|
||||
as those inputs (font-mono text-[13px] font-semibold px-2.5 border) so
|
||||
its border-box width is exactly what they need, including font-fallback
|
||||
advances and letter-spacing that char-count arithmetic cannot predict. -->
|
||||
<span ref="shortcutPlaceholderMirrorRef" aria-hidden="true" class="invisible pointer-events-none absolute top-0 left-0 h-0 max-w-64 overflow-hidden whitespace-pre rounded-[6px] border px-2.5 font-mono text-[13px] font-semibold">{{ shortcutPressShortcutLabel }}</span>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
|
||||
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
// @vitest-environment happy-dom
|
||||
// Source-assertion regression for #9144: the shortcut-capture input width must
|
||||
// come from measuring a hidden mirror span (same box + typography as the real
|
||||
// inputs), not from `label.length + 2em` char-count arithmetic — CJK fallback
|
||||
// fonts can advance wider than 1em and clip the last placeholder glyph. The
|
||||
// dialog cannot be mounted (a 10k-line SFC whose imports pull the whole
|
||||
// editor + stores), so this follows the same `?raw` pattern as
|
||||
// EditorSettingsDialog.mcpQueryTimeout.integration.spec.ts.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import dialogSource from "../EditorSettingsDialog.vue?raw";
|
||||
|
||||
function templateOnly(): string {
|
||||
// The root <template> block, not the first "</template>" — this SFC is full
|
||||
// of nested slot <template> elements, so the first closing tag (and the
|
||||
// naive slice up to it) lands inside the template and misses most of it.
|
||||
return dialogSource.slice(dialogSource.search(/^<template>$/m), dialogSource.lastIndexOf("</template>"));
|
||||
}
|
||||
|
||||
describe("EditorSettingsDialog shortcut capture input width (#9144)", () => {
|
||||
it("mounts exactly one hidden mirror span carrying the shared typography/box classes", () => {
|
||||
const template = templateOnly();
|
||||
|
||||
// The ref token appears exactly once in the template: the mirror span.
|
||||
// (The two script occurrences are the ref declaration and the
|
||||
// useMeasuredWidth call.)
|
||||
expect(template.match(/shortcutPlaceholderMirrorRef/g)).toHaveLength(1);
|
||||
expect(dialogSource.match(/shortcutPlaceholderMirrorRef/g)).toHaveLength(3);
|
||||
|
||||
// Same box + typography classes as the capture inputs so the measured
|
||||
// border-box width includes padding, border, letter-spacing and
|
||||
// fallback-font advances. Asserted against the span's own tag (not the
|
||||
// whole template) — the capture inputs share these classes, so a
|
||||
// template-wide match would pass even with the span stripped.
|
||||
const spanTag = template.match(/<span ref="shortcutPlaceholderMirrorRef"[^>]*>/)?.[0];
|
||||
expect(spanTag).toBeTruthy();
|
||||
for (const cls of ["font-mono", "text-[13px]", "font-semibold", "px-2.5", "border", "whitespace-pre", "invisible"]) {
|
||||
expect(spanTag).toContain(cls);
|
||||
}
|
||||
|
||||
// The label must be interpolated in-band with the span tags, not as
|
||||
// template text between newlines — whitespace-pre would otherwise widen
|
||||
// the measured box.
|
||||
expect(template).toMatch(/<span ref="shortcutPlaceholderMirrorRef"[^>]*>\{\{ shortcutPressShortcutLabel \}\}<\/span>/);
|
||||
});
|
||||
|
||||
it("binds all three capture inputs to the measured width ref", () => {
|
||||
// Two width bindings gate on the row-level editing state (formatter table
|
||||
// + shortcuts table), one on the SQL-shortcut dialog editing state. Each
|
||||
// conditional token appears in exactly one kind of binding.
|
||||
expect(dialogSource.match(/editingShortcutId === definition\.id \? shortcutPressShortcutInputWidth/g)).toHaveLength(2);
|
||||
expect(dialogSource.match(/editingSqlShortcutInputId === 'dialog' \? shortcutPressShortcutInputWidth/g)).toHaveLength(1);
|
||||
|
||||
// Account for every usage of the width ref: 1 fallback initializer,
|
||||
// 1 watcher application, 3 capture-input bindings.
|
||||
expect(dialogSource.match(/shortcutPressShortcutInputWidth/g)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("wires the measured width to the shared mirror ref with the label as revalidate source", () => {
|
||||
expect(dialogSource).toContain("useMeasuredWidth(shortcutPlaceholderMirrorRef, 0, [shortcutPressShortcutLabel])");
|
||||
|
||||
// The applied width adds a ~2px cushion and rounds up so sub-pixel
|
||||
// rounding never under-sizes.
|
||||
expect(dialogSource).toContain("${Math.ceil(width) + 2}px");
|
||||
});
|
||||
|
||||
it("keeps the char-count formula only as the ref fallback initializer", () => {
|
||||
// The legacy `length + 2em` estimate survives solely as the pre-measurement
|
||||
// initializer of the ref.
|
||||
expect(dialogSource).toContain("shortcutPressShortcutInputWidth = ref(`${shortcutPressShortcutLabel.value.length + 2}em`)");
|
||||
|
||||
// No computed re-derives the width from char count anymore.
|
||||
expect(dialogSource).not.toMatch(/computed\(\(\) => `\$\{shortcutPressShortcutLabel\.value\.length \+ 2\}em`\)/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user