Skip to content

Commit bb30922

Browse files
tdknmtsgrd
authored andcommitted
refactor: extract ime handling logic into reusable utility class
Consolidate IME (Input Method Editor) composition handling by creating a dedicated IMECompositionHandler utility class. This removes duplicate IME handling code from commit and review components and provides a unified approach for managing text composition in CJK languages. The utility handles blocking of keyboard shortcuts during text composition and tracks composition state to prevent unintended actions while users are typing with input method editors.
1 parent bdd7c08 commit bb30922

File tree

3 files changed

+96
-35
lines changed

3 files changed

+96
-35
lines changed

apps/desktop/src/components/CommitMessageEditor.svelte

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { WORKTREE_SERVICE } from '$lib/worktree/worktreeService.svelte';
1818
import { inject } from '@gitbutler/core/context';
1919
import { Button, TestId } from '@gitbutler/ui';
20+
import { IMECompositionHandler } from '@gitbutler/ui/utils/imeHandling';
2021
2122
import { tick } from 'svelte';
2223
@@ -68,7 +69,7 @@
6869
6970
let composer = $state<ReturnType<typeof MessageEditor>>();
7071
let titleInput = $state<HTMLTextAreaElement>();
71-
let isComposing = $state(false);
72+
const imeHandler = new IMECompositionHandler();
7273
7374
const suggestionsHandler = new CommitSuggestions(aiService, uiState);
7475
const diffInputArgs = $derived<DiffInputContextArgs>(
@@ -166,27 +167,15 @@
166167
onchange={(value) => {
167168
onChange?.({ title: value });
168169
}}
169-
oninput={(e: Event) => {
170-
if (e instanceof InputEvent) {
171-
isComposing = e.isComposing;
172-
}
173-
}}
174-
onkeydown={async (e: KeyboardEvent) => {
175-
if (
176-
['Enter', 'Escape', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key) &&
177-
isComposing
178-
) {
179-
e.preventDefault();
180-
isComposing = false;
181-
return;
182-
}
170+
oninput={imeHandler.handleInput()}
171+
onkeydown={imeHandler.handleKeydown(async (e: KeyboardEvent) => {
183172
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
184173
e.preventDefault();
185174
if (title.trim()) {
186175
emitAction();
187176
}
188177
}
189-
if ((e.key === 'Enter' || (e.key === 'Tab' && !e.shiftKey)) && !isComposing) {
178+
if (e.key === 'Enter' || (e.key === 'Tab' && !e.shiftKey)) {
190179
e.preventDefault();
191180
composer?.focus();
192181
}
@@ -195,7 +184,7 @@
195184
handleCancel();
196185
}
197186
e.stopPropagation();
198-
}}
187+
})}
199188
/>
200189
<MessageEditor
201190
testId={TestId.CommitDrawerDescriptionInput}

apps/desktop/src/components/ReviewCreation.svelte

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import { inject } from '@gitbutler/core/context';
3737
import { persisted } from '@gitbutler/shared/persisted';
3838
import { chipToasts, TestId } from '@gitbutler/ui';
39+
import { IMECompositionHandler } from '@gitbutler/ui/utils/imeHandling';
3940
import { isDefined } from '@gitbutler/ui/utils/typeguards';
4041
import { tick } from 'svelte';
4142
@@ -91,7 +92,7 @@
9192
9293
let titleInput = $state<HTMLTextAreaElement | undefined>(undefined);
9394
let messageEditor = $state<MessageEditor>();
94-
let isComposing = $state(false);
95+
const imeHandler = new IMECompositionHandler();
9596
9697
// AI things
9798
const aiGenEnabled = projectAiGenEnabled(projectId);
@@ -387,16 +388,8 @@
387388
onchange={(value) => {
388389
prTitle.set(value);
389390
}}
390-
onkeydown={(e: KeyboardEvent) => {
391-
if (
392-
['Enter', 'Escape', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key) &&
393-
isComposing
394-
) {
395-
e.preventDefault();
396-
isComposing = false;
397-
return;
398-
}
399-
if ((e.key === 'Enter' || (e.key === 'Tab' && !e.shiftKey)) && !isComposing) {
391+
onkeydown={imeHandler.handleKeydown((e: KeyboardEvent) => {
392+
if (e.key === 'Enter' || (e.key === 'Tab' && !e.shiftKey)) {
400393
e.preventDefault();
401394
messageEditor?.focus();
402395
}
@@ -407,20 +400,17 @@
407400
return true;
408401
}
409402

410-
if (e.key === 'Escape' && !isComposing) {
403+
if (e.key === 'Escape') {
411404
e.preventDefault();
412405
onClose();
413406
}
414-
}}
407+
})}
415408
placeholder="PR title"
416409
showCount={false}
417-
oninput={(e: Event) => {
410+
oninput={imeHandler.handleInput((e: Event) => {
418411
const target = e.target as HTMLInputElement;
419412
prTitle.set(target.value);
420-
if (e instanceof InputEvent) {
421-
isComposing = e.isComposing;
422-
}
423-
}}
413+
})}
424414
/>
425415
<MessageEditor
426416
forceSansFont
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* IME (Input Method Editor) handling utilities for text input components.
3+
* This class provides a unified handler to manage IME composition state
4+
* and prevent unintended keyboard shortcuts during text composition in
5+
* Japanese, Chinese, Korean, and other languages that require input method editors.
6+
*/
7+
export class IMECompositionHandler {
8+
private _isComposing = false;
9+
10+
get isComposing(): boolean {
11+
return this._isComposing;
12+
}
13+
14+
setComposing(composing: boolean): void {
15+
this._isComposing = composing;
16+
}
17+
18+
reset(): void {
19+
this._isComposing = false;
20+
}
21+
22+
/**
23+
* Creates an input event handler that tracks IME composition state
24+
*
25+
* @param originalHandler - Optional original input handler to call
26+
* @returns Input event handler function
27+
*/
28+
handleInput(originalHandler?: (e: Event) => void) {
29+
return (event: Event) => {
30+
if (event instanceof InputEvent) {
31+
this.setComposing(event.isComposing);
32+
}
33+
34+
originalHandler?.(event);
35+
};
36+
}
37+
38+
/**
39+
* Creates a keydown event handler that blocks actions during IME composition
40+
*
41+
* @param originalHandler - Optional original keydown handler to call
42+
* @param additionalBlockingKeys - Additional keys to block during composition
43+
* @returns Keydown event handler function
44+
*/
45+
handleKeydown(
46+
originalHandler?: (event: KeyboardEvent) => void,
47+
additionalBlockingKeys: string[] = []
48+
) {
49+
return (event: KeyboardEvent) => {
50+
if (
51+
[...IME_BLOCKING_KEYS, ...additionalBlockingKeys].includes(event.key) &&
52+
this.isComposing
53+
) {
54+
event.preventDefault();
55+
event.stopPropagation();
56+
this.reset();
57+
return;
58+
}
59+
60+
originalHandler?.(event);
61+
};
62+
}
63+
}
64+
65+
/**
66+
* Keys that should be blocked during IME composition to prevent unintended actions
67+
*/
68+
const IME_BLOCKING_KEYS = [
69+
'Enter',
70+
'Escape',
71+
'Tab',
72+
'0',
73+
'1',
74+
'2',
75+
'3',
76+
'4',
77+
'5',
78+
'6',
79+
'7',
80+
'8',
81+
'9'
82+
] as const;

0 commit comments

Comments
 (0)