@transferwise/components 46.42.0 → 46.43.0

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.
@@ -1,97 +1,66 @@
1
- import { action } from '@storybook/addon-actions';
2
- import { boolean, select, text } from '@storybook/addon-knobs';
3
- import { Freeze, Plus, Whatsapp } from '@transferwise/icons';
1
+ import * as Icons from '@transferwise/icons';
4
2
 
5
- import { Priority, ControlType } from '../common';
3
+ import { ControlType, Priority } from '../common';
6
4
 
5
+ import { Meta, StoryObj } from '@storybook/react';
7
6
  import CircularButton from './CircularButton';
8
7
 
9
8
  export default {
10
9
  component: CircularButton,
11
10
  title: 'Actions/CircularButton',
12
- };
11
+ args: {
12
+ children: 'Button text',
13
+ // Needs to be cast because we're mapping icon names to components
14
+ icon: 'Freeze' as unknown as React.ReactElement,
15
+ },
16
+ argTypes: {
17
+ icon: {
18
+ options: Object.keys(Icons),
19
+ mapping: Object.fromEntries(
20
+ Object.entries(Icons).map(([name, Icon]) => [name, <Icon key={name} />]),
21
+ ),
22
+ },
23
+ },
24
+ } satisfies Meta<typeof CircularButton>;
13
25
 
14
- const icons = {
15
- plus: <Plus />,
16
- freeze: <Freeze />,
17
- whatsapp: <Whatsapp />,
18
- };
26
+ type Story = StoryObj<typeof CircularButton>;
19
27
 
20
- export const Basic = () => {
21
- const icon = select('icon', ['freeze', 'plus', 'whatsapp'], 'freeze');
22
- const priority = select('priority', [Priority.PRIMARY, Priority.SECONDARY], Priority.PRIMARY);
23
- const type = select('type', Object.values(ControlType), ControlType.ACCENT);
24
- const disabled = boolean('disabled', false);
25
- const label = text('label', 'Button text');
26
- const background = select('theme', ['light', 'blue'], 'light');
27
- return (
28
- <div className={background === 'blue' ? 'bg--dark p-a-3' : ''}>
29
- <CircularButton
30
- icon={icons[icon]}
31
- priority={priority}
32
- type={type}
33
- disabled={disabled}
34
- onClick={action('Button Clicked')}
35
- >
36
- {label}
37
- </CircularButton>
38
- </div>
39
- );
28
+ export const Basic: Story = {
29
+ args: {
30
+ priority: Priority.PRIMARY,
31
+ type: ControlType.ACCENT,
32
+ disabled: false,
33
+ },
40
34
  };
41
35
 
42
- export const deprecated = () => {
43
- const icon = select('icon', ['freeze', 'plus', 'whatsapp'], 'freeze');
44
- const disabled = boolean('disabled', false);
45
- const label = text('label', 'Button text');
46
- const background = select('theme', ['light', 'blue'], 'light');
47
-
48
- const commonProps = {
49
- label,
36
+ export const All: Story = {
37
+ args: {
50
38
  className: 'm-r-2',
51
- disabled,
52
- icon: icons[icon],
53
- onClick: action('Button Clicked'),
54
- };
55
-
56
- return (
57
- <div className={background === 'blue' ? 'bg--dark p-a-3' : ''}>
58
- <div className="m-b-2">
59
- <div className="title-4 m-b-1">Accent</div>
60
- <CircularButton priority={Priority.PRIMARY} type={ControlType.ACCENT} {...commonProps}>
61
- {label}
62
- </CircularButton>
63
- <CircularButton priority={Priority.SECONDARY} type={ControlType.ACCENT} {...commonProps}>
64
- {label}
65
- </CircularButton>
66
- </div>
67
- <div className="m-b-2">
68
- <div className="title-4 m-b-1">Positive</div>
69
- <CircularButton priority={Priority.PRIMARY} type={ControlType.POSITIVE} {...commonProps}>
70
- {label}
71
- </CircularButton>
72
-
73
- <CircularButton priority={Priority.SECONDARY} type={ControlType.POSITIVE} {...commonProps}>
74
- {label}
75
- </CircularButton>
76
- </div>
77
- <div className="m-b-2">
78
- <div className="title-4 m-b-1">Negative</div>
79
- <CircularButton priority={Priority.PRIMARY} type={ControlType.NEGATIVE} {...commonProps}>
80
- {label}
81
- </CircularButton>
82
- <CircularButton priority={Priority.SECONDARY} type={ControlType.NEGATIVE} {...commonProps}>
83
- {label}
84
- </CircularButton>
85
- </div>
86
- <div className="m-b-2">
87
- <div className="title-4 m-b-1">Disabled</div>
88
- <CircularButton {...commonProps} disabled>
89
- {label}
90
- </CircularButton>
91
- <CircularButton {...commonProps} priority={Priority.SECONDARY} disabled>
92
- {label}
93
- </CircularButton>
94
- </div>
95
- </div>
96
- );
39
+ },
40
+ render: (props) => {
41
+ return (
42
+ <>
43
+ <div className="m-b-2">
44
+ <div className="title-4 m-b-1">Accent</div>
45
+ <CircularButton {...props} priority={Priority.PRIMARY} type={ControlType.ACCENT} />
46
+ <CircularButton {...props} priority={Priority.SECONDARY} type={ControlType.ACCENT} />
47
+ </div>
48
+ <div className="m-b-2">
49
+ <div className="title-4 m-b-1">Positive</div>
50
+ <CircularButton {...props} priority={Priority.PRIMARY} type={ControlType.POSITIVE} />
51
+ <CircularButton {...props} priority={Priority.SECONDARY} type={ControlType.POSITIVE} />
52
+ </div>
53
+ <div className="m-b-2">
54
+ <div className="title-4 m-b-1">Negative</div>
55
+ <CircularButton {...props} priority={Priority.PRIMARY} type={ControlType.NEGATIVE} />
56
+ <CircularButton {...props} priority={Priority.SECONDARY} type={ControlType.NEGATIVE} />
57
+ </div>
58
+ <div className="m-b-2">
59
+ <div className="title-4 m-b-1">Disabled</div>
60
+ <CircularButton {...props} disabled />
61
+ <CircularButton {...props} priority={Priority.SECONDARY} disabled />
62
+ </div>
63
+ </>
64
+ );
65
+ },
97
66
  };
@@ -0,0 +1,21 @@
1
+ import { useEffect } from 'react';
2
+
3
+ /**
4
+ * This progressive enhancement uses an experimental API, it might change,
5
+ * and at the time of authoring is not supported on iOS or mobile Firefox.
6
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard
7
+ */
8
+ export function useVirtualKeyboard() {
9
+ useEffect(() => {
10
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, functional/immutable-data */
11
+ if ('virtualKeyboard' in navigator) {
12
+ const virtualKeyboard: any = navigator.virtualKeyboard;
13
+ const initialOverlaysContent: unknown = virtualKeyboard.overlaysContent;
14
+ virtualKeyboard.overlaysContent = true;
15
+ return () => {
16
+ virtualKeyboard.overlaysContent = initialOverlaysContent;
17
+ };
18
+ }
19
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access, functional/immutable-data */
20
+ }, []);
21
+ }
@@ -0,0 +1,26 @@
1
+ import { Search } from '@transferwise/icons';
2
+
3
+ import { Field } from '../field/Field';
4
+ import { mockResizeObserver, render, screen } from '../test-utils';
5
+ import { Input } from './Input';
6
+ import { InputGroup } from './InputGroup';
7
+
8
+ mockResizeObserver();
9
+
10
+ describe('InputGroup', () => {
11
+ it('supports `Field` for labeling', () => {
12
+ render(
13
+ <Field label="Search…">
14
+ <InputGroup
15
+ addonStart={{
16
+ content: <Search size={24} />,
17
+ initialContentWidth: 24,
18
+ }}
19
+ >
20
+ <Input />
21
+ </InputGroup>
22
+ </Field>,
23
+ );
24
+ expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Search…/);
25
+ });
26
+ });
@@ -3,6 +3,13 @@ import { createContext, useContext, useMemo, useRef, useState } from 'react';
3
3
 
4
4
  import { useResizeObserver } from '../common/hooks/useResizeObserver';
5
5
  import { cssValueWithUnit } from '../utilities/cssValueWithUnit';
6
+ import {
7
+ FieldLabelIdContextProvider,
8
+ InputDescribedByProvider,
9
+ InputIdContextProvider,
10
+ InputInvalidProvider,
11
+ useInputAttributes,
12
+ } from './contexts';
6
13
 
7
14
  type InputPaddingContextType = [
8
15
  number | string | undefined,
@@ -60,23 +67,38 @@ export function InputGroup({
60
67
  className,
61
68
  children,
62
69
  }: InputGroupProps) {
70
+ const inputAttributes = useInputAttributes({ nonLabelable: true });
71
+
63
72
  const [paddingStart, setPaddingStart] = useState(inputPaddingInitialState(addonStart));
64
73
  const [paddingEnd, setPaddingEnd] = useState(inputPaddingInitialState(addonEnd));
65
74
 
66
75
  return (
67
- <InputPaddingStartContext.Provider
68
- value={useMemo(() => [paddingStart, setPaddingStart], [paddingStart])}
69
- >
70
- <InputPaddingEndContext.Provider
71
- value={useMemo(() => [paddingEnd, setPaddingEnd], [paddingEnd])}
72
- >
73
- <fieldset disabled={disabled} className={classNames(className, 'np-input-group')}>
74
- {addonStart != null ? <InputAddon placement="start" {...addonStart} /> : null}
75
- {children}
76
- {addonEnd != null ? <InputAddon placement="end" {...addonEnd} /> : null}
77
- </fieldset>
78
- </InputPaddingEndContext.Provider>
79
- </InputPaddingStartContext.Provider>
76
+ /* Prevent nested controls from being labeled redundantly */
77
+ <FieldLabelIdContextProvider value={undefined}>
78
+ <InputIdContextProvider value={undefined}>
79
+ <InputDescribedByProvider value={undefined}>
80
+ <InputInvalidProvider value={undefined}>
81
+ <InputPaddingStartContext.Provider
82
+ value={useMemo(() => [paddingStart, setPaddingStart], [paddingStart])}
83
+ >
84
+ <InputPaddingEndContext.Provider
85
+ value={useMemo(() => [paddingEnd, setPaddingEnd], [paddingEnd])}
86
+ >
87
+ <fieldset
88
+ {...inputAttributes}
89
+ disabled={disabled}
90
+ className={classNames(className, 'np-input-group')}
91
+ >
92
+ {addonStart != null ? <InputAddon placement="start" {...addonStart} /> : null}
93
+ {children}
94
+ {addonEnd != null ? <InputAddon placement="end" {...addonEnd} /> : null}
95
+ </fieldset>
96
+ </InputPaddingEndContext.Provider>
97
+ </InputPaddingStartContext.Provider>
98
+ </InputInvalidProvider>
99
+ </InputDescribedByProvider>
100
+ </InputIdContextProvider>
101
+ </FieldLabelIdContextProvider>
80
102
  );
81
103
  }
82
104
 
@@ -0,0 +1,16 @@
1
+ import { Field } from '../field/Field';
2
+ import { mockResizeObserver, render, screen } from '../test-utils';
3
+ import { SearchInput } from './SearchInput';
4
+
5
+ mockResizeObserver();
6
+
7
+ describe('SearchInput', () => {
8
+ it('supports `Field` for labeling', () => {
9
+ render(
10
+ <Field label="Search…">
11
+ <SearchInput />
12
+ </Field>,
13
+ );
14
+ expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Search…/);
15
+ });
16
+ });
@@ -2,67 +2,37 @@
2
2
  position: relative;
3
3
  z-index: 1060;
4
4
  }
5
- .np-bottom-sheet-v2-backdrop-container--enter,
6
- .np-bottom-sheet-v2-backdrop-container--leave {
7
- transition-property: opacity;
8
- transition-timing-function: ease-out;
9
- transition-duration: 300ms;
10
- }
11
- .np-bottom-sheet-v2-backdrop-container--enter-from,
12
- .np-bottom-sheet-v2-backdrop-container--leave-to {
13
- opacity: 0;
14
- }
15
5
  .np-bottom-sheet-v2-backdrop {
16
6
  position: fixed;
17
7
  inset: 0px;
18
8
  background-color: #37517e;
19
9
  background-color: var(--color-content-primary);
20
10
  opacity: 0.4;
11
+ transition-property: opacity;
12
+ transition-timing-function: ease-out;
13
+ transition-duration: 300ms;
14
+ }
15
+ .np-bottom-sheet-v2-backdrop--closed {
16
+ opacity: 0;
21
17
  }
22
18
  .np-bottom-sheet-v2 {
23
19
  position: fixed;
24
20
  inset: 0px;
21
+ bottom: env(keyboard-inset-height, 0px);
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);
25
28
  display: flex;
26
29
  flex-direction: column;
27
30
  justify-content: flex-end;
28
- padding-left: 8px;
29
- padding-left: var(--size-8);
30
- padding-right: 8px;
31
- padding-right: var(--size-8);
32
- padding-top: 64px;
33
- padding-top: var(--size-64);
34
31
  }
35
32
  .np-bottom-sheet-v2-content {
36
- max-height: 100%;
37
- }
38
- .np-bottom-sheet-v2-content--enter,
39
- .np-bottom-sheet-v2-content--leave {
40
- transition-property: transform;
41
- transition-timing-function: ease-out;
42
- transition-duration: 300ms;
43
- }
44
- @media (prefers-reduced-motion: reduce) {
45
- .np-bottom-sheet-v2-content--enter,
46
- .np-bottom-sheet-v2-content--leave {
47
- transition-property: opacity;
48
- }
49
- }
50
- @media (prefers-reduced-motion: no-preference) {
51
- .np-bottom-sheet-v2-content--enter-from,
52
- .np-bottom-sheet-v2-content--leave-to {
53
- transform: translateY(100%);
54
- }
55
- }
56
- @media (prefers-reduced-motion: reduce) {
57
- .np-bottom-sheet-v2-content--enter-from,
58
- .np-bottom-sheet-v2-content--leave-to {
59
- opacity: 0;
60
- }
61
- }
62
- .np-bottom-sheet-v2-content-inner-container {
63
33
  display: flex;
64
- height: 100%;
65
34
  flex-direction: column;
35
+ overflow: auto;
66
36
  border-top-left-radius: 32px;
67
37
  /* TODO: Tokenize */
68
38
  border-top-right-radius: 32px;
@@ -71,30 +41,50 @@
71
41
  background-color: var(--color-background-elevated);
72
42
  box-shadow: 0 0 40px rgba(69, 71, 69, 0.2);
73
43
  }
74
- .np-bottom-sheet-v2-content-inner-container:focus {
44
+ .np-bottom-sheet-v2-content:focus {
75
45
  outline: none;
76
46
  }
47
+ @media (prefers-reduced-motion: no-preference) {
48
+ .np-bottom-sheet-v2-content {
49
+ transition-property: transform;
50
+ transition-timing-function: ease-out;
51
+ transition-duration: 300ms;
52
+ }
53
+ .np-bottom-sheet-v2-content--closed {
54
+ transform: translateY(100%);
55
+ }
56
+ }
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .np-bottom-sheet-v2-content {
59
+ transition-property: opacity;
60
+ transition-timing-function: ease-out;
61
+ transition-duration: 300ms;
62
+ }
63
+ .np-bottom-sheet-v2-content--closed {
64
+ opacity: 0;
65
+ }
66
+ }
77
67
  .np-bottom-sheet-v2-header {
78
68
  align-self: flex-end;
79
69
  padding: 16px;
80
70
  padding: var(--size-16);
81
71
  }
82
72
  .np-bottom-sheet-v2-content-inner {
83
- padding-top: 0px;
84
73
  display: grid;
74
+ grid-template-rows: minmax(0, 1fr);
85
75
  grid-row-gap: 8px;
86
76
  grid-row-gap: var(--size-8);
87
77
  row-gap: 8px;
88
78
  row-gap: var(--size-8);
89
- overflow-y: auto;
90
- grid-template-rows: repeat(1, minmax(0, 1fr));
79
+ overflow: auto;
91
80
  }
92
81
  .np-bottom-sheet-v2-content-inner--has-title {
93
- grid-template-rows: auto 1fr;
82
+ grid-template-rows: auto minmax(0, 1fr);
94
83
  }
95
84
  .np-bottom-sheet-v2-content-inner--padding-md {
96
85
  padding: 16px;
97
86
  padding: var(--size-16);
87
+ padding-top: 0px;
98
88
  }
99
89
  .np-bottom-sheet-v2-title {
100
90
  color: #37517e;
@@ -141,10 +131,10 @@
141
131
  row-gap: 8px;
142
132
  row-gap: var(--size-8);
143
133
  overflow-y: auto;
144
- grid-template-rows: repeat(1, minmax(0, 1fr));
134
+ grid-template-rows: minmax(0, 1fr);
145
135
  }
146
136
  .np-popover-v2--has-title {
147
- grid-template-rows: auto 1fr;
137
+ grid-template-rows: auto minmax(0, 1fr);
148
138
  }
149
139
  .np-popover-v2--padding-md {
150
140
  padding: 16px;
@@ -214,6 +214,6 @@ describe('SelectInput', () => {
214
214
  <SelectInput items={[{ type: 'option', value: 'USD' }]} value="USD" />
215
215
  </Field>,
216
216
  );
217
- expect(screen.getByLabelText('Currency')).toHaveAttribute('aria-haspopup');
217
+ expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Currency/);
218
218
  });
219
219
  });
@@ -24,7 +24,7 @@ import { Merge } from '../utils';
24
24
  import { BottomSheet } from './_BottomSheet';
25
25
  import { ButtonInput } from './_ButtonInput';
26
26
  import { Popover } from './_Popover';
27
- import { useInputAttributes } from './contexts';
27
+ import { useInputAttributes, WithInputAttributesProps } from './contexts';
28
28
  import { InputGroup } from './InputGroup';
29
29
  import { SearchInput } from './SearchInput';
30
30
  import messages from './SelectInput.messages';
@@ -162,6 +162,7 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
162
162
  disabled?: boolean;
163
163
  size?: 'sm' | 'md' | 'lg';
164
164
  className?: string;
165
+ UNSAFE_triggerButtonProps?: WithInputAttributesProps['inputAttributes'];
165
166
  onFilterChange?: (args: { query: string; queryNormalized: string | null }) => void;
166
167
  onChange?: (value: M extends true ? T[] : T) => void;
167
168
  onClose?: () => void;
@@ -246,6 +247,7 @@ export function SelectInput<T = string, M extends boolean = false>({
246
247
  disabled,
247
248
  size = 'md',
248
249
  className,
250
+ UNSAFE_triggerButtonProps,
249
251
  onFilterChange = noop,
250
252
  onChange,
251
253
  onClose,
@@ -321,6 +323,7 @@ export function SelectInput<T = string, M extends boolean = false>({
321
323
  triggerRef.current = node;
322
324
  },
323
325
  ...inputAttributes,
326
+ ...UNSAFE_triggerButtonProps,
324
327
  id,
325
328
  ...mergeProps(
326
329
  {
@@ -3,98 +3,84 @@
3
3
  z-index: 1060;
4
4
  }
5
5
 
6
- .np-bottom-sheet-v2-backdrop-container {
7
- &--enter, &--leave {
8
- transition-property: opacity;
9
- transition-timing-function: ease-out;
10
- transition-duration: 300ms;
11
- }
12
-
13
- &--enter-from, &--leave-to {
14
- opacity: 0;
15
- }
16
- }
17
-
18
6
  .np-bottom-sheet-v2-backdrop {
19
7
  position: fixed;
20
8
  inset: 0px;
21
9
  background-color: var(--color-content-primary);
22
10
  opacity: 0.4;
11
+ transition-property: opacity;
12
+ transition-timing-function: ease-out;
13
+ transition-duration: 300ms;
14
+
15
+ &--closed {
16
+ opacity: 0;
17
+ }
23
18
  }
24
19
 
25
20
  .np-bottom-sheet-v2 {
26
21
  position: fixed;
27
22
  inset: 0px;
23
+ bottom: env(keyboard-inset-height, 0px);
24
+ margin-left: var(--size-8);
25
+ margin-right: var(--size-8);
26
+ margin-top: var(--size-64);
28
27
  display: flex;
29
28
  flex-direction: column;
30
29
  justify-content: flex-end;
31
- padding-left: var(--size-8);
32
- padding-right: var(--size-8);
33
- padding-top: var(--size-64);
34
30
  }
35
31
 
36
32
  .np-bottom-sheet-v2-content {
37
- max-height: 100%;
33
+ display: flex;
34
+ flex-direction: column;
35
+ overflow: auto;
36
+ border-top-left-radius: 32px; /* TODO: Tokenize */
37
+ border-top-right-radius: 32px; /* TODO: Tokenize */
38
+ background-color: var(--color-background-elevated);
39
+ box-shadow: 0 0 40px rgb(69 71 69 / 0.2);
40
+
41
+ &:focus {
42
+ outline: none;
43
+ }
38
44
 
39
- &--enter, &--leave {
45
+ @media (prefers-reduced-motion: no-preference) {
40
46
  transition-property: transform;
41
47
  transition-timing-function: ease-out;
42
48
  transition-duration: 300ms;
43
49
 
44
- @media (prefers-reduced-motion: reduce) {
45
- & {
46
- transition-property: opacity;
47
- }
50
+ &--closed {
51
+ transform: translateY(100%);
48
52
  }
49
53
  }
50
54
 
51
- &--enter-from, &--leave-to {
52
- @media (prefers-reduced-motion: no-preference) {
53
- & {
54
- transform: translateY(100%);
55
- }
56
- }
55
+ @media (prefers-reduced-motion: reduce) {
56
+ transition-property: opacity;
57
+ transition-timing-function: ease-out;
58
+ transition-duration: 300ms;
57
59
 
58
- @media (prefers-reduced-motion: reduce) {
59
- & {
60
- opacity: 0;
61
- }
60
+ &--closed {
61
+ opacity: 0;
62
62
  }
63
63
  }
64
64
  }
65
65
 
66
- .np-bottom-sheet-v2-content-inner-container {
67
- display: flex;
68
- height: 100%;
69
- flex-direction: column;
70
- border-top-left-radius: 32px; /* TODO: Tokenize */
71
- border-top-right-radius: 32px; /* TODO: Tokenize */
72
- background-color: var(--color-background-elevated);
73
- box-shadow: 0 0 40px rgb(69 71 69 / 0.2);
74
-
75
- &:focus {
76
- outline: none;
77
- }
78
- }
79
-
80
66
  .np-bottom-sheet-v2-header {
81
67
  align-self: flex-end;
82
68
  padding: var(--size-16);
83
69
  }
84
70
 
85
71
  .np-bottom-sheet-v2-content-inner {
86
- padding-top: 0px;
87
72
  display: grid;
73
+ grid-template-rows: minmax(0, 1fr);
88
74
  row-gap: var(--size-8);
89
- overflow-y: auto;
90
- grid-template-rows: repeat(1, minmax(0, 1fr));
75
+ overflow: auto;
91
76
 
92
77
  &--has-title {
93
- grid-template-rows: auto 1fr;
78
+ grid-template-rows: auto minmax(0, 1fr);
94
79
  }
95
80
 
96
81
  &--padding-md {
97
82
  padding: var(--size-16);
83
+ padding-top: 0px;
98
84
  }
99
85
  }
100
86