@transferwise/components 46.139.0 → 46.140.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 (35) hide show
  1. package/build/dateLookup/DateLookup.js +1 -1
  2. package/build/dateLookup/DateLookup.js.map +1 -1
  3. package/build/dateLookup/DateLookup.mjs +1 -1
  4. package/build/dateLookup/DateLookup.mjs.map +1 -1
  5. package/build/main.css +64 -323
  6. package/build/popover/Popover.js +2 -2
  7. package/build/popover/Popover.js.map +1 -1
  8. package/build/popover/Popover.mjs +2 -2
  9. package/build/popover/Popover.mjs.map +1 -1
  10. package/build/select/Select.js +0 -3
  11. package/build/select/Select.js.map +1 -1
  12. package/build/select/Select.mjs +0 -3
  13. package/build/select/Select.mjs.map +1 -1
  14. package/build/styles/css/neptune.css +58 -317
  15. package/build/styles/less/neptune-tokens.less +2 -2
  16. package/build/styles/main.css +64 -323
  17. package/build/styles/props/neptune-tokens.css +1 -1
  18. package/build/styles/styles/less/core/viewport-themes.css +46 -42
  19. package/build/styles/styles/less/neptune.css +58 -317
  20. package/build/types/select/Select.d.ts.map +1 -1
  21. package/package.json +2 -2
  22. package/src/dateLookup/DateLookup.test.story.tsx +16 -0
  23. package/src/dateLookup/DateLookup.tsx +1 -1
  24. package/src/inputs/SelectInput/_stories/SelectInput.test.story.tsx +82 -0
  25. package/src/main.css +64 -323
  26. package/src/modal/Modal.test.story.tsx +61 -0
  27. package/src/popover/Popover.story.tsx +45 -0
  28. package/src/popover/Popover.test.story.tsx +124 -0
  29. package/src/popover/Popover.test.tsx +55 -0
  30. package/src/popover/Popover.tsx +2 -2
  31. package/src/select/Select.test.story.tsx +74 -1
  32. package/src/select/Select.tsx +0 -3
  33. package/src/styles/less/core/viewport-themes.css +46 -42
  34. package/src/styles/less/core/viewport-themes.less +2 -45
  35. package/src/styles/less/neptune.css +58 -317
@@ -0,0 +1,124 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { expect, userEvent, within } from 'storybook/test';
3
+
4
+ import { withVariantConfig } from '../../.storybook/helpers';
5
+ import { Button } from '..';
6
+ import { Position } from '../common';
7
+
8
+ import Popover, { PopoverPreferredPlacement } from './Popover';
9
+
10
+ const meta = {
11
+ title: 'Dialogs/Popover/Tests',
12
+ component: Popover,
13
+ tags: ['!autodocs', '!manifest'],
14
+ } satisfies Meta<typeof Popover>;
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof Popover>;
18
+
19
+ const wait = async (duration = 500) =>
20
+ new Promise<void>((resolve) => {
21
+ setTimeout(resolve, duration);
22
+ });
23
+
24
+ const createPlacementStory = (
25
+ placement: PopoverPreferredPlacement,
26
+ containerStyle?: React.CSSProperties,
27
+ ): Story => ({
28
+ parameters: {
29
+ chromatic: {
30
+ delay: 1000,
31
+ },
32
+ },
33
+ render: () => (
34
+ <div
35
+ style={
36
+ containerStyle
37
+ ? { position: 'absolute', ...containerStyle }
38
+ : { display: 'flex', justifyContent: 'center', padding: '150px' }
39
+ }
40
+ >
41
+ <Popover preferredPlacement={placement} title="Info" content="Details">
42
+ <Button v2>{placement}</Button>
43
+ </Popover>
44
+ </div>
45
+ ),
46
+ play: async ({ canvasElement }) => {
47
+ const canvas = within(canvasElement);
48
+ await userEvent.click(canvas.getByRole('button'));
49
+ },
50
+ });
51
+
52
+ /**
53
+ * This test insures that there's no unintended page scroll after the popover
54
+ * is closed without selecting an option (e.g. clicking outside).
55
+ * It tests 2 scenarios:
56
+ * 1. when the popover fits naturally on the screen below the trigger
57
+ * 2. when there's not enough space below the trigger and it renders above it
58
+ */
59
+ const generateNoScrollTest = (inverted?: boolean): Story => ({
60
+ render: function Render() {
61
+ return (
62
+ <div>
63
+ <style>
64
+ {`
65
+ .storybook-container{
66
+ min-height: unset;
67
+ height: unset;
68
+ }
69
+ `}
70
+ </style>
71
+ {inverted ? <p style={{ height: '70vh', background: 'blanchedalmond' }} /> : null}
72
+
73
+ <Popover
74
+ content="Popover content for testing"
75
+ title="Popover Title"
76
+ preferredPlacement="top"
77
+ >
78
+ <Button v2>Open Popover</Button>
79
+ </Popover>
80
+
81
+ <p style={{ margin: '1rem 0', height: '150vh', background: 'blanchedalmond' }} />
82
+ </div>
83
+ );
84
+ },
85
+ play: async ({ canvasElement, step }) => {
86
+ const canvas = within(canvasElement);
87
+ const trigger = canvas.getByRole('button', { name: /Open Popover/i });
88
+
89
+ await step('Open the popover', async () => {
90
+ await userEvent.click(trigger);
91
+ await wait();
92
+ });
93
+
94
+ await step('Close the popover with Escape', async () => {
95
+ await userEvent.keyboard('{Escape}');
96
+ await wait();
97
+ });
98
+
99
+ await step('Verify the page has not scrolled', async () => {
100
+ const scrollParent = canvasElement.ownerDocument.documentElement;
101
+ await expect(scrollParent.scrollTop).toBe(0);
102
+ });
103
+ },
104
+ });
105
+
106
+ export const PlacementTop: Story = createPlacementStory(Position.TOP);
107
+ export const PlacementRight: Story = createPlacementStory(Position.RIGHT);
108
+ export const PlacementBottom: Story = createPlacementStory(Position.BOTTOM);
109
+ export const PlacementLeft: Story = createPlacementStory(Position.LEFT);
110
+
111
+ export const FlipTop: Story = createPlacementStory(Position.TOP, { top: 0, left: '50%' });
112
+ export const FlipRight: Story = createPlacementStory(Position.RIGHT, { top: '50%', right: 0 });
113
+ export const FlipBottom: Story = createPlacementStory(Position.BOTTOM, { bottom: 0, left: '50%' });
114
+ export const FlipLeft: Story = createPlacementStory(Position.LEFT, { top: '50%', left: 0 });
115
+
116
+ export const NoScrollAfterCloseTop: Story = {
117
+ ...generateNoScrollTest(),
118
+ ...withVariantConfig(['default']),
119
+ };
120
+
121
+ export const NoScrollAfterCloseBottom: Story = {
122
+ ...generateNoScrollTest(true),
123
+ ...withVariantConfig(['default']),
124
+ };
@@ -142,6 +142,61 @@ describe('Popover', () => {
142
142
  expect(handleOnClose).toHaveBeenCalledTimes(1);
143
143
  });
144
144
 
145
+ describe('className', () => {
146
+ describe('on desktop', () => {
147
+ beforeEach(() => {
148
+ global.innerWidth = Breakpoint.SMALL;
149
+ });
150
+
151
+ it('applies className to the panel container', async () => {
152
+ render(
153
+ <Popover {...props} className="custom-popover-class">
154
+ <button type="button">Open</button>
155
+ </Popover>,
156
+ );
157
+
158
+ await userEvent.click(screen.getByText('Open'));
159
+ await waitForPanel();
160
+
161
+ const panel = getPanel();
162
+ expect(panel).toHaveClass('np-popover__container');
163
+ expect(panel).toHaveClass('custom-popover-class');
164
+ });
165
+ });
166
+
167
+ describe('on mobile', () => {
168
+ beforeEach(() => {
169
+ global.innerWidth = Breakpoint.SMALL - 1;
170
+ });
171
+
172
+ it('applies className to the bottom sheet container', async () => {
173
+ render(
174
+ <Popover {...props} className="custom-popover-class">
175
+ <button type="button">Open</button>
176
+ </Popover>,
177
+ );
178
+
179
+ await userEvent.click(screen.getByText('Open'));
180
+
181
+ const bottomSheet = getBottomSheet();
182
+ expect(bottomSheet).toHaveClass('np-popover__container');
183
+ expect(bottomSheet).toHaveClass('custom-popover-class');
184
+ });
185
+ });
186
+
187
+ it('does not apply className to the trigger wrapper', () => {
188
+ const { container } = render(
189
+ <Popover {...props} className="custom-popover-class">
190
+ <button type="button">Open</button>
191
+ </Popover>,
192
+ );
193
+
194
+ const wrapper = container.firstChild;
195
+ expect(wrapper).toHaveClass('np-popover');
196
+ expect(wrapper).not.toHaveClass('custom-popover-class');
197
+ });
198
+ });
199
+
145
200
  const waitForPanel = async () => screen.findByText('content');
146
201
  const getDimmer = () => document.querySelector('.dimmer');
147
202
  const getPanel = () => document.querySelector('.np-panel');
@@ -75,7 +75,7 @@ export default function Popover({
75
75
  };
76
76
 
77
77
  return (
78
- <span className={clsx('np-popover', className)}>
78
+ <span className="np-popover">
79
79
  <span ref={anchorReference} className="d-inline-block">
80
80
  {isValidElement<{ onClick?: () => void }>(children)
81
81
  ? cloneElement(children, {
@@ -93,7 +93,7 @@ export default function Popover({
93
93
  anchorRef={anchorReference}
94
94
  position={resolvedPlacement}
95
95
  arrow
96
- className="np-popover__container"
96
+ className={clsx('np-popover__container', className)}
97
97
  onClose={handleOnClose}
98
98
  >
99
99
  <div className="np-popover__content np-text-default-body">
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
- import { userEvent, within } from 'storybook/test';
2
+ import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
3
3
  import { useState } from 'react';
4
4
 
5
5
  import { withVariantConfig } from '../../.storybook/helpers';
@@ -19,6 +19,11 @@ const options: SelectOptionItem[] = Array.from({ length: 10 }, (_, i) => ({
19
19
  label: `Option ${i + 1}`,
20
20
  }));
21
21
 
22
+ const wait = async (duration = 500) =>
23
+ new Promise<void>((resolve) => {
24
+ setTimeout(resolve, duration);
25
+ });
26
+
22
27
  /**
23
28
  * Visual regression story: Select placed at the bottom of the viewport so the dropdown
24
29
  * flips to the top when opened. The play function opens the dropdown automatically.
@@ -57,3 +62,71 @@ export const DropdownFlipsToTop: Story = {
57
62
  },
58
63
  ...withVariantConfig(['default']),
59
64
  };
65
+
66
+ /**
67
+ * This test insures that there's no unintended page scroll after the dropdown
68
+ * is closed without selecting an option (e.g. clicking outside).
69
+ * It tests 2 scenarios:
70
+ * 1. when the dropdown fits naturally on the screen below the trigger
71
+ * 2. when there's not enough space below the trigger and it renders above it
72
+ */
73
+ const generateNoScrollTest = (inverted?: boolean): Story => ({
74
+ render: function Render() {
75
+ const [selected, setSelected] = useState<SelectOptionItem>(options[0]);
76
+ return (
77
+ <div>
78
+ <style>
79
+ {`
80
+ .storybook-container{
81
+ min-height: unset;
82
+ height: unset;
83
+ }
84
+ `}
85
+ </style>
86
+ {inverted ? <p style={{ height: '70vh', background: 'blanchedalmond' }} /> : null}
87
+
88
+ <Select
89
+ id="flip-test-select"
90
+ size="md"
91
+ block
92
+ options={options}
93
+ selected={selected}
94
+ onChange={(option) => {
95
+ if (option && 'value' in option) {
96
+ setSelected(option as SelectOptionItem);
97
+ }
98
+ }}
99
+ />
100
+
101
+ <p style={{ margin: '1rem 0', height: '150vh', background: 'blanchedalmond' }} />
102
+ </div>
103
+ );
104
+ },
105
+ play: async ({ canvasElement, step }) => {
106
+ const canvas = within(canvasElement);
107
+ const trigger = canvas.getByRole('button', { name: /Option 1/i });
108
+
109
+ await step('Open the combobox', async () => {
110
+ await userEvent.click(trigger);
111
+ });
112
+
113
+ await step('Check if options are displayed', async () => {
114
+ await waitFor(async () =>
115
+ expect((await screen.findAllByRole('option')).length).toBeGreaterThan(0),
116
+ );
117
+ });
118
+
119
+ await step('Close the combobox', async () => {
120
+ await userEvent.keyboard('{Escape}');
121
+ await wait();
122
+ });
123
+
124
+ await step('Verify the page has not scrolled', async () => {
125
+ const scrollParent = canvasElement.ownerDocument.documentElement;
126
+ await expect(scrollParent.scrollTop).toBe(0);
127
+ });
128
+ },
129
+ });
130
+
131
+ export const NoScrollAfterCloseTop: Story = generateNoScrollTest();
132
+ export const NoScrollAfterCloseBottom: Story = generateNoScrollTest(true);
@@ -360,9 +360,6 @@ export default function Select({
360
360
  const handleCloseOptions = () => {
361
361
  setOpen(false);
362
362
  setKeyboardFocusedOptionIndex(-1);
363
- if (dropdownButtonReference.current) {
364
- dropdownButtonReference.current.focus();
365
- }
366
363
  };
367
364
 
368
365
  function createSelectHandlerForOption(option: SelectItemWithPlaceholder) {
@@ -1,47 +1,51 @@
1
1
  @media (max-width: 320px) {
2
2
  .np-theme-personal {
3
- --delta: 2;
4
- --size-4: calc(4px / var(--delta));
5
- --size-5: calc(5px / var(--delta));
6
- --size-8: calc(8px / var(--delta));
7
- --size-10: calc(10px / var(--delta));
8
- --size-12: calc(12px / var(--delta));
9
- --size-14: calc(14px / var(--delta));
10
- --size-16: calc(16px / var(--delta));
11
- --size-24: calc(24px / var(--delta));
12
- --size-32: calc(32px / var(--delta));
13
- --size-40: calc(40px / var(--delta));
14
- --size-48: calc(48px / var(--delta));
15
- --size-52: calc(52px / var(--delta));
16
- --size-56: calc(56px / var(--delta));
17
- --size-60: calc(60px / var(--delta));
18
- --size-64: calc(64px / var(--delta));
19
- --size-72: calc(72px / var(--delta));
20
- --size-80: calc(80px / var(--delta));
21
- --size-88: calc(88px / var(--delta));
22
- --size-96: calc(96px / var(--delta));
23
- --size-104: calc(104px / var(--delta));
24
- --size-112: calc(112px / var(--delta));
25
- --size-120: calc(120px / var(--delta));
26
- --size-126: calc(126px / var(--delta));
27
- --size-128: calc(128px / var(--delta));
28
- --size-146: calc(146px / var(--delta));
29
- --size-154: calc(154px / var(--delta));
30
- --size-x-small: calc(24px / var(--delta));
31
- --size-small: calc(32px / var(--delta));
32
- --size-medium: calc(40px / var(--delta));
33
- --size-large: calc(48px / var(--delta));
34
- --size-x-large: calc(56px / var(--delta));
35
- --size-2x-large: calc(72px / var(--delta));
36
- --space-content-horizontal: calc(16px / var(--delta));
37
- --space-small: calc(16px / var(--delta));
38
- --space-medium: calc(32px / var(--delta));
39
- --space-large: calc(40px / var(--delta));
40
- --space-x-large: calc(56px / var(--delta));
41
- --padding-x-small: var(--size-8);
42
- --padding-small: var(--size-16);
43
- --padding-medium: var(--size-24);
44
- --padding-large: var(--size-32);
3
+ --padding-x-small: 4px;
4
+ --padding-small: 8px;
5
+ --padding-medium: 12px;
6
+ --padding-large: 16px;
7
+ --size-4: 2px;
8
+ --size-5: 2.5px;
9
+ --size-8: 4px;
10
+ --size-10: 5px;
11
+ --size-12: 6px;
12
+ --size-14: 7px;
13
+ --size-16: 8px;
14
+ --size-24: 12px;
15
+ --size-32: 16px;
16
+ --size-40: 20px;
17
+ --size-48: 24px;
18
+ --size-52: 26px;
19
+ --size-56: 28px;
20
+ --size-60: 30px;
21
+ --size-64: 32px;
22
+ --size-72: 36px;
23
+ --size-80: 40px;
24
+ --size-88: 44px;
25
+ --size-96: 48px;
26
+ --size-104: 52px;
27
+ --size-112: 56px;
28
+ --size-120: 60px;
29
+ --size-126: 63px;
30
+ --size-128: 64px;
31
+ --size-146: 73px;
32
+ --size-154: 77px;
33
+ --size-160: 80px;
34
+ --size-x-small: 12px;
35
+ --size-small: 16px;
36
+ --size-medium: 20px;
37
+ --size-large: 24px;
38
+ --size-x-large: 28px;
39
+ --size-2x-large: 36px;
40
+ --space-content-horizontal: 8px;
41
+ --space-small: 8px;
42
+ --space-medium: 16px;
43
+ --space-large: 20px;
44
+ --space-x-large: 28px;
45
+ }
46
+ }
47
+ @media (max-width: 320px) {
48
+ .np-theme-personal {
45
49
  --input-height-base: var(--size-32);
46
50
  --input-height-large: var(--input-height-small);
47
51
  --input-padding: var(--input-padding-small);
@@ -1,51 +1,8 @@
1
- @import '@transferwise/neptune-tokens/themes/personal/tokens.less';
1
+ @import "@transferwise/neptune-tokens/themes/personal/tokens.less";
2
+ @import "@transferwise/neptune-tokens/tiny-viewport.less";
2
3
 
3
4
  @media (--screen-400-zoom) {
4
5
  .np-theme-personal {
5
- --delta: 2;
6
-
7
- --size-4: calc(@size-4 / var(--delta));
8
- --size-5: calc(@size-5 / var(--delta));
9
- --size-8: calc(@size-8 / var(--delta));
10
- --size-10: calc(@size-10 / var(--delta));
11
- --size-12: calc(@size-12 / var(--delta));
12
- --size-14: calc(@size-14 / var(--delta));
13
- --size-16: calc(@size-16 / var(--delta));
14
- --size-24: calc(@size-24 / var(--delta));
15
- --size-32: calc(@size-32 / var(--delta));
16
- --size-40: calc(@size-40 / var(--delta));
17
- --size-48: calc(@size-48 / var(--delta));
18
- --size-52: calc(@size-52 / var(--delta));
19
- --size-56: calc(@size-56 / var(--delta));
20
- --size-60: calc(@size-60 / var(--delta));
21
- --size-64: calc(@size-64 / var(--delta));
22
- --size-72: calc(@size-72 / var(--delta));
23
- --size-80: calc(@size-80 / var(--delta));
24
- --size-88: calc(@size-88 / var(--delta));
25
- --size-96: calc(@size-96 / var(--delta));
26
- --size-104: calc(@size-104 / var(--delta));
27
- --size-112: calc(@size-112 / var(--delta));
28
- --size-120: calc(@size-120 / var(--delta));
29
- --size-126: calc(@size-126 / var(--delta));
30
- --size-128: calc(@size-128 / var(--delta));
31
- --size-146: calc(@size-146 / var(--delta));
32
- --size-154: calc(@size-154 / var(--delta));
33
- --size-x-small: calc(@size-x-small / var(--delta));
34
- --size-small: calc(@size-small / var(--delta));
35
- --size-medium: calc(@size-medium / var(--delta));
36
- --size-large: calc(@size-large / var(--delta));
37
- --size-x-large: calc(@size-x-large / var(--delta));
38
- --size-2x-large: calc(@size-2x-large / var(--delta));
39
- --space-content-horizontal: calc(@space-content-horizontal / var(--delta));
40
- --space-small: calc(@space-small / var(--delta));
41
- --space-medium: calc(@space-medium / var(--delta));
42
- --space-large: calc(@space-large / var(--delta));
43
- --space-x-large: calc(@space-x-large / var(--delta));
44
- --padding-x-small: var(--size-8);
45
- --padding-small: var(--size-16);
46
- --padding-medium: var(--size-24);
47
- --padding-large: var(--size-32);
48
-
49
6
  --input-height-base: var(--size-32);
50
7
  --input-height-large: var(--input-height-small);
51
8
  --input-padding: var(--input-padding-small);