@transferwise/components 46.116.1 → 46.117.1

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 (68) hide show
  1. package/build/main.css +145 -156
  2. package/build/prompt/InlinePrompt/InlinePrompt.js +14 -8
  3. package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
  4. package/build/prompt/InlinePrompt/InlinePrompt.mjs +15 -9
  5. package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
  6. package/build/sentimentSurface/SentimentSurface.js +6 -5
  7. package/build/sentimentSurface/SentimentSurface.js.map +1 -1
  8. package/build/sentimentSurface/SentimentSurface.mjs +6 -5
  9. package/build/sentimentSurface/SentimentSurface.mjs.map +1 -1
  10. package/build/styles/button/Button.css +22 -22
  11. package/build/styles/button/Button.vars.css +21 -21
  12. package/build/styles/iconButton/IconButton.css +8 -8
  13. package/build/styles/link/Link.css +1 -0
  14. package/build/styles/main.css +145 -156
  15. package/build/styles/prompt/InlinePrompt/InlinePrompt.css +26 -105
  16. package/build/styles/sentimentSurface/SentimentSurface.css +88 -21
  17. package/build/types/button/_stories/helpers.d.ts +11 -0
  18. package/build/types/button/_stories/helpers.d.ts.map +1 -0
  19. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +19 -3
  20. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
  21. package/build/types/sentimentSurface/SentimentSurface.d.ts +5 -4
  22. package/build/types/sentimentSurface/SentimentSurface.d.ts.map +1 -1
  23. package/build/types/sentimentSurface/SentimentSurface.types.d.ts +4 -16
  24. package/build/types/sentimentSurface/SentimentSurface.types.d.ts.map +1 -1
  25. package/build/types/test-utils/story-config.d.ts +25 -1
  26. package/build/types/test-utils/story-config.d.ts.map +1 -1
  27. package/package.json +13 -12
  28. package/src/button/Button.css +22 -22
  29. package/src/button/Button.less +1 -1
  30. package/src/button/Button.vars.css +21 -21
  31. package/src/button/Button.vars.less +63 -23
  32. package/src/button/{Button.accessibility.docs.mdx → _stories/Button.accessibility.docs.mdx} +1 -1
  33. package/src/button/_stories/Button.brightGreen.tests.story.tsx +31 -0
  34. package/src/button/_stories/Button.dark.tests.story.tsx +25 -0
  35. package/src/button/_stories/Button.default.tests.story.tsx +25 -0
  36. package/src/button/_stories/Button.forestGreen.tests.story.tsx +28 -0
  37. package/src/button/{Button.story.tsx → _stories/Button.story.tsx} +78 -113
  38. package/src/button/_stories/Button.tests.story.tsx +139 -0
  39. package/src/button/_stories/helpers.tsx +118 -0
  40. package/src/iconButton/IconButton.css +8 -8
  41. package/src/iconButton/IconButton.less +35 -4
  42. package/src/iconButton/IconButton.story.tsx +72 -3
  43. package/src/link/Link.css +1 -0
  44. package/src/link/Link.less +1 -0
  45. package/src/link/Link.story.tsx +28 -0
  46. package/src/main.css +145 -156
  47. package/src/prompt/InlinePrompt/InlinePrompt.css +26 -105
  48. package/src/prompt/InlinePrompt/InlinePrompt.less +31 -119
  49. package/src/prompt/InlinePrompt/InlinePrompt.spec.tsx +87 -29
  50. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +223 -31
  51. package/src/prompt/InlinePrompt/InlinePrompt.tsx +42 -11
  52. package/src/sentimentSurface/SentimentSurface.css +88 -21
  53. package/src/sentimentSurface/SentimentSurface.docs.mdx +32 -495
  54. package/src/sentimentSurface/SentimentSurface.less +151 -114
  55. package/src/sentimentSurface/SentimentSurface.spec.tsx +31 -11
  56. package/src/sentimentSurface/SentimentSurface.story.tsx +323 -108
  57. package/src/sentimentSurface/SentimentSurface.tests.story.tsx +90 -40
  58. package/src/sentimentSurface/SentimentSurface.tsx +16 -9
  59. package/src/sentimentSurface/SentimentSurface.types.ts +5 -20
  60. package/src/test-utils/story-config.ts +5 -1
  61. package/build/sentimentSurface/classMap.js +0 -17
  62. package/build/sentimentSurface/classMap.js.map +0 -1
  63. package/build/sentimentSurface/classMap.mjs +0 -14
  64. package/build/sentimentSurface/classMap.mjs.map +0 -1
  65. package/build/types/sentimentSurface/classMap.d.ts +0 -4
  66. package/build/types/sentimentSurface/classMap.d.ts.map +0 -1
  67. package/src/button/Button.tests.story.tsx +0 -27
  68. package/src/sentimentSurface/classMap.ts +0 -15
@@ -1,6 +1,3 @@
1
- /// @FIXME all tokens here need to be adjusted after
2
- /// the new sentiment tokens land
3
-
4
1
  .wds-inline-prompt {
5
2
  display: inline-flex;
6
3
  text-align: left;
@@ -16,18 +13,37 @@
16
13
  &:has(button) {
17
14
  position: relative;
18
15
  z-index: 1;
16
+
17
+ &:hover {
18
+ background-color: var(--color-sentiment-background-surface-hover);
19
+ }
20
+
21
+ &:active {
22
+ background-color: var(--color-sentiment-background-surface-active);
23
+ }
19
24
  }
20
25
 
21
26
  &--muted {
22
- opacity: .93;
27
+ opacity: 0.93;
23
28
  filter: grayscale(1);
24
29
  }
25
30
 
26
- a, button {
31
+ a,
32
+ button {
33
+ color: var(--color-sentiment-content-primary);
27
34
  text-underline-offset: calc(var(--size-4) / 2);
35
+
36
+ &:hover {
37
+ color: var(--color-sentiment-content-primary-hover);
38
+ }
39
+
40
+ &:active {
41
+ color: var(--color-sentiment-content-primary-active);
42
+ }
43
+
28
44
  &:first-of-type {
29
45
  &:before {
30
- content: '';
46
+ content: "";
31
47
  position: absolute;
32
48
  inset: 0;
33
49
  }
@@ -41,122 +57,18 @@
41
57
 
42
58
  .tw-icon-tags,
43
59
  .tw-icon-confetti {
44
- color: var(--color-sentiment-positive-primary);
45
- }
46
- }
47
-
48
- &--negative {
49
- background-color: var(--color-sentiment-negative-secondary);
50
- color: var(--color-sentiment-negative-primary);
51
-
52
- a, button {
53
- color: var(--color-sentiment-negative-primary);
54
-
55
- &:hover {
56
- color: var(--color-sentiment-negative-primary-hover);
57
- }
58
-
59
- &:active {
60
- color: var(--color-sentiment-negative-primary-active);
61
- }
62
- }
63
-
64
- .wds-inline-prompt&:has(a, button) {
65
- &:hover {
66
- background-color: var(--color-sentiment-negative-secondary-hover);
67
- }
68
- &:active {
69
- background-color: var(--color-sentiment-negative-secondary-active);
70
- }
71
- }
72
- }
73
-
74
- &--positive {
75
- background-color: var(--color-sentiment-positive-secondary);
76
- color: var(--color-sentiment-positive-primary);
77
- a, button {
78
- color: var(--color-sentiment-positive-primary);
79
- &:hover {
80
- color: var(--color-sentiment-positive-primary-hover);
81
- }
82
- &:active {
83
- color: var(--color-sentiment-positive-primary-active);
84
- }
85
- }
86
- .wds-inline-prompt&:has(a, button) {
87
- &:hover {
88
- background-color: var(--color-sentiment-positive-secondary-hover);
89
- }
90
- &:active {
91
- background-color: var(--color-sentiment-positive-secondary-active);
92
- }
93
- }
94
- }
95
-
96
- &--proposition {
97
- background-color: #D2F9F7;
98
-
99
- color: var(--color-interactive-primary);
100
-
101
- a, button {
102
- color: var(--color-interactive-primary);
103
- &:hover {
104
- color: var(--color-interactive-primary-hover);
105
- }
106
- &:active {
107
- color: var(--color-interactive-primary-active);
108
- }
109
- }
110
-
111
- .wds-inline-prompt&:has(a, button) {
112
- &:hover {
113
- background-color: #B2F4F3;
114
- }
115
- &:active {
116
- background-color: #91F0EE;
117
- }
118
- }
119
- }
120
-
121
- &--neutral {
122
- background-color: var(--color-background-neutral);
123
- color: var(--color-content-primary);
124
-
125
- a, button {
126
- color: var(--color-content-primary);
127
- }
128
-
129
- .wds-inline-prompt&:has(a, button) {
130
- &:hover {
131
- background-color: var(--color-background-neutral-hover);
132
- }
133
- &:active {
134
- background-color: var(--color-background-neutral-active);
135
- }
60
+ color: var(--color-sentiment-content-primary);
136
61
  }
137
62
  }
138
63
 
139
- &--warning {
140
- background-color: var(--color-sentiment-warning-secondary);
141
- color: var(--color-sentiment-warning-content);
64
+ .wds-inline-prompt-process-indicator {
65
+ width: var(--size-16);
66
+ height: var(--size-16);
142
67
 
143
- a, button {
144
- color: var(--color-sentiment-warning-content);
145
- &:hover {
146
- color: var(--color-sentiment-warning-content-hover);
147
- }
148
- &:active {
149
- color: var(--color-sentiment-warning-content-active);
150
- }
151
- }
152
-
153
- .wds-inline-prompt&:has(a, button) {
154
- &:hover {
155
- background-color: color-mix(in srgb, var(--color-sentiment-warning-secondary) 92%, var(--color-sentiment-warning-primary));
156
- }
157
- &:active {
158
- background-color: color-mix(in srgb, var(--color-sentiment-warning-secondary) 84%, var(--color-sentiment-warning-primary));
159
- }
160
- }
68
+ // This belongs in ProcessIndicator but there are many conflicting
69
+ // styles in the CSS package, so keeping it colocated for now.
70
+ .process-circle {
71
+ stroke: currentColor;
72
+ };
161
73
  }
162
74
  }
@@ -1,10 +1,11 @@
1
- import React from 'react';
2
1
  import { mockMatchMedia, render, screen } from '../../test-utils';
3
2
  import { InlinePrompt, InlinePromptProps } from './InlinePrompt';
4
3
  import { Sentiment } from '../../common';
5
4
 
6
5
  mockMatchMedia();
7
6
 
7
+ const MEDIA = <span data-testid="custom-media">Custom Media</span>;
8
+
8
9
  describe('InlinePrompt', () => {
9
10
  const defaultProps: InlinePromptProps = {
10
11
  children: 'Prompt message',
@@ -21,39 +22,75 @@ describe('InlinePrompt', () => {
21
22
  expect(screen.getByTestId('gift-box-icon')).toBeInTheDocument();
22
23
  });
23
24
 
24
- describe('renders with each sentiment', () => {
25
- it.each([Sentiment.POSITIVE, Sentiment.NEUTRAL, Sentiment.NEGATIVE, Sentiment.WARNING])(
26
- 'renders with sentiment %s',
27
- (sentiment) => {
28
- render(
29
- <InlinePrompt
30
- {...defaultProps}
31
- sentiment={sentiment as InlinePromptProps['sentiment']}
32
- />,
33
- );
25
+ [
26
+ { sentiment: Sentiment.NEGATIVE as const, acceptsMedia: false, statusIconLabel: 'Error:' },
27
+ { sentiment: Sentiment.WARNING as const, acceptsMedia: false, statusIconLabel: 'Warning:' },
28
+ { sentiment: Sentiment.NEUTRAL as const, acceptsMedia: false, statusIconLabel: 'Information:' },
29
+ { sentiment: Sentiment.POSITIVE as const, acceptsMedia: true, statusIconLabel: '' },
30
+ { sentiment: 'proposition' as const, acceptsMedia: true, statusIconLabel: '' },
31
+ ].forEach(({ sentiment, statusIconLabel, acceptsMedia }) => {
32
+ describe(sentiment, () => {
33
+ it('should apply correct styles', () => {
34
+ render(<InlinePrompt {...defaultProps} sentiment={sentiment} />);
34
35
  expect(screen.getByText('Prompt message').parentElement).toHaveClass(
35
36
  `wds-inline-prompt--${sentiment}`,
36
37
  );
37
- },
38
- );
39
- });
38
+ });
40
39
 
41
- it('renders muted state', () => {
42
- render(<InlinePrompt {...defaultProps} muted />);
43
- screen.debug(undefined, 99999999);
44
- expect(screen.getByText('Prompt message').parentElement).toHaveClass(
45
- 'wds-inline-prompt--muted',
46
- );
47
- expect(screen.getByTestId('InlinePrompt_Muted')).toBeInTheDocument();
48
- });
40
+ if (statusIconLabel) {
41
+ it('should render StatusIcon', () => {
42
+ render(<InlinePrompt {...defaultProps} sentiment={sentiment} />);
43
+ expect(screen.getByLabelText(statusIconLabel)).toBeInTheDocument();
44
+ });
45
+ }
49
46
 
50
- it('renders loading state', () => {
51
- render(<InlinePrompt {...defaultProps} loading />);
52
- screen.debug(undefined, 99999999);
53
- expect(screen.getByText('Prompt message').parentElement).toHaveClass(
54
- 'wds-inline-prompt--loading',
55
- );
56
- expect(screen.getByTestId('InlinePrompt_ProcessIndicator')).toBeInTheDocument();
47
+ describe('muted state', () => {
48
+ it('should render icon and apply css', () => {
49
+ render(<InlinePrompt {...defaultProps} muted sentiment={sentiment} />);
50
+ expect(screen.getByText('Prompt message').parentElement).toHaveClass(
51
+ 'wds-inline-prompt--muted',
52
+ );
53
+ expect(screen.getByTestId('InlinePrompt_Muted')).toBeInTheDocument();
54
+ });
55
+
56
+ it('should trump `media`', () => {
57
+ render(<InlinePrompt {...defaultProps} muted sentiment={sentiment} media={MEDIA} />);
58
+ expect(screen.getByTestId('InlinePrompt_Muted')).toBeInTheDocument();
59
+ expect(screen.queryByTestId('custom-media')).not.toBeInTheDocument();
60
+ });
61
+ });
62
+
63
+ describe('loading state', () => {
64
+ it('should render icon and apply css', () => {
65
+ render(<InlinePrompt {...defaultProps} sentiment={sentiment} loading />);
66
+ expect(screen.getByText('Prompt message').parentElement).toHaveClass(
67
+ 'wds-inline-prompt--loading',
68
+ );
69
+ expect(screen.getByTestId('InlinePrompt_ProcessIndicator')).toBeInTheDocument();
70
+ });
71
+
72
+ it('should trump `media`', () => {
73
+ render(<InlinePrompt {...defaultProps} sentiment={sentiment} loading media={MEDIA} />);
74
+ expect(screen.getByTestId('InlinePrompt_ProcessIndicator')).toBeInTheDocument();
75
+ expect(screen.queryByTestId('custom-media')).not.toBeInTheDocument();
76
+ });
77
+ });
78
+
79
+ describe('custom media', () => {
80
+ if (acceptsMedia) {
81
+ it('should respect `media`', () => {
82
+ render(<InlinePrompt {...defaultProps} sentiment={sentiment} media={MEDIA} />);
83
+ expect(screen.getByTestId('custom-media')).toBeInTheDocument();
84
+ });
85
+ } else {
86
+ it('should ignore `media`', () => {
87
+ render(<InlinePrompt {...defaultProps} sentiment={sentiment} media={MEDIA} />);
88
+ expect(screen.getByLabelText(statusIconLabel)).toBeInTheDocument();
89
+ expect(screen.queryByTestId('custom-media')).not.toBeInTheDocument();
90
+ });
91
+ }
92
+ });
93
+ });
57
94
  });
58
95
 
59
96
  it('applies custom className, id, and data-testid', () => {
@@ -69,4 +106,25 @@ describe('InlinePrompt', () => {
69
106
  expect(el).toHaveClass('custom-class');
70
107
  expect(el).toHaveAttribute('id', 'custom-id');
71
108
  });
109
+
110
+ describe('SentimentSurface integration', () => {
111
+ it('maps positive sentiment to success for SentimentSurface', () => {
112
+ render(
113
+ <InlinePrompt {...defaultProps} sentiment={Sentiment.POSITIVE} data-testid="prompt" />,
114
+ );
115
+ const el = screen.getByTestId('prompt');
116
+ // The component should have both the original class and the SentimentSurface class
117
+ expect(el).toHaveClass('wds-inline-prompt--positive');
118
+ expect(el).toHaveClass('wds-sentiment-surface');
119
+ });
120
+
121
+ it('passes through other sentiments unchanged', () => {
122
+ render(
123
+ <InlinePrompt {...defaultProps} sentiment={Sentiment.NEGATIVE} data-testid="prompt" />,
124
+ );
125
+ const el = screen.getByTestId('prompt');
126
+ expect(el).toHaveClass('wds-inline-prompt--negative');
127
+ expect(el).toHaveClass('wds-sentiment-surface');
128
+ });
129
+ });
72
130
  });
@@ -1,39 +1,231 @@
1
- import { lorem10, lorem5 } from '../../test-utils';
2
- import { InlinePrompt } from './InlinePrompt';
3
- import { CardWise, Rewards } from '@transferwise/icons';
1
+ import type { ReactElement } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
3
+ import { Taxi, Travel } from '@transferwise/icons';
4
+ import { lorem5 } from '../../test-utils';
5
+ import Link from '../../link';
6
+ import { InlinePrompt, InlinePromptProps } from './InlinePrompt';
4
7
 
5
- export default {
6
- title: 'Internal/InlinePrompt',
7
- component: InlinePrompt,
8
- };
9
-
10
- export const AllVariants = () => {
11
- return (
8
+ const withComponentGrid =
9
+ ({ maxWidth = 'auto', gap = '1rem' } = {}) =>
10
+ (Story: () => ReactElement) => (
12
11
  <div
13
12
  style={{
14
- display: 'grid',
15
- gridAutoFlow: 'row',
16
- rowGap: 'var(--size-12)',
17
- justifyItems: 'start',
18
- width: 'fit-content',
13
+ width: '100%',
14
+ display: 'flex',
15
+ flexDirection: 'column',
16
+ gap,
17
+ maxWidth,
19
18
  }}
20
19
  >
21
- <InlinePrompt sentiment="positive">{lorem5}</InlinePrompt>
22
- <InlinePrompt media={<Rewards />} sentiment="positive">
23
- {lorem10}
24
- </InlinePrompt>
25
- <InlinePrompt sentiment="negative">{lorem5}</InlinePrompt>
26
- <InlinePrompt sentiment="negative">{lorem10}</InlinePrompt>
27
- <InlinePrompt sentiment="neutral">{lorem5}</InlinePrompt>
28
- <InlinePrompt sentiment="neutral">{lorem10}</InlinePrompt>
29
- <InlinePrompt sentiment="warning">{lorem5}</InlinePrompt>
30
- <InlinePrompt sentiment="warning">{lorem10}</InlinePrompt>
31
- <InlinePrompt sentiment="proposition">{lorem5}</InlinePrompt>
32
- <InlinePrompt sentiment="proposition">{lorem10}</InlinePrompt>
33
- <InlinePrompt sentiment="proposition" media={<CardWise />}>
34
- {lorem10}
35
- </InlinePrompt>
36
- <InlinePrompt>{lorem5}</InlinePrompt>
20
+ <Story />
37
21
  </div>
38
22
  );
23
+
24
+ export default {
25
+ title: 'Internal/InlinePrompt',
26
+ component: InlinePrompt,
27
+ args: {
28
+ loading: false,
29
+ muted: false,
30
+ children: lorem5,
31
+ },
32
+ argTypes: {
33
+ sentiment: {
34
+ control: 'select',
35
+ options: ['positive', 'negative', 'neutral', 'warning', 'proposition'],
36
+ },
37
+ loading: {
38
+ control: 'boolean',
39
+ },
40
+ muted: {
41
+ control: 'boolean',
42
+ },
43
+ children: {
44
+ control: 'text',
45
+ table: {
46
+ type: { summary: 'ReactNode' },
47
+ },
48
+ },
49
+ },
50
+ parameters: {
51
+ docs: {
52
+ toc: true,
53
+ },
54
+ },
55
+ } satisfies Meta<InlinePromptProps>;
56
+
57
+ /**
58
+ * Convenience controls for previewing rich markup,
59
+ * not otherwise possible via Storybook
60
+ */
61
+ type PreviewStoryArgs = InlinePromptProps & {
62
+ previewMedia: InlinePromptProps['media'];
63
+ };
64
+
65
+ const previewArgGroup = {
66
+ category: 'Storybook Preview options',
67
+ type: {
68
+ summary: undefined,
69
+ },
70
+ };
71
+
72
+ const previewArgTypes = {
73
+ previewMedia: {
74
+ name: 'Preview with `media`',
75
+ control: 'boolean',
76
+ mapping: {
77
+ false: undefined,
78
+ true: <Taxi />,
79
+ },
80
+ table: previewArgGroup,
81
+ },
82
+ } as const;
83
+
84
+ const getPropsForPreview = (
85
+ args: PreviewStoryArgs,
86
+ ): [InlinePromptProps, Partial<InlinePromptProps>] => {
87
+ const { previewMedia, ...props } = args;
88
+
89
+ return [
90
+ props,
91
+ {
92
+ media: previewMedia,
93
+ },
94
+ ];
95
+ };
96
+
97
+ export const Playground: StoryObj<PreviewStoryArgs> = {
98
+ tags: ['!autodocs'],
99
+ argTypes: previewArgTypes,
100
+ args: {
101
+ previewMedia: false,
102
+ },
103
+ render: (args: PreviewStoryArgs) => {
104
+ const [props, previewProps] = getPropsForPreview(args);
105
+ return <InlinePrompt {...props} {...previewProps} />;
106
+ },
107
+ };
108
+
109
+ /**
110
+ * Aside from the known `neutral`, `negative`, `warning` and `positive` sentiments, `InlinePrompt` is the first of the prompts to introduce a new `proposition` sentiment, which can be used to encourage the user to take an action that might benefit them.
111
+ *
112
+ * By default, its associated icon for the prompt is `GiftBox`.
113
+ */
114
+ export const Sentiments: StoryObj<PreviewStoryArgs> = {
115
+ argTypes: previewArgTypes,
116
+ args: {
117
+ previewMedia: false,
118
+ },
119
+ render: (args: PreviewStoryArgs) => {
120
+ const [props, previewProps] = getPropsForPreview(args);
121
+ return (
122
+ <>
123
+ <InlinePrompt {...props} {...previewProps} sentiment="neutral">
124
+ This is a neutral prompt.
125
+ </InlinePrompt>
126
+ <InlinePrompt {...props} {...previewProps} sentiment="negative">
127
+ This is a negative prompt.
128
+ </InlinePrompt>
129
+ <InlinePrompt {...props} {...previewProps} sentiment="warning">
130
+ This is a warning prompt.
131
+ </InlinePrompt>
132
+ <InlinePrompt {...props} {...previewProps} sentiment="positive">
133
+ This is a positive prompt.
134
+ </InlinePrompt>
135
+ <InlinePrompt {...props} {...previewProps} sentiment="proposition">
136
+ This is a proposition prompt.
137
+ </InlinePrompt>
138
+ </>
139
+ );
140
+ },
141
+ decorators: [withComponentGrid()],
142
+ };
143
+
144
+ /**
145
+ * Inline prompts are also used when forms need to check something in the background, like
146
+ * verifying an account number is real. Turning on the loading state will replace the icon with
147
+ * a spinner while waiting for the check to finish.
148
+ */
149
+ export const Loading: StoryObj<PreviewStoryArgs> = {
150
+ argTypes: previewArgTypes,
151
+ args: {
152
+ loading: true,
153
+ previewMedia: false,
154
+ },
155
+ render: (args: PreviewStoryArgs) => {
156
+ const [props, previewProps] = getPropsForPreview(args);
157
+ return <InlinePrompt {...props} {...previewProps} />;
158
+ },
159
+ };
160
+
161
+ /**
162
+ * Inline prompt is usually associated with a different component, such as `Input` or `ListItem`.
163
+ * When those components are disabled, the prompt is often used to communicate to the user why they
164
+ * cannot interact with them. For that reason, the prompt cannot inherit the usual disabled visual
165
+ * style, and it must retain its interactivity. To bring these 2 components visually closer to each
166
+ * other, `muted` prop introduces slightly reduced opacity and luminosity as well as a special
167
+ * backslash circle icon.
168
+ */
169
+ export const Muted: StoryObj<PreviewStoryArgs> = {
170
+ argTypes: previewArgTypes,
171
+ args: {
172
+ muted: true,
173
+ previewMedia: false,
174
+ },
175
+ render: (args: PreviewStoryArgs) => {
176
+ const [props, previewProps] = getPropsForPreview(args);
177
+ return <InlinePrompt {...props} {...previewProps} />;
178
+ },
179
+ };
180
+
181
+ /**
182
+ * While main sentiments (`warning`, `negative`, `neutral`) must retain their associated
183
+ * `StatusIcons`, the `positive` and `proposition` ones allow for Icon overrides to bring it
184
+ * closer to the prompt's content.
185
+ */
186
+ export const IconOverrides: StoryObj<PreviewStoryArgs> = {
187
+ render: (args: PreviewStoryArgs) => {
188
+ return (
189
+ <>
190
+ <InlinePrompt {...args} media={<Travel />} sentiment="positive">
191
+ Your travel account is set up and ready to use.
192
+ </InlinePrompt>
193
+ <InlinePrompt {...args} media={<Taxi />} sentiment="proposition">
194
+ Connect Wise with your taxi app to get exclusive discounts.
195
+ </InlinePrompt>
196
+ <InlinePrompt {...args} media={<Taxi />} sentiment="negative">
197
+ This icon override is ignored.
198
+ </InlinePrompt>
199
+ </>
200
+ );
201
+ },
202
+ decorators: [withComponentGrid()],
203
+ };
204
+
205
+ /**
206
+ * When configured with any of the supported sentiments, the colour scheme of the component will propagate to all of its supported descendants, such as instances of a `Link`, `Icon`, and `StatusIcon`.
207
+ */
208
+ export const SentimentAwareness: StoryObj<PreviewStoryArgs> = {
209
+ argTypes: previewArgTypes,
210
+ args: {
211
+ previewMedia: false,
212
+ },
213
+ render: (args: PreviewStoryArgs) => {
214
+ const [props, previewProps] = getPropsForPreview(args);
215
+
216
+ return (
217
+ <>
218
+ <InlinePrompt {...props} {...previewProps} sentiment="negative">
219
+ This is a negative prompt with an <Link href="#">inline link</Link>.
220
+ </InlinePrompt>
221
+ <InlinePrompt {...props} {...previewProps} sentiment="positive">
222
+ This is a positive prompt with an <Link href="#">inline link</Link>.
223
+ </InlinePrompt>
224
+ <InlinePrompt {...props} {...previewProps} sentiment="proposition">
225
+ This is a proposition prompt with an <Link href="#">inline link</Link>.
226
+ </InlinePrompt>
227
+ </>
228
+ );
229
+ },
230
+ decorators: [withComponentGrid()],
39
231
  };
@@ -4,23 +4,40 @@ import ProcessIndicator from '../../processIndicator';
4
4
  import StatusIcon from '../../statusIcon';
5
5
  import { clsx } from 'clsx';
6
6
  import Body from '../../body';
7
+ import SentimentSurface from '../../sentimentSurface';
7
8
 
8
9
  export type InlinePromptProps = {
10
+ /**
11
+ * The sentiment determines the colour scheme
12
+ */
9
13
  sentiment?:
10
14
  | `${Sentiment.POSITIVE | Sentiment.NEGATIVE | Sentiment.NEUTRAL | Sentiment.WARNING}`
11
15
  | 'proposition';
16
+ /**
17
+ * Replaces the icon with a spinner while waiting for the short-lived action to finish.
18
+ * @default false
19
+ */
12
20
  loading?: boolean;
13
21
  /**
14
- * Use for short-lived inline prompts to avoid swap of the icon (which is bad UX for short-lived prompts, e.g. when submit form)
22
+ * While prompts cannot be fully (visually and functionally) disabled, this prop should be enabled
23
+ * they are associated with actually disabled component (e.g. a disabled list item or input).
24
+ * @default false
15
25
  */
16
26
  muted?: boolean;
27
+ /**
28
+ * Icon override for `proposition` and `positive` sentiments. Unsupported for remaining ones.
29
+ */
30
+ media?: React.ReactNode;
17
31
  id?: string;
18
32
  className?: string;
19
33
  'data-testid'?: string;
20
34
  children: React.ReactNode;
21
- media?: React.ReactNode;
22
35
  };
23
36
 
37
+ /**
38
+ * Inline prompts appear alongside a specific component on the screen. They help the user stay
39
+ * informed, fix something, or get more out of what they're doing.
40
+ */
24
41
  export const InlinePrompt = ({
25
42
  sentiment = Sentiment.POSITIVE,
26
43
  muted = false,
@@ -28,26 +45,40 @@ export const InlinePrompt = ({
28
45
  className,
29
46
  children,
30
47
  media = null,
48
+ 'data-testid': dataTestId,
31
49
  ...rest
32
50
  }: InlinePromptProps) => {
51
+ const surfaceSentiment = sentiment === Sentiment.POSITIVE ? 'success' : sentiment;
52
+
33
53
  const renderMedia = () => {
34
- if (media && ['proposition', 'positive'].includes(sentiment)) {
35
- return media;
36
- }
37
- if (sentiment === 'proposition') {
38
- return <GiftBox />;
39
- }
40
54
  if (muted) {
41
55
  return <BackslashCircle size={16} data-testid="InlinePrompt_Muted" />;
42
56
  }
43
57
  if (loading) {
44
- return <ProcessIndicator data-testid="InlinePrompt_ProcessIndicator" size="xxs" />;
58
+ return (
59
+ <ProcessIndicator
60
+ data-testid="InlinePrompt_ProcessIndicator"
61
+ size="xxs"
62
+ className="wds-inline-prompt-process-indicator"
63
+ />
64
+ );
65
+ }
66
+
67
+ if (sentiment === 'positive' && media) {
68
+ return media;
45
69
  }
70
+
71
+ if (sentiment === 'proposition') {
72
+ return media || <GiftBox />;
73
+ }
74
+
46
75
  return <StatusIcon size={16} sentiment={sentiment} />;
47
76
  };
48
77
 
49
78
  return (
50
- <div
79
+ <SentimentSurface
80
+ sentiment={surfaceSentiment}
81
+ data-testid={dataTestId}
51
82
  className={clsx(
52
83
  'wds-inline-prompt',
53
84
  `wds-inline-prompt--${sentiment}`,
@@ -61,6 +92,6 @@ export const InlinePrompt = ({
61
92
  >
62
93
  <div className="wds-inline-prompt__media-wrapper">{renderMedia()}</div>
63
94
  <Body>{children}</Body>
64
- </div>
95
+ </SentimentSurface>
65
96
  );
66
97
  };