@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.
Files changed (48) hide show
  1. package/build/index.esm.js +339 -293
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +339 -292
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +1 -1
  6. package/build/styles/inputs/SelectInput.css +1 -1
  7. package/build/styles/instructionsList/InstructionsList.css +1 -1
  8. package/build/styles/main.css +1 -1
  9. package/build/styles/stepper/Stepper.css +1 -1
  10. package/build/styles/tooltip/Tooltip.css +1 -1
  11. package/build/types/button/Button.d.ts.map +1 -1
  12. package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts +13 -0
  13. package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts.map +1 -0
  14. package/build/types/index.d.ts +1 -1
  15. package/build/types/index.d.ts.map +1 -1
  16. package/build/types/inputs/SelectInput.d.ts +16 -6
  17. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  18. package/build/types/instructionsList/InstructionsList.d.ts +10 -3
  19. package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
  20. package/build/types/processIndicator/ProcessIndicator.d.ts +1 -1
  21. package/build/types/tooltip/Tooltip.d.ts +2 -1
  22. package/build/types/tooltip/Tooltip.d.ts.map +1 -1
  23. package/package.json +4 -3
  24. package/src/button/Button.story.tsx +6 -0
  25. package/src/button/Button.tsx +6 -1
  26. package/src/common/polymorphicWithOverrides/PolymorphicWithOverrides.tsx +19 -0
  27. package/src/index.ts +3 -0
  28. package/src/inputs/SelectInput.css +1 -1
  29. package/src/inputs/SelectInput.less +8 -2
  30. package/src/inputs/SelectInput.story.tsx +52 -5
  31. package/src/inputs/SelectInput.tsx +165 -104
  32. package/src/inputs/_Popover.less +1 -1
  33. package/src/instructionsList/InstructionList.story.tsx +39 -0
  34. package/src/instructionsList/InstructionsList.css +1 -1
  35. package/src/instructionsList/InstructionsList.less +3 -15
  36. package/src/instructionsList/InstructionsList.spec.tsx +35 -0
  37. package/src/instructionsList/InstructionsList.tsx +40 -25
  38. package/src/main.css +1 -1
  39. package/src/processIndicator/ProcessIndicator.js +2 -2
  40. package/src/stepper/Stepper.css +1 -1
  41. package/src/stepper/Stepper.less +1 -1
  42. package/src/tooltip/Tooltip.css +1 -1
  43. package/src/tooltip/Tooltip.less +13 -0
  44. package/src/tooltip/Tooltip.spec.tsx +97 -29
  45. package/src/tooltip/Tooltip.tsx +24 -31
  46. package/src/tooltip/__snapshots__/Tooltip.spec.tsx.snap +31 -0
  47. package/src/instructionsList/InstructionList.story.js +0 -27
  48. 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 { createContext, useState, useRef, forwardRef, useEffect, useMemo, useContext } from 'react';
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, type ButtonInputProps } from './_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
- const SelectInputOptionContentCompactContext = createContext(false);
42
-
43
- interface SelectInputOptionItem<T = string> {
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
- export function SelectInput<T>({
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
- <InputGroup
166
- addonEnd={{
167
- content: (
168
- <span
169
- className={classNames(
170
- 'np-select-input-addon-container',
171
- uiDisabled && 'disabled',
172
- )}
173
- >
174
- {onClear != null && value != null ? (
175
- <>
176
- <button
177
- type="button"
178
- aria-label={intl.formatMessage(messages.ariaLabel)}
179
- disabled={uiDisabled}
180
- className="np-select-input-addon np-select-input-addon--interactive"
181
- onClick={(event) => {
182
- event.preventDefault();
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
- <Cross size={16} />
188
- </button>
189
- <span className="np-select-input-addon-separator" />
190
- </>
191
- ) : null}
192
-
193
- <span className="np-select-input-addon">
194
- <ChevronDown size={16} />
195
- </span>
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
- <OptionsOverlay
203
- open={open}
204
- renderTrigger={({ ref, getInteractionProps }) => (
205
- <ListboxBase.Button
206
- ref={mergeRefs([ref, triggerRef])}
207
- as={SelectInputButton}
208
- overrides={getInteractionProps()}
209
- onClick={() => {
210
- setOpen((prev) => !prev);
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
- interface SelectInputButtonProps extends ButtonInputProps {
245
- overrides?: { [key: string]: unknown };
246
- }
288
+ export type SelectInputTriggerButtonProps<T extends React.ComponentType | 'button' = 'button'> = {
289
+ as?: T;
290
+ } & React.ComponentPropsWithoutRef<T>;
247
291
 
248
- const SelectInputButton = forwardRef(function SelectInputButton(
249
- { overrides, ...restProps }: SelectInputButtonProps,
250
- ref: React.ForwardedRef<HTMLButtonElement>,
251
- ) {
252
- return <ButtonInput ref={ref} {...restProps} {...overrides} />;
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>({ item, renderValue, needle }: SelectInputItemViewProps<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" aria-hidden />;
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={16}
499
- className={classNames(!selected && 'np-select-input-option-check--not-selected')}
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 compact = useContext(SelectInputOptionContentCompactContext);
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
- !compact && 'np-select-input-option-content-icon--not-compact',
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
- compact && 'np-select-input-option-content-text-compact',
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
- compact && 'np-select-input-option-content-text-compact',
617
+ withinTrigger && 'np-select-input-option-content-text-within-trigger',
557
618
  )}
558
619
  >
559
620
  {description}
@@ -3,7 +3,7 @@
3
3
  display: flex;
4
4
  max-height: var(--max-height);
5
5
  width: var(--width);
6
- min-width: min-content;
6
+ min-width: 20rem;
7
7
  flex-direction: column;
8
8
  border-radius: var(--radius-small);
9
9
  background-color: var(--color-background-elevated);
@@ -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:16px;margin-right:var(--padding-small)}.np-theme-personal .tw-instructions .instruction .do,.np-theme-personal .tw-instructions .instruction .dont{margin-right:8px;margin-right:var(--padding-x-small)}.tw-instructions .instruction .do{color:#008026;color:var(--color-content-positive)}.np-theme-personal .tw-instructions .instruction .do{color:var(--color-sentiment-positive)}.tw-instructions .instruction .dont{color:#cf2929;color:var(--color-content-negative)}.np-theme-personal .tw-instructions .instruction .dont{color:var(--color-sentiment-negative)}
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-content-positive);
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-content-negative);
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 { CrossCircle, CrossCircleFill, CheckCircle, CheckCircleFill } from '@transferwise/icons';
2
- import { useTheme } from '@wise/components-theming';
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 Props = CommonProps & {
10
- dos?: ReactNode[];
11
- donts?: ReactNode[];
7
+ type InstructionNode = {
8
+ content: ReactNode;
9
+ ['aria-label']: string;
12
10
  };
13
11
 
14
- const InstructionsList = (props: Props) => {
15
- const { isModern } = useTheme();
16
- const { dos, donts } = props;
17
- const DontIcon = isModern ? CrossCircleFill : CrossCircle;
18
- const DoIcon = isModern ? CheckCircleFill : CheckCircle;
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
- <div key={index} className="instruction">
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
- <div key={index} className="instruction">
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;