@transferwise/components 46.6.0 → 46.8.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 (148) hide show
  1. package/build/index.esm.js +290 -346
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +290 -345
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +107 -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 +107 -17
  10. package/build/styles/segmentedControl/SegmentedControl.css +101 -0
  11. package/build/styles/select/Select.css +0 -4
  12. package/build/types/common/locale/index.d.ts +26 -43
  13. package/build/types/common/locale/index.d.ts.map +1 -1
  14. package/build/types/index.d.ts +3 -0
  15. package/build/types/index.d.ts.map +1 -1
  16. package/build/types/inputs/SelectInput.d.ts +6 -5
  17. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  18. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +22 -27
  19. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  20. package/build/types/phoneNumberInput/data/countries.d.ts +5 -10
  21. package/build/types/phoneNumberInput/data/countries.d.ts.map +1 -1
  22. package/build/types/phoneNumberInput/index.d.ts +1 -1
  23. package/build/types/phoneNumberInput/index.d.ts.map +1 -1
  24. package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts +1 -1
  25. package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts.map +1 -1
  26. package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts +1 -1
  27. package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts.map +1 -1
  28. package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts +8 -1
  29. package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts.map +1 -1
  30. package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts +1 -1
  31. package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts.map +1 -1
  32. package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts +8 -4
  33. package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts.map +1 -1
  34. package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts +1 -1
  35. package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts.map +1 -1
  36. package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts +1 -1
  37. package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts.map +1 -1
  38. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts +2 -1
  39. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts.map +1 -1
  40. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts +1 -1
  41. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts.map +1 -1
  42. package/build/types/phoneNumberInput/utils/index.d.ts +11 -13
  43. package/build/types/phoneNumberInput/utils/index.d.ts.map +1 -1
  44. package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts +1 -1
  45. package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts.map +1 -1
  46. package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts +1 -1
  47. package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts.map +1 -1
  48. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts +1 -1
  49. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts.map +1 -1
  50. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts +6 -1
  51. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts.map +1 -1
  52. package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts +2 -1
  53. package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts.map +1 -1
  54. package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts +7 -1
  55. package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts.map +1 -1
  56. package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts +1 -1
  57. package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts.map +1 -1
  58. package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts +1 -1
  59. package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts.map +1 -1
  60. package/build/types/segmentedControl/SegmentedControl.d.ts +31 -0
  61. package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -0
  62. package/build/types/segmentedControl/index.d.ts +3 -0
  63. package/build/types/segmentedControl/index.d.ts.map +1 -0
  64. package/package.json +7 -17
  65. package/src/common/locale/{index.spec.js → index.spec.ts} +4 -4
  66. package/src/common/locale/index.ts +96 -0
  67. package/src/index.ts +3 -0
  68. package/src/inputs/Input.css +0 -4
  69. package/src/inputs/SelectInput.css +6 -1
  70. package/src/inputs/SelectInput.less +8 -1
  71. package/src/inputs/SelectInput.spec.tsx +26 -0
  72. package/src/inputs/SelectInput.story.tsx +73 -1
  73. package/src/inputs/SelectInput.tsx +104 -85
  74. package/src/inputs/TextArea.css +0 -4
  75. package/src/main.css +107 -17
  76. package/src/main.less +1 -0
  77. package/src/phoneNumberInput/PhoneNumberInput.spec.js +18 -22
  78. package/src/phoneNumberInput/PhoneNumberInput.tsx +193 -0
  79. package/src/phoneNumberInput/data/{countries.js → countries.ts} +9 -1
  80. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.ts +3 -0
  81. package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.spec.js → excludeCountries.spec.ts} +1 -1
  82. package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.js → excludeCountries.ts} +6 -5
  83. package/src/phoneNumberInput/utils/explodeNumberModel/{explodeNumberModel.spec.js → explodeNumberModel.spec.ts} +1 -1
  84. package/src/phoneNumberInput/utils/explodeNumberModel/index.ts +24 -0
  85. package/src/phoneNumberInput/utils/findCountryByCode/{findCountryByCode.spec.js → findCountryByCode.spec.ts} +0 -1
  86. package/src/phoneNumberInput/utils/findCountryByCode/index.ts +12 -0
  87. package/src/phoneNumberInput/utils/findCountryByPrefix/index.ts +12 -0
  88. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.ts +102 -0
  89. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.ts +12 -0
  90. package/src/phoneNumberInput/utils/{index.js → index.ts} +0 -2
  91. package/src/phoneNumberInput/utils/isStringNumeric/{isStringNumeric.spec.js → isStringNumeric.spec.ts} +0 -1
  92. package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.ts +1 -0
  93. package/src/phoneNumberInput/utils/isValidPhoneNumber/{isValidPhoneNumber.spec.js → isValidPhoneNumber.spec.ts} +1 -1
  94. package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.ts +7 -0
  95. package/src/phoneNumberInput/utils/longestMatchingPrefix/index.ts +4 -0
  96. package/src/phoneNumberInput/utils/setDefaultPrefix/index.ts +20 -0
  97. package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.ts +6 -0
  98. package/src/segmentedControl/SegmentedControl.css +101 -0
  99. package/src/segmentedControl/SegmentedControl.less +101 -0
  100. package/src/segmentedControl/SegmentedControl.spec.tsx +106 -0
  101. package/src/segmentedControl/SegmentedControl.story.tsx +55 -0
  102. package/src/segmentedControl/SegmentedControl.tsx +175 -0
  103. package/src/segmentedControl/index.ts +2 -0
  104. package/src/select/Select.css +0 -4
  105. package/src/ssr.spec.js +17 -0
  106. package/src/withDisplayFormat/WithDisplayFormat.spec.js +1 -1
  107. package/src/withDisplayFormat/WithDisplayFormat.tsx +1 -1
  108. package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts +0 -2
  109. package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts.map +0 -1
  110. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts +0 -2
  111. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts.map +0 -1
  112. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts +0 -3
  113. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts.map +0 -1
  114. package/build/types/utilities/wrapInFragment.d.ts +0 -3
  115. package/build/types/utilities/wrapInFragment.d.ts.map +0 -1
  116. package/src/common/locale/index.js +0 -139
  117. package/src/phoneNumberInput/PhoneNumberInput.js +0 -210
  118. package/src/phoneNumberInput/data/countries.spec.js +0 -12
  119. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.js +0 -4
  120. package/src/phoneNumberInput/utils/explodeNumberModel/index.js +0 -27
  121. package/src/phoneNumberInput/utils/filterOptionsForQuery/filterOptionsForQuery.spec.js +0 -36
  122. package/src/phoneNumberInput/utils/filterOptionsForQuery/index.js +0 -11
  123. package/src/phoneNumberInput/utils/findCountryByCode/index.js +0 -10
  124. package/src/phoneNumberInput/utils/findCountryByPrefix/index.js +0 -11
  125. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.js +0 -26
  126. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.js +0 -67
  127. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/index.js +0 -1
  128. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.js +0 -25
  129. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.spec.js +0 -66
  130. package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.js +0 -1
  131. package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js +0 -10
  132. package/src/phoneNumberInput/utils/longestMatchingPrefix/index.js +0 -2
  133. package/src/phoneNumberInput/utils/setDefaultPrefix/index.js +0 -25
  134. package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.js +0 -3
  135. package/src/utilities/wrapInFragment.tsx +0 -3
  136. /package/src/phoneNumberInput/{PhoneNumberInput.story.js → PhoneNumberInput.story.tsx} +0 -0
  137. /package/src/phoneNumberInput/{index.js → index.ts} +0 -0
  138. /package/src/phoneNumberInput/utils/cleanNumber/{cleanNumber.spec.js → cleanNumber.spec.ts} +0 -0
  139. /package/src/phoneNumberInput/utils/cleanNumber/{index.js → index.ts} +0 -0
  140. /package/src/phoneNumberInput/utils/excludeCountries/{index.js → index.ts} +0 -0
  141. /package/src/phoneNumberInput/utils/findCountryByPrefix/{findCountryByPrefix.spec.js → findCountryByPrefix.spec.ts} +0 -0
  142. /package/src/phoneNumberInput/utils/groupCountriesByPrefix/{index.js → index.ts} +0 -0
  143. /package/src/phoneNumberInput/utils/isStringNumeric/{index.js → index.ts} +0 -0
  144. /package/src/phoneNumberInput/utils/isValidPhoneNumber/{index.js → index.ts} +0 -0
  145. /package/src/phoneNumberInput/utils/longestMatchingPrefix/{longestMatchingPrefix.spec.js → longestMatchingPrefix.spec.ts} +0 -0
  146. /package/src/phoneNumberInput/utils/setDefaultPrefix/{setDefaultPrefix.spec.js → setDefaultPrefix.spec.ts} +0 -0
  147. /package/src/phoneNumberInput/utils/sortArrayByProperty/{index.js → index.ts} +0 -0
  148. /package/src/phoneNumberInput/utils/sortArrayByProperty/{sortArrayByProperty.spec.js → sortArrayByProperty.spec.ts} +0 -0
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export type {
15
15
  SelectInputTriggerButtonProps,
16
16
  } from './inputs/SelectInput';
17
17
  export type { TextAreaProps } from './inputs/TextArea';
18
+ export type { PhoneNumberInputProps } from './phoneNumberInput/PhoneNumberInput';
18
19
  export type { TextareaWithDisplayFormatProps } from './textareaWithDisplayFormat';
19
20
  export type { UploadedFile, UploadError, UploadResponse } from './uploadInput/types';
20
21
  export type { ModalProps } from './modal';
@@ -32,6 +33,7 @@ export type {
32
33
  LinkTypes,
33
34
  DisplayTypes,
34
35
  } from './common';
36
+ export type { SegmentedControlProps } from './segmentedControl';
35
37
 
36
38
  /**
37
39
  * Components
@@ -106,6 +108,7 @@ export { default as RadioGroup } from './radioGroup';
106
108
  export { default as RadioOption } from './radioOption';
107
109
  export { default as Section } from './section';
108
110
  export { default as Select } from './select';
111
+ export { default as SegmentedControl } from './segmentedControl';
109
112
  export { default as SlidingPanel } from './slidingPanel';
110
113
  export { default as SnackbarPortal } from './snackbar/Snackbar';
111
114
  export { default as SnackbarProvider } from './snackbar/SnackbarProvider';
@@ -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%;
@@ -158,10 +158,12 @@
158
158
  color: #5d7079;
159
159
  color: var(--color-content-secondary);
160
160
  }
161
- .np-select-input-placeholder {
161
+ .np-select-input-content {
162
162
  overflow: hidden;
163
163
  text-overflow: ellipsis;
164
164
  white-space: nowrap;
165
+ }
166
+ .np-select-input-placeholder {
165
167
  color: #768e9c;
166
168
  color: var(--color-content-tertiary);
167
169
  }
@@ -269,6 +271,9 @@
269
271
  padding: var(--size-12) var(--size-16);
270
272
  color: var(--color-interactive-primary);
271
273
  }
274
+ .np-select-input-option-container:focus {
275
+ outline: none;
276
+ }
272
277
  .np-select-input-option-container--active {
273
278
  box-shadow: inset 0 0 0 1px #c9cbce;
274
279
  box-shadow: inset 0 0 0 1px var(--color-interactive-secondary);
@@ -3,10 +3,13 @@
3
3
  @import "./_Popover.less";
4
4
  @import (reference) "../../node_modules/@transferwise/neptune-css/src/less/ring.less";
5
5
 
6
- .np-select-input-placeholder {
6
+ .np-select-input-content {
7
7
  overflow: hidden;
8
8
  text-overflow: ellipsis;
9
9
  white-space: nowrap;
10
+ }
11
+
12
+ .np-select-input-placeholder {
10
13
  color: var(--color-content-tertiary);
11
14
  }
12
15
 
@@ -100,6 +103,10 @@
100
103
  padding: var(--size-12) var(--size-16);
101
104
  color: var(--color-interactive-primary);
102
105
 
106
+ &:focus {
107
+ outline: none;
108
+ }
109
+
103
110
  &--active {
104
111
  box-shadow: inset 0 0 0 1px var(--color-interactive-secondary);
105
112
  }
@@ -182,4 +182,30 @@ describe('SelectInput', () => {
182
182
  const listbox = screen.getByRole('listbox');
183
183
  expect(within(listbox).getAllByRole('option')).toHaveLength(2);
184
184
  });
185
+
186
+ it('selects multiple options', async () => {
187
+ render(
188
+ <SelectInput
189
+ multiple
190
+ items={[
191
+ { type: 'option', value: 'USD' },
192
+ { type: 'option', value: 'EUR' },
193
+ ]}
194
+ />,
195
+ );
196
+
197
+ const trigger = screen.getAllByRole('button')[0];
198
+ // eslint-disable-next-line @typescript-eslint/require-await
199
+ await act(async () => {
200
+ userEvent.click(trigger);
201
+ });
202
+
203
+ const listbox = screen.getByRole('listbox');
204
+ const options = within(listbox).getAllByRole('option');
205
+ options.forEach((option) => {
206
+ userEvent.click(option);
207
+ });
208
+
209
+ expect(trigger).toHaveTextContent('USD, EUR');
210
+ });
185
211
  });
@@ -242,7 +242,7 @@ export const Currencies: StoryObj<{
242
242
  onChange: (value: Currency) => void;
243
243
  }> = {
244
244
  render: function Story({ onChange }) {
245
- const [selectedCurrency, setSelectedCurrency] = useState<Currency>(popularCurrencies[0]);
245
+ const [selectedCurrency, setSelectedCurrency] = useState(popularCurrencies[0]);
246
246
 
247
247
  return (
248
248
  <SelectInput
@@ -301,6 +301,78 @@ export const Currencies: StoryObj<{
301
301
  },
302
302
  };
303
303
 
304
+ export const MultipleCurrencies: StoryObj<{
305
+ onChange: (value: Currency[]) => void;
306
+ }> = {
307
+ render: function Story({ onChange }) {
308
+ const [selectedCurrencies, setSelectedCurrencies] = useState<readonly Currency[]>([
309
+ popularCurrencies[0],
310
+ ]);
311
+
312
+ return (
313
+ <SelectInput
314
+ multiple
315
+ placeholder="Choose currencies…"
316
+ items={[
317
+ {
318
+ type: 'group',
319
+ label: 'Popular currencies',
320
+ options: popularCurrencies.map((currency) => currencyOption(currency)),
321
+ },
322
+ {
323
+ type: 'group',
324
+ label: 'All currencies',
325
+ options: allCurrencies.map((currency) => currencyOption(currency)),
326
+ },
327
+ ]}
328
+ value={selectedCurrencies}
329
+ renderValue={(currency, withinTrigger) =>
330
+ withinTrigger ? (
331
+ currency.code
332
+ ) : (
333
+ <SelectInputOptionContent
334
+ title={currency.code}
335
+ note={currency.name}
336
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
337
+ />
338
+ )
339
+ }
340
+ // eslint-disable-next-line sonarjs/no-identical-functions
341
+ renderFooter={({ resultsEmpty, queryNormalized: normalizedQuery }) =>
342
+ resultsEmpty && normalizedQuery != null && /^[a-z]{3}$/u.test(normalizedQuery) ? (
343
+ <>
344
+ It’s not possible use {normalizedQuery.toUpperCase()} yet.{' '}
345
+ <a href="#_" className="np-text-link-default">
346
+ Email me when it’s available.
347
+ </a>
348
+ </>
349
+ ) : (
350
+ <>
351
+ Can’t find it?{' '}
352
+ <a href="#_" className="np-text-link-default">
353
+ Request the currency you need,
354
+ </a>{' '}
355
+ and we’ll notify you once it’s available.
356
+ </>
357
+ )
358
+ }
359
+ filterable
360
+ filterPlaceholder="Type a currency / country"
361
+ size="lg"
362
+ onChange={(currency) => {
363
+ setSelectedCurrencies(currency);
364
+ onChange(currency);
365
+ }}
366
+ />
367
+ );
368
+ },
369
+ argTypes: {
370
+ onChange: {
371
+ action: 'changed',
372
+ },
373
+ },
374
+ };
375
+
304
376
  export const CustomTrigger: StoryObj = {
305
377
  render: function Story() {
306
378
  return (
@@ -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%;
@@ -4640,6 +4629,107 @@ html:not([dir="rtl"]) .np-navigation-option {
4640
4629
  .np-theme-personal .np-dropdown-menu .np-dropdown-item--focused {
4641
4630
  box-shadow: inset 0 0 0 2px var(--color-interactive-primary);
4642
4631
  }
4632
+ .segmented-control {
4633
+ box-sizing: border-box;
4634
+ --segment-highlight-width: 0;
4635
+ --segment-highlight-x: var(--size-4);
4636
+ }
4637
+ .segmented-control__segments {
4638
+ display: inline-flex;
4639
+ position: relative;
4640
+ padding: 4px;
4641
+ padding: var(--size-4);
4642
+ width: 100%;
4643
+ justify-content: center;
4644
+ align-items: center;
4645
+ background: rgba(134,167,189,0.10196);
4646
+ background: var(--color-background-neutral);
4647
+ border-radius: 24px;
4648
+ border-radius: var(--size-24);
4649
+ transition: outline 300ms;
4650
+ outline: 2px solid transparent;
4651
+ }
4652
+ .segmented-control--input:has(:focus-visible) > .segmented-control__segments::after {
4653
+ outline: 2px solid var(--color-interactive-primary);
4654
+ }
4655
+ .segmented-control__segments::after {
4656
+ content: "";
4657
+ position: absolute;
4658
+ width: var(--segment-highlight-width);
4659
+ top: 4px;
4660
+ top: var(--size-4);
4661
+ bottom: 4px;
4662
+ bottom: var(--size-4);
4663
+ left: var(--segment-highlight-x);
4664
+ z-index: 0;
4665
+ background: #ffffff;
4666
+ background: var(--color-background-screen);
4667
+ border-radius: 24px;
4668
+ border-radius: var(--size-24);
4669
+ transition: left 300ms;
4670
+ }
4671
+ .segmented-control__segments--no-animate::after {
4672
+ transition: none !important;
4673
+ }
4674
+ .segmented-control__segment {
4675
+ position: relative;
4676
+ flex: 1 1 100%;
4677
+ flex-flow: column;
4678
+ padding: 8px 16px;
4679
+ padding: var(--size-8) var(--size-16);
4680
+ margin: 0 0 0 4px;
4681
+ margin: 0 0 0 var(--size-4);
4682
+ min-width: 0;
4683
+ line-height: inherit;
4684
+ align-items: center;
4685
+ text-align: center;
4686
+ vertical-align: middle;
4687
+ border-radius: 24px;
4688
+ border-radius: var(--size-24);
4689
+ z-index: 1;
4690
+ cursor: pointer;
4691
+ transition: background 300ms;
4692
+ color: var(--color-interactive-primary);
4693
+ }
4694
+ .segmented-control__segment:first-child {
4695
+ margin-left: 0;
4696
+ }
4697
+ .segmented-control__segment:hover {
4698
+ background: rgba(0,0,0,0.10196);
4699
+ background: var(--color-background-overlay);
4700
+ }
4701
+ .segmented-control__radio-input {
4702
+ position: fixed;
4703
+ opacity: 0;
4704
+ pointer-events: none;
4705
+ }
4706
+ .segmented-control__button {
4707
+ width: 100%;
4708
+ height: 100%;
4709
+ background: none;
4710
+ -webkit-appearance: none;
4711
+ -moz-appearance: none;
4712
+ appearance: none;
4713
+ border: none;
4714
+ outline: none;
4715
+ font: inherit;
4716
+ outline: 2px solid transparent;
4717
+ }
4718
+ .segmented-control__button:focus {
4719
+ outline-offset: 0px;
4720
+ }
4721
+ .segmented-control__button:focus-visible {
4722
+ outline-color: var(--color-interactive-primary);
4723
+ }
4724
+ .segmented-control__selected-segment:hover {
4725
+ background: transparent;
4726
+ }
4727
+ .segmented-control__text {
4728
+ word-wrap: break-word;
4729
+ word-break: break-word;
4730
+ color: var(--color-interactive-primary);
4731
+ transition: font-weight 300ms;
4732
+ }
4643
4733
  .np-summary {
4644
4734
  min-width: 280px;
4645
4735
  }
package/src/main.less CHANGED
@@ -53,6 +53,7 @@
53
53
  @import "./statusIcon/StatusIcon.less";
54
54
  @import "./stepper/Stepper.less";
55
55
  @import "./select/Select.less";
56
+ @import "./segmentedControl/SegmentedControl.less";
56
57
  @import "./summary/Summary.less";
57
58
  @import "./switch/Switch.less";
58
59
  @import "./tabs/Tabs.less";