@transferwise/components 0.0.0-experimental-58e9ef8 → 0.0.0-experimental-a88d24d

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 (55) hide show
  1. package/build/avatarView/AvatarView.js.map +1 -1
  2. package/build/avatarView/AvatarView.mjs.map +1 -1
  3. package/build/common/preventScroll/PreventScroll.js +1 -8
  4. package/build/common/preventScroll/PreventScroll.js.map +1 -1
  5. package/build/common/preventScroll/PreventScroll.mjs +1 -8
  6. package/build/common/preventScroll/PreventScroll.mjs.map +1 -1
  7. package/build/inputs/_BottomSheet.js +1 -29
  8. package/build/inputs/_BottomSheet.js.map +1 -1
  9. package/build/inputs/_BottomSheet.mjs +2 -30
  10. package/build/inputs/_BottomSheet.mjs.map +1 -1
  11. package/build/main.css +9 -13
  12. package/build/prompt/ActionPrompt/ActionPrompt.js +27 -4
  13. package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -1
  14. package/build/prompt/ActionPrompt/ActionPrompt.mjs +27 -4
  15. package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -1
  16. package/build/slidingPanel/SlidingPanel.js +20 -23
  17. package/build/slidingPanel/SlidingPanel.js.map +1 -1
  18. package/build/slidingPanel/SlidingPanel.mjs +21 -24
  19. package/build/slidingPanel/SlidingPanel.mjs.map +1 -1
  20. package/build/statusIcon/StatusIcon.js +2 -0
  21. package/build/statusIcon/StatusIcon.js.map +1 -1
  22. package/build/statusIcon/StatusIcon.mjs +2 -0
  23. package/build/statusIcon/StatusIcon.mjs.map +1 -1
  24. package/build/styles/dimmer/Dimmer.css +0 -1
  25. package/build/styles/inputs/SelectInput.css +9 -12
  26. package/build/styles/main.css +9 -13
  27. package/build/types/avatarView/AvatarView.d.ts +1 -1
  28. package/build/types/avatarView/AvatarView.d.ts.map +1 -1
  29. package/build/types/common/preventScroll/PreventScroll.d.ts +1 -1
  30. package/build/types/common/preventScroll/PreventScroll.d.ts.map +1 -1
  31. package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
  32. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +4 -2
  33. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -1
  34. package/build/types/slidingPanel/SlidingPanel.d.ts.map +1 -1
  35. package/build/types/statusIcon/StatusIcon.d.ts +2 -1
  36. package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
  37. package/package.json +3 -3
  38. package/src/avatarView/AvatarView.tsx +1 -1
  39. package/src/common/bottomSheet/__snapshots__/BottomSheet.test.tsx.snap +0 -6
  40. package/src/common/preventScroll/PreventScroll.tsx +1 -11
  41. package/src/dimmer/Dimmer.css +0 -1
  42. package/src/dimmer/Dimmer.less +0 -1
  43. package/src/inputs/SelectInput.css +9 -12
  44. package/src/inputs/_BottomSheet.less +6 -12
  45. package/src/inputs/_BottomSheet.tsx +5 -19
  46. package/src/main.css +9 -13
  47. package/src/prompt/ActionPrompt/ActionPrompt.accessibility.docs.mdx +65 -0
  48. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +4 -1
  49. package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +147 -0
  50. package/src/prompt/ActionPrompt/ActionPrompt.tsx +48 -7
  51. package/src/slidingPanel/SlidingPanel.tsx +24 -29
  52. package/src/statusIcon/StatusIcon.tsx +8 -1
  53. package/src/common/bottomSheet/BottomSheet.test.story.tsx +0 -94
  54. package/src/inputs/SelectInput.test.story.tsx +0 -83
  55. package/src/moneyInput/MoneyInput.test.story.tsx +0 -101
@@ -5,6 +5,7 @@ import { SizeSmall, SizeMedium, SizeLarge, Sentiment, Status } from '../common';
5
5
  type LegacySizes = SizeSmall | SizeMedium | SizeLarge;
6
6
  export type StatusIconSentiment = Sentiment | Status.PENDING;
7
7
  export type StatusIconProps = {
8
+ id?: string;
8
9
  sentiment?: `${StatusIconSentiment}`;
9
10
  size?: LegacySizes | 16 | 24 | 32 | 40 | 48 | 56 | 72;
10
11
  /**
@@ -14,6 +15,6 @@ export type StatusIconProps = {
14
15
  * */
15
16
  iconLabel?: string | null;
16
17
  };
17
- declare const StatusIcon: ({ sentiment, size: sizeProp, iconLabel }: StatusIconProps) => import("react").JSX.Element;
18
+ declare const StatusIcon: ({ id, sentiment, size: sizeProp, iconLabel, }: StatusIconProps) => import("react").JSX.Element;
18
19
  export default StatusIcon;
19
20
  //# sourceMappingURL=StatusIcon.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StatusIcon.d.ts","sourceRoot":"","sources":["../../../src/statusIcon/StatusIcon.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAoB,MAAM,EAAE,MAAM,WAAW,CAAC;AAMlG;;GAEG;AACH,KAAK,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtD,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACrC,IAAI,CAAC,EAAE,WAAW,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IACtD;;;;SAIK;IACL,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAQF,QAAA,MAAM,UAAU,GAAI,0CAA2D,eAAe,gCAyE7F,CAAC;AAEF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"StatusIcon.d.ts","sourceRoot":"","sources":["../../../src/statusIcon/StatusIcon.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAoB,MAAM,EAAE,MAAM,WAAW,CAAC;AAMlG;;GAEG;AACH,KAAK,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtD,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACrC,IAAI,CAAC,EAAE,WAAW,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IACtD;;;;SAIK;IACL,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAQF,QAAA,MAAM,UAAU,GAAI,+CAKjB,eAAe,gCA0EjB,CAAC;AAEF,eAAe,UAAU,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "0.0.0-experimental-58e9ef8",
3
+ "version": "0.0.0-experimental-a88d24d",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -106,8 +106,8 @@
106
106
  "@floating-ui/react": "^0.27.16",
107
107
  "@headlessui/react": "^2.2.9",
108
108
  "@popperjs/core": "^2.11.8",
109
- "@react-aria/focus": "^3.21.4",
110
- "@react-aria/overlays": "^3.31.1",
109
+ "@react-aria/focus": "^3.21.3",
110
+ "@react-aria/overlays": "^3.31.0",
111
111
  "@transferwise/formatting": "^2.13.4",
112
112
  "@transferwise/neptune-validation": "^3.3.1",
113
113
  "clsx": "^2.1.1",
@@ -32,7 +32,7 @@ export type Props = {
32
32
  style?: Pick<React.CSSProperties, 'border' | 'backgroundColor' | 'color'>;
33
33
  } & Pick<
34
34
  HTMLAttributes<HTMLDivElement>,
35
- 'className' | 'children' | 'role' | 'aria-label' | 'aria-labelledby' | 'aria-hidden'
35
+ 'id' | 'className' | 'children' | 'role' | 'aria-label' | 'aria-labelledby' | 'aria-hidden'
36
36
  >;
37
37
 
38
38
  function AvatarView({
@@ -20,12 +20,6 @@ exports[`BottomSheet renders content when open 1`] = `
20
20
  <div
21
21
  class="dimmer-content-positioner"
22
22
  >
23
- <style>
24
- html, body {
25
- height: 100dvh;
26
- overflow: hidden;
27
- }
28
- </style>
29
23
  <div
30
24
  class="sliding-panel sliding-panel--open-bottom np-bottom-sheet sliding-panel-appear sliding-panel-appear-active"
31
25
  >
@@ -2,15 +2,5 @@ import { usePreventScroll } from '@react-aria/overlays';
2
2
 
3
3
  export function PreventScroll() {
4
4
  usePreventScroll();
5
-
6
- // ios18
7
- return (
8
- <style>{`html, body {
9
- height: 100dvh;
10
- overflow: hidden;
11
- }`}</style>
12
- );
13
-
14
- // ios26
15
- return <style>{`html, body { height: 100dvh; overflow: hidden; }`}</style>;
5
+ return null;
16
6
  }
@@ -1,6 +1,5 @@
1
1
  .no-scroll {
2
2
  overflow: hidden;
3
- scroll-behavior: unset;
4
3
  }
5
4
  .dimmer {
6
5
  position: fixed;
@@ -6,7 +6,6 @@
6
6
  // (see https://bugs.webkit.org/show_bug.cgi?id=153852 & https://bugs.webkit.org/show_bug.cgi?id=220908)
7
7
  .no-scroll {
8
8
  overflow: hidden;
9
- scroll-behavior: unset;
10
9
  }
11
10
 
12
11
  .dimmer {
@@ -11,38 +11,35 @@
11
11
  transition-property: opacity;
12
12
  transition-timing-function: ease-out;
13
13
  transition-duration: 300ms;
14
- height: 100vh;
15
- min-height: -webkit-fill-available;
16
14
  }
17
15
  .np-bottom-sheet-v2-backdrop--closed {
18
16
  opacity: 0;
19
17
  }
20
18
  .np-bottom-sheet-v2 {
21
19
  position: fixed;
22
- inset: 0;
20
+ inset: 0px;
23
21
  bottom: env(keyboard-inset-height, 0px);
24
- padding-top: 64px;
25
- padding-top: var(--size-64);
26
- padding-bottom: min(env(safe-area-inset-bottom, 16px));
27
- padding-bottom: min(env(safe-area-inset-bottom, var(--size-16)));
22
+ margin-left: 8px;
23
+ margin-left: var(--size-8);
24
+ margin-right: 8px;
25
+ margin-right: var(--size-8);
26
+ margin-top: 64px;
27
+ margin-top: var(--size-64);
28
28
  display: flex;
29
29
  flex-direction: column;
30
30
  justify-content: flex-end;
31
- height: 100%;
32
- min-height: -webkit-fill-available;
33
31
  }
34
32
  .np-bottom-sheet-v2-content {
35
33
  display: flex;
36
34
  flex-direction: column;
37
35
  overflow: auto;
38
36
  border-top-left-radius: 32px;
39
- border-top-left-radius: var(--size-32);
37
+ /* TODO: Tokenize */
40
38
  border-top-right-radius: 32px;
41
- border-top-right-radius: var(--size-32);
39
+ /* TODO: Tokenize */
42
40
  background-color: #ffffff;
43
41
  background-color: var(--color-background-elevated);
44
42
  box-shadow: 0 0 40px rgba(69, 71, 69, 0.2);
45
- will-change: transform;
46
43
  }
47
44
  .np-bottom-sheet-v2-content:focus {
48
45
  outline: none;
@@ -11,8 +11,6 @@
11
11
  transition-property: opacity;
12
12
  transition-timing-function: ease-out;
13
13
  transition-duration: 300ms;
14
- height: 100vh;
15
- min-height: -webkit-fill-available;
16
14
 
17
15
  &--closed {
18
16
  opacity: 0;
@@ -21,28 +19,24 @@
21
19
 
22
20
  .np-bottom-sheet-v2 {
23
21
  position: fixed;
24
- inset: 0;
22
+ inset: 0px;
25
23
  bottom: env(keyboard-inset-height, 0px);
26
- // margin-left: var(--size-8);
27
- // margin-right: var(--size-8);
28
- padding-top: var(--size-64);
29
- padding-bottom: min(env(safe-area-inset-bottom, var(--size-16)));
24
+ margin-left: var(--size-8);
25
+ margin-right: var(--size-8);
26
+ margin-top: var(--size-64);
30
27
  display: flex;
31
28
  flex-direction: column;
32
29
  justify-content: flex-end;
33
- height: 100%;
34
- min-height: -webkit-fill-available;
35
30
  }
36
31
 
37
32
  .np-bottom-sheet-v2-content {
38
33
  display: flex;
39
34
  flex-direction: column;
40
35
  overflow: auto;
41
- border-top-left-radius: var(--size-32);
42
- border-top-right-radius: var(--size-32);
36
+ border-top-left-radius: 32px; /* TODO: Tokenize */
37
+ border-top-right-radius: 32px; /* TODO: Tokenize */
43
38
  background-color: var(--color-background-elevated);
44
39
  box-shadow: 0 0 40px rgb(69 71 69 / 0.2);
45
- will-change: transform;
46
40
 
47
41
  &:focus {
48
42
  outline: none;
@@ -10,11 +10,12 @@ import { Transition, TransitionChild } from '@headlessui/react';
10
10
  import { FocusScope } from '@react-aria/focus';
11
11
  import { ThemeProvider, useTheme } from '@wise/components-theming';
12
12
  import { clsx } from 'clsx';
13
- import { Fragment, useState, useEffect } from 'react';
13
+ import { Fragment, useState } from 'react';
14
14
 
15
- import { CloseButton, Size } from '../common';
15
+ import { CloseButton } from '../common/closeButton';
16
16
  import { useVirtualKeyboard } from '../common/hooks/useVirtualKeyboard';
17
17
  import { PreventScroll } from '../common/preventScroll/PreventScroll';
18
+ import { Size } from '../common/propsValues/size';
18
19
 
19
20
  export interface BottomSheetProps {
20
21
  open: boolean;
@@ -32,19 +33,6 @@ export interface BottomSheetProps {
32
33
  onCloseEnd?: () => void;
33
34
  }
34
35
 
35
- /**
36
- * App pages set scroll-behavior to 'smooth' which causes mobile Safari to
37
- * slow-scroll and glitch. This function temporarily disables that behaviour
38
- * while the BottomSheet is open.
39
- */
40
- const freezeScroll = (shouldFreeze = true) => {
41
- if (shouldFreeze) {
42
- document.documentElement.style.scrollBehavior = 'unset';
43
- } else {
44
- document.documentElement.style.removeProperty('scroll-behavior');
45
- }
46
- };
47
-
48
36
  export function BottomSheet({
49
37
  open,
50
38
  renderTrigger,
@@ -66,14 +54,12 @@ export function BottomSheet({
66
54
  },
67
55
  });
68
56
 
69
- useEffect(() => {
70
- freezeScroll(open);
71
- }, [open]);
72
-
73
57
  const dismiss = useDismiss(context);
74
58
  const role = useRole(context);
75
59
  const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, role]);
60
+
76
61
  const [floatingKey, setFloatingKey] = useState(0);
62
+
77
63
  const { theme, screenMode } = useTheme();
78
64
 
79
65
  return (
package/src/main.css CHANGED
@@ -2574,7 +2574,6 @@ button.np-option {
2574
2574
  }
2575
2575
  .no-scroll {
2576
2576
  overflow: hidden;
2577
- scroll-behavior: unset;
2578
2577
  }
2579
2578
  .dimmer {
2580
2579
  position: fixed;
@@ -3840,38 +3839,35 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
3840
3839
  transition-property: opacity;
3841
3840
  transition-timing-function: ease-out;
3842
3841
  transition-duration: 300ms;
3843
- height: 100vh;
3844
- min-height: -webkit-fill-available;
3845
3842
  }
3846
3843
  .np-bottom-sheet-v2-backdrop--closed {
3847
3844
  opacity: 0;
3848
3845
  }
3849
3846
  .np-bottom-sheet-v2 {
3850
3847
  position: fixed;
3851
- inset: 0;
3848
+ inset: 0px;
3852
3849
  bottom: env(keyboard-inset-height, 0px);
3853
- padding-top: 64px;
3854
- padding-top: var(--size-64);
3855
- padding-bottom: min(env(safe-area-inset-bottom, 16px));
3856
- padding-bottom: min(env(safe-area-inset-bottom, var(--size-16)));
3850
+ margin-left: 8px;
3851
+ margin-left: var(--size-8);
3852
+ margin-right: 8px;
3853
+ margin-right: var(--size-8);
3854
+ margin-top: 64px;
3855
+ margin-top: var(--size-64);
3857
3856
  display: flex;
3858
3857
  flex-direction: column;
3859
3858
  justify-content: flex-end;
3860
- height: 100%;
3861
- min-height: -webkit-fill-available;
3862
3859
  }
3863
3860
  .np-bottom-sheet-v2-content {
3864
3861
  display: flex;
3865
3862
  flex-direction: column;
3866
3863
  overflow: auto;
3867
3864
  border-top-left-radius: 32px;
3868
- border-top-left-radius: var(--size-32);
3865
+ /* TODO: Tokenize */
3869
3866
  border-top-right-radius: 32px;
3870
- border-top-right-radius: var(--size-32);
3867
+ /* TODO: Tokenize */
3871
3868
  background-color: #ffffff;
3872
3869
  background-color: var(--color-background-elevated);
3873
3870
  box-shadow: 0 0 40px rgba(69, 71, 69, 0.2);
3874
- will-change: transform;
3875
3871
  }
3876
3872
  .np-bottom-sheet-v2-content:focus {
3877
3873
  outline: none;
@@ -0,0 +1,65 @@
1
+ import { Meta, Canvas, Source } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Prompts/ActionPrompt/Accessibility" />
4
+
5
+ # Accessibility
6
+
7
+ Under the hood, `ActionPrompt` is marked as `role="region"` for a section of content that users might want to navigate to directly.
8
+
9
+ By default, it's labelled by the `media` asset and `title`, as well as described by the `description` if one is provided.
10
+
11
+ ## Announcement Behaviour
12
+
13
+ `ActionPrompt` should be treated as a hint or suggestion as part of the product UX. Even the negative variant still serves as a non-critical message. Therefore, it shouldn't be announced via screen reader—neither assertively (`role="alert"`) nor politely (`role="status"`).
14
+
15
+ **Note:** For immediate user feedback, use [InfoPrompt](?path=/docs/prompts-infoprompt--docs) or [InlinePrompt](https://storybook.wise.design/?path=/docs/prompts-inlineprompt--docs) instead.
16
+
17
+ If you want to provide a custom label for screen readers, you can use the `aria-label` prop. Make sure to include all necessary information in it, because when provided, the default labelling will be removed.
18
+
19
+ <Source
20
+ dark
21
+ code={`
22
+ <ActionPrompt
23
+ aria-label="JFYI: Henry requested 30 USD for lunch"
24
+ media={{ avatar: { imgSrc: 'profile-photo.webp' } }}
25
+ title="Henry requested 30 USD"
26
+ description="Lunch date"
27
+ ...
28
+ />
29
+ `}
30
+ />
31
+
32
+ ## Media
33
+
34
+ You can use the `aria-hidden` attribute on media assets to hide them from screen readers if they're not providing any additional information. This is useful when the media is purely decorative or when its content is already conveyed through other means (e.g., text).
35
+
36
+ <Source
37
+ dark
38
+ code={`
39
+ <ActionPrompt
40
+ media={{
41
+ 'aria-hidden': true,
42
+ avatar: { asset: <People /> },
43
+ }}
44
+ title="Sync contacts for a faster experience"
45
+ ...
46
+ />
47
+ `}
48
+ />
49
+
50
+ You can also use `aria-label` on media assets to provide a custom label for screen readers.
51
+
52
+ <Source
53
+ dark
54
+ code={`
55
+ <ActionPrompt
56
+ media={{
57
+ 'aria-label': 'Henry Adams photo',
58
+ avatar: { imgSrc: 'profile-photo.webp' },
59
+ }}
60
+ title="Henry requested 30 USD"
61
+ description="Lunch date"
62
+ ...
63
+ />
64
+ `}
65
+ />
@@ -1,9 +1,12 @@
1
1
  import { ReactElement, useState } from 'react';
2
2
  import { Meta, StoryObj } from '@storybook/react-webpack5';
3
3
  import { fn } from 'storybook/test';
4
- import { Bank } from '@transferwise/icons';
4
+ import { Bank, Freeze, People } from '@transferwise/icons';
5
5
  import { ActionPrompt } from './ActionPrompt';
6
6
  import { lorem10 } from '../../test-utils';
7
+ import Body from '../../body';
8
+ import { action } from 'storybook/actions';
9
+ import Title from '../../title';
7
10
  import { withVariantConfig } from '../../../.storybook/helpers';
8
11
 
9
12
  const meta: Meta<typeof ActionPrompt> = {
@@ -0,0 +1,147 @@
1
+ import { Freeze, People } from '@transferwise/icons';
2
+ import { action } from 'storybook/actions';
3
+ import ActionPrompt from './ActionPrompt';
4
+ import { Body, Title } from '../..';
5
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
6
+
7
+ export default {
8
+ title: 'Prompts/ActionPrompt/Tests',
9
+ component: ActionPrompt,
10
+ tags: ['!manifest', '!autodocs'],
11
+ };
12
+
13
+ type Story = StoryObj<typeof ActionPrompt>;
14
+
15
+ export const VariousA11yFeatures: Story = {
16
+ name: 'Various a11y features',
17
+ render: () => {
18
+ return (
19
+ <Body>
20
+ <Title type="title-body">Neutral Prompt with Avatar Image and Custom label for media</Title>
21
+ <ActionPrompt
22
+ className="m-b-2"
23
+ sentiment="neutral"
24
+ media={{
25
+ 'aria-label': 'Henry Profile Photo',
26
+ avatar: { imgSrc: '../../avatar-square-dude.webp' },
27
+ }}
28
+ title="Henry requested 30 USD"
29
+ description="Lunch date"
30
+ action={{
31
+ label: 'Sent',
32
+ onClick: () => {
33
+ action('send');
34
+ },
35
+ }}
36
+ actionSecondary={{
37
+ label: 'Decline',
38
+ onClick: () => {
39
+ action('decline');
40
+ },
41
+ }}
42
+ onDismiss={() => action('dismiss')}
43
+ />
44
+
45
+ <Title type="title-body">Warning Prompt</Title>
46
+ <ActionPrompt
47
+ className="m-b-2"
48
+ sentiment="warning"
49
+ media={{
50
+ 'aria-label': 'Image of beautiful Business Wise Card',
51
+ imgSrc: '../../wise-card.svg',
52
+ }}
53
+ title="Your Wise Card expires soon"
54
+ description="Renew your card to keep spending"
55
+ action={{
56
+ label: 'Renew card',
57
+ onClick: () => {
58
+ action('renew');
59
+ },
60
+ }}
61
+ actionSecondary={{
62
+ label: 'Notify later',
63
+ onClick: () => {
64
+ action('notifyLater');
65
+ },
66
+ }}
67
+ onDismiss={() => action('dismiss')}
68
+ />
69
+
70
+ <Title type="title-body">Warning Prompt Avatar Icon</Title>
71
+ <ActionPrompt
72
+ className="m-b-2"
73
+ sentiment="warning"
74
+ media={{ avatar: { asset: <Freeze /> } }}
75
+ title="Your Wise Card expires soon"
76
+ description="Renew your card to keep spending"
77
+ action={{
78
+ label: 'Renew card',
79
+ onClick: () => {
80
+ action('renew');
81
+ },
82
+ }}
83
+ onDismiss={() => {
84
+ action('dismiss');
85
+ }}
86
+ />
87
+
88
+ <Title type="title-body">Proposition Prompt Avatar Icon</Title>
89
+ <ActionPrompt
90
+ className="m-b-2"
91
+ sentiment="proposition"
92
+ media={{ avatar: { asset: <People /> } }}
93
+ title="Sync contacts for a faster experience"
94
+ description="Find contacts on Wise — it’s simple, secure and you pick who you add."
95
+ action={{
96
+ label: 'Sync contacts',
97
+ onClick: () => {
98
+ action('sync');
99
+ },
100
+ }}
101
+ onDismiss={() => {
102
+ action('dismiss');
103
+ }}
104
+ />
105
+
106
+ <Title type="title-body">Negative Prompt Avatar Icon + muted media</Title>
107
+ <ActionPrompt
108
+ className="m-b-2"
109
+ sentiment="negative"
110
+ media={{ avatar: { asset: <People /> }, 'aria-hidden': true }}
111
+ title="Sync contacts for a faster experience"
112
+ description="Find contacts on Wise — it’s simple, secure and you pick who you add."
113
+ action={{
114
+ label: 'Sync contacts',
115
+ onClick: () => {
116
+ action('sync');
117
+ },
118
+ }}
119
+ onDismiss={() => {
120
+ action('dismiss');
121
+ }}
122
+ />
123
+
124
+ <Title type="title-body">
125
+ Negative Prompt + override content with custom message via aria-label
126
+ </Title>
127
+ <ActionPrompt
128
+ className="m-b-2"
129
+ aria-label="hey customer, here is custom message"
130
+ sentiment="negative"
131
+ media={{ avatar: { asset: <People /> } }}
132
+ title="Sync contacts for a faster experience"
133
+ description="Find contacts on Wise — it’s simple, secure and you pick who you add."
134
+ action={{
135
+ label: 'Sync contacts',
136
+ onClick: () => {
137
+ action('sync');
138
+ },
139
+ }}
140
+ onDismiss={() => {
141
+ action('dismiss');
142
+ }}
143
+ />
144
+ </Body>
145
+ );
146
+ },
147
+ };
@@ -1,4 +1,4 @@
1
- import { ReactNode } from 'react';
1
+ import { AriaAttributes, ReactNode, useId } from 'react';
2
2
  import { clsx } from 'clsx';
3
3
 
4
4
  import StatusIcon from '../../statusIcon';
@@ -23,6 +23,7 @@ export type ActionPromptProps = {
23
23
  badge?: Pick<BadgeAssetsProps, 'flagCode'>;
24
24
  };
25
25
  'aria-label'?: string;
26
+ 'aria-hidden'?: boolean;
26
27
  };
27
28
  action: Pick<ButtonProps, 'onClick' | 'href' | 'target'> & {
28
29
  label: ButtonProps['children'];
@@ -30,6 +31,7 @@ export type ActionPromptProps = {
30
31
  actionSecondary?: Pick<ButtonProps, 'onClick' | 'href' | 'target'> & {
31
32
  label: ButtonProps['children'];
32
33
  };
34
+ 'aria-label'?: AriaAttributes['aria-label'];
33
35
  } & Pick<PrimitivePromptProps, 'id' | 'className' | 'data-testid' | 'sentiment' | 'onDismiss'>;
34
36
 
35
37
  export const ActionPrompt = ({
@@ -37,19 +39,32 @@ export const ActionPrompt = ({
37
39
  title,
38
40
  description,
39
41
  onDismiss,
40
- media,
42
+ media = {},
41
43
  action,
42
44
  actionSecondary,
43
45
  id,
44
46
  className,
45
47
  'data-testid': testId,
48
+ 'aria-label': ariaLabel,
46
49
  }: ActionPromptProps) => {
47
50
  const isMobile = !useScreenSize(Breakpoint.MEDIUM);
48
51
 
52
+ const mediaId = useId();
53
+ const titleId = useId();
54
+ const descId = useId();
55
+
56
+ const ariaLabelledByIds = [
57
+ media['aria-hidden'] ? undefined : mediaId,
58
+ Boolean(ariaLabel) ? undefined : titleId,
59
+ ]
60
+ .filter(Boolean)
61
+ .join(' ');
62
+
49
63
  const renderMedia = () => {
50
64
  if (media?.imgSrc) {
51
65
  return (
52
66
  <Image
67
+ id={mediaId}
53
68
  src={media.imgSrc}
54
69
  className="wds-action-prompt--media-image"
55
70
  alt={media['aria-label'] ?? ''}
@@ -63,17 +78,34 @@ export const ActionPrompt = ({
63
78
  ? {}
64
79
  : { status: sentiment };
65
80
  return (
66
- <AvatarView {...media.avatar} badge={badge} size={48}>
81
+ <AvatarView
82
+ {...media.avatar}
83
+ badge={badge}
84
+ aria-label={media['aria-label']}
85
+ aria-hidden={media['aria-hidden']}
86
+ id={mediaId}
87
+ size={48}
88
+ >
67
89
  {media.avatar.asset}
68
90
  </AvatarView>
69
91
  );
70
92
  }
71
93
  return sentiment === 'proposition' ? (
72
- <AvatarView size={48}>
94
+ <AvatarView
95
+ id={mediaId}
96
+ size={48}
97
+ aria-label={media['aria-label']}
98
+ aria-hidden={media['aria-hidden']}
99
+ >
73
100
  <GiftBox />
74
101
  </AvatarView>
75
102
  ) : (
76
- <StatusIcon size={48} sentiment={sentiment} />
103
+ <StatusIcon
104
+ id={mediaId}
105
+ size={48}
106
+ sentiment={sentiment}
107
+ iconLabel={Boolean(media['aria-hidden']) ? null : media['aria-label']}
108
+ />
77
109
  );
78
110
  };
79
111
 
@@ -117,10 +149,19 @@ export const ActionPrompt = ({
117
149
  </>
118
150
  }
119
151
  onDismiss={onDismiss}
152
+ role="region"
153
+ {...(Boolean(ariaLabel)
154
+ ? { 'aria-label': ariaLabel }
155
+ : {
156
+ 'aria-labelledby': ariaLabelledByIds,
157
+ 'aria-describedby': descId,
158
+ })}
120
159
  >
121
160
  <div className={clsx('d-flex', 'flex-column', 'justify-content-center')}>
122
- <Body type={Typography.BODY_LARGE_BOLD}>{title}</Body>
123
- {description && <Body>{description}</Body>}
161
+ <Body id={titleId} type={Typography.BODY_LARGE_BOLD}>
162
+ {title}
163
+ </Body>
164
+ {description && <Body id={descId}>{description}</Body>}
124
165
  </div>
125
166
  </PrimitivePrompt>
126
167
  );