Skip to content

Commit 51317b2

Browse files
dmytrokirpaCopilot
andauthored
fix(react-avatar): support initials calculation for GB18030-2022 extension characters (#35878)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent e084883 commit 51317b2

3 files changed

Lines changed: 57 additions & 12 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix: support initials calculation for GB18030-2022 extension characters",
4+
"packageName": "@fluentui/react-avatar",
5+
"email": "dmytrokirpa@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-avatar/library/src/utils/getInitials.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ describe('getInitials', () => {
106106
expect(result).toEqual('');
107107
});
108108

109+
it('calculates initials for GB18030-2022 extension characters (CJK Ext B-I)', () => {
110+
// These characters are encoded as surrogate pairs; the character itself should be returned as the initial
111+
expect(getInitials('𬸚', false)).toEqual('𬸚'); // GFZB-196
112+
expect(getInitials('𢃾', false)).toEqual('𢃾'); // CJK Ext B
113+
expect(getInitials('𪜀', false)).toEqual('𪜀'); // CJK Ext C
114+
expect(getInitials('𫜴', false)).toEqual('𫜴'); // CJK Ext C
115+
expect(getInitials('𫟰', false)).toEqual('𫟰'); // CJK Ext D
116+
expect(getInitials('𬺠', false)).toEqual('𬺠'); // CJK Ext E
117+
expect(getInitials('𮓇', false)).toEqual('𮓇'); // CJK Ext F
118+
expect(getInitials('𪛝', false)).toEqual('𪛝'); // BX
119+
expect(getInitials('𰉖', false)).toEqual('𰉖'); // GX
120+
expect(getInitials('𱘍', false)).toEqual('𱘍'); // HX
121+
expect(getInitials('𮯰', false)).toEqual('𮯰'); // IX
122+
});
123+
124+
it('calculates initials for mixed strings starting with GB18030-2022 extension characters', () => {
125+
// First code point of a mixed string should be used as the initial
126+
expect(getInitials('𫚭齅䶱5𮯠灋𬘭r𫟼蝌龯𪛒𪛛㊣𫜹⾢Z𱔟𫍲𮴋䶺𰆬a', false)).toEqual('𫚭');
127+
});
128+
109129
it('calculates an expected initials for Japanese names', () => {
110130
let result = getInitials('松田', false);
111131
expect(result).toEqual('');

packages/react-components/react-avatar/library/src/utils/getInitials.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ const UNWANTED_ENCLOSURES_REGEX: RegExp = /[\(\[\{][^\)\]\}]*[\)\]\}]/g;
1010

1111
/**
1212
* Regular expression matching special ASCII characters except space, plus some unicode special characters.
13-
* Applies after unwanted enclosures have been removed
13+
* Applies after unwanted enclosures have been removed.
14+
* Note: the range starts at \uE000 (not \uD800) to avoid matching surrogate code units, which would break
15+
* supplementary Unicode characters (encoded as surrogate pairs in UTF-16) such as GB18030-2022 extension characters.
1416
*/
15-
const UNWANTED_CHARS_REGEX: RegExp = /[\0-\u001F\!-/:-@\[-`\{-\u00BF\u0250-\u036F\uD800-\uFFFF]/g;
17+
const UNWANTED_CHARS_REGEX: RegExp = /[\0-\u001F\!-/:-@\[-`\{-\u00BF\u0250-\u036F\uE000-\uFFFF]/g;
1618

1719
/**
1820
* Regular expression matching phone numbers. Applied after chars matching UNWANTED_CHARS_REGEX have been removed
@@ -28,30 +30,43 @@ const MULTIPLE_WHITESPACES_REGEX: RegExp = /\s+/g;
2830
* Arabic: Arabic, Arabic Supplement, Arabic Extended-A.
2931
* Korean: Hangul Jamo, Hangul Compatibility Jamo, Hangul Jamo Extended-A, Hangul Syllables, Hangul Jamo Extended-B.
3032
* Japanese: Hiragana, Katakana.
31-
* CJK: CJK Unified Ideographs Extension A, CJK Unified Ideographs, CJK Compatibility Ideographs,
32-
* CJK Unified Ideographs Extension B
33+
* CJK: CJK Unified Ideographs Extension A, CJK Unified Ideographs, CJK Compatibility Ideographs.
34+
* Note: Supplementary CJK characters (GB18030-2022 extension characters in Ext B-I) are intentionally not listed
35+
* here so they can be rendered as initials.
3336
*/
3437
const UNSUPPORTED_TEXT_REGEX: RegExp =
35-
/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]|[\uD840-\uD869][\uDC00-\uDED6]/;
38+
/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/;
39+
40+
function getFirstCodePoint(value: string): string {
41+
if (!value) {
42+
return '';
43+
}
44+
45+
const codePoint = value.codePointAt(0);
46+
return codePoint === undefined ? '' : String.fromCodePoint(codePoint);
47+
}
3648

3749
function getInitialsLatin(displayName: string, isRtl: boolean, firstInitialOnly?: boolean): string {
3850
let initials = '';
3951

4052
const splits: string[] = displayName.split(' ');
4153
if (splits.length !== 0) {
42-
initials += splits[0].charAt(0).toUpperCase();
54+
// Use code point-aware helper to correctly handle supplementary characters (e.g. GB18030-2022 extension chars)
55+
// that are encoded as surrogate pairs; charAt(0) would only return half of such a character.
56+
initials += getFirstCodePoint(splits[0]).toUpperCase();
4357
}
4458

4559
if (!firstInitialOnly) {
4660
if (splits.length === 2) {
47-
initials += splits[1].charAt(0).toUpperCase();
61+
initials += getFirstCodePoint(splits[1]).toUpperCase();
4862
} else if (splits.length === 3) {
49-
initials += splits[2].charAt(0).toUpperCase();
63+
initials += getFirstCodePoint(splits[2]).toUpperCase();
5064
}
5165
}
5266

53-
if (isRtl && initials.length > 1) {
54-
return initials.charAt(1) + initials.charAt(0);
67+
if (isRtl && [...initials].length > 1) {
68+
const chars = [...initials];
69+
return chars[1] + chars[0];
5570
}
5671

5772
return initials;
@@ -95,9 +110,12 @@ export function getInitials(
95110

96111
displayName = cleanupDisplayName(displayName);
97112

98-
// For names containing CJK characters, and phone numbers, we don't display initials
113+
// Check only the first code point against UNSUPPORTED_TEXT_REGEX so that names starting with a supported
114+
// character (e.g. GB18030-2022 extension characters) produce an initial even when the rest of the string
115+
// contains BMP CJK characters that would otherwise trigger the regex.
116+
const firstCodePoint = getFirstCodePoint(displayName);
99117
if (
100-
UNSUPPORTED_TEXT_REGEX.test(displayName) ||
118+
UNSUPPORTED_TEXT_REGEX.test(firstCodePoint) ||
101119
(!options?.allowPhoneInitials && PHONENUMBER_REGEX.test(displayName))
102120
) {
103121
return '';

0 commit comments

Comments
 (0)