@transferwise/components 46.32.0 → 46.34.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 (38) hide show
  1. package/build/index.js +71 -39
  2. package/build/index.js.map +1 -1
  3. package/build/index.mjs +71 -39
  4. package/build/index.mjs.map +1 -1
  5. package/build/types/alert/Alert.d.ts +3 -2
  6. package/build/types/alert/Alert.d.ts.map +1 -1
  7. package/build/types/common/domHelpers/documentIosClick.d.ts +0 -1
  8. package/build/types/common/domHelpers/documentIosClick.d.ts.map +1 -1
  9. package/build/types/common/domHelpers/index.d.ts +1 -1
  10. package/build/types/common/domHelpers/index.d.ts.map +1 -1
  11. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  12. package/build/types/moneyInput/MoneyInput.d.ts +4 -2
  13. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  14. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
  15. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  16. package/build/types/select/Select.d.ts +7 -7
  17. package/build/types/select/Select.d.ts.map +1 -1
  18. package/build/types/typeahead/Typeahead.d.ts +4 -55
  19. package/build/types/typeahead/Typeahead.d.ts.map +1 -1
  20. package/package.json +3 -3
  21. package/src/alert/Alert.spec.tsx +12 -0
  22. package/src/alert/Alert.story.tsx +11 -1
  23. package/src/alert/Alert.tsx +33 -14
  24. package/src/common/domHelpers/documentIosClick.ts +0 -5
  25. package/src/common/domHelpers/index.ts +0 -1
  26. package/src/dateLookup/DateLookup.rtl.spec.tsx +2 -3
  27. package/src/dateLookup/DateLookup.tsx +1 -3
  28. package/src/inputs/SelectInput.spec.tsx +1 -1
  29. package/src/moneyInput/MoneyInput.rtl.spec.tsx +10 -0
  30. package/src/moneyInput/MoneyInput.spec.js +10 -5
  31. package/src/moneyInput/MoneyInput.tsx +21 -14
  32. package/src/phoneNumberInput/PhoneNumberInput.rtl.spec.tsx +10 -0
  33. package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -2
  34. package/src/select/Select.js +18 -15
  35. package/src/select/Select.rtl.spec.tsx +17 -0
  36. package/src/select/Select.spec.js +2 -7
  37. package/src/typeahead/Typeahead.rtl.spec.tsx +16 -0
  38. package/src/typeahead/Typeahead.tsx +21 -7
@@ -1,3 +1,4 @@
1
+ import { Field } from '../field/Field';
1
2
  import { mockMatchMedia, mockResizeObserver, render, screen, within } from '../test-utils';
2
3
 
3
4
  import PhoneNumberInput from './PhoneNumberInput';
@@ -19,4 +20,13 @@ describe('PhoneNumberInput', () => {
19
20
  within(screen.getByLabelText('Prioritized label')).getByRole('textbox'),
20
21
  ).toBeInTheDocument();
21
22
  });
23
+
24
+ it('supports `Field` for labeling', () => {
25
+ render(
26
+ <Field label="Phone number">
27
+ <PhoneNumberInput initialValue="+12345678" onChange={() => {}} />
28
+ </Field>,
29
+ );
30
+ expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Phone number/);
31
+ });
22
32
  });
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
 
4
4
  import { Size, SizeLarge, SizeMedium, SizeSmall } from '../common';
5
+ import { useInputAttributes } from '../inputs/contexts';
5
6
  import { SelectInput, SelectInputOptionContent, SelectInputProps } from '../inputs/SelectInput';
6
7
 
7
8
  import messages from './PhoneNumberInput.messages';
@@ -43,7 +44,7 @@ const defaultDisabledCountries = [] satisfies PhoneNumberInputProps['disabledCou
43
44
 
44
45
  const PhoneNumberInput = ({
45
46
  id,
46
- 'aria-labelledby': ariaLabelledBy,
47
+ 'aria-labelledby': ariaLabelledByProp,
47
48
  required,
48
49
  disabled,
49
50
  initialValue,
@@ -57,6 +58,9 @@ const PhoneNumberInput = ({
57
58
  selectProps = defaultSelectProps,
58
59
  disabledCountries = defaultDisabledCountries,
59
60
  }: PhoneNumberInputProps) => {
61
+ const inputAttributes = useInputAttributes({ nonLabelable: true });
62
+ const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes['aria-labelledby'];
63
+
60
64
  const { locale, formatMessage } = useIntl();
61
65
 
62
66
  const [internalValue, setInternalValue] = useState<PhoneNumber>(() => {
@@ -140,7 +144,12 @@ const PhoneNumberInput = ({
140
144
  }, [onChange, broadcastedValue, internalValue]);
141
145
 
142
146
  return (
143
- <div aria-labelledby={ariaLabelledBy} className="tw-telephone">
147
+ <div
148
+ role="group"
149
+ {...inputAttributes}
150
+ aria-labelledby={ariaLabelledBy}
151
+ className="tw-telephone"
152
+ >
144
153
  <div className="tw-telephone__country-select">
145
154
  <SelectInput
146
155
  placeholder={formatMessage(messages.selectInputPlaceholder)}
@@ -1,3 +1,4 @@
1
+ import { useId } from '@radix-ui/react-id';
1
2
  import { useTheme } from '@wise/components-theming';
2
3
  import classNames from 'classnames';
3
4
  import PropTypes from 'prop-types';
@@ -6,12 +7,13 @@ import { useIntl } from 'react-intl';
6
7
 
7
8
  import Button from '../button';
8
9
  import Chevron from '../chevron';
9
- import { Position, getSimpleRandomId } from '../common';
10
+ import { Position } from '../common';
10
11
  import BottomSheet from '../common/bottomSheet';
11
12
  import { stopPropagation } from '../common/domHelpers';
12
13
  import { useLayout } from '../common/hooks';
13
14
  import Panel from '../common/panel';
14
15
  import Drawer from '../drawer';
16
+ import { useInputAttributes } from '../inputs/contexts';
15
17
 
16
18
  import messages from './Select.messages';
17
19
  import Option from './option';
@@ -102,6 +104,8 @@ export default function Select({
102
104
  dropdownProps,
103
105
  buttonProps,
104
106
  }) {
107
+ const inputAttributes = useInputAttributes();
108
+
105
109
  const { formatMessage } = useIntl();
106
110
  const { isModern } = useTheme();
107
111
  const s = (className) => classNamesProp[className] || className;
@@ -118,8 +122,6 @@ export default function Select({
118
122
  const isSearchEnabled = !!onSearchChange || !!search;
119
123
  const isDropdownAutoWidth = dropdownWidth == null;
120
124
 
121
- const fallbackButtonId = useMemo(() => getSimpleRandomId('np-select-'), []);
122
-
123
125
  const options = useMemo(() => {
124
126
  if (!search || !searchValue) {
125
127
  return defaultOptions;
@@ -128,16 +130,16 @@ export default function Select({
128
130
  return defaultOptions.filter(isSearchableOption).filter((option) => {
129
131
  if (typeof search === 'function') {
130
132
  return search(option, searchValue);
131
- } else {
132
- return defaultFilterFunction(option, searchValue);
133
133
  }
134
+ return defaultFilterFunction(option, searchValue);
134
135
  });
135
136
  }, [defaultOptions, search, searchValue]);
136
137
 
137
138
  const selectableOptions = useMemo(() => options.filter(isActionableOption), [options]);
138
139
  const focusedOption = selectableOptions[keyboardFocusedOptionIndex];
139
140
 
140
- const computedId = id || fallbackButtonId;
141
+ const fallbackButtonId = useId();
142
+ const computedId = id || inputAttributes.id || fallbackButtonId;
141
143
  const listboxId = `${computedId}-listbox`;
142
144
  const searchBoxId = `${computedId}-searchbox`;
143
145
 
@@ -280,7 +282,7 @@ export default function Select({
280
282
  useEffect(() => {
281
283
  if (open) {
282
284
  if (!isMobile || searchValue) {
283
- if (isSearchEnabled && !!searchBoxReference.current) {
285
+ if (isSearchEnabled && searchBoxReference.current) {
284
286
  searchBoxReference.current.focus();
285
287
  }
286
288
  if (
@@ -496,6 +498,7 @@ export default function Select({
496
498
  >
497
499
  <Button
498
500
  ref={dropdownButtonReference}
501
+ {...inputAttributes}
499
502
  id={computedId}
500
503
  block={block}
501
504
  size={size}
@@ -585,9 +588,6 @@ Select.propTypes = {
585
588
  * if `function` you can define your own search function to implement custom search experience. This search function used while filtering the options array. The custom search function takes two parameters. First is the option the second is the keyword.
586
589
  */
587
590
  search: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
588
- onChange: PropTypes.func.isRequired,
589
- onFocus: PropTypes.func,
590
- onBlur: PropTypes.func,
591
591
  options: PropTypes.arrayOf(
592
592
  PropTypes.shape({
593
593
  value: PropTypes.any,
@@ -602,17 +602,20 @@ Select.propTypes = {
602
602
  searchStrings: PropTypes.arrayOf(PropTypes.string),
603
603
  }),
604
604
  ).isRequired,
605
- /**
606
- * To have full control of your search value and response use `onSearchChange` function combined with `searchValue` and custom filtering on the options array.
607
- * DO NOT USE TOGETHER WITH `search` PROPERTY
608
- */
609
- onSearchChange: PropTypes.func,
610
605
  searchValue: PropTypes.string,
611
606
  searchPlaceholder: PropTypes.string,
612
607
  classNames: PropTypes.objectOf(PropTypes.string),
613
608
  dropdownUp: PropTypes.bool,
614
609
  buttonProps: PropTypes.object,
615
610
  dropdownProps: PropTypes.object,
611
+ onChange: PropTypes.func.isRequired,
612
+ onFocus: PropTypes.func,
613
+ onBlur: PropTypes.func,
614
+ /**
615
+ * To have full control of your search value and response use `onSearchChange` function combined with `searchValue` and custom filtering on the options array.
616
+ * DO NOT USE TOGETHER WITH `search` PROPERTY
617
+ */
618
+ onSearchChange: PropTypes.func,
616
619
  };
617
620
 
618
621
  Select.defaultProps = {
@@ -0,0 +1,17 @@
1
+ import { Field } from '../field/Field';
2
+ import { mockMatchMedia, render, screen } from '../test-utils';
3
+ import Select from './Select';
4
+
5
+ mockMatchMedia();
6
+
7
+ describe('Select', () => {
8
+ it('supports `Field` for labeling', () => {
9
+ const options = [{ value: 'USD', label: 'USD' }];
10
+ render(
11
+ <Field label="Currency">
12
+ <Select options={options} selected={options[0]} onChange={() => {}} />
13
+ </Field>,
14
+ );
15
+ expect(screen.getByLabelText('Currency')).toHaveTextContent('USD');
16
+ });
17
+ });
@@ -5,11 +5,6 @@ import Select from '.';
5
5
 
6
6
  mockMatchMedia();
7
7
 
8
- jest.mock('../common/domHelpers', () => ({
9
- ...jest.requireActual('../common/domHelpers'),
10
- getSimpleRandomId: jest.fn((prefix) => `${prefix}mock-random-id`),
11
- }));
12
-
13
8
  function enableDesktopScreen() {
14
9
  window.innerWidth = Breakpoint.LARGE;
15
10
  }
@@ -326,8 +321,8 @@ describe('Select', () => {
326
321
  const button = screen.getByRole('button');
327
322
  const options = screen.getByRole('listbox');
328
323
 
329
- expect(button).toHaveAttribute('id', 'np-select-mock-random-id');
330
- expect(options).toHaveAttribute('id', 'np-select-mock-random-id-listbox');
324
+ expect(button).toHaveAttribute('id');
325
+ expect(options).toHaveAttribute('id');
331
326
  });
332
327
 
333
328
  it('renders controls with passed id', async () => {
@@ -0,0 +1,16 @@
1
+ import { Field } from '../field/Field';
2
+ import { mockMatchMedia, render, screen } from '../test-utils';
3
+ import Typeahead from './Typeahead';
4
+
5
+ mockMatchMedia();
6
+
7
+ describe('Typeahead', () => {
8
+ it('supports `Field` for labeling', () => {
9
+ render(
10
+ <Field id="test" label="Tags">
11
+ <Typeahead id="test" name="test" options={[{ label: 'Test' }]} onChange={() => {}} />
12
+ </Field>,
13
+ );
14
+ expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Tags/);
15
+ });
16
+ });
@@ -18,6 +18,7 @@ import {
18
18
  } from '../common/domHelpers';
19
19
  import InlineAlert from '../inlineAlert';
20
20
  import { InlineAlertProps } from '../inlineAlert/InlineAlert';
21
+ import { withInputAttributes, WithInputAttributesProps } from '../inputs/contexts';
21
22
 
22
23
  import TypeaheadInput from './typeaheadInput/TypeaheadInput';
23
24
  import TypeaheadOption from './typeaheadOption/TypeaheadOption';
@@ -68,6 +69,8 @@ export interface TypeaheadProps<T> {
68
69
  validateChip?: (chip: TypeaheadOption<T>) => boolean;
69
70
  }
70
71
 
72
+ type TypeaheadPropsWithInputAttributes<T> = TypeaheadProps<T> & Partial<WithInputAttributesProps>;
73
+
71
74
  type TypeaheadState<T> = {
72
75
  selected: readonly TypeaheadOption<T>[];
73
76
  keyboardFocusedOptionIndex: number | null;
@@ -77,9 +80,9 @@ type TypeaheadState<T> = {
77
80
  isFocused: boolean;
78
81
  };
79
82
 
80
- export default class Typeahead<T> extends Component<TypeaheadProps<T>, TypeaheadState<T>> {
81
- declare props: TypeaheadProps<T> &
82
- Required<Pick<TypeaheadProps<T>, keyof typeof Typeahead.defaultProps>>;
83
+ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, TypeaheadState<T>> {
84
+ declare props: TypeaheadPropsWithInputAttributes<T> &
85
+ Required<Pick<TypeaheadPropsWithInputAttributes<T>, keyof typeof Typeahead.defaultProps>>;
83
86
 
84
87
  static defaultProps = {
85
88
  allowNew: false,
@@ -98,7 +101,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
98
101
  validateChip: () => true,
99
102
  } satisfies Partial<TypeaheadProps<unknown>>;
100
103
 
101
- constructor(props: TypeaheadProps<T>) {
104
+ constructor(props: TypeaheadPropsWithInputAttributes<T>) {
102
105
  super(props);
103
106
  const { searchDelay, initialValue, multiple } = this.props;
104
107
  this.handleSearchDebounced = debounce(this.handleSearch, searchDelay);
@@ -115,7 +118,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
115
118
 
116
119
  handleSearchDebounced: DebouncedFunc<Typeahead<T>['handleSearch']>;
117
120
 
118
- UNSAFE_componentWillReceiveProps(nextProps: TypeaheadProps<T>) {
121
+ UNSAFE_componentWillReceiveProps(nextProps: TypeaheadPropsWithInputAttributes<T>) {
119
122
  if (nextProps.multiple !== this.props.multiple) {
120
123
  this.setState((previousState) => {
121
124
  const { selected } = previousState;
@@ -374,7 +377,10 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
374
377
  allowNew,
375
378
  showNewEntry,
376
379
  dropdownOpen,
377
- }: Pick<TypeaheadProps<T>, 'footer' | 'options' | 'id' | 'allowNew' | 'showNewEntry'> &
380
+ }: Pick<
381
+ TypeaheadPropsWithInputAttributes<T>,
382
+ 'footer' | 'options' | 'id' | 'allowNew' | 'showNewEntry'
383
+ > &
378
384
  Pick<TypeaheadState<T>, 'keyboardFocusedOptionIndex' | 'query'> & {
379
385
  dropdownOpen: boolean;
380
386
  }) => {
@@ -414,7 +420,8 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
414
420
 
415
421
  render() {
416
422
  const {
417
- id,
423
+ inputAttributes,
424
+ id: idProp,
418
425
  placeholder,
419
426
  multiple,
420
427
  size,
@@ -432,6 +439,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
432
439
  alert,
433
440
  inputAutoComplete,
434
441
  } = this.props;
442
+ const id = idProp ?? inputAttributes?.id;
435
443
 
436
444
  const { errorState, query, selected, optionsShown, keyboardFocusedOptionIndex } = this.state;
437
445
 
@@ -457,6 +465,8 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
457
465
  const hasInfo = displayAlert && alertType === Sentiment.NEUTRAL;
458
466
  return (
459
467
  <div
468
+ role="group"
469
+ {...inputAttributes}
460
470
  id={id}
461
471
  className={classNames('typeahead', `typeahead-${size}`, {
462
472
  'typeahead--has-value': selected.length > 0,
@@ -515,3 +525,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
515
525
  );
516
526
  }
517
527
  }
528
+
529
+ export default withInputAttributes(Typeahead, { nonLabelable: true }) as <T>(
530
+ props: TypeaheadProps<T>,
531
+ ) => React.ReactElement;