@transferwise/components 0.0.0-experimental-c91775b → 0.0.0-experimental-09b5867

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.
@@ -2,13 +2,16 @@
2
2
  @import (reference) "./../styles/less/addons/_spacing-utilities.less";
3
3
 
4
4
  .np-flow-navigation {
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
5
8
  width: 100%;
6
- // This prevents jumping when border disappears.
7
- min-height: 97px;
9
+ box-sizing: border-box;
10
+
11
+ // This is the natural height of FlowNavigation when the Avatar is displayed.
12
+ min-height: 96px;
8
13
 
9
14
  &--border-bottom {
10
- // This is the natural height of FlowNavigation when the Avatar is displayed.
11
- min-height: 96px;
12
15
  border-bottom: 1px solid var(--color-border-neutral);
13
16
  }
14
17
 
@@ -66,6 +69,19 @@
66
69
  visibility: hidden;
67
70
  }
68
71
 
72
+ &--composable {
73
+ min-height: 80px;
74
+
75
+ @media (--screen-xs) {
76
+ min-height: 128px;
77
+ }
78
+
79
+ .np-flow-navigation__content {
80
+ // Remove max-width constraint for fluid layout
81
+ max-width: none;
82
+ }
83
+ }
84
+
69
85
  .np-theme-personal--forest-green &,
70
86
  .np-theme-personal--bright-green &,
71
87
  .np-theme-personal--dark & {
@@ -25,6 +25,13 @@ export type StoryArgs = FlowNavigationProps & CustomControls;
25
25
 
26
26
  type Story = StoryObj<StoryArgs>;
27
27
 
28
+ /**
29
+ * FlowNavigation component for multi-step flows with progress tracking.
30
+ *
31
+ * **Border Behavior:**
32
+ * - Non-composable (default): Border respects `done` prop when `showBottomBorder=true`.
33
+ * - Composable: Border controlled only by `showBottomBorder`, ignoring the `done` property.
34
+ **/
28
35
  const meta: Meta<StoryArgs> = {
29
36
  component: FlowNavigation,
30
37
  title: 'Navigation/FlowNavigation',
@@ -32,6 +39,7 @@ const meta: Meta<StoryArgs> = {
32
39
  showCloseButton: { control: 'boolean', defaultValue: true },
33
40
  showMobileBackButton: { control: 'boolean', defaultValue: true },
34
41
  done: { control: 'boolean', defaultValue: false },
42
+ showBottomBorder: { control: 'boolean', defaultValue: true },
35
43
  avatarURL: { control: 'text', defaultValue: '../tapestry-01.png' },
36
44
  profileType: {
37
45
  control: 'radio',
@@ -372,3 +380,235 @@ export const WithOverlayHeaderComparison: Story = {
372
380
  );
373
381
  },
374
382
  };
383
+
384
+ export const ComposableVariant: Story = {
385
+ args: {
386
+ profileType: ProfileType.PERSONAL,
387
+ showCloseButton: true,
388
+ showMobileBackButton: true,
389
+ done: false,
390
+ avatarURL: '../tapestry-01.png',
391
+ },
392
+ parameters: {
393
+ padding: '0',
394
+ chromatic: {
395
+ viewports,
396
+ },
397
+ },
398
+ render: (args) => {
399
+ const [activeStep, setActiveStep] = useState(2);
400
+ const steps = [
401
+ {
402
+ label: 'Recipient',
403
+ onClick: () => setActiveStep(0),
404
+ },
405
+ {
406
+ label: 'Amount',
407
+ hoverLabel: (
408
+ <>
409
+ You send 100 GBP <br />
410
+ You get 99.39 GBP{' '}
411
+ </>
412
+ ),
413
+ ...(activeStep > 1 && { onClick: () => setActiveStep(1) }),
414
+ },
415
+ {
416
+ label: 'Review',
417
+ ...(activeStep > 2 && { onClick: () => setActiveStep(2) }),
418
+ },
419
+ {
420
+ label: 'Pay',
421
+ ...(activeStep > 3 && { onClick: () => setActiveStep(3) }),
422
+ },
423
+ ];
424
+ return (
425
+ <>
426
+ <FlowNavigation
427
+ composable
428
+ avatar={<AvatarView profileType={args.profileType} />}
429
+ logo={<Logo />}
430
+ activeStep={activeStep}
431
+ steps={steps}
432
+ onClose={args.showCloseButton ? () => alert('close') : undefined}
433
+ onGoBack={
434
+ args.showMobileBackButton
435
+ ? () => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)
436
+ : undefined
437
+ }
438
+ />
439
+ <Container size="narrow">
440
+ <Body className="m-a-3">
441
+ <Display type={Typography.DISPLAY_SMALL}>{steps[activeStep].label} Step</Display>
442
+ <br />
443
+ {lorem10}
444
+ </Body>
445
+ </Container>
446
+
447
+ <Sticky>
448
+ <Container
449
+ size="narrow"
450
+ className="d-flex justify-content-center align-items-center p-y-3"
451
+ >
452
+ <Button
453
+ v2
454
+ disabled={activeStep === 3}
455
+ block
456
+ onClick={() => setActiveStep(activeStep + 1)}
457
+ >
458
+ Continue
459
+ </Button>
460
+ </Container>
461
+ </Sticky>
462
+ </>
463
+ );
464
+ },
465
+ };
466
+
467
+ export const BorderControl: Story = {
468
+ args: {
469
+ profileType: ProfileType.PERSONAL,
470
+ avatarURL: '../tapestry-01.png',
471
+ },
472
+ parameters: {
473
+ chromatic: {
474
+ viewports,
475
+ },
476
+ },
477
+ render: (args) => {
478
+ const [activeStep, setActiveStep] = useState(2);
479
+
480
+ const steps = [
481
+ {
482
+ label: 'Recipient',
483
+ onClick: () => setActiveStep(0),
484
+ },
485
+ {
486
+ label: 'Amount',
487
+ hoverLabel: (
488
+ <>
489
+ You send 100 GBP <br />
490
+ You get 99.39 GBP{' '}
491
+ </>
492
+ ),
493
+ ...(activeStep > 1 && { onClick: () => setActiveStep(1) }),
494
+ },
495
+ {
496
+ label: 'Review',
497
+ ...(activeStep > 2 && { onClick: () => setActiveStep(2) }),
498
+ },
499
+ {
500
+ label: 'Pay',
501
+ ...(activeStep > 3 && { onClick: () => setActiveStep(3) }),
502
+ },
503
+ ];
504
+
505
+ return (
506
+ <>
507
+ <Body className="text-xs-center m-b-3">
508
+ <Display type={Typography.DISPLAY_SMALL}>Non-composable Variant</Display>
509
+ <br />
510
+ Border follows <code>done</code> when <code>showBottomBorder=true</code>
511
+ </Body>
512
+
513
+ {/* Non-composable: showBottomBorder=true, done=false - SHOWS BORDER */}
514
+ <FlowNavigation
515
+ avatar={<AvatarView profileType={args.profileType} />}
516
+ logo={<Logo />}
517
+ activeStep={activeStep}
518
+ done={false}
519
+ steps={steps}
520
+ showBottomBorder
521
+ onClose={() => {}}
522
+ onGoBack={() => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)}
523
+ />
524
+ <Body className="text-xs-center m-b-3">
525
+ <code>showBottomBorder=true, done=false</code> → ✅ Border shown
526
+ </Body>
527
+
528
+ {/* Non-composable: showBottomBorder=true, done=true - HIDES BORDER */}
529
+ <FlowNavigation
530
+ avatar={<AvatarView profileType={args.profileType} />}
531
+ logo={<Logo />}
532
+ activeStep={activeStep}
533
+ done
534
+ steps={steps}
535
+ showBottomBorder
536
+ onClose={() => {}}
537
+ onGoBack={() => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)}
538
+ />
539
+ <Body className="text-xs-center m-b-3">
540
+ <code>showBottomBorder=true, done=true</code> → ❌ Border hidden
541
+ </Body>
542
+
543
+ {/* Non-composable: showBottomBorder=false, done=false - HIDES BORDER */}
544
+ <FlowNavigation
545
+ avatar={<AvatarView profileType={args.profileType} />}
546
+ logo={<Logo />}
547
+ activeStep={activeStep}
548
+ done={false}
549
+ steps={steps}
550
+ showBottomBorder={false}
551
+ onClose={() => {}}
552
+ onGoBack={() => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)}
553
+ />
554
+ <Body className="text-xs-center m-b-5">
555
+ <code>showBottomBorder=false, done=false</code> → ❌ Border hidden
556
+ </Body>
557
+
558
+ <Body className="text-xs-center m-b-3 m-t-5">
559
+ <Display type={Typography.DISPLAY_SMALL}>Composable Variant</Display>
560
+ <br />
561
+ Border ignores <code>done</code>, follows <code>showBottomBorder</code> only
562
+ </Body>
563
+
564
+ {/* Composable: showBottomBorder=true, done=false - SHOWS BORDER */}
565
+ <FlowNavigation
566
+ composable
567
+ avatar={<AvatarView profileType={args.profileType} />}
568
+ logo={<Logo />}
569
+ activeStep={activeStep}
570
+ done={false}
571
+ steps={steps}
572
+ showBottomBorder
573
+ onClose={() => {}}
574
+ onGoBack={() => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)}
575
+ />
576
+ <Body className="text-xs-center m-b-3">
577
+ <code>showBottomBorder=true, done=false</code> → ✅ Border shown
578
+ </Body>
579
+
580
+ {/* Composable: showBottomBorder=true, done=true - SHOWS BORDER */}
581
+ <FlowNavigation
582
+ composable
583
+ avatar={<AvatarView profileType={args.profileType} />}
584
+ logo={<Logo />}
585
+ activeStep={activeStep}
586
+ done
587
+ steps={steps}
588
+ showBottomBorder
589
+ onClose={() => {}}
590
+ onGoBack={() => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)}
591
+ />
592
+ <Body className="text-xs-center m-b-3">
593
+ <code>showBottomBorder=true, done=true</code> → ✅ Border shown (ignores done)
594
+ </Body>
595
+
596
+ {/* Composable: showBottomBorder=false, done=false - HIDES BORDER */}
597
+ <FlowNavigation
598
+ composable
599
+ avatar={<AvatarView profileType={args.profileType} />}
600
+ logo={<Logo />}
601
+ activeStep={activeStep}
602
+ done={false}
603
+ steps={steps}
604
+ showBottomBorder={false}
605
+ onClose={() => {}}
606
+ onGoBack={() => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)}
607
+ />
608
+ <Body className="text-xs-center m-b-3">
609
+ <code>showBottomBorder=false, done=false</code> → ❌ Border hidden
610
+ </Body>
611
+ </>
612
+ );
613
+ },
614
+ };
@@ -7,6 +7,7 @@ import { CloseButton } from '../common/closeButton';
7
7
  import FlowHeader from '../common/flowHeader/FlowHeader';
8
8
  import Logo from '../logo';
9
9
  import Stepper, { type Step } from '../stepper/Stepper';
10
+ import Container from '../container';
10
11
 
11
12
  import { useScreenSize } from '../common/hooks/useScreenSize';
12
13
  import messages from './FlowNavigation.messages';
@@ -14,6 +15,8 @@ import AnimatedLabel from './animatedLabel';
14
15
  import IconButton from '../iconButton';
15
16
  import { ArrowLeft } from '@transferwise/icons';
16
17
 
18
+ const defaultLogo = <Logo />;
19
+
17
20
  export interface FlowNavigationProps {
18
21
  /** @default 0 */
19
22
  activeStep?: number;
@@ -26,18 +29,33 @@ export interface FlowNavigationProps {
26
29
  onClose?: () => void;
27
30
  /** Called when the back button is clicked. If not provided the back button won't show. The back button only shows on small screens */
28
31
  onGoBack?: () => void;
29
- /** Steps to be displayed in stepper. If you don't need the stepper, please use OverlayHeader instead */
32
+ /** Steps to be displayed in stepper. Pass an empty array to hide the stepper. */
30
33
  steps: readonly Step[];
34
+ /**
35
+ * When true, renders in composable mode using a fluid Container.
36
+ * This allows the FlowNavigation to be used within custom layout containers.
37
+ * @default false
38
+ */
39
+ composable?: boolean;
40
+ /**
41
+ * Controls whether the bottom border is displayed.
42
+ * - Non-composable variant: When true (default), shows border based on done state (!done). When false, always hides.
43
+ * - Composable variant: Directly controls border visibility, ignoring done state.
44
+ * @default true
45
+ */
46
+ showBottomBorder?: boolean;
31
47
  }
32
48
 
33
49
  const FlowNavigation = ({
34
50
  activeStep = 0,
35
51
  avatar,
36
- logo = <Logo />,
52
+ logo = defaultLogo,
37
53
  done = false,
38
54
  onClose,
39
55
  onGoBack,
40
56
  steps,
57
+ composable = false,
58
+ showBottomBorder = true,
41
59
  }: FlowNavigationProps) => {
42
60
  const intl = useIntl();
43
61
 
@@ -50,62 +68,80 @@ const FlowNavigation = ({
50
68
 
51
69
  const displayGoBack = onGoBack != null && activeStep > 0;
52
70
 
71
+ const flowHeaderContent = (
72
+ <FlowHeader
73
+ className={clsx(
74
+ 'np-flow-navigation__content',
75
+ !composable && 'p-x-3',
76
+ screenSm == null
77
+ ? 'np-flow-navigation--hidden'
78
+ : {
79
+ 'np-flow-navigation--xs-max': !screenSm,
80
+ // Size switches on parent container which may or may not have the same size as the window.
81
+ 'np-flow-navigation--sm': screenSm,
82
+ 'np-flow-navigation--lg': screenLg,
83
+ },
84
+ )}
85
+ leftContent={
86
+ <>
87
+ {!screenSm && displayGoBack ? (
88
+ <IconButton
89
+ size={40}
90
+ priority="tertiary"
91
+ type="default"
92
+ aria-label={intl.formatMessage(messages.back)}
93
+ onClick={onGoBack}
94
+ >
95
+ <ArrowLeft />
96
+ </IconButton>
97
+ ) : (
98
+ <div className="np-flow-header__left">{logo}</div>
99
+ )}
100
+ {!screenSm && !done && (
101
+ <AnimatedLabel className="m-x-1" steps={steps} activeLabel={activeStep} />
102
+ )}
103
+ </>
104
+ }
105
+ rightContent={
106
+ <div className="np-flow-header__right d-flex align-items-center justify-content-end order-2--lg">
107
+ {newAvatar}
108
+ {newAvatar && closeButton && <span className="m-x-1" />}
109
+ {closeButton}
110
+ </div>
111
+ }
112
+ bottomContent={
113
+ !done && steps.length > 0 ? (
114
+ <Stepper
115
+ activeStep={activeStep}
116
+ steps={steps}
117
+ className={clsx('np-flow-navigation__stepper')}
118
+ />
119
+ ) : null
120
+ }
121
+ layout={!screenLg ? Layout.VERTICAL : Layout.HORIZONTAL}
122
+ />
123
+ );
124
+
125
+ if (composable) {
126
+ return (
127
+ <Container
128
+ size="fluid"
129
+ className={clsx('np-flow-navigation np-flow-navigation--composable', {
130
+ 'np-flow-navigation--border-bottom': showBottomBorder,
131
+ })}
132
+ >
133
+ {flowHeaderContent}
134
+ </Container>
135
+ );
136
+ }
137
+
53
138
  return (
54
139
  <div
55
- className={clsx('np-flow-navigation d-flex align-items-center justify-content-center p-y-3', {
56
- 'np-flow-navigation--border-bottom': !done,
140
+ className={clsx('np-flow-navigation p-y-3', {
141
+ 'np-flow-navigation--border-bottom': showBottomBorder && !done,
57
142
  })}
58
143
  >
59
- <FlowHeader
60
- className={clsx(
61
- 'np-flow-navigation__content p-x-3',
62
- screenSm == null
63
- ? 'np-flow-navigation--hidden'
64
- : {
65
- 'np-flow-navigation--xs-max': !screenSm,
66
- // Size switches on parent container which may or may not have the same size as the window.
67
- 'np-flow-navigation--sm': screenSm,
68
- 'np-flow-navigation--lg': screenLg,
69
- },
70
- )}
71
- leftContent={
72
- <>
73
- {!screenSm && displayGoBack ? (
74
- <IconButton
75
- size={40}
76
- priority="tertiary"
77
- type="default"
78
- aria-label={intl.formatMessage(messages.back)}
79
- onClick={onGoBack}
80
- >
81
- <ArrowLeft />
82
- </IconButton>
83
- ) : (
84
- <div className="np-flow-header__left">{logo}</div>
85
- )}
86
- {!screenSm && !done && (
87
- <AnimatedLabel className="m-x-1" steps={steps} activeLabel={activeStep} />
88
- )}
89
- </>
90
- }
91
- rightContent={
92
- <div className="np-flow-header__right d-flex align-items-center justify-content-end order-2--lg">
93
- {newAvatar}
94
- {newAvatar && closeButton && <span className="m-x-1" />}
95
- {closeButton}
96
- </div>
97
- }
98
- bottomContent={
99
- !done && steps.length > 0 ? (
100
- <Stepper
101
- activeStep={activeStep}
102
- steps={steps}
103
- className={clsx('np-flow-navigation__stepper')}
104
- />
105
- ) : null
106
- }
107
- layout={!screenLg ? Layout.VERTICAL : Layout.HORIZONTAL}
108
- />
144
+ {flowHeaderContent}
109
145
  </div>
110
146
  );
111
147
  };
package/src/main.css CHANGED
@@ -28754,12 +28754,15 @@ button.np-option {
28754
28754
  }
28755
28755
 
28756
28756
  .np-flow-navigation {
28757
+ display: flex;
28758
+ align-items: center;
28759
+ justify-content: center;
28757
28760
  width: 100%;
28758
- min-height: 97px;
28761
+ box-sizing: border-box;
28762
+ min-height: 96px;
28759
28763
  }
28760
28764
 
28761
28765
  .np-flow-navigation--border-bottom {
28762
- min-height: 96px;
28763
28766
  border-bottom: 1px solid rgba(0,0,0,0.10196);
28764
28767
  border-bottom: 1px solid var(--color-border-neutral);
28765
28768
  }
@@ -28844,6 +28847,20 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
28844
28847
  visibility: hidden;
28845
28848
  }
28846
28849
 
28850
+ .np-flow-navigation--composable {
28851
+ min-height: 80px;
28852
+ }
28853
+
28854
+ @media (min-width: 320.02px) {
28855
+ .np-flow-navigation--composable {
28856
+ min-height: 128px;
28857
+ }
28858
+ }
28859
+
28860
+ .np-flow-navigation--composable .np-flow-navigation__content {
28861
+ max-width: none;
28862
+ }
28863
+
28847
28864
  .np-theme-personal--forest-green .np-flow-navigation .np-flow-header__left path,
28848
28865
  .np-theme-personal--bright-green .np-flow-navigation .np-flow-header__left path,
28849
28866
  .np-theme-personal--dark .np-flow-navigation .np-flow-header__left path {
@@ -15,9 +15,13 @@ interface CustomControls {
15
15
  }
16
16
  export type StoryArgs = OverlayHeaderProps & CustomControls;
17
17
 
18
+ /**
19
+ * @deprecated Use `FlowNavigation` component instead
20
+ */
18
21
  const meta: Meta<StoryArgs> = {
19
22
  component: OverlayHeader,
20
23
  title: 'Navigation/OverlayHeader',
24
+ tags: ['deprecated'],
21
25
  argTypes: {
22
26
  avatarType: { control: 'select', options: ['', 'Business', 'Profile'] },
23
27
  avatarURL: { control: 'text' },
@@ -5,6 +5,9 @@ import { CloseButton } from '../common/closeButton';
5
5
  import FlowHeader from '../common/flowHeader';
6
6
  import Logo from '../logo';
7
7
 
8
+ /**
9
+ * @deprecated Use `FlowNavigation` component instead
10
+ */
8
11
  export interface OverlayHeaderProps {
9
12
  /** An Avatar */
10
13
  avatar?: React.ReactNode;
@@ -15,6 +18,9 @@ export interface OverlayHeaderProps {
15
18
 
16
19
  const defaultLogo = <Logo />;
17
20
 
21
+ /**
22
+ * @deprecated Use `FlowNavigation` component instead
23
+ */
18
24
  export default function OverlayHeader({ avatar, onClose, logo = defaultLogo }: OverlayHeaderProps) {
19
25
  const closeButton = onClose && <CloseButton size={Size.LARGE} onClick={onClose} />;
20
26
  return (