From 2d5bfd31f2166ed386f3dd6ef5809ca37b0a9dc5 Mon Sep 17 00:00:00 2001 From: terminalchai <213856599+terminalchai@users.noreply.github.com> Date: Sat, 7 Mar 2026 01:37:31 +0530 Subject: [PATCH] fix: account for wide CJK characters in setWidth() ch calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The setWidth() method used str.length to compute the ch-unit width for the search input's min-width / width style properties. This is incorrect for wide (full-width) Unicode characters such as Japanese (Hiragana / Katakana), Chinese (CJK Ideographs), Korean (Hangul), and fullwidth Latin forms -- each of these occupies 2ch in a standard browser font yet was counted as only 1. The result is that a Japanese placeholder like 'エリアを選択してください' (12 characters -> 24ch wide) was only given 13ch of min-width, causing the text to be clipped. Fix: replace both .length calls in setWidth() with a new module-level helper getStringWidth() that iterates the string's Unicode code points and adds 2 for any character in a wide/fullwidth Unicode block (standard wcwidth-style logic), and 1 for everything else. Fixes #1216 --- src/scripts/components/input.ts | 49 +++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/scripts/components/input.ts b/src/scripts/components/input.ts index a467c9fe..87f15412 100644 --- a/src/scripts/components/input.ts +++ b/src/scripts/components/input.ts @@ -1,6 +1,46 @@ import { ClassNames } from '../interfaces/class-names'; import { PassedElementType, PassedElementTypes } from '../interfaces/passed-element-type'; +/** + * Returns the display width of a string in `ch` units. + * Wide characters (CJK, Hangul, fullwidth forms, etc.) count as 2, + * all other characters count as 1. This matches how browsers render + * these characters relative to the `ch` unit (width of '0'). + */ +function getStringWidth(str: string): number { + let width = 0; + for (const char of str) { + const code = char.codePointAt(0) ?? 0; + /* eslint-disable no-mixed-operators */ + if ( + (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo + code === 0x2329 || + code === 0x232a || + (code >= 0x2e80 && code <= 0x303e) || // CJK Radicals, Kangxi, CJK Symbols + (code >= 0x3041 && code <= 0x33bf) || // Hiragana, Katakana, Bopomofo, CJK Compat + (code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A + (code >= 0x4e00 && code <= 0x9fff) || // CJK Unified Ideographs + (code >= 0xa000 && code <= 0xa4cf) || // Yi Syllables / Radicals + (code >= 0xa960 && code <= 0xa97f) || // Hangul Jamo Extended-A + (code >= 0xac00 && code <= 0xd7af) || // Hangul Syllables + (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs + (code >= 0xfe10 && code <= 0xfe1f) || // Vertical Forms + (code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms + (code >= 0xff01 && code <= 0xff60) || // Fullwidth Latin / Punctuation + (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Signs + (code >= 0x1b000 && code <= 0x1b001) || // Kana Supplement + (code >= 0x20000 && code <= 0x2fffd) || // CJK Extension B–D + (code >= 0x30000 && code <= 0x3fffd) // CJK Extension E+ + ) { + width += 2; + } else { + width += 1; + } + /* eslint-enable no-mixed-operators */ + } + return width; +} + export default class Input { element: HTMLInputElement; @@ -110,10 +150,13 @@ export default class Input { * value or input value */ setWidth(): void { - // Resize input to contents or placeholder + // Resize input to contents or placeholder. + // Uses getStringWidth() instead of .length so that wide characters + // (CJK, Hangul, fullwidth forms, etc.) are counted as 2ch rather than 1ch, + // preventing placeholder text truncation for languages like Japanese. const { element } = this; - element.style.minWidth = `${element.placeholder.length + 1}ch`; - element.style.width = `${element.value.length + 1}ch`; + element.style.minWidth = `${getStringWidth(element.placeholder) + 1}ch`; + element.style.width = `${getStringWidth(element.value) + 1}ch`; } setActiveDescendant(activeDescendantID: string): void {