@transferwise/components 46.160.1 → 46.160.3
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/Calendar/Calendar.js +9 -3
- package/build/Calendar/Calendar.js.map +1 -1
- package/build/Calendar/Calendar.mjs +9 -3
- package/build/Calendar/Calendar.mjs.map +1 -1
- package/build/Carousel/Carousel.js +17 -10
- package/build/Carousel/Carousel.js.map +1 -1
- package/build/Carousel/Carousel.mjs +17 -10
- package/build/Carousel/Carousel.mjs.map +1 -1
- package/build/Inputs/SelectInput/Options/SelectInputOptions.js +33 -5
- package/build/Inputs/SelectInput/Options/SelectInputOptions.js.map +1 -1
- package/build/Inputs/SelectInput/Options/SelectInputOptions.mjs +34 -6
- package/build/Inputs/SelectInput/Options/SelectInputOptions.mjs.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.js +28 -4
- package/build/Inputs/SelectInput/SelectInput.js.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.mjs +29 -5
- package/build/Inputs/SelectInput/SelectInput.mjs.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.utils.js +0 -2
- package/build/Inputs/SelectInput/SelectInput.utils.js.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.utils.mjs +1 -2
- package/build/Inputs/SelectInput/SelectInput.utils.mjs.map +1 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js +5 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs +5 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
- package/build/Inputs/SelectInput/constants.js +8 -0
- package/build/Inputs/SelectInput/constants.js.map +1 -0
- package/build/Inputs/SelectInput/constants.mjs +5 -0
- package/build/Inputs/SelectInput/constants.mjs.map +1 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.js +57 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.js.map +1 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.mjs +54 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.mjs.map +1 -0
- package/build/SegmentedControl/SegmentedControl.js +9 -2
- package/build/SegmentedControl/SegmentedControl.js.map +1 -1
- package/build/SegmentedControl/SegmentedControl.mjs +9 -2
- package/build/SegmentedControl/SegmentedControl.mjs.map +1 -1
- package/build/Tooltip/Tooltip.js +6 -1
- package/build/Tooltip/Tooltip.js.map +1 -1
- package/build/Tooltip/Tooltip.mjs +6 -1
- package/build/Tooltip/Tooltip.mjs.map +1 -1
- package/build/i18n/ja.json +1 -1
- package/build/i18n/ja.json.js +1 -1
- package/build/i18n/ja.json.mjs +1 -1
- package/build/i18n/tr.json +5 -0
- package/build/i18n/tr.json.js +5 -0
- package/build/i18n/tr.json.js.map +1 -1
- package/build/i18n/tr.json.mjs +5 -0
- package/build/i18n/tr.json.mjs.map +1 -1
- package/build/i18n/zh-CN.json +5 -0
- package/build/i18n/zh-CN.json.js +5 -0
- package/build/i18n/zh-CN.json.js.map +1 -1
- package/build/i18n/zh-CN.json.mjs +5 -0
- package/build/i18n/zh-CN.json.mjs.map +1 -1
- package/build/main.css +34 -22
- package/build/styles/AvatarLayout/AvatarLayout.css +26 -9
- package/build/styles/SegmentedControl/SegmentedControl.css +2 -1
- package/build/styles/Tooltip/Tooltip.css +0 -6
- package/build/styles/common/Panel/Panel.css +6 -6
- package/build/styles/main.css +34 -22
- package/build/types/Calendar/Calendar.d.ts.map +1 -1
- package/build/types/Carousel/Carousel.d.ts.map +1 -1
- package/build/types/Carousel/_stories/constants.d.ts +3 -0
- package/build/types/Carousel/_stories/constants.d.ts.map +1 -0
- package/build/types/Inputs/SelectInput/Options/SelectInputOptions.d.ts +3 -1
- package/build/types/Inputs/SelectInput/Options/SelectInputOptions.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/SelectInput.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/SelectInput.utils.d.ts +0 -1
- package/build/types/Inputs/SelectInput/SelectInput.utils.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/constants.d.ts +3 -0
- package/build/types/Inputs/SelectInput/constants.d.ts.map +1 -0
- package/build/types/Inputs/SelectInput/hooks/useTypeahead.d.ts +8 -0
- package/build/types/Inputs/SelectInput/hooks/useTypeahead.d.ts.map +1 -0
- package/build/types/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/build/types/Tooltip/Tooltip.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/AvatarLayout/AvatarLayout.css +26 -9
- package/src/AvatarLayout/AvatarLayout.less +46 -22
- package/src/AvatarLayout/_stories/AvatarLayout.test.story.tsx +17 -42
- package/src/Calendar/Calendar.test.tsx +82 -0
- package/src/Calendar/Calendar.tsx +6 -3
- package/src/Carousel/Carousel.test.tsx +50 -2
- package/src/Carousel/Carousel.tsx +13 -10
- package/src/Carousel/_stories/Carousel.story.tsx +12 -52
- package/src/Carousel/_stories/Carousel.test.story.tsx +7 -43
- package/src/Carousel/_stories/constants.tsx +46 -0
- package/src/DateLookup/_stories/DateLookup.rtl.test.story.tsx +59 -48
- package/src/Field/_stories/Field.test.story.tsx +48 -37
- package/src/FlowNavigation/_stories/FlowNavigation.rtl.test.story.tsx +78 -0
- package/src/FlowNavigation/_stories/FlowNavigation.test.story.tsx +0 -44
- package/src/Inputs/SelectInput/Options/SelectInputOptions.tsx +36 -6
- package/src/Inputs/SelectInput/SelectInput.test.tsx +143 -0
- package/src/Inputs/SelectInput/SelectInput.tsx +31 -5
- package/src/Inputs/SelectInput/SelectInput.utils.ts +0 -2
- package/src/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +3 -0
- package/src/Inputs/SelectInput/constants.ts +3 -0
- package/src/Inputs/SelectInput/hooks/useTypeahead.ts +82 -0
- package/src/Label/_stories/Label.test.story.tsx +35 -22
- package/src/Popover/_stories/Popover.rtl.test.story.tsx +40 -50
- package/src/SegmentedControl/SegmentedControl.css +2 -1
- package/src/SegmentedControl/SegmentedControl.less +4 -2
- package/src/SegmentedControl/SegmentedControl.tsx +8 -2
- package/src/SegmentedControl/_stories/SegmentedControl.test.story.tsx +16 -8
- package/src/Tooltip/Tooltip.css +0 -6
- package/src/Tooltip/Tooltip.less +0 -12
- package/src/Tooltip/Tooltip.test.tsx +51 -0
- package/src/Tooltip/Tooltip.tsx +11 -1
- package/src/Tooltip/_stories/Tooltip.rtl.test.story.tsx +16 -34
- package/src/common/Panel/Panel.css +6 -6
- package/src/common/Panel/Panel.less +6 -6
- package/src/i18n/ja.json +1 -1
- package/src/i18n/tr.json +5 -0
- package/src/i18n/zh-CN.json +5 -0
- package/src/main.css +34 -22
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
2
|
import { userEvent, within } from 'storybook/test';
|
|
3
3
|
|
|
4
|
-
import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
4
|
+
import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
5
5
|
import DateLookup from '..';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
@@ -14,39 +14,47 @@ type Story = StoryObj<typeof DateLookup>;
|
|
|
14
14
|
|
|
15
15
|
const selectedDate = new Date(2024, 5, 14);
|
|
16
16
|
|
|
17
|
+
const i18nConfig = {
|
|
18
|
+
label: { ltr: 'Select date', rtl: 'اختر التاريخ' },
|
|
19
|
+
placeholder: { ltr: 'Choose a date', rtl: 'اختر تاريخًا' },
|
|
20
|
+
};
|
|
21
|
+
|
|
17
22
|
export const TriggerVariants: Story = {
|
|
18
|
-
render: () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
23
|
+
render: () => {
|
|
24
|
+
const i18n = useRtlDiffI18n<{ label: string; placeholder: string }>();
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<DateLookup
|
|
28
|
+
label={i18n.label}
|
|
29
|
+
placeholder={i18n.placeholder}
|
|
30
|
+
value={selectedDate}
|
|
31
|
+
onChange={() => {}}
|
|
32
|
+
/>
|
|
33
|
+
<DateLookup
|
|
34
|
+
label={i18n.label}
|
|
35
|
+
placeholder={i18n.placeholder}
|
|
36
|
+
value={selectedDate}
|
|
37
|
+
clearable
|
|
38
|
+
onChange={() => {}}
|
|
39
|
+
/>
|
|
40
|
+
<DateLookup
|
|
41
|
+
label={i18n.label}
|
|
42
|
+
placeholder={i18n.placeholder}
|
|
43
|
+
size="sm"
|
|
44
|
+
value={selectedDate}
|
|
45
|
+
onChange={() => {}}
|
|
46
|
+
/>
|
|
47
|
+
<DateLookup
|
|
48
|
+
label={i18n.label}
|
|
49
|
+
placeholder={i18n.placeholder}
|
|
50
|
+
size="lg"
|
|
51
|
+
value={selectedDate}
|
|
52
|
+
onChange={() => {}}
|
|
53
|
+
/>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
...withRTLDiff({ renderAs: 'iframe', i18n: i18nConfig }),
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
const containerStyle: React.CSSProperties = {
|
|
@@ -59,16 +67,19 @@ const containerStyle: React.CSSProperties = {
|
|
|
59
67
|
};
|
|
60
68
|
|
|
61
69
|
export const OpenCalendarPanel: Story = {
|
|
62
|
-
render: () =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
render: () => {
|
|
71
|
+
const i18n = useRtlDiffI18n<{ label: string; placeholder: string }>();
|
|
72
|
+
return (
|
|
73
|
+
<div style={containerStyle}>
|
|
74
|
+
<DateLookup
|
|
75
|
+
label={i18n.label}
|
|
76
|
+
placeholder={i18n.placeholder}
|
|
77
|
+
value={selectedDate}
|
|
78
|
+
onChange={() => {}}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
72
83
|
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
73
84
|
const canvas = within(canvasElement);
|
|
74
85
|
const trigger = canvas.queryByRole('button');
|
|
@@ -77,12 +88,12 @@ export const OpenCalendarPanel: Story = {
|
|
|
77
88
|
// Suppress focus ring on calendar cells — FocusScope traps focus inside the panel
|
|
78
89
|
// and the autofocus cycle causes the selected date's ring to flicker between iframes.
|
|
79
90
|
const style = canvasElement.ownerDocument.createElement('style');
|
|
80
|
-
style.textContent = `
|
|
81
|
-
.np-panel__content *:focus {
|
|
82
|
-
outline: none !important;
|
|
83
|
-
box-shadow: none !important;
|
|
91
|
+
style.textContent = `
|
|
92
|
+
.np-panel__content *:focus {
|
|
93
|
+
outline: none !important;
|
|
94
|
+
box-shadow: none !important;
|
|
84
95
|
}`;
|
|
85
96
|
canvasElement.ownerDocument.head.appendChild(style);
|
|
86
97
|
},
|
|
87
|
-
...withRTLDiff({ renderAs: 'iframe' }),
|
|
98
|
+
...withRTLDiff({ renderAs: 'iframe', i18n: i18nConfig }),
|
|
88
99
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
-
import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
3
|
+
import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
4
4
|
import { Input } from '../../Inputs/Input/Input';
|
|
5
5
|
import { TextArea } from '../../Inputs/TextArea/TextArea';
|
|
6
|
-
import { lorem10 } from '../../test-utils';
|
|
7
6
|
import Field, { FieldProps } from '..';
|
|
8
7
|
import { Sentiment } from '../../common';
|
|
9
8
|
|
|
@@ -59,44 +58,56 @@ export const DeprecatedProps: Story = {
|
|
|
59
58
|
};
|
|
60
59
|
|
|
61
60
|
export const RTLDiff: Story = {
|
|
62
|
-
render: () =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
61
|
+
render: () => {
|
|
62
|
+
const i18n = useRtlDiffI18n();
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<Field label={i18n.requiredLabel}>
|
|
66
|
+
<Input defaultValue={i18n.inputValue} />
|
|
67
|
+
</Field>
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</Field>
|
|
69
|
+
<Field label={i18n.optionalLabel} required={false}>
|
|
70
|
+
<Input defaultValue={i18n.inputValue} />
|
|
71
|
+
</Field>
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
</Field>
|
|
73
|
+
<Field
|
|
74
|
+
label={i18n.descriptionLabel}
|
|
75
|
+
required={false}
|
|
76
|
+
description={i18n.description}
|
|
77
|
+
message={i18n.validationMessage}
|
|
78
|
+
sentiment="negative"
|
|
79
|
+
>
|
|
80
|
+
<Input defaultValue={i18n.inputValue} />
|
|
81
|
+
</Field>
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</Field>
|
|
83
|
+
<Field message={i18n.helpfulMessage} sentiment="positive">
|
|
84
|
+
<Input defaultValue={i18n.inputValue} />
|
|
85
|
+
</Field>
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</Field>
|
|
87
|
+
<Field label={i18n.loadingLabel} message={i18n.savingMessage} messageLoading>
|
|
88
|
+
<Input defaultValue={i18n.inputValue} />
|
|
89
|
+
</Field>
|
|
94
90
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
...withRTLDiff(
|
|
91
|
+
<Field label={i18n.textareaLabel} required={false} description={i18n.maxChars}>
|
|
92
|
+
<TextArea maxLength={100} defaultValue={i18n.textareaValue} />
|
|
93
|
+
</Field>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
...withRTLDiff({
|
|
98
|
+
i18n: {
|
|
99
|
+
requiredLabel: { ltr: 'Required field', rtl: 'حقل مطلوب' },
|
|
100
|
+
optionalLabel: { ltr: 'Optional field', rtl: 'حقل اختياري' },
|
|
101
|
+
descriptionLabel: { ltr: 'Field with description and message', rtl: 'حقل مع وصف ورسالة' },
|
|
102
|
+
description: { ltr: 'A short description of the field', rtl: 'وصف قصير للحقل' },
|
|
103
|
+
validationMessage: { ltr: 'This is a validation message', rtl: 'هذه رسالة تحقق' },
|
|
104
|
+
helpfulMessage: { ltr: 'A helpful message', rtl: 'رسالة مفيدة' },
|
|
105
|
+
loadingLabel: { ltr: 'Field with loading message', rtl: 'حقل مع رسالة تحميل' },
|
|
106
|
+
savingMessage: { ltr: 'Saving…', rtl: 'جارٍ الحفظ…' },
|
|
107
|
+
textareaLabel: { ltr: 'Textarea with char counter', rtl: 'حقل نص مع عداد الأحرف' },
|
|
108
|
+
maxChars: { ltr: 'Max 100 characters', rtl: 'الحد الأقصى 100 حرف' },
|
|
109
|
+
inputValue: { ltr: 'Input value', rtl: 'قيمة الإدخال' },
|
|
110
|
+
textareaValue: { ltr: 'Textarea value', rtl: 'قيمة حقل النص' },
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
102
113
|
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
+
|
|
3
|
+
import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
4
|
+
import AvatarView from '../../AvatarView';
|
|
5
|
+
import { ProfileType } from '../../common';
|
|
6
|
+
import Logo from '../../Logo';
|
|
7
|
+
|
|
8
|
+
import FlowNavigation from '../FlowNavigation';
|
|
9
|
+
|
|
10
|
+
const noop = () => {};
|
|
11
|
+
|
|
12
|
+
const steps = [
|
|
13
|
+
{ label: 'Amount', hoverLabel: '100 GBP', onClick: noop },
|
|
14
|
+
{ label: 'You', hoverLabel: 'Elena Durante', onClick: noop },
|
|
15
|
+
{ label: 'Review', onClick: noop },
|
|
16
|
+
{ label: 'Pay', onClick: noop },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof FlowNavigation> = {
|
|
20
|
+
component: FlowNavigation,
|
|
21
|
+
title: 'Navigation/FlowNavigation/Tests/RTL Diff',
|
|
22
|
+
tags: ['!autodocs', '!manifest'],
|
|
23
|
+
parameters: {
|
|
24
|
+
padding: '0',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
export default meta;
|
|
28
|
+
|
|
29
|
+
type Story = StoryObj<typeof FlowNavigation>;
|
|
30
|
+
|
|
31
|
+
export const Desktop: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<>
|
|
34
|
+
<FlowNavigation
|
|
35
|
+
activeStep={2}
|
|
36
|
+
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
37
|
+
logo={<Logo />}
|
|
38
|
+
steps={steps}
|
|
39
|
+
onClose={noop}
|
|
40
|
+
onGoBack={noop}
|
|
41
|
+
/>
|
|
42
|
+
<FlowNavigation
|
|
43
|
+
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
44
|
+
logo={<Logo />}
|
|
45
|
+
onClose={noop}
|
|
46
|
+
/>
|
|
47
|
+
<FlowNavigation
|
|
48
|
+
done
|
|
49
|
+
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
50
|
+
logo={<Logo />}
|
|
51
|
+
steps={steps}
|
|
52
|
+
onClose={noop}
|
|
53
|
+
/>
|
|
54
|
+
</>
|
|
55
|
+
),
|
|
56
|
+
...withRTLDiff(),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Mobile: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<>
|
|
62
|
+
<FlowNavigation
|
|
63
|
+
activeStep={2}
|
|
64
|
+
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
65
|
+
logo={<Logo />}
|
|
66
|
+
steps={steps}
|
|
67
|
+
onClose={noop}
|
|
68
|
+
onGoBack={noop}
|
|
69
|
+
/>
|
|
70
|
+
<FlowNavigation
|
|
71
|
+
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
72
|
+
logo={<Logo />}
|
|
73
|
+
onClose={noop}
|
|
74
|
+
/>
|
|
75
|
+
</>
|
|
76
|
+
),
|
|
77
|
+
...withRTLDiff({ viewport: 'largeMobile' }),
|
|
78
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
2
|
|
|
3
|
-
import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
4
3
|
import AvatarView from '../../AvatarView';
|
|
5
4
|
import { ProfileType } from '../../common';
|
|
6
5
|
import Logo from '../../Logo';
|
|
@@ -77,46 +76,3 @@ export const LongLabel: Story = {
|
|
|
77
76
|
/>
|
|
78
77
|
),
|
|
79
78
|
};
|
|
80
|
-
|
|
81
|
-
const rtlSteps = [
|
|
82
|
-
{ label: 'Amount', hoverLabel: '100 GBP', onClick: noop },
|
|
83
|
-
{ label: 'You', hoverLabel: 'Elena Durante', onClick: noop },
|
|
84
|
-
{ label: 'Review', onClick: noop },
|
|
85
|
-
{ label: 'Pay', onClick: noop },
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* FlowNavigation has directional layout: back button on inline-start,
|
|
90
|
-
* close button and avatar on inline-end, and a stepper progress bar that
|
|
91
|
-
* flows in the reading direction. All of these flip in RTL.
|
|
92
|
-
*/
|
|
93
|
-
export const RTLDiff: Story = {
|
|
94
|
-
render: () => (
|
|
95
|
-
<>
|
|
96
|
-
{/* With stepper, back button, close button, and avatar */}
|
|
97
|
-
<FlowNavigation
|
|
98
|
-
activeStep={2}
|
|
99
|
-
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
100
|
-
logo={<Logo />}
|
|
101
|
-
steps={rtlSteps}
|
|
102
|
-
onClose={noop}
|
|
103
|
-
onGoBack={noop}
|
|
104
|
-
/>
|
|
105
|
-
{/* Without stepper — logo left, close button right */}
|
|
106
|
-
<FlowNavigation
|
|
107
|
-
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
108
|
-
logo={<Logo />}
|
|
109
|
-
onClose={noop}
|
|
110
|
-
/>
|
|
111
|
-
{/* Done state — no stepper, logo and close button only */}
|
|
112
|
-
<FlowNavigation
|
|
113
|
-
done
|
|
114
|
-
avatar={<AvatarView profileType={ProfileType.PERSONAL} />}
|
|
115
|
-
logo={<Logo />}
|
|
116
|
-
steps={rtlSteps}
|
|
117
|
-
onClose={noop}
|
|
118
|
-
/>
|
|
119
|
-
</>
|
|
120
|
-
),
|
|
121
|
-
...withRTLDiff(),
|
|
122
|
-
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { CrossCircle } from '@transferwise/icons';
|
|
2
2
|
import { ListboxOptions } from '@headlessui/react';
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
|
-
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useDeferredValue, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { useIntl } from 'react-intl';
|
|
6
6
|
import { Virtualizer, type VirtualizerHandle } from 'virtua';
|
|
7
7
|
|
|
8
8
|
import { SearchInput } from '../../SearchInput/SearchInput';
|
|
9
|
+
import { MAX_ITEMS_WITHOUT_VIRTUALIZATION } from '../constants';
|
|
10
|
+
import { useTypeahead } from '../hooks/useTypeahead';
|
|
9
11
|
import {
|
|
10
12
|
SelectInputItemsCountContext,
|
|
11
13
|
SelectInputItemPositionContext,
|
|
@@ -13,7 +15,6 @@ import {
|
|
|
13
15
|
import {
|
|
14
16
|
dedupeSelectInputItems,
|
|
15
17
|
filterSelectInputItems,
|
|
16
|
-
MAX_ITEMS_WITHOUT_VIRTUALIZATION,
|
|
17
18
|
searchableString,
|
|
18
19
|
selectInputOptionItemIncludesNeedle,
|
|
19
20
|
sortSelectInputItems,
|
|
@@ -42,7 +43,9 @@ export interface SelectInputOptionsProps<T = string> extends Pick<
|
|
|
42
43
|
searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
|
|
43
44
|
listboxRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
44
45
|
filterQuery: string;
|
|
46
|
+
initialTypeaheadKey?: string | null;
|
|
45
47
|
onFilterChange: (query: string) => void;
|
|
48
|
+
onInitialTypeaheadHandled?: () => void;
|
|
46
49
|
listBoxLabel?: string;
|
|
47
50
|
listBoxLabelledBy?: string;
|
|
48
51
|
autocomplete?: string;
|
|
@@ -67,7 +70,9 @@ export function SelectInputOptions<T = string>({
|
|
|
67
70
|
searchInputRef,
|
|
68
71
|
listboxRef,
|
|
69
72
|
filterQuery,
|
|
73
|
+
initialTypeaheadKey,
|
|
70
74
|
onFilterChange,
|
|
75
|
+
onInitialTypeaheadHandled,
|
|
71
76
|
listBoxLabel,
|
|
72
77
|
listBoxLabelledBy,
|
|
73
78
|
autocomplete,
|
|
@@ -78,13 +83,37 @@ export function SelectInputOptions<T = string>({
|
|
|
78
83
|
const virtualiserHandlerRef = useRef<VirtualizerHandle>(null);
|
|
79
84
|
const controllerRef = filterable ? searchInputRef : listboxRef;
|
|
80
85
|
const initialRenderRef = useRef(true);
|
|
86
|
+
const deferredFilterQuery = useDeferredValue(filterQuery);
|
|
87
|
+
const handleTypeahead = useTypeahead({
|
|
88
|
+
disabled: filterable,
|
|
89
|
+
onMatch: onInitialTypeaheadHandled,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (filterable || initialTypeaheadKey == null) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const animationFrame = requestAnimationFrame(() => {
|
|
98
|
+
if (listboxRef.current == null) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
listboxRef.current.focus({ preventScroll: true });
|
|
103
|
+
listboxRef.current.dispatchEvent(
|
|
104
|
+
new KeyboardEvent('keydown', { key: initialTypeaheadKey, bubbles: true }),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return () => cancelAnimationFrame(animationFrame);
|
|
109
|
+
}, [filterable, initialTypeaheadKey, listboxRef]);
|
|
81
110
|
|
|
82
111
|
const needle = useMemo(() => {
|
|
83
112
|
if (filterable) {
|
|
84
|
-
return
|
|
113
|
+
return deferredFilterQuery ? searchableString(deferredFilterQuery) : null;
|
|
85
114
|
}
|
|
86
115
|
return undefined;
|
|
87
|
-
}, [
|
|
116
|
+
}, [deferredFilterQuery, filterable]);
|
|
88
117
|
useEffect(() => {
|
|
89
118
|
if (needle) {
|
|
90
119
|
// Ensure having an active option while filtering.
|
|
@@ -151,7 +180,7 @@ export function SelectInputOptions<T = string>({
|
|
|
151
180
|
return item;
|
|
152
181
|
});
|
|
153
182
|
|
|
154
|
-
return sortSelectInputItems(filtered, sortFilteredOptions,
|
|
183
|
+
return sortSelectInputItems(filtered, sortFilteredOptions, deferredFilterQuery);
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
return filterSelectInputItems(dedupedItems, (item) =>
|
|
@@ -284,7 +313,7 @@ export function SelectInputOptions<T = string>({
|
|
|
284
313
|
shape="rectangle"
|
|
285
314
|
placeholder={filterPlaceholder}
|
|
286
315
|
aria-label={filterPlaceholder}
|
|
287
|
-
|
|
316
|
+
value={filterQuery}
|
|
288
317
|
aria-autocomplete="list"
|
|
289
318
|
aria-expanded
|
|
290
319
|
aria-controls={listboxId}
|
|
@@ -352,6 +381,7 @@ export function SelectInputOptions<T = string>({
|
|
|
352
381
|
aria-labelledby={listBoxLabelledBy}
|
|
353
382
|
tabIndex={0}
|
|
354
383
|
className="np-select-input-listbox"
|
|
384
|
+
onKeyDownCapture={handleTypeahead}
|
|
355
385
|
>
|
|
356
386
|
{!virtualized ? (
|
|
357
387
|
filteredItems.map((_, index) => getItemNode(index))
|
|
@@ -34,6 +34,20 @@ describe('SelectInput', () => {
|
|
|
34
34
|
expect(screen.getByText('Currency')).toBeInTheDocument();
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
it('explicitly includes the trigger in the sequential keyboard focus order', () => {
|
|
38
|
+
render(
|
|
39
|
+
<SelectInput
|
|
40
|
+
placeholder="Currency"
|
|
41
|
+
items={[
|
|
42
|
+
{ type: 'option', value: 'USD' },
|
|
43
|
+
{ type: 'option', value: 'EUR' },
|
|
44
|
+
]}
|
|
45
|
+
/>,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('tabindex', '0');
|
|
49
|
+
});
|
|
50
|
+
|
|
37
51
|
it('renders footer', async () => {
|
|
38
52
|
render(
|
|
39
53
|
<SelectInput
|
|
@@ -221,6 +235,135 @@ describe('SelectInput', () => {
|
|
|
221
235
|
expect(trigger).toHaveTextContent('EUR');
|
|
222
236
|
});
|
|
223
237
|
|
|
238
|
+
it('starts filtering when typing on the focused trigger', async () => {
|
|
239
|
+
const handleFilterChange = jest.fn();
|
|
240
|
+
|
|
241
|
+
render(
|
|
242
|
+
<SelectInput
|
|
243
|
+
items={[
|
|
244
|
+
{ type: 'option', value: 'January' },
|
|
245
|
+
{ type: 'option', value: 'February' },
|
|
246
|
+
{ type: 'option', value: 'March' },
|
|
247
|
+
]}
|
|
248
|
+
filterable
|
|
249
|
+
onFilterChange={handleFilterChange}
|
|
250
|
+
/>,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const trigger = screen.getByRole('combobox');
|
|
254
|
+
await userEvent.tab();
|
|
255
|
+
expect(trigger).toHaveFocus();
|
|
256
|
+
await userEvent.keyboard('f');
|
|
257
|
+
|
|
258
|
+
const searchInput = screen.getByRole('combobox', { expanded: true });
|
|
259
|
+
expect(searchInput).toHaveFocus();
|
|
260
|
+
expect(searchInput).toHaveValue('f');
|
|
261
|
+
expect(handleFilterChange).toHaveBeenLastCalledWith({
|
|
262
|
+
query: 'f',
|
|
263
|
+
queryNormalized: 'f',
|
|
264
|
+
});
|
|
265
|
+
expect(screen.getByRole('option', { name: 'February' })).toBeInTheDocument();
|
|
266
|
+
expect(screen.queryByRole('option', { name: 'January' })).not.toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('starts typeahead when typing on the focused trigger', async () => {
|
|
270
|
+
render(
|
|
271
|
+
<SelectInput
|
|
272
|
+
items={[
|
|
273
|
+
{ type: 'option', value: 'Frost' },
|
|
274
|
+
{ type: 'option', value: 'February' },
|
|
275
|
+
{ type: 'option', value: 'March' },
|
|
276
|
+
]}
|
|
277
|
+
/>,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const trigger = screen.getByRole('combobox');
|
|
281
|
+
await userEvent.tab();
|
|
282
|
+
expect(trigger).toHaveFocus();
|
|
283
|
+
await userEvent.keyboard('f');
|
|
284
|
+
|
|
285
|
+
const listbox = screen.getByRole('listbox');
|
|
286
|
+
const frost = within(listbox).getByRole('option', { name: 'Frost' });
|
|
287
|
+
const february = within(listbox).getByRole('option', { name: 'February' });
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
expect(listbox).toHaveFocus();
|
|
290
|
+
});
|
|
291
|
+
await waitFor(() => {
|
|
292
|
+
expect(frost).toHaveClass('np-select-input-option-container--active');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await userEvent.keyboard('e');
|
|
296
|
+
await waitFor(() => {
|
|
297
|
+
expect(february).toHaveClass('np-select-input-option-container--active');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
301
|
+
expect(within(listbox).getByRole('option', { name: 'March' })).toHaveClass(
|
|
302
|
+
'np-select-input-option-container--active',
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('continues typeahead across spaces after the query starts', async () => {
|
|
307
|
+
render(
|
|
308
|
+
<SelectInput
|
|
309
|
+
items={[
|
|
310
|
+
{ type: 'option', value: 'New Zealand' },
|
|
311
|
+
{ type: 'option', value: 'New York' },
|
|
312
|
+
{ type: 'option', value: 'Yorkshire' },
|
|
313
|
+
]}
|
|
314
|
+
/>,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
await userEvent.tab();
|
|
318
|
+
await userEvent.keyboard('n');
|
|
319
|
+
|
|
320
|
+
const listbox = screen.getByRole('listbox');
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(listbox).toHaveFocus();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await userEvent.keyboard('ew y');
|
|
326
|
+
|
|
327
|
+
await waitFor(() => {
|
|
328
|
+
expect(within(listbox).getByRole('option', { name: 'New York' })).toHaveClass(
|
|
329
|
+
'np-select-input-option-container--active',
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it.each([
|
|
335
|
+
['filterable', true],
|
|
336
|
+
['non-filterable', false],
|
|
337
|
+
])('supports arrow navigation after opening a %s select with Enter', async (_, filterable) => {
|
|
338
|
+
render(
|
|
339
|
+
<SelectInput
|
|
340
|
+
items={[
|
|
341
|
+
{ type: 'option', value: 'GBP' },
|
|
342
|
+
{ type: 'option', value: 'EUR' },
|
|
343
|
+
{ type: 'option', value: 'USD' },
|
|
344
|
+
]}
|
|
345
|
+
filterable={filterable}
|
|
346
|
+
/>,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const trigger = screen.getByRole('combobox');
|
|
350
|
+
await userEvent.tab();
|
|
351
|
+
expect(trigger).toHaveFocus();
|
|
352
|
+
await userEvent.keyboard('{Enter}');
|
|
353
|
+
|
|
354
|
+
const controller = filterable
|
|
355
|
+
? screen.getByRole('combobox', { expanded: true })
|
|
356
|
+
: screen.getByRole('listbox');
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(controller).toHaveFocus();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
362
|
+
expect(screen.getByRole('option', { name: 'EUR' })).toHaveClass(
|
|
363
|
+
'np-select-input-option-container--active',
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
224
367
|
it('clears filter query on close', async () => {
|
|
225
368
|
const handleFilterChange = jest.fn();
|
|
226
369
|
|