@transferwise/components 45.17.1 → 45.18.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.esm.js +339 -293
- package/build/index.esm.js.map +1 -1
- package/build/index.js +339 -292
- package/build/index.js.map +1 -1
- package/build/main.css +1 -1
- package/build/styles/inputs/SelectInput.css +1 -1
- package/build/styles/instructionsList/InstructionsList.css +1 -1
- package/build/styles/main.css +1 -1
- package/build/styles/stepper/Stepper.css +1 -1
- package/build/styles/tooltip/Tooltip.css +1 -1
- package/build/types/button/Button.d.ts.map +1 -1
- package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts +13 -0
- package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts.map +1 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +16 -6
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/instructionsList/InstructionsList.d.ts +10 -3
- package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
- package/build/types/processIndicator/ProcessIndicator.d.ts +1 -1
- package/build/types/tooltip/Tooltip.d.ts +2 -1
- package/build/types/tooltip/Tooltip.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/button/Button.story.tsx +6 -0
- package/src/button/Button.tsx +6 -1
- package/src/common/polymorphicWithOverrides/PolymorphicWithOverrides.tsx +19 -0
- package/src/index.ts +3 -0
- package/src/inputs/SelectInput.css +1 -1
- package/src/inputs/SelectInput.less +8 -2
- package/src/inputs/SelectInput.story.tsx +52 -5
- package/src/inputs/SelectInput.tsx +165 -104
- package/src/inputs/_Popover.less +1 -1
- package/src/instructionsList/InstructionList.story.tsx +39 -0
- package/src/instructionsList/InstructionsList.css +1 -1
- package/src/instructionsList/InstructionsList.less +3 -15
- package/src/instructionsList/InstructionsList.spec.tsx +35 -0
- package/src/instructionsList/InstructionsList.tsx +40 -25
- package/src/main.css +1 -1
- package/src/processIndicator/ProcessIndicator.js +2 -2
- package/src/stepper/Stepper.css +1 -1
- package/src/stepper/Stepper.less +1 -1
- package/src/tooltip/Tooltip.css +1 -1
- package/src/tooltip/Tooltip.less +13 -0
- package/src/tooltip/Tooltip.spec.tsx +97 -29
- package/src/tooltip/Tooltip.tsx +24 -31
- package/src/tooltip/__snapshots__/Tooltip.spec.tsx.snap +31 -0
- package/src/instructionsList/InstructionList.story.js +0 -27
- package/src/instructionsList/InstructionsList.spec.js +0 -29
|
@@ -2,12 +2,14 @@ import { Listbox as ListboxBase } from '@headlessui/react';
|
|
|
2
2
|
import { useId } from '@radix-ui/react-id';
|
|
3
3
|
import { Check, ChevronDown, Cross } from '@transferwise/icons';
|
|
4
4
|
import classNames from 'classnames';
|
|
5
|
-
import
|
|
5
|
+
import mergeProps from 'merge-props';
|
|
6
|
+
import { createContext, forwardRef, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
7
|
import { useIntl } from 'react-intl';
|
|
7
8
|
import mergeRefs from 'react-merge-refs';
|
|
8
9
|
|
|
9
10
|
import { useEffectEvent } from '../common/hooks/useEffectEvent';
|
|
10
11
|
import { useScreenSize } from '../common/hooks/useScreenSize';
|
|
12
|
+
import { PolymorphicWithOverrides } from '../common/polymorphicWithOverrides/PolymorphicWithOverrides';
|
|
11
13
|
import { Breakpoint } from '../common/propsValues/breakpoint';
|
|
12
14
|
import messages from '../dateLookup/dateTrigger/DateTrigger.messages';
|
|
13
15
|
import { wrapInFragment } from '../utilities/wrapInFragment';
|
|
@@ -15,7 +17,7 @@ import { wrapInFragment } from '../utilities/wrapInFragment';
|
|
|
15
17
|
import { InputGroup } from './InputGroup';
|
|
16
18
|
import { SearchInput } from './SearchInput';
|
|
17
19
|
import { BottomSheet } from './_BottomSheet';
|
|
18
|
-
import { ButtonInput
|
|
20
|
+
import { ButtonInput } from './_ButtonInput';
|
|
19
21
|
import { Popover } from './_Popover';
|
|
20
22
|
|
|
21
23
|
function searchableString(value: string) {
|
|
@@ -37,23 +39,27 @@ function inferSearchableStrings(value: unknown) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const SelectInputHasValueContext = createContext(false);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const SelectInputTriggerButtonPropsContext = createContext<{
|
|
43
|
+
ref?: React.ForwardedRef<HTMLButtonElement>;
|
|
44
|
+
onClick?: () => void;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}>({});
|
|
47
|
+
const SelectInputOptionContentWithinTriggerContext = createContext(false);
|
|
48
|
+
|
|
49
|
+
export interface SelectInputOptionItem<T = string> {
|
|
44
50
|
type: 'option';
|
|
45
51
|
value: T;
|
|
46
52
|
filterMatchers?: readonly string[];
|
|
47
53
|
disabled?: boolean;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
interface SelectInputGroupItem<T = string> {
|
|
56
|
+
export interface SelectInputGroupItem<T = string> {
|
|
51
57
|
type: 'group';
|
|
52
58
|
label: string;
|
|
53
59
|
options: readonly SelectInputOptionItem<T>[];
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
interface SelectInputSeparatorItem {
|
|
62
|
+
export interface SelectInputSeparatorItem {
|
|
57
63
|
type: 'separator';
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -106,10 +112,17 @@ export interface SelectInputProps<T = string> {
|
|
|
106
112
|
items: readonly SelectInputItem<NonNullable<T>>[];
|
|
107
113
|
defaultValue?: T;
|
|
108
114
|
value?: T;
|
|
109
|
-
renderValue?: (value: NonNullable<T>, compact: boolean) => React.ReactNode;
|
|
110
115
|
compareValues?:
|
|
111
116
|
| (keyof NonNullable<T> & string)
|
|
112
117
|
| ((a: T | undefined, b: T | undefined) => boolean);
|
|
118
|
+
renderValue?: (value: NonNullable<T>, withinTrigger: boolean) => React.ReactNode;
|
|
119
|
+
renderTrigger?: (args: {
|
|
120
|
+
content: React.ReactNode;
|
|
121
|
+
placeholderShown: boolean;
|
|
122
|
+
clear: (() => void) | undefined;
|
|
123
|
+
disabled: boolean;
|
|
124
|
+
className: string | undefined;
|
|
125
|
+
}) => React.ReactNode;
|
|
113
126
|
filterable?: boolean;
|
|
114
127
|
filterPlaceholder?: string;
|
|
115
128
|
disabled?: boolean;
|
|
@@ -118,14 +131,66 @@ export interface SelectInputProps<T = string> {
|
|
|
118
131
|
onClear?: () => void;
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
const defaultRenderTrigger = (({ content, placeholderShown, clear, disabled, className }) => (
|
|
135
|
+
<InputGroup
|
|
136
|
+
addonEnd={{
|
|
137
|
+
content: (
|
|
138
|
+
<span className={classNames('np-select-input-addon-container', disabled && 'disabled')}>
|
|
139
|
+
{clear != null && !placeholderShown ? (
|
|
140
|
+
<>
|
|
141
|
+
<SelectInputClearButton
|
|
142
|
+
onClick={(event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
clear();
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
<span className="np-select-input-addon-separator" />
|
|
148
|
+
</>
|
|
149
|
+
) : null}
|
|
150
|
+
|
|
151
|
+
<span className="np-select-input-addon">
|
|
152
|
+
<ChevronDown size={16} />
|
|
153
|
+
</span>
|
|
154
|
+
</span>
|
|
155
|
+
),
|
|
156
|
+
padding: 'sm',
|
|
157
|
+
}}
|
|
158
|
+
disabled={disabled}
|
|
159
|
+
className={className}
|
|
160
|
+
>
|
|
161
|
+
<SelectInputTriggerButton as={ButtonInput}>
|
|
162
|
+
{placeholderShown ? <span className="np-select-input-placeholder"> {content}</span> : content}
|
|
163
|
+
</SelectInputTriggerButton>
|
|
164
|
+
</InputGroup>
|
|
165
|
+
)) satisfies SelectInputProps['renderTrigger'];
|
|
166
|
+
|
|
167
|
+
interface SelectInputClearButtonProps
|
|
168
|
+
extends Pick<React.ComponentPropsWithoutRef<'button'>, 'className' | 'onClick'> {}
|
|
169
|
+
|
|
170
|
+
function SelectInputClearButton({ className, onClick }: SelectInputClearButtonProps) {
|
|
171
|
+
const intl = useIntl();
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
aria-label={intl.formatMessage(messages.ariaLabel)}
|
|
177
|
+
className={classNames(className, 'np-select-input-addon np-select-input-addon--interactive')}
|
|
178
|
+
onClick={onClick}
|
|
179
|
+
>
|
|
180
|
+
<Cross size={16} />
|
|
181
|
+
</button>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function SelectInput<T = string>({
|
|
122
186
|
name,
|
|
123
187
|
placeholder,
|
|
124
188
|
items,
|
|
125
189
|
defaultValue,
|
|
126
190
|
value: controlledValue,
|
|
127
|
-
renderValue = wrapInFragment,
|
|
128
191
|
compareValues,
|
|
192
|
+
renderValue = wrapInFragment,
|
|
193
|
+
renderTrigger = defaultRenderTrigger,
|
|
129
194
|
filterable,
|
|
130
195
|
filterPlaceholder,
|
|
131
196
|
disabled,
|
|
@@ -133,8 +198,6 @@ export function SelectInput<T>({
|
|
|
133
198
|
onChange,
|
|
134
199
|
onClear,
|
|
135
200
|
}: SelectInputProps<T>) {
|
|
136
|
-
const intl = useIntl();
|
|
137
|
-
|
|
138
201
|
const [open, setOpen] = useState(false);
|
|
139
202
|
|
|
140
203
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
@@ -162,95 +225,85 @@ export function SelectInput<T>({
|
|
|
162
225
|
>
|
|
163
226
|
{({ disabled: uiDisabled, value }) => (
|
|
164
227
|
<SelectInputHasValueContext.Provider value={value != null}>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
228
|
+
<OptionsOverlay
|
|
229
|
+
open={open}
|
|
230
|
+
renderTrigger={({ ref, getInteractionProps }) => (
|
|
231
|
+
<SelectInputTriggerButtonPropsContext.Provider
|
|
232
|
+
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
233
|
+
value={{
|
|
234
|
+
ref: mergeRefs([ref, triggerRef]),
|
|
235
|
+
...mergeProps(
|
|
236
|
+
{
|
|
237
|
+
onClick: () => {
|
|
238
|
+
setOpen((prev) => !prev);
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
getInteractionProps(),
|
|
242
|
+
),
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{renderTrigger({
|
|
246
|
+
content:
|
|
247
|
+
value != null ? (
|
|
248
|
+
<SelectInputOptionContentWithinTriggerContext.Provider value>
|
|
249
|
+
{renderValue(value, true)}
|
|
250
|
+
</SelectInputOptionContentWithinTriggerContext.Provider>
|
|
251
|
+
) : (
|
|
252
|
+
placeholder
|
|
253
|
+
),
|
|
254
|
+
placeholderShown: value == null,
|
|
255
|
+
clear:
|
|
256
|
+
onClear != null
|
|
257
|
+
? () => {
|
|
183
258
|
onClear();
|
|
184
259
|
triggerRef.current?.focus({ preventScroll: true });
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</span>
|
|
197
|
-
),
|
|
198
|
-
padding: 'sm',
|
|
260
|
+
}
|
|
261
|
+
: undefined,
|
|
262
|
+
disabled: uiDisabled,
|
|
263
|
+
className: classNames(className, 'np-text-body-large'),
|
|
264
|
+
})}
|
|
265
|
+
</SelectInputTriggerButtonPropsContext.Provider>
|
|
266
|
+
)}
|
|
267
|
+
initialFocusRef={controllerRef}
|
|
268
|
+
padding="none"
|
|
269
|
+
onClose={() => {
|
|
270
|
+
setOpen(false);
|
|
199
271
|
}}
|
|
200
|
-
className={className}
|
|
201
272
|
>
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}}
|
|
212
|
-
>
|
|
213
|
-
{value != null ? (
|
|
214
|
-
<SelectInputOptionContentCompactContext.Provider value>
|
|
215
|
-
{renderValue(value, true)}
|
|
216
|
-
</SelectInputOptionContentCompactContext.Provider>
|
|
217
|
-
) : (
|
|
218
|
-
<span className="np-select-input-placeholder">{placeholder}</span>
|
|
219
|
-
)}
|
|
220
|
-
</ListboxBase.Button>
|
|
221
|
-
)}
|
|
222
|
-
initialFocusRef={controllerRef}
|
|
223
|
-
padding="none"
|
|
224
|
-
onClose={() => {
|
|
225
|
-
setOpen(false);
|
|
226
|
-
}}
|
|
227
|
-
>
|
|
228
|
-
<SelectInputOptions
|
|
229
|
-
items={items}
|
|
230
|
-
renderValue={renderValue}
|
|
231
|
-
filterable={filterable}
|
|
232
|
-
filterPlaceholder={filterPlaceholder}
|
|
233
|
-
searchInputRef={searchInputRef}
|
|
234
|
-
listboxRef={listboxRef}
|
|
235
|
-
/>
|
|
236
|
-
</OptionsOverlay>
|
|
237
|
-
</InputGroup>
|
|
273
|
+
<SelectInputOptions
|
|
274
|
+
items={items}
|
|
275
|
+
renderValue={renderValue}
|
|
276
|
+
filterable={filterable}
|
|
277
|
+
filterPlaceholder={filterPlaceholder}
|
|
278
|
+
searchInputRef={searchInputRef}
|
|
279
|
+
listboxRef={listboxRef}
|
|
280
|
+
/>
|
|
281
|
+
</OptionsOverlay>
|
|
238
282
|
</SelectInputHasValueContext.Provider>
|
|
239
283
|
)}
|
|
240
284
|
</ListboxBase>
|
|
241
285
|
);
|
|
242
286
|
}
|
|
243
287
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
288
|
+
export type SelectInputTriggerButtonProps<T extends React.ComponentType | 'button' = 'button'> = {
|
|
289
|
+
as?: T;
|
|
290
|
+
} & React.ComponentPropsWithoutRef<T>;
|
|
247
291
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
) {
|
|
252
|
-
|
|
253
|
-
|
|
292
|
+
export function SelectInputTriggerButton<T extends React.ComponentType | 'button' = 'button'>({
|
|
293
|
+
as = 'button' as T,
|
|
294
|
+
...restProps
|
|
295
|
+
}: SelectInputTriggerButtonProps<T>) {
|
|
296
|
+
const { ref, onClick, ...interactionProps } = useContext(SelectInputTriggerButtonPropsContext);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<ListboxBase.Button
|
|
300
|
+
ref={ref}
|
|
301
|
+
as={PolymorphicWithOverrides}
|
|
302
|
+
__overrides={{ as, ...interactionProps }}
|
|
303
|
+
{...mergeProps({ onClick }, restProps)}
|
|
304
|
+
/>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
254
307
|
|
|
255
308
|
interface SelectInputOptionsContainerProps extends React.ComponentPropsWithRef<'div'> {
|
|
256
309
|
onAriaActiveDescendantChange: (value: React.AriaAttributes['aria-activedescendant']) => void;
|
|
@@ -294,7 +347,7 @@ interface SelectInputOptionsProps<T = string>
|
|
|
294
347
|
listboxRef: React.RefObject<HTMLDivElement>;
|
|
295
348
|
}
|
|
296
349
|
|
|
297
|
-
function SelectInputOptions<T>({
|
|
350
|
+
function SelectInputOptions<T = string>({
|
|
298
351
|
items,
|
|
299
352
|
renderValue = wrapInFragment,
|
|
300
353
|
filterable,
|
|
@@ -395,7 +448,11 @@ interface SelectInputItemViewProps<
|
|
|
395
448
|
needle: string | null;
|
|
396
449
|
}
|
|
397
450
|
|
|
398
|
-
function SelectInputItemView<T
|
|
451
|
+
function SelectInputItemView<T = string>({
|
|
452
|
+
item,
|
|
453
|
+
renderValue,
|
|
454
|
+
needle,
|
|
455
|
+
}: SelectInputItemViewProps<T>) {
|
|
399
456
|
switch (item.type) {
|
|
400
457
|
case 'option': {
|
|
401
458
|
if (
|
|
@@ -418,7 +475,7 @@ function SelectInputItemView<T>({ item, renderValue, needle }: SelectInputItemVi
|
|
|
418
475
|
}
|
|
419
476
|
case 'separator': {
|
|
420
477
|
if (needle == null) {
|
|
421
|
-
return <hr className="np-select-input-separator-item"
|
|
478
|
+
return <hr className="np-select-input-separator-item" />;
|
|
422
479
|
}
|
|
423
480
|
break;
|
|
424
481
|
}
|
|
@@ -429,7 +486,7 @@ function SelectInputItemView<T>({ item, renderValue, needle }: SelectInputItemVi
|
|
|
429
486
|
interface SelectInputGroupItemViewProps<T = string>
|
|
430
487
|
extends SelectInputItemViewProps<T, SelectInputGroupItem<T | undefined>> {}
|
|
431
488
|
|
|
432
|
-
function SelectInputGroupItemView<T>({
|
|
489
|
+
function SelectInputGroupItemView<T = string>({
|
|
433
490
|
item,
|
|
434
491
|
renderValue,
|
|
435
492
|
needle,
|
|
@@ -472,7 +529,7 @@ interface SelectInputOptionProps<T = string> {
|
|
|
472
529
|
children?: React.ReactNode;
|
|
473
530
|
}
|
|
474
531
|
|
|
475
|
-
function SelectInputOption<T>({ value, disabled, children }: SelectInputOptionProps<T>) {
|
|
532
|
+
function SelectInputOption<T = string>({ value, disabled, children }: SelectInputOptionProps<T>) {
|
|
476
533
|
const parentHasValue = useContext(SelectInputHasValueContext);
|
|
477
534
|
|
|
478
535
|
// Avoid flash during exit transition
|
|
@@ -483,23 +540,27 @@ function SelectInputOption<T>({ value, disabled, children }: SelectInputOptionPr
|
|
|
483
540
|
as="div"
|
|
484
541
|
value={value}
|
|
485
542
|
disabled={disabled}
|
|
486
|
-
className={({ active, disabled: uiDisabled }) =>
|
|
543
|
+
className={({ active, selected, disabled: uiDisabled }) =>
|
|
487
544
|
classNames(
|
|
488
545
|
'np-select-input-option-container np-text-body-large',
|
|
489
546
|
active && 'np-select-input-option-container--active',
|
|
547
|
+
selected && 'np-select-input-option-container--selected',
|
|
490
548
|
uiDisabled && 'np-select-input-option-container--disabled',
|
|
491
549
|
)
|
|
492
550
|
}
|
|
493
551
|
>
|
|
494
552
|
{({ selected }) => (
|
|
495
553
|
<>
|
|
554
|
+
<div className="np-select-input-option">{children}</div>
|
|
496
555
|
{cachedParentHasValue ? (
|
|
497
556
|
<Check
|
|
498
|
-
size={
|
|
499
|
-
className={classNames(
|
|
557
|
+
size={24}
|
|
558
|
+
className={classNames(
|
|
559
|
+
'np-select-input-option-check',
|
|
560
|
+
!selected && 'np-select-input-option-check--not-selected',
|
|
561
|
+
)}
|
|
500
562
|
/>
|
|
501
563
|
) : null}
|
|
502
|
-
<div className="np-select-input-option">{children}</div>
|
|
503
564
|
</>
|
|
504
565
|
)}
|
|
505
566
|
</ListboxBase.Option>
|
|
@@ -519,7 +580,7 @@ export function SelectInputOptionContent({
|
|
|
519
580
|
description,
|
|
520
581
|
icon,
|
|
521
582
|
}: SelectInputOptionContentProps) {
|
|
522
|
-
const
|
|
583
|
+
const withinTrigger = useContext(SelectInputOptionContentWithinTriggerContext);
|
|
523
584
|
|
|
524
585
|
return (
|
|
525
586
|
<div className="np-select-input-option-content-container np-text-body-large">
|
|
@@ -527,7 +588,7 @@ export function SelectInputOptionContent({
|
|
|
527
588
|
<div
|
|
528
589
|
className={classNames(
|
|
529
590
|
'np-select-input-option-content-icon',
|
|
530
|
-
!
|
|
591
|
+
!withinTrigger && 'np-select-input-option-content-icon--not-within-trigger',
|
|
531
592
|
)}
|
|
532
593
|
>
|
|
533
594
|
{icon}
|
|
@@ -538,7 +599,7 @@ export function SelectInputOptionContent({
|
|
|
538
599
|
<div
|
|
539
600
|
className={classNames(
|
|
540
601
|
'np-select-input-option-content-text-line-1',
|
|
541
|
-
|
|
602
|
+
withinTrigger && 'np-select-input-option-content-text-within-trigger',
|
|
542
603
|
)}
|
|
543
604
|
>
|
|
544
605
|
<h4 className="d-inline np-text-body-large">{title}</h4>
|
|
@@ -553,7 +614,7 @@ export function SelectInputOptionContent({
|
|
|
553
614
|
<div
|
|
554
615
|
className={classNames(
|
|
555
616
|
'np-select-input-option-content-text-secondary np-text-body-default',
|
|
556
|
-
|
|
617
|
+
withinTrigger && 'np-select-input-option-content-text-within-trigger',
|
|
557
618
|
)}
|
|
558
619
|
>
|
|
559
620
|
{description}
|
package/src/inputs/_Popover.less
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Link from '../link';
|
|
2
|
+
|
|
3
|
+
import InstructionsList from './InstructionsList';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
component: InstructionsList,
|
|
7
|
+
title: 'Typography/InstructionsList',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const Basic = () => {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<InstructionsList
|
|
14
|
+
dos={[
|
|
15
|
+
'Do an initial money transfer',
|
|
16
|
+
'Invite at least 3 friends',
|
|
17
|
+
<span key="12">
|
|
18
|
+
Paying extra{' '}
|
|
19
|
+
<Link href="" type="link-large">
|
|
20
|
+
hidden fees
|
|
21
|
+
</Link>{' '}
|
|
22
|
+
for transfers
|
|
23
|
+
</span>,
|
|
24
|
+
]}
|
|
25
|
+
donts={['Paying extra hidden fees for transfers', 'Use bad exchange rate']}
|
|
26
|
+
/>
|
|
27
|
+
<InstructionsList
|
|
28
|
+
dos={[
|
|
29
|
+
{ content: 'Multiple currencies', 'aria-label': 'Supports multiple currencies' },
|
|
30
|
+
{ content: 'Existing recipients', 'aria-label': 'Supports existing recipients' },
|
|
31
|
+
]}
|
|
32
|
+
donts={[
|
|
33
|
+
{ content: 'Create recipients', 'aria-label': "Doesn't support creating recipients" },
|
|
34
|
+
{ content: 'Edit recipients', 'aria-label': "Doesn't support editing recipients" },
|
|
35
|
+
]}
|
|
36
|
+
/>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.tw-instructions{display:flex;flex-direction:column}.tw-instructions .instruction{display:flex;margin-top:8px;margin-top:var(--padding-x-small)}.tw-instructions .instruction .do,.tw-instructions .instruction .dont{flex-shrink:0;margin-bottom:0;margin-right:
|
|
1
|
+
.tw-instructions{display:flex;flex-direction:column}.tw-instructions .instruction{display:flex;margin-top:8px;margin-top:var(--padding-x-small)}.tw-instructions .instruction .do,.tw-instructions .instruction .dont{flex-shrink:0;margin-bottom:0;margin-right:8px;margin-right:var(--padding-x-small)}.tw-instructions .instruction .do{color:var(--color-sentiment-positive)}.tw-instructions .instruction .dont{color:var(--color-sentiment-negative)}
|
|
@@ -9,28 +9,16 @@
|
|
|
9
9
|
.do,
|
|
10
10
|
.dont {
|
|
11
11
|
flex-shrink: 0;
|
|
12
|
-
margin-right: var(--padding-small);
|
|
12
|
+
margin-right: var(--padding-x-small);
|
|
13
13
|
margin-bottom: 0;
|
|
14
|
-
|
|
15
|
-
.np-theme-personal & {
|
|
16
|
-
margin-right: var(--padding-x-small);
|
|
17
|
-
}
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
.do {
|
|
21
|
-
color: var(--color-
|
|
22
|
-
|
|
23
|
-
.np-theme-personal & {
|
|
24
|
-
color: var(--color-sentiment-positive);
|
|
25
|
-
}
|
|
17
|
+
color: var(--color-sentiment-positive);
|
|
26
18
|
}
|
|
27
19
|
|
|
28
20
|
.dont {
|
|
29
|
-
color: var(--color-
|
|
30
|
-
|
|
31
|
-
.np-theme-personal & {
|
|
32
|
-
color: var(--color-sentiment-negative);
|
|
33
|
-
}
|
|
21
|
+
color: var(--color-sentiment-negative);
|
|
34
22
|
}
|
|
35
23
|
}
|
|
36
24
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen } from '../test-utils';
|
|
2
|
+
|
|
3
|
+
import InstructionsList from '.';
|
|
4
|
+
|
|
5
|
+
describe('InstructionsList', () => {
|
|
6
|
+
it('should render dos list only', () => {
|
|
7
|
+
const dos = ['Test this component', 'With multiple dos'];
|
|
8
|
+
render(<InstructionsList dos={dos} />);
|
|
9
|
+
|
|
10
|
+
expect(screen.getByText(dos[0])).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByText(dos[1])).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should render donts list only', () => {
|
|
15
|
+
const donts = ['Card validation', 'ID verification'];
|
|
16
|
+
|
|
17
|
+
render(<InstructionsList donts={donts} />);
|
|
18
|
+
expect(screen.getByText(donts[0])).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByText(donts[1])).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should render do/dont lists with description for icons', () => {
|
|
23
|
+
const dos = [{ content: 'Card validation', 'aria-label': 'Provides card validation' }];
|
|
24
|
+
const donts = [{ content: 'ID verification', 'aria-label': 'No ID verification' }];
|
|
25
|
+
|
|
26
|
+
const { container } = render(<InstructionsList dos={dos} donts={donts} />);
|
|
27
|
+
expect(screen.getByText(dos[0].content)).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByText(donts[0].content)).toBeInTheDocument();
|
|
29
|
+
|
|
30
|
+
const instructions = container.querySelectorAll('.instruction');
|
|
31
|
+
expect(instructions).toHaveLength(2);
|
|
32
|
+
expect(instructions[0]).toHaveAttribute('aria-label', dos[0]['aria-label']);
|
|
33
|
+
expect(instructions[1]).toHaveAttribute('aria-label', donts[0]['aria-label']);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -1,46 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ReactNode } from 'react';
|
|
1
|
+
import { CrossCircleFill as DontIcon, CheckCircleFill as DoIcon } from '@transferwise/icons';
|
|
2
|
+
import { ReactNode, isValidElement } from 'react';
|
|
4
3
|
|
|
5
4
|
import Body from '../body/Body';
|
|
6
|
-
import { CommonProps } from '../common';
|
|
7
|
-
import { Typography } from '../common/propsValues/typography';
|
|
5
|
+
import { Typography, CommonProps } from '../common';
|
|
8
6
|
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
type InstructionNode = {
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
['aria-label']: string;
|
|
12
10
|
};
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
type Props = CommonProps &
|
|
13
|
+
(
|
|
14
|
+
| {
|
|
15
|
+
dos?: ReactNode[];
|
|
16
|
+
donts?: ReactNode[];
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
dos?: InstructionNode[];
|
|
20
|
+
donts?: InstructionNode[];
|
|
21
|
+
}
|
|
22
|
+
);
|
|
19
23
|
|
|
24
|
+
const InstructionsList = ({ dos, donts }: Props) => {
|
|
20
25
|
return (
|
|
21
26
|
<div className="tw-instructions">
|
|
22
27
|
{dos &&
|
|
23
28
|
dos.map((doThis, index) => (
|
|
24
29
|
// eslint-disable-next-line react/no-array-index-key
|
|
25
|
-
<
|
|
26
|
-
<DoIcon size={24} className="do" />
|
|
27
|
-
<Body className="text-primary" type={Typography.BODY_LARGE}>
|
|
28
|
-
{doThis}
|
|
29
|
-
</Body>
|
|
30
|
-
</div>
|
|
30
|
+
<Instruction key={index} item={doThis} type="do" />
|
|
31
31
|
))}
|
|
32
32
|
{donts &&
|
|
33
33
|
donts.map((dont, index) => (
|
|
34
34
|
// eslint-disable-next-line react/no-array-index-key
|
|
35
|
-
<
|
|
36
|
-
<DontIcon size={24} className="dont" />
|
|
37
|
-
<Body className="text-primary" type={Typography.BODY_LARGE}>
|
|
38
|
-
{dont}
|
|
39
|
-
</Body>
|
|
40
|
-
</div>
|
|
35
|
+
<Instruction key={index} item={dont} type="dont" />
|
|
41
36
|
))}
|
|
42
37
|
</div>
|
|
43
38
|
);
|
|
44
39
|
};
|
|
45
40
|
|
|
41
|
+
function Instruction({ item, type }: { item: ReactNode | InstructionNode; type: 'do' | 'dont' }) {
|
|
42
|
+
const isInstructionNode =
|
|
43
|
+
typeof item === 'object' && item !== null && 'content' in item && 'aria-label' in item;
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className="instruction"
|
|
47
|
+
aria-label={isInstructionNode ? (item['aria-label'] as string) : undefined}
|
|
48
|
+
>
|
|
49
|
+
{type === 'do' ? (
|
|
50
|
+
<DoIcon size={24} className={type} />
|
|
51
|
+
) : (
|
|
52
|
+
<DontIcon size={24} className={type} />
|
|
53
|
+
)}
|
|
54
|
+
<Body className="text-primary" type={Typography.BODY_LARGE}>
|
|
55
|
+
{isInstructionNode ? item.content : item}
|
|
56
|
+
</Body>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
export default InstructionsList;
|