fix(grid): preserve IME composition when condition expands
This commit is contained in:
parent
9cf2ec4194
commit
8e8cb0f58c
|
|
@ -44,12 +44,14 @@ const overlayRef = ref<HTMLTextAreaElement>();
|
|||
const controlRef = ref<HTMLDivElement>();
|
||||
const dropdownRef = ref<HTMLDivElement>();
|
||||
const expanded = ref(false);
|
||||
const composing = ref(false);
|
||||
const expandedRect = ref({ left: 0, top: 0, width: 0, controlsTop: 0, inputTop: 0, prefix: 0, suffix: 28 });
|
||||
const expandedHeight = ref(56);
|
||||
const suggestionPosition = ref({ left: 0, top: 0, width: 180 });
|
||||
const historyPreview = ref<{ value: string; left: number; top: number; maxWidth: number; arrowTop: number; side: "left" | "right" } | null>(null);
|
||||
let collapseTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
let expandAfterComposition = false;
|
||||
|
||||
const editor = useDataGridConditionEditor({
|
||||
kind: props.kind,
|
||||
|
|
@ -146,6 +148,12 @@ function resizeEditor(forceExpand = false) {
|
|||
void nextTick(() => {
|
||||
const input = inputRef.value;
|
||||
if (!input) return;
|
||||
// Expanding swaps focus to a teleported overlay textarea; doing that mid-IME
|
||||
// composition truncates unfinished pinyin. Defer until composition ends.
|
||||
if (composing.value && !expanded.value) {
|
||||
expandAfterComposition = true;
|
||||
return;
|
||||
}
|
||||
const focused = document.activeElement === input || document.activeElement === overlayRef.value;
|
||||
const nextExpanded = focused && shouldExpand(input) && (forceExpand || expanded.value);
|
||||
if (nextExpanded) {
|
||||
|
|
@ -154,10 +162,10 @@ function resizeEditor(forceExpand = false) {
|
|||
}
|
||||
expanded.value = nextExpanded;
|
||||
updateSuggestionPosition();
|
||||
if (nextExpanded && document.activeElement === input) {
|
||||
if (nextExpanded && document.activeElement === input && !composing.value) {
|
||||
void nextTick(() => {
|
||||
const overlay = overlayRef.value;
|
||||
if (!overlay) return;
|
||||
if (!overlay || composing.value) return;
|
||||
const start = input.selectionStart;
|
||||
overlay.focus();
|
||||
overlay.setSelectionRange(start, start);
|
||||
|
|
@ -166,6 +174,17 @@ function resizeEditor(forceExpand = false) {
|
|||
});
|
||||
}
|
||||
|
||||
function onCompositionStart() {
|
||||
composing.value = true;
|
||||
}
|
||||
|
||||
function onCompositionEnd() {
|
||||
composing.value = false;
|
||||
if (!expandAfterComposition) return;
|
||||
expandAfterComposition = false;
|
||||
resizeEditor(true);
|
||||
}
|
||||
|
||||
function focus(select = false) {
|
||||
const target = activeEditor.value ?? inputRef.value;
|
||||
target?.focus();
|
||||
|
|
@ -284,6 +303,7 @@ onMounted(() => {
|
|||
|
||||
onUnmounted(() => {
|
||||
if (collapseTimer) clearTimeout(collapseTimer);
|
||||
expandAfterComposition = false;
|
||||
resizeObserver?.disconnect();
|
||||
window.removeEventListener("resize", onViewportResize);
|
||||
window.visualViewport?.removeEventListener("resize", onViewportResize);
|
||||
|
|
@ -323,6 +343,8 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
|
|||
@focus="resizeEditor(true)"
|
||||
@blur="scheduleCollapse"
|
||||
@click="resizeEditor(true)"
|
||||
@compositionstart="onCompositionStart"
|
||||
@compositionend="onCompositionEnd"
|
||||
@input="onInput"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
|
|
@ -351,6 +373,8 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
|
|||
class="data-grid-topbar-condition-input data-grid-topbar-condition-input--expanded absolute resize-none outline-none"
|
||||
:class="[props.kind === 'where' ? 'data-grid-topbar-condition-input--where' : 'data-grid-topbar-condition-input--order', { 'data-grid-topbar-condition-input--compact': props.compact }]"
|
||||
@blur="scheduleCollapse"
|
||||
@compositionstart="onCompositionStart"
|
||||
@compositionend="onCompositionEnd"
|
||||
@input="onInput"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ vi.stubGlobal("localStorage", {
|
|||
clear: () => storage.clear(),
|
||||
});
|
||||
|
||||
function keyboardEvent(key: string) {
|
||||
return { key, shiftKey: false, preventDefault: vi.fn() } as unknown as KeyboardEvent;
|
||||
function keyboardEvent(key: string, extras: Partial<KeyboardEvent> = {}) {
|
||||
return { key, shiftKey: false, preventDefault: vi.fn(), ...extras } as unknown as KeyboardEvent;
|
||||
}
|
||||
|
||||
describe("useDataGridConditionEditor", () => {
|
||||
|
|
@ -192,4 +192,21 @@ describe("useDataGridConditionEditor", () => {
|
|||
expect(editor.handleKeydown(escape)).toBe("dismiss");
|
||||
expect(editor.dropdownOpen.value).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores shortcut keys while an IME composition is active", async () => {
|
||||
const value = ref("");
|
||||
const editor = useDataGridConditionEditor({ kind: "where", value, columns: ["name"], historyScope: {} });
|
||||
value.value = "na";
|
||||
await nextTick();
|
||||
await vi.waitFor(() => expect(editor.suggestions.value).toHaveLength(1));
|
||||
|
||||
const composingEnter = keyboardEvent("Enter", { isComposing: true });
|
||||
expect(editor.handleKeydown(composingEnter)).toBeUndefined();
|
||||
expect(composingEnter.preventDefault).not.toHaveBeenCalled();
|
||||
expect(value.value).toBe("na");
|
||||
|
||||
const processEnter = keyboardEvent("Process");
|
||||
expect(editor.handleKeydown(processEnter)).toBeUndefined();
|
||||
expect(processEnter.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ export function useDataGridConditionEditor(options: UseDataGridConditionEditorOp
|
|||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent): "accept" | "apply" | "dismiss" | "navigate" | undefined {
|
||||
if (event.isComposing || event.key === "Process" || event.keyCode === 229) return undefined;
|
||||
if (dropdownOpen.value && event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
dismiss();
|
||||
|
|
|
|||
Loading…
Reference in New Issue