@transferwise/components 0.0.0-experimental-7a83d86 → 0.0.0-experimental-b2a535a

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 (52) hide show
  1. package/build/index.js +17 -22
  2. package/build/index.js.map +1 -1
  3. package/build/index.mjs +17 -22
  4. package/build/index.mjs.map +1 -1
  5. package/build/types/alert/Alert.d.ts.map +1 -1
  6. package/build/types/dimmer/Dimmer.d.ts.map +1 -1
  7. package/build/types/loader/Loader.d.ts.map +1 -1
  8. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  9. package/build/types/popover/Popover.d.ts.map +1 -1
  10. package/build/types/segmentedControl/SegmentedControl.d.ts +2 -2
  11. package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -1
  12. package/build/types/select/Select.d.ts.map +1 -1
  13. package/build/types/stepper/deviceDetection.d.ts.map +1 -1
  14. package/build/types/uploadInput/uploadButton/UploadButton.d.ts.map +1 -1
  15. package/package.json +3 -3
  16. package/src/accordion/Accordion.story.tsx +1 -1
  17. package/src/alert/Alert.tsx +1 -2
  18. package/src/avatar/colors/colors.ts +1 -1
  19. package/src/body/Body.spec.tsx +1 -1
  20. package/src/body/Body.story.tsx +8 -8
  21. package/src/checkbox/Checkbox.js +1 -1
  22. package/src/checkboxButton/CheckboxButton.spec.tsx +1 -0
  23. package/src/common/Option/Option.tsx +1 -1
  24. package/src/common/deviceDetection/deviceDetection.js +1 -1
  25. package/src/common/deviceDetection/deviceDetection.spec.js +2 -4
  26. package/src/common/responsivePanel/ResponsivePanel.spec.js +15 -11
  27. package/src/decision/Decision.spec.js +1 -0
  28. package/src/dimmer/Dimmer.tsx +2 -6
  29. package/src/inlineAlert/InlineAlert.story.tsx +7 -8
  30. package/src/link/Link.story.tsx +16 -16
  31. package/src/loader/Loader.tsx +1 -0
  32. package/src/logo/Logo.js +2 -2
  33. package/src/moneyInput/MoneyInput.story.tsx +3 -3
  34. package/src/nudge/Nudge.spec.tsx +5 -5
  35. package/src/phoneNumberInput/PhoneNumberInput.tsx +1 -2
  36. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.ts +1 -1
  37. package/src/popover/Popover.tsx +1 -2
  38. package/src/promoCard/PromoCard.tsx +1 -1
  39. package/src/radioGroup/RadioGroup.spec.js +1 -1
  40. package/src/section/Section.story.tsx +1 -2
  41. package/src/segmentedControl/SegmentedControl.spec.tsx +41 -3
  42. package/src/segmentedControl/SegmentedControl.story.tsx +51 -13
  43. package/src/segmentedControl/SegmentedControl.tsx +3 -4
  44. package/src/select/Select.js +3 -2
  45. package/src/stepper/deviceDetection.js +2 -1
  46. package/src/stepper/deviceDetection.spec.js +3 -8
  47. package/src/test-utils/index.js +1 -1
  48. package/src/test-utils/story-config.ts +1 -1
  49. package/src/title/Title.spec.tsx +1 -1
  50. package/src/typeahead/Typeahead.spec.js +2 -4
  51. package/src/upload/Upload.spec.js +4 -8
  52. package/src/uploadInput/uploadButton/UploadButton.tsx +0 -1
@@ -32,7 +32,7 @@ const onChange = jest.fn();
32
32
 
33
33
  const defaultProps: SegmentedControlProps = {
34
34
  name: 'segmentedControl',
35
- defaultValue: defaultSegments[0].value,
35
+ selectedValue: defaultSegments[0].value,
36
36
  mode: 'input',
37
37
  segments: defaultSegments,
38
38
  onChange,
@@ -96,7 +96,7 @@ describe('SegmentedControl', () => {
96
96
 
97
97
  // new function is created on every render
98
98
  const onChange = () => {
99
- onChangeCallCount += 1;
99
+ onChangeCallCount++;
100
100
  simulateRerender({});
101
101
  };
102
102
 
@@ -118,7 +118,7 @@ describe('SegmentedControl', () => {
118
118
 
119
119
  // a new onChange function is created on every render
120
120
  const onChange = () => {
121
- onChangeCallCount += 1;
121
+ onChangeCallCount++;
122
122
  simulateRerender({});
123
123
  };
124
124
 
@@ -134,6 +134,44 @@ describe('SegmentedControl', () => {
134
134
  });
135
135
  });
136
136
 
137
+ it('updates the selected segment when the selectedValue prop changes', () => {
138
+ const { rerender } = render(<SegmentedControl {...defaultProps} />);
139
+
140
+ const payroll = screen.getByRole('radio', { name: 'Payroll' });
141
+ userEvent.click(payroll);
142
+
143
+ expect(onChange).toHaveBeenCalledWith('payroll');
144
+
145
+ rerender(<SegmentedControl {...defaultProps} selectedValue="reporting" />);
146
+
147
+ const reporting = screen.getByRole('radio', { name: 'Reporting' });
148
+ expect(reporting).toBeChecked();
149
+ });
150
+
151
+ it('updates the options when the segments prop changes', () => {
152
+ const { rerender } = render(<SegmentedControl {...defaultProps} />);
153
+
154
+ const newSegments = [
155
+ {
156
+ id: '1',
157
+ value: 'payroll',
158
+ label: 'Payroll',
159
+ },
160
+ {
161
+ id: '3',
162
+ value: 'anotherOne',
163
+ label: 'Another One',
164
+ },
165
+ ];
166
+
167
+ rerender(<SegmentedControl {...defaultProps} segments={newSegments} />);
168
+
169
+ const anotherOne = screen.getByRole('radio', { name: 'Another One' });
170
+ userEvent.click(anotherOne);
171
+
172
+ expect(onChange).toHaveBeenCalledWith('anotherOne');
173
+ });
174
+
137
175
  it('throws error if user tries to add too many segments', () => {
138
176
  expect(() => {
139
177
  renderSegmentedControl({
@@ -1,33 +1,36 @@
1
1
  import { StoryFn } from '@storybook/react';
2
2
  import React from 'react';
3
3
 
4
- import SegmentedControl, { Segments } from './SegmentedControl';
4
+ import Button from '../button';
5
+
6
+ import SegmentedControl from './SegmentedControl';
5
7
 
6
8
  export default {
7
9
  component: SegmentedControl,
8
10
  title: 'Forms/SegmentedControl',
9
11
  };
10
12
 
11
- const segments: Segments = [
12
- { id: 'CUPCAKE', label: 'Cupcakes', value: 'cupcakes' },
13
- { id: 'SPONGECAKE', label: 'Sponge cake', value: 'spongecake' },
14
- { id: 'CARROT_CAKE', label: 'Carrot cake', value: 'carrotcake' },
15
- ];
13
+ const Template: StoryFn = (args) => {
14
+ const [segments, setSegments] = React.useState([
15
+ { id: 'CUPCAKE', label: 'Cupcakes', value: 'cupcakes' },
16
+ { id: 'SPONGECAKE', label: 'Sponge cake', value: 'spongecake' },
17
+ { id: 'CARROT_CAKE', label: 'Carrot cake', value: 'carrotcake' },
18
+ ]);
16
19
 
17
- const segmentsWithControls: Segments = [
18
- { id: 'CUPCAKE', label: 'Cupcakes', value: 'cupcakes', controls: 'aControlId' },
19
- { id: 'SPONGECAKE', label: 'Sponge cake', value: 'spongecake', controls: 'aControlId' },
20
- { id: 'CARROT_CAKE', label: 'Carrot cake', value: 'carrotcake', controls: 'aControlId' },
21
- ];
20
+ const [segmentsWithControls, setSegmentsWithControls] = React.useState([
21
+ { id: 'CUPCAKE', label: 'Cupcakes', value: 'cupcakes', controls: 'aControlId' },
22
+ { id: 'SPONGECAKE', label: 'Sponge cake', value: 'spongecake', controls: 'aControlId' },
23
+ { id: 'CARROT_CAKE', label: 'Carrot cake', value: 'carrotcake', controls: 'aControlId' },
24
+ ]);
22
25
 
23
- const Template: StoryFn = (args) => {
24
26
  const [selectedValue, setSelectedValue] = React.useState(segments[0].value);
25
27
 
28
+ console.log('render: segments.length', segments.length);
26
29
  return (
27
30
  <div className="p-a-2">
28
31
  <SegmentedControl
29
32
  name="aSegmentedControl"
30
- defaultValue={selectedValue}
33
+ selectedValue={selectedValue}
31
34
  onChange={setSelectedValue}
32
35
  {...(args.mode === 'view'
33
36
  ? { segments: segmentsWithControls, mode: 'view', controls: 'aControlId' }
@@ -36,6 +39,41 @@ const Template: StoryFn = (args) => {
36
39
  <div className="m-a-2" id="aControlId">
37
40
  <p>Selected value: {selectedValue}</p>
38
41
  </div>
42
+ <div className="m-a-2">
43
+ <p>
44
+ Force the <b>selectedValue</b> to be one of the following:
45
+ <ul>
46
+ {segments.map((segment) => (
47
+ <li key={segment.id}>
48
+ <a
49
+ href="/"
50
+ onClick={(e) => {
51
+ e.preventDefault();
52
+ setSelectedValue(segment.value);
53
+ }}
54
+ >
55
+ {segment.label}
56
+ </a>
57
+ </li>
58
+ ))}
59
+ </ul>
60
+ </p>
61
+ </div>
62
+ <div className="m-a-2">
63
+ <Button
64
+ priority="secondary"
65
+ type="danger"
66
+ size="sm"
67
+ disabled={segments.length < 2}
68
+ onClick={() => {
69
+ const index = segments.findIndex((s) => s.value !== selectedValue);
70
+ setSegments((prev) => prev.filter((_, i) => i !== index));
71
+ setSegmentsWithControls((prev) => prev.filter((_, i) => i !== index));
72
+ }}
73
+ >
74
+ Remove one segment
75
+ </Button>
76
+ </div>
39
77
  </div>
40
78
  );
41
79
  };
@@ -13,7 +13,7 @@ export type Segments = readonly Segment[] | readonly SegmentWithControls[];
13
13
 
14
14
  type SegmentedControlPropsBase = {
15
15
  name: string;
16
- defaultValue: string;
16
+ selectedValue: string;
17
17
  mode: 'input' | 'view';
18
18
  onChange: (value: string) => void;
19
19
  };
@@ -33,12 +33,11 @@ export type SegmentedControlProps = SegmentedControlPropsBase &
33
33
 
34
34
  const SegmentedControl = ({
35
35
  name,
36
- defaultValue,
36
+ selectedValue,
37
37
  mode = 'input',
38
38
  segments,
39
39
  onChange,
40
40
  }: SegmentedControlProps) => {
41
- const [selectedValue, setSelectedValue] = useState(defaultValue || segments[0].value);
42
41
  const [animate, setAnimate] = useState(false);
43
42
 
44
43
  const segmentsRef = useRef<HTMLDivElement>(null);
@@ -70,6 +69,7 @@ const SegmentedControl = ({
70
69
  };
71
70
 
72
71
  useEffect(() => {
72
+ setAnimate(true);
73
73
  updateSegmentPosition();
74
74
 
75
75
  const handleWindowSizeChange = () => {
@@ -102,7 +102,6 @@ const SegmentedControl = ({
102
102
  {segmentsWithRefs.map((segment) => {
103
103
  const onSelect = () => {
104
104
  setAnimate(true);
105
- setSelectedValue(segment.value);
106
105
  onChange(segment.value);
107
106
  };
108
107
  return mode === 'input' ? (
@@ -246,8 +246,8 @@ export default function Select({
246
246
  };
247
247
 
248
248
  function selectKeyboardFocusedOption() {
249
- if (keyboardFocusedOptionIndex != null && selectableOptions.length > 0) {
250
- selectOption(selectableOptions[keyboardFocusedOptionIndex]);
249
+ if (keyboardFocusedOptionIndex != null) {
250
+ selectableOptions.length > 0 && selectOption(selectableOptions[keyboardFocusedOptionIndex]);
251
251
  }
252
252
  }
253
253
 
@@ -511,6 +511,7 @@ export default function Select({
511
511
  disabled={disabled}
512
512
  aria-controls={listboxId}
513
513
  aria-expanded={open}
514
+ aria-autocomplete="none"
514
515
  onClick={handleOnClick}
515
516
  {...buttonProps}
516
517
  >
@@ -1,5 +1,6 @@
1
1
  function supportsTouchEvents() {
2
2
  const onTouchStartIsDefined = typeof window !== 'undefined' && window.ontouchstart !== undefined;
3
+ // eslint-disable-next-line compat/compat
3
4
  const maxTouchPointsIsDefined = typeof navigator !== 'undefined' && navigator.maxTouchPoints;
4
5
  const documentTouchIsDefined =
5
6
  typeof window !== 'undefined' &&
@@ -20,7 +21,7 @@ function userAgentSuggestsTouchDevice() {
20
21
  'bada',
21
22
  ];
22
23
  const matchString = sampleTouchDevices.map((device) => `(${device})`).join('|');
23
- const regex = new RegExp(matchString, 'gi');
24
+ const regex = new RegExp(matchString, 'ig');
24
25
  return typeof navigator !== 'undefined' && !!navigator.userAgent.match(regex);
25
26
  }
26
27
  // Important: this is not fool-proof! It gives false positives and negatives, and will be outdated.
@@ -2,17 +2,12 @@ import { isTouchDevice } from './deviceDetection';
2
2
 
3
3
  describe('Device detection', () => {
4
4
  function fakeUserAgent(userAgent) {
5
- Object.defineProperty(navigator, 'userAgent', {
6
- value: userAgent,
7
- configurable: true,
8
- });
5
+ navigator.__defineGetter__('userAgent', () => userAgent);
6
+ // need to use this instead of defineProperty, as it's blocked from overriding
9
7
  }
10
8
 
11
9
  function fakeMaxTouchPoints(maxTouchPoints) {
12
- Object.defineProperty(navigator, 'maxTouchPoints', {
13
- value: maxTouchPoints,
14
- configurable: true,
15
- });
10
+ navigator.__defineGetter__('maxTouchPoints', () => maxTouchPoints);
16
11
  }
17
12
 
18
13
  // We don't test DocumentTouch api as it's basically impossible to test :(
@@ -13,7 +13,7 @@ import en from '../i18n/en.json';
13
13
  */
14
14
  function customRender(ui, { locale = DEFAULT_LOCALE, messages = en, ...renderOptions } = {}) {
15
15
  // eslint-disable-next-line react/prop-types
16
- const Wrapper = ({ children }) => {
16
+ var Wrapper = ({ children }) => {
17
17
  return <Provider i18n={{ locale, messages }}>{children}</Provider>;
18
18
  };
19
19
  return render(ui, { wrapper: Wrapper, ...renderOptions });
@@ -30,7 +30,7 @@ interface StoryConfig {
30
30
 
31
31
  const getViewportWidth = (viewport: (typeof viewports)[keyof typeof viewports]) => {
32
32
  if (viewport.styles && typeof viewport.styles === 'object') {
33
- return Number.parseInt(viewport.styles.width, 10);
33
+ return parseInt(viewport.styles.width);
34
34
  }
35
35
  throw new Error('Unknown viewport styles');
36
36
  };
@@ -34,7 +34,7 @@ describe('Title', () => {
34
34
 
35
35
  it('handles unsupported typography type', () => {
36
36
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
37
- // @ts-expect-error
37
+ // @ts-ignore
38
38
  render(<Title type={Typography.BODY_LARGE_BOLD}>Test</Title>);
39
39
  const copy = screen.getByText('Test');
40
40
  expect(copy).toBeInTheDocument();
@@ -314,9 +314,7 @@ describe('Typeahead', () => {
314
314
  let selectedOption;
315
315
 
316
316
  component.setProps({
317
- onChange: (selections) => {
318
- selectedOption = selections[0];
319
- },
317
+ onChange: (selections) => (selectedOption = selections[0]),
320
318
  options: options,
321
319
  });
322
320
 
@@ -342,7 +340,7 @@ describe('Typeahead', () => {
342
340
  it('renders all options', () => {
343
341
  const options = option().map((optNode) => optNode.text());
344
342
  expect(options).toHaveLength(props.options.length);
345
- expect(options.every((label, i) => label === props.options[i].label)).toBe(true);
343
+ expect(options.every((label, i) => label === props.options[i].label));
346
344
  });
347
345
 
348
346
  it('does not render new option if showNewEntry is false', () => {
@@ -8,7 +8,7 @@ import Upload from '.';
8
8
 
9
9
  jest.useFakeTimers();
10
10
  jest.mock('./utils/postData', () => ({
11
- postData: async () => 'ServerResponse',
11
+ postData: () => new Promise((resolve) => resolve('ServerResponse')),
12
12
  }));
13
13
 
14
14
  jest.mock('./utils/asyncFileRead');
@@ -88,7 +88,7 @@ describe('Upload', () => {
88
88
  let component;
89
89
  beforeEach(() => {
90
90
  component = shallow(<Upload {...props} />).dive();
91
- asyncFileRead.mockImplementation(async () => 'a value');
91
+ asyncFileRead.mockImplementation(() => new Promise((resolve) => resolve('a value')));
92
92
  });
93
93
 
94
94
  afterEach(() => {
@@ -183,9 +183,7 @@ describe('Upload', () => {
183
183
  });
184
184
 
185
185
  it('step ProcessingStep is called with error props', async () => {
186
- asyncFileRead.mockImplementation(async () => {
187
- throw 'An error';
188
- });
186
+ asyncFileRead.mockImplementation(() => new Promise((resolve, reject) => reject('An error')));
189
187
 
190
188
  await component.instance().fileDropped(TEST_FILE);
191
189
  jest.advanceTimersByTime(props.animationDelay);
@@ -232,9 +230,7 @@ describe('Upload', () => {
232
230
  it('step CompleteStep is called with error props', async () => {
233
231
  component = mount(<Upload {...props} />);
234
232
  const upload = component.children();
235
- asyncFileRead.mockImplementation(async () => {
236
- throw 'An error';
237
- });
233
+ asyncFileRead.mockImplementation(() => new Promise((resolve, reject) => reject('An error')));
238
234
 
239
235
  await upload.instance().fileDropped(TEST_FILE);
240
236
  jest.advanceTimersByTime(props.animationDelay + ANIMATION_DELAY);
@@ -225,7 +225,6 @@ const UploadButton = ({
225
225
  data-testid={TEST_IDS.uploadInput}
226
226
  onChange={filesSelected}
227
227
  />
228
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
229
228
  <label
230
229
  htmlFor={id}
231
230
  className={classNames('btn', 'np-upload-accent', 'np-upload-button', {