@transferwise/components 46.6.0 → 46.7.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 (133) hide show
  1. package/build/index.esm.js +186 -342
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +185 -341
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +6 -17
  6. package/build/styles/inputs/Input.css +0 -4
  7. package/build/styles/inputs/SelectInput.css +6 -1
  8. package/build/styles/inputs/TextArea.css +0 -4
  9. package/build/styles/main.css +6 -17
  10. package/build/styles/select/Select.css +0 -4
  11. package/build/types/common/locale/index.d.ts +26 -43
  12. package/build/types/common/locale/index.d.ts.map +1 -1
  13. package/build/types/index.d.ts +1 -0
  14. package/build/types/index.d.ts.map +1 -1
  15. package/build/types/inputs/SelectInput.d.ts +6 -5
  16. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  17. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +22 -27
  18. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  19. package/build/types/phoneNumberInput/data/countries.d.ts +5 -10
  20. package/build/types/phoneNumberInput/data/countries.d.ts.map +1 -1
  21. package/build/types/phoneNumberInput/index.d.ts +1 -1
  22. package/build/types/phoneNumberInput/index.d.ts.map +1 -1
  23. package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts +1 -1
  24. package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts.map +1 -1
  25. package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts +1 -1
  26. package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts.map +1 -1
  27. package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts +8 -1
  28. package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts.map +1 -1
  29. package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts +1 -1
  30. package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts.map +1 -1
  31. package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts +8 -4
  32. package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts.map +1 -1
  33. package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts +1 -1
  34. package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts.map +1 -1
  35. package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts +1 -1
  36. package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts.map +1 -1
  37. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts +2 -1
  38. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts.map +1 -1
  39. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts +1 -1
  40. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts.map +1 -1
  41. package/build/types/phoneNumberInput/utils/index.d.ts +11 -13
  42. package/build/types/phoneNumberInput/utils/index.d.ts.map +1 -1
  43. package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts +1 -1
  44. package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts.map +1 -1
  45. package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts +1 -1
  46. package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts.map +1 -1
  47. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts +1 -1
  48. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts.map +1 -1
  49. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts +6 -1
  50. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts.map +1 -1
  51. package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts +2 -1
  52. package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts.map +1 -1
  53. package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts +7 -1
  54. package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts.map +1 -1
  55. package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts +1 -1
  56. package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts.map +1 -1
  57. package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts +1 -1
  58. package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts.map +1 -1
  59. package/package.json +3 -4
  60. package/src/common/locale/{index.spec.js → index.spec.ts} +4 -4
  61. package/src/common/locale/index.ts +96 -0
  62. package/src/index.ts +1 -0
  63. package/src/inputs/Input.css +0 -4
  64. package/src/inputs/SelectInput.css +6 -1
  65. package/src/inputs/SelectInput.less +8 -1
  66. package/src/inputs/SelectInput.spec.tsx +26 -0
  67. package/src/inputs/SelectInput.story.tsx +73 -1
  68. package/src/inputs/SelectInput.tsx +104 -85
  69. package/src/inputs/TextArea.css +0 -4
  70. package/src/main.css +6 -17
  71. package/src/phoneNumberInput/PhoneNumberInput.spec.js +18 -22
  72. package/src/phoneNumberInput/PhoneNumberInput.tsx +193 -0
  73. package/src/phoneNumberInput/data/{countries.js → countries.ts} +9 -1
  74. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.ts +3 -0
  75. package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.spec.js → excludeCountries.spec.ts} +1 -1
  76. package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.js → excludeCountries.ts} +6 -5
  77. package/src/phoneNumberInput/utils/explodeNumberModel/{explodeNumberModel.spec.js → explodeNumberModel.spec.ts} +1 -1
  78. package/src/phoneNumberInput/utils/explodeNumberModel/index.ts +24 -0
  79. package/src/phoneNumberInput/utils/findCountryByCode/{findCountryByCode.spec.js → findCountryByCode.spec.ts} +0 -1
  80. package/src/phoneNumberInput/utils/findCountryByCode/index.ts +12 -0
  81. package/src/phoneNumberInput/utils/findCountryByPrefix/index.ts +12 -0
  82. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.ts +102 -0
  83. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.ts +12 -0
  84. package/src/phoneNumberInput/utils/{index.js → index.ts} +0 -2
  85. package/src/phoneNumberInput/utils/isStringNumeric/{isStringNumeric.spec.js → isStringNumeric.spec.ts} +0 -1
  86. package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.ts +1 -0
  87. package/src/phoneNumberInput/utils/isValidPhoneNumber/{isValidPhoneNumber.spec.js → isValidPhoneNumber.spec.ts} +1 -1
  88. package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.ts +7 -0
  89. package/src/phoneNumberInput/utils/longestMatchingPrefix/index.ts +4 -0
  90. package/src/phoneNumberInput/utils/setDefaultPrefix/index.ts +20 -0
  91. package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.ts +6 -0
  92. package/src/select/Select.css +0 -4
  93. package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts +0 -2
  94. package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts.map +0 -1
  95. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts +0 -2
  96. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts.map +0 -1
  97. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts +0 -3
  98. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts.map +0 -1
  99. package/build/types/utilities/wrapInFragment.d.ts +0 -3
  100. package/build/types/utilities/wrapInFragment.d.ts.map +0 -1
  101. package/src/common/locale/index.js +0 -139
  102. package/src/phoneNumberInput/PhoneNumberInput.js +0 -210
  103. package/src/phoneNumberInput/data/countries.spec.js +0 -12
  104. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.js +0 -4
  105. package/src/phoneNumberInput/utils/explodeNumberModel/index.js +0 -27
  106. package/src/phoneNumberInput/utils/filterOptionsForQuery/filterOptionsForQuery.spec.js +0 -36
  107. package/src/phoneNumberInput/utils/filterOptionsForQuery/index.js +0 -11
  108. package/src/phoneNumberInput/utils/findCountryByCode/index.js +0 -10
  109. package/src/phoneNumberInput/utils/findCountryByPrefix/index.js +0 -11
  110. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.js +0 -26
  111. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.js +0 -67
  112. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/index.js +0 -1
  113. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.js +0 -25
  114. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.spec.js +0 -66
  115. package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.js +0 -1
  116. package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js +0 -10
  117. package/src/phoneNumberInput/utils/longestMatchingPrefix/index.js +0 -2
  118. package/src/phoneNumberInput/utils/setDefaultPrefix/index.js +0 -25
  119. package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.js +0 -3
  120. package/src/utilities/wrapInFragment.tsx +0 -3
  121. /package/src/phoneNumberInput/{PhoneNumberInput.story.js → PhoneNumberInput.story.tsx} +0 -0
  122. /package/src/phoneNumberInput/{index.js → index.ts} +0 -0
  123. /package/src/phoneNumberInput/utils/cleanNumber/{cleanNumber.spec.js → cleanNumber.spec.ts} +0 -0
  124. /package/src/phoneNumberInput/utils/cleanNumber/{index.js → index.ts} +0 -0
  125. /package/src/phoneNumberInput/utils/excludeCountries/{index.js → index.ts} +0 -0
  126. /package/src/phoneNumberInput/utils/findCountryByPrefix/{findCountryByPrefix.spec.js → findCountryByPrefix.spec.ts} +0 -0
  127. /package/src/phoneNumberInput/utils/groupCountriesByPrefix/{index.js → index.ts} +0 -0
  128. /package/src/phoneNumberInput/utils/isStringNumeric/{index.js → index.ts} +0 -0
  129. /package/src/phoneNumberInput/utils/isValidPhoneNumber/{index.js → index.ts} +0 -0
  130. /package/src/phoneNumberInput/utils/longestMatchingPrefix/{longestMatchingPrefix.spec.js → longestMatchingPrefix.spec.ts} +0 -0
  131. /package/src/phoneNumberInput/utils/setDefaultPrefix/{setDefaultPrefix.spec.js → setDefaultPrefix.spec.ts} +0 -0
  132. /package/src/phoneNumberInput/utils/sortArrayByProperty/{index.js → index.ts} +0 -0
  133. /package/src/phoneNumberInput/utils/sortArrayByProperty/{sortArrayByProperty.spec.js → sortArrayByProperty.spec.ts} +0 -0
@@ -12,7 +12,6 @@ import { useScreenSize } from '../common/hooks/useScreenSize';
12
12
  import { PolymorphicWithOverrides } from '../common/polymorphicWithOverrides/PolymorphicWithOverrides';
13
13
  import { Breakpoint } from '../common/propsValues/breakpoint';
14
14
  import dateTriggerMessages from '../dateLookup/dateTrigger/DateTrigger.messages';
15
- import { wrapInFragment } from '../utilities/wrapInFragment';
16
15
  import { Merge } from '../utils';
17
16
 
18
17
  import { InputGroup } from './InputGroup';
@@ -125,13 +124,13 @@ function filterSelectInputItems<T>(items: readonly SelectInputItem<T>[], needle:
125
124
  });
126
125
  }
127
126
 
128
- export interface SelectInputProps<T = string> {
127
+ export interface SelectInputProps<T = string, M extends boolean = false> {
129
128
  name?: string;
129
+ multiple?: M;
130
130
  placeholder?: string;
131
- // TODO: multiple?: boolean;
132
131
  items: readonly SelectInputItem<NonNullable<T>>[];
133
- defaultValue?: T;
134
- value?: T;
132
+ defaultValue?: M extends true ? readonly T[] : T;
133
+ value?: M extends true ? readonly T[] : T;
135
134
  compareValues?:
136
135
  | (keyof NonNullable<T> & string)
137
136
  | ((a: T | undefined, b: T | undefined) => boolean);
@@ -154,7 +153,7 @@ export interface SelectInputProps<T = string> {
154
153
  size?: 'sm' | 'md' | 'lg';
155
154
  className?: string;
156
155
  onFilterChange?: (args: { query: string; queryNormalized: string | null }) => void;
157
- onChange?: (value: T) => void;
156
+ onChange?: (value: M extends true ? T[] : T) => void;
158
157
  onClear?: () => void;
159
158
  }
160
159
 
@@ -186,7 +185,14 @@ const defaultRenderTrigger = (({ content, placeholderShown, clear, disabled, siz
186
185
  className={className}
187
186
  >
188
187
  <SelectInputTriggerButton as={ButtonInput} size={size}>
189
- {placeholderShown ? <span className="np-select-input-placeholder"> {content}</span> : content}
188
+ <span
189
+ className={classNames(
190
+ 'np-select-input-content',
191
+ placeholderShown && 'np-select-input-placeholder',
192
+ )}
193
+ >
194
+ {content}
195
+ </span>
190
196
  </SelectInputTriggerButton>
191
197
  </InputGroup>
192
198
  )) satisfies SelectInputProps['renderTrigger'];
@@ -211,14 +217,15 @@ function SelectInputClearButton({ className, onClick }: SelectInputClearButtonPr
211
217
 
212
218
  const noop = () => {};
213
219
 
214
- export function SelectInput<T = string>({
220
+ export function SelectInput<T = string, M extends boolean = false>({
215
221
  name,
222
+ multiple,
216
223
  placeholder,
217
224
  items,
218
225
  defaultValue,
219
226
  value: controlledValue,
220
227
  compareValues,
221
- renderValue = wrapInFragment,
228
+ renderValue = String,
222
229
  renderFooter,
223
230
  renderTrigger = defaultRenderTrigger,
224
231
  filterable,
@@ -229,7 +236,7 @@ export function SelectInput<T = string>({
229
236
  onFilterChange = noop,
230
237
  onChange,
231
238
  onClear,
232
- }: SelectInputProps<T>) {
239
+ }: SelectInputProps<T, M>) {
233
240
  const [open, setOpen] = useState(false);
234
241
 
235
242
  const [filterQuery, _setFilterQuery] = useState('');
@@ -253,94 +260,106 @@ export function SelectInput<T = string>({
253
260
  return (
254
261
  <ListboxBase
255
262
  name={name}
263
+ multiple={multiple}
256
264
  defaultValue={defaultValue}
257
265
  value={controlledValue}
258
266
  // TODO: Remove assertion when upgrading TypeScript to v5
259
267
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
260
268
  by={compareValues as any}
261
269
  disabled={disabled}
262
- onChange={(value) => {
263
- setOpen(false);
264
- onChange?.(value);
265
- }}
270
+ onChange={
271
+ ((value) => {
272
+ if (!multiple) {
273
+ setOpen(false);
274
+ }
275
+ onChange?.(value);
276
+ }) satisfies SelectInputProps<T, M>['onChange']
277
+ }
266
278
  >
267
- {({ disabled: uiDisabled, value }) => (
268
- <OptionsOverlay
269
- placement="bottom-start"
270
- open={open}
271
- renderTrigger={({ ref, getInteractionProps }) => (
272
- <SelectInputTriggerButtonPropsContext.Provider
273
- // eslint-disable-next-line react/jsx-no-constructed-context-values
274
- value={{
275
- ref: mergeRefs([ref, triggerRef]),
276
- ...mergeProps(
277
- {
278
- onClick: () => {
279
- setOpen((prev) => !prev);
280
- },
281
- onKeyDown: (event: React.KeyboardEvent) => {
282
- if (
283
- event.key === ' ' ||
284
- event.key === 'Enter' ||
285
- event.key === 'ArrowDown' ||
286
- event.key === 'ArrowUp'
287
- ) {
279
+ {({ disabled: uiDisabled, value }) => {
280
+ const placeholderShown =
281
+ multiple && Array.isArray(value) ? value.length === 0 : value == null;
282
+ return (
283
+ <OptionsOverlay
284
+ placement="bottom-start"
285
+ open={open}
286
+ renderTrigger={({ ref, getInteractionProps }) => (
287
+ <SelectInputTriggerButtonPropsContext.Provider
288
+ // eslint-disable-next-line react/jsx-no-constructed-context-values
289
+ value={{
290
+ ref: mergeRefs([ref, triggerRef]),
291
+ ...mergeProps(
292
+ {
293
+ onClick: () => {
288
294
  setOpen((prev) => !prev);
289
- }
295
+ },
296
+ onKeyDown: (event: React.KeyboardEvent) => {
297
+ if (
298
+ event.key === ' ' ||
299
+ event.key === 'Enter' ||
300
+ event.key === 'ArrowDown' ||
301
+ event.key === 'ArrowUp'
302
+ ) {
303
+ setOpen((prev) => !prev);
304
+ }
305
+ },
290
306
  },
291
- },
292
- getInteractionProps(),
293
- ),
294
- }}
295
- >
296
- {renderTrigger({
297
- content:
298
- value != null ? (
307
+ getInteractionProps(),
308
+ ),
309
+ }}
310
+ >
311
+ {renderTrigger({
312
+ content: !placeholderShown ? (
299
313
  <SelectInputOptionContentWithinTriggerContext.Provider value>
300
- {renderValue(value, true)}
314
+ {multiple && Array.isArray(value)
315
+ ? value
316
+ .map((option: NonNullable<T>) => renderValue(option, true))
317
+ .join(', ')
318
+ : renderValue(value as NonNullable<T>, true)}
301
319
  </SelectInputOptionContentWithinTriggerContext.Provider>
302
320
  ) : (
303
321
  placeholder
304
322
  ),
305
- placeholderShown: value == null,
306
- clear:
307
- onClear != null
308
- ? () => {
309
- onClear();
310
- triggerRef.current?.focus({ preventScroll: true });
311
- }
312
- : undefined,
313
- disabled: uiDisabled,
314
- size,
315
- className,
316
- })}
317
- </SelectInputTriggerButtonPropsContext.Provider>
318
- )}
319
- initialFocusRef={controllerRef}
320
- size={filterable ? 'lg' : 'md'}
321
- padding="none"
322
- onClose={() => {
323
- setOpen(false);
324
- }}
325
- onCloseEnd={() => {
326
- if (filterQuery !== '') {
327
- setFilterQuery('');
328
- }
329
- }}
330
- >
331
- <SelectInputOptions
332
- items={items}
333
- renderValue={renderValue}
334
- renderFooter={renderFooter}
335
- filterable={filterable}
336
- filterPlaceholder={filterPlaceholder}
337
- searchInputRef={searchInputRef}
338
- listboxRef={listboxRef}
339
- filterQuery={filterQuery}
340
- onFilterChange={setFilterQuery}
341
- />
342
- </OptionsOverlay>
343
- )}
323
+ placeholderShown,
324
+ clear:
325
+ onClear != null
326
+ ? () => {
327
+ onClear();
328
+ triggerRef.current?.focus({ preventScroll: true });
329
+ }
330
+ : undefined,
331
+ disabled: uiDisabled,
332
+ size,
333
+ className,
334
+ })}
335
+ </SelectInputTriggerButtonPropsContext.Provider>
336
+ )}
337
+ initialFocusRef={controllerRef}
338
+ size={filterable ? 'lg' : 'md'}
339
+ padding="none"
340
+ onClose={() => {
341
+ setOpen(false);
342
+ }}
343
+ onCloseEnd={() => {
344
+ if (filterQuery !== '') {
345
+ setFilterQuery('');
346
+ }
347
+ }}
348
+ >
349
+ <SelectInputOptions
350
+ items={items}
351
+ renderValue={renderValue}
352
+ renderFooter={renderFooter}
353
+ filterable={filterable}
354
+ filterPlaceholder={filterPlaceholder}
355
+ searchInputRef={searchInputRef}
356
+ listboxRef={listboxRef}
357
+ filterQuery={filterQuery}
358
+ onFilterChange={setFilterQuery}
359
+ />
360
+ </OptionsOverlay>
361
+ );
362
+ }}
344
363
  </ListboxBase>
345
364
  );
346
365
  }
@@ -430,7 +449,7 @@ interface SelectInputOptionsProps<T = string>
430
449
 
431
450
  function SelectInputOptions<T = string>({
432
451
  items,
433
- renderValue = wrapInFragment,
452
+ renderValue = String,
434
453
  renderFooter,
435
454
  filterable = false,
436
455
  filterPlaceholder,
@@ -47,10 +47,6 @@
47
47
  padding-top: 0 !important;
48
48
  padding-bottom: 0 !important;
49
49
  }.np-form-control--size-sm {
50
- line-height: 1.5;
51
- line-height: var(--line-height-body);
52
- font-size: 1rem;
53
- font-size: var(--font-size-16);
54
50
  font-size: 0.875rem;
55
51
  font-size: var(--font-size-14);
56
52
  line-height: 155%;
package/src/main.css CHANGED
@@ -74,14 +74,6 @@ div.critical-comms .critical-comms-body {
74
74
  flex-wrap: wrap;
75
75
  }
76
76
  }
77
- .tw-date-lookup-calendar > tbody > tr > td.weekend button {
78
- font-weight: 400;
79
- font-weight: var(--font-weight-regular);line-height: 1.5;line-height: var(--line-height-body);
80
- }
81
- .tw-date-lookup-calendar > tbody > tr > td.weekend button {
82
- font-size: 1rem;
83
- font-size: var(--font-size-16);
84
- }
85
77
  .tw-date-lookup-calendar > tbody > tr > td.weekend button {
86
78
  font-size: 0.875rem;
87
79
  font-size: var(--font-size-14);line-height: 155%;letter-spacing: -0.006em;font-weight: 400;font-weight: var(--font-weight-regular);
@@ -2226,10 +2218,6 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2226
2218
  padding-bottom: 0 !important;
2227
2219
  }
2228
2220
  .np-form-control--size-sm {
2229
- line-height: 1.5;
2230
- line-height: var(--line-height-body);
2231
- font-size: 1rem;
2232
- font-size: var(--font-size-16);
2233
2221
  font-size: 0.875rem;
2234
2222
  font-size: var(--font-size-14);
2235
2223
  line-height: 155%;
@@ -2511,10 +2499,12 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2511
2499
  color: #5d7079;
2512
2500
  color: var(--color-content-secondary);
2513
2501
  }
2514
- .np-select-input-placeholder {
2502
+ .np-select-input-content {
2515
2503
  overflow: hidden;
2516
2504
  text-overflow: ellipsis;
2517
2505
  white-space: nowrap;
2506
+ }
2507
+ .np-select-input-placeholder {
2518
2508
  color: #768e9c;
2519
2509
  color: var(--color-content-tertiary);
2520
2510
  }
@@ -2622,6 +2612,9 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2622
2612
  padding: var(--size-12) var(--size-16);
2623
2613
  color: var(--color-interactive-primary);
2624
2614
  }
2615
+ .np-select-input-option-container:focus {
2616
+ outline: none;
2617
+ }
2625
2618
  .np-select-input-option-container--active {
2626
2619
  box-shadow: inset 0 0 0 1px #c9cbce;
2627
2620
  box-shadow: inset 0 0 0 1px var(--color-interactive-secondary);
@@ -4455,10 +4448,6 @@ html:not([dir="rtl"]) .np-navigation-option {
4455
4448
  border-radius: var(--radius-small);
4456
4449
  color: #37517e;
4457
4450
  color: var(--color-content-primary);
4458
- line-height: 1.5;
4459
- line-height: var(--line-height-body);
4460
- font-size: 1rem;
4461
- font-size: var(--font-size-16);
4462
4451
  font-size: 0.875rem;
4463
4452
  font-size: var(--font-size-14);
4464
4453
  line-height: 155%;
@@ -37,11 +37,7 @@ describe('Given a telephone number component', () => {
37
37
  });
38
38
 
39
39
  it('should set prefix control to default UK value', () => {
40
- expect(select.props().value).toStrictEqual({
41
- value: '+44',
42
- note: 'GBR, GGY, IMN, JEY',
43
- label: '+44',
44
- });
40
+ expect(select.props().value).toBe('+44');
45
41
  });
46
42
 
47
43
  it('should set number control to empty', () => {
@@ -49,11 +45,11 @@ describe('Given a telephone number component', () => {
49
45
  });
50
46
 
51
47
  it('should not disable the select', () => {
52
- expect(select.prop('disabled')).toBe(false);
48
+ expect(select.prop('disabled')).toBeFalsy();
53
49
  });
54
50
 
55
51
  it('should not disable the input', () => {
56
- expect(input.prop('disabled')).toBe(false);
52
+ expect(input.prop('disabled')).toBeFalsy();
57
53
  });
58
54
  });
59
55
 
@@ -65,7 +61,7 @@ describe('Given a telephone number component', () => {
65
61
  });
66
62
 
67
63
  it('should set control values correctly', () => {
68
- expect(select.props().value.value).toStrictEqual('+39');
64
+ expect(select.props().value).toBe('+39');
69
65
  expect(input.prop('value')).toBe('123456789');
70
66
  });
71
67
  });
@@ -92,8 +88,8 @@ describe('Given a telephone number component', () => {
92
88
  input = component.find(NUMBER_SELECTOR);
93
89
  });
94
90
 
95
- it('should render input with null id', () => {
96
- expect(input.prop('id')).toBeNull();
91
+ it('should render input with unspecified id', () => {
92
+ expect(input.prop('id')).toBeUndefined();
97
93
  });
98
94
  });
99
95
 
@@ -118,7 +114,7 @@ describe('Given a telephone number component', () => {
118
114
  it(`${number} code should update the value properly`, () => {
119
115
  simulatePaste(component.find('input'), number);
120
116
 
121
- expect(select().props().value.value).toStrictEqual(countryCode);
117
+ expect(select().props().value).toBe(countryCode);
122
118
  expect(input().prop('value')).toBe(localNumber);
123
119
  expect(props.onChange).toHaveBeenCalledWith(number.replace(/(\s|-)+/g, ''), countryCode);
124
120
  });
@@ -126,28 +122,28 @@ describe('Given a telephone number component', () => {
126
122
 
127
123
  it('should not paste invalid characters', () => {
128
124
  simulatePaste(component.find('input'), '+36asdasdasd');
129
- expect(select().props().value.value).toStrictEqual('+39');
125
+ expect(select().props().value).toBe('+39');
130
126
  expect(input().prop('value')).toBe('123456789');
131
127
  expect(props.onChange).not.toHaveBeenCalled();
132
128
  });
133
129
 
134
130
  it('should not paste countries which are not in the select', () => {
135
131
  simulatePaste(component.find('input'), '+9992342343423');
136
- expect(select().props().value.value).toStrictEqual('+39');
132
+ expect(select().props().value).toBe('+39');
137
133
  expect(input().prop('value')).toBe('123456789');
138
134
  expect(props.onChange).not.toHaveBeenCalled();
139
135
  });
140
136
 
141
137
  it("should not paste numbers which doesn't start with the country code", () => {
142
138
  simulatePaste(component.find('input'), '0+36303932551');
143
- expect(select().props().value.value).toStrictEqual('+39');
139
+ expect(select().props().value).toBe('+39');
144
140
  expect(input().prop('value')).toBe('123456789');
145
141
  expect(props.onChange).not.toHaveBeenCalled();
146
142
  });
147
143
 
148
144
  it("should not paste numbers which doesn't contain a country code", () => {
149
145
  simulatePaste(component.find('input'), '06303932551');
150
- expect(select().props().value.value).toStrictEqual('+39');
146
+ expect(select().props().value).toBe('+39');
151
147
  expect(input().prop('value')).toBe('123456789');
152
148
  expect(props.onChange).not.toHaveBeenCalled();
153
149
  });
@@ -161,7 +157,7 @@ describe('Given a telephone number component', () => {
161
157
  });
162
158
 
163
159
  it('should set the select to the longest matching prefix', () => {
164
- expect(select.props().value.value).toStrictEqual('+1868');
160
+ expect(select.props().value).toBe('+1868');
165
161
  });
166
162
 
167
163
  it('should set the number input to the rest of the number', () => {
@@ -177,7 +173,7 @@ describe('Given a telephone number component', () => {
177
173
  });
178
174
 
179
175
  it('should empty the select', () => {
180
- expect(select.props().value).toBeUndefined();
176
+ expect(select.props().value).toBeNull();
181
177
  });
182
178
 
183
179
  it('should put the whole value in the input without the plus', () => {
@@ -191,7 +187,7 @@ describe('Given a telephone number component', () => {
191
187
  select = component.find(PREFIX_SELECT_SELECTOR);
192
188
  input = component.find(NUMBER_SELECTOR);
193
189
 
194
- expect(select.props().value.value).toStrictEqual('+44');
190
+ expect(select.props().value).toBe('+44');
195
191
  expect(input.prop('value')).toBe('');
196
192
  });
197
193
  });
@@ -254,7 +250,7 @@ describe('Given a telephone number component', () => {
254
250
  });
255
251
 
256
252
  it('should use the prefix of the supplied value', () => {
257
- expect(select.props().value.value).toBe('+1');
253
+ expect(select.props().value).toBe('+1');
258
254
  });
259
255
  });
260
256
 
@@ -268,7 +264,7 @@ describe('Given a telephone number component', () => {
268
264
  });
269
265
 
270
266
  it('should default the prefix to the local country', () => {
271
- expect(select.props().value.value).toBe('+34');
267
+ expect(select.props().value).toBe('+34');
272
268
  });
273
269
  });
274
270
 
@@ -281,7 +277,7 @@ describe('Given a telephone number component', () => {
281
277
  });
282
278
 
283
279
  it('should override locale prefix with country specific prefix', () => {
284
- expect(select.props().value.value).toBe('+1');
280
+ expect(select.props().value).toBe('+1');
285
281
  });
286
282
  });
287
283
  });
@@ -349,7 +345,7 @@ describe('Given a telephone number component', () => {
349
345
 
350
346
  it('renders Select component with expected props', () => {
351
347
  const select = component.find(PREFIX_SELECT_SELECTOR);
352
- expect(select.prop('className')).toStrictEqual('custom-class');
348
+ expect(select.prop('className')).toBe('custom-class');
353
349
  });
354
350
  });
355
351
  });
@@ -0,0 +1,193 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+
4
+ import { Size, SizeLarge, SizeMedium, SizeSmall } from '../common';
5
+ import { SelectInput, SelectInputOptionContent, SelectInputProps } from '../inputs/SelectInput';
6
+
7
+ import countries from './data/countries';
8
+ import {
9
+ explodeNumberModel,
10
+ isValidPhoneNumber,
11
+ cleanNumber,
12
+ setDefaultPrefix,
13
+ sortArrayByProperty,
14
+ groupCountriesByPrefix,
15
+ excludeCountries,
16
+ findCountryByPrefix,
17
+ } from './utils';
18
+ import { PhoneNumber } from './utils/explodeNumberModel';
19
+
20
+ const ALLOWED_PHONE_CHARS = /^$|^[\d-\s]+$/;
21
+
22
+ export interface PhoneNumberInputProps {
23
+ id?: string;
24
+ required?: boolean;
25
+ disabled?: boolean;
26
+ initialValue?: string;
27
+ onChange: (value: string | null, prefix: string) => void;
28
+ onFocus?: React.FocusEventHandler<HTMLInputElement>;
29
+ onBlur?: React.FocusEventHandler<HTMLInputElement>;
30
+ countryCode?: string;
31
+ searchPlaceholder?: string;
32
+ size?: SizeSmall | SizeMedium | SizeLarge;
33
+ placeholder?: string;
34
+ selectProps?: Partial<SelectInputProps<string | null>>;
35
+ /** List of iso3 codes of countries to remove from the list */
36
+ disabledCountries?: string[];
37
+ }
38
+
39
+ const defaultSelectProps = {} satisfies PhoneNumberInputProps['selectProps'];
40
+ const defaultDisabledCountries = [] satisfies PhoneNumberInputProps['disabledCountries'];
41
+
42
+ const PhoneNumberInput = ({
43
+ id,
44
+ required,
45
+ disabled,
46
+ initialValue,
47
+ onChange,
48
+ onFocus,
49
+ onBlur,
50
+ countryCode,
51
+ searchPlaceholder = 'Prefix',
52
+ size = Size.MEDIUM,
53
+ placeholder,
54
+ selectProps = defaultSelectProps,
55
+ disabledCountries = defaultDisabledCountries,
56
+ }: PhoneNumberInputProps) => {
57
+ const { locale } = useIntl();
58
+
59
+ const [internalValue, setInternalValue] = useState<PhoneNumber>(() => {
60
+ const cleanValue = initialValue ? cleanNumber(initialValue) : null;
61
+
62
+ if (!cleanValue || !isValidPhoneNumber(cleanValue)) {
63
+ return {
64
+ prefix: setDefaultPrefix(locale, countryCode),
65
+ suffix: '',
66
+ };
67
+ }
68
+
69
+ return explodeNumberModel(cleanValue);
70
+ });
71
+ const [broadcastedValue, setBroadcastedValue] = useState<PhoneNumber | null>(null);
72
+
73
+ const countriesByPrefix = useMemo(
74
+ () =>
75
+ groupCountriesByPrefix(
76
+ sortArrayByProperty(excludeCountries(countries, disabledCountries), 'iso3'),
77
+ ),
78
+ [disabledCountries],
79
+ );
80
+
81
+ const onSuffixChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
82
+ const suffix = event.target.value;
83
+ if (ALLOWED_PHONE_CHARS.test(suffix)) {
84
+ setInternalValue((prev) => ({ ...prev, suffix }));
85
+ }
86
+ };
87
+
88
+ const onPaste: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
89
+ if (!event.nativeEvent.clipboardData) {
90
+ return;
91
+ }
92
+
93
+ const pastedValue = (event.nativeEvent.clipboardData.getData('text/plain') || '').replace(
94
+ /(\s|-)+/g,
95
+ '',
96
+ );
97
+ const pastedNumber = explodeNumberModel(pastedValue);
98
+
99
+ if (
100
+ pastedNumber.prefix != null &&
101
+ countriesByPrefix.has(pastedNumber.prefix) &&
102
+ ALLOWED_PHONE_CHARS.test(pastedNumber.suffix)
103
+ ) {
104
+ setInternalValue(pastedNumber);
105
+ }
106
+ };
107
+
108
+ useEffect(() => {
109
+ if (broadcastedValue === null) {
110
+ return setBroadcastedValue(internalValue);
111
+ }
112
+
113
+ const internalPhoneNumber = `${internalValue.prefix ?? ''}${internalValue.suffix}`;
114
+ const broadcastedPhoneNumber = `${broadcastedValue.prefix ?? ''}${broadcastedValue.suffix}`;
115
+
116
+ if (internalPhoneNumber === broadcastedPhoneNumber) {
117
+ return;
118
+ }
119
+
120
+ const newValue = isValidPhoneNumber(internalPhoneNumber)
121
+ ? cleanNumber(internalPhoneNumber)
122
+ : null;
123
+
124
+ onChange(
125
+ newValue,
126
+ internalValue.prefix ?? '', // TODO: Allow `null` in public API
127
+ );
128
+ setBroadcastedValue(internalValue);
129
+ }, [onChange, broadcastedValue, internalValue]);
130
+
131
+ return (
132
+ <div className="tw-telephone">
133
+ <div className="tw-telephone__country-select">
134
+ <SelectInput
135
+ placeholder="Select an option…"
136
+ items={[...countriesByPrefix].map(([prefix, countries]) => ({
137
+ type: 'option',
138
+ value: prefix,
139
+ filterMatchers: [
140
+ prefix,
141
+ ...countries.map((country) => country.name),
142
+ ...countries.map((country) => country.iso3),
143
+ ],
144
+ }))}
145
+ value={internalValue.prefix}
146
+ renderValue={(prefix, withinTrigger) => (
147
+ <SelectInputOptionContent
148
+ title={prefix}
149
+ note={
150
+ withinTrigger
151
+ ? undefined
152
+ : countriesByPrefix
153
+ .get(prefix)
154
+ ?.map((country) => country.iso3)
155
+ .join(', ')
156
+ }
157
+ />
158
+ )}
159
+ filterable
160
+ filterPlaceholder={searchPlaceholder}
161
+ disabled={disabled}
162
+ size={size}
163
+ onChange={(prefix) => {
164
+ const country = prefix != null ? findCountryByPrefix(prefix) : null;
165
+ setInternalValue((prev) => ({ ...prev, prefix, format: country?.phoneFormat }));
166
+ }}
167
+ {...selectProps}
168
+ />
169
+ </div>
170
+ <div className="tw-telephone__number-input">
171
+ <div className={`input-group input-group-${size}`}>
172
+ <input
173
+ id={id}
174
+ autoComplete="tel-national"
175
+ name="phoneNumber"
176
+ inputMode="numeric"
177
+ value={internalValue.suffix}
178
+ className="form-control"
179
+ disabled={disabled}
180
+ required={required}
181
+ placeholder={placeholder}
182
+ onChange={onSuffixChange}
183
+ onPaste={onPaste}
184
+ onFocus={onFocus}
185
+ onBlur={onBlur}
186
+ />
187
+ </div>
188
+ </div>
189
+ </div>
190
+ );
191
+ };
192
+
193
+ export default PhoneNumberInput;