@transferwise/components 46.27.0 → 46.29.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.
Files changed (206) hide show
  1. package/build/i18n/th.json +2 -2
  2. package/build/index.js +344 -1147
  3. package/build/index.js.map +1 -1
  4. package/build/index.mjs +346 -1147
  5. package/build/index.mjs.map +1 -1
  6. package/build/main.css +16 -5
  7. package/build/styles/logo/Logo.css +16 -0
  8. package/build/styles/main.css +16 -5
  9. package/build/types/alert/Alert.d.ts +47 -58
  10. package/build/types/alert/Alert.d.ts.map +1 -1
  11. package/build/types/alert/index.d.ts +2 -1
  12. package/build/types/alert/index.d.ts.map +1 -1
  13. package/build/types/button/Button.d.ts +7 -9
  14. package/build/types/button/Button.d.ts.map +1 -1
  15. package/build/types/common/dateUtils/isWithinRange/isWithinRange.d.ts +1 -1
  16. package/build/types/common/dateUtils/isWithinRange/isWithinRange.d.ts.map +1 -1
  17. package/build/types/common/dateUtils/moveToWithinRange/moveToWithinRange.d.ts +1 -1
  18. package/build/types/common/dateUtils/moveToWithinRange/moveToWithinRange.d.ts.map +1 -1
  19. package/build/types/common/propsValues/sentiment.d.ts +0 -1
  20. package/build/types/common/propsValues/sentiment.d.ts.map +1 -1
  21. package/build/types/dateLookup/DateLookup.d.ts +75 -28
  22. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  23. package/build/types/dateLookup/DateLookup.messages.d.ts +42 -63
  24. package/build/types/dateLookup/DateLookup.messages.d.ts.map +1 -1
  25. package/build/types/dateLookup/dateHeader/DateHeader.d.ts +9 -22
  26. package/build/types/dateLookup/dateHeader/DateHeader.d.ts.map +1 -1
  27. package/build/types/dateLookup/dateHeader/index.d.ts +1 -1
  28. package/build/types/dateLookup/dateHeader/index.d.ts.map +1 -1
  29. package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts +13 -31
  30. package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts.map +1 -1
  31. package/build/types/dateLookup/dateTrigger/index.d.ts +1 -1
  32. package/build/types/dateLookup/dateTrigger/index.d.ts.map +1 -1
  33. package/build/types/dateLookup/dayCalendar/DayCalendar.d.ts +19 -2
  34. package/build/types/dateLookup/dayCalendar/DayCalendar.d.ts.map +1 -1
  35. package/build/types/dateLookup/dayCalendar/index.d.ts +1 -1
  36. package/build/types/dateLookup/dayCalendar/index.d.ts.map +1 -1
  37. package/build/types/dateLookup/dayCalendar/table/DayCalendarTable.d.ts +12 -2
  38. package/build/types/dateLookup/dayCalendar/table/DayCalendarTable.d.ts.map +1 -1
  39. package/build/types/dateLookup/dayCalendar/table/index.d.ts +1 -1
  40. package/build/types/dateLookup/dayCalendar/table/index.d.ts.map +1 -1
  41. package/build/types/dateLookup/getStartOfDay/getStartOfDay.d.ts +1 -1
  42. package/build/types/dateLookup/getStartOfDay/getStartOfDay.d.ts.map +1 -1
  43. package/build/types/dateLookup/getStartOfDay/index.d.ts +1 -1
  44. package/build/types/dateLookup/getStartOfDay/index.d.ts.map +1 -1
  45. package/build/types/dateLookup/index.d.ts +2 -1
  46. package/build/types/dateLookup/index.d.ts.map +1 -1
  47. package/build/types/dateLookup/monthCalendar/MonthCalendar.d.ts +17 -2
  48. package/build/types/dateLookup/monthCalendar/MonthCalendar.d.ts.map +1 -1
  49. package/build/types/dateLookup/monthCalendar/index.d.ts +1 -1
  50. package/build/types/dateLookup/monthCalendar/index.d.ts.map +1 -1
  51. package/build/types/dateLookup/monthCalendar/table/MonthCalendarTable.d.ts +10 -26
  52. package/build/types/dateLookup/monthCalendar/table/MonthCalendarTable.d.ts.map +1 -1
  53. package/build/types/dateLookup/monthCalendar/table/index.d.ts +1 -1
  54. package/build/types/dateLookup/monthCalendar/table/index.d.ts.map +1 -1
  55. package/build/types/dateLookup/yearCalendar/YearCalendar.d.ts +15 -2
  56. package/build/types/dateLookup/yearCalendar/YearCalendar.d.ts.map +1 -1
  57. package/build/types/dateLookup/yearCalendar/index.d.ts +1 -1
  58. package/build/types/dateLookup/yearCalendar/index.d.ts.map +1 -1
  59. package/build/types/dateLookup/yearCalendar/table/YearCalendarTable.d.ts +10 -26
  60. package/build/types/dateLookup/yearCalendar/table/YearCalendarTable.d.ts.map +1 -1
  61. package/build/types/dateLookup/yearCalendar/table/index.d.ts +1 -1
  62. package/build/types/dateLookup/yearCalendar/table/index.d.ts.map +1 -1
  63. package/build/types/dimmer/Dimmer.d.ts.map +1 -1
  64. package/build/types/index.d.ts +2 -2
  65. package/build/types/index.d.ts.map +1 -1
  66. package/build/types/inlineAlert/InlineAlert.d.ts +2 -4
  67. package/build/types/inlineAlert/InlineAlert.d.ts.map +1 -1
  68. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  69. package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
  70. package/build/types/loader/Loader.d.ts.map +1 -1
  71. package/build/types/logo/Logo.d.ts.map +1 -1
  72. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  73. package/build/types/popover/Popover.d.ts.map +1 -1
  74. package/build/types/segmentedControl/SegmentedControl.d.ts +2 -2
  75. package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -1
  76. package/build/types/select/Select.d.ts.map +1 -1
  77. package/build/types/statusIcon/StatusIcon.d.ts +1 -1
  78. package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
  79. package/build/types/stepper/deviceDetection.d.ts.map +1 -1
  80. package/build/types/uploadInput/uploadButton/UploadButton.d.ts.map +1 -1
  81. package/package.json +3 -3
  82. package/src/accordion/Accordion.story.tsx +1 -1
  83. package/src/alert/{Alert.spec.js → Alert.spec.tsx} +43 -40
  84. package/src/alert/Alert.story.tsx +1 -2
  85. package/src/alert/Alert.tsx +219 -0
  86. package/src/alert/index.ts +2 -0
  87. package/src/avatar/colors/colors.ts +1 -1
  88. package/src/body/Body.spec.tsx +1 -1
  89. package/src/body/Body.story.tsx +8 -8
  90. package/src/button/Button.tsx +6 -10
  91. package/src/checkbox/Checkbox.js +1 -1
  92. package/src/checkboxButton/CheckboxButton.spec.tsx +0 -1
  93. package/src/common/Option/Option.tsx +1 -1
  94. package/src/common/dateUtils/isWithinRange/isWithinRange.spec.ts +21 -0
  95. package/src/common/dateUtils/isWithinRange/isWithinRange.ts +2 -2
  96. package/src/common/dateUtils/moveToWithinRange/moveToWithinRange.ts +8 -4
  97. package/src/common/deviceDetection/deviceDetection.js +1 -1
  98. package/src/common/deviceDetection/deviceDetection.spec.js +4 -2
  99. package/src/common/propsValues/sentiment.ts +0 -10
  100. package/src/common/responsivePanel/ResponsivePanel.spec.js +11 -15
  101. package/src/dateLookup/DateLookup.state.spec.js +7 -0
  102. package/src/dateLookup/{DateLookup.story.js → DateLookup.story.tsx} +13 -14
  103. package/src/dateLookup/DateLookup.tests.story.tsx +70 -0
  104. package/src/dateLookup/{DateLookup.js → DateLookup.tsx} +115 -81
  105. package/src/dateLookup/dateHeader/{DateHeader.js → DateHeader.tsx} +15 -15
  106. package/src/dateLookup/dateTrigger/DateTrigger.spec.js +0 -22
  107. package/src/dateLookup/dateTrigger/{DateTrigger.js → DateTrigger.tsx} +15 -32
  108. package/src/dateLookup/dayCalendar/{DayCalendar.js → DayCalendar.tsx} +14 -21
  109. package/src/dateLookup/dayCalendar/table/{DayCalendarTable.js → DayCalendarTable.tsx} +26 -37
  110. package/src/dateLookup/getStartOfDay/{getStartOfDay.js → getStartOfDay.tsx} +1 -1
  111. package/src/dateLookup/index.ts +2 -0
  112. package/src/dateLookup/monthCalendar/{MonthCalendar.js → MonthCalendar.tsx} +19 -22
  113. package/src/dateLookup/monthCalendar/table/{MonthCalendarTable.js → MonthCalendarTable.tsx} +31 -30
  114. package/src/dateLookup/yearCalendar/{YearCalendar.js → YearCalendar.tsx} +18 -21
  115. package/src/dateLookup/yearCalendar/table/{YearCalendarTable.js → YearCalendarTable.tsx} +26 -28
  116. package/src/decision/Decision.spec.js +0 -1
  117. package/src/dimmer/Dimmer.tsx +6 -2
  118. package/src/i18n/th.json +2 -2
  119. package/src/index.ts +2 -2
  120. package/src/inlineAlert/InlineAlert.spec.tsx +0 -7
  121. package/src/inlineAlert/InlineAlert.story.tsx +8 -7
  122. package/src/inlineAlert/InlineAlert.tsx +19 -47
  123. package/src/inputs/InputGroup.tsx +3 -3
  124. package/src/inputs/SelectInput.tsx +2 -0
  125. package/src/inputs/_BottomSheet.tsx +21 -26
  126. package/src/inputs/_Popover.tsx +4 -4
  127. package/src/link/Link.story.tsx +16 -16
  128. package/src/loader/Loader.tsx +0 -1
  129. package/src/logo/Logo.css +16 -0
  130. package/src/logo/Logo.js +4 -9
  131. package/src/logo/Logo.less +16 -0
  132. package/src/logo/__snapshots__/Logo.spec.js.snap +104 -8
  133. package/src/main.css +16 -5
  134. package/src/main.less +0 -1
  135. package/src/moneyInput/MoneyInput.story.tsx +3 -3
  136. package/src/nudge/Nudge.spec.tsx +5 -5
  137. package/src/phoneNumberInput/PhoneNumberInput.tsx +2 -1
  138. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.ts +1 -1
  139. package/src/popover/Popover.tsx +2 -1
  140. package/src/promoCard/PromoCard.tsx +1 -1
  141. package/src/radioGroup/RadioGroup.spec.js +1 -1
  142. package/src/section/Section.story.tsx +2 -1
  143. package/src/segmentedControl/SegmentedControl.spec.tsx +88 -2
  144. package/src/segmentedControl/SegmentedControl.story.tsx +54 -16
  145. package/src/segmentedControl/SegmentedControl.tsx +21 -33
  146. package/src/select/Select.js +2 -3
  147. package/src/statusIcon/StatusIcon.tsx +14 -14
  148. package/src/stepper/deviceDetection.js +1 -2
  149. package/src/stepper/deviceDetection.spec.js +8 -3
  150. package/src/test-utils/index.js +1 -1
  151. package/src/test-utils/story-config.ts +1 -1
  152. package/src/title/Title.spec.tsx +1 -1
  153. package/src/typeahead/Typeahead.spec.js +4 -2
  154. package/src/upload/Upload.spec.js +8 -4
  155. package/src/uploadInput/uploadButton/UploadButton.tsx +1 -0
  156. package/build/styles/dynamicFieldDefinitionList/FormattedValue/FormattedValue.css +0 -5
  157. package/build/types/alert/withArrow/alertArrowPositions.d.ts +0 -9
  158. package/build/types/alert/withArrow/alertArrowPositions.d.ts.map +0 -1
  159. package/build/types/alert/withArrow/index.d.ts +0 -3
  160. package/build/types/alert/withArrow/index.d.ts.map +0 -1
  161. package/build/types/alert/withArrow/withArrow.d.ts +0 -11
  162. package/build/types/alert/withArrow/withArrow.d.ts.map +0 -1
  163. package/build/types/common/requirements.d.ts +0 -3
  164. package/build/types/common/requirements.d.ts.map +0 -1
  165. package/build/types/dynamicFieldDefinitionList/DynamicFieldDefinitionList.d.ts +0 -21
  166. package/build/types/dynamicFieldDefinitionList/DynamicFieldDefinitionList.d.ts.map +0 -1
  167. package/build/types/dynamicFieldDefinitionList/FormattedValue/FormattedValue.d.ts +0 -12
  168. package/build/types/dynamicFieldDefinitionList/FormattedValue/FormattedValue.d.ts.map +0 -1
  169. package/build/types/dynamicFieldDefinitionList/FormattedValue/index.d.ts +0 -2
  170. package/build/types/dynamicFieldDefinitionList/FormattedValue/index.d.ts.map +0 -1
  171. package/build/types/dynamicFieldDefinitionList/index.d.ts +0 -2
  172. package/build/types/dynamicFieldDefinitionList/index.d.ts.map +0 -1
  173. package/build/types/dynamicFieldDefinitionList/utils/createDefinitions.d.ts +0 -2
  174. package/build/types/dynamicFieldDefinitionList/utils/createDefinitions.d.ts.map +0 -1
  175. package/build/types/dynamicFieldDefinitionList/utils/text-format.d.ts +0 -2
  176. package/build/types/dynamicFieldDefinitionList/utils/text-format.d.ts.map +0 -1
  177. package/src/alert/Alert.js +0 -196
  178. package/src/alert/index.js +0 -1
  179. package/src/alert/withArrow/alertArrowPositions.ts +0 -9
  180. package/src/alert/withArrow/index.js +0 -2
  181. package/src/alert/withArrow/withArrow.js +0 -50
  182. package/src/alert/withArrow/withArrow.spec.js +0 -51
  183. package/src/dateLookup/index.js +0 -1
  184. package/src/dynamicFieldDefinitionList/DynamicFieldDefinitionList.js +0 -41
  185. package/src/dynamicFieldDefinitionList/DynamicFieldDefinitionList.spec.js +0 -21
  186. package/src/dynamicFieldDefinitionList/DynamicFieldDefinitionList.story.js +0 -134
  187. package/src/dynamicFieldDefinitionList/FormattedValue/FormattedValue.css +0 -5
  188. package/src/dynamicFieldDefinitionList/FormattedValue/FormattedValue.js +0 -73
  189. package/src/dynamicFieldDefinitionList/FormattedValue/FormattedValue.less +0 -4
  190. package/src/dynamicFieldDefinitionList/FormattedValue/FormattedValue.spec.js +0 -200
  191. package/src/dynamicFieldDefinitionList/FormattedValue/index.js +0 -1
  192. package/src/dynamicFieldDefinitionList/index.js +0 -1
  193. package/src/dynamicFieldDefinitionList/utils/createDefinitions.js +0 -33
  194. package/src/dynamicFieldDefinitionList/utils/createDefinitions.spec.js +0 -83
  195. package/src/dynamicFieldDefinitionList/utils/text-format.js +0 -46
  196. package/src/dynamicFieldDefinitionList/utils/text-format.spec.js +0 -43
  197. /package/src/dateLookup/{DateLookup.messages.js → DateLookup.messages.ts} +0 -0
  198. /package/src/dateLookup/dateHeader/{index.js → index.ts} +0 -0
  199. /package/src/dateLookup/dateTrigger/{index.js → index.ts} +0 -0
  200. /package/src/dateLookup/dayCalendar/{index.js → index.ts} +0 -0
  201. /package/src/dateLookup/dayCalendar/table/{index.js → index.ts} +0 -0
  202. /package/src/dateLookup/getStartOfDay/{index.js → index.ts} +0 -0
  203. /package/src/dateLookup/monthCalendar/{index.js → index.ts} +0 -0
  204. /package/src/dateLookup/monthCalendar/table/{index.js → index.ts} +0 -0
  205. /package/src/dateLookup/yearCalendar/{index.js → index.ts} +0 -0
  206. /package/src/dateLookup/yearCalendar/table/{index.js → index.ts} +0 -0
package/src/main.css CHANGED
@@ -1857,11 +1857,6 @@ button.np-option {
1857
1857
  transition: opacity 150ms ease-in, height 150ms ease-in 150ms;
1858
1858
  opacity: 0;
1859
1859
  }
1860
- .formatted-value__h3-custom-alignment {
1861
- margin-bottom: 16px;
1862
- margin-bottom: var(--size-16);
1863
- line-height: 24px;
1864
- }
1865
1860
  .emphasis {
1866
1861
  font-weight: 600;
1867
1862
  font-weight: var(--font-weight-semi-bold);
@@ -3028,6 +3023,22 @@ a {
3028
3023
  .np-theme-personal--dark .np-logo-svg path {
3029
3024
  fill: var(--color-white);
3030
3025
  }
3026
+ .np-logo-svg--size-sm {
3027
+ display: block;
3028
+ }
3029
+ @media (min-width: 576px) {
3030
+ .np-logo-svg--size-sm {
3031
+ display: none;
3032
+ }
3033
+ }
3034
+ .np-logo-svg--size-md {
3035
+ display: none;
3036
+ }
3037
+ @media (min-width: 576px) {
3038
+ .np-logo-svg--size-md {
3039
+ display: block;
3040
+ }
3041
+ }
3031
3042
  .tw-modal--scrollable {
3032
3043
  max-height: 100%;
3033
3044
  /* mobile viewport bug fix */
package/src/main.less CHANGED
@@ -22,7 +22,6 @@
22
22
  @import "./dimmer/Dimmer.less";
23
23
  @import "./drawer/Drawer.less";
24
24
  @import "./dropFade/DropFade.less";
25
- @import "./dynamicFieldDefinitionList/FormattedValue/FormattedValue.less";
26
25
  @import "./emphasis/Emphasis.less";
27
26
  @import "./flowNavigation/animatedLabel/AnimatedLabel.less";
28
27
  @import "./flowNavigation/backButton/BackButton.less";
@@ -16,7 +16,7 @@ export default {
16
16
 
17
17
  return (
18
18
  <>
19
- <label htmlFor={args.id as string}>Editable money input label</label>
19
+ <label htmlFor={args.id}>Editable money input label</label>
20
20
  <MoneyInput
21
21
  {...args}
22
22
  selectedCurrency={selectedCurrency}
@@ -160,13 +160,13 @@ export const SmallInput: Story = {
160
160
  render: (args) => {
161
161
  return (
162
162
  <>
163
- <label htmlFor={args.id as string}>Money inputs</label>
163
+ <label htmlFor={args.id}>Money inputs</label>
164
164
  <MoneyInput {...args} {...SingleCurrency.args} />
165
165
  <br />
166
166
  <MoneyInput {...args} {...MultipleCurrencies.args} />
167
167
  <hr />
168
168
  <div className="has-error">
169
- <label htmlFor={args.id as string}>Error states</label>
169
+ <label htmlFor={args.id}>Error states</label>
170
170
  <MoneyInput {...args} {...SingleCurrency.args} />
171
171
  <br />
172
172
  <MoneyInput {...args} {...MultipleCurrencies.args} />
@@ -51,14 +51,14 @@ describe('Nudge', () => {
51
51
  });
52
52
 
53
53
  it('does NOT render a nudge if localStorage has been set with the id of the nudge', () => {
54
- jest.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue('["CAKE","TEST"]');
54
+ jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('["CAKE","TEST"]');
55
55
 
56
56
  render(<Nudge {...defaultProps} id="TEST" persistDismissal />);
57
57
  expect(screen.queryByText('A nudge title')).not.toBeInTheDocument();
58
58
  });
59
59
 
60
60
  it('does NOT render a nudge if localStorage has been set with the id of the nudge AND calls is previously dismissed with TRUE', () => {
61
- jest.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue('["CAKE","TEST"]');
61
+ jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('["CAKE","TEST"]');
62
62
  const isPreviouslyDismissed = jest.fn();
63
63
 
64
64
  render(
@@ -75,7 +75,7 @@ describe('Nudge', () => {
75
75
  });
76
76
 
77
77
  it('shows a nudge if localStorage has been set with a different id for a different nudge and calls is previously dismissed with FALSE', () => {
78
- jest.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue('["BANANA"]');
78
+ jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('["BANANA"]');
79
79
  const isPreviouslyDismissed = jest.fn();
80
80
 
81
81
  render(
@@ -92,8 +92,8 @@ describe('Nudge', () => {
92
92
  });
93
93
 
94
94
  it('calls local storage with updated dismissed nudges value', () => {
95
- jest.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue('["BANANA"]');
96
- const setItem = jest.spyOn(window.localStorage.__proto__, 'setItem');
95
+ jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('["BANANA"]');
96
+ const setItem = jest.spyOn(Storage.prototype, 'setItem');
97
97
 
98
98
  render(<Nudge {...defaultProps} id="TEST" persistDismissal />);
99
99
  expect(screen.getByText('A nudge title')).toBeInTheDocument();
@@ -110,7 +110,8 @@ const PhoneNumberInput = ({
110
110
 
111
111
  useEffect(() => {
112
112
  if (broadcastedValue === null) {
113
- return setBroadcastedValue(internalValue);
113
+ setBroadcastedValue(internalValue);
114
+ return;
114
115
  }
115
116
 
116
117
  const internalPhoneNumber = `${internalValue.prefix ?? ''}${internalValue.suffix}`;
@@ -1,3 +1,3 @@
1
- const DIGITS_MATCH = /^$|^(\+)|([\d]+)/g;
1
+ const DIGITS_MATCH = /^$|^(\+)|(\d+)/g;
2
2
 
3
3
  export const cleanNumber = (number: string) => number.match(DIGITS_MATCH)?.join('') ?? '';
@@ -38,8 +38,9 @@ function resolvePlacement(preferredPlacement: PopoverPreferredPlacement) {
38
38
  case 'bottom-left':
39
39
  case 'bottom-right':
40
40
  return 'bottom';
41
+ default:
42
+ return preferredPlacement;
41
43
  }
42
- return preferredPlacement;
43
44
  }
44
45
 
45
46
  export default function Popover({
@@ -298,7 +298,7 @@ const PromoCard: FunctionComponent<PromoCardProps> = forwardRef(
298
298
  ...commonProps,
299
299
  'aria-checked':
300
300
  type === 'radio' ? value === state : type === 'checkbox' ? checked : undefined,
301
- 'aria-describedby': `${componentId}-title` || undefined,
301
+ 'aria-describedby': `${componentId}-title`,
302
302
  'aria-disabled': isDisabled,
303
303
  'data-value': value ?? undefined,
304
304
  role: type === 'checkbox' || type === 'radio' ? type : undefined,
@@ -34,7 +34,7 @@ const RADIOS = [
34
34
 
35
35
  describe('RadioGroup', () => {
36
36
  it('renders null if no radios are provided', () => {
37
- expect(shallow(<RadioGroup radios={[]} {...props} />).toBeNully);
37
+ expect(shallow(<RadioGroup radios={[]} {...props} />).isEmptyRender()).toBe(true);
38
38
  });
39
39
 
40
40
  it('renders radio options', () => {
@@ -74,7 +74,8 @@ export const WithCards = () => {
74
74
 
75
75
  const handleOnCardClick = (index: number) => {
76
76
  if (expandedCardIndex === index) {
77
- return setExpandedCardIndex(null);
77
+ setExpandedCardIndex(null);
78
+ return;
78
79
  }
79
80
  setExpandedCardIndex(index);
80
81
  };
@@ -1,5 +1,7 @@
1
1
  import '@testing-library/jest-dom';
2
- import { render, screen, userEvent } from '../test-utils';
2
+ import React from 'react';
3
+
4
+ import { render, screen, userEvent, waitFor } from '../test-utils';
3
5
 
4
6
  import SegmentedControl, { SegmentedControlProps } from './SegmentedControl';
5
7
 
@@ -30,7 +32,7 @@ const onChange = jest.fn();
30
32
 
31
33
  const defaultProps: SegmentedControlProps = {
32
34
  name: 'segmentedControl',
33
- defaultValue: defaultSegments[0].value,
35
+ value: defaultSegments[0].value,
34
36
  mode: 'input',
35
37
  segments: defaultSegments,
36
38
  onChange,
@@ -86,6 +88,90 @@ describe('SegmentedControl', () => {
86
88
  expect(onChange).toHaveBeenCalledWith('reporting');
87
89
  });
88
90
 
91
+ it('does not call onChange on mount', async () => {
92
+ let onChangeCallCount = 0;
93
+
94
+ const ParentComponent = () => {
95
+ const [_, simulateRerender] = React.useState({});
96
+
97
+ // new function is created on every render
98
+ const onChange = () => {
99
+ onChangeCallCount += 1;
100
+ simulateRerender({});
101
+ };
102
+
103
+ return <SegmentedControl {...defaultProps} onChange={onChange} />;
104
+ };
105
+
106
+ render(<ParentComponent />);
107
+
108
+ await waitFor(() => {
109
+ expect(onChangeCallCount).toBe(0);
110
+ });
111
+ });
112
+
113
+ it('does not repeatedly call the onChange when the provided onChange prop changes on every render of its parent component', async () => {
114
+ let onChangeCallCount = 0;
115
+
116
+ const ParentComponent = () => {
117
+ const [_, simulateRerender] = React.useState({});
118
+
119
+ // a new onChange function is created on every render
120
+ const onChange = () => {
121
+ onChangeCallCount += 1;
122
+ simulateRerender({});
123
+ };
124
+
125
+ return <SegmentedControl {...defaultProps} onChange={onChange} />;
126
+ };
127
+
128
+ const { rerender } = render(<ParentComponent />);
129
+
130
+ rerender(<ParentComponent />);
131
+
132
+ await waitFor(() => {
133
+ expect(onChangeCallCount).toBe(0);
134
+ });
135
+ });
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} value="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
+
89
175
  it('throws error if user tries to add too many segments', () => {
90
176
  expect(() => {
91
177
  renderSegmentedControl({
@@ -1,40 +1,78 @@
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
- const [selectedValue, setSelectedValue] = React.useState(segments[0].value);
26
+ const [value, setValue] = 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}
31
- onChange={setSelectedValue}
33
+ value={value}
34
+ onChange={setValue}
32
35
  {...(args.mode === 'view'
33
36
  ? { segments: segmentsWithControls, mode: 'view', controls: 'aControlId' }
34
37
  : { segments, mode: 'input' })}
35
38
  />
36
39
  <div className="m-a-2" id="aControlId">
37
- <p>Selected value: {selectedValue}</p>
40
+ <p>Selected value: {value}</p>
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
+ setValue(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 !== value);
70
+ setSegments((prev) => prev.filter((_, i) => i !== index));
71
+ setSegmentsWithControls((prev) => prev.filter((_, i) => i !== index));
72
+ }}
73
+ >
74
+ Remove one segment
75
+ </Button>
38
76
  </div>
39
77
  </div>
40
78
  );
@@ -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
+ value: 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
+ value,
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);
@@ -55,9 +54,7 @@ const SegmentedControl = ({
55
54
  }));
56
55
 
57
56
  const updateSegmentPosition = () => {
58
- const selectedSegmentRef = segmentsWithRefs.find(
59
- (segment) => segment.value === selectedValue,
60
- )?.ref;
57
+ const selectedSegmentRef = segmentsWithRefs.find((segment) => segment.value === value)?.ref;
61
58
 
62
59
  // We grab the active segments style object from the ref
63
60
  // and set the css variables to the selected segments width and x position.
@@ -70,6 +67,7 @@ const SegmentedControl = ({
70
67
  };
71
68
 
72
69
  useEffect(() => {
70
+ setAnimate(true);
73
71
  updateSegmentPosition();
74
72
 
75
73
  const handleWindowSizeChange = () => {
@@ -83,11 +81,7 @@ const SegmentedControl = ({
83
81
  };
84
82
 
85
83
  // eslint-disable-next-line react-hooks/exhaustive-deps
86
- }, [segmentsWithRefs, selectedValue]);
87
-
88
- useEffect(() => {
89
- onChange(selectedValue);
90
- }, [onChange, selectedValue]);
84
+ }, [segmentsWithRefs, value]);
91
85
 
92
86
  return (
93
87
  <div
@@ -103,14 +97,18 @@ const SegmentedControl = ({
103
97
  })}
104
98
  role={mode !== 'input' ? 'tablist' : undefined}
105
99
  >
106
- {segmentsWithRefs.map((segment) =>
107
- mode === 'input' ? (
100
+ {segmentsWithRefs.map((segment) => {
101
+ const onSelect = () => {
102
+ setAnimate(true);
103
+ onChange(segment.value);
104
+ };
105
+ return mode === 'input' ? (
108
106
  <label
109
107
  ref={segment.ref as React.RefObject<HTMLLabelElement>}
110
108
  key={segment.id}
111
109
  htmlFor={segment.id}
112
110
  className={classNames('segmented-control__segment', {
113
- 'segmented-control__selected-segment': selectedValue === segment.value,
111
+ 'segmented-control__selected-segment': value === segment.value,
114
112
  })}
115
113
  >
116
114
  <input
@@ -119,19 +117,14 @@ const SegmentedControl = ({
119
117
  id={segment.id}
120
118
  name={name}
121
119
  value={segment.value}
122
- checked={selectedValue === segment.value}
123
- onChange={() => {
124
- setAnimate(true);
125
- setSelectedValue(segment.value);
126
- }}
120
+ checked={value === segment.value}
121
+ onChange={onSelect}
127
122
  />
128
123
  <Body
129
124
  className="segmented-control__text"
130
125
  as="span"
131
126
  type={
132
- selectedValue === segment.value
133
- ? Typography.BODY_DEFAULT_BOLD
134
- : Typography.BODY_DEFAULT
127
+ value === segment.value ? Typography.BODY_DEFAULT_BOLD : Typography.BODY_DEFAULT
135
128
  }
136
129
  >
137
130
  {segment.label}
@@ -145,29 +138,24 @@ const SegmentedControl = ({
145
138
  role="tab"
146
139
  id={segment.id}
147
140
  aria-controls={segment.controls}
148
- aria-selected={selectedValue === segment.value}
141
+ aria-selected={value === segment.value}
149
142
  className={classNames('segmented-control__segment', 'segmented-control__button', {
150
- 'segmented-control__selected-segment': selectedValue === segment.value,
143
+ 'segmented-control__selected-segment': value === segment.value,
151
144
  })}
152
- onClick={() => {
153
- setAnimate(true);
154
- setSelectedValue(segment.value);
155
- }}
145
+ onClick={onSelect}
156
146
  >
157
147
  <Body
158
148
  as="span"
159
149
  className="segmented-control__text"
160
150
  type={
161
- selectedValue === segment.value
162
- ? Typography.BODY_DEFAULT_BOLD
163
- : Typography.BODY_DEFAULT
151
+ value === segment.value ? Typography.BODY_DEFAULT_BOLD : Typography.BODY_DEFAULT
164
152
  }
165
153
  >
166
154
  {segment.label}
167
155
  </Body>
168
156
  </button>
169
- ),
170
- )}
157
+ );
158
+ })}
171
159
  </div>
172
160
  </div>
173
161
  );
@@ -246,8 +246,8 @@ export default function Select({
246
246
  };
247
247
 
248
248
  function selectKeyboardFocusedOption() {
249
- if (keyboardFocusedOptionIndex != null) {
250
- selectableOptions.length > 0 && selectOption(selectableOptions[keyboardFocusedOptionIndex]);
249
+ if (keyboardFocusedOptionIndex != null && selectableOptions.length > 0) {
250
+ selectOption(selectableOptions[keyboardFocusedOptionIndex]);
251
251
  }
252
252
  }
253
253
 
@@ -511,7 +511,6 @@ export default function Select({
511
511
  disabled={disabled}
512
512
  aria-controls={listboxId}
513
513
  aria-expanded={open}
514
- aria-autocomplete="none"
515
514
  onClick={handleOnClick}
516
515
  {...buttonProps}
517
516
  >
@@ -1,27 +1,27 @@
1
1
  import { Info, Alert, Cross, Check, ClockBorderless } from '@transferwise/icons';
2
2
  import classNames from 'classnames';
3
3
 
4
- import { SizeSmall, SizeMedium, SizeLarge, Sentiment, Size } from '../common';
4
+ import { SizeSmall, SizeMedium, SizeLarge, Sentiment } from '../common';
5
5
 
6
6
  export type StatusIconProps = {
7
- sentiment: Sentiment;
7
+ sentiment: `${Sentiment}`;
8
8
  size: SizeSmall | SizeMedium | SizeLarge;
9
9
  };
10
10
 
11
- const StatusIcon = ({ sentiment = Sentiment.NEUTRAL, size = Size.MEDIUM }: StatusIconProps) => {
12
- const iconTypeMap = {
13
- [Sentiment.POSITIVE]: Check,
14
- [Sentiment.NEUTRAL]: Info,
15
- [Sentiment.WARNING]: Alert,
16
- [Sentiment.NEGATIVE]: Cross,
17
- [Sentiment.PENDING]: ClockBorderless,
18
- [Sentiment.INFO]: Info,
19
- [Sentiment.ERROR]: Cross,
20
- [Sentiment.SUCCESS]: Check,
21
- };
11
+ const iconTypeMap = {
12
+ positive: Check,
13
+ neutral: Info,
14
+ warning: Alert,
15
+ negative: Cross,
16
+ pending: ClockBorderless,
17
+ info: Info,
18
+ error: Cross,
19
+ success: Check,
20
+ } satisfies Record<`${Sentiment}`, React.ElementType>;
22
21
 
23
- const iconColor = [Sentiment.WARNING, Sentiment.PENDING].includes(sentiment) ? 'dark' : 'light';
22
+ const StatusIcon = ({ sentiment = 'neutral', size = 'md' }: StatusIconProps) => {
24
23
  const Icon = iconTypeMap[sentiment];
24
+ const iconColor = sentiment === 'warning' || sentiment === 'pending' ? 'dark' : 'light';
25
25
 
26
26
  return (
27
27
  <span
@@ -1,6 +1,5 @@
1
1
  function supportsTouchEvents() {
2
2
  const onTouchStartIsDefined = typeof window !== 'undefined' && window.ontouchstart !== undefined;
3
- // eslint-disable-next-line compat/compat
4
3
  const maxTouchPointsIsDefined = typeof navigator !== 'undefined' && navigator.maxTouchPoints;
5
4
  const documentTouchIsDefined =
6
5
  typeof window !== 'undefined' &&
@@ -21,7 +20,7 @@ function userAgentSuggestsTouchDevice() {
21
20
  'bada',
22
21
  ];
23
22
  const matchString = sampleTouchDevices.map((device) => `(${device})`).join('|');
24
- const regex = new RegExp(matchString, 'ig');
23
+ const regex = new RegExp(matchString, 'gi');
25
24
  return typeof navigator !== 'undefined' && !!navigator.userAgent.match(regex);
26
25
  }
27
26
  // Important: this is not fool-proof! It gives false positives and negatives, and will be outdated.
@@ -2,12 +2,17 @@ import { isTouchDevice } from './deviceDetection';
2
2
 
3
3
  describe('Device detection', () => {
4
4
  function fakeUserAgent(userAgent) {
5
- navigator.__defineGetter__('userAgent', () => userAgent);
6
- // need to use this instead of defineProperty, as it's blocked from overriding
5
+ Object.defineProperty(navigator, 'userAgent', {
6
+ value: userAgent,
7
+ configurable: true,
8
+ });
7
9
  }
8
10
 
9
11
  function fakeMaxTouchPoints(maxTouchPoints) {
10
- navigator.__defineGetter__('maxTouchPoints', () => maxTouchPoints);
12
+ Object.defineProperty(navigator, 'maxTouchPoints', {
13
+ value: maxTouchPoints,
14
+ configurable: true,
15
+ });
11
16
  }
12
17
 
13
18
  // 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
- var Wrapper = ({ children }) => {
16
+ const 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 parseInt(viewport.styles.width);
33
+ return Number.parseInt(viewport.styles.width, 10);
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-ignore
37
+ // @ts-expect-error
38
38
  render(<Title type={Typography.BODY_LARGE_BOLD}>Test</Title>);
39
39
  const copy = screen.getByText('Test');
40
40
  expect(copy).toBeInTheDocument();
@@ -314,7 +314,9 @@ describe('Typeahead', () => {
314
314
  let selectedOption;
315
315
 
316
316
  component.setProps({
317
- onChange: (selections) => (selectedOption = selections[0]),
317
+ onChange: (selections) => {
318
+ selectedOption = selections[0];
319
+ },
318
320
  options: options,
319
321
  });
320
322
 
@@ -340,7 +342,7 @@ describe('Typeahead', () => {
340
342
  it('renders all options', () => {
341
343
  const options = option().map((optNode) => optNode.text());
342
344
  expect(options).toHaveLength(props.options.length);
343
- expect(options.every((label, i) => label === props.options[i].label));
345
+ expect(options.every((label, i) => label === props.options[i].label)).toBe(true);
344
346
  });
345
347
 
346
348
  it('does not render new option if showNewEntry is false', () => {