From 017122d61dcfe6bbbc944a140ec538f0bd1040e2 Mon Sep 17 00:00:00 2001 From: greymoth-jp Date: Sun, 28 Jun 2026 22:20:58 +0900 Subject: [PATCH] fix(ui): keep truncateWithEndVisible code-point-safe in short-width fallback --- .changeset/truncate-surrogate-safe.md | 5 +++++ .../utils/__tests__/truncateTextWithEndVisible.test.ts | 8 ++++++++ packages/ui/src/utils/truncateTextWithEndVisible.ts | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/truncate-surrogate-safe.md diff --git a/.changeset/truncate-surrogate-safe.md b/.changeset/truncate-surrogate-safe.md new file mode 100644 index 00000000000..6b07979059f --- /dev/null +++ b/.changeset/truncate-surrogate-safe.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Stop `truncateWithEndVisible` from splitting characters outside the BMP (such as CJK Extension B kanji and emoji) into a broken replacement character when truncating to a very small width. The short-width fallback now slices by code point, matching the main truncation path. diff --git a/packages/ui/src/utils/__tests__/truncateTextWithEndVisible.test.ts b/packages/ui/src/utils/__tests__/truncateTextWithEndVisible.test.ts index 151cdc7a429..7d6f3212a84 100644 --- a/packages/ui/src/utils/__tests__/truncateTextWithEndVisible.test.ts +++ b/packages/ui/src/utils/__tests__/truncateTextWithEndVisible.test.ts @@ -28,6 +28,14 @@ describe('truncateWithEndVisible', () => { expect(truncateWithEndVisible('1234567890', 5, 3)).toBe('...890'); }); + test('should not split surrogate pairs when maxLength is too small', () => { + // The short-maxLength fallback must stay code-point-safe like the main path. + // Slicing on UTF-16 code units cuts characters outside the BMP (CJK Extension B, + // emoji) mid-surrogate, leaving a replacement character. + expect(truncateWithEndVisible('𠮷𠮷𠮷𠮷𠮷', 8, 5)).toBe('...𠮷𠮷𠮷𠮷𠮷'); + expect(truncateWithEndVisible('🍣🍣🍣🍣🍣', 8, 5)).toBe('...🍣🍣🍣🍣🍣'); + }); + test('should handle email addresses', () => { expect(truncateWithEndVisible('test@example.com', 10)).toBe('te...e.com'); }); diff --git a/packages/ui/src/utils/truncateTextWithEndVisible.ts b/packages/ui/src/utils/truncateTextWithEndVisible.ts index a09cb7dc0d7..a743b8bef82 100644 --- a/packages/ui/src/utils/truncateTextWithEndVisible.ts +++ b/packages/ui/src/utils/truncateTextWithEndVisible.ts @@ -20,7 +20,7 @@ export function truncateWithEndVisible(str: string, maxLength = 20, endChars = 5 } if (maxLength <= endChars + ELLIPSIS_LENGTH) { - return ELLIPSIS + str.slice(-endChars); + return ELLIPSIS + Array.from(str).slice(-endChars).join(''); } const chars = Array.from(str);