@transferwise/components 0.0.0-experimental-9442a3d → 0.0.0-experimental-6fb3b67

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 (33) hide show
  1. package/build/dateInput/DateInput.js +2 -2
  2. package/build/dateInput/DateInput.js.map +1 -1
  3. package/build/dateInput/DateInput.mjs +2 -2
  4. package/build/dateInput/DateInput.mjs.map +1 -1
  5. package/build/inputs/_BottomSheet.js +29 -1
  6. package/build/inputs/_BottomSheet.js.map +1 -1
  7. package/build/inputs/_BottomSheet.mjs +30 -2
  8. package/build/inputs/_BottomSheet.mjs.map +1 -1
  9. package/build/main.css +20 -5
  10. package/build/phoneNumberInput/PhoneNumberInput.js +1 -1
  11. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  12. package/build/phoneNumberInput/PhoneNumberInput.mjs +1 -1
  13. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  14. package/build/styles/inputs/Input.css +5 -0
  15. package/build/styles/inputs/SelectInput.css +15 -5
  16. package/build/styles/inputs/TextArea.css +5 -0
  17. package/build/styles/main.css +20 -5
  18. package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
  19. package/package.json +5 -5
  20. package/src/DisabledComponents.story.tsx +156 -0
  21. package/src/common/bottomSheet/BottomSheet.test.story.tsx +94 -0
  22. package/src/dateInput/DateInput.tsx +2 -2
  23. package/src/inputs/Input.css +5 -0
  24. package/src/inputs/SelectInput.css +15 -5
  25. package/src/inputs/SelectInput.spec.tsx +33 -0
  26. package/src/inputs/SelectInput.test.story.tsx +83 -0
  27. package/src/inputs/TextArea.css +5 -0
  28. package/src/inputs/_BottomSheet.less +15 -3
  29. package/src/inputs/_BottomSheet.tsx +19 -3
  30. package/src/inputs/_common.less +5 -0
  31. package/src/main.css +20 -5
  32. package/src/moneyInput/MoneyInput.test.story.tsx +101 -0
  33. package/src/phoneNumberInput/PhoneNumberInput.tsx +1 -1
@@ -291,7 +291,7 @@ const DateInput = ({
291
291
  <div className="col-sm-3 tw-date--day">
292
292
  <label>
293
293
  <Body type={Typography.BODY_DEFAULT}>{dayLabel}</Body>
294
- <div className={`input-group input-group-${size}`}>
294
+ <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
295
295
  <Input
296
296
  ref={dayRef}
297
297
  id={`${id}:day`}
@@ -319,7 +319,7 @@ const DateInput = ({
319
319
  <div className="col-sm-4 tw-date--year">
320
320
  <label>
321
321
  <Body type={Typography.BODY_DEFAULT}>{yearLabel}</Body>
322
- <div className={`input-group input-group-${size}`}>
322
+ <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
323
323
  <Input
324
324
  ref={yearRef}
325
325
  id={`${id}:year`}
@@ -18,6 +18,11 @@
18
18
  transition-duration: 300ms;
19
19
  /* TODO: Remove these overrides once `.form-control` isn’t used anymore */
20
20
  }
21
+ .disabled .np-form-control,
22
+ :disabled .np-form-control {
23
+ opacity: 1;
24
+ opacity: initial;
25
+ }
21
26
  .np-form-control:focus-visible {
22
27
  outline: none;
23
28
  }
@@ -1,3 +1,10 @@
1
+ .wds-select-input-scroll-freeze {
2
+ scroll-behavior: unset !important;
3
+ height: 100vh;
4
+ }
5
+ .wds-select-input-scroll-freeze body {
6
+ height: 100vh;
7
+ }
1
8
  .np-bottom-sheet-v2-container {
2
9
  position: relative;
3
10
  z-index: 1060;
@@ -11,6 +18,8 @@
11
18
  transition-property: opacity;
12
19
  transition-timing-function: ease-out;
13
20
  transition-duration: 300ms;
21
+ will-change: transform;
22
+ min-height: 100vh;
14
23
  }
15
24
  .np-bottom-sheet-v2-backdrop--closed {
16
25
  opacity: 0;
@@ -18,7 +27,7 @@
18
27
  .np-bottom-sheet-v2 {
19
28
  position: fixed;
20
29
  inset: 0px;
21
- bottom: env(keyboard-inset-height, 0px);
30
+ bottom: env(safe-area-inset-bottom, 0);
22
31
  margin-left: 8px;
23
32
  margin-left: var(--size-8);
24
33
  margin-right: 8px;
@@ -28,18 +37,19 @@
28
37
  display: flex;
29
38
  flex-direction: column;
30
39
  justify-content: flex-end;
40
+ height: calc(100vh - 64px - 40px);
41
+ height: calc(100vh - var(--size-64) - 40px);
31
42
  }
32
43
  .np-bottom-sheet-v2-content {
33
44
  display: flex;
34
45
  flex-direction: column;
35
46
  overflow: auto;
36
- border-top-left-radius: 32px;
37
- /* TODO: Tokenize */
38
- border-top-right-radius: 32px;
39
- /* TODO: Tokenize */
47
+ border-radius: 32px;
48
+ border-radius: var(--size-32);
40
49
  background-color: #ffffff;
41
50
  background-color: var(--color-background-elevated);
42
51
  box-shadow: 0 0 40px rgba(69, 71, 69, 0.2);
52
+ will-change: transform;
43
53
  }
44
54
  .np-bottom-sheet-v2-content:focus {
45
55
  outline: none;
@@ -3,13 +3,21 @@ import { userEvent } from '@testing-library/user-event';
3
3
 
4
4
  import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
5
5
 
6
+ import { useScreenSize } from '../common/hooks/useScreenSize';
6
7
  import { SelectInput, type SelectInputOptionItem, type SelectInputProps } from './SelectInput';
7
8
  import { Field } from '../field/Field';
8
9
 
9
10
  mockMatchMedia();
10
11
  mockResizeObserver();
12
+ jest.mock('../common/hooks/useScreenSize');
11
13
 
12
14
  describe('SelectInput', () => {
15
+ beforeEach(() => {
16
+ (useScreenSize as jest.Mock).mockReturnValue(true);
17
+ });
18
+
19
+ afterEach(jest.clearAllMocks);
20
+
13
21
  it('renders placeholder', () => {
14
22
  render(
15
23
  <SelectInput
@@ -309,4 +317,29 @@ describe('SelectInput', () => {
309
317
  expect(listBox).not.toHaveAttribute('aria-labelledby');
310
318
  });
311
319
  });
320
+
321
+ describe('smooth scroll reset', () => {
322
+ const options: SelectInputOptionItem[] = [
323
+ { type: 'option', value: 'Banana' },
324
+ { type: 'option', value: 'Orange' },
325
+ { type: 'option', value: 'Olive' },
326
+ ];
327
+
328
+ beforeEach(() => {
329
+ (useScreenSize as jest.Mock).mockReturnValue(false);
330
+ });
331
+
332
+ afterEach(jest.clearAllMocks);
333
+
334
+ it('toggles scroll-resetting class', async () => {
335
+ render(<SelectInput items={options} />);
336
+ expect(document.documentElement).not.toHaveClass('wds-select-input-scroll-freeze');
337
+
338
+ await userEvent.click(screen.getByRole('combobox'));
339
+ expect(document.documentElement).toHaveClass('wds-select-input-scroll-freeze');
340
+
341
+ await userEvent.click(document.documentElement);
342
+ expect(document.documentElement).not.toHaveClass('wds-select-input-scroll-freeze');
343
+ });
344
+ });
312
345
  });
@@ -0,0 +1,83 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { fn, type Mock, userEvent, within } from 'storybook/test';
3
+ import { allModes } from '../../.storybook/modes';
4
+ import { lorem5, lorem500 } from '../test-utils';
5
+ import { Field } from '../field/Field';
6
+ import Body from '../body';
7
+ import { SelectInput, type SelectInputProps } from './SelectInput';
8
+
9
+ const meta = {
10
+ title: 'Forms/SelectInput/tests',
11
+ component: SelectInput,
12
+ args: {
13
+ onFilterChange: fn() satisfies Mock,
14
+ onChange: fn() satisfies Mock,
15
+ onClose: fn() satisfies Mock,
16
+ onOpen: fn() satisfies Mock,
17
+ },
18
+ tags: ['!autodocs'],
19
+ } satisfies Meta<typeof SelectInput>;
20
+ export default meta;
21
+
22
+ type Story<T, M extends boolean = false> = StoryObj<SelectInputProps<T, M>>;
23
+
24
+ const wait = async (duration = 500) =>
25
+ new Promise<void>((resolve) => {
26
+ setTimeout(resolve, duration);
27
+ });
28
+
29
+ /**
30
+ * This test ensures that when the SelectInput is used within a scrollable page,
31
+ * opening the dropdown does not cause any unwanted scrolling or layout shifts.
32
+ * Expected preview should start with green bar at the top, with yellow section
33
+ * not in the viewport.
34
+ *
35
+ * NB: This test is disabled in Chromatic as there's no obvious way to control <html/> element of a snapshot.
36
+ */
37
+ export const SmoothScrollReset: Story<string> = {
38
+ args: {
39
+ items: Array.from({ length: 15 }).map((_, id) => ({
40
+ type: 'option',
41
+ value: `option ${id + 1}`,
42
+ })),
43
+ placeholder: 'Select option',
44
+ },
45
+ decorators: [
46
+ (Story) => (
47
+ <>
48
+ <style>{`html { scroll-behavior: smooth; }`}</style>
49
+ <div>
50
+ <div
51
+ className="d-flex align-items-center justify-content-center"
52
+ style={{
53
+ backgroundColor: 'var(--color-bright-yellow)',
54
+ height: 400,
55
+ }}
56
+ >
57
+ This block should not be in the viewport.
58
+ </div>
59
+ <div style={{ height: 10, backgroundColor: 'var(--color-bright-green)' }} />
60
+ <Field id="el1" label={lorem5}>
61
+ <Story />
62
+ </Field>
63
+ <Body as="p">{lorem500}</Body>
64
+ </div>
65
+ </>
66
+ ),
67
+ ],
68
+ play: async ({ canvasElement }) => {
69
+ document.documentElement.scrollTop = 400;
70
+ await wait();
71
+ const canvas = within(canvasElement);
72
+ const triggerButton = canvas.getByRole('combobox');
73
+ await userEvent.click(triggerButton);
74
+ },
75
+ globals: {
76
+ viewport: { value: allModes.largeMobile.viewport, isRotated: false },
77
+ },
78
+ parameters: {
79
+ chromatic: {
80
+ disableSnapshot: true,
81
+ },
82
+ },
83
+ };
@@ -18,6 +18,11 @@
18
18
  transition-duration: 300ms;
19
19
  /* TODO: Remove these overrides once `.form-control` isn’t used anymore */
20
20
  }
21
+ .disabled .np-form-control,
22
+ :disabled .np-form-control {
23
+ opacity: 1;
24
+ opacity: initial;
25
+ }
21
26
  .np-form-control:focus-visible {
22
27
  outline: none;
23
28
  }
@@ -1,3 +1,12 @@
1
+ .wds-select-input-scroll-freeze {
2
+ scroll-behavior: unset !important;
3
+ height: 100vh;
4
+
5
+ body{
6
+ height: 100vh;
7
+ }
8
+ }
9
+
1
10
  .np-bottom-sheet-v2-container {
2
11
  position: relative;
3
12
  z-index: 1060;
@@ -11,6 +20,8 @@
11
20
  transition-property: opacity;
12
21
  transition-timing-function: ease-out;
13
22
  transition-duration: 300ms;
23
+ will-change: transform;
24
+ min-height: 100vh;
14
25
 
15
26
  &--closed {
16
27
  opacity: 0;
@@ -20,23 +31,24 @@
20
31
  .np-bottom-sheet-v2 {
21
32
  position: fixed;
22
33
  inset: 0px;
23
- bottom: env(keyboard-inset-height, 0px);
34
+ bottom: env(safe-area-inset-bottom, 0);
24
35
  margin-left: var(--size-8);
25
36
  margin-right: var(--size-8);
26
37
  margin-top: var(--size-64);
27
38
  display: flex;
28
39
  flex-direction: column;
29
40
  justify-content: flex-end;
41
+ height: calc(100vh - var(--size-64) - 40px);
30
42
  }
31
43
 
32
44
  .np-bottom-sheet-v2-content {
33
45
  display: flex;
34
46
  flex-direction: column;
35
47
  overflow: auto;
36
- border-top-left-radius: 32px; /* TODO: Tokenize */
37
- border-top-right-radius: 32px; /* TODO: Tokenize */
48
+ border-radius: var(--size-32);
38
49
  background-color: var(--color-background-elevated);
39
50
  box-shadow: 0 0 40px rgb(69 71 69 / 0.2);
51
+ will-change: transform;
40
52
 
41
53
  &:focus {
42
54
  outline: none;
@@ -10,12 +10,11 @@ import { Transition } 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 } from 'react';
13
+ import { Fragment, useEffect, useState } from 'react';
14
14
 
15
- import { CloseButton } from '../common/closeButton';
15
+ import { CloseButton , Size } from '../common';
16
16
  import { useVirtualKeyboard } from '../common/hooks/useVirtualKeyboard';
17
17
  import { PreventScroll } from '../common/preventScroll/PreventScroll';
18
- import { Size } from '../common/propsValues/size';
19
18
 
20
19
  export interface BottomSheetProps {
21
20
  open: boolean;
@@ -33,6 +32,19 @@ export interface BottomSheetProps {
33
32
  onCloseEnd?: () => void;
34
33
  }
35
34
 
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. It complements <PreventScroll />.
39
+ */
40
+ const freezeScroll = (shouldFreeze = true) => {
41
+ if (shouldFreeze) {
42
+ document.documentElement.classList.add('wds-select-input-scroll-freeze');
43
+ } else {
44
+ document.documentElement.classList.remove('wds-select-input-scroll-freeze');
45
+ }
46
+ };
47
+
36
48
  export function BottomSheet({
37
49
  open,
38
50
  renderTrigger,
@@ -54,6 +66,10 @@ export function BottomSheet({
54
66
  },
55
67
  });
56
68
 
69
+ useEffect(() => {
70
+ freezeScroll(open);
71
+ }, [open]);
72
+
57
73
  const dismiss = useDismiss(context);
58
74
  const role = useRole(context);
59
75
  const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, role]);
@@ -15,6 +15,11 @@
15
15
  transition-timing-function: ease-in-out;
16
16
  transition-duration: 300ms;
17
17
 
18
+ .disabled &,
19
+ :disabled & {
20
+ opacity: unset;
21
+ }
22
+
18
23
  &:focus-visible {
19
24
  outline: none;
20
25
  }
package/src/main.css CHANGED
@@ -2660,6 +2660,11 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2660
2660
  transition-duration: 300ms;
2661
2661
  /* TODO: Remove these overrides once `.form-control` isn’t used anymore */
2662
2662
  }
2663
+ .disabled .np-form-control,
2664
+ :disabled .np-form-control {
2665
+ opacity: 1;
2666
+ opacity: initial;
2667
+ }
2663
2668
  .np-form-control:focus-visible {
2664
2669
  outline: none;
2665
2670
  }
@@ -3519,6 +3524,13 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
3519
3524
  padding-inline-start: 8px;
3520
3525
  padding-inline-start: var(--size-8);
3521
3526
  }
3527
+ .wds-select-input-scroll-freeze {
3528
+ scroll-behavior: unset !important;
3529
+ height: 100vh;
3530
+ }
3531
+ .wds-select-input-scroll-freeze body {
3532
+ height: 100vh;
3533
+ }
3522
3534
  .np-bottom-sheet-v2-container {
3523
3535
  position: relative;
3524
3536
  z-index: 1060;
@@ -3532,6 +3544,8 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
3532
3544
  transition-property: opacity;
3533
3545
  transition-timing-function: ease-out;
3534
3546
  transition-duration: 300ms;
3547
+ will-change: transform;
3548
+ min-height: 100vh;
3535
3549
  }
3536
3550
  .np-bottom-sheet-v2-backdrop--closed {
3537
3551
  opacity: 0;
@@ -3539,7 +3553,7 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
3539
3553
  .np-bottom-sheet-v2 {
3540
3554
  position: fixed;
3541
3555
  inset: 0px;
3542
- bottom: env(keyboard-inset-height, 0px);
3556
+ bottom: env(safe-area-inset-bottom, 0);
3543
3557
  margin-left: 8px;
3544
3558
  margin-left: var(--size-8);
3545
3559
  margin-right: 8px;
@@ -3549,18 +3563,19 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
3549
3563
  display: flex;
3550
3564
  flex-direction: column;
3551
3565
  justify-content: flex-end;
3566
+ height: calc(100vh - 64px - 40px);
3567
+ height: calc(100vh - var(--size-64) - 40px);
3552
3568
  }
3553
3569
  .np-bottom-sheet-v2-content {
3554
3570
  display: flex;
3555
3571
  flex-direction: column;
3556
3572
  overflow: auto;
3557
- border-top-left-radius: 32px;
3558
- /* TODO: Tokenize */
3559
- border-top-right-radius: 32px;
3560
- /* TODO: Tokenize */
3573
+ border-radius: 32px;
3574
+ border-radius: var(--size-32);
3561
3575
  background-color: #ffffff;
3562
3576
  background-color: var(--color-background-elevated);
3563
3577
  box-shadow: 0 0 40px rgba(69, 71, 69, 0.2);
3578
+ will-change: transform;
3564
3579
  }
3565
3580
  .np-bottom-sheet-v2-content:focus {
3566
3581
  outline: none;
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { userEvent, within } from 'storybook/test';
3
+ import { allModes } from '../../.storybook/modes';
4
+ import { lorem500 } from '../test-utils';
5
+ import { Field } from '../field/Field';
6
+ import Body from '../body';
7
+ import MoneyInput from './MoneyInput';
8
+
9
+ const meta = {
10
+ title: 'Forms/MoneyInput/tests',
11
+ component: MoneyInput,
12
+ args: {
13
+ amount: 1000,
14
+ id: 'moneyInput',
15
+ currencies: [
16
+ {
17
+ value: 'EUR',
18
+ label: 'EUR',
19
+ note: 'Euro',
20
+ currency: 'eur',
21
+ searchable: 'Spain, Germany, France, Austria',
22
+ },
23
+ {
24
+ value: 'GBP',
25
+ label: 'GBP',
26
+ note: 'British pound',
27
+ currency: 'gbp',
28
+ searchable: 'England, Scotland, Wales',
29
+ },
30
+ ],
31
+ selectedCurrency: {
32
+ value: 'EUR',
33
+ label: 'EUR',
34
+ note: 'Euro',
35
+ currency: 'eur',
36
+ searchable: 'Spain, Germany, France, Austria',
37
+ },
38
+ searchPlaceholder: '',
39
+ onAmountChange: () => {},
40
+ onCurrencyChange: () => {},
41
+ },
42
+ tags: ['!autodocs'],
43
+ } satisfies Meta<typeof MoneyInput>;
44
+ export default meta;
45
+
46
+ type Story = StoryObj<typeof MoneyInput>;
47
+
48
+ const wait = async (duration = 500) =>
49
+ new Promise<void>((resolve) => {
50
+ setTimeout(resolve, duration);
51
+ });
52
+
53
+ /**
54
+ * This test ensures that when the SelectInput is used within a scrollable page,
55
+ * opening the dropdown does not cause any unwanted scrolling or layout shifts.
56
+ * Expected preview should start with green bar at the top, with yellow section
57
+ * not in the viewport.
58
+ *
59
+ * NB: This test is disabled in Chromatic as there's no obvious way to control <html/> element of a snapshot.
60
+ */
61
+ export const SmoothScrollReset: Story = {
62
+ decorators: [
63
+ (Story) => (
64
+ <>
65
+ <style>{`html { scroll-behavior: smooth; }`}</style>
66
+ <div>
67
+ <div
68
+ className="d-flex align-items-center justify-content-center"
69
+ style={{
70
+ backgroundColor: 'var(--color-bright-yellow)',
71
+ height: 400,
72
+ }}
73
+ >
74
+ This block should not be in the viewport.
75
+ </div>
76
+ <div style={{ height: 10, backgroundColor: 'var(--color-bright-green)' }} />
77
+ <Field id="el1" label="Select currency">
78
+ <Story />
79
+ </Field>
80
+ <Body as="p">{lorem500}</Body>
81
+ </div>
82
+ </>
83
+ ),
84
+ ],
85
+ play: async ({ canvasElement }) => {
86
+ await wait();
87
+ document.documentElement.scrollTop = 400;
88
+ await wait();
89
+ const canvas = within(canvasElement);
90
+ const triggerButton = canvas.getByRole('combobox');
91
+ await userEvent.click(triggerButton);
92
+ },
93
+ globals: {
94
+ viewport: { value: allModes.largeMobile.viewport, isRotated: false },
95
+ },
96
+ parameters: {
97
+ chromatic: {
98
+ disableSnapshot: true,
99
+ },
100
+ },
101
+ };
@@ -245,7 +245,7 @@ const PhoneNumberInput = ({
245
245
  {formatMessage(messages.phoneNumberLabel)}
246
246
  </label>
247
247
  <div className="tw-telephone__number-input">
248
- <div className={`input-group input-group-${size}`}>
248
+ <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
249
249
  <Input
250
250
  ref={phoneNumberInputRef}
251
251
  id={ids.phoneNumber.input}