@transferwise/components 46.160.5 → 46.160.7

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 (110) hide show
  1. package/build/ExpressiveMoneyInput/AmountInput/AmountInput.js +2 -0
  2. package/build/ExpressiveMoneyInput/AmountInput/AmountInput.js.map +1 -1
  3. package/build/ExpressiveMoneyInput/AmountInput/AmountInput.mjs +2 -0
  4. package/build/ExpressiveMoneyInput/AmountInput/AmountInput.mjs.map +1 -1
  5. package/build/ExpressiveMoneyInput/hooks/useInputStyle.js +1 -1
  6. package/build/ExpressiveMoneyInput/hooks/useInputStyle.js.map +1 -1
  7. package/build/ExpressiveMoneyInput/hooks/useInputStyle.mjs +1 -1
  8. package/build/ExpressiveMoneyInput/hooks/useInputStyle.mjs.map +1 -1
  9. package/build/Inputs/Input/Input.js +1 -0
  10. package/build/Inputs/Input/Input.js.map +1 -1
  11. package/build/Inputs/Input/Input.mjs +1 -0
  12. package/build/Inputs/Input/Input.mjs.map +1 -1
  13. package/build/Inputs/TextArea/TextArea.js +1 -0
  14. package/build/Inputs/TextArea/TextArea.js.map +1 -1
  15. package/build/Inputs/TextArea/TextArea.mjs +1 -0
  16. package/build/Inputs/TextArea/TextArea.mjs.map +1 -1
  17. package/build/MoneyInput/MoneyInput.js +1 -0
  18. package/build/MoneyInput/MoneyInput.js.map +1 -1
  19. package/build/MoneyInput/MoneyInput.mjs +1 -0
  20. package/build/MoneyInput/MoneyInput.mjs.map +1 -1
  21. package/build/PhoneNumberInput/PhoneNumberInput.js +3 -2
  22. package/build/PhoneNumberInput/PhoneNumberInput.js.map +1 -1
  23. package/build/PhoneNumberInput/PhoneNumberInput.messages.js +3 -0
  24. package/build/PhoneNumberInput/PhoneNumberInput.messages.js.map +1 -1
  25. package/build/PhoneNumberInput/PhoneNumberInput.messages.mjs +3 -0
  26. package/build/PhoneNumberInput/PhoneNumberInput.messages.mjs.map +1 -1
  27. package/build/PhoneNumberInput/PhoneNumberInput.mjs +3 -2
  28. package/build/PhoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  29. package/build/Prompt/InfoPrompt/InfoPrompt.js +13 -6
  30. package/build/Prompt/InfoPrompt/InfoPrompt.js.map +1 -1
  31. package/build/Prompt/InfoPrompt/InfoPrompt.mjs +13 -6
  32. package/build/Prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
  33. package/build/Prompt/PrimitivePrompt/PrimitivePrompt.js +1 -0
  34. package/build/Prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
  35. package/build/Prompt/PrimitivePrompt/PrimitivePrompt.mjs +1 -0
  36. package/build/Prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
  37. package/build/Typeahead/TypeaheadInput/TypeaheadInput.js +50 -20
  38. package/build/Typeahead/TypeaheadInput/TypeaheadInput.js.map +1 -1
  39. package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs +50 -20
  40. package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs.map +1 -1
  41. package/build/i18n/en.json +1 -0
  42. package/build/i18n/en.json.js +1 -0
  43. package/build/i18n/en.json.js.map +1 -1
  44. package/build/i18n/en.json.mjs +1 -0
  45. package/build/i18n/en.json.mjs.map +1 -1
  46. package/build/main.css +68 -1
  47. package/build/styles/ExpressiveMoneyInput/AmountInput/AmountInput.css +6 -0
  48. package/build/styles/ExpressiveMoneyInput/ExpressiveMoneyInput.css +6 -0
  49. package/build/styles/Inputs/Input/Input.css +19 -0
  50. package/build/styles/Inputs/TextArea/TextArea.css +16 -1
  51. package/build/styles/MoneyInput/MoneyInput.css +3 -0
  52. package/build/styles/PhoneNumberInput/PhoneNumberInput.css +7 -0
  53. package/build/styles/Prompt/PrimitivePrompt/PrimitivePrompt.css +17 -0
  54. package/build/styles/main.css +68 -1
  55. package/build/types/ExpressiveMoneyInput/AmountInput/AmountInput.d.ts.map +1 -1
  56. package/build/types/ExpressiveMoneyInput/hooks/useInputStyle.d.ts.map +1 -1
  57. package/build/types/Inputs/Input/Input.d.ts.map +1 -1
  58. package/build/types/Inputs/TextArea/TextArea.d.ts.map +1 -1
  59. package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
  60. package/build/types/PhoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  61. package/build/types/PhoneNumberInput/PhoneNumberInput.messages.d.ts +5 -0
  62. package/build/types/PhoneNumberInput/PhoneNumberInput.messages.d.ts.map +1 -1
  63. package/build/types/Prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
  64. package/build/types/Prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
  65. package/build/types/Typeahead/TypeaheadInput/TypeaheadInput.d.ts +1 -1
  66. package/build/types/Typeahead/TypeaheadInput/TypeaheadInput.d.ts.map +1 -1
  67. package/build/types/test-utils/index.d.ts +2 -0
  68. package/build/types/test-utils/index.d.ts.map +1 -1
  69. package/package.json +1 -1
  70. package/src/ExpressiveMoneyInput/AmountInput/AmountInput.css +6 -0
  71. package/src/ExpressiveMoneyInput/AmountInput/AmountInput.less +7 -0
  72. package/src/ExpressiveMoneyInput/AmountInput/AmountInput.tsx +2 -1
  73. package/src/ExpressiveMoneyInput/ExpressiveMoneyInput.css +6 -0
  74. package/src/ExpressiveMoneyInput/hooks/useInputStyle.ts +1 -2
  75. package/src/Field/_stories/Field.test.story.tsx +27 -0
  76. package/src/Inputs/Input/Input.css +19 -0
  77. package/src/Inputs/Input/Input.less +16 -0
  78. package/src/Inputs/Input/Input.tsx +1 -0
  79. package/src/Inputs/{_stories → InputGroup/_stories}/InputGroup.story.tsx +9 -9
  80. package/src/Inputs/InputGroup/_stories/InputGroup.test.story.tsx +80 -0
  81. package/src/Inputs/{_stories → SearchInput/_stories}/SearchInput.story.tsx +4 -4
  82. package/src/Inputs/SearchInput/_stories/SearchInput.test.story.tsx +38 -0
  83. package/src/Inputs/SelectInput/_stories/SelectInput.rtl.test.story.tsx +31 -19
  84. package/src/Inputs/TextArea/TextArea.css +16 -1
  85. package/src/Inputs/TextArea/TextArea.less +6 -1
  86. package/src/Inputs/TextArea/TextArea.tsx +1 -0
  87. package/src/Inputs/{_stories → TextArea/_stories}/TextArea.story.tsx +4 -4
  88. package/src/Inputs/{_stories → TextArea/_stories}/TextArea.test.story.tsx +50 -26
  89. package/src/MoneyInput/MoneyInput.css +3 -0
  90. package/src/MoneyInput/MoneyInput.less +6 -0
  91. package/src/MoneyInput/MoneyInput.tsx +1 -0
  92. package/src/MoneyInput/_stories/MoneyInput.rtl.test.story.tsx +14 -10
  93. package/src/PhoneNumberInput/PhoneNumberInput.css +7 -0
  94. package/src/PhoneNumberInput/PhoneNumberInput.less +13 -0
  95. package/src/PhoneNumberInput/PhoneNumberInput.messages.ts +5 -0
  96. package/src/PhoneNumberInput/PhoneNumberInput.tsx +3 -2
  97. package/src/PhoneNumberInput/_stories/PhoneNumberInput.rtl.test.story.tsx +15 -7
  98. package/src/Prompt/InfoPrompt/InfoPrompt.test.tsx +56 -2
  99. package/src/Prompt/InfoPrompt/InfoPrompt.tsx +14 -7
  100. package/src/Prompt/InfoPrompt/_stories/InfoPrompt.story.tsx +6 -1
  101. package/src/Prompt/PrimitivePrompt/PrimitivePrompt.css +17 -0
  102. package/src/Prompt/PrimitivePrompt/PrimitivePrompt.less +15 -0
  103. package/src/Prompt/PrimitivePrompt/PrimitivePrompt.tsx +1 -0
  104. package/src/TextareaWithDisplayFormat/_stories/TextareaWithDisplayFormat.test.story.tsx +49 -34
  105. package/src/Typeahead/TypeaheadInput/TypeaheadInput.tsx +31 -20
  106. package/src/Typeahead/_stories/Typeahead.rtl.test.story.tsx +90 -57
  107. package/src/_deprecated/Select/_stories/Select.rtl.test.story.tsx +8 -1
  108. package/src/main.css +68 -1
  109. package/src/Inputs/_stories/InputGroup.test.story.tsx +0 -69
  110. package/src/Inputs/_stories/SearchInput.test.story.tsx +0 -32
@@ -6,6 +6,12 @@
6
6
  .tw-telephone__number-input {
7
7
  margin-left: var(--size-12);
8
8
  }
9
+
10
+ &__number-input-field{
11
+ [dir="rtl"] & {
12
+ text-align: end;
13
+ }
14
+ }
9
15
  }
10
16
 
11
17
  .tw-telephone__country-select {
@@ -15,6 +21,13 @@
15
21
  .np-input-group {
16
22
  width: 100%;
17
23
  }
24
+
25
+ .np-select-input-content{
26
+ [dir="rtl"] & {
27
+ text-align: end;
28
+ direction: ltr;
29
+ }
30
+ }
18
31
  }
19
32
 
20
33
  .tw-telephone__country-select--with-input-group-addon-start {
@@ -13,4 +13,9 @@ export default defineMessages({
13
13
  id: 'neptune.PhoneNumberInput.phoneNumberLabel',
14
14
  defaultMessage: 'Phone number',
15
15
  },
16
+ phoneNumberPrefix: {
17
+ id: 'neptune.PhoneNumberInput.phoneNumberPrefix',
18
+ defaultMessage: 'Prefix',
19
+ description: 'phone number prefix / country code',
20
+ },
16
21
  });
@@ -59,7 +59,7 @@ const PhoneNumberInput = ({
59
59
  onFocus,
60
60
  onBlur,
61
61
  countryCode,
62
- searchPlaceholder = 'Prefix',
62
+ searchPlaceholder,
63
63
  size = Size.MEDIUM,
64
64
  placeholder,
65
65
  selectProps = defaultSelectProps,
@@ -232,7 +232,7 @@ const PhoneNumberInput = ({
232
232
  />
233
233
  )}
234
234
  filterable
235
- filterPlaceholder={searchPlaceholder}
235
+ filterPlaceholder={searchPlaceholder ?? formatMessage(messages.phoneNumberPrefix)}
236
236
  disabled={disabled}
237
237
  size={size}
238
238
  id={ids.countryCode.select}
@@ -261,6 +261,7 @@ const PhoneNumberInput = ({
261
261
  <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
262
262
  <Input
263
263
  ref={phoneNumberInputRef}
264
+ className="tw-telephone__number-input-field"
264
265
  id={ids.phoneNumber.input}
265
266
  autoComplete="tel-national"
266
267
  name="phoneNumber"
@@ -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 Field from '../../Field/Field';
6
6
  import PhoneNumberInput from '..';
7
7
 
@@ -37,16 +37,24 @@ export const Sizes: Story = {
37
37
  };
38
38
 
39
39
  export const DropdownOpen: Story = {
40
- render: () => (
41
- <Field label="Phone number">
42
- <PhoneNumberInput onChange={() => {}} />
43
- </Field>
44
- ),
40
+ render: () => {
41
+ const i18n = useRtlDiffI18n();
42
+
43
+ return (
44
+ <Field label="Phone number">
45
+ <PhoneNumberInput searchPlaceholder={i18n.searchPlaceholder} onChange={() => {}} />
46
+ </Field>
47
+ );
48
+ },
45
49
  play: async ({ canvasElement }) => {
46
50
  const canvas = within(canvasElement);
47
51
  const trigger = canvas.queryByRole('combobox');
48
52
  if (!trigger) return;
49
53
  await userEvent.click(trigger);
50
54
  },
51
- ...withRTLDiff({ renderAs: 'iframe', hAlign: 'center' }),
55
+ ...withRTLDiff({
56
+ renderAs: 'iframe',
57
+ hAlign: 'center',
58
+ i18n: { searchPlaceholder: { ltr: 'Country code', rtl: 'رمز الدولة' } },
59
+ }),
52
60
  };
@@ -274,11 +274,12 @@ describe('InfoPrompt', () => {
274
274
  expect(window.location.assign).not.toHaveBeenCalled();
275
275
  });
276
276
 
277
- it('should not navigate if no action href is provided', () => {
277
+ it('should call onClick on touch tap when action has no href', () => {
278
+ const onClick = jest.fn();
278
279
  render(
279
280
  <InfoPrompt
280
281
  {...defaultProps}
281
- action={{ label: 'Click me', onClick: jest.fn() }}
282
+ action={{ label: 'Click me', onClick }}
282
283
  data-testid="prompt"
283
284
  />,
284
285
  );
@@ -286,8 +287,61 @@ describe('InfoPrompt', () => {
286
287
 
287
288
  fireEvent.touchStart(prompt);
288
289
  fireEvent.touchEnd(prompt);
290
+ expect(onClick).toHaveBeenCalledTimes(1);
289
291
  expect(window.location.assign).not.toHaveBeenCalled();
290
292
  expect(window.open).not.toHaveBeenCalled();
291
293
  });
294
+
295
+ it('should call onClick and navigate when action has both href and onClick', () => {
296
+ const onClick = jest.fn();
297
+ render(
298
+ <InfoPrompt
299
+ {...defaultProps}
300
+ action={{ label: 'Learn more', href: '/learn-more', onClick }}
301
+ data-testid="prompt"
302
+ />,
303
+ );
304
+ const prompt = screen.getByTestId('prompt');
305
+
306
+ fireEvent.touchStart(prompt);
307
+ fireEvent.touchEnd(prompt);
308
+ expect(onClick).toHaveBeenCalledTimes(1);
309
+ expect(window.location.assign).toHaveBeenCalledWith('/learn-more');
310
+ });
311
+
312
+ it('should not fire action when tapping dismiss button', () => {
313
+ const onClick = jest.fn();
314
+ render(
315
+ <InfoPrompt
316
+ {...defaultProps}
317
+ action={{ label: 'Learn more', href: '/learn-more', onClick }}
318
+ data-testid="prompt"
319
+ onDismiss={jest.fn()}
320
+ />,
321
+ );
322
+ const dismissButton = screen.getByRole('button', { name: 'Close', hidden: true });
323
+
324
+ fireEvent.touchStart(dismissButton);
325
+ fireEvent.touchEnd(dismissButton);
326
+ expect(onClick).not.toHaveBeenCalled();
327
+ expect(window.location.assign).not.toHaveBeenCalled();
328
+ });
329
+
330
+ it('should not double-fire onClick when tapping the action element directly', () => {
331
+ const onClick = jest.fn();
332
+ render(
333
+ <InfoPrompt
334
+ {...defaultProps}
335
+ action={{ label: 'Learn more', href: '/learn-more', onClick }}
336
+ data-testid="prompt"
337
+ />,
338
+ );
339
+ const actionLink = screen.getByRole('link', { name: 'Learn more', hidden: true });
340
+
341
+ fireEvent.touchStart(actionLink);
342
+ fireEvent.touchEnd(actionLink);
343
+ expect(onClick).not.toHaveBeenCalled();
344
+ expect(window.location.assign).not.toHaveBeenCalled();
345
+ });
292
346
  });
293
347
  });
@@ -1,4 +1,4 @@
1
- import { Children, HTMLAttributes, ReactNode, isValidElement, useState } from 'react';
1
+ import { Children, HTMLAttributes, ReactNode, TouchEvent, isValidElement, useState } from 'react';
2
2
  import { LiveRegion, Sentiment, Typography } from '../../common';
3
3
  import type { AriaLive } from '../../common';
4
4
  import { GiftBox } from '@transferwise/icons';
@@ -103,16 +103,23 @@ export const InfoPrompt = ({
103
103
  ? Sentiment.POSITIVE
104
104
  : (sentiment as Exclude<SurfaceSentiment, 'proposition'>);
105
105
 
106
- const handleTouchStart = () => {
106
+ const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
107
+ const target = event.target as HTMLElement;
108
+ if (target.closest('.wds-prompt__dismiss') || target.closest('.wds-info-prompt__action')) {
109
+ return;
110
+ }
107
111
  setShouldFire(true);
108
112
  };
109
113
 
110
114
  const handleTouchEnd = () => {
111
- if (shouldFire && action?.href) {
112
- if (action.target === '_blank') {
113
- window.top?.open(action.href, '_blank', 'noopener, noreferrer');
114
- } else {
115
- window.top?.location.assign(action.href);
115
+ if (shouldFire && action) {
116
+ (action.onClick as (() => void) | undefined)?.();
117
+ if (action.href) {
118
+ if (action.target === '_blank') {
119
+ window.top?.open(action.href, '_blank', 'noopener, noreferrer');
120
+ } else {
121
+ window.top?.location.assign(action.href);
122
+ }
116
123
  }
117
124
  }
118
125
  setShouldFire(false);
@@ -236,7 +236,11 @@ export const WithTitle: Story = {
236
236
 
237
237
  /**
238
238
  * Use the `action` prop to add a clickable link below the message.
239
- * Actions can have an `href` for navigation or an `onClick` for custom behavior.
239
+ * Actions can have an `href` for navigation, an `onClick` for custom behavior, or both.
240
+ *
241
+ * On touch devices the entire prompt surface acts as a tap target for the action,
242
+ * making it easier to interact with on small screens. A scroll gesture (touch move)
243
+ * cancels the tap. Tapping the dismiss button does not trigger the action.
240
244
  */
241
245
  export const WithAction: Story = {
242
246
  render: (args: InfoPromptProps) => (
@@ -322,6 +326,7 @@ export const Dismissible: Story = {
322
326
  sentiment="success"
323
327
  title="Payment successful"
324
328
  description="Your payment was successful."
329
+ action={{ label: 'Learn more', href: '#', target: '_blank' }}
325
330
  onDismiss={() => setShowPrompt(false)}
326
331
  />
327
332
  ) : (
@@ -51,3 +51,20 @@
51
51
  grid-column: span 3;
52
52
  }
53
53
  }
54
+ .wds-prompt__dismiss {
55
+ position: relative;
56
+ }
57
+ .wds-prompt__dismiss:before {
58
+ --size: calc(var(--size-24) + var(--Prompt-padding-x, var(--padding-x-small)) * 2);
59
+ --offset: calc(-1 * var(--Prompt-padding-x, var(--padding-x-small)));
60
+ content: "";
61
+ position: absolute;
62
+ inset: calc(-1 * 8px);
63
+ inset: var(--offset);
64
+ inline-size: calc(24px + 8px * 2);
65
+ inline-size: var(--size);
66
+ block-size: calc(24px + 8px * 2);
67
+ block-size: var(--size);
68
+ border-radius: 10px;
69
+ border-radius: var(--Prompt-border-radius, var(--radius-small));
70
+ }
@@ -46,4 +46,19 @@
46
46
  }
47
47
  }
48
48
  }
49
+
50
+ &__dismiss {
51
+ position: relative;
52
+
53
+ &:before {
54
+ --size: calc(var(--size-24) + var(--Prompt-padding-x, var(--padding-x-small)) * 2);
55
+ --offset: calc(-1 * var(--Prompt-padding-x, var(--padding-x-small)));
56
+ content: "";
57
+ position: absolute;
58
+ inset: var(--offset);
59
+ width: var(--size);
60
+ height: var(--size);
61
+ border-radius: var(--Prompt-border-radius, var(--radius-small));
62
+ }
63
+ }
49
64
  }
@@ -75,6 +75,7 @@ export const PrimitivePrompt = forwardRef<HTMLDivElement, PrimitivePromptProps>(
75
75
  size={24}
76
76
  priority="secondary"
77
77
  aria-label={intl.formatMessage(closeBtnMessages.ariaLabel)}
78
+ className="wds-prompt__dismiss"
78
79
  onClick={onDismiss}
79
80
  >
80
81
  <Cross />
@@ -1,8 +1,8 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
 
3
- import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
4
- import TextareaWithDisplayFormat from '..';
3
+ import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
5
4
  import Field from '../../Field/Field';
5
+ import TextareaWithDisplayFormat from '..';
6
6
 
7
7
  export default {
8
8
  component: TextareaWithDisplayFormat,
@@ -13,36 +13,51 @@ export default {
13
13
  type Story = StoryObj<typeof TextareaWithDisplayFormat>;
14
14
 
15
15
  export const RTLDiff: Story = {
16
- render: () => (
17
- <>
18
- <Field id="card-code" label="Gift card code">
19
- <TextareaWithDisplayFormat
20
- id="card-code"
21
- value="1234567890123456"
22
- maxLength={16}
23
- displayPattern="**** - **** - ****"
24
- onChange={() => {}}
25
- />
26
- </Field>
27
- <Field id="iban" label="IBAN">
28
- <TextareaWithDisplayFormat
29
- id="iban"
30
- value="GB29NWBK60161331926819"
31
- maxLength={22}
32
- displayPattern="** **** **** **** **** **"
33
- onChange={() => {}}
34
- />
35
- </Field>
36
- <Field id="empty" label="Empty with placeholder">
37
- <TextareaWithDisplayFormat
38
- id="empty"
39
- value=""
40
- displayPattern="**** - **** - ****"
41
- placeholder="Enter code"
42
- onChange={() => {}}
43
- />
44
- </Field>
45
- </>
46
- ),
47
- ...withRTLDiff(),
16
+ render: () => {
17
+ const i18n = useRtlDiffI18n();
18
+
19
+ return (
20
+ <>
21
+ <Field id="empty" label="Empty with placeholder">
22
+ <TextareaWithDisplayFormat
23
+ id="empty"
24
+ value=""
25
+ displayPattern="**** - **** - ****"
26
+ placeholder={i18n.placeholder}
27
+ onChange={() => {}}
28
+ />
29
+ </Field>
30
+ <Field id="card-code" label="Localised pattern">
31
+ <TextareaWithDisplayFormat
32
+ id="card-code"
33
+ value={i18n.value}
34
+ maxLength={16}
35
+ displayPattern="**** - **** - ****"
36
+ onChange={() => {}}
37
+ />
38
+ </Field>
39
+ <Field id="iban" label="IBAN (not localised)">
40
+ <TextareaWithDisplayFormat
41
+ id="iban"
42
+ value="GB29NWBK60161331926819"
43
+ maxLength={22}
44
+ displayPattern="** **** **** **** **** **"
45
+ onChange={() => {}}
46
+ />
47
+ </Field>
48
+ </>
49
+ );
50
+ },
51
+ ...withRTLDiff({
52
+ i18n: {
53
+ value: {
54
+ ltr: 'XKQMRTBWJNFL',
55
+ rtl: 'خكقمرتبوجنفل',
56
+ },
57
+ placeholder: {
58
+ ltr: 'Enter code',
59
+ rtl: 'أدخل الرمز',
60
+ },
61
+ },
62
+ }),
48
63
  };
@@ -6,6 +6,8 @@ import { Component, createRef, ReactNode } from 'react';
6
6
 
7
7
  import { Input } from '../../Inputs/Input/Input';
8
8
  import { TypeaheadOption, TypeaheadProps } from '../Typeahead';
9
+ import { Direction } from '../../common';
10
+ import { DirectionContext } from '../../Provider/Direction';
9
11
 
10
12
  const DEFAULT_INPUT_MIN_WIDTH = 10;
11
13
 
@@ -67,7 +69,7 @@ export default class TypeaheadInput<T> extends Component<
67
69
  });
68
70
  };
69
71
 
70
- renderInput = () => {
72
+ renderInput = (isRTL: boolean) => {
71
73
  const {
72
74
  typeaheadId,
73
75
  autoFocus,
@@ -90,6 +92,7 @@ export default class TypeaheadInput<T> extends Component<
90
92
  return (
91
93
  <Input
92
94
  ref={this.inputRef}
95
+ dir={isRTL ? 'rtl' : 'auto'}
93
96
  className={clsx(multiple && 'typeahead__input')}
94
97
  name={name}
95
98
  id={`input-${typeaheadId}`}
@@ -116,26 +119,34 @@ export default class TypeaheadInput<T> extends Component<
116
119
  render() {
117
120
  const { multiple, selected, value, maxHeight, renderChip } = this.props;
118
121
 
119
- return multiple ? (
120
- <div
121
- className="form-control typeahead__input-container"
122
- style={{ maxHeight }}
123
- onClick={() => {
124
- this.inputRef.current?.focus();
125
- }}
126
- >
127
- <div className="typeahead__input-wrapper">
128
- {selected && selected.map((chip, idx) => renderChip(chip, idx))}
122
+ return (
123
+ <DirectionContext.Consumer>
124
+ {(direction) => {
125
+ const isRTL = direction === Direction.RTL;
129
126
 
130
- {this.renderInput()}
131
- <div className="typeahead__input-aligner" />
132
- </div>
133
- <div ref={this.sizerRef} className="sizer form-control typeahead__input">
134
- {value}
135
- </div>
136
- </div>
137
- ) : (
138
- this.renderInput()
127
+ return multiple ? (
128
+ <div
129
+ className="form-control typeahead__input-container"
130
+ style={{ maxHeight }}
131
+ onClick={() => {
132
+ this.inputRef.current?.focus();
133
+ }}
134
+ >
135
+ <div className="typeahead__input-wrapper">
136
+ {selected && selected.map((chip, idx) => renderChip(chip, idx))}
137
+
138
+ {this.renderInput(isRTL)}
139
+ <div className="typeahead__input-aligner" />
140
+ </div>
141
+ <div ref={this.sizerRef} className="sizer form-control typeahead__input">
142
+ {value}
143
+ </div>
144
+ </div>
145
+ ) : (
146
+ this.renderInput(isRTL)
147
+ );
148
+ }}
149
+ </DirectionContext.Consumer>
139
150
  );
140
151
  }
141
152
  }
@@ -2,7 +2,11 @@ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { userEvent, within } from 'storybook/test';
3
3
  import { Search as SearchIcon } from '@transferwise/icons';
4
4
 
5
- import { forEachColumn, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
5
+ import {
6
+ forEachColumn,
7
+ useRtlDiffI18n,
8
+ withRTLDiff,
9
+ } from '../../../.storybook/decorators/RtlDiffDecorator';
6
10
  import { Size } from '../../common';
7
11
  import Typeahead from '..';
8
12
 
@@ -14,70 +18,99 @@ export default {
14
18
 
15
19
  type Story = StoryObj<typeof Typeahead>;
16
20
 
17
- const baseOptions = [
18
- { label: 'British pound', note: 'GBP' },
19
- { label: 'Euro', note: 'EUR' },
20
- { label: 'US Dollar', note: 'USD' },
21
- ];
21
+ const localisedStrings = {
22
+ gbp: { ltr: 'British pound', rtl: 'جنيه إسترليني' },
23
+ eur: { ltr: 'Euro', rtl: 'يورو' },
24
+ usd: { ltr: 'US Dollar', rtl: 'دولار أمريكي' },
25
+ placeholder: { ltr: 'Search currencies…', rtl: 'ابحث عن العملات...' },
26
+ };
22
27
 
23
28
  export const TriggerVariants: Story = {
24
- render: () => (
25
- <>
26
- <Typeahead
27
- id="rtl-addon-md"
28
- name="rtl-addon-md"
29
- placeholder="Search currencies…"
30
- size={Size.MEDIUM}
31
- addon={<SearchIcon size={24} />}
32
- clearable
33
- options={baseOptions}
34
- initialValue={[{ label: 'British pound', note: 'GBP' }]}
35
- onChange={() => {}}
36
- />
29
+ render: () => {
30
+ const i18n = useRtlDiffI18n();
31
+
32
+ const baseOptions = [
33
+ { label: i18n.gbp, note: 'GBP' },
34
+ { label: i18n.eur, note: 'EUR' },
35
+ { label: i18n.usd, note: 'USD' },
36
+ ];
37
+
38
+ return (
39
+ <>
40
+ <Typeahead
41
+ id="rtl-addon-placeholder"
42
+ name="rtl-addon-placeholder"
43
+ placeholder={i18n.placeholder}
44
+ size={Size.MEDIUM}
45
+ addon={<SearchIcon size={24} />}
46
+ clearable
47
+ options={baseOptions}
48
+ onChange={() => {}}
49
+ />
50
+ <Typeahead
51
+ id="rtl-addon-md"
52
+ name="rtl-addon-md"
53
+ placeholder={i18n.placeholder}
54
+ size={Size.MEDIUM}
55
+ addon={<SearchIcon size={24} />}
56
+ clearable
57
+ options={baseOptions}
58
+ initialValue={[{ label: i18n.gbp, note: 'GBP' }]}
59
+ onChange={() => {}}
60
+ />
61
+ <Typeahead
62
+ id="rtl-addon-lg"
63
+ name="rtl-addon-lg"
64
+ placeholder={i18n.placeholder}
65
+ size={Size.LARGE}
66
+ addon={<SearchIcon size={24} />}
67
+ clearable
68
+ options={baseOptions}
69
+ initialValue={[{ label: i18n.eur, note: 'EUR' }]}
70
+ onChange={() => {}}
71
+ />
72
+ <Typeahead
73
+ id="rtl-multiple"
74
+ name="rtl-multiple"
75
+ placeholder={i18n.placeholder}
76
+ size={Size.MEDIUM}
77
+ addon={<SearchIcon size={24} />}
78
+ multiple
79
+ clearable
80
+ options={baseOptions}
81
+ initialValue={[
82
+ { label: i18n.gbp, note: 'GBP' },
83
+ { label: i18n.eur, note: 'EUR' },
84
+ ]}
85
+ onChange={() => {}}
86
+ />
87
+ </>
88
+ );
89
+ },
90
+ ...withRTLDiff({ width: '80%', i18n: localisedStrings }),
91
+ };
92
+
93
+ export const OpenDropdown: Story = {
94
+ render: () => {
95
+ const i18n = useRtlDiffI18n();
96
+
97
+ return (
37
98
  <Typeahead
38
- id="rtl-addon-lg"
39
- name="rtl-addon-lg"
40
- placeholder="Search currencies…"
99
+ id="rtl-open"
100
+ name="rtl-open"
101
+ placeholder={i18n.placeholder}
41
102
  size={Size.LARGE}
42
103
  addon={<SearchIcon size={24} />}
43
104
  clearable
44
- options={baseOptions}
45
- initialValue={[{ label: 'Euro', note: 'EUR' }]}
46
- onChange={() => {}}
47
- />
48
- <Typeahead
49
- id="rtl-multiple"
50
- name="rtl-multiple"
51
- placeholder="Search currencies…"
52
- size={Size.MEDIUM}
53
- addon={<SearchIcon size={24} />}
54
- multiple
55
- clearable
56
- options={baseOptions}
57
- initialValue={[
58
- { label: 'British pound', note: 'GBP' },
59
- { label: 'Euro', note: 'EUR' },
105
+ options={[
106
+ { label: i18n.gbp, note: 'GBP' },
107
+ { label: i18n.eur, note: 'EUR' },
108
+ { label: i18n.usd, note: 'USD' },
60
109
  ]}
61
110
  onChange={() => {}}
62
111
  />
63
- </>
64
- ),
65
- ...withRTLDiff({ width: '80%' }),
66
- };
67
-
68
- export const OpenDropdown: Story = {
69
- render: () => (
70
- <Typeahead
71
- id="rtl-open"
72
- name="rtl-open"
73
- placeholder="Search currencies…"
74
- size={Size.LARGE}
75
- addon={<SearchIcon size={24} />}
76
- clearable
77
- options={baseOptions}
78
- onChange={() => {}}
79
- />
80
- ),
112
+ );
113
+ },
81
114
  play: async ({ canvasElement }) => {
82
115
  await forEachColumn(canvasElement, async (column) => {
83
116
  const scope = within(column);
@@ -86,5 +119,5 @@ export const OpenDropdown: Story = {
86
119
  await userEvent.keyboard('Brit');
87
120
  });
88
121
  },
89
- ...withRTLDiff({ width: '80%' }),
122
+ ...withRTLDiff({ width: '80%', i18n: localisedStrings }),
90
123
  };
@@ -67,5 +67,12 @@ export const OpenDropdown: Story = {
67
67
  if (!trigger) return;
68
68
  await userEvent.click(trigger);
69
69
  },
70
- ...withRTLDiff({ renderAs: 'iframe', width: '80%' }),
70
+ ...withRTLDiff({
71
+ renderAs: 'iframe',
72
+ width: '80%',
73
+ i18n: {
74
+ searchPlaceholder: { ltr: 'Type a currency', rtl: 'اكتب عملة' },
75
+ placeholder: { ltr: 'Select currency', rtl: 'اختر العملة' },
76
+ },
77
+ }),
71
78
  };