fix: account for wide CJK characters in setWidth() ch calculation

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
This commit is contained in:
terminalchai 2026-03-07 01:37:31 +05:30
commit 2d5bfd31f2

View file

@ -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 BD
(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 {