@transferwise/components 0.0.0-experimental-af6dee0 → 0.0.0-experimental-199a021

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 (145) 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/monthCalendar/table/MonthCalendarTable.js +1 -1
  20. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.js.map +1 -1
  21. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs +1 -1
  22. package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs.map +1 -1
  23. package/build/dateLookup/yearCalendar/table/YearCalendarTable.js +1 -1
  24. package/build/dateLookup/yearCalendar/table/YearCalendarTable.js.map +1 -1
  25. package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs +1 -1
  26. package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs.map +1 -1
  27. package/build/expressiveMoneyInput/ExpressiveMoneyInput.js.map +1 -1
  28. package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs.map +1 -1
  29. package/build/expressiveMoneyInput/amountInput/AmountInput.js +17 -11
  30. package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -1
  31. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +18 -12
  32. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs.map +1 -1
  33. package/build/expressiveMoneyInput/hooks/useInputStyle.js +8 -6
  34. package/build/expressiveMoneyInput/hooks/useInputStyle.js.map +1 -1
  35. package/build/expressiveMoneyInput/hooks/useInputStyle.mjs +9 -7
  36. package/build/expressiveMoneyInput/hooks/useInputStyle.mjs.map +1 -1
  37. package/build/header/Header.js +1 -1
  38. package/build/header/Header.js.map +1 -1
  39. package/build/header/Header.mjs +1 -1
  40. package/build/header/Header.mjs.map +1 -1
  41. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.js.map +1 -1
  42. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.mjs.map +1 -1
  43. package/build/inputs/SelectInput/Options/SelectInputOptions.js +31 -22
  44. package/build/inputs/SelectInput/Options/SelectInputOptions.js.map +1 -1
  45. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs +32 -23
  46. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs.map +1 -1
  47. package/build/inputs/SelectInput/Popover/SelectInputPopover.js.map +1 -1
  48. package/build/inputs/SelectInput/Popover/SelectInputPopover.mjs.map +1 -1
  49. package/build/inputs/SelectInput/SelectInput.js +8 -6
  50. package/build/inputs/SelectInput/SelectInput.js.map +1 -1
  51. package/build/inputs/SelectInput/SelectInput.mjs +9 -7
  52. package/build/inputs/SelectInput/SelectInput.mjs.map +1 -1
  53. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
  54. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
  55. package/build/loader/Loader.js +6 -16
  56. package/build/loader/Loader.js.map +1 -1
  57. package/build/loader/Loader.mjs +7 -17
  58. package/build/loader/Loader.mjs.map +1 -1
  59. package/build/main.css +4 -15
  60. package/build/nudge/Nudge.js +26 -15
  61. package/build/nudge/Nudge.js.map +1 -1
  62. package/build/nudge/Nudge.mjs +26 -15
  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/loader/Loader.css +4 -14
  77. package/build/styles/main.css +4 -15
  78. package/build/tabs/Tabs.js +1 -1
  79. package/build/tabs/Tabs.js.map +1 -1
  80. package/build/tabs/Tabs.mjs +1 -1
  81. package/build/tabs/Tabs.mjs.map +1 -1
  82. package/build/tooltip/Tooltip.js +6 -3
  83. package/build/tooltip/Tooltip.js.map +1 -1
  84. package/build/tooltip/Tooltip.mjs +6 -3
  85. package/build/tooltip/Tooltip.mjs.map +1 -1
  86. package/build/types/avatarWrapper/AvatarWrapper.d.ts.map +1 -1
  87. package/build/types/common/hooks/useHasIntersected/useHasIntersected.d.ts.map +1 -1
  88. package/build/types/common/liveRegion/LiveRegion.d.ts.map +1 -1
  89. package/build/types/dateLookup/monthCalendar/table/MonthCalendarTable.d.ts.map +1 -1
  90. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts.map +1 -1
  91. package/build/types/expressiveMoneyInput/amountInput/AmountInput.d.ts.map +1 -1
  92. package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts +2 -2
  93. package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts.map +1 -1
  94. package/build/types/expressiveMoneyInput/hooks/useSelectionRange.d.ts.map +1 -1
  95. package/build/types/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.d.ts.map +1 -1
  96. package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts.map +1 -1
  97. package/build/types/inputs/SelectInput/Popover/SelectInputPopover.d.ts.map +1 -1
  98. package/build/types/inputs/SelectInput/SelectInput.d.ts.map +1 -1
  99. package/build/types/loader/Loader.d.ts +1 -4
  100. package/build/types/loader/Loader.d.ts.map +1 -1
  101. package/build/types/nudge/Nudge.d.ts.map +1 -1
  102. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  103. package/build/types/promoCard/PromoCardGroup.d.ts.map +1 -1
  104. package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -1
  105. package/build/types/tooltip/Tooltip.d.ts.map +1 -1
  106. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  107. package/build/uploadInput/UploadInput.js +29 -28
  108. package/build/uploadInput/UploadInput.js.map +1 -1
  109. package/build/uploadInput/UploadInput.mjs +29 -28
  110. package/build/uploadInput/UploadInput.mjs.map +1 -1
  111. package/package.json +3 -3
  112. package/src/avatarWrapper/AvatarWrapper.tsx +5 -6
  113. package/src/button/LegacyButton.tsx +1 -1
  114. package/src/button/_stories/Button.test.story.tsx +3 -3
  115. package/src/common/hooks/useContainerSize.test.tsx +1 -1
  116. package/src/common/hooks/useHasIntersected/useHasIntersected.ts +12 -4
  117. package/src/common/liveRegion/LiveRegion.tsx +5 -2
  118. package/src/dateInput/DateInput.tsx +10 -10
  119. package/src/dateLookup/monthCalendar/table/MonthCalendarTable.tsx +1 -5
  120. package/src/dateLookup/yearCalendar/table/YearCalendarTable.tsx +1 -1
  121. package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +1 -1
  122. package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +22 -15
  123. package/src/expressiveMoneyInput/hooks/useInputStyle.ts +20 -8
  124. package/src/expressiveMoneyInput/hooks/useSelectionRange.ts +2 -0
  125. package/src/header/Header.tsx +2 -2
  126. package/src/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.tsx +4 -0
  127. package/src/inputs/SelectInput/Options/SelectInputOptions.tsx +39 -26
  128. package/src/inputs/SelectInput/Popover/SelectInputPopover.tsx +4 -0
  129. package/src/inputs/SelectInput/SelectInput.tsx +21 -15
  130. package/src/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +1 -1
  131. package/src/loader/Loader.css +4 -14
  132. package/src/loader/Loader.less +88 -54
  133. package/src/loader/Loader.story.tsx +0 -4
  134. package/src/loader/Loader.test.tsx +5 -20
  135. package/src/loader/Loader.tsx +7 -18
  136. package/src/main.css +4 -15
  137. package/src/nudge/Nudge.tsx +22 -19
  138. package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -13
  139. package/src/promoCard/PromoCard.story.tsx +3 -3
  140. package/src/promoCard/PromoCardGroup.tsx +39 -21
  141. package/src/segmentedControl/SegmentedControl.tsx +7 -1
  142. package/src/select/Select.story.tsx +1 -1
  143. package/src/tabs/Tabs.tsx +1 -1
  144. package/src/tooltip/Tooltip.tsx +3 -0
  145. package/src/uploadInput/UploadInput.tsx +28 -27
package/src/main.css CHANGED
@@ -30839,17 +30839,6 @@ button.np-link {
30839
30839
  }
30840
30840
 
30841
30841
  .tw-loader {
30842
- display: flex;
30843
- flex-direction: column;
30844
- align-items: center;
30845
- gap: 16px;
30846
- gap: var(--size-16);
30847
- width: 190px;
30848
- text-align: center;
30849
- word-break: break-word;
30850
- }
30851
-
30852
- .tw-loader-asset {
30853
30842
  overflow: hidden;
30854
30843
  border-radius: 50%;
30855
30844
  backface-visibility: hidden;
@@ -30866,11 +30855,11 @@ button.np-link {
30866
30855
  transition: opacity 300ms cubic-bezier(0.24, 0, 0.3, 1);
30867
30856
  }
30868
30857
 
30869
- .tw-loader-asset--visible {
30858
+ .tw-loader--visible {
30870
30859
  opacity: 1;
30871
30860
  }
30872
30861
 
30873
- .tw-loader-asset::before {
30862
+ .tw-loader::before {
30874
30863
  content: "";
30875
30864
  display: block;
30876
30865
  width: 4.5rem;
@@ -30890,7 +30879,7 @@ button.np-link {
30890
30879
  animation-iteration-count: infinite;
30891
30880
  }
30892
30881
 
30893
- .tw-loader-asset--sm {
30882
+ .tw-loader--sm {
30894
30883
  --coin-size: 3rem;
30895
30884
  --coin-edge-width-offset: translateX(0.275rem);
30896
30885
  --coin-edge-width-negative-offset: translateX(-0.275rem);
@@ -30898,7 +30887,7 @@ button.np-link {
30898
30887
  --box-shadow-backwards: -0.025rem 0 0 var(--coin-colour), -0.05rem 0 0 var(--coin-colour), -0.075rem 0 0 var(--coin-colour), -0.1rem 0 0 var(--coin-colour), -0.125rem 0 0 var(--coin-colour), -0.15rem 0 0 var(--coin-colour), -0.175rem 0 0 var(--coin-colour), -0.2rem 0 0 var(--coin-colour), -0.225rem 0 0 var(--coin-colour), -0.25rem 0 0 var(--coin-colour), -0.275rem 0 0 var(--coin-colour), -0.3rem 0 0 var(--coin-colour), -0.325rem 0 0 var(--coin-colour), -0.35rem 0 0 var(--coin-colour), -0.375rem 0 0 var(--coin-colour), -0.4rem 0 0 var(--coin-colour), -0.425rem 0 0 var(--coin-colour), -0.45rem 0 0 var(--coin-colour), -0.475rem 0 0 var(--coin-colour);
30899
30888
  }
30900
30889
 
30901
- .tw-loader-asset--md {
30890
+ .tw-loader--md {
30902
30891
  --coin-size: 4.5rem;
30903
30892
  --coin-edge-width-offset: translateX(0.375rem);
30904
30893
  --coin-edge-width-negative-offset: translateX(-0.375rem);
@@ -96,8 +96,21 @@ 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 [nudgeState, setNudgeState] = useState(() => {
100
+ const initialDismissed =
101
+ persistDismissal && id ? !!getLocalStorage()?.find((item) => item === id) : false;
102
+ return {
103
+ isDismissed: initialDismissed,
104
+ isMounted: false,
105
+ };
106
+ });
107
+
108
+ useEffect(() => {
109
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Setting mount state in mount effect
110
+ setNudgeState((prev) => ({ ...prev, isMounted: true }));
111
+ }, []);
112
+
113
+ const { isDismissed } = nudgeState;
101
114
 
102
115
  const handleOnDismiss = () => {
103
116
  const dismissedNudgesStorage = getLocalStorage();
@@ -105,9 +118,9 @@ const Nudge = ({
105
118
  if (persistDismissal && id) {
106
119
  try {
107
120
  localStorage.setItem(STORAGE_NAME, JSON.stringify([...dismissedNudgesStorage, id]));
108
- } catch (error) {}
121
+ } catch {}
109
122
 
110
- setIsDismissed(true);
123
+ setNudgeState((prev) => ({ ...prev, isDismissed: true }));
111
124
  }
112
125
 
113
126
  if (onDismiss) {
@@ -116,25 +129,15 @@ const Nudge = ({
116
129
  };
117
130
 
118
131
  useEffect(() => {
119
- if (persistDismissal && id) {
132
+ if (persistDismissal && id && isPreviouslyDismissed) {
120
133
  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
- }
134
+ const wasDismissed = !!dismissedNudgesStorage?.find((item) => item === id);
135
+ isPreviouslyDismissed(wasDismissed);
131
136
  }
132
-
133
- setIsMounted(true);
134
137
  // eslint-disable-next-line react-hooks/exhaustive-deps
135
138
  }, [id, persistDismissal]);
136
139
 
137
- if (persistDismissal && (isDismissed || !isMounted)) {
140
+ if (persistDismissal && (isDismissed || !nudgeState.isMounted)) {
138
141
  return null;
139
142
  }
140
143
 
@@ -143,7 +146,7 @@ const Nudge = ({
143
146
  {!!mediaName && (
144
147
  <div className="wds-nudge-media">
145
148
  <Illustration
146
- name={mediaName as IllustrationNames}
149
+ name={mediaName}
147
150
  className={clsx(`wds-nudge-media-${mediaName}`)}
148
151
  size="small"
149
152
  disablePadding
@@ -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>
@@ -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 = {
package/src/tabs/Tabs.tsx CHANGED
@@ -254,7 +254,7 @@ export default class Tabs extends Component<TabsProps, TabsState> {
254
254
  };
255
255
 
256
256
  onKeyDown = (index: number) => (event: React.KeyboardEvent<HTMLLIElement>) => {
257
- if (event && event.key === 'Enter') {
257
+ if (event?.key === 'Enter') {
258
258
  this.switchTab(index);
259
259
  }
260
260
  };
@@ -51,6 +51,7 @@ const Tooltip = ({
51
51
  middleware: [
52
52
  offset(16),
53
53
  flip({ fallbackPlacements: [Position.TOP] }),
54
+ // eslint-disable-next-line react-hooks/refs -- arrowRef is passed to floating-ui middleware, legitimate API usage
54
55
  arrowMiddleware({ element: arrowRef, padding: 8 }),
55
56
  ],
56
57
  whileElementsMounted: open ? autoUpdate : undefined,
@@ -63,6 +64,7 @@ const Tooltip = ({
63
64
  };
64
65
 
65
66
  return (
67
+ /* eslint-disable react-hooks/refs -- setReference and setFloating are callback refs from floating-ui, safe to pass during render */
66
68
  <span
67
69
  ref={refs.setReference}
68
70
  className="tw-tooltip-container"
@@ -96,6 +98,7 @@ const Tooltip = ({
96
98
  </div>
97
99
  </div>
98
100
  </span>
101
+ /* eslint-enable react-hooks/refs */
99
102
  );
100
103
  };
101
104
 
@@ -170,10 +170,10 @@ const UploadInput = ({
170
170
  const inputAttributes = useInputAttributes({ nonLabelable: true });
171
171
  const [markedFileForDelete, setMarkedFileForDelete] = useState<UploadedFile | null>(null);
172
172
  const [lastAttemptedDeleteId, setLastAttemptedDeleteId] = useState<string | number | null>(null);
173
- const [mounted, setMounted] = useState(false);
173
+ const mountedRef = useRef(false);
174
174
  const { formatMessage } = useIntl();
175
175
  const uploadInputRef = useRef<HTMLInputElement | null>(null);
176
- let fileRefs: (HTMLDivElement | UploadItemRef | null)[] = [];
176
+ const fileRefs = useRef<(HTMLDivElement | UploadItemRef | null)[]>([]);
177
177
 
178
178
  const PROGRESS_STATUSES = new Set([Status.PENDING, Status.PROCESSING]);
179
179
 
@@ -196,7 +196,7 @@ const UploadInput = ({
196
196
  updateFileList((list) =>
197
197
  list.filter((fileInList) => file !== fileInList && file.id !== fileInList.id),
198
198
  );
199
- fileRefs = fileRefs.filter((ref) => ref && ref.id !== file.id);
199
+ fileRefs.current = fileRefs.current.filter((ref) => ref && ref.id !== file.id);
200
200
  }
201
201
 
202
202
  function modifyFileInList(file: UploadedFile, updates: Partial<UploadedFile>) {
@@ -208,18 +208,18 @@ const UploadInput = ({
208
208
  }
209
209
 
210
210
  const removeFile = async (file: UploadedFile) => {
211
- const { id, status } = file;
212
- fileRefs = fileRefs.filter((item) => item && item.id !== file.id);
211
+ const { id: fileId, status } = file;
212
+ fileRefs.current = fileRefs.current.filter((item) => item && item.id !== file.id);
213
213
 
214
214
  if (status === Status.FAILED) {
215
215
  removeFileFromList(file);
216
216
  return Promise.resolve();
217
217
  }
218
218
 
219
- if (onDeleteFile && id) {
219
+ if (onDeleteFile && fileId) {
220
220
  modifyFileInList(file, { status: Status.PROCESSING, error: undefined });
221
221
 
222
- return onDeleteFile(id)
222
+ return onDeleteFile(fileId)
223
223
  .then(() => {
224
224
  removeFileFromList(file);
225
225
  })
@@ -248,6 +248,7 @@ const UploadInput = ({
248
248
 
249
249
  function getNumberOfFilesUploaded() {
250
250
  const uploadInitiatedStatus = new Set([Status.SUCCEEDED, Status.PENDING]);
251
+
251
252
  const validFiles = uploadedFilesListReference.current.filter(
252
253
  (file) => file.status && uploadInitiatedStatus.has(file.status),
253
254
  );
@@ -274,13 +275,11 @@ const UploadInput = ({
274
275
 
275
276
  if (!isTypeValid(file, allowedFileTypes)) {
276
277
  handleFileUploadFailure(file, formatMessage(MESSAGES.fileTypeNotSupported));
277
- continue;
278
278
  }
279
279
 
280
280
  if (typeof sizeLimit === 'number' && !isSizeValid(file, sizeLimit * 1000)) {
281
281
  const failureMessage = sizeLimitErrorMessage || formatMessage(MESSAGES.fileIsTooLarge);
282
282
  handleFileUploadFailure(file, failureMessage);
283
- continue;
284
283
  }
285
284
 
286
285
  if (areMaximumFilesUploadedAlready()) {
@@ -288,7 +287,6 @@ const UploadInput = ({
288
287
  maxFilesErrorMessage ||
289
288
  formatMessage(MESSAGES.maximumFilesAlreadyUploaded, { maxFilesAllowed: maxFiles });
290
289
  handleFileUploadFailure(file, failureMessage);
291
- continue;
292
290
  }
293
291
 
294
292
  const existingFile = uploadedFiles.find((f) => f.filename === file.name);
@@ -321,14 +319,16 @@ const UploadInput = ({
321
319
  };
322
320
 
323
321
  useEffect(() => {
324
- setMounted(true);
325
- }, []);
322
+ // Skip onFilesChange call on initial mount, only call on updates
323
+ if (!mountedRef.current) {
324
+ mountedRef.current = true;
325
+ return;
326
+ }
326
327
 
327
- useEffect(() => {
328
- if (onFilesChange && mounted) {
328
+ if (onFilesChange) {
329
329
  onFilesChange([...uploadedFiles]);
330
330
  }
331
- }, [onFilesChange, uploadedFiles]); // eslint-disable-line react-hooks/exhaustive-deps
331
+ }, [onFilesChange, uploadedFiles]);
332
332
 
333
333
  type NextFocusable =
334
334
  | HTMLDivElement
@@ -336,14 +336,14 @@ const UploadInput = ({
336
336
  | { ref: HTMLDivElement | UploadItemRef; target: 'button' | 'link' }
337
337
  | null;
338
338
 
339
- const [nextFocusable, setNextFocusable] = useState<NextFocusable>(uploadInputRef.current);
339
+ const [nextFocusable, setNextFocusable] = useState<NextFocusable>(null);
340
340
 
341
341
  const handleFocus = (fileId: string | number) => {
342
- fileRefs = fileRefs.filter((ref) => {
342
+ fileRefs.current = fileRefs.current.filter((ref) => {
343
343
  return ref && ref.id !== markedFileForDelete?.id;
344
344
  });
345
345
 
346
- const filesCount = fileRefs.length;
346
+ const filesCount = fileRefs.current.length;
347
347
  let next: UploadItemRef | HTMLDivElement | null = uploadInputRef.current;
348
348
  let focusTarget: 'button' | 'link' = 'button';
349
349
 
@@ -355,15 +355,15 @@ const UploadInput = ({
355
355
  }
356
356
 
357
357
  if (filesCount > 1) {
358
- const currentFileIndex = fileRefs.findIndex((file) => file?.id === fileId);
359
- const currentFileId = fileRefs?.[currentFileIndex]?.id;
360
- const lastFileId = fileRefs?.[filesCount - 1]?.id;
358
+ const currentFileIndex = fileRefs.current.findIndex((file) => file?.id === fileId);
359
+ const currentFileId = fileRefs.current?.[currentFileIndex]?.id;
360
+ const lastFileId = fileRefs.current?.[filesCount - 1]?.id;
361
361
 
362
362
  // if last file, select a previous one
363
363
  if (currentFileId === lastFileId) {
364
- next = fileRefs[filesCount - 2];
364
+ next = fileRefs.current[filesCount - 2];
365
365
  } else {
366
- next = fileRefs[currentFileIndex + 1];
366
+ next = fileRefs.current[currentFileIndex + 1];
367
367
  }
368
368
 
369
369
  // If next is an UploadItemRef, check if it has a URL (succeeded)
@@ -401,7 +401,7 @@ const UploadInput = ({
401
401
  typeof uploadInputRef.current.focus === 'function'
402
402
  ) {
403
403
  setTimeout(() => {
404
- uploadInputRef.current!.focus();
404
+ uploadInputRef.current?.focus();
405
405
  }, 0);
406
406
  } else if (
407
407
  typeof focusTarget === 'object' &&
@@ -436,17 +436,17 @@ const UploadInput = ({
436
436
  aria-relevant="all"
437
437
  role="region"
438
438
  >
439
- {uploadedFiles.map((file, index) => (
439
+ {uploadedFiles.map((file) => (
440
440
  <UploadItem
441
441
  key={file.id}
442
442
  ref={(el: UploadItemRef | null) => {
443
443
  if (
444
444
  el &&
445
445
  el.id !== markedFileForDelete?.id &&
446
- !fileRefs.some((ref) => ref && ref.id === el.id) &&
446
+ !fileRefs.current.some((ref) => ref?.id === el.id) &&
447
447
  el.status !== 'processing'
448
448
  ) {
449
- fileRefs.push(el);
449
+ fileRefs.current.push(el);
450
450
  }
451
451
  }}
452
452
  file={file}
@@ -478,6 +478,7 @@ const UploadInput = ({
478
478
  ref={uploadInputRef}
479
479
  id={id}
480
480
  uploadButtonTitle={uploadButtonTitle}
481
+ // eslint-disable-next-line react-hooks/refs -- Function reads ref for file count check
481
482
  disabled={areMaximumFilesUploadedAlready() || disabled}
482
483
  multiple={multiple}
483
484
  fileTypes={fileTypes}