@transferwise/components 46.140.0 → 46.141.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 (151) hide show
  1. package/build/avatarWrapper/AvatarWrapper.js +3 -4
  2. package/build/avatarWrapper/AvatarWrapper.js.map +1 -1
  3. package/build/avatarWrapper/AvatarWrapper.mjs +4 -5
  4. package/build/avatarWrapper/AvatarWrapper.mjs.map +1 -1
  5. package/build/button/LegacyButton.js.map +1 -1
  6. package/build/button/LegacyButton.mjs.map +1 -1
  7. package/build/common/hooks/useHasIntersected/useHasIntersected.js +6 -4
  8. package/build/common/hooks/useHasIntersected/useHasIntersected.js.map +1 -1
  9. package/build/common/hooks/useHasIntersected/useHasIntersected.mjs +6 -4
  10. package/build/common/hooks/useHasIntersected/useHasIntersected.mjs.map +1 -1
  11. package/build/common/liveRegion/LiveRegion.js +4 -1
  12. package/build/common/liveRegion/LiveRegion.js.map +1 -1
  13. package/build/common/liveRegion/LiveRegion.mjs +4 -1
  14. package/build/common/liveRegion/LiveRegion.mjs.map +1 -1
  15. package/build/dateInput/DateInput.js +10 -10
  16. package/build/dateInput/DateInput.js.map +1 -1
  17. package/build/dateInput/DateInput.mjs +10 -10
  18. package/build/dateInput/DateInput.mjs.map +1 -1
  19. package/build/dateLookup/DateLookup.js +1 -1
  20. package/build/dateLookup/DateLookup.js.map +1 -1
  21. package/build/dateLookup/DateLookup.mjs +1 -1
  22. package/build/dateLookup/DateLookup.mjs.map +1 -1
  23. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.js +1 -1
  24. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.js.map +1 -1
  25. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs +1 -1
  26. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs.map +1 -1
  27. package/build/dateLookup/yearCalendar/table/YearCalendarTable.js +1 -1
  28. package/build/dateLookup/yearCalendar/table/YearCalendarTable.js.map +1 -1
  29. package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs +1 -1
  30. package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs.map +1 -1
  31. package/build/expressiveMoneyInput/ExpressiveMoneyInput.js.map +1 -1
  32. package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs.map +1 -1
  33. package/build/expressiveMoneyInput/amountInput/AmountInput.js +17 -11
  34. package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -1
  35. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +18 -12
  36. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs.map +1 -1
  37. package/build/expressiveMoneyInput/hooks/useInputStyle.js +8 -6
  38. package/build/expressiveMoneyInput/hooks/useInputStyle.js.map +1 -1
  39. package/build/expressiveMoneyInput/hooks/useInputStyle.mjs +9 -7
  40. package/build/expressiveMoneyInput/hooks/useInputStyle.mjs.map +1 -1
  41. package/build/header/Header.js +1 -1
  42. package/build/header/Header.js.map +1 -1
  43. package/build/header/Header.mjs +1 -1
  44. package/build/header/Header.mjs.map +1 -1
  45. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.js.map +1 -1
  46. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.mjs.map +1 -1
  47. package/build/inputs/SelectInput/Options/SelectInputOptions.js +34 -22
  48. package/build/inputs/SelectInput/Options/SelectInputOptions.js.map +1 -1
  49. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs +35 -23
  50. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs.map +1 -1
  51. package/build/inputs/SelectInput/Popover/SelectInputPopover.js.map +1 -1
  52. package/build/inputs/SelectInput/Popover/SelectInputPopover.mjs.map +1 -1
  53. package/build/inputs/SelectInput/SelectInput.js +8 -6
  54. package/build/inputs/SelectInput/SelectInput.js.map +1 -1
  55. package/build/inputs/SelectInput/SelectInput.mjs +9 -7
  56. package/build/inputs/SelectInput/SelectInput.mjs.map +1 -1
  57. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
  58. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
  59. package/build/main.css +58 -53
  60. package/build/nudge/Nudge.js +31 -15
  61. package/build/nudge/Nudge.js.map +1 -1
  62. package/build/nudge/Nudge.mjs +32 -16
  63. package/build/nudge/Nudge.mjs.map +1 -1
  64. package/build/phoneNumberInput/PhoneNumberInput.js +9 -12
  65. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  66. package/build/phoneNumberInput/PhoneNumberInput.mjs +9 -12
  67. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  68. package/build/promoCard/PromoCardGroup.js +34 -16
  69. package/build/promoCard/PromoCardGroup.js.map +1 -1
  70. package/build/promoCard/PromoCardGroup.mjs +35 -17
  71. package/build/promoCard/PromoCardGroup.mjs.map +1 -1
  72. package/build/segmentedControl/SegmentedControl.js +6 -1
  73. package/build/segmentedControl/SegmentedControl.js.map +1 -1
  74. package/build/segmentedControl/SegmentedControl.mjs +7 -2
  75. package/build/segmentedControl/SegmentedControl.mjs.map +1 -1
  76. package/build/styles/css/neptune.css +58 -53
  77. package/build/styles/less/neptune-tokens.less +2 -2
  78. package/build/styles/main.css +58 -53
  79. package/build/styles/props/neptune-tokens.css +1 -1
  80. package/build/styles/styles/less/core/viewport-themes.css +46 -42
  81. package/build/styles/styles/less/neptune.css +58 -53
  82. package/build/tabs/Tabs.js +1 -1
  83. package/build/tabs/Tabs.js.map +1 -1
  84. package/build/tabs/Tabs.mjs +1 -1
  85. package/build/tabs/Tabs.mjs.map +1 -1
  86. package/build/tooltip/Tooltip.js +6 -3
  87. package/build/tooltip/Tooltip.js.map +1 -1
  88. package/build/tooltip/Tooltip.mjs +6 -3
  89. package/build/tooltip/Tooltip.mjs.map +1 -1
  90. package/build/types/avatarWrapper/AvatarWrapper.d.ts.map +1 -1
  91. package/build/types/common/hooks/useHasIntersected/useHasIntersected.d.ts.map +1 -1
  92. package/build/types/common/liveRegion/LiveRegion.d.ts.map +1 -1
  93. package/build/types/dateLookup/monthCalendar/table/MonthCalendarTable.d.ts.map +1 -1
  94. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts.map +1 -1
  95. package/build/types/expressiveMoneyInput/amountInput/AmountInput.d.ts.map +1 -1
  96. package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts +2 -2
  97. package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts.map +1 -1
  98. package/build/types/expressiveMoneyInput/hooks/useSelectionRange.d.ts.map +1 -1
  99. package/build/types/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.d.ts.map +1 -1
  100. package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts.map +1 -1
  101. package/build/types/inputs/SelectInput/Popover/SelectInputPopover.d.ts.map +1 -1
  102. package/build/types/inputs/SelectInput/SelectInput.d.ts.map +1 -1
  103. package/build/types/nudge/Nudge.d.ts.map +1 -1
  104. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  105. package/build/types/promoCard/PromoCardGroup.d.ts.map +1 -1
  106. package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -1
  107. package/build/types/tooltip/Tooltip.d.ts.map +1 -1
  108. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  109. package/build/uploadInput/UploadInput.js +29 -25
  110. package/build/uploadInput/UploadInput.js.map +1 -1
  111. package/build/uploadInput/UploadInput.mjs +29 -25
  112. package/build/uploadInput/UploadInput.mjs.map +1 -1
  113. package/package.json +3 -3
  114. package/src/avatarWrapper/AvatarWrapper.test.tsx +33 -3
  115. package/src/avatarWrapper/AvatarWrapper.tsx +5 -6
  116. package/src/button/LegacyButton.tsx +1 -1
  117. package/src/button/_stories/Button.test.story.tsx +3 -3
  118. package/src/common/hooks/useContainerSize.test.tsx +1 -1
  119. package/src/common/hooks/useHasIntersected/useHasIntersected.ts +12 -4
  120. package/src/common/liveRegion/LiveRegion.tsx +5 -2
  121. package/src/dateInput/DateInput.tsx +10 -10
  122. package/src/dateLookup/DateLookup.test.story.tsx +16 -0
  123. package/src/dateLookup/DateLookup.tsx +1 -1
  124. package/src/dateLookup/monthCalendar/table/MonthCalendarTable.tsx +1 -5
  125. package/src/dateLookup/yearCalendar/table/YearCalendarTable.tsx +1 -1
  126. package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +1 -1
  127. package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +22 -15
  128. package/src/expressiveMoneyInput/hooks/useInputStyle.ts +20 -8
  129. package/src/expressiveMoneyInput/hooks/useSelectionRange.ts +2 -0
  130. package/src/header/Header.tsx +2 -2
  131. package/src/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.tsx +4 -0
  132. package/src/inputs/SelectInput/Options/SelectInputOptions.tsx +43 -27
  133. package/src/inputs/SelectInput/Popover/SelectInputPopover.tsx +4 -0
  134. package/src/inputs/SelectInput/SelectInput.tsx +21 -15
  135. package/src/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +1 -1
  136. package/src/main.css +58 -53
  137. package/src/nudge/Nudge.tsx +29 -20
  138. package/src/phoneNumberInput/PhoneNumberInput.test.tsx +16 -0
  139. package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -13
  140. package/src/promoCard/PromoCard.story.tsx +3 -3
  141. package/src/promoCard/PromoCardGroup.tsx +39 -21
  142. package/src/segmentedControl/SegmentedControl.test.tsx +25 -0
  143. package/src/segmentedControl/SegmentedControl.tsx +7 -1
  144. package/src/select/Select.story.tsx +1 -1
  145. package/src/styles/less/core/viewport-themes.css +46 -42
  146. package/src/styles/less/core/viewport-themes.less +2 -45
  147. package/src/styles/less/neptune.css +58 -53
  148. package/src/tabs/Tabs.tsx +1 -1
  149. package/src/tooltip/Tooltip.tsx +3 -0
  150. package/src/uploadInput/UploadInput.test.tsx +19 -0
  151. package/src/uploadInput/UploadInput.tsx +28 -24
@@ -1,6 +1,6 @@
1
1
  import { Illustration, Assets, type IllustrationNames } from '@wise/art';
2
2
  import { clsx } from 'clsx';
3
- import { ReactNode, useEffect, useState, MouseEvent } from 'react';
3
+ import { ReactNode, useEffect, useState, MouseEvent, useCallback } from 'react';
4
4
 
5
5
  import Body from '../body';
6
6
  import { Typography } from '../common';
@@ -96,8 +96,27 @@ const Nudge = ({
96
96
  action,
97
97
  }: Props) => {
98
98
  const intl = useIntl();
99
- const [isDismissed, setIsDismissed] = useState(false);
100
- const [isMounted, setIsMounted] = useState(false);
99
+ const getIsDismissed = useCallback(
100
+ () => (persistDismissal && id ? !!getLocalStorage()?.find((item) => item === id) : false),
101
+ [persistDismissal, id],
102
+ );
103
+
104
+ const [nudgeState, setNudgeState] = useState(() => ({
105
+ isDismissed: getIsDismissed(),
106
+ isMounted: false,
107
+ }));
108
+
109
+ useEffect(() => {
110
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Setting mount state in mount effect
111
+ setNudgeState((prev) => ({ ...prev, isMounted: true }));
112
+ }, []);
113
+
114
+ useEffect(() => {
115
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing dismissed state from localStorage on prop change
116
+ setNudgeState((prev) => ({ ...prev, isDismissed: getIsDismissed() }));
117
+ }, [getIsDismissed, id, persistDismissal]);
118
+
119
+ const { isDismissed } = nudgeState;
101
120
 
102
121
  const handleOnDismiss = () => {
103
122
  const dismissedNudgesStorage = getLocalStorage();
@@ -105,9 +124,9 @@ const Nudge = ({
105
124
  if (persistDismissal && id) {
106
125
  try {
107
126
  localStorage.setItem(STORAGE_NAME, JSON.stringify([...dismissedNudgesStorage, id]));
108
- } catch (error) {}
127
+ } catch {}
109
128
 
110
- setIsDismissed(true);
129
+ setNudgeState((prev) => ({ ...prev, isDismissed: true }));
111
130
  }
112
131
 
113
132
  if (onDismiss) {
@@ -116,25 +135,15 @@ const Nudge = ({
116
135
  };
117
136
 
118
137
  useEffect(() => {
119
- if (persistDismissal && id) {
138
+ if (persistDismissal && id && isPreviouslyDismissed) {
120
139
  const dismissedNudgesStorage = getLocalStorage();
121
- let isDismissed = false;
122
-
123
- if (dismissedNudgesStorage?.find((item) => item === id)) {
124
- setIsDismissed(true);
125
- isDismissed = true;
126
- }
127
-
128
- if (isPreviouslyDismissed) {
129
- isPreviouslyDismissed(isDismissed);
130
- }
140
+ const wasDismissed = !!dismissedNudgesStorage?.find((item) => item === id);
141
+ isPreviouslyDismissed(wasDismissed);
131
142
  }
132
-
133
- setIsMounted(true);
134
143
  // eslint-disable-next-line react-hooks/exhaustive-deps
135
144
  }, [id, persistDismissal]);
136
145
 
137
- if (persistDismissal && (isDismissed || !isMounted)) {
146
+ if (persistDismissal && (isDismissed || !nudgeState.isMounted)) {
138
147
  return null;
139
148
  }
140
149
 
@@ -143,7 +152,7 @@ const Nudge = ({
143
152
  {!!mediaName && (
144
153
  <div className="wds-nudge-media">
145
154
  <Illustration
146
- name={mediaName as IllustrationNames}
155
+ name={mediaName}
147
156
  className={clsx(`wds-nudge-media-${mediaName}`)}
148
157
  size="small"
149
158
  disablePadding
@@ -283,6 +283,22 @@ describe('PhoneNumberInput', () => {
283
283
  expect(props.onChange).toHaveBeenCalledWith('+201111111', '+20');
284
284
  });
285
285
  });
286
+
287
+ describe('onChange deduplication', () => {
288
+ it('should not call onChange when the composed phone number has not changed', () => {
289
+ const onChangeMock = jest.fn();
290
+ const { rerender } = render(
291
+ <PhoneNumberInput initialValue="+441234567890" onChange={onChangeMock} />,
292
+ );
293
+
294
+ onChangeMock.mockClear();
295
+
296
+ // Rerender with the same initialValue - internal state should not trigger onChange
297
+ rerender(<PhoneNumberInput initialValue="+441234567890" onChange={onChangeMock} />);
298
+
299
+ expect(onChangeMock).not.toHaveBeenCalled();
300
+ });
301
+ });
286
302
  });
287
303
 
288
304
  describe('when selectProps is supplied', () => {
@@ -75,12 +75,13 @@ const PhoneNumberInput = ({
75
75
 
76
76
  const { locale, formatMessage } = useIntl();
77
77
 
78
+ const [randomId] = useState(() => Math.random().toString(36).slice(2, 8));
79
+
78
80
  const createId = (customID: string | undefined, backup: string): string => {
79
81
  if (customID) {
80
82
  return customID + (backup ? `-${backup}` : '');
81
83
  }
82
- const random = Math.random().toString(36).slice(2, 8);
83
- return `${backup}-${random}`;
84
+ return `${backup}-${randomId}`;
84
85
  };
85
86
 
86
87
  // Link the first non-disabled input to the the Field label, if present
@@ -107,14 +108,16 @@ const PhoneNumberInput = ({
107
108
 
108
109
  return explodeNumberModel(cleanValue);
109
110
  });
110
- const [broadcastedValue, setBroadcastedValue] = useState<PhoneNumber | null>(null);
111
+ const broadcastedValueRef = useRef<PhoneNumber>(internalValue);
111
112
 
112
113
  const [suffixDirty, setSuffixDirty] = useState(false);
114
+
113
115
  useEffect(() => {
114
- if (internalValue.suffix) {
116
+ if (!suffixDirty && internalValue.suffix) {
117
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Tracking when suffix becomes dirty
115
118
  setSuffixDirty(true);
116
119
  }
117
- }, [internalValue.suffix]);
120
+ }, [internalValue.suffix, suffixDirty]);
118
121
 
119
122
  const countriesByPrefix = useMemo(
120
123
  () =>
@@ -152,13 +155,8 @@ const PhoneNumberInput = ({
152
155
  };
153
156
 
154
157
  useEffect(() => {
155
- if (broadcastedValue === null) {
156
- setBroadcastedValue(internalValue);
157
- return;
158
- }
159
-
160
158
  const internalPhoneNumber = `${internalValue.prefix ?? ''}${internalValue.suffix}`;
161
- const broadcastedPhoneNumber = `${broadcastedValue.prefix ?? ''}${broadcastedValue.suffix}`;
159
+ const broadcastedPhoneNumber = `${broadcastedValueRef.current.prefix ?? ''}${broadcastedValueRef.current.suffix}`;
162
160
 
163
161
  if (internalPhoneNumber === broadcastedPhoneNumber) {
164
162
  return;
@@ -172,8 +170,8 @@ const PhoneNumberInput = ({
172
170
  newValue,
173
171
  internalValue.prefix ?? '', // TODO: Allow `null` in public API
174
172
  );
175
- setBroadcastedValue(internalValue);
176
- }, [onChange, broadcastedValue, internalValue]);
173
+ broadcastedValueRef.current = internalValue;
174
+ }, [onChange, internalValue]);
177
175
 
178
176
  useEffect(() => {
179
177
  const labelRef = fieldLabelRef?.current;
@@ -47,7 +47,7 @@ export const TaskCard: Story = {
47
47
  isSmall: true,
48
48
  useDisplayFont: false,
49
49
  className: 'taskCard',
50
- } as PromoCardLinkProps,
50
+ },
51
51
  decorators: [
52
52
  (Story) => (
53
53
  <div>
@@ -90,7 +90,7 @@ export const TaskCardWithCustomIcon: Story = {
90
90
  args: {
91
91
  ...TaskCard.args,
92
92
  indicatorIcon: <StarFill size={24} aria-hidden="true" />,
93
- } as PromoCardLinkProps,
93
+ },
94
94
  decorators: TaskCard.decorators,
95
95
  };
96
96
 
@@ -101,7 +101,7 @@ export const TaskCardCompleted: Story = {
101
101
  href: undefined,
102
102
  indicatorIcon: 'check',
103
103
  className: 'taskCard taskCard--completed np-theme--personal np-theme-personal--forest-green',
104
- } as PromoCardLinkProps,
104
+ },
105
105
  decorators: TaskCard.decorators,
106
106
  };
107
107
 
@@ -67,8 +67,45 @@ const PromoCardGroup: FunctionComponent<PromoCardGroupProps> = ({
67
67
  onChange = () => {},
68
68
  testId,
69
69
  }) => {
70
- const [state, setState] = useState<string>(defaultChecked);
71
- const [containerRole, setContainerRole] = useState<string | null>(null);
70
+ const [promoCardState, setPromoCardState] = useState<{
71
+ defaultChecked: string;
72
+ state: string;
73
+ }>({
74
+ defaultChecked,
75
+ state: defaultChecked,
76
+ });
77
+
78
+ useEffect(() => {
79
+ if (promoCardState.defaultChecked !== defaultChecked) {
80
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing defaultChecked prop to internal state
81
+ setPromoCardState({
82
+ defaultChecked,
83
+ state: defaultChecked,
84
+ });
85
+ }
86
+ }, [defaultChecked, promoCardState.defaultChecked]);
87
+
88
+ const { state } = promoCardState;
89
+ const setState = (newState: string) =>
90
+ setPromoCardState((prev) => ({ ...prev, state: newState }));
91
+
92
+ // Derive container role from children
93
+ const containerRole = useMemo(() => {
94
+ // Collect an array of types from the children PromoCard components
95
+ const types =
96
+ React.Children.map(children, (child) => {
97
+ if (React.isValidElement<PromoCardProps>(child) && child.props.type) {
98
+ return child.props.type;
99
+ }
100
+ return null;
101
+ })?.filter((type): type is 'radio' | 'checkbox' => type !== null && type !== undefined) ?? [];
102
+
103
+ // Check if all types are the same
104
+ const allTypesAreTheSame = types.every((type) => type === types[0]);
105
+
106
+ // If all types are the same and the type is 'radio', return 'radiogroup'
107
+ return allTypesAreTheSame && types[0] === 'radio' ? 'radiogroup' : null;
108
+ }, [children]);
72
109
 
73
110
  /**
74
111
  * The context value for the PromoCardGroup.
@@ -103,25 +140,6 @@ const PromoCardGroup: FunctionComponent<PromoCardGroupProps> = ({
103
140
  role: containerRole as AriaRoleRadioGroup | undefined, // Add the role attribute here
104
141
  };
105
142
 
106
- useEffect(() => {
107
- setState(defaultChecked);
108
-
109
- // Collect an array of types from the children PromoCard components
110
- const types =
111
- React.Children.map(children, (child) => {
112
- if (React.isValidElement<PromoCardProps>(child) && child.props.type) {
113
- return child.props.type;
114
- }
115
- return null;
116
- })?.filter((type): type is 'radio' | 'checkbox' => type !== null && type !== undefined) ?? [];
117
-
118
- // Check if all types are the same
119
- const allTypesAreTheSame = types.every((type) => type === types[0]);
120
-
121
- // If all types are the same and the type is 'radio', set the container role
122
- setContainerRole(allTypesAreTheSame && types[0] === 'radio' ? 'radiogroup' : null);
123
- }, [defaultChecked, children]);
124
-
125
143
  return (
126
144
  <PromoCardContext.Provider value={contextValue}>
127
145
  <div {...commonProps}>{children}</div>
@@ -188,4 +188,29 @@ describe('SegmentedControl', () => {
188
188
  'SegmentedControl only supports up to 3 segments. Please refer to: https://wise.design/components/segmented-control',
189
189
  );
190
190
  });
191
+
192
+ describe('animation behavior', () => {
193
+ it('skips animation on initial render', () => {
194
+ renderSegmentedControl();
195
+
196
+ // On initial render, the component should render without triggering animation
197
+ // The isMountedRef ensures animation is skipped on first render
198
+ const segmentedControl = screen.getByTestId('segmented-control');
199
+
200
+ // Verify the component renders successfully
201
+ expect(segmentedControl).toBeInTheDocument();
202
+ });
203
+
204
+ it('enables animation after value change', async () => {
205
+ renderSegmentedControl();
206
+
207
+ // Change value by clicking on a different segment
208
+ const payroll = screen.getByRole('radio', { name: 'Payroll' });
209
+ await userEvent.click(payroll);
210
+
211
+ // After a value change, the component should have completed at least one update cycle
212
+ // verifying that the isMountedRef tracking works correctly
213
+ expect(onChange).toHaveBeenCalledWith('payroll');
214
+ });
215
+ });
191
216
  });
@@ -38,6 +38,7 @@ const SegmentedControl = ({
38
38
  segments,
39
39
  onChange,
40
40
  }: SegmentedControlProps) => {
41
+ const isMountedRef = useRef(false);
41
42
  const [animate, setAnimate] = useState(false);
42
43
 
43
44
  const segmentsRef = useRef<HTMLDivElement>(null);
@@ -67,7 +68,12 @@ const SegmentedControl = ({
67
68
  };
68
69
 
69
70
  useEffect(() => {
70
- setAnimate(true);
71
+ if (isMountedRef.current) {
72
+ setAnimate(true);
73
+ } else {
74
+ isMountedRef.current = true;
75
+ }
76
+
71
77
  updateSegmentPosition();
72
78
 
73
79
  const handleWindowSizeChange = () => {
@@ -43,7 +43,7 @@ const ImageIcon = () => (
43
43
  );
44
44
 
45
45
  const isSelectOptionItem = (option: SelectItem | null): option is SelectOptionItem => {
46
- return option !== null && typeof option.value !== 'undefined';
46
+ return typeof option?.value !== 'undefined';
47
47
  };
48
48
 
49
49
  export const Basic: Story = {
@@ -1,47 +1,51 @@
1
1
  @media (max-width: 320px) {
2
2
  .np-theme-personal {
3
- --delta: 2;
4
- --size-4: calc(4px / var(--delta));
5
- --size-5: calc(5px / var(--delta));
6
- --size-8: calc(8px / var(--delta));
7
- --size-10: calc(10px / var(--delta));
8
- --size-12: calc(12px / var(--delta));
9
- --size-14: calc(14px / var(--delta));
10
- --size-16: calc(16px / var(--delta));
11
- --size-24: calc(24px / var(--delta));
12
- --size-32: calc(32px / var(--delta));
13
- --size-40: calc(40px / var(--delta));
14
- --size-48: calc(48px / var(--delta));
15
- --size-52: calc(52px / var(--delta));
16
- --size-56: calc(56px / var(--delta));
17
- --size-60: calc(60px / var(--delta));
18
- --size-64: calc(64px / var(--delta));
19
- --size-72: calc(72px / var(--delta));
20
- --size-80: calc(80px / var(--delta));
21
- --size-88: calc(88px / var(--delta));
22
- --size-96: calc(96px / var(--delta));
23
- --size-104: calc(104px / var(--delta));
24
- --size-112: calc(112px / var(--delta));
25
- --size-120: calc(120px / var(--delta));
26
- --size-126: calc(126px / var(--delta));
27
- --size-128: calc(128px / var(--delta));
28
- --size-146: calc(146px / var(--delta));
29
- --size-154: calc(154px / var(--delta));
30
- --size-x-small: calc(24px / var(--delta));
31
- --size-small: calc(32px / var(--delta));
32
- --size-medium: calc(40px / var(--delta));
33
- --size-large: calc(48px / var(--delta));
34
- --size-x-large: calc(56px / var(--delta));
35
- --size-2x-large: calc(72px / var(--delta));
36
- --space-content-horizontal: calc(16px / var(--delta));
37
- --space-small: calc(16px / var(--delta));
38
- --space-medium: calc(32px / var(--delta));
39
- --space-large: calc(40px / var(--delta));
40
- --space-x-large: calc(56px / var(--delta));
41
- --padding-x-small: var(--size-8);
42
- --padding-small: var(--size-16);
43
- --padding-medium: var(--size-24);
44
- --padding-large: var(--size-32);
3
+ --padding-x-small: 4px;
4
+ --padding-small: 8px;
5
+ --padding-medium: 12px;
6
+ --padding-large: 16px;
7
+ --size-4: 2px;
8
+ --size-5: 2.5px;
9
+ --size-8: 4px;
10
+ --size-10: 5px;
11
+ --size-12: 6px;
12
+ --size-14: 7px;
13
+ --size-16: 8px;
14
+ --size-24: 12px;
15
+ --size-32: 16px;
16
+ --size-40: 20px;
17
+ --size-48: 24px;
18
+ --size-52: 26px;
19
+ --size-56: 28px;
20
+ --size-60: 30px;
21
+ --size-64: 32px;
22
+ --size-72: 36px;
23
+ --size-80: 40px;
24
+ --size-88: 44px;
25
+ --size-96: 48px;
26
+ --size-104: 52px;
27
+ --size-112: 56px;
28
+ --size-120: 60px;
29
+ --size-126: 63px;
30
+ --size-128: 64px;
31
+ --size-146: 73px;
32
+ --size-154: 77px;
33
+ --size-160: 80px;
34
+ --size-x-small: 12px;
35
+ --size-small: 16px;
36
+ --size-medium: 20px;
37
+ --size-large: 24px;
38
+ --size-x-large: 28px;
39
+ --size-2x-large: 36px;
40
+ --space-content-horizontal: 8px;
41
+ --space-small: 8px;
42
+ --space-medium: 16px;
43
+ --space-large: 20px;
44
+ --space-x-large: 28px;
45
+ }
46
+ }
47
+ @media (max-width: 320px) {
48
+ .np-theme-personal {
45
49
  --input-height-base: var(--size-32);
46
50
  --input-height-large: var(--input-height-small);
47
51
  --input-padding: var(--input-padding-small);
@@ -1,51 +1,8 @@
1
- @import '@transferwise/neptune-tokens/themes/personal/tokens.less';
1
+ @import "@transferwise/neptune-tokens/themes/personal/tokens.less";
2
+ @import "@transferwise/neptune-tokens/tiny-viewport.less";
2
3
 
3
4
  @media (--screen-400-zoom) {
4
5
  .np-theme-personal {
5
- --delta: 2;
6
-
7
- --size-4: calc(@size-4 / var(--delta));
8
- --size-5: calc(@size-5 / var(--delta));
9
- --size-8: calc(@size-8 / var(--delta));
10
- --size-10: calc(@size-10 / var(--delta));
11
- --size-12: calc(@size-12 / var(--delta));
12
- --size-14: calc(@size-14 / var(--delta));
13
- --size-16: calc(@size-16 / var(--delta));
14
- --size-24: calc(@size-24 / var(--delta));
15
- --size-32: calc(@size-32 / var(--delta));
16
- --size-40: calc(@size-40 / var(--delta));
17
- --size-48: calc(@size-48 / var(--delta));
18
- --size-52: calc(@size-52 / var(--delta));
19
- --size-56: calc(@size-56 / var(--delta));
20
- --size-60: calc(@size-60 / var(--delta));
21
- --size-64: calc(@size-64 / var(--delta));
22
- --size-72: calc(@size-72 / var(--delta));
23
- --size-80: calc(@size-80 / var(--delta));
24
- --size-88: calc(@size-88 / var(--delta));
25
- --size-96: calc(@size-96 / var(--delta));
26
- --size-104: calc(@size-104 / var(--delta));
27
- --size-112: calc(@size-112 / var(--delta));
28
- --size-120: calc(@size-120 / var(--delta));
29
- --size-126: calc(@size-126 / var(--delta));
30
- --size-128: calc(@size-128 / var(--delta));
31
- --size-146: calc(@size-146 / var(--delta));
32
- --size-154: calc(@size-154 / var(--delta));
33
- --size-x-small: calc(@size-x-small / var(--delta));
34
- --size-small: calc(@size-small / var(--delta));
35
- --size-medium: calc(@size-medium / var(--delta));
36
- --size-large: calc(@size-large / var(--delta));
37
- --size-x-large: calc(@size-x-large / var(--delta));
38
- --size-2x-large: calc(@size-2x-large / var(--delta));
39
- --space-content-horizontal: calc(@space-content-horizontal / var(--delta));
40
- --space-small: calc(@space-small / var(--delta));
41
- --space-medium: calc(@space-medium / var(--delta));
42
- --space-large: calc(@space-large / var(--delta));
43
- --space-x-large: calc(@space-x-large / var(--delta));
44
- --padding-x-small: var(--size-8);
45
- --padding-small: var(--size-16);
46
- --padding-medium: var(--size-24);
47
- --padding-large: var(--size-32);
48
-
49
6
  --input-height-base: var(--size-32);
50
7
  --input-height-large: var(--input-height-small);
51
8
  --input-padding: var(--input-padding-small);