@transferwise/components 46.1.1 → 46.2.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 (41) hide show
  1. package/build/index.esm.js +38 -15
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +38 -15
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +26 -13
  6. package/build/styles/common/card/Card.css +0 -4
  7. package/build/styles/criticalBanner/CriticalCommsBanner.css +0 -1
  8. package/build/styles/flowNavigation/FlowNavigation.css +26 -4
  9. package/build/styles/inputs/Input.css +0 -1
  10. package/build/styles/inputs/TextArea.css +0 -1
  11. package/build/styles/main.css +26 -13
  12. package/build/styles/nudge/Nudge.css +0 -3
  13. package/build/types/accordion/Accordion.d.ts +1 -0
  14. package/build/types/accordion/Accordion.d.ts.map +1 -1
  15. package/build/types/accordion/AccordionItem/AccordionItem.d.ts +3 -0
  16. package/build/types/accordion/AccordionItem/AccordionItem.d.ts.map +1 -1
  17. package/build/types/inputs/SelectInput.d.ts +6 -3
  18. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  19. package/build/types/inputs/_BottomSheet.d.ts +2 -1
  20. package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
  21. package/build/types/inputs/_Popover.d.ts +2 -1
  22. package/build/types/inputs/_Popover.d.ts.map +1 -1
  23. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  24. package/package.json +4 -4
  25. package/src/accordion/Accordion.story.tsx +8 -0
  26. package/src/accordion/Accordion.tsx +2 -0
  27. package/src/accordion/AccordionItem/AccordionItem.tsx +5 -0
  28. package/src/common/card/Card.css +0 -4
  29. package/src/criticalBanner/CriticalCommsBanner.css +0 -1
  30. package/src/flowNavigation/FlowNavigation.css +26 -4
  31. package/src/inputs/Input.css +0 -1
  32. package/src/inputs/SelectInput.spec.tsx +52 -3
  33. package/src/inputs/SelectInput.story.tsx +1 -1
  34. package/src/inputs/SelectInput.tsx +30 -15
  35. package/src/inputs/TextArea.css +0 -1
  36. package/src/inputs/_BottomSheet.tsx +3 -0
  37. package/src/inputs/_Popover.tsx +3 -0
  38. package/src/main.css +26 -13
  39. package/src/moneyInput/MoneyInput.js +3 -1
  40. package/src/moneyInput/MoneyInput.spec.js +4 -1
  41. package/src/nudge/Nudge.css +0 -3
@@ -1,4 +1,4 @@
1
- import { act, screen, within } from '@testing-library/react';
1
+ import { act, screen, waitFor, within } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
 
4
4
  import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
@@ -30,7 +30,7 @@ describe('SelectInput', () => {
30
30
  { type: 'option', value: 'USD' },
31
31
  { type: 'option', value: 'EUR' },
32
32
  ]}
33
- renderFooter={({ normalizedQuery }) =>
33
+ renderFooter={({ queryNormalized: normalizedQuery }) =>
34
34
  normalizedQuery != null ? (
35
35
  <>Showing results for ‘{normalizedQuery}’</>
36
36
  ) : (
@@ -129,8 +129,57 @@ describe('SelectInput', () => {
129
129
  expect(within(listbox).getByRole('option')).toBeInTheDocument();
130
130
 
131
131
  const option = within(listbox).getAllByRole('option')[0];
132
- userEvent.click(option);
132
+ // eslint-disable-next-line @typescript-eslint/require-await
133
+ await act(async () => {
134
+ userEvent.click(option);
135
+ });
133
136
 
134
137
  expect(trigger).toHaveTextContent('EUR');
135
138
  });
139
+
140
+ it('clears filter query on close', async () => {
141
+ const handleFilterChange = jest.fn();
142
+
143
+ render(
144
+ <SelectInput
145
+ items={[
146
+ { type: 'option', value: 'USD' },
147
+ { type: 'option', value: 'EUR' },
148
+ ]}
149
+ filterable
150
+ onFilterChange={handleFilterChange}
151
+ />,
152
+ );
153
+
154
+ const trigger = screen.getAllByRole('button')[0];
155
+ // eslint-disable-next-line @typescript-eslint/require-await
156
+ await act(async () => {
157
+ userEvent.tab();
158
+ userEvent.keyboard(' ');
159
+ });
160
+
161
+ expect(handleFilterChange).not.toHaveBeenCalled();
162
+
163
+ userEvent.keyboard(' x');
164
+ expect(handleFilterChange).toHaveBeenLastCalledWith({
165
+ query: ' x',
166
+ queryNormalized: 'x',
167
+ });
168
+
169
+ userEvent.keyboard('{Escape}');
170
+ await waitFor(() => {
171
+ expect(handleFilterChange).toHaveBeenLastCalledWith({
172
+ query: '',
173
+ queryNormalized: null,
174
+ });
175
+ });
176
+
177
+ // eslint-disable-next-line @typescript-eslint/require-await
178
+ await act(async () => {
179
+ userEvent.click(trigger);
180
+ });
181
+
182
+ const listbox = screen.getByRole('listbox');
183
+ expect(within(listbox).getAllByRole('option')).toHaveLength(2);
184
+ });
136
185
  });
@@ -266,7 +266,7 @@ export const Currencies: StoryObj<{
266
266
  icon={<Flag code={currency.code} intrinsicSize={24} />}
267
267
  />
268
268
  )}
269
- renderFooter={({ resultsEmpty, normalizedQuery }) =>
269
+ renderFooter={({ resultsEmpty, queryNormalized: normalizedQuery }) =>
270
270
  resultsEmpty && normalizedQuery != null && /^[a-z]{3}$/u.test(normalizedQuery) ? (
271
271
  <>
272
272
  It’s not possible use {normalizedQuery.toUpperCase()} yet.{' '}
@@ -138,7 +138,7 @@ export interface SelectInputProps<T = string> {
138
138
  renderValue?: (value: NonNullable<T>, withinTrigger: boolean) => React.ReactNode;
139
139
  renderFooter?: (args: {
140
140
  resultsEmpty: boolean;
141
- normalizedQuery: string | null | undefined;
141
+ queryNormalized: string | null | undefined;
142
142
  }) => React.ReactNode;
143
143
  renderTrigger?: (args: {
144
144
  content: React.ReactNode;
@@ -153,8 +153,8 @@ export interface SelectInputProps<T = string> {
153
153
  disabled?: boolean;
154
154
  size?: 'md' | 'lg';
155
155
  className?: string;
156
+ onFilterChange?: (args: { query: string; queryNormalized: string | null }) => void;
156
157
  onChange?: (value: T) => void;
157
- onSearchChange?: (query: string) => void;
158
158
  onClear?: () => void;
159
159
  }
160
160
 
@@ -209,6 +209,8 @@ function SelectInputClearButton({ className, onClick }: SelectInputClearButtonPr
209
209
  );
210
210
  }
211
211
 
212
+ const noop = () => {};
213
+
212
214
  export function SelectInput<T = string>({
213
215
  name,
214
216
  placeholder,
@@ -224,12 +226,21 @@ export function SelectInput<T = string>({
224
226
  disabled,
225
227
  size = 'md',
226
228
  className,
229
+ onFilterChange = noop,
227
230
  onChange,
228
- onSearchChange,
229
231
  onClear,
230
232
  }: SelectInputProps<T>) {
231
233
  const [open, setOpen] = useState(false);
232
234
 
235
+ const [filterQuery, _setFilterQuery] = useState('');
236
+ const setFilterQuery = useEffectEvent((query: string) => {
237
+ _setFilterQuery(query);
238
+ onFilterChange({
239
+ query,
240
+ queryNormalized: query ? searchableString(query) : null,
241
+ });
242
+ });
243
+
233
244
  const triggerRef = useRef<HTMLButtonElement>(null);
234
245
 
235
246
  const screenSm = useScreenSize(Breakpoint.SMALL);
@@ -311,6 +322,11 @@ export function SelectInput<T = string>({
311
322
  onClose={() => {
312
323
  setOpen(false);
313
324
  }}
325
+ onCloseEnd={() => {
326
+ if (filterQuery !== '') {
327
+ setFilterQuery('');
328
+ }
329
+ }}
314
330
  >
315
331
  <SelectInputOptions
316
332
  items={items}
@@ -320,7 +336,8 @@ export function SelectInput<T = string>({
320
336
  filterPlaceholder={filterPlaceholder}
321
337
  searchInputRef={searchInputRef}
322
338
  listboxRef={listboxRef}
323
- onSearchChange={onSearchChange}
339
+ filterQuery={filterQuery}
340
+ onFilterChange={setFilterQuery}
324
341
  />
325
342
  </OptionsOverlay>
326
343
  )}
@@ -407,7 +424,8 @@ interface SelectInputOptionsProps<T = string>
407
424
  > {
408
425
  searchInputRef: React.RefObject<HTMLInputElement>;
409
426
  listboxRef: React.RefObject<HTMLDivElement>;
410
- onSearchChange?: (query: string) => void;
427
+ filterQuery: string;
428
+ onFilterChange: (query: string) => void;
411
429
  }
412
430
 
413
431
  function SelectInputOptions<T = string>({
@@ -418,19 +436,19 @@ function SelectInputOptions<T = string>({
418
436
  filterPlaceholder,
419
437
  searchInputRef,
420
438
  listboxRef,
421
- onSearchChange,
439
+ filterQuery,
440
+ onFilterChange,
422
441
  }: SelectInputOptionsProps<T>) {
423
442
  const intl = useIntl();
424
443
 
425
444
  const controllerRef = filterable ? searchInputRef : listboxRef;
426
445
 
427
- const [query, setQuery] = useState('');
428
446
  const needle = useMemo(() => {
429
447
  if (filterable) {
430
- return query ? searchableString(query) : null;
448
+ return filterQuery ? searchableString(filterQuery) : null;
431
449
  }
432
450
  return undefined;
433
- }, [filterable, query]);
451
+ }, [filterQuery, filterable]);
434
452
  const resultsEmpty = needle != null && filterSelectInputItems(items, needle).length === 0;
435
453
 
436
454
  const listboxContainerRef = useRef<HTMLDivElement>(null);
@@ -468,7 +486,7 @@ function SelectInputOptions<T = string>({
468
486
  ref={searchInputRef}
469
487
  shape="rectangle"
470
488
  placeholder={filterPlaceholder}
471
- value={query}
489
+ value={filterQuery}
472
490
  aria-controls={listboxId}
473
491
  aria-describedby={showStatus ? statusId : undefined}
474
492
  onKeyDown={(event) => {
@@ -479,10 +497,7 @@ function SelectInputOptions<T = string>({
479
497
  }
480
498
  }}
481
499
  onChange={(event) => {
482
- setQuery(event.currentTarget.value);
483
- if (onSearchChange) {
484
- onSearchChange(event.currentTarget.value);
485
- }
500
+ onFilterChange(event.currentTarget.value);
486
501
  }}
487
502
  />
488
503
  </div>
@@ -535,7 +550,7 @@ function SelectInputOptions<T = string>({
535
550
  >
536
551
  {renderFooter({
537
552
  resultsEmpty,
538
- normalizedQuery: needle,
553
+ queryNormalized: needle,
539
554
  })}
540
555
  </div>
541
556
  </footer>
@@ -14,7 +14,6 @@
14
14
  color: #37517e;
15
15
  color: var(--color-content-primary);
16
16
  box-shadow: inset 0 0 0 1px #c9cbce;
17
- box-shadow: inset 0 0 0 1px var(--color-interactive-secondary);
18
17
  box-shadow: inset 0 0 0 var(--ring-width) var(--ring-color);
19
18
  transition-property: color, opacity, box-shadow;
20
19
  transition-timing-function: ease-in-out;
@@ -29,6 +29,7 @@ export interface BottomSheetProps {
29
29
  padding?: 'none' | 'md';
30
30
  children?: React.ReactNode;
31
31
  onClose?: () => void;
32
+ onCloseEnd?: () => void;
32
33
  }
33
34
 
34
35
  export function BottomSheet({
@@ -39,6 +40,7 @@ export function BottomSheet({
39
40
  padding = 'md',
40
41
  children,
41
42
  onClose,
43
+ onCloseEnd,
42
44
  }: BottomSheetProps) {
43
45
  const { refs, context } = useFloating<Element>({
44
46
  open,
@@ -73,6 +75,7 @@ export function BottomSheet({
73
75
  beforeEnter={() => {
74
76
  setFloatingKey((prev) => prev + 1);
75
77
  }}
78
+ afterLeave={onCloseEnd}
76
79
  >
77
80
  <FocusBoundary>
78
81
  <Transition.Child
@@ -34,6 +34,7 @@ export interface PopoverProps {
34
34
  padding?: 'none' | 'md';
35
35
  children?: React.ReactNode;
36
36
  onClose?: () => void;
37
+ onCloseEnd?: () => void;
37
38
  }
38
39
 
39
40
  const floatingPadding = 16;
@@ -47,6 +48,7 @@ export function Popover({
47
48
  padding = 'md',
48
49
  children,
49
50
  onClose,
51
+ onCloseEnd,
50
52
  }: PopoverProps) {
51
53
  const { refs, floatingStyles, context } = useFloating<Element>({
52
54
  placement,
@@ -96,6 +98,7 @@ export function Popover({
96
98
  beforeEnter={() => {
97
99
  setFloatingKey((prev) => prev + 1);
98
100
  }}
101
+ afterLeave={onCloseEnd}
99
102
  >
100
103
  <FocusBoundary>
101
104
  <FloatingFocusManager context={context} guards={false} modal={false}>
package/src/main.css CHANGED
@@ -5,7 +5,6 @@ div.critical-comms {
5
5
  --critical-comms-subtitle-color-padding-left: var(--size-16);
6
6
  --critical-comms-vertical-spacing: var(--size-8);
7
7
  background-color: rgba(255,135,135,0.10196);
8
- background-color: var(--color-background-negative);
9
8
  background-color: var(--critical-comms-background-color);
10
9
  display: flex;
11
10
  justify-content: center;
@@ -1142,16 +1141,12 @@ div.critical-comms .critical-comms-body {
1142
1141
  flex-direction: column;
1143
1142
  align-items: stretch;
1144
1143
  background-color: rgba(134,167,189,0.10196);
1145
- background-color: var(--color-background-neutral);
1146
1144
  background-color: var(--Card-background-color);
1147
1145
  border-radius: 32px;
1148
- border-radius: var(--size-32);
1149
1146
  border-radius: var(--Card-border-radius);
1150
1147
  gap: 16px;
1151
- gap: var(--size-16);
1152
1148
  gap: var(--Card-flex-gap);
1153
1149
  padding: 24px;
1154
- padding: var(--size-24);
1155
1150
  padding: var(--Card-padding);
1156
1151
  position: relative;
1157
1152
  box-sizing: border-box;
@@ -2047,9 +2042,19 @@ button.np-option {
2047
2042
  max-width: 1164px;
2048
2043
  }
2049
2044
  .np-flow-navigation__stepper {
2050
- padding-bottom: 0px !important;
2051
- padding-left: 0px !important;
2052
- padding-right: 0px !important;
2045
+ padding-bottom: 0 !important;
2046
+ }
2047
+ [dir="rtl"] .np-flow-navigation__stepper {
2048
+ padding-right: 0 !important;
2049
+ }
2050
+ html:not([dir="rtl"]) .np-flow-navigation__stepper {
2051
+ padding-left: 0 !important;
2052
+ }
2053
+ [dir="rtl"] .np-flow-navigation__stepper {
2054
+ padding-left: 0 !important;
2055
+ }
2056
+ html:not([dir="rtl"]) .np-flow-navigation__stepper {
2057
+ padding-right: 0 !important;
2053
2058
  }
2054
2059
  .np-flow-navigation--xs-max .np-flow-navigation__stepper .tw-stepper-steps {
2055
2060
  display: none;
@@ -2061,11 +2066,23 @@ button.np-option {
2061
2066
  height: auto;
2062
2067
  }
2063
2068
  .np-flow-navigation--sm .np-flow-navigation__stepper {
2069
+ min-height: 56px;
2070
+ }
2071
+ [dir="rtl"] .np-flow-navigation--sm .np-flow-navigation__stepper {
2072
+ padding-right: 24px !important;
2073
+ padding-right: var(--size-24) !important;
2074
+ }
2075
+ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2076
+ padding-left: 24px !important;
2077
+ padding-left: var(--size-24) !important;
2078
+ }
2079
+ [dir="rtl"] .np-flow-navigation--sm .np-flow-navigation__stepper {
2064
2080
  padding-left: 24px !important;
2065
2081
  padding-left: var(--size-24) !important;
2082
+ }
2083
+ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2066
2084
  padding-right: 24px !important;
2067
2085
  padding-right: var(--size-24) !important;
2068
- min-height: 56px;
2069
2086
  }
2070
2087
  .np-flow-navigation--lg .np-flow-navigation__stepper {
2071
2088
  max-width: 562px;
@@ -2169,7 +2186,6 @@ button.np-option {
2169
2186
  color: #37517e;
2170
2187
  color: var(--color-content-primary);
2171
2188
  box-shadow: inset 0 0 0 1px #c9cbce;
2172
- box-shadow: inset 0 0 0 1px var(--color-interactive-secondary);
2173
2189
  box-shadow: inset 0 0 0 var(--ring-width) var(--ring-color);
2174
2190
  transition-property: color, opacity, box-shadow;
2175
2191
  transition-timing-function: ease-in-out;
@@ -3424,15 +3440,12 @@ html:not([dir="rtl"]) .np-navigation-option {
3424
3440
  --nudge-control-background-color: var(--color-background-neutral);
3425
3441
  align-items: stretch;
3426
3442
  background-color: rgba(134,167,189,0.10196);
3427
- background-color: var(--color-background-neutral);
3428
3443
  background-color: var(--nudge-background-color);
3429
3444
  border-radius: 16px;
3430
- border-radius: var(--radius-medium);
3431
3445
  border-radius: var(--nudge-border-radius);
3432
3446
  display: flex;
3433
3447
  flex: 1;
3434
3448
  gap: 16px;
3435
- gap: var(--size-16);
3436
3449
  gap: var(--nudge-flex-gap);
3437
3450
  min-height: 106px;
3438
3451
  min-height: var(--nudge-min-height);
@@ -376,7 +376,9 @@ class MoneyInput extends Component {
376
376
  disabled={disabled}
377
377
  size={size}
378
378
  onChange={this.handleSelectChange}
379
- onSearchChange={this.handleSearchChange}
379
+ onFilterChange={({ queryNormalized }) => {
380
+ this.handleSearchChange(queryNormalized ?? '');
381
+ }}
380
382
  {...selectProps}
381
383
  />
382
384
  </div>
@@ -95,7 +95,10 @@ describe('Money Input', () => {
95
95
  }
96
96
 
97
97
  function searchCurrencies(query) {
98
- currencySelect().prop('onSearchChange')(query);
98
+ currencySelect().prop('onFilterChange')({
99
+ query,
100
+ queryNormalized: query.trim().replace(/\s+/gu, ' ').normalize('NFKC').toLowerCase(),
101
+ });
99
102
  component.update();
100
103
  }
101
104
 
@@ -15,15 +15,12 @@
15
15
  --nudge-control-background-color: var(--color-background-neutral);
16
16
  align-items: stretch;
17
17
  background-color: rgba(134,167,189,0.10196);
18
- background-color: var(--color-background-neutral);
19
18
  background-color: var(--nudge-background-color);
20
19
  border-radius: 16px;
21
- border-radius: var(--radius-medium);
22
20
  border-radius: var(--nudge-border-radius);
23
21
  display: flex;
24
22
  flex: 1;
25
23
  gap: 16px;
26
- gap: var(--size-16);
27
24
  gap: var(--nudge-flex-gap);
28
25
  min-height: 106px;
29
26
  min-height: var(--nudge-min-height);