@transferwise/components 46.121.2 → 46.122.1
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/inputs/SelectInput.js +24 -6
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +24 -6
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.js +1 -4
- package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.mjs +1 -4
- package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +2 -2
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/inputs/SelectInput.spec.tsx +123 -0
- package/src/inputs/SelectInput.tsx +39 -5
- package/src/prompt/InlinePrompt/InlinePrompt.spec.tsx +10 -18
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +9 -6
- package/src/prompt/InlinePrompt/InlinePrompt.tsx +4 -7
|
@@ -93,8 +93,13 @@ export type SelectInputItem<T = string> =
|
|
|
93
93
|
function dedupeSelectInputOptionItem<T>(
|
|
94
94
|
item: SelectInputOptionItem<T>,
|
|
95
95
|
existingValues: Set<T>,
|
|
96
|
+
compareValues?: (a: T, b: T) => boolean,
|
|
96
97
|
): SelectInputOptionItem<T | undefined> {
|
|
97
|
-
|
|
98
|
+
const isDuplicate = compareValues
|
|
99
|
+
? Array.from(existingValues).some((existingValue) => compareValues(item.value, existingValue))
|
|
100
|
+
: existingValues.has(item.value);
|
|
101
|
+
|
|
102
|
+
if (!isDuplicate) {
|
|
98
103
|
existingValues.add(item.value);
|
|
99
104
|
return item;
|
|
100
105
|
}
|
|
@@ -108,18 +113,20 @@ function dedupeSelectInputOptionItem<T>(
|
|
|
108
113
|
*/
|
|
109
114
|
function dedupeSelectInputItems<T>(
|
|
110
115
|
items: readonly SelectInputItem<T>[],
|
|
116
|
+
compareValues?: (a: T, b: T) => boolean,
|
|
111
117
|
): SelectInputItem<T | undefined>[] {
|
|
112
118
|
const existingValues = new Set<T>();
|
|
119
|
+
|
|
113
120
|
return items.map((item) => {
|
|
114
121
|
switch (item.type) {
|
|
115
122
|
case 'option': {
|
|
116
|
-
return dedupeSelectInputOptionItem(item, existingValues);
|
|
123
|
+
return dedupeSelectInputOptionItem(item, existingValues, compareValues);
|
|
117
124
|
}
|
|
118
125
|
case 'group': {
|
|
119
126
|
return {
|
|
120
127
|
...item,
|
|
121
128
|
options: item.options.map((option) =>
|
|
122
|
-
dedupeSelectInputOptionItem(option, existingValues),
|
|
129
|
+
dedupeSelectInputOptionItem(option, existingValues, compareValues),
|
|
123
130
|
),
|
|
124
131
|
};
|
|
125
132
|
}
|
|
@@ -481,6 +488,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
481
488
|
id={id ? `${id}Search` : undefined}
|
|
482
489
|
parentId={parentId}
|
|
483
490
|
items={items}
|
|
491
|
+
compareValues={compareValues}
|
|
484
492
|
renderValue={renderValue}
|
|
485
493
|
renderFooter={renderFooter}
|
|
486
494
|
filterable={filterable}
|
|
@@ -589,7 +597,14 @@ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContai
|
|
|
589
597
|
|
|
590
598
|
interface SelectInputOptionsProps<T = string> extends Pick<
|
|
591
599
|
SelectInputProps<T>,
|
|
592
|
-
|
|
600
|
+
| 'items'
|
|
601
|
+
| 'renderValue'
|
|
602
|
+
| 'renderFooter'
|
|
603
|
+
| 'filterable'
|
|
604
|
+
| 'filterPlaceholder'
|
|
605
|
+
| 'id'
|
|
606
|
+
| 'parentId'
|
|
607
|
+
| 'compareValues'
|
|
593
608
|
> {
|
|
594
609
|
searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
|
|
595
610
|
listboxRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
@@ -606,6 +621,7 @@ function SelectInputOptions<T = string>({
|
|
|
606
621
|
id,
|
|
607
622
|
parentId,
|
|
608
623
|
items,
|
|
624
|
+
compareValues: compareValuesProp,
|
|
609
625
|
renderValue = String,
|
|
610
626
|
renderFooter,
|
|
611
627
|
filterable = false,
|
|
@@ -651,9 +667,27 @@ function SelectInputOptions<T = string>({
|
|
|
651
667
|
}
|
|
652
668
|
}, [controllerRef, needle]);
|
|
653
669
|
|
|
670
|
+
const compareValues = useMemo(() => {
|
|
671
|
+
if (!compareValuesProp) {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (typeof compareValuesProp === 'function') {
|
|
676
|
+
return (a: NonNullable<T>, b: NonNullable<T>) => compareValuesProp(a, b);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const key = compareValuesProp;
|
|
680
|
+
return (a: NonNullable<T>, b: NonNullable<T>) => {
|
|
681
|
+
if (typeof a === 'object' && a != null && typeof b === 'object' && b != null) {
|
|
682
|
+
return (a as Record<string, unknown>)[key] === (b as Record<string, unknown>)[key];
|
|
683
|
+
}
|
|
684
|
+
return a === b;
|
|
685
|
+
};
|
|
686
|
+
}, [compareValuesProp]);
|
|
687
|
+
|
|
654
688
|
const filteredItems: readonly SelectInputItem<NonNullable<T> | undefined>[] =
|
|
655
689
|
needle != null
|
|
656
|
-
? filterSelectInputItems(dedupeSelectInputItems(items), (item) =>
|
|
690
|
+
? filterSelectInputItems(dedupeSelectInputItems(items, compareValues), (item) =>
|
|
657
691
|
selectInputOptionItemIncludesNeedle(item, needle),
|
|
658
692
|
)
|
|
659
693
|
: items;
|
|
@@ -23,12 +23,12 @@ describe('InlinePrompt', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
[
|
|
26
|
-
{ sentiment: Sentiment.NEGATIVE as const,
|
|
27
|
-
{ sentiment: Sentiment.WARNING as const,
|
|
28
|
-
{ sentiment: Sentiment.NEUTRAL as const,
|
|
29
|
-
{ sentiment: Sentiment.POSITIVE as const,
|
|
30
|
-
{ sentiment: 'proposition' as const,
|
|
31
|
-
].forEach(({ sentiment, statusIconLabel
|
|
26
|
+
{ sentiment: Sentiment.NEGATIVE as const, statusIconLabel: 'Error:' },
|
|
27
|
+
{ sentiment: Sentiment.WARNING as const, statusIconLabel: 'Warning:' },
|
|
28
|
+
{ sentiment: Sentiment.NEUTRAL as const, statusIconLabel: 'Information:' },
|
|
29
|
+
{ sentiment: Sentiment.POSITIVE as const, statusIconLabel: 'Success:' },
|
|
30
|
+
{ sentiment: 'proposition' as const, statusIconLabel: '' },
|
|
31
|
+
].forEach(({ sentiment, statusIconLabel }) => {
|
|
32
32
|
describe(sentiment, () => {
|
|
33
33
|
it('should apply correct styles', () => {
|
|
34
34
|
render(<InlinePrompt {...defaultProps} sentiment={sentiment} />);
|
|
@@ -77,18 +77,10 @@ describe('InlinePrompt', () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
describe('custom media', () => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
85
|
-
} else {
|
|
86
|
-
it('should ignore `media`', () => {
|
|
87
|
-
render(<InlinePrompt {...defaultProps} sentiment={sentiment} media={MEDIA} />);
|
|
88
|
-
expect(screen.getByLabelText(statusIconLabel)).toBeInTheDocument();
|
|
89
|
-
expect(screen.queryByTestId('custom-media')).not.toBeInTheDocument();
|
|
90
|
-
});
|
|
91
|
-
}
|
|
80
|
+
it('should respect `media`', () => {
|
|
81
|
+
render(<InlinePrompt {...defaultProps} sentiment={sentiment} media={MEDIA} />);
|
|
82
|
+
expect(screen.getByTestId('custom-media')).toBeInTheDocument();
|
|
83
|
+
});
|
|
92
84
|
});
|
|
93
85
|
|
|
94
86
|
describe('mediaLabel', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
3
|
import { action } from 'storybook/actions';
|
|
4
|
-
import { Taxi, Travel } from '@transferwise/icons';
|
|
4
|
+
import { Clock, Id, Taxi, Travel } from '@transferwise/icons';
|
|
5
5
|
import { lorem5 } from '../../test-utils';
|
|
6
6
|
import Link from '../../link';
|
|
7
7
|
import { InlinePrompt, InlinePromptProps } from './InlinePrompt';
|
|
@@ -222,8 +222,8 @@ export const Muted: StoryObj<PreviewStoryArgs> = {
|
|
|
222
222
|
};
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
|
-
* While main sentiments (`warning`, `negative`, `neutral`)
|
|
226
|
-
*
|
|
225
|
+
* While all main sentiments (`warning`, `negative`, `positive` and`neutral`) are associated with a
|
|
226
|
+
* default `StatusIcon`s, we also allow for Icon overrides to bring the prompt's visual language
|
|
227
227
|
* closer to the prompt's content. <br /><br />
|
|
228
228
|
* It's also possible to override the default icon's accessible name announced by screen readers
|
|
229
229
|
* via `mediaLabel` prop, which is especially useful for the `proposition` sentiment.
|
|
@@ -232,14 +232,17 @@ export const IconOverrides: StoryObj<PreviewStoryArgs> = {
|
|
|
232
232
|
render: (args: PreviewStoryArgs) => {
|
|
233
233
|
return (
|
|
234
234
|
<>
|
|
235
|
-
<InlinePrompt {...args} media={<Travel />} sentiment="positive">
|
|
235
|
+
<InlinePrompt {...args} media={<Travel />} sentiment="positive" mediaLabel="Success: ">
|
|
236
236
|
Your travel account is set up and ready to use.
|
|
237
237
|
</InlinePrompt>
|
|
238
238
|
<InlinePrompt {...args} media={<Taxi />} sentiment="proposition" mediaLabel="Taxi addon: ">
|
|
239
239
|
Connect Wise with your taxi app to get exclusive discounts.
|
|
240
240
|
</InlinePrompt>
|
|
241
|
-
<InlinePrompt {...args} media={<
|
|
242
|
-
|
|
241
|
+
<InlinePrompt {...args} media={<Clock />} sentiment="warning" mediaLabel="Processing: ">
|
|
242
|
+
The account verification is taking longer than usual.
|
|
243
|
+
</InlinePrompt>
|
|
244
|
+
<InlinePrompt {...args} media={<Id />} sentiment="negative" mediaLabel="Error: ">
|
|
245
|
+
The identity document you provided has expired.
|
|
243
246
|
</InlinePrompt>
|
|
244
247
|
</>
|
|
245
248
|
);
|
|
@@ -25,12 +25,12 @@ export type InlinePromptProps = {
|
|
|
25
25
|
*/
|
|
26
26
|
muted?: boolean;
|
|
27
27
|
/**
|
|
28
|
-
* Icon override for
|
|
28
|
+
* Icon override for all sentiments. If the sentiment uses StatusIcon by default, it will be
|
|
29
|
+
* replaced by a plain icon.
|
|
29
30
|
*/
|
|
30
31
|
media?: React.ReactNode;
|
|
31
32
|
/**
|
|
32
33
|
* Override for the sentiment's-derived, default, accessible name announced by the screen readers.
|
|
33
|
-
* To be used primarily for `proposition` sentiment.
|
|
34
34
|
*/
|
|
35
35
|
mediaLabel?: string;
|
|
36
36
|
/**
|
|
@@ -68,6 +68,7 @@ export const InlinePrompt = ({
|
|
|
68
68
|
if (muted) {
|
|
69
69
|
return <BackslashCircle size={16} data-testid="InlinePrompt_Muted" title={mediaLabel} />;
|
|
70
70
|
}
|
|
71
|
+
|
|
71
72
|
if (loading) {
|
|
72
73
|
return (
|
|
73
74
|
<ProcessIndicator
|
|
@@ -78,15 +79,11 @@ export const InlinePrompt = ({
|
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
if (sentiment === 'positive' && media) {
|
|
82
|
-
return media;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
82
|
if (sentiment === 'proposition') {
|
|
86
83
|
return media || <GiftBox title={mediaLabel} />;
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
return <StatusIcon size={16} sentiment={sentiment} iconLabel={mediaLabel} />;
|
|
86
|
+
return media || <StatusIcon size={16} sentiment={sentiment} iconLabel={mediaLabel} />;
|
|
90
87
|
};
|
|
91
88
|
|
|
92
89
|
return (
|