@transferwise/components 0.0.0-experimental-8bc11c4 → 0.0.0-experimental-151e673

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.
@@ -117,27 +117,26 @@ export const EdgeInstances: Story = {
117
117
  ),
118
118
  };
119
119
 
120
+ const horizontalSizes = [16, 24, 32, 40, 48, 56, 72, 88, 96] as const;
121
+ const diagonalSizes = [24, 32, 40, 48, 56, 72, 88, 96] as const;
122
+
123
+ const rowStyle = { display: 'flex', alignItems: 'center', gap: '16px', flexWrap: 'wrap' } as const;
124
+ const twoAvatars = [{ asset: <Plane /> }, { asset: <Plane /> }] as const;
125
+
120
126
  export const RTLDiff: Story = {
121
127
  render: () => (
122
128
  <>
123
- <AvatarLayout
124
- orientation="horizontal"
125
- size={48}
126
- avatars={[{ asset: <Flag code="br" /> }, { asset: <Flag code="jp" /> }]}
127
- />
128
-
129
- <AvatarLayout
130
- orientation="horizontal"
131
- size={48}
132
- interactive
133
- avatars={[{ asset: <Freeze /> }, { asset: <Freeze /> }]}
134
- />
129
+ <div style={rowStyle}>
130
+ {horizontalSizes.map((size) => (
131
+ <AvatarLayout key={size} orientation="horizontal" size={size} avatars={[...twoAvatars]} />
132
+ ))}
133
+ </div>
135
134
 
136
- <AvatarLayout
137
- orientation="horizontal"
138
- size={48}
139
- avatars={[{ profileName: 'Jay Jay' }, { profileName: 'Amy Amy' }]}
140
- />
135
+ <div style={rowStyle}>
136
+ {diagonalSizes.map((size) => (
137
+ <AvatarLayout key={size} orientation="diagonal" size={size} avatars={[...twoAvatars]} />
138
+ ))}
139
+ </div>
141
140
 
142
141
  <AvatarLayout
143
142
  orientation="horizontal"
@@ -148,32 +147,8 @@ export const RTLDiff: Story = {
148
147
 
149
148
  <AvatarLayout
150
149
  orientation="horizontal"
151
- size={32}
152
- avatars={[{ asset: <Flag code="eu" /> }, { asset: <Flag code="gb" /> }]}
153
- />
154
-
155
- <AvatarLayout
156
- orientation="horizontal"
157
- size={72}
158
- avatars={[{ asset: <Freeze /> }, { asset: <Freeze /> }, { asset: <Freeze /> }]}
159
- />
160
-
161
- <AvatarLayout
162
- orientation="diagonal"
163
- size={48}
164
- avatars={[{ asset: <Flag code="br" /> }, { asset: <Flag code="jp" /> }]}
165
- />
166
-
167
- <AvatarLayout
168
- orientation="diagonal"
169
- size={48}
170
- avatars={[{ profileName: 'Jay Jay' }, { profileName: 'Amy Amy' }]}
171
- />
172
-
173
- <AvatarLayout
174
- orientation="diagonal"
175
150
  size={72}
176
- avatars={[{ asset: <Freeze /> }, { asset: <Freeze /> }]}
151
+ avatars={[{ asset: <Plane /> }, { asset: <Plane /> }, { asset: <Plane /> }]}
177
152
  />
178
153
  </>
179
154
  ),
@@ -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
- {/* Required label (default) — label text aligns to start */}
65
- <Field label="Required field">
66
- <Input defaultValue="Input value" />
67
- </Field>
61
+ render: () => {
62
+ const i18n = useRtlDiffI18n();
63
+ return (
64
+ <>
65
+ <Field label={i18n.requiredLabel}>
66
+ <Input defaultValue={i18n.inputValue} />
67
+ </Field>
68
68
 
69
- {/* Optional label — "(optional)" suffix uses directional margin-left */}
70
- <Field label="Optional field" required={false}>
71
- <Input defaultValue="Input value" />
72
- </Field>
69
+ <Field label={i18n.optionalLabel} required={false}>
70
+ <Input defaultValue={i18n.inputValue} />
71
+ </Field>
73
72
 
74
- {/* Label + description + message — full vertical layout with directional icon */}
75
- <Field
76
- label="Field with description and message"
77
- required={false}
78
- description={lorem10}
79
- message="This is a validation message"
80
- sentiment="negative"
81
- >
82
- <Input defaultValue="Input value" />
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
- {/* Message only (no label) — InlinePrompt icon+text layout visible */}
86
- <Field message="A helpful message" sentiment="positive">
87
- <Input defaultValue="Input value" />
88
- </Field>
83
+ <Field message={i18n.helpfulMessage} sentiment="positive">
84
+ <Input defaultValue={i18n.inputValue} />
85
+ </Field>
89
86
 
90
- {/* Loading message — spinner replaces icon but layout structure is identical */}
91
- <Field label="Field with loading message" message="Saving…" messageLoading>
92
- <Input defaultValue="Input value" />
93
- </Field>
87
+ <Field label={i18n.loadingLabel} message={i18n.savingMessage} messageLoading>
88
+ <Input defaultValue={i18n.inputValue} />
89
+ </Field>
94
90
 
95
- {/* Textarea with char counter — counter uses text-align:right + margin-left:auto */}
96
- <Field label="Textarea with char counter" required={false} description="Max 100 characters">
97
- <TextArea maxLength={100} defaultValue="Textarea value" />
98
- </Field>
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,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
- import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
2
+ import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
3
3
  import Info from '../../Info';
4
4
  import Label from '..';
5
5
 
@@ -12,25 +12,38 @@ export default {
12
12
  type Story = StoryObj<typeof Label>;
13
13
 
14
14
  export const RTLDiff: Story = {
15
- render: () => (
16
- <>
17
- <Label>Plain label text</Label>
18
- <Label>
19
- <Label.Optional>Label with optional marker</Label.Optional>
20
- </Label>
21
- <Label>
22
- <div>
23
- Label with info icon{' '}
24
- <Info content="Helpful tooltip content" aria-label="More information" />
25
- </div>
26
- </Label>
27
- <Label>
28
- <Label.Optional>
29
- Label with optional and info icon{' '}
30
- <Info content="Helpful tooltip content" aria-label="More information" />
31
- </Label.Optional>
32
- </Label>
33
- </>
34
- ),
35
- ...withRTLDiff(),
15
+ render: () => {
16
+ const i18n = useRtlDiffI18n();
17
+ return (
18
+ <>
19
+ <Label>{i18n.plain}</Label>
20
+ <Label>
21
+ <Label.Optional>{i18n.optional}</Label.Optional>
22
+ </Label>
23
+ <Label>
24
+ <div>
25
+ {i18n.withInfo} <Info content={i18n.tooltip} aria-label={i18n.moreInfo} />
26
+ </div>
27
+ </Label>
28
+ <Label>
29
+ <Label.Optional>
30
+ {i18n.optionalWithInfo} <Info content={i18n.tooltip} aria-label={i18n.moreInfo} />
31
+ </Label.Optional>
32
+ </Label>
33
+ </>
34
+ );
35
+ },
36
+ ...withRTLDiff({
37
+ i18n: {
38
+ plain: { ltr: 'Plain label text', rtl: 'نص التسمية العادي' },
39
+ optional: { ltr: 'Label with optional marker', rtl: 'تسمية مع علامة اختيارية' },
40
+ withInfo: { ltr: 'Label with info icon', rtl: 'تسمية مع أيقونة معلومات' },
41
+ optionalWithInfo: {
42
+ ltr: 'Label with optional and info icon',
43
+ rtl: 'تسمية مع اختيارية وأيقونة معلومات',
44
+ },
45
+ tooltip: { ltr: 'Helpful tooltip content', rtl: 'محتوى تلميح مفيد' },
46
+ moreInfo: { ltr: 'More information', rtl: 'مزيد من المعلومات' },
47
+ },
48
+ }),
36
49
  };
@@ -14,16 +14,6 @@ export default {
14
14
 
15
15
  type Story = StoryObj<typeof Popover>;
16
16
 
17
- const iframeDiff = withRTLDiff({ renderAs: 'iframe' });
18
-
19
- const containerStyle: React.CSSProperties = {
20
- display: 'flex',
21
- justifyContent: 'center',
22
- alignItems: 'center',
23
- minHeight: '24rem',
24
- width: '100%',
25
- };
26
-
27
17
  const clickTrigger = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
28
18
  const canvas = within(canvasElement);
29
19
  const trigger = canvas.queryByRole('button');
@@ -33,64 +23,64 @@ const clickTrigger = async ({ canvasElement }: { canvasElement: HTMLElement }) =
33
23
 
34
24
  export const PlacementRight: Story = {
35
25
  render: () => (
36
- <div style={containerStyle}>
37
- <Popover
38
- preferredPlacement={Position.RIGHT}
39
- title="Guaranteed rate"
40
- content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
41
- >
42
- <Button v2>Placement right</Button>
43
- </Popover>
44
- </div>
26
+ <Popover
27
+ preferredPlacement={Position.RIGHT}
28
+ title="Guaranteed rate"
29
+ content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
30
+ >
31
+ <Button size="sm" v2>
32
+ right
33
+ </Button>
34
+ </Popover>
45
35
  ),
46
36
  play: clickTrigger,
47
- ...iframeDiff,
37
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'left', vAlign: 'center' }),
48
38
  };
49
39
 
50
40
  export const PlacementLeft: Story = {
51
41
  render: () => (
52
- <div style={containerStyle}>
53
- <Popover
54
- preferredPlacement={Position.LEFT}
55
- title="Guaranteed rate"
56
- content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
57
- >
58
- <Button v2>Placement left</Button>
59
- </Popover>
60
- </div>
42
+ <Popover
43
+ preferredPlacement={Position.LEFT}
44
+ title="Guaranteed rate"
45
+ content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
46
+ >
47
+ <Button size="sm" v2>
48
+ left
49
+ </Button>
50
+ </Popover>
61
51
  ),
62
52
  play: clickTrigger,
63
- ...iframeDiff,
53
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'right', vAlign: 'center' }),
64
54
  };
65
55
 
66
56
  export const PlacementTop: Story = {
67
57
  render: () => (
68
- <div style={containerStyle}>
69
- <Popover
70
- preferredPlacement={Position.TOP}
71
- title="Guaranteed rate"
72
- content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
73
- >
74
- <Button v2>Placement top</Button>
75
- </Popover>
76
- </div>
58
+ <Popover
59
+ preferredPlacement={Position.TOP}
60
+ title="Guaranteed rate"
61
+ content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
62
+ >
63
+ <Button size="sm" v2>
64
+ top
65
+ </Button>
66
+ </Popover>
77
67
  ),
78
68
  play: clickTrigger,
79
- ...iframeDiff,
69
+ ...withRTLDiff({ renderAs: 'iframe', vAlign: 'bottom', hAlign: 'center' }),
80
70
  };
81
71
 
82
72
  export const PlacementBottom: Story = {
83
73
  render: () => (
84
- <div style={containerStyle}>
85
- <Popover
86
- preferredPlacement={Position.BOTTOM}
87
- title="Guaranteed rate"
88
- content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
89
- >
90
- <Button v2>Placement bottom</Button>
91
- </Popover>
92
- </div>
74
+ <Popover
75
+ preferredPlacement={Position.BOTTOM}
76
+ title="Guaranteed rate"
77
+ content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
78
+ >
79
+ <Button size="sm" v2>
80
+ bottom
81
+ </Button>
82
+ </Popover>
93
83
  ),
94
84
  play: clickTrigger,
95
- ...iframeDiff,
85
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'center' }),
96
86
  };
@@ -35,7 +35,8 @@
35
35
  background: var(--color-background-screen);
36
36
  border-radius: 24px;
37
37
  border-radius: var(--size-24);
38
- transition: left 300ms;
38
+ transition: left 300ms,
39
+ right 300ms;
39
40
  }
40
41
  .segmented-control__segments--no-animate::after {
41
42
  transition: none !important;
@@ -12,7 +12,7 @@
12
12
  justify-content: center;
13
13
  align-items: center;
14
14
  background: var(--color-background-neutral);
15
- border-radius: var(--size-24);
15
+ border-radius: var(--size-24);
16
16
  transition: outline 300ms;
17
17
  outline: 2px solid transparent;
18
18
  }
@@ -31,7 +31,9 @@
31
31
  z-index: 0;
32
32
  background: var(--color-background-screen);
33
33
  border-radius: var(--size-24);
34
- transition: left 300ms;
34
+ transition:
35
+ left 300ms,
36
+ right 300ms;
35
37
  }
36
38
 
37
39
  .segmented-control__segments--no-animate::after {
@@ -3,6 +3,7 @@ import { createRef, useEffect, useRef, useState } from 'react';
3
3
 
4
4
  import Body from '../Body';
5
5
  import { Typography } from '../common';
6
+ import { useDirection } from '../common/hooks';
6
7
 
7
8
  type SegmentBase = { id: string; label: string; value: string };
8
9
 
@@ -40,6 +41,7 @@ const SegmentedControl = ({
40
41
  }: SegmentedControlProps) => {
41
42
  const isMountedRef = useRef(false);
42
43
  const [animate, setAnimate] = useState(false);
44
+ const { isRTL } = useDirection();
43
45
 
44
46
  const segmentsRef = useRef<HTMLDivElement>(null);
45
47
 
@@ -62,8 +64,12 @@ const SegmentedControl = ({
62
64
  // This is so we can animate the highlight to the selected segment
63
65
  if (selectedSegmentRef?.current && segmentsRef.current) {
64
66
  const { style } = segmentsRef.current;
65
- style.setProperty('--segment-highlight-width', `${selectedSegmentRef.current.offsetWidth}px`);
66
- style.setProperty('--segment-highlight-x', `${selectedSegmentRef.current.offsetLeft}px`);
67
+ const segment = selectedSegmentRef.current;
68
+ const inlineOffset = isRTL
69
+ ? segmentsRef.current.offsetWidth - segment.offsetLeft - segment.offsetWidth
70
+ : segment.offsetLeft;
71
+ style.setProperty('--segment-highlight-width', `${segment.offsetWidth}px`);
72
+ style.setProperty('--segment-highlight-x', `${inlineOffset}px`);
67
73
  }
68
74
  };
69
75
 
@@ -1,3 +1,4 @@
1
+ import { useState } from 'react';
1
2
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
3
  import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
3
4
  import SegmentedControl from '../SegmentedControl';
@@ -22,24 +23,31 @@ const viewSegments = [
22
23
  { id: 'seg-gamma', label: 'Gamma', value: 'gamma', controls: 'panel-gamma' },
23
24
  ] as const;
24
25
 
25
- export const RTLDiff: Story = {
26
- render: () => (
26
+ function InteractiveSegmentedControl() {
27
+ const [inputValue, setInputValue] = useState('one');
28
+ const [viewValue, setViewValue] = useState('beta');
29
+
30
+ return (
27
31
  <>
28
32
  <SegmentedControl
29
33
  name="rtl-input"
30
34
  mode="input"
31
35
  segments={inputSegments}
32
- value="one"
33
- onChange={() => {}}
36
+ value={inputValue}
37
+ onChange={setInputValue}
34
38
  />
35
39
  <SegmentedControl
36
40
  name="rtl-view"
37
41
  mode="view"
38
42
  segments={viewSegments}
39
- value="alpha"
40
- onChange={() => {}}
43
+ value={viewValue}
44
+ onChange={setViewValue}
41
45
  />
42
46
  </>
43
- ),
44
- ...withRTLDiff(),
47
+ );
48
+ }
49
+
50
+ export const RTLDiff: Story = {
51
+ render: () => <InteractiveSegmentedControl />,
52
+ ...withRTLDiff({ width: 'stretch', renderAs: 'iframe' }),
45
53
  };