@transferwise/components 46.140.1 → 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 (221) 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/avatarWrapper/AvatarWrapper.js +3 -4
  14. package/build/avatarWrapper/AvatarWrapper.js.map +1 -1
  15. package/build/avatarWrapper/AvatarWrapper.mjs +4 -5
  16. package/build/avatarWrapper/AvatarWrapper.mjs.map +1 -1
  17. package/build/button/LegacyButton.js.map +1 -1
  18. package/build/button/LegacyButton.mjs.map +1 -1
  19. package/build/common/circle/Circle.js +6 -2
  20. package/build/common/circle/Circle.js.map +1 -1
  21. package/build/common/circle/Circle.mjs +6 -2
  22. package/build/common/circle/Circle.mjs.map +1 -1
  23. package/build/common/hooks/useHasIntersected/useHasIntersected.js +6 -4
  24. package/build/common/hooks/useHasIntersected/useHasIntersected.js.map +1 -1
  25. package/build/common/hooks/useHasIntersected/useHasIntersected.mjs +6 -4
  26. package/build/common/hooks/useHasIntersected/useHasIntersected.mjs.map +1 -1
  27. package/build/common/liveRegion/LiveRegion.js +4 -1
  28. package/build/common/liveRegion/LiveRegion.js.map +1 -1
  29. package/build/common/liveRegion/LiveRegion.mjs +4 -1
  30. package/build/common/liveRegion/LiveRegion.mjs.map +1 -1
  31. package/build/dateInput/DateInput.js +10 -10
  32. package/build/dateInput/DateInput.js.map +1 -1
  33. package/build/dateInput/DateInput.mjs +10 -10
  34. package/build/dateInput/DateInput.mjs.map +1 -1
  35. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.js +1 -1
  36. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.js.map +1 -1
  37. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs +1 -1
  38. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs.map +1 -1
  39. package/build/dateLookup/yearCalendar/table/YearCalendarTable.js +1 -1
  40. package/build/dateLookup/yearCalendar/table/YearCalendarTable.js.map +1 -1
  41. package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs +1 -1
  42. package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs.map +1 -1
  43. package/build/expressiveMoneyInput/ExpressiveMoneyInput.js.map +1 -1
  44. package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs.map +1 -1
  45. package/build/expressiveMoneyInput/amountInput/AmountInput.js +18 -12
  46. package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -1
  47. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +19 -13
  48. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs.map +1 -1
  49. package/build/expressiveMoneyInput/hooks/useInputStyle.js +8 -6
  50. package/build/expressiveMoneyInput/hooks/useInputStyle.js.map +1 -1
  51. package/build/expressiveMoneyInput/hooks/useInputStyle.mjs +9 -7
  52. package/build/expressiveMoneyInput/hooks/useInputStyle.mjs.map +1 -1
  53. package/build/field/Field.js +63 -32
  54. package/build/field/Field.js.map +1 -1
  55. package/build/field/Field.messages.js +14 -0
  56. package/build/field/Field.messages.js.map +1 -0
  57. package/build/field/Field.messages.mjs +10 -0
  58. package/build/field/Field.messages.mjs.map +1 -0
  59. package/build/field/Field.mjs +65 -34
  60. package/build/field/Field.mjs.map +1 -1
  61. package/build/header/Header.js +1 -1
  62. package/build/header/Header.js.map +1 -1
  63. package/build/header/Header.mjs +1 -1
  64. package/build/header/Header.mjs.map +1 -1
  65. package/build/i18n/en.json +1 -0
  66. package/build/i18n/en.json.js +1 -0
  67. package/build/i18n/en.json.js.map +1 -1
  68. package/build/i18n/en.json.mjs +1 -0
  69. package/build/i18n/en.json.mjs.map +1 -1
  70. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.js.map +1 -1
  71. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.mjs.map +1 -1
  72. package/build/inputs/SelectInput/Options/SelectInputOptions.js +34 -22
  73. package/build/inputs/SelectInput/Options/SelectInputOptions.js.map +1 -1
  74. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs +35 -23
  75. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs.map +1 -1
  76. package/build/inputs/SelectInput/Popover/SelectInputPopover.js.map +1 -1
  77. package/build/inputs/SelectInput/Popover/SelectInputPopover.mjs.map +1 -1
  78. package/build/inputs/SelectInput/SelectInput.js +8 -6
  79. package/build/inputs/SelectInput/SelectInput.js.map +1 -1
  80. package/build/inputs/SelectInput/SelectInput.mjs +9 -7
  81. package/build/inputs/SelectInput/SelectInput.mjs.map +1 -1
  82. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
  83. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
  84. package/build/inputs/TextArea.js +5 -0
  85. package/build/inputs/TextArea.js.map +1 -1
  86. package/build/inputs/TextArea.mjs +6 -1
  87. package/build/inputs/TextArea.mjs.map +1 -1
  88. package/build/inputs/contexts.js +16 -0
  89. package/build/inputs/contexts.js.map +1 -1
  90. package/build/inputs/contexts.mjs +16 -2
  91. package/build/inputs/contexts.mjs.map +1 -1
  92. package/build/main.css +42 -8
  93. package/build/nudge/Nudge.js +31 -15
  94. package/build/nudge/Nudge.js.map +1 -1
  95. package/build/nudge/Nudge.mjs +32 -16
  96. package/build/nudge/Nudge.mjs.map +1 -1
  97. package/build/phoneNumberInput/PhoneNumberInput.js +9 -12
  98. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  99. package/build/phoneNumberInput/PhoneNumberInput.mjs +9 -12
  100. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  101. package/build/promoCard/PromoCardGroup.js +34 -16
  102. package/build/promoCard/PromoCardGroup.js.map +1 -1
  103. package/build/promoCard/PromoCardGroup.mjs +35 -17
  104. package/build/promoCard/PromoCardGroup.mjs.map +1 -1
  105. package/build/segmentedControl/SegmentedControl.js +6 -1
  106. package/build/segmentedControl/SegmentedControl.js.map +1 -1
  107. package/build/segmentedControl/SegmentedControl.mjs +7 -2
  108. package/build/segmentedControl/SegmentedControl.mjs.map +1 -1
  109. package/build/styles/avatarView/AvatarView.css +4 -4
  110. package/build/styles/avatarView/Dot.css +4 -4
  111. package/build/styles/css/neptune.css +15 -1
  112. package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
  113. package/build/styles/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
  114. package/build/styles/field/Field.css +19 -3
  115. package/build/styles/main.css +42 -8
  116. package/build/styles/styles/less/neptune.css +15 -1
  117. package/build/tabs/Tabs.js +1 -1
  118. package/build/tabs/Tabs.js.map +1 -1
  119. package/build/tabs/Tabs.mjs +1 -1
  120. package/build/tabs/Tabs.mjs.map +1 -1
  121. package/build/tooltip/Tooltip.js +6 -3
  122. package/build/tooltip/Tooltip.js.map +1 -1
  123. package/build/tooltip/Tooltip.mjs +6 -3
  124. package/build/tooltip/Tooltip.mjs.map +1 -1
  125. package/build/types/avatarView/AvatarView.d.ts +1 -1
  126. package/build/types/avatarView/AvatarView.d.ts.map +1 -1
  127. package/build/types/avatarView/Dot.d.ts.map +1 -1
  128. package/build/types/avatarWrapper/AvatarWrapper.d.ts.map +1 -1
  129. package/build/types/common/circle/Circle.d.ts +1 -1
  130. package/build/types/common/circle/Circle.d.ts.map +1 -1
  131. package/build/types/common/hooks/useHasIntersected/useHasIntersected.d.ts.map +1 -1
  132. package/build/types/common/liveRegion/LiveRegion.d.ts.map +1 -1
  133. package/build/types/dateLookup/monthCalendar/table/MonthCalendarTable.d.ts.map +1 -1
  134. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts.map +1 -1
  135. package/build/types/expressiveMoneyInput/amountInput/AmountInput.d.ts.map +1 -1
  136. package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts +2 -2
  137. package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts.map +1 -1
  138. package/build/types/expressiveMoneyInput/hooks/useSelectionRange.d.ts.map +1 -1
  139. package/build/types/field/Field.d.ts.map +1 -1
  140. package/build/types/field/Field.messages.d.ts +8 -0
  141. package/build/types/field/Field.messages.d.ts.map +1 -0
  142. package/build/types/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.d.ts.map +1 -1
  143. package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts.map +1 -1
  144. package/build/types/inputs/SelectInput/Popover/SelectInputPopover.d.ts.map +1 -1
  145. package/build/types/inputs/SelectInput/SelectInput.d.ts.map +1 -1
  146. package/build/types/inputs/TextArea.d.ts.map +1 -1
  147. package/build/types/inputs/contexts.d.ts +6 -0
  148. package/build/types/inputs/contexts.d.ts.map +1 -1
  149. package/build/types/nudge/Nudge.d.ts.map +1 -1
  150. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  151. package/build/types/promoCard/PromoCardGroup.d.ts.map +1 -1
  152. package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -1
  153. package/build/types/test-utils/index.d.ts +2 -0
  154. package/build/types/test-utils/index.d.ts.map +1 -1
  155. package/build/types/tooltip/Tooltip.d.ts.map +1 -1
  156. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  157. package/build/uploadInput/UploadInput.js +29 -25
  158. package/build/uploadInput/UploadInput.js.map +1 -1
  159. package/build/uploadInput/UploadInput.mjs +29 -25
  160. package/build/uploadInput/UploadInput.mjs.map +1 -1
  161. package/package.json +3 -3
  162. package/src/avatarLayout/AvatarLayout.story.tsx +1 -1
  163. package/src/avatarLayout/AvatarLayout.tsx +4 -0
  164. package/src/avatarView/AvatarView.css +4 -4
  165. package/src/avatarView/AvatarView.story.tsx +17 -13
  166. package/src/avatarView/AvatarView.tsx +5 -1
  167. package/src/avatarView/Dot.css +4 -4
  168. package/src/avatarView/Dot.less +6 -6
  169. package/src/avatarView/Dot.tsx +2 -0
  170. package/src/avatarWrapper/AvatarWrapper.test.tsx +33 -3
  171. package/src/avatarWrapper/AvatarWrapper.tsx +5 -6
  172. package/src/button/LegacyButton.tsx +1 -1
  173. package/src/button/_stories/Button.test.story.tsx +3 -3
  174. package/src/common/circle/Circle.tsx +5 -1
  175. package/src/common/hooks/useContainerSize.test.tsx +1 -1
  176. package/src/common/hooks/useHasIntersected/useHasIntersected.ts +12 -4
  177. package/src/common/liveRegion/LiveRegion.tsx +5 -2
  178. package/src/dateInput/DateInput.tsx +10 -10
  179. package/src/dateLookup/monthCalendar/table/MonthCalendarTable.tsx +1 -5
  180. package/src/dateLookup/yearCalendar/table/YearCalendarTable.tsx +1 -1
  181. package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
  182. package/src/expressiveMoneyInput/ExpressiveMoneyInput.test.story.tsx +43 -0
  183. package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +1 -1
  184. package/src/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
  185. package/src/expressiveMoneyInput/amountInput/AmountInput.less +2 -0
  186. package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +23 -16
  187. package/src/expressiveMoneyInput/hooks/useInputStyle.ts +20 -8
  188. package/src/expressiveMoneyInput/hooks/useSelectionRange.ts +2 -0
  189. package/src/field/Field.css +19 -3
  190. package/src/field/Field.less +17 -3
  191. package/src/field/Field.messages.ts +8 -0
  192. package/src/field/Field.story.tsx +5 -1
  193. package/src/field/Field.test.tsx +90 -0
  194. package/src/field/Field.tsx +84 -37
  195. package/src/header/Header.tsx +2 -2
  196. package/src/i18n/en.json +1 -0
  197. package/src/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.tsx +4 -0
  198. package/src/inputs/SelectInput/Options/SelectInputOptions.tsx +43 -27
  199. package/src/inputs/SelectInput/Popover/SelectInputPopover.tsx +4 -0
  200. package/src/inputs/SelectInput/SelectInput.tsx +21 -15
  201. package/src/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +1 -1
  202. package/src/inputs/TextArea.story.tsx +97 -0
  203. package/src/inputs/TextArea.test.story.tsx +142 -0
  204. package/src/inputs/TextArea.tsx +7 -2
  205. package/src/inputs/contexts.tsx +18 -1
  206. package/src/main.css +42 -8
  207. package/src/nudge/Nudge.tsx +29 -20
  208. package/src/phoneNumberInput/PhoneNumberInput.test.tsx +16 -0
  209. package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -13
  210. package/src/promoCard/PromoCard.story.tsx +3 -3
  211. package/src/promoCard/PromoCardGroup.tsx +39 -21
  212. package/src/segmentedControl/SegmentedControl.test.tsx +25 -0
  213. package/src/segmentedControl/SegmentedControl.tsx +7 -1
  214. package/src/select/Select.story.tsx +1 -1
  215. package/src/styles/less/core/_typography.less +28 -6
  216. package/src/styles/less/neptune.css +15 -1
  217. package/src/tabs/Tabs.tsx +1 -1
  218. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +1 -0
  219. package/src/tooltip/Tooltip.tsx +3 -0
  220. package/src/uploadInput/UploadInput.test.tsx +19 -0
  221. package/src/uploadInput/UploadInput.tsx +28 -24
@@ -134,10 +134,10 @@ const DateInput = ({
134
134
  const monthBeforeDay = MDY.has(locale);
135
135
  const yearFirst = YMD.has(locale);
136
136
 
137
- dayLabel ||= formatMessage(messages.dayLabel);
138
- monthLabel ||= formatMessage(messages.monthLabel);
139
- yearLabel ||= formatMessage(messages.yearLabel);
140
- placeholders = {
137
+ const resolvedDayLabel = dayLabel || formatMessage(messages.dayLabel);
138
+ const resolvedMonthLabel = monthLabel || formatMessage(messages.monthLabel);
139
+ const resolvedYearLabel = yearLabel || formatMessage(messages.yearLabel);
140
+ const resolvedPlaceholders = {
141
141
  day: placeholders?.day || formatMessage(messages.dayPlaceholder),
142
142
  month: placeholders?.month || formatMessage(messages.monthLabel),
143
143
  year: placeholders?.year || formatMessage(messages.yearPlaceholder),
@@ -186,14 +186,14 @@ const DateInput = ({
186
186
  const getSelectElement = () => {
187
187
  return (
188
188
  <label className="d-flex flex-column">
189
- <Body type={Typography.BODY_DEFAULT}>{monthLabel}</Body>
189
+ <Body type={Typography.BODY_DEFAULT}>{resolvedMonthLabel}</Body>
190
190
  <SelectInput
191
191
  triggerRef={monthRef}
192
192
  id={`${id}:month`}
193
193
  parentId={DATE_INPUT_PARENT_ID}
194
194
  name="month"
195
195
  disabled={disabled}
196
- placeholder={placeholders?.month}
196
+ placeholder={resolvedPlaceholders.month}
197
197
  items={Array.from({ length: 12 }, (_, index) => ({ type: 'option', value: index }))}
198
198
  size={size}
199
199
  value={month}
@@ -295,7 +295,7 @@ const DateInput = ({
295
295
  return (
296
296
  <div className="col-sm-3 tw-date--day">
297
297
  <label>
298
- <Body type={Typography.BODY_DEFAULT}>{dayLabel}</Body>
298
+ <Body type={Typography.BODY_DEFAULT}>{resolvedDayLabel}</Body>
299
299
  <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
300
300
  <Input
301
301
  ref={dayRef}
@@ -306,7 +306,7 @@ const DateInput = ({
306
306
  name="day"
307
307
  autoComplete={dayAutoComplete}
308
308
  value={displayDay || ''}
309
- placeholder={placeholders?.day}
309
+ placeholder={resolvedPlaceholders.day}
310
310
  disabled={disabled}
311
311
  min={1}
312
312
  max={31}
@@ -323,7 +323,7 @@ const DateInput = ({
323
323
  return (
324
324
  <div className="col-sm-4 tw-date--year">
325
325
  <label>
326
- <Body type={Typography.BODY_DEFAULT}>{yearLabel}</Body>
326
+ <Body type={Typography.BODY_DEFAULT}>{resolvedYearLabel}</Body>
327
327
  <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
328
328
  <Input
329
329
  ref={yearRef}
@@ -333,7 +333,7 @@ const DateInput = ({
333
333
  pattern="[0-9]*"
334
334
  name="year"
335
335
  autoComplete={yearAutoComplete}
336
- placeholder={placeholders?.year}
336
+ placeholder={resolvedPlaceholders.year}
337
337
  value={displayYear || ''}
338
338
  disabled={disabled}
339
339
  min={0}
@@ -43,11 +43,7 @@ const MonthCalendarTable = ({
43
43
  };
44
44
 
45
45
  const isActive = (month: number) => {
46
- return !!(
47
- selectedDate &&
48
- month === selectedDate.getMonth() &&
49
- viewYear === selectedDate.getFullYear()
50
- );
46
+ return !!(month === selectedDate?.getMonth() && viewYear === selectedDate?.getFullYear());
51
47
  };
52
48
 
53
49
  const isThisMonth = (month: number) => {
@@ -43,7 +43,7 @@ const YearCalendarTable = ({
43
43
  };
44
44
 
45
45
  const isActive = (year: number) => {
46
- return !!(selectedDate && year === selectedDate.getFullYear());
46
+ return !!(year === selectedDate?.getFullYear());
47
47
  };
48
48
 
49
49
  const isThisYear = (year: number) => {
@@ -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
+ );
@@ -72,7 +72,7 @@ export type Props = {
72
72
  export default function ExpressiveMoneyInput({
73
73
  label,
74
74
  currency,
75
- currencySelector = { options: [] } as DefaultCurrencySelectorInstanceType,
75
+ currencySelector = { options: [] },
76
76
  amount,
77
77
  onAmountChange,
78
78
  className,
@@ -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
  }
@@ -42,7 +42,7 @@ export const AmountInput = ({
42
42
  const intl = useIntl();
43
43
  const { focus, setFocus, visualFocus, setVisualFocus } = useFocus();
44
44
 
45
- const [value, setValue] = useState<string>(
45
+ const [value, setValue] = useState<string>(() =>
46
46
  amount
47
47
  ? getFormattedString({
48
48
  value: amount,
@@ -51,6 +51,7 @@ export const AmountInput = ({
51
51
  })
52
52
  : '',
53
53
  );
54
+ const prevAmountRef = useRef<string | number | null | undefined>(amount);
54
55
  const numericValue = useMemo(() => {
55
56
  return getUnformattedNumber({
56
57
  value,
@@ -85,23 +86,28 @@ export const AmountInput = ({
85
86
  const decimalMode = decimalSeparator && value.includes(decimalSeparator);
86
87
 
87
88
  useEffect(() => {
88
- if (!focus) {
89
- setValue(
90
- amount
91
- ? getFormattedString({
92
- value: amount,
93
- currency,
94
- locale: intl.locale,
95
- })
96
- : '',
97
- );
89
+ if (prevAmountRef.current !== amount) {
90
+ prevAmountRef.current = amount;
91
+
92
+ // Only update the displayed value if not focused (preserve user input when focused)
93
+ if (!focus) {
94
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing external prop to internal state when unfocused
95
+ setValue(
96
+ amount
97
+ ? getFormattedString({
98
+ value: amount,
99
+ currency,
100
+ locale: intl.locale,
101
+ })
102
+ : '',
103
+ );
104
+ }
98
105
  }
99
- // eslint-disable-next-line react-hooks/exhaustive-deps
100
- }, [amount]);
106
+ }, [amount, focus, currency, intl.locale]);
101
107
 
102
108
  useEffect(() => {
103
109
  onFocusChange?.(visualFocus);
104
- }, [visualFocus]);
110
+ }, [onFocusChange, visualFocus]);
105
111
 
106
112
  const shouldReformatAfterUserInput = (newValue: string) => {
107
113
  // don't reformat if formatting would wipe out user's input
@@ -246,6 +252,7 @@ export const AmountInput = ({
246
252
  const addonContent = useMemo((): string | null | undefined => {
247
253
  // because we're using a separate "addon" element for the placeholder decimals, there is a possibility that the input itself will become scrollable
248
254
  // and the decimals will appear on top of the input. Safest thing to do is to just hide the addon if there is not enough room
255
+ // eslint-disable-next-line react-hooks/refs -- Reading layout dimensions for overflow detection
249
256
  if (isInputPossiblyOverflowing({ ref, value })) {
250
257
  return null;
251
258
  }
@@ -284,14 +291,14 @@ export const AmountInput = ({
284
291
  // whenever decimals are shown, we need to account for the full decimal part for the font size calculation
285
292
  value: addonContent ? valueWithFullDecimals : value,
286
293
  focus: visualFocus,
287
- inputElement: ref.current,
294
+ inputElement: ref,
288
295
  loading,
289
296
  });
290
297
 
291
298
  return (
292
299
  <div className="wds-amount-input-container">
293
300
  <div
294
- className={clsx('wds-amount-input-input-container', 'np-text-display-large')}
301
+ className={clsx('wds-amount-input-input-container', 'np-text-display-large--forced')}
295
302
  style={style}
296
303
  >
297
304
  <input
@@ -1,17 +1,27 @@
1
- import { type CSSProperties, useEffect, useLayoutEffect, useState } from 'react';
1
+ import {
2
+ type CSSProperties,
3
+ type RefObject,
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useState,
8
+ } from 'react';
2
9
  import { Props as ExpressiveMoneyInputProps } from '../ExpressiveMoneyInput';
3
10
 
4
11
  type InputStyleObject = {
5
12
  value: string;
6
13
  focus: boolean;
7
- inputElement: HTMLInputElement | null;
14
+ inputElement: RefObject<HTMLInputElement> | null;
8
15
  } & Pick<ExpressiveMoneyInputProps, 'loading'>;
9
16
 
10
17
  export const useInputStyle = ({ value, focus, inputElement, loading }: InputStyleObject) => {
11
18
  const initialRender = !useTimeout(300);
12
- const inputWidth = useFirstDefinedValue(inputElement?.clientWidth, [inputElement, value]);
19
+ const inputWidth = useFirstDefinedValue(inputElement?.current?.clientWidth, [
20
+ inputElement?.current,
21
+ value,
22
+ ]);
13
23
 
14
- const getStyle = (): CSSProperties => {
24
+ const getStyle = useCallback((): CSSProperties => {
15
25
  const fontSize = getFontSize(value, focus, inputWidth);
16
26
 
17
27
  return {
@@ -23,13 +33,14 @@ export const useInputStyle = ({ value, focus, inputElement, loading }: InputStyl
23
33
  transition: initialRender ? 'none' : undefined,
24
34
  color: loading ? 'var(--color-interactive-secondary)' : undefined,
25
35
  };
26
- };
36
+ }, [value, focus, inputWidth, loading, initialRender]);
27
37
 
28
- const [style, setStyle] = useState(getStyle());
38
+ const [style, setStyle] = useState<CSSProperties>(() => getStyle());
29
39
 
30
40
  useLayoutEffect(() => {
41
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Computing style based on layout measurements
31
42
  setStyle(getStyle());
32
- }, [value, focus, loading, inputWidth]);
43
+ }, [value, focus, loading, inputWidth, getStyle]);
33
44
 
34
45
  return style;
35
46
  };
@@ -60,10 +71,11 @@ const useFirstDefinedValue = (newValue: number | undefined, dependencies: unknow
60
71
 
61
72
  useLayoutEffect(() => {
62
73
  if (typeof newValue !== 'undefined' && typeof value === 'undefined') {
74
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Lazy initialization from prop
63
75
  setValue(newValue);
64
76
  }
65
77
  // eslint-disable-next-line react-hooks/exhaustive-deps
66
- }, [...dependencies, value]);
78
+ }, [...dependencies, value, newValue]);
67
79
 
68
80
  return value;
69
81
  };
@@ -15,9 +15,11 @@ export const useSelectionRange = () => {
15
15
  selection.current = undefined;
16
16
  };
17
17
 
18
+ /* eslint-disable react-hooks/refs */
18
19
  return {
19
20
  selection: selection.current,
20
21
  handleSelect,
21
22
  handleSelectionBlur,
22
23
  };
24
+ /* eslint-enable react-hooks/refs */
23
25
  };
@@ -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
  });