@transferwise/components 46.119.5 β†’ 46.120.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/build/alert/Alert.js +1 -1
  2. package/build/alert/Alert.js.map +1 -1
  3. package/build/alert/Alert.mjs +1 -1
  4. package/build/alert/Alert.mjs.map +1 -1
  5. package/build/checkbox/Checkbox.js +1 -1
  6. package/build/checkbox/Checkbox.js.map +1 -1
  7. package/build/checkbox/Checkbox.mjs +1 -1
  8. package/build/checkbox/Checkbox.mjs.map +1 -1
  9. package/build/common/initials.js +17 -7
  10. package/build/common/initials.js.map +1 -1
  11. package/build/common/initials.mjs +17 -7
  12. package/build/common/initials.mjs.map +1 -1
  13. package/build/field/Field.js +8 -4
  14. package/build/field/Field.js.map +1 -1
  15. package/build/field/Field.mjs +8 -4
  16. package/build/field/Field.mjs.map +1 -1
  17. package/build/inlineAlert/InlineAlert.js +1 -7
  18. package/build/inlineAlert/InlineAlert.js.map +1 -1
  19. package/build/inlineAlert/InlineAlert.mjs +1 -7
  20. package/build/inlineAlert/InlineAlert.mjs.map +1 -1
  21. package/build/main.css +20 -1
  22. package/build/prompt/InlinePrompt/InlinePrompt.js +2 -0
  23. package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
  24. package/build/prompt/InlinePrompt/InlinePrompt.mjs +2 -0
  25. package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
  26. package/build/radioGroup/RadioGroup.js +1 -0
  27. package/build/radioGroup/RadioGroup.js.map +1 -1
  28. package/build/radioGroup/RadioGroup.mjs +1 -0
  29. package/build/radioGroup/RadioGroup.mjs.map +1 -1
  30. package/build/styles/field/Field.css +10 -1
  31. package/build/styles/main.css +20 -1
  32. package/build/styles/prompt/InlinePrompt/InlinePrompt.css +3 -0
  33. package/build/styles/radioGroup/RadioGroup.css +3 -0
  34. package/build/styles/typeahead/Typeahead.css +4 -0
  35. package/build/typeahead/Typeahead.js +20 -7
  36. package/build/typeahead/Typeahead.js.map +1 -1
  37. package/build/typeahead/Typeahead.mjs +20 -7
  38. package/build/typeahead/Typeahead.mjs.map +1 -1
  39. package/build/types/alert/Alert.d.ts +1 -1
  40. package/build/types/alert/Alert.d.ts.map +1 -1
  41. package/build/types/common/initials.d.ts.map +1 -1
  42. package/build/types/field/Field.d.ts +8 -4
  43. package/build/types/field/Field.d.ts.map +1 -1
  44. package/build/types/inlineAlert/InlineAlert.d.ts +1 -7
  45. package/build/types/inlineAlert/InlineAlert.d.ts.map +1 -1
  46. package/build/types/listItem/_stories/variants/helpers.d.ts +7 -4
  47. package/build/types/listItem/_stories/variants/helpers.d.ts.map +1 -1
  48. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +6 -1
  49. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
  50. package/build/types/radioGroup/RadioGroup.d.ts.map +1 -1
  51. package/build/types/test-utils/index.d.ts +0 -1
  52. package/build/types/test-utils/index.d.ts.map +1 -1
  53. package/build/types/typeahead/Typeahead.d.ts +8 -4
  54. package/build/types/typeahead/Typeahead.d.ts.map +1 -1
  55. package/build/types/upload/Upload.d.ts +1 -1
  56. package/build/types/upload/steps/uploadImageStep/uploadImageStep.d.ts.map +1 -1
  57. package/build/upload/Upload.js.map +1 -1
  58. package/build/upload/Upload.mjs.map +1 -1
  59. package/build/upload/steps/uploadImageStep/uploadImageStep.js +5 -4
  60. package/build/upload/steps/uploadImageStep/uploadImageStep.js.map +1 -1
  61. package/build/upload/steps/uploadImageStep/uploadImageStep.mjs +5 -4
  62. package/build/upload/steps/uploadImageStep/uploadImageStep.mjs.map +1 -1
  63. package/package.json +9 -8
  64. package/src/DisabledComponents.story.tsx +1 -3
  65. package/src/actionButton/ActionButton.story.tsx +42 -45
  66. package/src/alert/Alert.spec.tsx +1 -1
  67. package/src/alert/Alert.tsx +2 -2
  68. package/src/avatar/Avatar.story.tsx +192 -188
  69. package/src/button/_stories/Button.tests.story.tsx +122 -119
  70. package/src/carousel/Carousel.story.tsx +4 -7
  71. package/src/checkbox/Checkbox.story.tsx +42 -21
  72. package/src/checkbox/Checkbox.tsx +1 -1
  73. package/src/checkbox/__snapshots__/Checkbox.spec.tsx.snap +1 -1
  74. package/src/circularButton/CircularButton.story.tsx +10 -2
  75. package/src/common/bottomSheet/BottomSheet.story.tsx +48 -14
  76. package/src/common/circle/Circle.story.tsx +62 -55
  77. package/src/common/initials.spec.tsx +31 -0
  78. package/src/common/initials.ts +19 -8
  79. package/src/criticalBanner/CriticalCommsBanner.story.tsx +30 -19
  80. package/src/dateInput/DateInput.tests.story.tsx +101 -74
  81. package/src/dateLookup/DateLookup.story.tsx +69 -59
  82. package/src/field/Field.css +10 -1
  83. package/src/field/Field.less +13 -2
  84. package/src/field/Field.spec.tsx +19 -3
  85. package/src/field/Field.story.tsx +18 -0
  86. package/src/field/Field.tsx +17 -5
  87. package/src/header/Header.story.tsx +5 -16
  88. package/src/header/Header.tests.story.tsx +95 -69
  89. package/src/info/Info.story.tsx +27 -11
  90. package/src/inlineAlert/InlineAlert.story.tsx +4 -0
  91. package/src/inlineAlert/InlineAlert.tsx +1 -7
  92. package/src/instructionsList/InstructionsList.story.tsx +0 -1
  93. package/src/listItem/_stories/ListItem.layout.test.story.tsx +1 -3
  94. package/src/listItem/_stories/variants/ListItem.brightGreen.test.story.tsx +77 -35
  95. package/src/listItem/_stories/variants/ListItem.dark.test.story.tsx +65 -29
  96. package/src/listItem/_stories/variants/ListItem.forestGreen.test.story.tsx +77 -35
  97. package/src/listItem/_stories/variants/ListItem.medium.test.story.tsx +38 -18
  98. package/src/listItem/_stories/variants/ListItem.neutral.test.story.tsx +0 -1
  99. package/src/listItem/_stories/variants/ListItem.personal.test.story.tsx +38 -18
  100. package/src/listItem/_stories/variants/ListItem.rtl.test.story.tsx +77 -29
  101. package/src/listItem/_stories/variants/ListItem.small.test.story.tsx +65 -18
  102. package/src/listItem/_stories/variants/helpers.tsx +136 -133
  103. package/src/main.css +20 -1
  104. package/src/main.less +1 -0
  105. package/src/modal/Modal.story.tsx +47 -8
  106. package/src/moneyInput/MoneyInput.story.tsx +2 -2
  107. package/src/primitives/PrimitiveAnchor/stories/PrimitiveAnchor.story.tsx +1 -0
  108. package/src/primitives/PrimitiveAnchor/stories/PrimitiveAnchor.tests.story.tsx +1 -0
  109. package/src/primitives/PrimitiveButton/stories/PrimitiveButton.story.tsx +1 -0
  110. package/src/primitives/PrimitiveButton/stories/PrimitiveButton.tests.story.tsx +1 -0
  111. package/src/prompt/InlinePrompt/InlinePrompt.css +3 -0
  112. package/src/prompt/InlinePrompt/InlinePrompt.less +5 -1
  113. package/src/prompt/InlinePrompt/InlinePrompt.spec.tsx +17 -0
  114. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +35 -0
  115. package/src/prompt/InlinePrompt/InlinePrompt.tsx +7 -0
  116. package/src/provider/theme/ThemeProvider.story.tsx +1 -0
  117. package/src/radioGroup/RadioGroup.css +3 -0
  118. package/src/radioGroup/RadioGroup.less +3 -0
  119. package/src/radioGroup/RadioGroup.story.tsx +2 -0
  120. package/src/radioGroup/RadioGroup.test.story.tsx +62 -0
  121. package/src/radioGroup/RadioGroup.tsx +6 -1
  122. package/src/segmentedControl/SegmentedControl.story.tsx +71 -67
  123. package/src/snackbar/Snackbar.tests.story.tsx +116 -114
  124. package/src/statusIcon/StatusIcon.story.tsx +41 -38
  125. package/src/test-utils/index.tsx +0 -2
  126. package/src/tokens/tokens.story.tsx +1 -1
  127. package/src/tooltip/Tooltip.story.tsx +10 -2
  128. package/src/typeahead/Typeahead.css +4 -0
  129. package/src/typeahead/Typeahead.less +5 -1
  130. package/src/typeahead/Typeahead.spec.tsx +1 -1
  131. package/src/typeahead/Typeahead.story.tsx +151 -3
  132. package/src/typeahead/Typeahead.tsx +33 -9
  133. package/src/upload/Upload.story.tsx +1 -1
  134. package/src/upload/Upload.tests.story.tsx +36 -1
  135. package/src/upload/Upload.tsx +1 -1
  136. package/src/upload/steps/uploadImageStep/uploadImageStep.tsx +7 -3
  137. package/src/withId/withId.story.tsx +1 -1
  138. package/build/types/test-utils/story-config.d.ts +0 -64
  139. package/build/types/test-utils/story-config.d.ts.map +0 -1
  140. package/src/test-utils/story-config.ts +0 -95
@@ -1,6 +1,5 @@
1
- import type { StoryFn } from '@storybook/react-webpack5';
1
+ import type { Args } from '@storybook/react-webpack5';
2
2
  import { Illustration } from '@wise/art';
3
- import React from 'react';
4
3
 
5
4
  import Display from '../display';
6
5
 
@@ -54,11 +53,9 @@ const carouselCards: CarouselCard[] = [
54
53
  },
55
54
  ];
56
55
 
57
- const Template: StoryFn = (args) => {
58
- return <Carousel header="Pretty nifty stuff" cards={carouselCards} {...args} />;
59
- };
60
-
61
56
  export const CarouselDefault = {
62
- render: Template,
57
+ render: (args: Args) => {
58
+ return <Carousel header="Pretty nifty stuff" cards={carouselCards} {...args} />;
59
+ },
63
60
  args: {},
64
61
  };
@@ -2,9 +2,10 @@ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { fn } from 'storybook/test';
3
3
 
4
4
  import { Field } from '../field/Field';
5
- import { lorem10, storyConfig } from '../test-utils';
5
+ import { lorem10 } from '../test-utils';
6
6
 
7
7
  import Checkbox from './Checkbox';
8
+ import { allModes } from '../../.storybook/modes';
8
9
 
9
10
  const meta: Meta<typeof Checkbox> = {
10
11
  component: Checkbox,
@@ -20,33 +21,53 @@ type Story = StoryObj<typeof Checkbox>;
20
21
 
21
22
  export const Basic: Story = {};
22
23
 
23
- export const Multiple: Story = storyConfig(
24
- {
25
- render: (args) => {
26
- return (
27
- <>
28
- <Checkbox {...args} />
29
- <Checkbox {...args} checked />
30
- <Checkbox {...args} label={lorem10} />
31
- <Checkbox {...args} label={lorem10} secondary={lorem10} />
32
- <Checkbox {...args} disabled />
33
- </>
34
- );
24
+ export const Multiple: Story = {
25
+ render: (args) => {
26
+ return (
27
+ <>
28
+ <Checkbox {...args} />
29
+ <Checkbox {...args} checked />
30
+ <Checkbox {...args} label={lorem10} />
31
+ <Checkbox {...args} label={lorem10} secondary={lorem10} />
32
+ <Checkbox {...args} disabled />
33
+ </>
34
+ );
35
+ },
36
+ parameters: {
37
+ variants: ['default', 'dark', 'rtl'],
38
+ chromatic: {
39
+ dark: allModes.dark,
40
+ rtl: allModes.rtl,
35
41
  },
36
42
  },
37
- { variants: ['default', 'dark', 'rtl'] },
38
- );
43
+ };
39
44
 
40
- export const MultipleMobile: Story = storyConfig(Multiple, {
41
- variants: ['default', 'dark', 'rtl', 'mobile'],
42
- });
45
+ export const MultipleMobile: Story = {
46
+ ...Multiple,
47
+ parameters: {
48
+ variants: ['default', 'dark', 'rtl', 'mobile'],
49
+ chromatic: {
50
+ dark: allModes.dark,
51
+ rtl: allModes.rtl,
52
+ mobile: allModes.largeMobile,
53
+ },
54
+ },
55
+ };
43
56
 
44
57
  export const WithinField = {
45
58
  decorators: [
46
59
  (Story) => (
47
- <Field message="Something went wrong" sentiment="negative">
48
- <Story />
49
- </Field>
60
+ <>
61
+ <Field label="Field label">
62
+ <Story />
63
+ </Field>
64
+ <Field label="Field label" message="Something went wrong" sentiment="negative">
65
+ <Story />
66
+ </Field>
67
+ <Field label="Field label" message="Something went great" sentiment="positive">
68
+ <Story />
69
+ </Field>
70
+ </>
50
71
  ),
51
72
  ],
52
73
  } satisfies Story;
@@ -44,7 +44,7 @@ export default function Checkbox({
44
44
  const innerDisabled = disabled || readOnly;
45
45
  return (
46
46
  <div id={id} className={classList}>
47
- <label className={clsx({ disabled })}>
47
+ <label className={clsx('np-checkbox-label', { disabled })}>
48
48
  <CheckboxButton
49
49
  className="p-r-2"
50
50
  checked={checked}
@@ -6,7 +6,7 @@ exports[`Checkbox renders the given label 1`] = `
6
6
  class="np-checkbox checkbox"
7
7
  >
8
8
  <label
9
- class=""
9
+ class="np-checkbox-label"
10
10
  >
11
11
  <span
12
12
  class="np-checkbox-button p-r-2"
@@ -4,9 +4,9 @@ import { ControlType, Priority } from '../common';
4
4
 
5
5
  import { Meta, StoryObj } from '@storybook/react-webpack5';
6
6
  import CircularButton from './CircularButton';
7
- import { storyConfig } from '../test-utils';
8
7
  import Title from '../title';
9
8
  import Body from '../body';
9
+ import { allModes } from '../../.storybook/modes';
10
10
 
11
11
  export default {
12
12
  component: CircularButton,
@@ -87,4 +87,12 @@ export const All: Story = {
87
87
  },
88
88
  };
89
89
 
90
- export const All400Zoom: Story = storyConfig(All, { variants: ['400%'] });
90
+ export const All400Zoom: Story = {
91
+ ...All,
92
+ parameters: {
93
+ variants: ['400%'],
94
+ chromatic: {
95
+ '400%': allModes.zoom400,
96
+ },
97
+ },
98
+ };
@@ -6,11 +6,12 @@ import { useState } from 'react';
6
6
  import Body from '../../body/Body';
7
7
  import Button from '../../button';
8
8
  import NavigationOption from '../../navigationOption';
9
- import { lorem10, lorem500, storyConfig } from '../../test-utils';
9
+ import { lorem10, lorem500 } from '../../test-utils';
10
10
  import Title from '../../title/Title';
11
11
  import { Typography } from '../propsValues/typography';
12
12
 
13
13
  import BottomSheet from './BottomSheet';
14
+ import { allModes } from '../../../.storybook/modes';
14
15
 
15
16
  export default {
16
17
  component: BottomSheet,
@@ -64,7 +65,15 @@ export const Basic: Story = {
64
65
  },
65
66
  };
66
67
 
67
- export const BasicMobile: Story = storyConfig(Basic, { variants: ['mobile'] });
68
+ export const BasicMobile: Story = {
69
+ ...Basic,
70
+ parameters: {
71
+ variants: ['mobile'],
72
+ chromatic: {
73
+ mobile: allModes.largeMobile,
74
+ },
75
+ },
76
+ };
68
77
 
69
78
  export const WithOverflowContent: Story = {
70
79
  args: {
@@ -95,18 +104,43 @@ export const WithOverflowContent: Story = {
95
104
  },
96
105
  };
97
106
 
98
- export const WithOverflowContentMobile: Story = storyConfig(WithOverflowContent, {
99
- variants: ['mobile'],
100
- });
107
+ export const WithOverflowContentMobile: Story = {
108
+ ...WithOverflowContent,
109
+ parameters: {
110
+ variants: ['mobile'],
111
+ chromatic: {
112
+ mobile: allModes.largeMobile,
113
+ },
114
+ },
115
+ };
101
116
 
102
- export const WithOverflowContentDark: Story = storyConfig(WithOverflowContent, {
103
- variants: ['dark'],
104
- });
117
+ export const WithOverflowContentDark: Story = {
118
+ ...WithOverflowContent,
119
+ parameters: {
120
+ variants: ['dark'],
121
+ chromatic: {
122
+ dark: allModes.dark,
123
+ },
124
+ },
125
+ };
105
126
 
106
- export const WithOverflowContentDarkMobile: Story = storyConfig(WithOverflowContent, {
107
- variants: ['dark', 'mobile'],
108
- });
127
+ export const WithOverflowContentDarkMobile: Story = {
128
+ ...WithOverflowContent,
129
+ parameters: {
130
+ variants: ['dark', 'mobile'],
131
+ chromatic: {
132
+ dark: allModes.dark,
133
+ mobile: allModes.largeMobile,
134
+ },
135
+ },
136
+ };
109
137
 
110
- export const WithOverflowContentZoom400: Story = storyConfig(WithOverflowContent, {
111
- variants: ['400%'],
112
- });
138
+ export const WithOverflowContentZoom400: Story = {
139
+ ...WithOverflowContent,
140
+ parameters: {
141
+ variants: ['400%'],
142
+ chromatic: {
143
+ '400%': allModes.zoom400,
144
+ },
145
+ },
146
+ };
@@ -1,14 +1,14 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-webpack5';
2
- import { storyConfig } from '../../test-utils';
3
2
  import Circle from './Circle';
4
3
  import { Profile } from '@transferwise/icons';
5
- import { action } from 'storybook/actions';
6
4
  import { CircleProps } from '.';
7
5
  import Body from '../../body';
6
+ import { allModes } from '../../../.storybook/modes';
8
7
 
9
8
  export default {
10
9
  title: 'Internal/Circle',
11
10
  component: Circle,
11
+ tags: ['!manifest'],
12
12
  } satisfies Meta<typeof Circle>;
13
13
 
14
14
  type Story = StoryObj<typeof Circle>;
@@ -25,61 +25,68 @@ export const Basic: Story = {
25
25
  },
26
26
  };
27
27
 
28
- export const Sizes: Story = storyConfig(
29
- {
30
- render: () => {
31
- const content = <Profile size={16} />;
32
- const sizes: CircleProps['size'][] = [16, 24, 32, 40, 48, 56, 72];
33
- return (
34
- <div
35
- style={{
36
- gap: '1em',
37
- display: 'grid',
38
- justifyContent: 'space-between',
39
- gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
40
- }}
41
- >
42
- {sizes.map((size) => (
43
- <Circle key={size} size={size} className="bg-neutral">
44
- {content}
45
- </Circle>
46
- ))}
47
- </div>
48
- );
28
+ export const Sizes: Story = {
29
+ render: () => {
30
+ const content = <Profile size={16} />;
31
+ const sizes: CircleProps['size'][] = [16, 24, 32, 40, 48, 56, 72];
32
+ return (
33
+ <div
34
+ style={{
35
+ gap: '1em',
36
+ display: 'grid',
37
+ justifyContent: 'space-between',
38
+ gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
39
+ }}
40
+ >
41
+ {sizes.map((size) => (
42
+ <Circle key={size} size={size} className="bg-neutral">
43
+ {content}
44
+ </Circle>
45
+ ))}
46
+ </div>
47
+ );
48
+ },
49
+ parameters: {
50
+ variants: ['light', 'dark'],
51
+ chromatic: {
52
+ light: allModes.light,
53
+ dark: allModes.dark,
49
54
  },
50
55
  },
51
- { variants: ['light', 'dark'] },
52
- );
56
+ };
53
57
 
54
- export const FixedSize: Story = storyConfig(
55
- {
56
- render: () => {
57
- const size = 72;
58
- const content = <Profile size={16} />;
59
- return (
60
- <div
61
- style={{
62
- gap: '1em',
63
- display: 'grid',
64
- justifyContent: 'space-between',
65
- gridTemplate: 'auto auto / repeat(2, 180px)',
66
- }}
67
- >
68
- <Body className="d-block">
69
- Dynamic Size (<code>--size-{size}</code>)
70
- </Body>
71
- <Body className="d-block">
72
- Fixed Size (<code>{size}px</code>)
73
- </Body>
74
- <Circle size={72} fixedSize={false} className="bg-neutral">
75
- {content}
76
- </Circle>
77
- <Circle size={72} fixedSize className="bg-neutral">
78
- {content}
79
- </Circle>
80
- </div>
81
- );
58
+ export const FixedSize: Story = {
59
+ render: () => {
60
+ const size = 72;
61
+ const content = <Profile size={16} />;
62
+ return (
63
+ <div
64
+ style={{
65
+ gap: '1em',
66
+ display: 'grid',
67
+ justifyContent: 'space-between',
68
+ gridTemplate: 'auto auto / repeat(2, 180px)',
69
+ }}
70
+ >
71
+ <Body className="d-block">
72
+ Dynamic Size (<code>--size-{size}</code>)
73
+ </Body>
74
+ <Body className="d-block">
75
+ Fixed Size (<code>{size}px</code>)
76
+ </Body>
77
+ <Circle size={72} fixedSize={false} className="bg-neutral">
78
+ {content}
79
+ </Circle>
80
+ <Circle size={72} fixedSize className="bg-neutral">
81
+ {content}
82
+ </Circle>
83
+ </div>
84
+ );
85
+ },
86
+ parameters: {
87
+ variants: ['400%'],
88
+ chromatic: {
89
+ zoom400: allModes.zoom400,
82
90
  },
83
91
  },
84
- { variants: ['400%'] },
85
- );
92
+ };
@@ -52,4 +52,35 @@ describe('getInitials', () => {
52
52
  expect(getInitials('(Super) Fantastic Squirrel')).toBe('FS');
53
53
  expect(getInitials('Super (Fantastic) Squirrel')).toBe('SS');
54
54
  });
55
+
56
+ it('handles emojis correctly', () => {
57
+ expect(getInitials('πŸ˜€')).toBe('πŸ˜€');
58
+ expect(getInitials('πŸ˜€ Christian Dalby')).toBe('πŸ˜€D');
59
+ expect(getInitials('Christian πŸ˜€')).toBe('CπŸ˜€');
60
+ expect(getInitials('😎 Christian 😎')).toBe('😎😎');
61
+ expect(getInitials('πŸŽ‰ Party 🎊 Time')).toBe('πŸŽ‰T');
62
+ });
63
+
64
+ it('handles unicode strings correctly', () => {
65
+ expect(getInitials('\u{1F600}')).toBe('\u{1F600}');
66
+ expect(getInitials('\u{1F600} Christian Dalby')).toBe('\u{1F600}D');
67
+ expect(getInitials('Christian \u{1F600}')).toBe('C\u{1F600}');
68
+ expect(getInitials('\u{1F60E} Christian \u{1F60E}')).toBe('\u{1F60E}\u{1F60E}');
69
+ expect(getInitials('\u{1F389} Party \u{1F38A} Time')).toBe('\u{1F389}T');
70
+ });
71
+
72
+ it('handles multi-byte emojis with skin tones and ZWJ sequences', () => {
73
+ expect(getInitials('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')).toBe('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦');
74
+ expect(getInitials('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ Family')).toBe('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦F');
75
+ expect(getInitials('πŸ‘‹πŸ½')).toBe('πŸ‘‹πŸ½');
76
+ expect(getInitials('Christian πŸ‘‹πŸ½')).toBe('CπŸ‘‹πŸ½');
77
+ });
78
+
79
+ it('handles diacritical marks correctly', () => {
80
+ expect(getInitials('AndrΓ© MΓΌller')).toBe('AM');
81
+ expect(getInitials('Ángel Muñoz')).toBe('ÁM');
82
+ expect(getInitials('François Élis')).toBe('FÉ');
83
+ expect(getInitials('Łukasz Ślusarczyk')).toBe('ŁŚ');
84
+ expect(getInitials('Γ–mer Γ–zdemir')).toBe('Γ–Γ–');
85
+ });
55
86
  });
@@ -1,3 +1,15 @@
1
+ function startsWithParenthesis(part: string) {
2
+ return /^[({[<]/.test(part);
3
+ }
4
+
5
+ // Reuse a single Intl.Segmenter instance to avoid repeated allocations
6
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' });
7
+
8
+ // Helper to split the string into grapheme clusters, which handles complex characters correctly
9
+ function getGraphemes(str: string): string[] {
10
+ return Array.from(GRAPHEME_SEGMENTER.segment(str), (s) => s.segment);
11
+ }
12
+
1
13
  export function getInitials(name: string) {
2
14
  if (name.length === 2 && /^[A-Z]{2}$/.test(name)) {
3
15
  return name;
@@ -6,17 +18,16 @@ export function getInitials(name: string) {
6
18
  const allInitials = name
7
19
  .split(' ')
8
20
  .filter((part) => !startsWithParenthesis(part))
9
- .map((part) => part[0])
21
+ .map((part) => getGraphemes(part)[0])
10
22
  .join('')
11
23
  .toUpperCase();
12
24
 
13
- if (allInitials.length === 1) {
14
- return allInitials[0];
15
- }
16
-
17
- return allInitials[0] + allInitials.slice(-1);
25
+ // Get graphemes of the initials string to handle complex characters correctly
26
+ const graphemes = getGraphemes(allInitials);
18
27
 
19
- function startsWithParenthesis(part: string) {
20
- return /^[({[<]/.test(part);
28
+ if (graphemes.length === 1) {
29
+ return graphemes[0];
21
30
  }
31
+
32
+ return graphemes[0] + graphemes[graphemes.length - 1];
22
33
  }
@@ -1,13 +1,15 @@
1
- import { Meta } from '@storybook/react-webpack5';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
 
3
3
  import CriticalCommsBanner from '.';
4
- import { storyConfig } from '../test-utils';
4
+ import { allModes } from '../../.storybook/modes';
5
5
 
6
6
  export default {
7
7
  component: CriticalCommsBanner,
8
8
  title: 'Prompts/CriticalCommsBanner',
9
9
  } satisfies Meta<typeof CriticalCommsBanner>;
10
10
 
11
+ type Story = StoryObj<typeof CriticalCommsBanner>;
12
+
11
13
  export const Basic = {
12
14
  args: {
13
15
  title: 'Your account is overdrawn',
@@ -16,25 +18,34 @@ export const Basic = {
16
18
  },
17
19
  };
18
20
 
19
- export const Variants = storyConfig(
20
- {
21
- args: {
22
- title: 'Your account is overdrawn',
23
- subtitle: 'Add money within the next 30 days',
24
- action: { label: 'Take action', href: 'https://wise.com' },
21
+ export const Variants: Story = {
22
+ args: {
23
+ title: 'Your account is overdrawn',
24
+ subtitle: 'Add money within the next 30 days',
25
+ action: { label: 'Take action', href: 'https://wise.com' },
26
+ },
27
+ parameters: {
28
+ variants: ['default', 'dark', 'rtl'],
29
+ chromatic: {
30
+ dark: allModes.dark,
31
+ rtl: allModes.rtl,
25
32
  },
26
33
  },
27
- { variants: ['default', 'dark', 'rtl'] },
28
- );
34
+ };
29
35
 
30
- export const Mobile = storyConfig(
31
- {
32
- tags: ['!autodocs'],
33
- args: {
34
- title: 'Your account is overdrawn',
35
- subtitle: 'Add money within the next 30 days',
36
- action: { label: 'Take action', href: 'https://wise.com' },
36
+ export const Mobile: Story = {
37
+ tags: ['!autodocs'],
38
+ args: {
39
+ title: 'Your account is overdrawn',
40
+ subtitle: 'Add money within the next 30 days',
41
+ action: { label: 'Take action', href: 'https://wise.com' },
42
+ },
43
+ parameters: {
44
+ variants: ['default', 'dark', 'rtl', 'mobile'],
45
+ chromatic: {
46
+ dark: allModes.dark,
47
+ rtl: allModes.rtl,
48
+ mobile: allModes.largeMobile,
37
49
  },
38
50
  },
39
- { variants: ['default', 'dark', 'rtl', 'mobile'] },
40
- );
51
+ };