@transferwise/components 0.0.0-experimental-820ab92 → 0.0.0-experimental-6bb771e

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.
@@ -11,7 +11,7 @@ export type BodyProps = HTMLAttributes<HTMLSpanElement | HTMLParagraphElement> &
11
11
  */
12
12
  preserveNewlines?: boolean;
13
13
  };
14
- declare const Body: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLSpanElement | HTMLParagraphElement> & {
14
+ declare const Body: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLParagraphElement | HTMLSpanElement> & {
15
15
  /** @default 'body-default' */
16
16
  type?: BodyTypes;
17
17
  /** @default 'div' */
@@ -21,6 +21,6 @@ declare const Body: import("react").ForwardRefExoticComponent<HTMLAttributes<HTM
21
21
  * @default false
22
22
  */
23
23
  preserveNewlines?: boolean;
24
- } & import("react").RefAttributes<HTMLDivElement | HTMLSpanElement | HTMLParagraphElement | null>>;
24
+ } & import("react").RefAttributes<HTMLDivElement | HTMLParagraphElement | HTMLSpanElement | null>>;
25
25
  export default Body;
26
26
  //# sourceMappingURL=Body.d.ts.map
@@ -16,6 +16,6 @@ declare const IconButton: import("react").ForwardRefExoticComponent<{
16
16
  /** @default 'default' */
17
17
  type?: "default" | "negative";
18
18
  'data-testid'?: string;
19
- } & Pick<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick" | "target" | "href"> & Pick<ButtonHTMLAttributes<HTMLButtonElement>, "disabled" | "onClick"> & Pick<HTMLAttributes<HTMLDivElement>, "children" | "className" | "id" | "tabIndex" | "role" | "aria-label"> & import("react").RefAttributes<unknown>>;
19
+ } & Pick<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick" | "target" | "href"> & Pick<ButtonHTMLAttributes<HTMLButtonElement>, "disabled" | "onClick"> & Pick<HTMLAttributes<HTMLDivElement>, "id" | "className" | "tabIndex" | "role" | "aria-label" | "children"> & import("react").RefAttributes<unknown>>;
20
20
  export default IconButton;
21
21
  //# sourceMappingURL=IconButton.d.ts.map
@@ -55,6 +55,7 @@ declare const _default: import("react").FC<{
55
55
  selectProps?: Partial<SelectInputProps<CurrencyOptionItem>> | undefined;
56
56
  amount: number | null;
57
57
  searchPlaceholder?: string | undefined;
58
+ decimals?: number | undefined;
58
59
  currencies: readonly CurrencyItem[];
59
60
  selectedCurrency: CurrencyOptionItem;
60
61
  onCurrencyChange?: ((value: CurrencyOptionItem) => void) | undefined;
@@ -66,7 +67,6 @@ declare const _default: import("react").FC<{
66
67
  }) => void) | undefined;
67
68
  customActionLabel?: import("react").ReactNode;
68
69
  onCustomAction?: (() => void) | undefined;
69
- decimals?: number | undefined;
70
70
  }>;
71
71
  export default _default;
72
72
  //# sourceMappingURL=MoneyInput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MoneyInput.d.ts","sourceRoot":"","sources":["../../../src/MoneyInput/MoneyInput.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAGL,SAAS,EACT,UAAU,EACV,SAAS,EAEV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAuB,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAEnF,OAAO,EAKL,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAoDnE,MAAM,WAAW,eAAgB,SAAQ,qBAAqB;IAC5D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,SAAS,YAAY,EAAE,CAAC;IACpC,gBAAgB,EAAE,kBAAkB,CAAC;IACrC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;IAC1C,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,YAAY,EAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3F,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC5D;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kCAAkC,GAAG,eAAe,GAC9D,OAAO,CAAC,wBAAwB,CAAC,CAAC;;SA5B7B,MAAM;wBACS,MAAM;YAMnB,SAAS,GAAG,UAAU,GAAG,SAAS;kBAF3B,MAAM;iBAYP,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;kBACrB,OAAO,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;YAZnD,MAAM,GAAG,IAAI;wBAID,MAAM;gBARd,SAAS,YAAY,EAAE;sBACjB,kBAAkB;wBACjB,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI;sBAIrC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI;;sBAM9B,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,YAAY,EAAE,CAAA;KAAE,KAAK,IAAI;;sBAEzE,MAAM,IAAI;eAQhB,MAAM;;AA0anB,wBAA2F"}
1
+ {"version":3,"file":"MoneyInput.d.ts","sourceRoot":"","sources":["../../../src/MoneyInput/MoneyInput.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAGL,SAAS,EACT,UAAU,EACV,SAAS,EAEV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAuB,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAEnF,OAAO,EAKL,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAoDnE,MAAM,WAAW,eAAgB,SAAQ,qBAAqB;IAC5D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,SAAS,YAAY,EAAE,CAAC;IACpC,gBAAgB,EAAE,kBAAkB,CAAC;IACrC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;IAC1C,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,YAAY,EAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3F,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC5D;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kCAAkC,GAAG,eAAe,GAC9D,OAAO,CAAC,wBAAwB,CAAC,CAAC;;SA5B7B,MAAM;wBACS,MAAM;YAMnB,SAAS,GAAG,UAAU,GAAG,SAAS;kBAF3B,MAAM;iBAYP,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;kBACrB,OAAO,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;YAZnD,MAAM,GAAG,IAAI;wBAID,MAAM;eAcf,MAAM;gBAtBL,SAAS,YAAY,EAAE;sBACjB,kBAAkB;wBACjB,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI;sBAIrC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI;;sBAM9B,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,YAAY,EAAE,CAAA;KAAE,KAAK,IAAI;;sBAEzE,MAAM,IAAI;;AAkb7B,wBAA2F"}
@@ -10,7 +10,7 @@ export type TitleProps = LabelHTMLAttributes<HTMLHeadingElement | HTMLSpanElemen
10
10
  */
11
11
  type?: TitleTypes;
12
12
  };
13
- declare const Title: React.ForwardRefExoticComponent<React.LabelHTMLAttributes<HTMLSpanElement | HTMLHeadingElement | HTMLLabelElement> & React.LiHTMLAttributes<HTMLLIElement> & {
13
+ declare const Title: React.ForwardRefExoticComponent<React.LabelHTMLAttributes<HTMLHeadingElement | HTMLLabelElement | HTMLSpanElement> & React.LiHTMLAttributes<HTMLLIElement> & {
14
14
  /**
15
15
  * Default value will based one `type` prop
16
16
  */
@@ -19,6 +19,6 @@ declare const Title: React.ForwardRefExoticComponent<React.LabelHTMLAttributes<H
19
19
  * Default value: {@link DEFAULT_TYPE}
20
20
  */
21
21
  type?: TitleTypes;
22
- } & React.RefAttributes<HTMLSpanElement | HTMLHeadingElement | HTMLLabelElement | HTMLLegendElement | HTMLLIElement>>;
22
+ } & React.RefAttributes<HTMLHeadingElement | HTMLLabelElement | HTMLLegendElement | HTMLLIElement | HTMLSpanElement>>;
23
23
  export default Title;
24
24
  //# sourceMappingURL=Title.d.ts.map
@@ -15,7 +15,7 @@ export default class UploadImageStep extends PureComponent<UploadImageStepProps>
15
15
  uploadInputRef: import("react").RefObject<HTMLInputElement>;
16
16
  onManualUpload: () => void;
17
17
  onKeyDown: (event: React.KeyboardEvent<HTMLLabelElement>) => void;
18
- getImage: () => number | true | import("react").JSX.Element | Iterable<import("react").ReactNode>;
18
+ getImage: () => number | true | Iterable<import("react").ReactNode> | import("react").JSX.Element;
19
19
  render(): import("react").JSX.Element;
20
20
  }
21
21
  //# sourceMappingURL=UploadImageStep.d.ts.map
@@ -5,5 +5,5 @@ export declare const UploadItemLink: import("react").ForwardRefExoticComponent<{
5
5
  singleFileUpload: boolean;
6
6
  } & {
7
7
  children?: import("react").ReactNode | undefined;
8
- } & import("react").RefAttributes<HTMLDivElement | HTMLAnchorElement>>;
8
+ } & import("react").RefAttributes<HTMLAnchorElement | HTMLDivElement>>;
9
9
  //# sourceMappingURL=UploadItemLink.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "0.0.0-experimental-820ab92",
3
+ "version": "0.0.0-experimental-6bb771e",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,73 @@
1
+ import { Meta, Source } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Prompts/Snackbar/Accessibility" />
4
+
5
+ # Accessibility
6
+
7
+ Snackbar is a polite live region. It uses `role="status"` and `aria-live="polite"`, so its message
8
+ is announced when the screen reader is idle without interrupting the current task. It does not move
9
+ keyboard focus when it appears.
10
+
11
+ Visual sentiment and announcement urgency are separate. A negative Snackbar remains polite; use a
12
+ different component when an update is urgent enough to interrupt the user.
13
+
14
+ <Source
15
+ dark
16
+ code={`showSnackbar({
17
+ text: 'Transfer failed',
18
+ sentiment: 'negative',
19
+ });`}
20
+ />
21
+
22
+ ## Actions and dismissal
23
+
24
+ The optional action uses its visible label as its accessible name. Keep the label concise and make
25
+ the outcome clear. Timed Snackbars pause while the pointer is hovering over them or while keyboard
26
+ focus is inside.
27
+
28
+ The polite announcement tells screen-reader users that pressing `Escape` dismisses the Snackbar
29
+ without moving focus. On desktop, the close button also has an accessible name and dismisses through
30
+ the normal exit motion. On mobile, the Snackbar can be dismissed with a downward swipe. A swipe
31
+ below 50% of its rendered height, an upward or horizontal gesture, or a cancelled gesture returns it
32
+ to its resting position.
33
+
34
+ <Source
35
+ dark
36
+ code={`showSnackbar({
37
+ text: 'Transfer deleted',
38
+ action: {
39
+ label: 'Undo',
40
+ onClick: handleUndo,
41
+ },
42
+ duration: 'extended',
43
+ onDismiss: handleDismiss,
44
+ });`}
45
+ />
46
+
47
+ ## Media
48
+
49
+ Default sentiment media already includes the appropriate accessible treatment. If custom media
50
+ conveys meaning, add its accessibility attributes directly to the element you pass. Decorative
51
+ custom media should be hidden from assistive technology.
52
+
53
+ <Source
54
+ dark
55
+ code={`showSnackbar({
56
+ text: 'Added to your wallet',
57
+ media: <Card title="Card" />,
58
+ });`}
59
+ />
60
+
61
+ <Source
62
+ dark
63
+ code={`showSnackbar({
64
+ text: 'Offer applied',
65
+ media: <GiftBox aria-hidden />,
66
+ });`}
67
+ />
68
+
69
+ ## Motion and zoom
70
+
71
+ Snackbar removes movement and scaling when `prefers-reduced-motion: reduce` is enabled. Its content
72
+ reflows at high zoom and narrow viewport widths without horizontal scrolling. Keep messages short
73
+ and plain so they remain easy to read when text wraps.
@@ -1,89 +1,473 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
- import { fn, userEvent, within } from 'storybook/test';
2
+ import { fn } from 'storybook/test';
3
+ import { Card } from '@transferwise/icons';
3
4
 
4
- import { wait } from '../../test-utils/wait';
5
5
  import {
6
6
  createSandboxStory,
7
7
  globalScope,
8
8
  } from '../../../.storybook/components/sandbox/SandboxEditor';
9
9
  import Button from '../../Button';
10
- import { Snackbar } from '../Snackbar';
10
+ import { Snackbar, type SnackbarProps } from '../Snackbar';
11
11
  import { SnackbarConsumer } from '../SnackbarContext';
12
12
  import SnackbarProvider from '../SnackbarProvider';
13
13
 
14
- const meta: Meta = {
15
- component: Snackbar,
14
+ const SENTIMENTS = ['neutral', 'negative', 'warning', 'success', 'proposition'] as const;
15
+ const DURATIONS = ['default', 'extended', 'indefinite'] as const;
16
+
17
+ const meta = {
16
18
  title: 'Prompts/Snackbar',
17
- };
19
+ component: Snackbar,
20
+ parameters: {
21
+ docs: {
22
+ toc: true,
23
+ description: {
24
+ component:
25
+ 'Snackbar is a brief, temporary notification that confirms the outcome of an action or process that has just happened. Keep its message short and plain: it supports one optional action, but no inline links or rich-text formatting. `Snackbar` can be rendered directly; use `SnackbarProvider` with `useSnackbar()` for app-level transient feedback.',
26
+ },
27
+ },
28
+ },
29
+ args: {
30
+ duration: 'default',
31
+ loading: false,
32
+ sentiment: 'neutral',
33
+ text: 'Changes saved',
34
+ },
35
+ argTypes: {
36
+ text: {
37
+ control: 'text',
38
+ description: 'Short, plain-text Snackbar message.',
39
+ table: {
40
+ type: { summary: 'ReactNode' },
41
+ },
42
+ },
43
+ sentiment: {
44
+ control: 'radio',
45
+ options: SENTIMENTS,
46
+ description: 'Controls the colour scheme and sentiment-derived default media.',
47
+ table: {
48
+ defaultValue: { summary: 'neutral' },
49
+ type: { summary: 'Sentiment' },
50
+ },
51
+ },
52
+ media: {
53
+ control: false,
54
+ description:
55
+ 'Overrides the sentiment-derived media. Custom media is displayed in the 24×24px media slot and should provide its own accessibility attributes when meaningful.',
56
+ table: {
57
+ type: { summary: 'ReactNode' },
58
+ },
59
+ },
60
+ loading: {
61
+ control: 'boolean',
62
+ description:
63
+ 'Replaces the media with a ProcessIndicator, uses neutral styling, and prevents automatic dismissal.',
64
+ table: {
65
+ defaultValue: { summary: 'false' },
66
+ type: { summary: 'boolean' },
67
+ },
68
+ },
69
+ action: {
70
+ control: false,
71
+ description:
72
+ 'One optional small Button. Supports a callback action or navigation through href and target.',
73
+ table: {
74
+ type: {
75
+ summary: 'SnackbarAction',
76
+ detail:
77
+ "Pick<ButtonProps, 'onClick' | 'href' | 'target'> & { label: ButtonProps['children'] }",
78
+ },
79
+ },
80
+ },
81
+ duration: {
82
+ control: 'inline-radio',
83
+ options: DURATIONS,
84
+ description: 'Controls automatic dismissal using the design-defined duration presets.',
85
+ table: {
86
+ defaultValue: { summary: 'default' },
87
+ type: { summary: "'default' | 'extended' | 'indefinite'" },
88
+ },
89
+ },
90
+ onDismiss: {
91
+ control: false,
92
+ description: 'Called after the Snackbar completes its exit transition.',
93
+ table: {
94
+ type: { summary: '() => void' },
95
+ },
96
+ },
97
+ timeout: {
98
+ table: { disable: true },
99
+ },
100
+ theme: {
101
+ table: { disable: true },
102
+ },
103
+ },
104
+ } satisfies Meta<SnackbarProps>;
105
+
18
106
  export default meta;
107
+ type Story = StoryObj<typeof meta>;
108
+
109
+ type PreviewStoryArgs = SnackbarProps & {
110
+ previewAction: boolean;
111
+ previewMedia: 'default' | 'custom';
112
+ previewOnDismiss: boolean;
113
+ };
114
+
115
+ type SnackbarExample = {
116
+ label: string;
117
+ props: SnackbarProps;
118
+ };
119
+
120
+ const previewArgGroup = {
121
+ category: 'Storybook Preview options',
122
+ type: { summary: undefined },
123
+ };
124
+
125
+ const previewArgTypes = {
126
+ previewAction: {
127
+ name: 'Preview with `action`',
128
+ control: 'boolean',
129
+ table: previewArgGroup,
130
+ },
131
+ previewMedia: {
132
+ name: 'Preview with `media`',
133
+ control: 'radio',
134
+ options: ['default', 'custom'],
135
+ table: previewArgGroup,
136
+ },
137
+ previewOnDismiss: {
138
+ name: 'Preview with `onDismiss`',
139
+ control: 'boolean',
140
+ table: previewArgGroup,
141
+ },
142
+ } as const;
143
+
144
+ const SnackbarExamples = ({ examples }: { examples: SnackbarExample[] }) => (
145
+ <SnackbarProvider>
146
+ <SnackbarConsumer>
147
+ {({ createSnackbar }) => (
148
+ <div
149
+ style={{
150
+ display: 'flex',
151
+ flexWrap: 'wrap',
152
+ gap: 'var(--size-8)',
153
+ }}
154
+ >
155
+ {examples.map(({ label, props }) => (
156
+ <Button key={label} v2 size="md" onClick={() => createSnackbar(props)}>
157
+ {label}
158
+ </Button>
159
+ ))}
160
+ </div>
161
+ )}
162
+ </SnackbarConsumer>
163
+ </SnackbarProvider>
164
+ );
165
+
166
+ /**
167
+ * Use the controls to preview the public Snackbar properties. Rich values such as `action`,
168
+ * `media`, and `onDismiss` use dedicated preview controls because Storybook cannot edit React
169
+ * elements or callback functions directly.
170
+ */
171
+ export const Playground: StoryObj<PreviewStoryArgs> = {
172
+ args: {
173
+ duration: 'default',
174
+ previewAction: true,
175
+ previewMedia: 'default',
176
+ previewOnDismiss: true,
177
+ loading: false,
178
+ },
179
+ argTypes: previewArgTypes,
180
+ render: ({ previewAction, previewMedia, previewOnDismiss, ...props }) => (
181
+ <SnackbarExamples
182
+ examples={[
183
+ {
184
+ label: 'Show Snackbar',
185
+ props: {
186
+ ...props,
187
+ action: previewAction ? { label: 'View', onClick: fn() } : undefined,
188
+ media: previewMedia === 'custom' ? <Card title="Card" /> : undefined,
189
+ onDismiss: previewOnDismiss ? fn() : undefined,
190
+ },
191
+ },
192
+ ]}
193
+ />
194
+ ),
195
+ };
19
196
 
20
197
  export const Sandbox = createSandboxStory({
21
- code: `<SnackbarProvider>
22
- <SnackbarConsumer>
23
- {({ createSnackbar }) => (
24
- <Button v2 size="md"
25
- onClick={() =>
26
- createSnackbar({
27
- text: <span>Transfer completed successfully</span>,
28
- action: {
29
- label: 'View',
30
- onClick: () => console.log('View clicked'),
31
- },
32
- })
33
- }
34
- >
35
- Show Snackbar
36
- </Button>
37
- )}
38
- </SnackbarConsumer>
39
- </SnackbarProvider>`,
198
+ code: `const SnackbarTrigger = () => {
199
+ const showSnackbar = useSnackbar();
200
+
201
+ return (
202
+ <Button
203
+ v2
204
+ size="md"
205
+ onClick={() =>
206
+ showSnackbar({
207
+ text: 'Transfer completed successfully',
208
+ sentiment: 'success',
209
+ action: {
210
+ label: 'View',
211
+ onClick: () => console.log('View clicked'),
212
+ },
213
+ duration: 'extended',
214
+ })
215
+ }
216
+ >
217
+ Show Snackbar
218
+ </Button>
219
+ );
220
+ };
221
+
222
+ const App = () => (
223
+ <SnackbarProvider>
224
+ <SnackbarTrigger />
225
+ </SnackbarProvider>
226
+ );`,
40
227
  scope: globalScope,
41
228
  });
42
229
 
43
- export const Basic: StoryObj = {
230
+ /**
231
+ * `Snackbar` remains available for direct rendering. For app-level feedback, wrap the relevant
232
+ * app boundary in `SnackbarProvider`, then call the function returned by `useSnackbar()` after an
233
+ * action or process completes. The Provider owns mounting, timers, replacement, and
234
+ * exit-before-enter behaviour.
235
+ *
236
+ * Keep the message short and plain. Snackbar copy does not support inline links or bold text.
237
+ */
238
+ export const Basic: Story = {
239
+ args: {
240
+ duration: 'indefinite',
241
+ sentiment: 'success',
242
+ text: 'Changes saved',
243
+ },
244
+ parameters: {
245
+ docs: {
246
+ source: {
247
+ code: `// Direct rendering
248
+ <Snackbar text="Changes saved" sentiment="success" />;
249
+
250
+ // Programmatic app-level feedback
251
+ const showSnackbar = useSnackbar();
252
+
253
+ showSnackbar({
254
+ text: 'Changes saved',
255
+ sentiment: 'success',
256
+ });`,
257
+ },
258
+ },
259
+ },
260
+ render: (args) => <SnackbarExamples examples={[{ label: 'Show Snackbar', props: args }]} />,
261
+ };
262
+
263
+ /**
264
+ * Use `sentiment` to match the meaning of the feedback. Sentiment controls appearance and default
265
+ * media; it does not change the polite screen-reader announcement.
266
+ *
267
+ * - `neutral`: general confirmation or information
268
+ * - `negative`: failed or unsuccessful outcome
269
+ * - `warning`: outcome that needs attention
270
+ * - `success`: successful completion
271
+ * - `proposition`: a relevant product proposition or benefit
272
+ */
273
+ export const Sentiments: Story = {
274
+ parameters: {
275
+ docs: {
276
+ source: {
277
+ code: `showSnackbar({ text: 'Details updated', sentiment: 'neutral' });
278
+ showSnackbar({ text: 'Transfer failed', sentiment: 'negative' });
279
+ showSnackbar({ text: 'Check the recipient details', sentiment: 'warning' });
280
+ showSnackbar({ text: 'Transfer completed', sentiment: 'success' });
281
+ showSnackbar({ text: 'You can earn a referral reward', sentiment: 'proposition' });`,
282
+ },
283
+ },
284
+ },
285
+ render: () => (
286
+ <SnackbarExamples
287
+ examples={[
288
+ { label: 'Neutral', props: { text: 'Details updated', sentiment: 'neutral' } },
289
+ { label: 'Negative', props: { text: 'Transfer failed', sentiment: 'negative' } },
290
+ {
291
+ label: 'Warning',
292
+ props: { text: 'Check the recipient details', sentiment: 'warning' },
293
+ },
294
+ { label: 'Success', props: { text: 'Transfer completed', sentiment: 'success' } },
295
+ {
296
+ label: 'Proposition',
297
+ props: { text: 'You can earn a referral reward', sentiment: 'proposition' },
298
+ },
299
+ ]}
300
+ />
301
+ ),
302
+ };
303
+
304
+ /**
305
+ * Snackbar supports one optional small action. Use `onClick` for an in-place action or `href` for
306
+ * navigation. An action dismisses the Snackbar, so use `duration="extended"` when people need
307
+ * enough time to read and act.
308
+ */
309
+ export const Actions: Story = {
310
+ parameters: {
311
+ docs: {
312
+ source: {
313
+ code: `showSnackbar({
314
+ text: 'Transfer deleted',
315
+ action: {
316
+ label: 'Undo',
317
+ onClick: handleUndo,
318
+ },
319
+ duration: 'extended',
320
+ });
321
+
322
+ showSnackbar({
323
+ text: 'Your transfer is ready',
324
+ action: {
325
+ label: 'View transfer',
326
+ href: '/transfers/123',
327
+ },
328
+ duration: 'extended',
329
+ });`,
330
+ },
331
+ },
332
+ },
333
+ render: () => (
334
+ <SnackbarExamples
335
+ examples={[
336
+ {
337
+ label: 'Callback action',
338
+ props: {
339
+ text: 'Transfer deleted',
340
+ action: { label: 'Undo', onClick: fn() },
341
+ duration: 'extended',
342
+ },
343
+ },
344
+ {
345
+ label: 'Navigation action',
346
+ props: {
347
+ text: 'Your transfer is ready',
348
+ action: { label: 'View transfer', href: '/transfers/123' },
349
+ duration: 'extended',
350
+ },
351
+ },
352
+ ]}
353
+ />
354
+ ),
355
+ };
356
+
357
+ /**
358
+ * Default media is derived from `sentiment`. Use `media` only when a custom icon communicates the
359
+ * message more clearly. `loading` has the highest priority: it replaces default or custom media
360
+ * with a ProcessIndicator, switches to neutral styling, and remains visible indefinitely.
361
+ */
362
+ export const MediaAndLoading: Story = {
44
363
  parameters: {
45
364
  docs: {
46
365
  source: {
47
- type: 'code',
48
- transform: async (src = '') => {
49
- const match = /(function Render\(\).*?)(,\s+play:\s+async)/s.exec(src);
50
- return match?.[1] || src;
366
+ code: `// Sentiment-derived default media
367
+ showSnackbar({ text: 'Transfer completed', sentiment: 'success' });
368
+
369
+ // Custom media
370
+ showSnackbar({
371
+ text: 'Added to your wallet',
372
+ sentiment: 'success',
373
+ media: <Card title="Card" />,
374
+ });
375
+
376
+ // Loading takes precedence over media and duration
377
+ showSnackbar({
378
+ text: 'Uploading document',
379
+ loading: true,
380
+ });`,
381
+ },
382
+ },
383
+ },
384
+ render: () => (
385
+ <SnackbarExamples
386
+ examples={[
387
+ {
388
+ label: 'Default media',
389
+ props: { text: 'Transfer completed', sentiment: 'success', duration: 'indefinite' },
390
+ },
391
+ {
392
+ label: 'Custom media',
393
+ props: {
394
+ text: 'Added to your wallet',
395
+ sentiment: 'success',
396
+ media: <Card title="Card" />,
397
+ duration: 'indefinite',
398
+ },
51
399
  },
400
+ {
401
+ label: 'Loading',
402
+ props: { text: 'Uploading document', loading: true },
403
+ },
404
+ ]}
405
+ />
406
+ ),
407
+ };
408
+
409
+ /**
410
+ * Duration is semantic rather than accepting arbitrary milliseconds:
411
+ *
412
+ * - `default`: 4 seconds for short confirmation without an action
413
+ * - `extended`: 6 seconds for additional reading or an action
414
+ * - `indefinite`: no automatic dismissal
415
+ *
416
+ * Timed Snackbars pause while hovered or while keyboard focus is inside, then resume when the
417
+ * interaction ends. Loading Snackbars are always indefinite.
418
+ *
419
+ * The deprecated millisecond `timeout` remains supported during migration but is not available in
420
+ * Storybook controls. Migrate values from 0–5000ms to `default`, values above 5000ms to `extended`,
421
+ * and intentionally persistent usage to `indefinite`. Semantic `duration` takes precedence when
422
+ * both are provided.
423
+ */
424
+ export const Durations: Story = {
425
+ parameters: {
426
+ docs: {
427
+ source: {
428
+ code: `showSnackbar({ text: 'Changes saved', duration: 'default' });
429
+ showSnackbar({ text: 'Transfer deleted', duration: 'extended' });
430
+ showSnackbar({ text: 'Uploading document', duration: 'indefinite' });`,
52
431
  },
53
432
  },
54
- source: {
55
- type: 'code',
56
- },
57
- },
58
- render: function Render() {
59
- return (
60
- <SnackbarProvider>
61
- <SnackbarConsumer>
62
- {({ createSnackbar }) => {
63
- return (
64
- <Button
65
- block
66
- onClick={() => {
67
- createSnackbar({
68
- action: {
69
- label: 'View',
70
- onClick: fn(),
71
- },
72
- text: <span>Some text</span>,
73
- });
74
- }}
75
- >
76
- Show Snackbar
77
- </Button>
78
- );
79
- }}
80
- </SnackbarConsumer>
81
- </SnackbarProvider>
82
- );
83
433
  },
84
- play: async ({ canvasElement }) => {
85
- await wait(0);
86
- const canvas = within(canvasElement);
87
- await userEvent.click(canvas.getByRole('button'));
434
+ render: () => (
435
+ <SnackbarExamples
436
+ examples={[
437
+ { label: 'Default · 4s', props: { text: 'Changes saved', duration: 'default' } },
438
+ { label: 'Extended · 6s', props: { text: 'Transfer deleted', duration: 'extended' } },
439
+ {
440
+ label: 'Indefinite',
441
+ props: { text: 'Uploading document', duration: 'indefinite' },
442
+ },
443
+ ]}
444
+ />
445
+ ),
446
+ };
447
+
448
+ /**
449
+ * Snackbars dismiss after their duration, after their action is used, through the desktop close
450
+ * button, or with a downward swipe on mobile. A mobile swipe must reach 50% of the rendered
451
+ * Snackbar height; shorter or cancelled swipes return it to its resting position.
452
+ *
453
+ * `onDismiss` runs after the exit transition completes, regardless of how dismissal started.
454
+ */
455
+ export const Dismissal: Story = {
456
+ args: {
457
+ duration: 'indefinite',
458
+ onDismiss: fn(),
459
+ text: 'Swipe down or use the close button',
460
+ },
461
+ parameters: {
462
+ docs: {
463
+ source: {
464
+ code: `showSnackbar({
465
+ text: 'Changes saved',
466
+ duration: 'indefinite',
467
+ onDismiss: handleDismiss,
468
+ });`,
469
+ },
470
+ },
88
471
  },
472
+ render: (args) => <SnackbarExamples examples={[{ label: 'Show Snackbar', props: args }]} />,
89
473
  };