@transferwise/components 46.135.1 → 46.135.3

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 (59) hide show
  1. package/build/avatarWrapper/AvatarWrapper.js.map +1 -1
  2. package/build/avatarWrapper/AvatarWrapper.mjs.map +1 -1
  3. package/build/common/panel/Panel.js +49 -54
  4. package/build/common/panel/Panel.js.map +1 -1
  5. package/build/common/panel/Panel.mjs +54 -59
  6. package/build/common/panel/Panel.mjs.map +1 -1
  7. package/build/listItem/ListItem.js +2 -2
  8. package/build/listItem/ListItem.js.map +1 -1
  9. package/build/listItem/ListItem.mjs +2 -2
  10. package/build/listItem/ListItem.mjs.map +1 -1
  11. package/build/main.css +4 -3
  12. package/build/select/Select.js +1 -1
  13. package/build/select/Select.js.map +1 -1
  14. package/build/select/Select.mjs +1 -1
  15. package/build/select/Select.mjs.map +1 -1
  16. package/build/styles/main.css +4 -3
  17. package/build/styles/modal/Modal.css +2 -2
  18. package/build/styles/select/Select.css +2 -1
  19. package/build/tooltip/Tooltip.js +29 -50
  20. package/build/tooltip/Tooltip.js.map +1 -1
  21. package/build/tooltip/Tooltip.mjs +30 -51
  22. package/build/tooltip/Tooltip.mjs.map +1 -1
  23. package/build/types/common/panel/Panel.d.ts.map +1 -1
  24. package/build/types/tooltip/Tooltip.d.ts.map +1 -1
  25. package/package.json +9 -11
  26. package/src/actionButton/ActionButton.story.tsx +4 -4
  27. package/src/actionButton/ActionButton.test.story.tsx +4 -4
  28. package/src/avatarWrapper/AvatarWrapper.tsx +3 -3
  29. package/src/common/circle/Circle.story.tsx +3 -3
  30. package/src/common/panel/Panel.tsx +56 -49
  31. package/src/iconButton/IconButton.story.tsx +5 -6
  32. package/src/iconButton/IconButton.test.story.tsx +8 -8
  33. package/src/icons/Icons.story.tsx +381 -0
  34. package/src/listItem/ListItem.test.tsx +24 -0
  35. package/src/listItem/ListItem.tsx +2 -2
  36. package/src/listItem/_stories/ListItem.context.test.story.tsx +63 -0
  37. package/src/listItem/_stories/ListItem.scenarios.story.tsx +3 -3
  38. package/src/main.css +4 -3
  39. package/src/modal/Modal.css +2 -2
  40. package/src/modal/Modal.less +1 -1
  41. package/src/moneyInput/MoneyInput.story.tsx +2 -2
  42. package/src/navigationOption/NavigationOption.story.tsx +3 -3
  43. package/src/overlayHeader/OverlayHeader.story.tsx +2 -2
  44. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +3 -3
  45. package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +3 -3
  46. package/src/prompt/InfoPrompt/InfoPrompt.accessibility.docs.mdx +1 -1
  47. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +3 -3
  48. package/src/prompt/InlinePrompt/InlinePrompt.accessibility.docs.mdx +1 -1
  49. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +5 -5
  50. package/src/prompt/InlinePrompt/InlinePrompt.test.story.tsx +2 -2
  51. package/src/select/Select.css +2 -1
  52. package/src/select/Select.less +6 -5
  53. package/src/select/Select.story.tsx +3 -3
  54. package/src/select/Select.test.story.tsx +59 -0
  55. package/src/select/Select.tsx +1 -1
  56. package/src/select/option/Option.test.tsx +3 -3
  57. package/src/summary/Summary.story.tsx +5 -5
  58. package/src/summary/Summary.test.story.tsx +2 -2
  59. package/src/tooltip/Tooltip.tsx +26 -45
@@ -64,7 +64,7 @@ Custom media icons should include their own accessibility attributes. Use the `t
64
64
  <InfoPrompt
65
65
  sentiment="success"
66
66
  description="Your travel account is ready!"
67
- media={{ asset: <Travel title="Travel feature" /> }}
67
+ media={{ asset: <Suitcase title="Travel feature" /> }}
68
68
  />
69
69
 
70
70
  <InfoPrompt
@@ -2,7 +2,7 @@ import type { ReactElement } from 'react';
2
2
  import { useState } from 'react';
3
3
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
4
4
  import { action } from 'storybook/actions';
5
- import { Confetti, GiftBox, Star, Travel, Briefcase, Plane } from '@transferwise/icons';
5
+ import { Confetti, GiftBox, Star, Suitcase, Briefcase, Plane } from '@transferwise/icons';
6
6
  import { lorem10, lorem20 } from '../../test-utils';
7
7
  import Button from '../../button';
8
8
  import Title from '../../title';
@@ -90,7 +90,7 @@ const MEDIA_OPTIONS: Record<string, { asset: ReactElement } | undefined> = {
90
90
  star: { asset: <Star title="Starred" /> },
91
91
  confetti: { asset: <Confetti size={24} title="Celebration" /> },
92
92
  giftbox: { asset: <GiftBox title="Gift" /> },
93
- travel: { asset: <Travel title="Travel" /> },
93
+ travel: { asset: <Suitcase title="Travel" /> },
94
94
  briefcase: { asset: <Briefcase title="Business" /> },
95
95
  plane: { asset: <Plane title="Travel" /> },
96
96
  };
@@ -297,7 +297,7 @@ export const MediaTypes: StoryObj<InfoPromptProps> = {
297
297
  <InfoPrompt
298
298
  sentiment="success"
299
299
  description="Your travel account is ready!"
300
- media={{ asset: <Travel title="Travel" /> }}
300
+ media={{ asset: <Suitcase title="Travel" /> }}
301
301
  />
302
302
  <InfoPrompt
303
303
  sentiment="warning"
@@ -30,7 +30,7 @@ By default, sentiment icons have these labels:
30
30
  dark
31
31
  code={`
32
32
  <InlinePrompt
33
- media={<Travel />}
33
+ media={<Suitcase />}
34
34
  sentiment="positive"
35
35
  mediaLabel="Travel feature enabled: "
36
36
  >
@@ -1,7 +1,7 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
3
3
  import { action } from 'storybook/actions';
4
- import { Clock, Taxi, Travel } from '@transferwise/icons';
4
+ import { Clock, Taxi, Suitcase } from '@transferwise/icons';
5
5
  import { lorem5 } from '../../test-utils';
6
6
  import Link from '../../link';
7
7
  import { InlinePrompt, InlinePromptProps } from './InlinePrompt';
@@ -233,7 +233,7 @@ export const IconOverrides: StoryObj<PreviewStoryArgs> = {
233
233
  <InlinePrompt {...args} media={<Clock title="Processing: " />} sentiment="warning">
234
234
  The account verification is taking longer than usual.
235
235
  </InlinePrompt>
236
- <InlinePrompt {...args} media={<Travel title="Activated: " />} sentiment="positive">
236
+ <InlinePrompt {...args} media={<Suitcase title="Activated: " />} sentiment="positive">
237
237
  Your travel account is set up and ready to use.
238
238
  </InlinePrompt>
239
239
  <InlinePrompt {...args} media={<Taxi title="Taxi addon: " />} sentiment="proposition">
@@ -256,13 +256,13 @@ export const SizingStrategies: StoryObj<PreviewStoryArgs> = {
256
256
  render: (args: PreviewStoryArgs) => {
257
257
  return (
258
258
  <>
259
- <InlinePrompt {...args} media={<Travel />} sentiment="positive" width="full">
259
+ <InlinePrompt {...args} media={<Suitcase />} sentiment="positive" width="full">
260
260
  This prompt will take the full width of its container.
261
261
  </InlinePrompt>
262
- <InlinePrompt {...args} media={<Travel />} sentiment="positive">
262
+ <InlinePrompt {...args} media={<Suitcase />} sentiment="positive">
263
263
  This prompt will hug its content.
264
264
  </InlinePrompt>
265
- <InlinePrompt {...args} media={<Travel />} sentiment="positive">
265
+ <InlinePrompt {...args} media={<Suitcase />} sentiment="positive">
266
266
  This prompt is configured to hug its content, but since the content is long enough to span
267
267
  across multiple lines, it will expand to the full width of its container. And no, you
268
268
  should not put pages of text here in the first place, but we recognise that some
@@ -1,5 +1,5 @@
1
1
  import { StoryObj } from '@storybook/react-webpack5';
2
- import { Travel } from '@transferwise/icons';
2
+ import { Suitcase } from '@transferwise/icons';
3
3
  import { InlinePrompt } from './InlinePrompt';
4
4
  import { withVariantConfig } from '../../../.storybook/helpers';
5
5
 
@@ -13,7 +13,7 @@ type Story = StoryObj<typeof InlinePrompt>;
13
13
 
14
14
  export const TinyScreen: Story = {
15
15
  render: () => (
16
- <InlinePrompt sentiment="positive" media={<Travel />}>
16
+ <InlinePrompt sentiment="positive" media={<Suitcase />}>
17
17
  Your travel money account is now active and ready to use for international transactions.
18
18
  </InlinePrompt>
19
19
  ),
@@ -123,14 +123,15 @@
123
123
  max-width: calc(100% - 32px);
124
124
  }
125
125
  .np-dropdown-menu-desktop {
126
- max-height: 70vh;
127
126
  max-height: 70svh;
127
+ max-height: var(--np-panel-available-height, 70svh);
128
128
  min-width: 160px;
129
129
  max-width: calc(100vw - 32px);
130
130
  }
131
131
  @media (min-height: 592px) {
132
132
  .np-dropdown-menu-desktop {
133
133
  max-height: 592px;
134
+ max-height: var(--np-panel-available-height, 592px);
134
135
  }
135
136
  }
136
137
  .np-dropdown-menu {
@@ -81,7 +81,7 @@
81
81
 
82
82
  // Override some of Button component styles
83
83
  .btn {
84
- &:not(.disabled,:disabled,.btn-loading) {
84
+ &:not(.disabled, :disabled, .btn-loading) {
85
85
  color: var(--color-content-primary);
86
86
 
87
87
  &:hover {
@@ -108,15 +108,14 @@
108
108
  }
109
109
 
110
110
  .np-dropdown-menu-desktop {
111
- max-height: 70vh;
112
- max-height: 70svh;
111
+ max-height: var(--np-panel-available-height, 70svh);
113
112
  min-width: 160px;
114
113
  max-width: ~"calc(100vw - 32px)";
115
114
  }
116
115
 
117
116
  @media (min-height: 592px) {
118
117
  .np-dropdown-menu-desktop {
119
- max-height: 592px;
118
+ max-height: var(--np-panel-available-height, 592px);
120
119
  }
121
120
  }
122
121
 
@@ -270,7 +269,9 @@
270
269
  /* stylelint-disable-next-line no-duplicate-selectors */
271
270
  .np-select .np-dropdown-toggle {
272
271
  background-color: unset;
273
- transition: background-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
272
+ transition:
273
+ background-color 0.3s ease-in-out,
274
+ box-shadow 0.3s ease-in-out;
274
275
  border: none;
275
276
  box-shadow: inset 0 0 0 1px var(--color-interactive-secondary);
276
277
 
@@ -1,5 +1,5 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-webpack5';
2
- import { Person as ProfileIcon, Globe as GlobeIcon } from '@transferwise/icons';
2
+ import { Person, Globe as GlobeIcon } from '@transferwise/icons';
3
3
  import { useState } from 'react';
4
4
  import Select, { SelectItem, SelectOptionItem } from './Select';
5
5
 
@@ -75,7 +75,7 @@ export const Basic: Story = {
75
75
  { value: 2, label: 'A disabled thing', disabled: true },
76
76
  { separator: true },
77
77
  { header: 'Icons' },
78
- { value: 3, label: 'Profile', icon: <ProfileIcon /> },
78
+ { value: 3, label: 'Profile', icon: <Person /> },
79
79
  {
80
80
  value: 4,
81
81
  label: 'USD',
@@ -312,7 +312,7 @@ export const AdvancedSearch: Story = {
312
312
  { value: 2, label: 'A disabled thing', disabled: true },
313
313
  { separator: true },
314
314
  { header: 'Icons' },
315
- { value: 3, label: 'Profile', icon: <ProfileIcon /> },
315
+ { value: 3, label: 'Profile', icon: <Person /> },
316
316
  { value: 4, label: 'Globe', icon: <GlobeIcon /> },
317
317
  { header: 'Currencies' },
318
318
  { value: 5, label: 'British pound', currency: 'gbp' },
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { userEvent, within } from 'storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { withVariantConfig } from '../../.storybook/helpers';
6
+ import Select, { SelectOptionItem } from './Select';
7
+
8
+ const meta = {
9
+ title: 'Forms/Select/Tests',
10
+ component: Select,
11
+ tags: ['!autodocs', '!manifest'],
12
+ } satisfies Meta<typeof Select>;
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof Select>;
16
+
17
+ const options: SelectOptionItem[] = Array.from({ length: 10 }, (_, i) => ({
18
+ value: i + 1,
19
+ label: `Option ${i + 1}`,
20
+ }));
21
+
22
+ /**
23
+ * Visual regression story: Select placed at the bottom of the viewport so the dropdown
24
+ * flips to the top when opened. The play function opens the dropdown automatically.
25
+ */
26
+ export const DropdownFlipsToTop: Story = {
27
+ render: function Render() {
28
+ const [selected, setSelected] = useState<SelectOptionItem>(options[0]);
29
+ return (
30
+ <div
31
+ style={{
32
+ display: 'flex',
33
+ flexDirection: 'column',
34
+ justifyContent: 'flex-end',
35
+ minHeight: '50vh',
36
+ }}
37
+ >
38
+ <Select
39
+ id="flip-test-select"
40
+ size="md"
41
+ block
42
+ options={options}
43
+ selected={selected}
44
+ onChange={(option) => {
45
+ if (option && 'value' in option) {
46
+ setSelected(option as SelectOptionItem);
47
+ }
48
+ }}
49
+ />
50
+ </div>
51
+ );
52
+ },
53
+ play: async ({ canvasElement }) => {
54
+ const canvas = within(canvasElement);
55
+ const trigger = canvas.getByRole('button', { name: /Option 1/i });
56
+ await userEvent.click(trigger);
57
+ },
58
+ ...withVariantConfig(['default']),
59
+ };
@@ -608,8 +608,8 @@ export default function Select({
608
608
  ) : (
609
609
  <Panel
610
610
  open={open}
611
- flip={false}
612
611
  altAxis
612
+ considerHeight
613
613
  anchorRef={selectReference}
614
614
  anchorWidth={isDropdownAutoWidth}
615
615
  position={dropdownUp ? Position.TOP : Position.BOTTOM}
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
- import { Person as ProfileIcon } from '@transferwise/icons';
3
+ import { Person } from '@transferwise/icons';
4
4
 
5
5
  import Option, { Props as OptionProps } from './Option';
6
6
 
@@ -31,7 +31,7 @@ describe('Option', () => {
31
31
  });
32
32
 
33
33
  it('should render the given icon', () => {
34
- renderOption({ icon: <ProfileIcon /> });
34
+ renderOption({ icon: <Person /> });
35
35
  const optionElement = screen.getByTestId('test-option');
36
36
  expect(optionElement.querySelector('svg')).toBeInTheDocument();
37
37
  });
@@ -43,7 +43,7 @@ describe('Option', () => {
43
43
  });
44
44
 
45
45
  it('should override the currency flag with the given icon', () => {
46
- renderOption({ currency: 'USD', icon: <ProfileIcon /> });
46
+ renderOption({ currency: 'USD', icon: <Person /> });
47
47
  const optionElement = screen.getByTestId('test-option');
48
48
  expect(optionElement.querySelector('svg')).toBeInTheDocument();
49
49
  expect(optionElement.querySelectorAll('svg')).toHaveLength(1);
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { action } from 'storybook/actions';
3
- import { Home as HomeIcon } from '@transferwise/icons';
3
+ import { House } from '@transferwise/icons';
4
4
  import { Status } from '../common';
5
5
  import { InfoPresentation } from '../info';
6
6
  import Summary, { type Props as SummaryProps } from './Summary';
@@ -32,7 +32,7 @@ export const Basic = (args: SummaryProps) => {
32
32
  }}
33
33
  as="li"
34
34
  description={description}
35
- icon={<HomeIcon size={24} />}
35
+ icon={<House size={24} />}
36
36
  title="Verify your address"
37
37
  status={Status.NOT_DONE}
38
38
  />
@@ -51,7 +51,7 @@ export const Basic = (args: SummaryProps) => {
51
51
  'aria-label': 'Please click here to know more about your address update status',
52
52
  onClick: action('Summary Info clicked'),
53
53
  }}
54
- icon={<HomeIcon size={24} />}
54
+ icon={<House size={24} />}
55
55
  title="You verified your address"
56
56
  status={Status.DONE}
57
57
  />
@@ -72,7 +72,7 @@ export const Basic = (args: SummaryProps) => {
72
72
  'aria-label': 'Please click here to know more about your address update status',
73
73
  onClick: action('Summary Info clicked'),
74
74
  }}
75
- icon={<HomeIcon size={24} />}
75
+ icon={<House size={24} />}
76
76
  title="We’re verifying your address"
77
77
  status={Status.PENDING}
78
78
  />
@@ -85,7 +85,7 @@ export const Basic = (args: SummaryProps) => {
85
85
  }}
86
86
  as="li"
87
87
  description={description}
88
- icon={<HomeIcon size={24} />}
88
+ icon={<House size={24} />}
89
89
  title="We’re verifying your address"
90
90
  status={Status.PENDING}
91
91
  />
@@ -1,4 +1,4 @@
1
- import { Home as HomeIcon } from '@transferwise/icons';
1
+ import { House } from '@transferwise/icons';
2
2
  import { Status } from '../common';
3
3
  import Summary, { type SummaryProps } from '.';
4
4
 
@@ -16,7 +16,7 @@ export const LongText = () => {
16
16
  'aria-label': ' Click here to change address',
17
17
  },
18
18
  as: 'li',
19
- icon: <HomeIcon size={24} />,
19
+ icon: <House size={24} />,
20
20
  status: Status.NOT_DONE,
21
21
  info: {
22
22
  title: 'Title',
@@ -1,17 +1,22 @@
1
- /* eslint-disable @typescript-eslint/ban-ts-comment */
1
+ import {
2
+ arrow as arrowMiddleware,
3
+ autoUpdate,
4
+ flip,
5
+ offset,
6
+ useFloating,
7
+ } from '@floating-ui/react';
2
8
  import { clsx } from 'clsx';
3
9
  import {
4
10
  AriaAttributes,
11
+ CSSProperties,
5
12
  PropsWithChildren,
6
13
  ReactElement,
7
14
  ReactNode,
8
15
  cloneElement,
9
- useEffect,
10
16
  useId,
11
17
  useRef,
12
18
  useState,
13
19
  } from 'react';
14
- import { usePopper } from 'react-popper';
15
20
 
16
21
  import { CommonProps, Position } from '../common';
17
22
  import {
@@ -36,48 +41,30 @@ const Tooltip = ({
36
41
  className,
37
42
  }: TooltipProps) => {
38
43
  const [open, setOpen] = useState(false);
39
- const anchorReference = useRef(null);
40
- const [arrowElement, setArrowElement] = useState(null);
41
- const [popperElement, setPopperElement] = useState(null);
44
+ const arrowRef = useRef<HTMLDivElement | null>(null);
42
45
 
43
46
  const fallbackId = useId();
44
47
  const tooltipId = id ?? fallbackId;
45
- const modifiers = [];
46
-
47
- modifiers.push({
48
- name: 'arrow',
49
- options: {
50
- element: arrowElement,
51
- options: {
52
- padding: 8, // 8px from the edges of the popper
53
- },
54
- },
55
- });
56
- // This lets you displace a popper element from its reference element.
57
- modifiers.push({ name: 'offset', options: { offset: [0, 16] } });
58
- modifiers.push({
59
- name: 'flip',
60
- options: {
61
- fallbackPlacements: Position.TOP,
62
- },
63
- });
64
48
 
65
- const { styles, attributes, forceUpdate } = usePopper(anchorReference.current, popperElement, {
49
+ const { refs, floatingStyles, placement, middlewareData } = useFloating({
66
50
  placement: position,
67
- modifiers,
51
+ middleware: [
52
+ offset(16),
53
+ flip({ fallbackPlacements: [Position.TOP] }),
54
+ arrowMiddleware({ element: arrowRef, padding: 8 }),
55
+ ],
56
+ whileElementsMounted: open ? autoUpdate : undefined,
57
+ open,
68
58
  });
69
59
 
70
- // If the trigger is not visible when the position is calculated, it will be incorrect. Because this can happen repeatedly (on resize for example),
71
- // it is most simple just to always position before opening
72
- useEffect(() => {
73
- if (open && forceUpdate) {
74
- forceUpdate();
75
- }
76
- }, [open]);
60
+ const arrowStyle: CSSProperties = {
61
+ ...(middlewareData.arrow?.x != null ? { left: middlewareData.arrow.x } : {}),
62
+ ...(middlewareData.arrow?.y != null ? { top: middlewareData.arrow.y } : {}),
63
+ };
77
64
 
78
65
  return (
79
66
  <span
80
- ref={anchorReference}
67
+ ref={refs.setReference}
81
68
  className="tw-tooltip-container"
82
69
  onMouseOver={() => setOpen(true)}
83
70
  onFocus={() => setOpen(true)}
@@ -90,28 +77,22 @@ const Tooltip = ({
90
77
  })
91
78
  : null}
92
79
  <div
93
- // @ts-expect-error
94
- ref={setPopperElement}
80
+ ref={refs.setFloating}
95
81
  className={clsx(
96
82
  'np-tooltip',
97
83
  'np-panel',
98
84
  open ? `np-panel--open np-tooltip--open` : null,
99
85
  className,
100
86
  )}
101
- style={{ ...styles.popper }}
102
- {...attributes.popper}
87
+ style={floatingStyles}
88
+ data-popper-placement={placement}
103
89
  aria-hidden={!open}
104
90
  role="tooltip"
105
91
  id={`${tooltipId}-tooltip`}
106
92
  >
107
93
  <div className="np-panel__content tooltip-inner">
108
94
  {label}
109
- <div
110
- // @ts-expect-error
111
- ref={setArrowElement}
112
- className={clsx('np-panel__arrow')}
113
- style={styles.arrow}
114
- />
95
+ <div ref={arrowRef} className={clsx('np-panel__arrow')} style={arrowStyle} />
115
96
  </div>
116
97
  </div>
117
98
  </span>