@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
@@ -42,7 +42,7 @@ export const AmountInput = ({
42
42
  const intl = useIntl();
43
43
  const { focus, setFocus, visualFocus, setVisualFocus } = useFocus();
44
44
 
45
- const [value, setValue] = useState<string>(
45
+ const [value, setValue] = useState<string>(() =>
46
46
  amount
47
47
  ? getFormattedString({
48
48
  value: amount,
@@ -51,6 +51,7 @@ export const AmountInput = ({
51
51
  })
52
52
  : '',
53
53
  );
54
+ const prevAmountRef = useRef<string | number | null | undefined>(amount);
54
55
  const numericValue = useMemo(() => {
55
56
  return getUnformattedNumber({
56
57
  value,
@@ -85,23 +86,28 @@ export const AmountInput = ({
85
86
  const decimalMode = decimalSeparator && value.includes(decimalSeparator);
86
87
 
87
88
  useEffect(() => {
88
- if (!focus) {
89
- setValue(
90
- amount
91
- ? getFormattedString({
92
- value: amount,
93
- currency,
94
- locale: intl.locale,
95
- })
96
- : '',
97
- );
89
+ if (prevAmountRef.current !== amount) {
90
+ prevAmountRef.current = amount;
91
+
92
+ // Only update the displayed value if not focused (preserve user input when focused)
93
+ if (!focus) {
94
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing external prop to internal state when unfocused
95
+ setValue(
96
+ amount
97
+ ? getFormattedString({
98
+ value: amount,
99
+ currency,
100
+ locale: intl.locale,
101
+ })
102
+ : '',
103
+ );
104
+ }
98
105
  }
99
- // eslint-disable-next-line react-hooks/exhaustive-deps
100
- }, [amount]);
106
+ }, [amount, focus, currency, intl.locale]);
101
107
 
102
108
  useEffect(() => {
103
109
  onFocusChange?.(visualFocus);
104
- }, [visualFocus]);
110
+ }, [onFocusChange, visualFocus]);
105
111
 
106
112
  const shouldReformatAfterUserInput = (newValue: string) => {
107
113
  // don't reformat if formatting would wipe out user's input
@@ -246,6 +252,7 @@ export const AmountInput = ({
246
252
  const addonContent = useMemo((): string | null | undefined => {
247
253
  // because we're using a separate "addon" element for the placeholder decimals, there is a possibility that the input itself will become scrollable
248
254
  // and the decimals will appear on top of the input. Safest thing to do is to just hide the addon if there is not enough room
255
+ // eslint-disable-next-line react-hooks/refs -- Reading layout dimensions for overflow detection
249
256
  if (isInputPossiblyOverflowing({ ref, value })) {
250
257
  return null;
251
258
  }
@@ -284,7 +291,7 @@ export const AmountInput = ({
284
291
  // whenever decimals are shown, we need to account for the full decimal part for the font size calculation
285
292
  value: addonContent ? valueWithFullDecimals : value,
286
293
  focus: visualFocus,
287
- inputElement: ref.current,
294
+ inputElement: ref,
288
295
  loading,
289
296
  });
290
297
 
@@ -1,17 +1,27 @@
1
- import { type CSSProperties, useEffect, useLayoutEffect, useState } from 'react';
1
+ import {
2
+ type CSSProperties,
3
+ type RefObject,
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useState,
8
+ } from 'react';
2
9
  import { Props as ExpressiveMoneyInputProps } from '../ExpressiveMoneyInput';
3
10
 
4
11
  type InputStyleObject = {
5
12
  value: string;
6
13
  focus: boolean;
7
- inputElement: HTMLInputElement | null;
14
+ inputElement: RefObject<HTMLInputElement> | null;
8
15
  } & Pick<ExpressiveMoneyInputProps, 'loading'>;
9
16
 
10
17
  export const useInputStyle = ({ value, focus, inputElement, loading }: InputStyleObject) => {
11
18
  const initialRender = !useTimeout(300);
12
- const inputWidth = useFirstDefinedValue(inputElement?.clientWidth, [inputElement, value]);
19
+ const inputWidth = useFirstDefinedValue(inputElement?.current?.clientWidth, [
20
+ inputElement?.current,
21
+ value,
22
+ ]);
13
23
 
14
- const getStyle = (): CSSProperties => {
24
+ const getStyle = useCallback((): CSSProperties => {
15
25
  const fontSize = getFontSize(value, focus, inputWidth);
16
26
 
17
27
  return {
@@ -23,13 +33,14 @@ export const useInputStyle = ({ value, focus, inputElement, loading }: InputStyl
23
33
  transition: initialRender ? 'none' : undefined,
24
34
  color: loading ? 'var(--color-interactive-secondary)' : undefined,
25
35
  };
26
- };
36
+ }, [value, focus, inputWidth, loading, initialRender]);
27
37
 
28
- const [style, setStyle] = useState(getStyle());
38
+ const [style, setStyle] = useState<CSSProperties>(() => getStyle());
29
39
 
30
40
  useLayoutEffect(() => {
41
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Computing style based on layout measurements
31
42
  setStyle(getStyle());
32
- }, [value, focus, loading, inputWidth]);
43
+ }, [value, focus, loading, inputWidth, getStyle]);
33
44
 
34
45
  return style;
35
46
  };
@@ -60,10 +71,11 @@ const useFirstDefinedValue = (newValue: number | undefined, dependencies: unknow
60
71
 
61
72
  useLayoutEffect(() => {
62
73
  if (typeof newValue !== 'undefined' && typeof value === 'undefined') {
74
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Lazy initialization from prop
63
75
  setValue(newValue);
64
76
  }
65
77
  // eslint-disable-next-line react-hooks/exhaustive-deps
66
- }, [...dependencies, value]);
78
+ }, [...dependencies, value, newValue]);
67
79
 
68
80
  return value;
69
81
  };
@@ -15,9 +15,11 @@ export const useSelectionRange = () => {
15
15
  selection.current = undefined;
16
16
  };
17
17
 
18
+ /* eslint-disable react-hooks/refs */
18
19
  return {
19
20
  selection: selection.current,
20
21
  handleSelect,
21
22
  handleSelectionBlur,
22
23
  };
24
+ /* eslint-enable react-hooks/refs */
23
25
  };
@@ -104,7 +104,7 @@ const Header: FunctionComponent<HeaderProps> = React.forwardRef(
104
104
  useEffect(() => {
105
105
  if (as === 'legend' && internalRef.current) {
106
106
  const { parentElement } = internalRef.current;
107
- if (!parentElement || parentElement.tagName.toLowerCase() !== 'fieldset') {
107
+ if (parentElement?.tagName.toLowerCase() !== 'fieldset') {
108
108
  console.warn(
109
109
  'Legends should be the first child in a fieldset, and this is not possible when including an action',
110
110
  );
@@ -121,7 +121,7 @@ const Header: FunctionComponent<HeaderProps> = React.forwardRef(
121
121
  }
122
122
 
123
123
  return (
124
- <div {...commonProps} {...props} ref={ref as React.Ref<HTMLDivElement>}>
124
+ <div {...commonProps} {...props} ref={ref}>
125
125
  <Title as={as} type={levelTypography} className="np-header__title">
126
126
  {title}
127
127
  </Title>
@@ -65,10 +65,12 @@ export function SelectInputBottomSheet({
65
65
  return (
66
66
  <>
67
67
  {open ? <PreventScroll /> : null}
68
+ {/* eslint-disable react-hooks/refs -- setReference is a callback ref from floating-ui, safe to pass during render */}
68
69
  {renderTrigger?.({
69
70
  ref: refs.setReference,
70
71
  getInteractionProps: getReferenceProps,
71
72
  })}
73
+ {/* eslint-enable react-hooks/refs */}
72
74
 
73
75
  <FloatingPortal>
74
76
  <ThemeProvider theme="personal" screenMode={theme === 'personal' ? screenMode : 'light'}>
@@ -94,6 +96,7 @@ export function SelectInputBottomSheet({
94
96
  <Fragment
95
97
  key={floatingKey} // Force inner state invalidation on open
96
98
  >
99
+ {/* eslint-disable react-hooks/refs -- setFloating is a callback ref from floating-ui, safe to pass during render */}
97
100
  <TransitionChild
98
101
  ref={refs.setFloating}
99
102
  as="div"
@@ -102,6 +105,7 @@ export function SelectInputBottomSheet({
102
105
  leaveTo="np-bottom-sheet-v2-content--closed"
103
106
  {...getFloatingProps()}
104
107
  >
108
+ {/* eslint-enable react-hooks/refs */}
105
109
  <div className="np-bottom-sheet-v2-header">
106
110
  <CloseButton
107
111
  size={Size.SMALL}
@@ -77,7 +77,7 @@ export function SelectInputOptions<T = string>({
77
77
  const intl = useIntl();
78
78
  const virtualiserHandlerRef = useRef<VirtualizerHandle>(null);
79
79
  const controllerRef = filterable ? searchInputRef : listboxRef;
80
- const [initialRender, setInitialRender] = useState(true);
80
+ const initialRenderRef = useRef(true);
81
81
 
82
82
  const needle = useMemo(() => {
83
83
  if (filterable) {
@@ -166,28 +166,42 @@ export function SelectInputOptions<T = string>({
166
166
  // Items shown once shall be kept mounted until the needle changes, otherwise
167
167
  // the scroll position may jump around inadvertently. Pattern adopted from:
168
168
  // https://inokawa.github.io/virtua/?path=/story/advanced-keep-offscreen-items--append-only
169
- const [mountedIndexes, setMountedIndexes] = useState<number[]>([]);
170
- const prevNeedleRef = useRef(needle);
171
-
169
+ const [virtualState, setVirtualState] = useState<{
170
+ needle: typeof needle;
171
+ length: number;
172
+ mountedIndexes: number[];
173
+ }>({
174
+ needle,
175
+ length: filteredItems.length,
176
+ mountedIndexes: [],
177
+ });
178
+
179
+ // Note: virtualState.mountedIndexes is in deps but only read in the guarded branch.
180
+ // This means external updates to mountedIndexes will trigger this effect but hit the guard
181
+ // and bail out early. This is intentional and harmless - the guard ensures no unnecessary work.
172
182
  useEffect(() => {
173
- const needleChanged = prevNeedleRef.current !== needle;
174
- prevNeedleRef.current = needle;
175
-
176
- if (needleChanged) {
177
- // Reset mounted indexes when search changes to avoid stale scroll positions
178
- setMountedIndexes([]);
179
- return;
180
- }
181
-
182
- // Ensure the 'End' key works as intended by keeping the last item mounted.
183
- // Skipped on needle change to prevent auto-scrolling on search.
184
- if (filteredItems.length > 0) {
185
- setMountedIndexes((prevMountedIndexes) => {
186
- // Create a new array with existing indexes plus the last item index
187
- return [...new Set([...prevMountedIndexes, filteredItems.length - 1])]; // Sorting is redundant by nature here
183
+ if (virtualState.needle !== needle || virtualState.length !== filteredItems.length) {
184
+ const needleChanged = virtualState.needle !== needle;
185
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing virtual scroll state with filtered items
186
+ setVirtualState({
187
+ needle,
188
+ length: filteredItems.length,
189
+ mountedIndexes: needleChanged
190
+ ? [] // Reset on needle change
191
+ : filteredItems.length > 0
192
+ ? [...new Set([...virtualState.mountedIndexes, filteredItems.length - 1])] // Add last index
193
+ : virtualState.mountedIndexes,
188
194
  });
189
195
  }
190
- }, [needle, filteredItems.length]);
196
+ }, [
197
+ needle,
198
+ filteredItems.length,
199
+ virtualState.needle,
200
+ virtualState.length,
201
+ virtualState.mountedIndexes,
202
+ ]);
203
+
204
+ const { mountedIndexes } = virtualState;
191
205
 
192
206
  const listboxContainerRef = useRef<HTMLDivElement>(null);
193
207
  useEffect(() => {
@@ -200,7 +214,7 @@ export function SelectInputOptions<T = string>({
200
214
  }, []);
201
215
 
202
216
  useEffect(() => {
203
- setInitialRender(false);
217
+ initialRenderRef.current = false;
204
218
  }, []);
205
219
 
206
220
  const showStatus = resultsEmpty;
@@ -251,7 +265,7 @@ export function SelectInputOptions<T = string>({
251
265
  className="np-select-input-options-container"
252
266
  onAriaActiveDescendantChange={(value: React.AriaAttributes['aria-activedescendant']) => {
253
267
  if (controllerRef.current != null) {
254
- if (!initialRender && value != null) {
268
+ if (!initialRenderRef.current && value != null) {
255
269
  controllerRef.current.setAttribute('aria-activedescendant', value);
256
270
  } else {
257
271
  controllerRef.current.removeAttribute('aria-activedescendant');
@@ -288,7 +302,7 @@ export function SelectInputOptions<T = string>({
288
302
  const inputValue = event.currentTarget.value;
289
303
 
290
304
  // Free up resources and ensure not to go out of bounds
291
- setMountedIndexes([]);
305
+ setVirtualState((prev) => ({ ...prev, mountedIndexes: [] }));
292
306
  onFilterChange(inputValue);
293
307
  }}
294
308
  onInput={(event) => {
@@ -358,7 +372,7 @@ export function SelectInputOptions<T = string>({
358
372
  virtualiserHandlerRef.current.viewportSize,
359
373
  );
360
374
 
361
- setMountedIndexes((prevMountedIndexes) => {
375
+ setVirtualState((prev) => {
362
376
  // Create an array of all indexes that should be visible
363
377
 
364
378
  const visibleIndexes = [];
@@ -368,9 +382,11 @@ export function SelectInputOptions<T = string>({
368
382
  }
369
383
 
370
384
  // Combine with previous indexes and sort
371
- return [...new Set([...prevMountedIndexes, ...visibleIndexes])].sort(
372
- (a, b) => a - b,
373
- );
385
+ const newMountedIndexes = [
386
+ ...new Set([...prev.mountedIndexes, ...visibleIndexes]),
387
+ ].sort((a, b) => a - b);
388
+
389
+ return { ...prev, mountedIndexes: newMountedIndexes };
374
390
  });
375
391
  }}
376
392
  >
@@ -85,10 +85,12 @@ export function SelectInputPopover({
85
85
  return (
86
86
  <>
87
87
  {open ? <PreventScroll /> : null}
88
+ {/* eslint-disable react-hooks/refs -- setReference is a callback ref from floating-ui, safe to pass during render */}
88
89
  {renderTrigger({
89
90
  ref: refs.setReference,
90
91
  getInteractionProps: getReferenceProps,
91
92
  })}
93
+ {/* eslint-enable react-hooks/refs */}
92
94
 
93
95
  <FloatingPortal>
94
96
  <ThemeProvider theme="personal" screenMode={theme === 'personal' ? screenMode : 'light'}>
@@ -104,6 +106,7 @@ export function SelectInputPopover({
104
106
  >
105
107
  <FocusScope>
106
108
  <FloatingFocusManager context={context}>
109
+ {/* eslint-disable react-hooks/refs -- setFloating is a callback ref from floating-ui, safe to pass during render */}
107
110
  <div
108
111
  key={floatingKey} // Force inner state invalidation on open
109
112
  ref={refs.setFloating}
@@ -114,6 +117,7 @@ export function SelectInputPopover({
114
117
  style={floatingStyles}
115
118
  {...getFloatingProps()}
116
119
  >
120
+ {/* eslint-enable react-hooks/refs */}
117
121
  <div
118
122
  className={clsx('np-popover-v2', title && 'np-popover-v2--has-title', {
119
123
  'np-popover-v2--padding-md': padding === 'md',
@@ -1,5 +1,5 @@
1
1
  import mergeProps from 'merge-props';
2
- import { useEffect, useRef, useState, useDeferredValue } from 'react';
2
+ import { useCallback, useEffect, useRef, useState, useDeferredValue } from 'react';
3
3
  import { Listbox as ListboxBase } from '@headlessui/react';
4
4
  import { useScreenSize } from '../../common/hooks/useScreenSize';
5
5
  import { Breakpoint } from '../../common/propsValues/breakpoint';
@@ -60,6 +60,7 @@ export function SelectInput<T = string, M extends boolean = false>({
60
60
  const initialized = useRef(false);
61
61
  const handleClose = useEffectEvent(onClose ?? (() => {}));
62
62
  const handleOpen = useEffectEvent(onOpen ?? (() => {}));
63
+
63
64
  useEffect(() => {
64
65
  if (initialized.current) {
65
66
  if (open) {
@@ -70,29 +71,34 @@ export function SelectInput<T = string, M extends boolean = false>({
70
71
  } else {
71
72
  initialized.current = true;
72
73
  }
73
- }, [handleClose, handleOpen, open]);
74
+ }, [open]);
74
75
 
75
76
  const [filterQuery, _setFilterQuery] = useState('');
76
77
  const deferredFilterQuery = useDeferredValue(filterQuery);
77
- const setFilterQuery = useEffectEvent((query: string) => {
78
- _setFilterQuery(query);
79
- if (query !== filterQuery) {
80
- onFilterChange({
81
- query,
82
- queryNormalized: query ? searchableString(query) : null,
83
- });
84
- }
85
- });
86
-
87
- const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
78
+ const previousFilterQueryRef = useRef(filterQuery);
88
79
 
89
- const screenSm = useScreenSize(Breakpoint.SMALL);
90
- const OptionsOverlay = screenSm ? SelectInputPopover : SelectInputBottomSheet;
80
+ const setFilterQuery = useCallback(
81
+ (query: string) => {
82
+ _setFilterQuery(query);
83
+ if (query !== previousFilterQueryRef.current) {
84
+ onFilterChange({
85
+ query,
86
+ queryNormalized: query ? searchableString(query) : null,
87
+ });
88
+ previousFilterQueryRef.current = query;
89
+ }
90
+ },
91
+ [onFilterChange],
92
+ );
91
93
 
94
+ const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
92
95
  const searchInputRef = useRef<HTMLInputElement>(null);
93
96
  const listboxRef = useRef<HTMLDivElement>(null);
94
97
  const controllerRef = filterable ? searchInputRef : listboxRef;
95
98
 
99
+ const screenSm = useScreenSize(Breakpoint.SMALL);
100
+ const OptionsOverlay = screenSm ? SelectInputPopover : SelectInputBottomSheet;
101
+
96
102
  /**
97
103
  * Attempts to resolve the `listbox` label
98
104
  * @see https://storybook.wise.design/?path=/docs/forms-selectinput-accessibility--docs#labelling
@@ -29,7 +29,7 @@ export function SelectInputTriggerButton<T extends SelectInputTriggerButtonEleme
29
29
  ref={ref}
30
30
  as={PolymorphicWithOverrides}
31
31
  role="combobox"
32
- __overrides={{ as, size, ...interactionProps } as Record<string, unknown>}
32
+ __overrides={{ as, size, ...interactionProps }}
33
33
  {...mergeProps({ onClick, onKeyDown }, restProps)}
34
34
  />
35
35
  );
package/src/main.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly, this file was auto-generated.
3
- * Generated on Wed, 13 May 2026 12:45:11 GMT
3
+ * Generated on Thu, 14 May 2026 16:11:43 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -144,7 +144,7 @@
144
144
 
145
145
  /**
146
146
  * Do not edit directly, this file was auto-generated.
147
- * Generated on Wed, 13 May 2026 12:45:12 GMT
147
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
148
148
  */
149
149
 
150
150
  .np-theme-personal {
@@ -328,7 +328,7 @@
328
328
 
329
329
  /**
330
330
  * Do not edit directly, this file was auto-generated.
331
- * Generated on Wed, 13 May 2026 12:45:12 GMT
331
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
332
332
  */
333
333
 
334
334
  .np-theme-personal--forest-green {
@@ -512,7 +512,7 @@
512
512
 
513
513
  /**
514
514
  * Do not edit directly, this file was auto-generated.
515
- * Generated on Wed, 13 May 2026 12:45:12 GMT
515
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
516
516
  */
517
517
 
518
518
  .np-theme-personal--bright-green {
@@ -696,7 +696,7 @@
696
696
 
697
697
  /**
698
698
  * Do not edit directly, this file was auto-generated.
699
- * Generated on Wed, 13 May 2026 12:45:12 GMT
699
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
700
700
  */
701
701
 
702
702
  .np-theme-personal--dark {
@@ -880,7 +880,7 @@
880
880
 
881
881
  /**
882
882
  * Do not edit directly, this file was auto-generated.
883
- * Generated on Wed, 13 May 2026 12:45:12 GMT
883
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
884
884
  */
885
885
 
886
886
  .np-theme-platform {
@@ -1064,7 +1064,7 @@
1064
1064
 
1065
1065
  /**
1066
1066
  * Do not edit directly, this file was auto-generated.
1067
- * Generated on Wed, 13 May 2026 12:45:12 GMT
1067
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
1068
1068
  */
1069
1069
 
1070
1070
  .np-theme-platform--forest-green {
@@ -1248,7 +1248,7 @@
1248
1248
 
1249
1249
  /**
1250
1250
  * Do not edit directly, this file was auto-generated.
1251
- * Generated on Wed, 13 May 2026 12:45:12 GMT
1251
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
1252
1252
  */
1253
1253
 
1254
1254
  .np-theme-business {
@@ -1433,7 +1433,7 @@
1433
1433
 
1434
1434
  /**
1435
1435
  * Do not edit directly, this file was auto-generated.
1436
- * Generated on Wed, 13 May 2026 12:45:12 GMT
1436
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
1437
1437
  */
1438
1438
 
1439
1439
  .np-theme-business--dark {
@@ -1618,7 +1618,7 @@
1618
1618
 
1619
1619
  /**
1620
1620
  * Do not edit directly, this file was auto-generated.
1621
- * Generated on Wed, 13 May 2026 12:45:12 GMT
1621
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
1622
1622
  */
1623
1623
 
1624
1624
  .np-theme-business--forest-green {
@@ -1803,7 +1803,7 @@
1803
1803
 
1804
1804
  /**
1805
1805
  * Do not edit directly, this file was auto-generated.
1806
- * Generated on Wed, 13 May 2026 12:45:12 GMT
1806
+ * Generated on Thu, 14 May 2026 16:11:44 GMT
1807
1807
  */
1808
1808
 
1809
1809
  .np-theme-business--bright-green {
@@ -4351,48 +4351,53 @@ a.text-inverse:focus {
4351
4351
 
4352
4352
  @media (max-width: 320px) {
4353
4353
  .np-theme-personal {
4354
- --delta: 2;
4355
- --size-4: calc(4px / var(--delta));
4356
- --size-5: calc(5px / var(--delta));
4357
- --size-8: calc(8px / var(--delta));
4358
- --size-10: calc(10px / var(--delta));
4359
- --size-12: calc(12px / var(--delta));
4360
- --size-14: calc(14px / var(--delta));
4361
- --size-16: calc(16px / var(--delta));
4362
- --size-24: calc(24px / var(--delta));
4363
- --size-32: calc(32px / var(--delta));
4364
- --size-40: calc(40px / var(--delta));
4365
- --size-48: calc(48px / var(--delta));
4366
- --size-52: calc(52px / var(--delta));
4367
- --size-56: calc(56px / var(--delta));
4368
- --size-60: calc(60px / var(--delta));
4369
- --size-64: calc(64px / var(--delta));
4370
- --size-72: calc(72px / var(--delta));
4371
- --size-80: calc(80px / var(--delta));
4372
- --size-88: calc(88px / var(--delta));
4373
- --size-96: calc(96px / var(--delta));
4374
- --size-104: calc(104px / var(--delta));
4375
- --size-112: calc(112px / var(--delta));
4376
- --size-120: calc(120px / var(--delta));
4377
- --size-126: calc(126px / var(--delta));
4378
- --size-128: calc(128px / var(--delta));
4379
- --size-146: calc(146px / var(--delta));
4380
- --size-154: calc(154px / var(--delta));
4381
- --size-x-small: calc(24px / var(--delta));
4382
- --size-small: calc(32px / var(--delta));
4383
- --size-medium: calc(40px / var(--delta));
4384
- --size-large: calc(48px / var(--delta));
4385
- --size-x-large: calc(56px / var(--delta));
4386
- --size-2x-large: calc(72px / var(--delta));
4387
- --space-content-horizontal: calc(16px / var(--delta));
4388
- --space-small: calc(16px / var(--delta));
4389
- --space-medium: calc(32px / var(--delta));
4390
- --space-large: calc(40px / var(--delta));
4391
- --space-x-large: calc(56px / var(--delta));
4392
- --padding-x-small: var(--size-8);
4393
- --padding-small: var(--size-16);
4394
- --padding-medium: var(--size-24);
4395
- --padding-large: var(--size-32);
4354
+ --padding-x-small: 4px;
4355
+ --padding-small: 8px;
4356
+ --padding-medium: 12px;
4357
+ --padding-large: 16px;
4358
+ --size-4: 2px;
4359
+ --size-5: 2.5px;
4360
+ --size-8: 4px;
4361
+ --size-10: 5px;
4362
+ --size-12: 6px;
4363
+ --size-14: 7px;
4364
+ --size-16: 8px;
4365
+ --size-24: 12px;
4366
+ --size-32: 16px;
4367
+ --size-40: 20px;
4368
+ --size-48: 24px;
4369
+ --size-52: 26px;
4370
+ --size-56: 28px;
4371
+ --size-60: 30px;
4372
+ --size-64: 32px;
4373
+ --size-72: 36px;
4374
+ --size-80: 40px;
4375
+ --size-88: 44px;
4376
+ --size-96: 48px;
4377
+ --size-104: 52px;
4378
+ --size-112: 56px;
4379
+ --size-120: 60px;
4380
+ --size-126: 63px;
4381
+ --size-128: 64px;
4382
+ --size-146: 73px;
4383
+ --size-154: 77px;
4384
+ --size-160: 80px;
4385
+ --size-x-small: 12px;
4386
+ --size-small: 16px;
4387
+ --size-medium: 20px;
4388
+ --size-large: 24px;
4389
+ --size-x-large: 28px;
4390
+ --size-2x-large: 36px;
4391
+ --space-content-horizontal: 8px;
4392
+ --space-small: 8px;
4393
+ --space-medium: 16px;
4394
+ --space-large: 20px;
4395
+ --space-x-large: 28px;
4396
+ }
4397
+ }
4398
+
4399
+ @media (max-width: 320px) {
4400
+ .np-theme-personal {
4396
4401
  --input-height-base: var(--size-32);
4397
4402
  --input-height-large: var(--input-height-small);
4398
4403
  --input-padding: var(--input-padding-small);