@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.
- package/build/index.js +764 -474
- package/build/index.js.map +1 -1
- package/build/index.mjs +763 -474
- package/build/index.mjs.map +1 -1
- package/build/main.css +135 -0
- package/build/styles/carousel/Carousel.css +135 -0
- package/build/styles/main.css +135 -0
- package/build/types/carousel/Carousel.d.ts +26 -0
- package/build/types/carousel/Carousel.d.ts.map +1 -0
- package/build/types/carousel/index.d.ts +3 -0
- package/build/types/carousel/index.d.ts.map +1 -0
- package/build/types/common/card/Card.d.ts +2 -2
- package/build/types/common/card/Card.d.ts.map +1 -1
- package/build/types/common/domHelpers/documentIosClick.d.ts +0 -1
- package/build/types/common/domHelpers/documentIosClick.d.ts.map +1 -1
- package/build/types/common/domHelpers/index.d.ts +1 -1
- package/build/types/common/domHelpers/index.d.ts.map +1 -1
- package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.d.ts +4 -2
- package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/promoCard/PromoCard.d.ts +16 -5
- package/build/types/promoCard/PromoCard.d.ts.map +1 -1
- package/build/types/select/Select.d.ts +7 -7
- package/build/types/select/Select.d.ts.map +1 -1
- package/build/types/typeahead/Typeahead.d.ts +4 -55
- package/build/types/typeahead/Typeahead.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/carousel/Carousel.css +135 -0
- package/src/carousel/Carousel.less +133 -0
- package/src/carousel/Carousel.spec.tsx +221 -0
- package/src/carousel/Carousel.story.tsx +63 -0
- package/src/carousel/Carousel.tsx +345 -0
- package/src/carousel/index.ts +3 -0
- package/src/common/card/Card.tsx +51 -43
- package/src/common/domHelpers/documentIosClick.ts +0 -5
- package/src/common/domHelpers/index.ts +0 -1
- package/src/dateLookup/DateLookup.rtl.spec.tsx +2 -3
- package/src/dateLookup/DateLookup.tsx +1 -3
- package/src/index.ts +2 -0
- package/src/inputs/SelectInput.spec.tsx +1 -1
- package/src/main.css +135 -0
- package/src/main.less +1 -0
- package/src/moneyInput/MoneyInput.rtl.spec.tsx +10 -0
- package/src/moneyInput/MoneyInput.spec.js +10 -5
- package/src/moneyInput/MoneyInput.tsx +21 -14
- package/src/phoneNumberInput/PhoneNumberInput.rtl.spec.tsx +10 -0
- package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -2
- package/src/promoCard/PromoCard.story.tsx +2 -2
- package/src/promoCard/PromoCard.tsx +30 -9
- package/src/select/Select.js +18 -15
- package/src/select/Select.rtl.spec.tsx +17 -0
- package/src/select/Select.spec.js +2 -7
- package/src/typeahead/Typeahead.rtl.spec.tsx +16 -0
- 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
|
@@ -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) =>
|
|
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) =>
|
|
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).
|
|
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')).
|
|
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<
|
|
121
|
-
declare props:
|
|
122
|
-
Required<Pick<
|
|
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<
|
|
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
|
|
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
|
|
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={
|
|
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
|
|
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':
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
335
|
+
ref,
|
|
315
336
|
tabIndex: 0,
|
|
316
337
|
}
|
|
317
338
|
: {};
|
package/src/select/Select.js
CHANGED
|
@@ -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
|
|
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
|
|
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 &&
|
|
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
|
+
});
|