@transferwise/components 46.163.0 → 46.165.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/FlowNavigation/FlowNavigation.js +4 -2
- package/build/FlowNavigation/FlowNavigation.js.map +1 -1
- package/build/FlowNavigation/FlowNavigation.mjs +4 -2
- package/build/FlowNavigation/FlowNavigation.mjs.map +1 -1
- package/build/FlowNavigation/helpers.js +24 -0
- package/build/FlowNavigation/helpers.js.map +1 -0
- package/build/FlowNavigation/helpers.mjs +21 -0
- package/build/FlowNavigation/helpers.mjs.map +1 -0
- package/build/ListItem/ListItem.js +2 -0
- package/build/ListItem/ListItem.js.map +1 -1
- package/build/ListItem/ListItem.mjs +2 -0
- package/build/ListItem/ListItem.mjs.map +1 -1
- package/build/i18n/cs.json +1 -0
- package/build/i18n/cs.json.js +1 -0
- package/build/i18n/cs.json.js.map +1 -1
- package/build/i18n/cs.json.mjs +1 -0
- package/build/i18n/cs.json.mjs.map +1 -1
- package/build/i18n/de.json +1 -0
- package/build/i18n/de.json.js +1 -0
- package/build/i18n/de.json.js.map +1 -1
- package/build/i18n/de.json.mjs +1 -0
- package/build/i18n/de.json.mjs.map +1 -1
- package/build/i18n/fr.json +1 -0
- package/build/i18n/fr.json.js +1 -0
- package/build/i18n/fr.json.js.map +1 -1
- package/build/i18n/fr.json.mjs +1 -0
- package/build/i18n/fr.json.mjs.map +1 -1
- package/build/i18n/pt.json +1 -0
- package/build/i18n/pt.json.js +1 -0
- package/build/i18n/pt.json.js.map +1 -1
- package/build/i18n/pt.json.mjs +1 -0
- package/build/i18n/pt.json.mjs.map +1 -1
- package/build/i18n/ru.json +1 -0
- package/build/i18n/ru.json.js +1 -0
- package/build/i18n/ru.json.js.map +1 -1
- package/build/i18n/ru.json.mjs +1 -0
- package/build/i18n/ru.json.mjs.map +1 -1
- package/build/i18n/th.json +1 -0
- package/build/i18n/th.json.js +1 -0
- package/build/i18n/th.json.js.map +1 -1
- package/build/i18n/th.json.mjs +1 -0
- package/build/i18n/th.json.mjs.map +1 -1
- package/build/i18n/tr.json +1 -0
- package/build/i18n/tr.json.js +1 -0
- package/build/i18n/tr.json.js.map +1 -1
- package/build/i18n/tr.json.mjs +1 -0
- package/build/i18n/tr.json.mjs.map +1 -1
- package/build/main.css +5 -0
- package/build/styles/ListItem/ListItem.css +5 -0
- package/build/styles/main.css +5 -0
- package/build/types/Body/Body.d.ts +2 -2
- package/build/types/FlowNavigation/FlowNavigation.d.ts +6 -2
- package/build/types/FlowNavigation/FlowNavigation.d.ts.map +1 -1
- package/build/types/FlowNavigation/helpers.d.ts +5 -0
- package/build/types/FlowNavigation/helpers.d.ts.map +1 -0
- package/build/types/FlowNavigation/index.d.ts +1 -0
- package/build/types/FlowNavigation/index.d.ts.map +1 -1
- package/build/types/FlowNavigation/types.d.ts +15 -0
- package/build/types/FlowNavigation/types.d.ts.map +1 -0
- package/build/types/IconButton/IconButton.d.ts +1 -1
- package/build/types/ListItem/ListItem.d.ts +6 -1
- package/build/types/ListItem/ListItem.d.ts.map +1 -1
- package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
- package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/Title/Title.d.ts +2 -2
- package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
- package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
- package/build/types/index.d.ts +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/FlowNavigation/FlowNavigation.test.tsx +45 -0
- package/src/FlowNavigation/FlowNavigation.tsx +14 -3
- package/src/FlowNavigation/_stories/FlowNavigation.CriticalBanner.docs.mdx +66 -0
- package/src/FlowNavigation/_stories/FlowNavigation.story.tsx +49 -0
- package/src/FlowNavigation/helpers.ts +28 -0
- package/src/FlowNavigation/index.ts +1 -0
- package/src/FlowNavigation/types.ts +16 -0
- package/src/ListItem/ListItem.css +5 -0
- package/src/ListItem/ListItem.less +4 -0
- package/src/ListItem/ListItem.test.tsx +17 -0
- package/src/ListItem/ListItem.tsx +7 -0
- package/src/ListItem/_stories/ListItem.layout.test.story.tsx +23 -0
- package/src/ListItem/_stories/ListItem.story.tsx +8 -0
- package/src/Prompt/CriticalBanner/_stories/CriticalBanner.story.tsx +2 -0
- package/src/Snackbar/Snackbar.test.tsx +190 -0
- package/src/i18n/cs.json +1 -0
- package/src/i18n/de.json +1 -0
- package/src/i18n/fr.json +1 -0
- package/src/i18n/pt.json +1 -0
- package/src/i18n/ru.json +1 -0
- package/src/i18n/th.json +1 -0
- package/src/i18n/tr.json +1 -0
- package/src/index.ts +1 -1
- package/src/main.css +5 -0
|
@@ -15,6 +15,8 @@ import AnimatedLabel from './AnimatedLabel';
|
|
|
15
15
|
import IconButton from '../IconButton';
|
|
16
16
|
import { ArrowLeft } from '@transferwise/icons';
|
|
17
17
|
import { CriticalBanner, CriticalBannerProps } from '../Prompt';
|
|
18
|
+
import { isCriticalBannerData, mapCriticalBannerDataToBannerProps } from './helpers';
|
|
19
|
+
import { CriticalBannerLegacyData } from './types';
|
|
18
20
|
|
|
19
21
|
const defaultLogo = <Logo />;
|
|
20
22
|
|
|
@@ -32,8 +34,11 @@ export interface FlowNavigationProps {
|
|
|
32
34
|
onGoBack?: () => void;
|
|
33
35
|
/** Steps to be displayed in stepper. If not provided, the stepper won't show. */
|
|
34
36
|
steps?: readonly Step[];
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Optional critical banner data, to be displayed above the flow navigation.
|
|
39
|
+
* Accepts either the internal `CriticalBannerProps` shape, or the legacy backend `CriticalBannerData` shape — the latter is mapped to `CriticalBannerProps` internally.
|
|
40
|
+
*/
|
|
41
|
+
banner?: CriticalBannerLegacyData | CriticalBannerProps | null;
|
|
37
42
|
/**
|
|
38
43
|
* Controls whether the bottom border is displayed.
|
|
39
44
|
* @default true
|
|
@@ -59,6 +64,12 @@ const FlowNavigation = ({
|
|
|
59
64
|
const hasSteps = steps && steps.length > 0;
|
|
60
65
|
const displayGoBack = onGoBack != null && activeStep > 0;
|
|
61
66
|
|
|
67
|
+
const resolvedBannerData: CriticalBannerProps | null = banner
|
|
68
|
+
? isCriticalBannerData(banner)
|
|
69
|
+
? mapCriticalBannerDataToBannerProps(banner)
|
|
70
|
+
: banner
|
|
71
|
+
: null;
|
|
72
|
+
|
|
62
73
|
const closeButton = onClose != null && <CloseButton size="lg" onClick={onClose} />;
|
|
63
74
|
const newAvatar = done ? null : avatar;
|
|
64
75
|
|
|
@@ -117,7 +128,7 @@ const FlowNavigation = ({
|
|
|
117
128
|
|
|
118
129
|
return (
|
|
119
130
|
<>
|
|
120
|
-
{
|
|
131
|
+
{resolvedBannerData && <CriticalBanner {...resolvedBannerData} />}
|
|
121
132
|
<Container
|
|
122
133
|
size="fluid"
|
|
123
134
|
className={clsx('np-flow-navigation', {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Meta, Source } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Navigation/FlowNavigation/Critical Banner Guide" />
|
|
4
|
+
|
|
5
|
+
# Critical Banner in FlowNavigation
|
|
6
|
+
|
|
7
|
+
The `FlowNavigation` component supports displaying critical banners above the navigation, by passing a `banner` prop.
|
|
8
|
+
|
|
9
|
+
`banner` accepts either the internal `CriticalBannerProps` shape, or the legacy backend `CriticalBannerData` shape (e.g. as received from `navigation-service`/`navigation-ui`) — `FlowNavigation` maps the legacy shape to `CriticalBannerProps` internally, so no manual mapping is required.
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
When you're using `FlowNavigation` directly from `@transferwise/components`.
|
|
14
|
+
|
|
15
|
+
<Source
|
|
16
|
+
code={`import { FlowNavigation, CriticalBannerProps } from '@transferwise/components';
|
|
17
|
+
|
|
18
|
+
const banner: CriticalBannerProps = {
|
|
19
|
+
sentiment: 'negative',
|
|
20
|
+
title: 'Action required',
|
|
21
|
+
description: 'Please verify your identity to continue with this transfer.',
|
|
22
|
+
action: {
|
|
23
|
+
label: 'Verify now',
|
|
24
|
+
onClick: () => {
|
|
25
|
+
// Handle action
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
<FlowNavigation
|
|
31
|
+
banner={banner}
|
|
32
|
+
steps={steps}
|
|
33
|
+
activeStep={activeStep}
|
|
34
|
+
avatar={avatar}
|
|
35
|
+
onClose={onClose}
|
|
36
|
+
/>;
|
|
37
|
+
`}
|
|
38
|
+
language="typescript"
|
|
39
|
+
dark
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
## Using Legacy Backend Data
|
|
43
|
+
|
|
44
|
+
If your application receives banner data from a backend service (e.g. navigation-ui) in the legacy shape, you can pass it directly to `banner` — `FlowNavigation` maps it to the internal `CriticalBannerProps` shape automatically.
|
|
45
|
+
|
|
46
|
+
<Source
|
|
47
|
+
code={`import { FlowNavigation, CriticalBannerData } from '@transferwise/components';
|
|
48
|
+
|
|
49
|
+
const banner: CriticalBannerData = {
|
|
50
|
+
id: 'banner-123',
|
|
51
|
+
title: 'Your account requires attention', // Rendered as the banner description
|
|
52
|
+
subtitle: 'Action required', // Rendered as the banner title
|
|
53
|
+
style: 'WARNING',
|
|
54
|
+
action: { label: 'Take action', href: '/action' },
|
|
55
|
+
collapsed: false, // Rendered expanded
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
<FlowNavigation
|
|
59
|
+
banner={banner}
|
|
60
|
+
steps={steps}
|
|
61
|
+
activeStep={activeStep}
|
|
62
|
+
/>;
|
|
63
|
+
`}
|
|
64
|
+
language="typescript"
|
|
65
|
+
dark
|
|
66
|
+
/>
|
|
@@ -646,3 +646,52 @@ export const WithCriticalBanner: Story = {
|
|
|
646
646
|
);
|
|
647
647
|
},
|
|
648
648
|
};
|
|
649
|
+
|
|
650
|
+
export const WithLegacyCriticalBannerData: Story = {
|
|
651
|
+
args: {
|
|
652
|
+
profileType: ProfileType.PERSONAL,
|
|
653
|
+
showCloseButton: true,
|
|
654
|
+
showMobileBackButton: true,
|
|
655
|
+
avatarURL: '../tapestry-01.png',
|
|
656
|
+
},
|
|
657
|
+
parameters: {
|
|
658
|
+
padding: '0',
|
|
659
|
+
},
|
|
660
|
+
render: (args) => {
|
|
661
|
+
const [activeStep, setActiveStep] = useState(2);
|
|
662
|
+
|
|
663
|
+
const steps = [
|
|
664
|
+
{ label: 'Recipient', onClick: () => setActiveStep(0) },
|
|
665
|
+
{ label: 'Amount', ...(activeStep > 1 && { onClick: () => setActiveStep(1) }) },
|
|
666
|
+
{ label: 'Review', ...(activeStep > 2 && { onClick: () => setActiveStep(2) }) },
|
|
667
|
+
{ label: 'Pay', ...(activeStep > 3 && { onClick: () => setActiveStep(3) }) },
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
return (
|
|
671
|
+
// Legacy backend banner data shape - mapped to CriticalBannerProps internally
|
|
672
|
+
<FlowNavigation
|
|
673
|
+
avatar={<AvatarView profileType={args.profileType} />}
|
|
674
|
+
logo={<Logo />}
|
|
675
|
+
activeStep={activeStep}
|
|
676
|
+
steps={steps}
|
|
677
|
+
banner={{
|
|
678
|
+
id: 'banner-123',
|
|
679
|
+
title: 'Your account requires attention',
|
|
680
|
+
subtitle: 'Action required',
|
|
681
|
+
style: 'WARNING',
|
|
682
|
+
action: {
|
|
683
|
+
label: 'Take action',
|
|
684
|
+
href: '#action',
|
|
685
|
+
},
|
|
686
|
+
collapsed: false,
|
|
687
|
+
}}
|
|
688
|
+
onClose={args.showCloseButton ? () => {} : undefined}
|
|
689
|
+
onGoBack={
|
|
690
|
+
args.showMobileBackButton
|
|
691
|
+
? () => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)
|
|
692
|
+
: undefined
|
|
693
|
+
}
|
|
694
|
+
/>
|
|
695
|
+
);
|
|
696
|
+
},
|
|
697
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CriticalBannerProps } from '../Prompt';
|
|
2
|
+
import { CriticalBannerLegacyData } from './types';
|
|
3
|
+
|
|
4
|
+
const sentimentMap: Record<CriticalBannerLegacyData['style'], CriticalBannerProps['sentiment']> = {
|
|
5
|
+
NEGATIVE: 'negative',
|
|
6
|
+
WARNING: 'warning',
|
|
7
|
+
NEUTRAL: 'neutral',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const isCriticalBannerData = (
|
|
11
|
+
banner: CriticalBannerLegacyData | CriticalBannerProps,
|
|
12
|
+
): banner is CriticalBannerLegacyData => 'style' in banner;
|
|
13
|
+
|
|
14
|
+
export const mapCriticalBannerDataToBannerProps = (
|
|
15
|
+
data: CriticalBannerLegacyData,
|
|
16
|
+
): CriticalBannerProps => ({
|
|
17
|
+
id: data.id,
|
|
18
|
+
title: data.title,
|
|
19
|
+
description: data.subtitle, // subtitle → description
|
|
20
|
+
sentiment: sentimentMap[data.style],
|
|
21
|
+
action: data.action
|
|
22
|
+
? {
|
|
23
|
+
label: data.action.label,
|
|
24
|
+
href: data.action.href,
|
|
25
|
+
}
|
|
26
|
+
: undefined,
|
|
27
|
+
expanded: !data.collapsed, // collapsed → expanded (inverted)
|
|
28
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type CriticalBannerStyle = 'WARNING' | 'NEGATIVE' | 'NEUTRAL';
|
|
2
|
+
|
|
3
|
+
export type CriticalBannerAction = {
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type CriticalBannerLegacyData = {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
style: CriticalBannerStyle;
|
|
13
|
+
action?: CriticalBannerAction;
|
|
14
|
+
mixpanelEvent?: string;
|
|
15
|
+
collapsed?: boolean;
|
|
16
|
+
};
|
|
@@ -397,6 +397,11 @@
|
|
|
397
397
|
padding-inline: 0;
|
|
398
398
|
container-type: inline-size;
|
|
399
399
|
}
|
|
400
|
+
.wds-list-item-paddingY-24 {
|
|
401
|
+
padding-block: 24px;
|
|
402
|
+
padding-block: var(--size-24);
|
|
403
|
+
padding-inline: 0;
|
|
404
|
+
}
|
|
400
405
|
.wds-list-item + .wds-list-item-spotlight,
|
|
401
406
|
.wds-list-item-spotlight + .wds-list-item {
|
|
402
407
|
margin-block-start: 16px;
|
|
@@ -1513,6 +1513,23 @@ describe('ListItem', () => {
|
|
|
1513
1513
|
});
|
|
1514
1514
|
});
|
|
1515
1515
|
|
|
1516
|
+
describe('paddingY', () => {
|
|
1517
|
+
it('uses the default vertical padding when not set', () => {
|
|
1518
|
+
render(<ListItem title="Test Title" />);
|
|
1519
|
+
expect(screen.getByRole('listitem')).not.toHaveClass('wds-list-item-paddingY-24');
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
it('uses the default vertical padding when set to 12', () => {
|
|
1523
|
+
render(<ListItem title="Test Title" paddingY={12} />);
|
|
1524
|
+
expect(screen.getByRole('listitem')).not.toHaveClass('wds-list-item-paddingY-24');
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
it('uses the larger vertical padding when set to 24', () => {
|
|
1528
|
+
render(<ListItem title="Test Title" paddingY={24} />);
|
|
1529
|
+
expect(screen.getByRole('listitem')).toHaveClass('wds-list-item-paddingY-24');
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1516
1533
|
describe('disable states', () => {
|
|
1517
1534
|
describe('short-lived', () => {
|
|
1518
1535
|
it('should override existing prompt if disabled and disabledPromptMessage are set', () => {
|
|
@@ -59,6 +59,11 @@ export type ListItemProps = {
|
|
|
59
59
|
* Highlights the list item as an action to be taken or already taken. <br />
|
|
60
60
|
*/
|
|
61
61
|
spotlight?: 'active' | 'inactive';
|
|
62
|
+
/**
|
|
63
|
+
* Vertical padding of the list item, in pixels.
|
|
64
|
+
* @default 12
|
|
65
|
+
*/
|
|
66
|
+
paddingY?: 12 | 24;
|
|
62
67
|
title: ReactNode;
|
|
63
68
|
subtitle?: ReactNode;
|
|
64
69
|
/**
|
|
@@ -112,6 +117,7 @@ export const ListItem = ({
|
|
|
112
117
|
inverted,
|
|
113
118
|
media,
|
|
114
119
|
spotlight,
|
|
120
|
+
paddingY = 12,
|
|
115
121
|
valueTitle,
|
|
116
122
|
valueSubtitle,
|
|
117
123
|
control = null,
|
|
@@ -232,6 +238,7 @@ export const ListItem = ({
|
|
|
232
238
|
'wds-list-item-partially-interactive': isPartiallyInteractive,
|
|
233
239
|
[`wds-list-item-spotlight wds-list-item-spotlight-${spotlight}`]:
|
|
234
240
|
isFullyInteractive && !!spotlight,
|
|
241
|
+
'wds-list-item-paddingY-24': paddingY === 24,
|
|
235
242
|
disabled: disabled && !isPartiallyInteractive,
|
|
236
243
|
'disabled--has-prompt-reason':
|
|
237
244
|
!disabledPromptMessage && disabled && !isPartiallyInteractive,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment } from 'react';
|
|
2
2
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
+
import { Plane } from '@transferwise/icons';
|
|
3
4
|
import List from '../../List';
|
|
4
5
|
import { ListItem, type ListItemProps } from '../ListItem';
|
|
5
6
|
import { lorem10, lorem20, lorem5 } from '../../test-utils';
|
|
@@ -61,6 +62,28 @@ export const GapsBetweenItems: Story = {
|
|
|
61
62
|
},
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
export const PaddingY: Story = {
|
|
66
|
+
render: () => {
|
|
67
|
+
const props = {
|
|
68
|
+
title: lorem5,
|
|
69
|
+
subtitle: lorem10,
|
|
70
|
+
media: <ListItem.AvatarLayout size={32} avatars={[{ asset: <Plane /> }, { asset: '7+' }]} />,
|
|
71
|
+
control: CONTROLS.navigation,
|
|
72
|
+
};
|
|
73
|
+
return (
|
|
74
|
+
<List>
|
|
75
|
+
{([12, 24] as const).map((paddingY) => (
|
|
76
|
+
<Fragment key={paddingY}>
|
|
77
|
+
<ListItem {...props} paddingY={paddingY} />
|
|
78
|
+
<ListItem {...props} paddingY={paddingY} spotlight="active" />
|
|
79
|
+
<ListItem {...props} paddingY={paddingY} spotlight="inactive" />
|
|
80
|
+
</Fragment>
|
|
81
|
+
))}
|
|
82
|
+
</List>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
64
87
|
type PreviewStoryArgs = ListItemProps & {
|
|
65
88
|
previewImageSize: ListItemMediaSize;
|
|
66
89
|
previewWithLineGuides: boolean;
|
|
@@ -60,6 +60,7 @@ export default {
|
|
|
60
60
|
disabled: false,
|
|
61
61
|
disabledPromptMessage: undefined,
|
|
62
62
|
spotlight: undefined,
|
|
63
|
+
paddingY: undefined,
|
|
63
64
|
inverted: false,
|
|
64
65
|
valueColumnWidth: undefined,
|
|
65
66
|
},
|
|
@@ -73,6 +74,13 @@ export default {
|
|
|
73
74
|
inactive: 'inactive',
|
|
74
75
|
},
|
|
75
76
|
},
|
|
77
|
+
paddingY: {
|
|
78
|
+
control: 'radio',
|
|
79
|
+
options: ['unset (undefined)', 12, 24],
|
|
80
|
+
mapping: {
|
|
81
|
+
'unset (undefined)': undefined,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
76
84
|
},
|
|
77
85
|
} satisfies Meta<ListItemProps>;
|
|
78
86
|
|
|
@@ -14,6 +14,8 @@ import { CriticalBanner, type CriticalBannerProps } from '..';
|
|
|
14
14
|
* **Design guidance**: <a href="https://docs.wise.design/components/critical-banner" target="_blank">wise.design/components/critical-banner</a>
|
|
15
15
|
*
|
|
16
16
|
* Can be controlled or uncontrolled. When `onToggle` is provided, the consumer manages the expanded state via `expanded` and the banner calls `onToggle` when the user clicks the chevron. When `onToggle` is not provided, the banner manages its own expanded state internally and toggles it when the user clicks the chevron.
|
|
17
|
+
*
|
|
18
|
+
* **Using with FlowNavigation**: See the [FlowNavigation Critical Banner Guide](?path=/docs/navigation-flownavigation-critical-banner-guide--docs) for integration patterns and data mapping examples.
|
|
17
19
|
*/
|
|
18
20
|
const meta = {
|
|
19
21
|
component: CriticalBanner,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { act, render, screen, userEvent } from '../test-utils';
|
|
2
|
+
import Button from '../Button';
|
|
3
|
+
|
|
4
|
+
import { Snackbar, type SnackbarProps } from './Snackbar';
|
|
5
|
+
import { SnackbarConsumer } from './SnackbarContext';
|
|
6
|
+
import SnackbarProvider from './SnackbarProvider';
|
|
7
|
+
|
|
8
|
+
const CSS_TRANSITION_DURATION = 400;
|
|
9
|
+
const TIMEOUT = 4000;
|
|
10
|
+
|
|
11
|
+
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTimeAsync });
|
|
12
|
+
|
|
13
|
+
const advanceTimers = (duration: number) => {
|
|
14
|
+
act(() => {
|
|
15
|
+
jest.advanceTimersByTime(duration);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const renderSnackbar = (props: Partial<SnackbarProps> = {}) =>
|
|
20
|
+
render(<Snackbar text="" timeout={TIMEOUT} timestamp={0} {...props} />);
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.useFakeTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.clearAllTimers();
|
|
28
|
+
jest.useRealTimers();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Snackbar', () => {
|
|
32
|
+
it('renders the live region wrapper even while no notification is shown', () => {
|
|
33
|
+
renderSnackbar();
|
|
34
|
+
|
|
35
|
+
expect(screen.getByRole('alert')).toHaveAttribute('aria-live', 'assertive');
|
|
36
|
+
expect(screen.getByRole('alert')).toHaveClass('snackbar');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not show a notification that is already present on mount', () => {
|
|
40
|
+
renderSnackbar({ text: 'Changes saved', timestamp: 1 });
|
|
41
|
+
|
|
42
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('shows the notification once text arrives after mount', () => {
|
|
46
|
+
const { rerender } = renderSnackbar();
|
|
47
|
+
|
|
48
|
+
rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders an action and calls its onClick handler', async () => {
|
|
54
|
+
const onClick = jest.fn();
|
|
55
|
+
const { rerender } = renderSnackbar();
|
|
56
|
+
|
|
57
|
+
rerender(
|
|
58
|
+
<Snackbar
|
|
59
|
+
action={{ label: 'View', onClick }}
|
|
60
|
+
text="Transfer completed"
|
|
61
|
+
timeout={TIMEOUT}
|
|
62
|
+
timestamp={1}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
await user.click(screen.getByRole('button', { name: 'View' }));
|
|
66
|
+
|
|
67
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('dismisses the notification once the timeout elapses', () => {
|
|
71
|
+
const { rerender } = renderSnackbar();
|
|
72
|
+
|
|
73
|
+
rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
|
|
74
|
+
|
|
75
|
+
advanceTimers(TIMEOUT - 1);
|
|
76
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
77
|
+
|
|
78
|
+
advanceTimers(1);
|
|
79
|
+
advanceTimers(CSS_TRANSITION_DURATION);
|
|
80
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('exits the current notification before entering the next one', () => {
|
|
84
|
+
const { rerender } = renderSnackbar();
|
|
85
|
+
|
|
86
|
+
rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
|
|
87
|
+
rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={2} />);
|
|
88
|
+
|
|
89
|
+
advanceTimers(CSS_TRANSITION_DURATION - 1);
|
|
90
|
+
expect(screen.queryByText('Second message')).not.toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
advanceTimers(1);
|
|
93
|
+
expect(screen.getByText('Second message')).toBeInTheDocument();
|
|
94
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('shows the next notification immediately when the previous one has already been dismissed', () => {
|
|
98
|
+
const { rerender } = renderSnackbar();
|
|
99
|
+
|
|
100
|
+
rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
|
|
101
|
+
advanceTimers(TIMEOUT + CSS_TRANSITION_DURATION);
|
|
102
|
+
|
|
103
|
+
rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={2} />);
|
|
104
|
+
expect(screen.getByText('Second message')).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('restarts the timeout for a replacing notification', () => {
|
|
108
|
+
const { rerender } = renderSnackbar();
|
|
109
|
+
|
|
110
|
+
rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
|
|
111
|
+
advanceTimers(TIMEOUT - 100);
|
|
112
|
+
|
|
113
|
+
rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={2} />);
|
|
114
|
+
advanceTimers(CSS_TRANSITION_DURATION);
|
|
115
|
+
expect(screen.getByText('Second message')).toBeInTheDocument();
|
|
116
|
+
|
|
117
|
+
advanceTimers(TIMEOUT - 1);
|
|
118
|
+
expect(screen.getByText('Second message')).toBeInTheDocument();
|
|
119
|
+
|
|
120
|
+
advanceTimers(1);
|
|
121
|
+
advanceTimers(CSS_TRANSITION_DURATION);
|
|
122
|
+
expect(screen.queryByText('Second message')).not.toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Documents existing behaviour rather than endorsing it: clearing the text leaves the notification
|
|
126
|
+
// on screen indefinitely, because the previous shouldComponentUpdate blocked the dismissal too.
|
|
127
|
+
it('keeps the notification visible for as long as text is empty', () => {
|
|
128
|
+
const { rerender } = renderSnackbar();
|
|
129
|
+
|
|
130
|
+
rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
|
|
131
|
+
rerender(<Snackbar text="" timeout={TIMEOUT} timestamp={2} />);
|
|
132
|
+
|
|
133
|
+
advanceTimers(TIMEOUT + CSS_TRANSITION_DURATION);
|
|
134
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('ignores a repeated timestamp', () => {
|
|
138
|
+
const { rerender } = renderSnackbar();
|
|
139
|
+
|
|
140
|
+
rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
|
|
141
|
+
rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={1} />);
|
|
142
|
+
|
|
143
|
+
advanceTimers(CSS_TRANSITION_DURATION);
|
|
144
|
+
expect(screen.getByText('First message')).toBeInTheDocument();
|
|
145
|
+
expect(screen.queryByText('Second message')).not.toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('clears its pending dismissal timer on unmount', () => {
|
|
149
|
+
const { rerender, unmount } = renderSnackbar();
|
|
150
|
+
|
|
151
|
+
rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
|
|
152
|
+
const pendingTimers = jest.getTimerCount();
|
|
153
|
+
unmount();
|
|
154
|
+
|
|
155
|
+
// The timer left behind belongs to CSSTransition's enter timeout, not to Snackbar.
|
|
156
|
+
expect(jest.getTimerCount()).toBe(pendingTimers - 1);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Snackbar created through SnackbarProvider', () => {
|
|
161
|
+
const renderProvider = () =>
|
|
162
|
+
render(
|
|
163
|
+
<SnackbarProvider timeout={TIMEOUT}>
|
|
164
|
+
<SnackbarConsumer>
|
|
165
|
+
{({ createSnackbar }) => (
|
|
166
|
+
<Button onClick={() => createSnackbar({ text: 'Changes saved' })}>Show</Button>
|
|
167
|
+
)}
|
|
168
|
+
</SnackbarConsumer>
|
|
169
|
+
</SnackbarProvider>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
it('shows a notification created through the context', async () => {
|
|
173
|
+
renderProvider();
|
|
174
|
+
|
|
175
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
176
|
+
|
|
177
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('shows only one notification when the trigger is clicked repeatedly', async () => {
|
|
181
|
+
renderProvider();
|
|
182
|
+
|
|
183
|
+
const trigger = screen.getByRole('button', { name: 'Show' });
|
|
184
|
+
await user.click(trigger);
|
|
185
|
+
await user.click(trigger);
|
|
186
|
+
await user.click(trigger);
|
|
187
|
+
|
|
188
|
+
expect(screen.getAllByRole('alert')).toHaveLength(1);
|
|
189
|
+
});
|
|
190
|
+
});
|
package/src/i18n/cs.json
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"neptune.PhoneNumberInput.SelectInput.placeholder": "Vybrat možnost...",
|
|
36
36
|
"neptune.PhoneNumberInput.countryCodeLabel": "Kód země",
|
|
37
37
|
"neptune.PhoneNumberInput.phoneNumberLabel": "Telefonní číslo",
|
|
38
|
+
"neptune.PhoneNumberInput.phoneNumberPrefix": "Předčíslí",
|
|
38
39
|
"neptune.Select.searchPlaceholder": "Hledat...",
|
|
39
40
|
"neptune.SelectInput.noResultsFound": "Nebyly nalezeny žádné výsledky",
|
|
40
41
|
"neptune.StatusIcon.iconLabel.error": "Chyba:",
|
package/src/i18n/de.json
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"neptune.PhoneNumberInput.SelectInput.placeholder": "Wähle eine Option...",
|
|
36
36
|
"neptune.PhoneNumberInput.countryCodeLabel": "Ländercode",
|
|
37
37
|
"neptune.PhoneNumberInput.phoneNumberLabel": "Telefonnummer",
|
|
38
|
+
"neptune.PhoneNumberInput.phoneNumberPrefix": "Vorwahl",
|
|
38
39
|
"neptune.Select.searchPlaceholder": "Wird gesucht...",
|
|
39
40
|
"neptune.SelectInput.noResultsFound": "Keine Ergebnisse gefunden",
|
|
40
41
|
"neptune.StatusIcon.iconLabel.error": "Fehler:",
|
package/src/i18n/fr.json
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"neptune.PhoneNumberInput.SelectInput.placeholder": "Sélectionnez une option…",
|
|
36
36
|
"neptune.PhoneNumberInput.countryCodeLabel": "Code pays",
|
|
37
37
|
"neptune.PhoneNumberInput.phoneNumberLabel": "Numéro de téléphone",
|
|
38
|
+
"neptune.PhoneNumberInput.phoneNumberPrefix": "Indicatif",
|
|
38
39
|
"neptune.Select.searchPlaceholder": "Recherche...",
|
|
39
40
|
"neptune.SelectInput.noResultsFound": "Aucun résultat trouvé",
|
|
40
41
|
"neptune.StatusIcon.iconLabel.error": "Erreur :",
|
package/src/i18n/pt.json
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"neptune.PhoneNumberInput.SelectInput.placeholder": "Escolha uma opção...",
|
|
36
36
|
"neptune.PhoneNumberInput.countryCodeLabel": "Código do país",
|
|
37
37
|
"neptune.PhoneNumberInput.phoneNumberLabel": "Número de telefone",
|
|
38
|
+
"neptune.PhoneNumberInput.phoneNumberPrefix": "Prefixo",
|
|
38
39
|
"neptune.Select.searchPlaceholder": "Buscar...",
|
|
39
40
|
"neptune.SelectInput.noResultsFound": "Nenhum resultado encontrado",
|
|
40
41
|
"neptune.StatusIcon.iconLabel.error": "Erro:",
|
package/src/i18n/ru.json
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"neptune.PhoneNumberInput.SelectInput.placeholder": "Выберите вариант...",
|
|
36
36
|
"neptune.PhoneNumberInput.countryCodeLabel": "Код страны",
|
|
37
37
|
"neptune.PhoneNumberInput.phoneNumberLabel": "Номер телефона",
|
|
38
|
+
"neptune.PhoneNumberInput.phoneNumberPrefix": "Код страны",
|
|
38
39
|
"neptune.Select.searchPlaceholder": "Поиск...",
|
|
39
40
|
"neptune.SelectInput.noResultsFound": "Ничего не найдено",
|
|
40
41
|
"neptune.StatusIcon.iconLabel.error": "Ошибка:",
|
package/src/i18n/th.json
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"neptune.PhoneNumberInput.SelectInput.placeholder": "เลือกตัวเลือก...",
|
|
36
36
|
"neptune.PhoneNumberInput.countryCodeLabel": "รหัสประเทศ",
|
|
37
37
|
"neptune.PhoneNumberInput.phoneNumberLabel": "หมายเลขโทรศัพท์",
|
|
38
|
+
"neptune.PhoneNumberInput.phoneNumberPrefix": "คำนำหน้า",
|
|
38
39
|
"neptune.Select.searchPlaceholder": "ค้นหา...",
|
|
39
40
|
"neptune.SelectInput.noResultsFound": "ไม่พบผลลัพธ์",
|
|
40
41
|
"neptune.StatusIcon.iconLabel.error": "ข้อผิดพลาด:",
|
package/src/i18n/tr.json
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"neptune.PhoneNumberInput.SelectInput.placeholder": "Bir seçenek seçin...",
|
|
36
36
|
"neptune.PhoneNumberInput.countryCodeLabel": "Ülke kodu",
|
|
37
37
|
"neptune.PhoneNumberInput.phoneNumberLabel": "Telefon numarası",
|
|
38
|
+
"neptune.PhoneNumberInput.phoneNumberPrefix": "Ön ek",
|
|
38
39
|
"neptune.Select.searchPlaceholder": "Ara...",
|
|
39
40
|
"neptune.SelectInput.noResultsFound": "Sonuç bulunamadı",
|
|
40
41
|
"neptune.StatusIcon.iconLabel.error": "Hata:",
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export type { ContainerProps } from './Container';
|
|
7
7
|
export type { LogoProps, LogoSize, LogoAssetName } from './Logo';
|
|
8
|
-
export type { FlowNavigationProps } from './FlowNavigation';
|
|
8
|
+
export type { FlowNavigationProps, CriticalBannerLegacyData } from './FlowNavigation';
|
|
9
9
|
export type { AccordionItem, AccordionProps } from './Accordion';
|
|
10
10
|
export type { ActionOptionProps } from './_deprecated/ActionOption';
|
|
11
11
|
export type { AlertAction, AlertProps, AlertType } from './_deprecated/Alert';
|
package/src/main.css
CHANGED
|
@@ -23806,6 +23806,11 @@ button.np-option {
|
|
|
23806
23806
|
padding-inline: 0;
|
|
23807
23807
|
container-type: inline-size;
|
|
23808
23808
|
}
|
|
23809
|
+
.wds-list-item-paddingY-24 {
|
|
23810
|
+
padding-block: 24px;
|
|
23811
|
+
padding-block: var(--size-24);
|
|
23812
|
+
padding-inline: 0;
|
|
23813
|
+
}
|
|
23809
23814
|
.wds-list-item + .wds-list-item-spotlight,
|
|
23810
23815
|
.wds-list-item-spotlight + .wds-list-item {
|
|
23811
23816
|
margin-block-start: 16px;
|