@transferwise/components 46.141.0 → 46.142.0

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 (93) hide show
  1. package/build/avatarLayout/AvatarLayout.js +15 -1
  2. package/build/avatarLayout/AvatarLayout.js.map +1 -1
  3. package/build/avatarLayout/AvatarLayout.mjs +15 -1
  4. package/build/avatarLayout/AvatarLayout.mjs.map +1 -1
  5. package/build/avatarView/AvatarView.js +6 -2
  6. package/build/avatarView/AvatarView.js.map +1 -1
  7. package/build/avatarView/AvatarView.mjs +6 -2
  8. package/build/avatarView/AvatarView.mjs.map +1 -1
  9. package/build/avatarView/Dot.js +8 -0
  10. package/build/avatarView/Dot.js.map +1 -1
  11. package/build/avatarView/Dot.mjs +8 -0
  12. package/build/avatarView/Dot.mjs.map +1 -1
  13. package/build/common/circle/Circle.js +6 -2
  14. package/build/common/circle/Circle.js.map +1 -1
  15. package/build/common/circle/Circle.mjs +6 -2
  16. package/build/common/circle/Circle.mjs.map +1 -1
  17. package/build/expressiveMoneyInput/amountInput/AmountInput.js +1 -1
  18. package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -1
  19. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +1 -1
  20. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs.map +1 -1
  21. package/build/field/Field.js +63 -32
  22. package/build/field/Field.js.map +1 -1
  23. package/build/field/Field.messages.js +14 -0
  24. package/build/field/Field.messages.js.map +1 -0
  25. package/build/field/Field.messages.mjs +10 -0
  26. package/build/field/Field.messages.mjs.map +1 -0
  27. package/build/field/Field.mjs +65 -34
  28. package/build/field/Field.mjs.map +1 -1
  29. package/build/i18n/en.json +1 -0
  30. package/build/i18n/en.json.js +1 -0
  31. package/build/i18n/en.json.js.map +1 -1
  32. package/build/i18n/en.json.mjs +1 -0
  33. package/build/i18n/en.json.mjs.map +1 -1
  34. package/build/inputs/TextArea.js +5 -0
  35. package/build/inputs/TextArea.js.map +1 -1
  36. package/build/inputs/TextArea.mjs +6 -1
  37. package/build/inputs/TextArea.mjs.map +1 -1
  38. package/build/inputs/contexts.js +16 -0
  39. package/build/inputs/contexts.js.map +1 -1
  40. package/build/inputs/contexts.mjs +16 -2
  41. package/build/inputs/contexts.mjs.map +1 -1
  42. package/build/main.css +42 -8
  43. package/build/styles/avatarView/AvatarView.css +4 -4
  44. package/build/styles/avatarView/Dot.css +4 -4
  45. package/build/styles/css/neptune.css +15 -1
  46. package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
  47. package/build/styles/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
  48. package/build/styles/field/Field.css +19 -3
  49. package/build/styles/main.css +42 -8
  50. package/build/styles/styles/less/neptune.css +15 -1
  51. package/build/types/avatarView/AvatarView.d.ts +1 -1
  52. package/build/types/avatarView/AvatarView.d.ts.map +1 -1
  53. package/build/types/avatarView/Dot.d.ts.map +1 -1
  54. package/build/types/common/circle/Circle.d.ts +1 -1
  55. package/build/types/common/circle/Circle.d.ts.map +1 -1
  56. package/build/types/field/Field.d.ts.map +1 -1
  57. package/build/types/field/Field.messages.d.ts +8 -0
  58. package/build/types/field/Field.messages.d.ts.map +1 -0
  59. package/build/types/inputs/TextArea.d.ts.map +1 -1
  60. package/build/types/inputs/contexts.d.ts +6 -0
  61. package/build/types/inputs/contexts.d.ts.map +1 -1
  62. package/build/types/test-utils/index.d.ts +2 -0
  63. package/build/types/test-utils/index.d.ts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/avatarLayout/AvatarLayout.story.tsx +1 -1
  66. package/src/avatarLayout/AvatarLayout.tsx +4 -0
  67. package/src/avatarView/AvatarView.css +4 -4
  68. package/src/avatarView/AvatarView.story.tsx +17 -13
  69. package/src/avatarView/AvatarView.tsx +5 -1
  70. package/src/avatarView/Dot.css +4 -4
  71. package/src/avatarView/Dot.less +6 -6
  72. package/src/avatarView/Dot.tsx +2 -0
  73. package/src/common/circle/Circle.tsx +5 -1
  74. package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
  75. package/src/expressiveMoneyInput/ExpressiveMoneyInput.test.story.tsx +43 -0
  76. package/src/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
  77. package/src/expressiveMoneyInput/amountInput/AmountInput.less +2 -0
  78. package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +1 -1
  79. package/src/field/Field.css +19 -3
  80. package/src/field/Field.less +17 -3
  81. package/src/field/Field.messages.ts +8 -0
  82. package/src/field/Field.story.tsx +5 -1
  83. package/src/field/Field.test.tsx +90 -0
  84. package/src/field/Field.tsx +84 -37
  85. package/src/i18n/en.json +1 -0
  86. package/src/inputs/TextArea.story.tsx +97 -0
  87. package/src/inputs/TextArea.test.story.tsx +142 -0
  88. package/src/inputs/TextArea.tsx +7 -2
  89. package/src/inputs/contexts.tsx +18 -1
  90. package/src/main.css +42 -8
  91. package/src/styles/less/core/_typography.less +28 -6
  92. package/src/styles/less/neptune.css +15 -1
  93. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/test-utils/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAM,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,MAAM,EAAc,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAMxD;;;;GAIG;AACH,iBAAS,YAAY,CACnB,EAAE,EAAE,YAAY,EAChB,EAAE,MAAuB,EAAE,QAAa,EAAE,GAAG,aAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAK,GAChE,UAAU,CAAC,OAAO,MAAM,CAAC,CAK3B;AAED;;;GAGG;AACH,iBAAS,gBAAgB,CACvB,QAAQ,EAAE,MAAM,OAAO,EACvB,EAAE,MAAuB,EAAE,QAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAK,uEAKhD;AAED,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,gBAAgB,IAAI,UAAU,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/test-utils/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAM,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,MAAM,EAAc,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAMxD;;;;GAIG;AACH,iBAAS,YAAY,CACnB,EAAE,EAAE,YAAY,EAChB,EAAE,MAAuB,EAAE,QAAa,EAAE,GAAG,aAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAK,GAChE,UAAU,CAAC,OAAO,MAAM,CAAC,CAK3B;AAED;;;GAGG;AACH,iBAAS,gBAAgB,CACvB,QAAQ,EAAE,MAAM,OAAO,EACvB,EAAE,MAAuB,EAAE,QAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAK,uEAKhD;AAED,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,gBAAgB,IAAI,UAAU,EAAE,SAAS,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "46.141.0",
3
+ "version": "46.142.0",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -91,7 +91,7 @@
91
91
  "storybook-addon-tag-badges": "^3.1.0",
92
92
  "storybook-addon-test-codegen": "^3.0.1",
93
93
  "@transferwise/less-config": "3.1.2",
94
- "@transferwise/neptune-css": "14.27.1",
94
+ "@transferwise/neptune-css": "14.27.2",
95
95
  "@wise/components-theming": "1.10.2",
96
96
  "@wise/wds-configs": "0.0.0"
97
97
  },
@@ -13,7 +13,7 @@ export default {
13
13
 
14
14
  type Story = StoryObj<typeof AvatarLayout>;
15
15
 
16
- const sizes: AvatarLayoutProps['size'][] = [16, 24, 32, 40, 48, 56, 72];
16
+ const sizes: AvatarLayoutProps['size'][] = [16, 24, 32, 40, 48, 56, 72, 88, 96];
17
17
 
18
18
  export const Diagonal: Story = {
19
19
  render: () => (
@@ -94,6 +94,8 @@ const DIAGONAL_LAYOUT_STYLE_CONFIG = {
94
94
  48: { avatarSize: 30, offset: 3.5, fontSize: 14, iconSize: 16.87 },
95
95
  56: { avatarSize: 34, offset: 5, fontSize: 14, iconSize: 19.12 },
96
96
  72: { avatarSize: 44, offset: 6, fontSize: 22, iconSize: 22 },
97
+ 88: { avatarSize: 54, offset: 7, fontSize: 26, iconSize: 27 },
98
+ 96: { avatarSize: 58, offset: 8, fontSize: 28, iconSize: 29 },
97
99
  };
98
100
 
99
101
  /** Horizontal layout have custom offset between avatars */
@@ -105,4 +107,6 @@ const HORIZONTAL_LAYOUT_OFFSET = {
105
107
  48: 4,
106
108
  56: 6,
107
109
  72: 8,
110
+ 88: 10,
111
+ 96: 10,
108
112
  };
@@ -4,10 +4,10 @@
4
4
  display: inline-block;
5
5
  }
6
6
  .np-dot-mask {
7
- -webkit-mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
8
- mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
9
- -webkit-mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
10
- mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
7
+ -webkit-mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
8
+ mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
9
+ -webkit-mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
10
+ mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
11
11
  }
12
12
  .np-dot-badge {
13
13
  position: absolute;
@@ -30,7 +30,7 @@ type Story = StoryObj<typeof AvatarView>;
30
30
 
31
31
  const profileName1 = 'Wolter White';
32
32
  const profileName2 = 'Tyler Durden';
33
- const sizes: AvatarViewProps['size'][] = [16, 24, 32, 40, 48, 56, 72];
33
+ const sizes: AvatarViewProps['size'][] = [16, 24, 32, 40, 48, 56, 72, 88, 96];
34
34
 
35
35
  const withComponentGrid = (Story: () => JSX.Element) => (
36
36
  <div
@@ -97,15 +97,19 @@ export const Badge: Story = {
97
97
  gap: '1em',
98
98
  display: 'grid',
99
99
  justifyContent: 'space-between',
100
- gridTemplate: 'auto auto / repeat(7, min-content)',
100
+ gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
101
101
  }}
102
102
  >
103
103
  {sizes.map((size) => (
104
104
  <Body type="body-large-bold">{size}</Body>
105
105
  ))}
106
106
  {sizes.map((size, index) => (
107
- <AvatarView key={size} size={size} badge={{ flagCode: currencies[index] }}>
108
- {icons[index]}
107
+ <AvatarView
108
+ key={size}
109
+ size={size}
110
+ badge={{ flagCode: currencies[index % currencies.length] }}
111
+ >
112
+ {icons[index % icons.length]}
109
113
  </AvatarView>
110
114
  ))}
111
115
 
@@ -129,43 +133,43 @@ export const Badge: Story = {
129
133
 
130
134
  {sizes.map((size, index) => (
131
135
  <AvatarView key={size} size={size} badge={{ imgSrc: '../tapestry-01.png' }}>
132
- {icons[index]}
136
+ {icons[index % icons.length]}
133
137
  </AvatarView>
134
138
  ))}
135
139
 
136
140
  {sizes.map((size, index) => (
137
141
  <AvatarView key={size} size={size} badge={{ status: 'warning' }}>
138
- {icons[index]}
142
+ {icons[index % icons.length]}
139
143
  </AvatarView>
140
144
  ))}
141
145
 
142
146
  {sizes.map((size, index) => (
143
147
  <AvatarView key={size} size={size} badge={{ status: 'neutral' }}>
144
- {icons[index]}
148
+ {icons[index % icons.length]}
145
149
  </AvatarView>
146
150
  ))}
147
151
 
148
152
  {sizes.map((size, index) => (
149
153
  <AvatarView key={size} size={size} badge={{ status: 'negative' }}>
150
- {icons[index]}
154
+ {icons[index % icons.length]}
151
155
  </AvatarView>
152
156
  ))}
153
157
 
154
158
  {sizes.map((size, index) => (
155
159
  <AvatarView key={size} size={size} badge={{ icon: <FastFlag /> }}>
156
- {icons[index]}
160
+ {icons[index % icons.length]}
157
161
  </AvatarView>
158
162
  ))}
159
163
 
160
164
  {sizes.map((size, index) => (
161
165
  <AvatarView key={size} size={size} badge={{ type: 'reference' }}>
162
- {icons[index]}
166
+ {icons[index % icons.length]}
163
167
  </AvatarView>
164
168
  ))}
165
169
 
166
170
  {sizes.map((size, index) => (
167
171
  <AvatarView key={size} size={size} badge={{ type: 'action' }}>
168
- {icons[index]}
172
+ {icons[index % icons.length]}
169
173
  </AvatarView>
170
174
  ))}
171
175
 
@@ -322,7 +326,7 @@ export const Images: Story = {
322
326
  gap: '1em',
323
327
  display: 'grid',
324
328
  justifyContent: 'space-between',
325
- gridTemplate: 'auto auto / repeat(7, min-content)',
329
+ gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
326
330
  }}
327
331
  >
328
332
  {sizes.map((size) => (
@@ -376,7 +380,7 @@ export const Profiles: Story = {
376
380
  gap: '1em',
377
381
  display: 'grid',
378
382
  justifyContent: 'space-between',
379
- gridTemplate: 'auto auto / repeat(7, min-content)',
383
+ gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
380
384
  }}
381
385
  >
382
386
  {sizes.map((size) => (
@@ -25,7 +25,7 @@ export type Props = {
25
25
  */
26
26
  profileName?: string | null;
27
27
  profileType?: ProfileTypeBusiness | ProfileTypePersonal;
28
- size?: 16 | 24 | 32 | 40 | 48 | 56 | 72;
28
+ size?: 16 | 24 | 32 | 40 | 48 | 56 | 72 | 88 | 96;
29
29
  badge?: AvatarViewBadgeProps;
30
30
  interactive?: boolean;
31
31
  selected?: boolean;
@@ -92,6 +92,8 @@ const MAP_BADGE_ASSET_SIZE = {
92
92
  48: 16,
93
93
  56: 24,
94
94
  72: 24,
95
+ 88: 24,
96
+ 96: 24,
95
97
  } satisfies Record<number, BadgeAssetsProps['size']>;
96
98
 
97
99
  /** Border width for `selected` state determined by avatar size */
@@ -103,6 +105,8 @@ const MAP_SELECTED_BORDER_WIDTH = {
103
105
  48: 2,
104
106
  56: 2,
105
107
  72: 2,
108
+ 88: 2,
109
+ 96: 2,
106
110
  };
107
111
 
108
112
  /** Certain sizes of AvatarView has a custom offset for badge */
@@ -4,10 +4,10 @@
4
4
  display: inline-block;
5
5
  }
6
6
  .np-dot-mask {
7
- -webkit-mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
8
- mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
9
- -webkit-mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
10
- mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
7
+ -webkit-mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
8
+ mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
9
+ -webkit-mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
10
+ mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
11
11
  }
12
12
  .np-dot-badge {
13
13
  position: absolute;
@@ -5,11 +5,11 @@
5
5
 
6
6
  &-mask {
7
7
  mask-image: radial-gradient(
8
- circle at bottom calc(100% - calc(var(--np-dot-size) / 2))
9
- left calc(100% - calc(var(--np-dot-size) / 2)),
10
- transparent 0,
11
- transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)),
12
- black 0
8
+ circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left
9
+ calc(100% - calc(var(--np-dot-size) / 2)),
10
+ transparent 0,
11
+ transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)),
12
+ black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px)
13
13
  );
14
14
  }
15
15
 
@@ -19,7 +19,7 @@
19
19
  height: var(--np-dot-size);
20
20
  border-radius: var(--radius-full);
21
21
  right: 0;
22
-
22
+
23
23
  &-notification {
24
24
  background-color: var(--color-sentiment-negative);
25
25
  }
@@ -18,6 +18,8 @@ const MAP_STYLE_CONFIG = {
18
18
  48: { size: 14, offset: 2 },
19
19
  56: { size: 16, offset: 3 },
20
20
  72: { size: 20, offset: 3 },
21
+ 88: { size: 24, offset: 4 },
22
+ 96: { size: 24, offset: 4 },
21
23
  };
22
24
 
23
25
  export default function Dot({ children, avatarSize = 48, variant = 'notification' }: DotProps) {
@@ -3,7 +3,7 @@ import { clsx } from 'clsx';
3
3
  import { useMedia } from '../hooks/useMedia';
4
4
  import { Breakpoint } from '../propsValues/breakpoint';
5
5
 
6
- export type ShapeSize = 16 | 24 | 32 | 40 | 48 | 56 | 72;
6
+ export type ShapeSize = 16 | 24 | 32 | 40 | 48 | 56 | 72 | 88 | 96;
7
7
 
8
8
  export type Props = {
9
9
  /**
@@ -33,6 +33,8 @@ const MAP_ICON_SIZE = {
33
33
  48: 24,
34
34
  56: 28,
35
35
  72: 36,
36
+ 88: 44,
37
+ 96: 48,
36
38
  };
37
39
 
38
40
  /**
@@ -48,6 +50,8 @@ const MAP_FONT_SIZE = {
48
50
  48: 22,
49
51
  56: 26,
50
52
  72: 30,
53
+ 88: 36,
54
+ 96: 40,
51
55
  };
52
56
 
53
57
  const Circle = forwardRef(function Circle(
@@ -21,6 +21,7 @@
21
21
  flex-grow: 1;
22
22
  text-align: right;
23
23
  background-color: transparent;
24
+ line-height: inherit;
24
25
  }
25
26
  .wds-amount-input-input:focus-visible {
26
27
  outline: none;
@@ -29,6 +30,7 @@
29
30
  flex-grow: 0;
30
31
  display: flex;
31
32
  align-items: center;
33
+ line-height: inherit;
32
34
  }
33
35
  .wds-currency-selector:disabled {
34
36
  opacity: 1 !important;
@@ -1,4 +1,5 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { fn } from 'storybook/test';
2
3
  import ExpressiveMoneyInput, { Props as ExpressiveMoneyInputProps } from './ExpressiveMoneyInput';
3
4
 
4
5
  const meta: Meta<typeof ExpressiveMoneyInput> = {
@@ -21,3 +22,45 @@ export const WithAutofocus: Story = {
21
22
  autoFocus: true,
22
23
  },
23
24
  };
25
+
26
+ const locales = [
27
+ { lang: 'en', label: 'English', currency: 'GBP' },
28
+ { lang: 'cs', label: 'Czech', currency: 'CZK' },
29
+ { lang: 'de', label: 'German', currency: 'EUR' },
30
+ { lang: 'es', label: 'Spanish', currency: 'EUR' },
31
+ { lang: 'fr', label: 'French', currency: 'EUR' },
32
+ { lang: 'hu', label: 'Hungarian', currency: 'HUF', supportsDecimals: false },
33
+ { lang: 'id', label: 'Indonesian', currency: 'IDR', supportsDecimals: false },
34
+ { lang: 'it', label: 'Italian', currency: 'EUR' },
35
+ { lang: 'ja', label: 'Japanese', currency: 'JPY', supportsDecimals: false },
36
+ { lang: 'nl', label: 'Dutch', currency: 'EUR' },
37
+ { lang: 'pl', label: 'Polish', currency: 'PLN' },
38
+ { lang: 'pt', label: 'Portuguese', currency: 'EUR' },
39
+ { lang: 'ro', label: 'Romanian', currency: 'RON' },
40
+ { lang: 'ru', label: 'Russian', currency: 'RUB' },
41
+ { lang: 'th', label: 'Thai', currency: 'THB' },
42
+ { lang: 'tr', label: 'Turkish', currency: 'TRY' },
43
+ { lang: 'uk', label: 'Ukrainian', currency: 'UAH' },
44
+ { lang: 'zh-CN', label: 'Simplified Chinese', currency: 'CNY' },
45
+ { lang: 'zh-HK', label: 'Traditional Chinese', currency: 'HKD' },
46
+ ];
47
+
48
+ /**
49
+ * Verifies that the .wds-text-display--forced class correctly overrides locale-specific
50
+ * font restrictions (ja, zh-HK, zh-CN, th) so amounts render in Wise Sans across all
51
+ * supported locales. Each row represents a different language context.
52
+ */
53
+ export const LocaleFontOverride = () => (
54
+ <>
55
+ {locales.map(({ lang, label, currency, supportsDecimals = true }) => (
56
+ <div key={lang} lang={lang} className="m-b-4">
57
+ <ExpressiveMoneyInput
58
+ label={`${label} locale (${lang})`}
59
+ currency={currency}
60
+ amount={supportsDecimals ? 1234.56 : 123456}
61
+ onAmountChange={fn()}
62
+ />
63
+ </div>
64
+ ))}
65
+ </>
66
+ );
@@ -21,6 +21,7 @@
21
21
  flex-grow: 1;
22
22
  text-align: right;
23
23
  background-color: transparent;
24
+ line-height: inherit;
24
25
  }
25
26
  .wds-amount-input-input:focus-visible {
26
27
  outline: none;
@@ -29,4 +30,5 @@
29
30
  flex-grow: 0;
30
31
  display: flex;
31
32
  align-items: center;
33
+ line-height: inherit;
32
34
  }
@@ -29,6 +29,7 @@
29
29
  flex-grow: 1;
30
30
  text-align: right;
31
31
  background-color: transparent;
32
+ line-height: inherit;
32
33
 
33
34
  &:focus-visible {
34
35
  outline: none;
@@ -39,5 +40,6 @@
39
40
  flex-grow: 0;
40
41
  display: flex;
41
42
  align-items: center;
43
+ line-height: inherit;
42
44
  }
43
45
  }
@@ -298,7 +298,7 @@ export const AmountInput = ({
298
298
  return (
299
299
  <div className="wds-amount-input-container">
300
300
  <div
301
- className={clsx('wds-amount-input-input-container', 'np-text-display-large')}
301
+ className={clsx('wds-amount-input-input-container', 'np-text-display-large--forced')}
302
302
  style={style}
303
303
  >
304
304
  <input
@@ -1,13 +1,29 @@
1
- .np-field-control,
2
- .np-field__prompt {
1
+ .np-field-control {
3
2
  margin-top: 4px;
4
3
  margin-top: var(--size-4);
5
4
  }
5
+ .np-field-validation {
6
+ display: flex;
7
+ align-items: flex-start;
8
+ margin-top: 4px;
9
+ margin-top: var(--size-4);
10
+ gap: 8px;
11
+ gap: var(--size-8);
12
+ }
13
+ .np-field-textarea-char-counter {
14
+ min-width: 55px;
15
+ text-align: right;
16
+ margin-left: auto;
17
+ padding: 4px 0;
18
+ padding: var(--size-4) 0;
19
+ color: #768e9c;
20
+ color: var(--color-content-tertiary);
21
+ }
6
22
  .np-field .form-group--typeahead[class],
7
23
  .np-field .np-checkbox-label[class] {
8
24
  margin-bottom: 0;
9
25
  }
10
- .np-field:has(.wds-radio-group) .np-field__prompt {
26
+ .np-field:has(.wds-radio-group) .np-field-validation {
11
27
  margin-top: 12px;
12
28
  margin-top: var(--size-12);
13
29
  }
@@ -1,16 +1,30 @@
1
1
  .np-field {
2
- &-control,
3
- &__prompt {
2
+ &-control {
4
3
  margin-top: var(--size-4);
5
4
  }
6
5
 
6
+ &-validation {
7
+ display: flex;
8
+ align-items: flex-start;
9
+ margin-top: var(--size-4);
10
+ gap: var(--size-8);
11
+ }
12
+
13
+ &-textarea-char-counter {
14
+ min-width: 55px;
15
+ text-align: right;
16
+ margin-left: auto;
17
+ padding: var(--size-4) 0;
18
+ color: var(--color-content-tertiary);
19
+ }
20
+
7
21
  // @FIXME space between individual fields should be 24px, while some older inputs
8
22
  // inject extraneous space.
9
23
  .form-group--typeahead[class],
10
24
  .np-checkbox-label[class] {
11
25
  margin-bottom: 0;
12
26
  }
13
- &:has(.wds-radio-group) &__prompt {
27
+ &:has(.wds-radio-group) &-validation {
14
28
  margin-top: var(--size-12);
15
29
  }
16
30
  }
@@ -0,0 +1,8 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ export default defineMessages({
4
+ characterCount: {
5
+ id: 'neptune.Field.characterCount',
6
+ defaultMessage: '{current} of {max} characters used',
7
+ },
8
+ });
@@ -84,7 +84,11 @@ export const Basic = (args: FieldProps) => {
84
84
  messageIconLabel={args.messageIconLabel}
85
85
  messageLoading={args.messageLoading}
86
86
  >
87
- <TextArea />
87
+ <TextArea
88
+ maxLength={200}
89
+ value={value}
90
+ onChange={({ target }) => setValue(target.value)}
91
+ />
88
92
  </Field>
89
93
 
90
94
  <Field
@@ -1,5 +1,6 @@
1
1
  import Info from '../info/Info';
2
2
  import { Input } from '../inputs/Input';
3
+ import { TextArea } from '../inputs/TextArea';
3
4
  import { mockMatchMedia, render, screen, userEvent } from '../test-utils';
4
5
 
5
6
  import { Field } from './Field';
@@ -163,4 +164,93 @@ describe('Field', () => {
163
164
  expect(screen.getByTestId('InlinePrompt_ProcessIndicator')).toBeInTheDocument();
164
165
  expect(screen.getByText('Processing your request')).toBeInTheDocument();
165
166
  });
167
+
168
+ describe('TextArea character count', () => {
169
+ it('renders counter when TextArea has maxLength', () => {
170
+ render(
171
+ <Field label="Message">
172
+ <TextArea maxLength={200} value="hello" onChange={() => {}} />
173
+ </Field>,
174
+ );
175
+
176
+ expect(screen.getByText('5/200')).toBeInTheDocument();
177
+ });
178
+
179
+ it('does not render counter when TextArea has no maxLength', () => {
180
+ render(
181
+ <Field label="Message">
182
+ <TextArea value="hello" onChange={() => {}} />
183
+ </Field>,
184
+ );
185
+
186
+ expect(screen.queryByText(/\/\d+/)).not.toBeInTheDocument();
187
+ });
188
+
189
+ it('includes counter id in aria-describedby of the textarea', () => {
190
+ render(
191
+ <Field label="Message">
192
+ <TextArea maxLength={200} value="hello" onChange={() => {}} />
193
+ </Field>,
194
+ );
195
+
196
+ const textarea = screen.getByRole('textbox');
197
+ const counter = screen.getByText('5/200');
198
+ expect(textarea).toHaveAttribute('aria-describedby', expect.stringContaining(counter.id));
199
+ });
200
+
201
+ it('does not have role=status below 80% threshold', () => {
202
+ render(
203
+ <Field label="Message">
204
+ <TextArea maxLength={200} value="short" onChange={() => {}} />
205
+ </Field>,
206
+ );
207
+
208
+ const counter = screen.getByText('5/200');
209
+ expect(counter).not.toHaveAttribute('role');
210
+ expect(counter).not.toHaveAttribute('aria-live');
211
+ });
212
+
213
+ it('has role=status and aria-live=polite at 80% threshold', () => {
214
+ const value = 'x'.repeat(160);
215
+ render(
216
+ <Field label="Message">
217
+ <TextArea maxLength={200} value={value} onChange={() => {}} />
218
+ </Field>,
219
+ );
220
+
221
+ const counter = screen.getByText('160/200');
222
+ expect(counter).toHaveAttribute('role', 'status');
223
+ expect(counter).toHaveAttribute('aria-live', 'polite');
224
+ expect(counter).toHaveAttribute('aria-atomic', 'true');
225
+ });
226
+
227
+ it('updates counter when text changes', () => {
228
+ const { rerender } = render(
229
+ <Field label="Message">
230
+ <TextArea maxLength={200} value="hi" onChange={() => {}} />
231
+ </Field>,
232
+ );
233
+
234
+ expect(screen.getByText('2/200')).toBeInTheDocument();
235
+
236
+ rerender(
237
+ <Field label="Message">
238
+ <TextArea maxLength={200} value="hello world" onChange={() => {}} />
239
+ </Field>,
240
+ );
241
+
242
+ expect(screen.getByText('11/200')).toBeInTheDocument();
243
+ });
244
+
245
+ it('provides accessible aria-label on the counter', () => {
246
+ render(
247
+ <Field label="Message">
248
+ <TextArea maxLength={200} value="hello" onChange={() => {}} />
249
+ </Field>,
250
+ );
251
+
252
+ const counter = screen.getByText('5/200');
253
+ expect(counter).toHaveAttribute('aria-label', '5 of 200 characters used');
254
+ });
255
+ });
166
256
  });