@transferwise/components 46.31.0 → 46.33.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 (58) hide show
  1. package/build/index.js +764 -474
  2. package/build/index.js.map +1 -1
  3. package/build/index.mjs +763 -474
  4. package/build/index.mjs.map +1 -1
  5. package/build/main.css +135 -0
  6. package/build/styles/carousel/Carousel.css +135 -0
  7. package/build/styles/main.css +135 -0
  8. package/build/types/carousel/Carousel.d.ts +26 -0
  9. package/build/types/carousel/Carousel.d.ts.map +1 -0
  10. package/build/types/carousel/index.d.ts +3 -0
  11. package/build/types/carousel/index.d.ts.map +1 -0
  12. package/build/types/common/card/Card.d.ts +2 -2
  13. package/build/types/common/card/Card.d.ts.map +1 -1
  14. package/build/types/common/domHelpers/documentIosClick.d.ts +0 -1
  15. package/build/types/common/domHelpers/documentIosClick.d.ts.map +1 -1
  16. package/build/types/common/domHelpers/index.d.ts +1 -1
  17. package/build/types/common/domHelpers/index.d.ts.map +1 -1
  18. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  19. package/build/types/index.d.ts +2 -0
  20. package/build/types/index.d.ts.map +1 -1
  21. package/build/types/moneyInput/MoneyInput.d.ts +4 -2
  22. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  23. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
  24. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  25. package/build/types/promoCard/PromoCard.d.ts +16 -5
  26. package/build/types/promoCard/PromoCard.d.ts.map +1 -1
  27. package/build/types/select/Select.d.ts +7 -7
  28. package/build/types/select/Select.d.ts.map +1 -1
  29. package/build/types/typeahead/Typeahead.d.ts +4 -55
  30. package/build/types/typeahead/Typeahead.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/carousel/Carousel.css +135 -0
  33. package/src/carousel/Carousel.less +133 -0
  34. package/src/carousel/Carousel.spec.tsx +221 -0
  35. package/src/carousel/Carousel.story.tsx +63 -0
  36. package/src/carousel/Carousel.tsx +345 -0
  37. package/src/carousel/index.ts +3 -0
  38. package/src/common/card/Card.tsx +51 -43
  39. package/src/common/domHelpers/documentIosClick.ts +0 -5
  40. package/src/common/domHelpers/index.ts +0 -1
  41. package/src/dateLookup/DateLookup.rtl.spec.tsx +2 -3
  42. package/src/dateLookup/DateLookup.tsx +1 -3
  43. package/src/index.ts +2 -0
  44. package/src/inputs/SelectInput.spec.tsx +1 -1
  45. package/src/main.css +135 -0
  46. package/src/main.less +1 -0
  47. package/src/moneyInput/MoneyInput.rtl.spec.tsx +10 -0
  48. package/src/moneyInput/MoneyInput.spec.js +10 -5
  49. package/src/moneyInput/MoneyInput.tsx +21 -14
  50. package/src/phoneNumberInput/PhoneNumberInput.rtl.spec.tsx +10 -0
  51. package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -2
  52. package/src/promoCard/PromoCard.story.tsx +2 -2
  53. package/src/promoCard/PromoCard.tsx +30 -9
  54. package/src/select/Select.js +18 -15
  55. package/src/select/Select.rtl.spec.tsx +17 -0
  56. package/src/select/Select.spec.js +2 -7
  57. package/src/typeahead/Typeahead.rtl.spec.tsx +16 -0
  58. package/src/typeahead/Typeahead.tsx +21 -7
package/src/main.css CHANGED
@@ -643,6 +643,141 @@ div.critical-comms .critical-comms-body {
643
643
  border-radius: 16px 16px 0 0;
644
644
  border-radius: var(--radius-medium) var(--radius-medium) 0 0;
645
645
  }
646
+ .carousel-wrapper {
647
+ overflow: hidden;
648
+ }
649
+ .carousel {
650
+ display: flex;
651
+ align-items: center;
652
+ overflow-x: scroll;
653
+ overflow-y: hidden;
654
+ scroll-snap-type: x mandatory;
655
+ scroll-behavior: smooth;
656
+ gap: 16px;
657
+ gap: var(--size-16);
658
+ padding: 8px;
659
+ padding: var(--size-8);
660
+ margin: 8px;
661
+ margin: var(--size-8);
662
+ }
663
+ @media (max-width: 767px) {
664
+ .carousel {
665
+ gap: 8px;
666
+ gap: var(--size-8);
667
+ }
668
+ }
669
+ .carousel__header {
670
+ display: flex;
671
+ align-items: center;
672
+ overflow: hidden;
673
+ min-height: 32px;
674
+ min-height: var(--size-32);
675
+ padding-bottom: 16px;
676
+ padding-bottom: var(--size-16);
677
+ }
678
+ .carousel__card,
679
+ .carousel__card:hover,
680
+ .carousel__card:focus,
681
+ .carousel__card:focus-within {
682
+ -webkit-text-decoration: none;
683
+ text-decoration: none;
684
+ transition: none !important;
685
+ box-shadow: none !important;
686
+ }
687
+ .carousel__card {
688
+ display: block;
689
+ position: relative;
690
+ text-align: left;
691
+ border: none;
692
+ overflow: hidden;
693
+ background: rgba(134,167,189,0.10196);
694
+ background: var(--color-background-neutral);
695
+ border-radius: 32px;
696
+ border-radius: var(--size-32);
697
+ scroll-snap-align: center;
698
+ -webkit-scroll-snap-align: center;
699
+ transition: all 0.4s !important;
700
+ }
701
+ @media (min-width: 1200px) {
702
+ .carousel__card {
703
+ min-width: 280px;
704
+ width: 280px;
705
+ height: 280px;
706
+ }
707
+ }
708
+ @media (max-width: 1199px) {
709
+ .carousel__card {
710
+ min-width: 242px;
711
+ width: 242px;
712
+ height: 242px;
713
+ }
714
+ }
715
+ @media (max-width: 767px) {
716
+ .carousel__card {
717
+ min-width: 336px;
718
+ width: 336px;
719
+ height: 336px;
720
+ scroll-snap-stop: always;
721
+ }
722
+ }
723
+ .carousel__card:focus,
724
+ .carousel__card:has(:focus-visible) {
725
+ outline: var(--ring-outline-color) solid var(--ring-outline-width) !important;
726
+ outline-offset: var(--ring-outline-offset) !important;
727
+ }
728
+ .carousel__card:hover {
729
+ background-color: var(--color-background-neutral-hover);
730
+ }
731
+ .carousel__card:focus {
732
+ background-color: var(--color-background-neutral-hover);
733
+ }
734
+ .carousel__card-content {
735
+ height: 100%;
736
+ font-weight: normal;
737
+ padding: 24px;
738
+ padding: var(--size-24);
739
+ }
740
+ .carousel__scroll-button {
741
+ width: 32px;
742
+ width: var(--size-32);
743
+ height: 32px;
744
+ height: var(--size-32);
745
+ align-items: center;
746
+ justify-content: center;
747
+ }
748
+ .carousel__indicators {
749
+ display: flex;
750
+ justify-content: center;
751
+ padding-top: 8px;
752
+ padding-top: var(--size-8);
753
+ gap: 8px;
754
+ gap: var(--size-8);
755
+ }
756
+ .carousel__indicator {
757
+ width: 12px;
758
+ width: var(--size-12);
759
+ height: 12px;
760
+ height: var(--size-12);
761
+ border-radius: 8px;
762
+ border-radius: var(--size-8);
763
+ background: #c9cbce;
764
+ background: var(--color-interactive-secondary);
765
+ border: none;
766
+ -webkit-appearance: none;
767
+ -moz-appearance: none;
768
+ appearance: none;
769
+ transition: all 0.1s;
770
+ }
771
+ .carousel__indicator:hover {
772
+ width: 16px;
773
+ width: var(--size-16);
774
+ }
775
+ .carousel__indicator--selected,
776
+ .carousel__indicator--selected:hover {
777
+ background: var(--color-interactive-primary);
778
+ width: 24px;
779
+ width: var(--size-24);
780
+ }
646
781
  .np-checkbox-button input[type="checkbox"] {
647
782
  position: absolute;
648
783
  width: 24px;
package/src/main.less CHANGED
@@ -5,6 +5,7 @@
5
5
  @import "./badge/Badge.less";
6
6
  @import "./button/Button.less";
7
7
  @import "./card/Card.less";
8
+ @import "./carousel/Carousel.less";
8
9
  @import "./checkboxButton/CheckboxButton.less";
9
10
  @import "./chips/Chip.less";
10
11
  @import "./circularButton/CircularButton.less";
@@ -1,3 +1,4 @@
1
+ import { Field } from '../field/Field';
1
2
  import { mockMatchMedia, mockResizeObserver, render, screen, userEvent } from '../test-utils';
2
3
 
3
4
  import MoneyInput from './MoneyInput';
@@ -80,4 +81,13 @@ describe('MoneyInput', () => {
80
81
 
81
82
  expect(screen.getByLabelText('Prioritized label')).toHaveClass('input-group');
82
83
  });
84
+
85
+ it('supports `Field` for labeling', () => {
86
+ render(
87
+ <Field label="Recipient gets">
88
+ <MoneyInput {...props} />
89
+ </Field>,
90
+ );
91
+ expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Recipient gets/);
92
+ });
83
93
  });
@@ -6,6 +6,11 @@ import { mockMatchMedia, mockResizeObserver } from '../test-utils';
6
6
  mockMatchMedia();
7
7
  mockResizeObserver();
8
8
 
9
+ jest.mock('../inputs/contexts', () => ({
10
+ ...jest.requireActual('../inputs/contexts'),
11
+ withInputAttributes: (Component) => Component,
12
+ }));
13
+
9
14
  jest.mock('./currencyFormatting', () => ({
10
15
  parseAmount: jest.fn(),
11
16
  formatAmount: jest.fn(),
@@ -18,11 +23,11 @@ jest.mock('react-intl', () => ({
18
23
  injectIntl: (Component) =>
19
24
  function (props) {
20
25
  return (
21
- <Component {...props} intl={{ locale: defaultLocale, formatMessage: (id) => `${id}` }} />
26
+ <Component {...props} intl={{ locale: defaultLocale, formatMessage: (id) => String(id) }} />
22
27
  );
23
28
  },
24
29
  defineMessages: (translations) => translations,
25
- useIntl: () => ({ locale: defaultLocale, formatMessage: (id) => `${id}` }),
30
+ useIntl: () => ({ locale: defaultLocale, formatMessage: (id) => String(id) }),
26
31
  }));
27
32
 
28
33
  describe('Money Input', () => {
@@ -510,7 +515,7 @@ describe('Money Input', () => {
510
515
 
511
516
  it('formats the number you input after you blur it', () => {
512
517
  component.setProps({ numberFormatPrecision: 3 });
513
- jest.spyOn(numberFormatting, 'parseAmount').mockImplementation(parseFloat);
518
+ jest.spyOn(numberFormatting, 'parseAmount').mockImplementation(Number.parseFloat);
514
519
  enterAmount('123.45');
515
520
  expect(amountInput().prop('value')).toBe('123.45');
516
521
 
@@ -535,7 +540,7 @@ describe('Money Input', () => {
535
540
  enterAmount('500.1234');
536
541
  expect(onAmountChange).toHaveBeenCalledTimes(1);
537
542
  expect(onAmountChange).toHaveBeenLastCalledWith(500.1);
538
- expect(assertions).toStrictEqual(1);
543
+ expect(assertions).toBe(1);
539
544
  });
540
545
 
541
546
  it('does call onAmountChange when input value is empty', () => {
@@ -565,7 +570,7 @@ describe('Money Input', () => {
565
570
  );
566
571
 
567
572
  it('passes the id given to the input element', () => {
568
- expect(amountInput().prop('id')).not.toBeDefined();
573
+ expect(amountInput().prop('id')).toBeUndefined();
569
574
  component.setProps({ id: 'some-id' });
570
575
 
571
576
  expect(amountInput().prop('id')).toBe('some-id');
@@ -6,6 +6,7 @@ import { injectIntl, WrappedComponentProps } from 'react-intl';
6
6
 
7
7
  import { Typography } from '../common';
8
8
  import { Size, SizeLarge, SizeMedium, SizeSmall } from '../common/propsValues/size';
9
+ import { withInputAttributes, WithInputAttributesProps } from '../inputs/contexts';
9
10
  import { Input } from '../inputs/Input';
10
11
  import {
11
12
  SelectInput,
@@ -49,9 +50,8 @@ const formatAmountIfSet = ({
49
50
  }) => {
50
51
  if (maxLengthOverride) {
51
52
  return amount != null ? String(amount) : '';
52
- } else {
53
- return typeof amount === 'number' ? formatAmount(amount, currency, locale) : '';
54
53
  }
54
+ return typeof amount === 'number' ? formatAmount(amount, currency, locale) : '';
55
55
  };
56
56
 
57
57
  const parseNumber = ({
@@ -111,21 +111,23 @@ export interface MoneyInputProps extends WrappedComponentProps {
111
111
  maxLengthOverride?: number;
112
112
  }
113
113
 
114
+ type MoneyInputPropsWithInputAttributes = MoneyInputProps & Partial<WithInputAttributesProps>;
115
+
114
116
  interface MoneyInputState {
115
117
  searchQuery: string;
116
118
  formattedAmount: string;
117
119
  locale: string;
118
120
  }
119
121
 
120
- class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
121
- declare props: MoneyInputProps &
122
- Required<Pick<MoneyInputProps, keyof typeof MoneyInput.defaultProps>>;
122
+ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInputState> {
123
+ declare props: MoneyInputPropsWithInputAttributes &
124
+ Required<Pick<MoneyInputPropsWithInputAttributes, keyof typeof MoneyInput.defaultProps>>;
123
125
 
124
126
  static defaultProps = {
125
127
  size: Size.LARGE,
126
128
  classNames: {},
127
129
  selectProps: {},
128
- } satisfies Partial<MoneyInputProps>;
130
+ } satisfies Partial<MoneyInputPropsWithInputAttributes>;
129
131
 
130
132
  amountFocused = false;
131
133
 
@@ -160,7 +162,7 @@ class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
160
162
 
161
163
  isInputAllowedForKeyEvent = (event: React.KeyboardEvent<HTMLInputElement>) => {
162
164
  const { metaKey, key, ctrlKey } = event;
163
- const isNumberKey = isNumber(parseInt(key, 10));
165
+ const isNumberKey = isNumber(Number.parseInt(key, 10));
164
166
 
165
167
  return isNumberKey || metaKey || ctrlKey || allowedInputKeys.has(key);
166
168
  };
@@ -179,7 +181,7 @@ class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
179
181
  : parseNumber({
180
182
  amount: paste,
181
183
  currency: this.props.selectedCurrency.currency,
182
- locale: locale,
184
+ locale,
183
185
  maxLengthOverride: this.props.maxLengthOverride,
184
186
  });
185
187
 
@@ -188,7 +190,7 @@ class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
188
190
  formattedAmount: formatAmountIfSet({
189
191
  amount: parsed,
190
192
  currency: this.props.selectedCurrency.currency,
191
- locale: locale,
193
+ locale,
192
194
  maxLengthOverride: this.props.maxLengthOverride,
193
195
  }),
194
196
  });
@@ -297,15 +299,18 @@ class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
297
299
 
298
300
  render() {
299
301
  const {
302
+ inputAttributes,
303
+ id: amountInputId,
304
+ 'aria-labelledby': ariaLabelledByProp,
300
305
  selectedCurrency,
301
306
  onCurrencyChange,
302
307
  size,
303
308
  addon,
304
- id,
305
- 'aria-labelledby': ariaLabelledBy,
306
309
  selectProps,
307
310
  maxLengthOverride,
308
311
  } = this.props;
312
+ const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes?.['aria-labelledby'];
313
+
309
314
  const selectOptions = this.getSelectOptions();
310
315
 
311
316
  const hasSingleCurrency = () => {
@@ -335,6 +340,8 @@ class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
335
340
  const disabled = !this.props.onAmountChange;
336
341
  return (
337
342
  <div
343
+ role="group"
344
+ {...inputAttributes}
338
345
  aria-labelledby={ariaLabelledBy}
339
346
  className={classNames(
340
347
  this.style('tw-money-input'),
@@ -343,7 +350,7 @@ class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
343
350
  )}
344
351
  >
345
352
  <Input
346
- id={id}
353
+ id={amountInputId}
347
354
  value={this.state.formattedAmount}
348
355
  inputMode="decimal"
349
356
  disabled={disabled}
@@ -486,7 +493,7 @@ function currencyOptionFitsQuery(option: CurrencyOptionItem, query: string) {
486
493
  }
487
494
 
488
495
  function contains(property: string | undefined, query: string) {
489
- return property && property.toLowerCase().includes(query.toLowerCase());
496
+ return property?.toLowerCase().includes(query.toLowerCase());
490
497
  }
491
498
 
492
499
  function sortOptionsLabelsToFirst(options: readonly CurrencyOptionItem[], query: string) {
@@ -507,4 +514,4 @@ function sortOptionsLabelsToFirst(options: readonly CurrencyOptionItem[], query:
507
514
  });
508
515
  }
509
516
 
510
- export default injectIntl(MoneyInput);
517
+ export default injectIntl(withInputAttributes(MoneyInput, { nonLabelable: true }));
@@ -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,7 +1,7 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { StarFill } from '@transferwise/icons';
3
3
 
4
- import PromoCard, { PromoCardCheckedProps, PromoCardLinkProps } from './PromoCard';
4
+ import PromoCard, { type PromoCardCheckedProps, type PromoCardLinkProps } from './PromoCard';
5
5
 
6
6
  const meta: Meta<typeof PromoCard> = {
7
7
  component: PromoCard,
@@ -1,19 +1,19 @@
1
1
  import { useId } from '@radix-ui/react-id';
2
2
  import { Check } from '@transferwise/icons';
3
3
  import classNames from 'classnames';
4
- import React, { forwardRef, FunctionComponent, useEffect, useState } from 'react';
4
+ import React, { forwardRef, type FunctionComponent, useEffect, useState } from 'react';
5
5
 
6
6
  import Body from '../body';
7
7
  import { Typography } from '../common';
8
- import Card, { CardProps } from '../common/card';
8
+ import Card, { type CardProps } from '../common/card';
9
9
  import Display from '../display';
10
10
  import Image from '../image/Image';
11
11
  import Title from '../title';
12
12
 
13
13
  import { usePromoCardContext } from './PromoCardContext';
14
- import PromoCardIndicator, { PromoCardIndicatorProps } from './PromoCardIndicator';
14
+ import PromoCardIndicator, { type PromoCardIndicatorProps } from './PromoCardIndicator';
15
15
 
16
- export type ReferenceType = React.Ref<HTMLInputElement>;
16
+ export type ReferenceType = React.Ref<HTMLInputElement> | React.Ref<HTMLDivElement>;
17
17
  export type RelatedTypes =
18
18
  | ''
19
19
  | 'alternate'
@@ -68,6 +68,9 @@ export interface PromoCardCommonProps {
68
68
  /** Specify an onClick event handler */
69
69
  onClick?: () => void;
70
70
 
71
+ /** Specify an onKeyDown event handler */
72
+ onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
73
+
71
74
  /** Optional prop to specify the ID used for testing */
72
75
  testId?: string;
73
76
 
@@ -76,6 +79,8 @@ export interface PromoCardCommonProps {
76
79
 
77
80
  /** Set to false to use body font style for the title */
78
81
  useDisplayFont?: boolean;
82
+
83
+ ref?: ReferenceType;
79
84
  }
80
85
 
81
86
  export interface PromoCardLinkProps extends PromoCardCommonProps, Omit<CardProps, 'children'> {
@@ -91,6 +96,14 @@ export interface PromoCardLinkProps extends PromoCardCommonProps, Omit<CardProps
91
96
  /** Optionally specify the language of the linked URL */
92
97
  hrefLang?: string;
93
98
 
99
+ /** Optional property that can be pass a ref for the anchor. */
100
+ anchorRef?: React.Ref<HTMLAnchorElement>;
101
+
102
+ /**
103
+ * Optional prop to specify the ID of the anchor element which can be useful when using a ref.
104
+ */
105
+ anchorId?: string;
106
+
94
107
  /**
95
108
  * Relationship between the PromoCard href URL and the current page. See
96
109
  * [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel).
@@ -105,7 +118,7 @@ export interface PromoCardLinkProps extends PromoCardCommonProps, Omit<CardProps
105
118
  isChecked?: never;
106
119
  tabIndex?: never;
107
120
  type?: never;
108
- reference?: never;
121
+ ref?: ReferenceType;
109
122
  value?: never;
110
123
  }
111
124
 
@@ -120,7 +133,7 @@ export interface PromoCardCheckedProps extends PromoCardCommonProps, Omit<CardPr
120
133
  tabIndex?: number;
121
134
 
122
135
  /** Optional property to provide component Ref */
123
- reference?: ReferenceType;
136
+ ref?: ReferenceType;
124
137
 
125
138
  /** Optional prop to specify the input type of the PromoCard */
126
139
  type?: 'checkbox' | 'radio';
@@ -131,6 +144,8 @@ export interface PromoCardCheckedProps extends PromoCardCommonProps, Omit<CardPr
131
144
  /** Only applies to <a />s */
132
145
  download?: never;
133
146
  href?: never;
147
+ anchorRef?: never;
148
+ anchorId?: never;
134
149
  hrefLang?: never;
135
150
  rel?: never;
136
151
  target?: never;
@@ -202,6 +217,7 @@ const PromoCard: FunctionComponent<PromoCardProps> = forwardRef(
202
217
  isChecked,
203
218
  isDisabled,
204
219
  onClick,
220
+ onKeyDown,
205
221
  rel,
206
222
  tabIndex,
207
223
  target,
@@ -211,9 +227,11 @@ const PromoCard: FunctionComponent<PromoCardProps> = forwardRef(
211
227
  value,
212
228
  isSmall,
213
229
  useDisplayFont = true,
230
+ anchorRef,
231
+ anchorId,
214
232
  ...props
215
233
  },
216
- reference,
234
+ ref: ReferenceType,
217
235
  ) => {
218
236
  // Set the `checked` state to the value of `defaultChecked` if it is truthy,
219
237
  // or the value of `isChecked` if it is truthy, or `false` if neither
@@ -276,7 +294,8 @@ const PromoCard: FunctionComponent<PromoCardProps> = forwardRef(
276
294
  id: componentId,
277
295
  isDisabled: isDisabled || contextIsDisabled,
278
296
  onClick,
279
- ref: reference,
297
+ onKeyDown,
298
+ ref,
280
299
  'data-testid': testId,
281
300
  isSmall,
282
301
  };
@@ -291,6 +310,8 @@ const PromoCard: FunctionComponent<PromoCardProps> = forwardRef(
291
310
  hrefLang,
292
311
  rel,
293
312
  target,
313
+ ref: anchorRef,
314
+ id: anchorId,
294
315
  }
295
316
  : {};
296
317
 
@@ -311,7 +332,7 @@ const PromoCard: FunctionComponent<PromoCardProps> = forwardRef(
311
332
  handleClick();
312
333
  }
313
334
  },
314
- ref: reference,
335
+ ref,
315
336
  tabIndex: 0,
316
337
  }
317
338
  : {};
@@ -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
+ });