@telus-uds/components-base 4.0.0-alpha.2 → 4.0.0-beta.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 (99) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
  3. package/lib/cjs/Autocomplete/constants.js +1 -1
  4. package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
  5. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +7 -1
  6. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -28
  7. package/lib/cjs/TextInput/TextInputBase.js +2 -1
  8. package/lib/cjs/utils/useOverlaidPosition.js +83 -42
  9. package/lib/esm/Autocomplete/Autocomplete.js +13 -1
  10. package/lib/esm/Autocomplete/constants.js +1 -1
  11. package/lib/esm/Listbox/ListboxOverlay.js +7 -1
  12. package/lib/esm/MultiSelectFilter/ModalOverlay.js +8 -2
  13. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -28
  14. package/lib/esm/TextInput/TextInputBase.js +2 -1
  15. package/lib/esm/utils/useOverlaidPosition.js +83 -42
  16. package/lib/package.json +1 -1
  17. package/package.json +1 -1
  18. package/src/Autocomplete/Autocomplete.jsx +11 -2
  19. package/src/Autocomplete/constants.js +1 -1
  20. package/src/Listbox/ListboxOverlay.jsx +6 -2
  21. package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
  22. package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
  23. package/src/TextInput/TextInputBase.jsx +2 -1
  24. package/src/utils/useOverlaidPosition.js +84 -34
  25. package/types/A11yInfoProvider.d.ts +9 -0
  26. package/types/A11yText.d.ts +13 -0
  27. package/types/ActionCard.d.ts +19 -0
  28. package/types/ActivityIndicator.d.ts +13 -0
  29. package/types/Autocomplete.d.ts +27 -0
  30. package/types/Badge.d.ts +15 -4
  31. package/types/BaseProvider.d.ts +26 -0
  32. package/types/Box.d.ts +13 -3
  33. package/types/Button.d.ts +35 -0
  34. package/types/Card.d.ts +19 -0
  35. package/types/CardGroup.d.ts +23 -0
  36. package/types/Carousel.d.ts +25 -0
  37. package/types/Checkbox.d.ts +15 -2
  38. package/types/CheckboxCard.d.ts +20 -0
  39. package/types/CheckboxCardGroup.d.ts +10 -4
  40. package/types/ChevronLink.d.ts +9 -2
  41. package/types/ColourToggle.d.ts +16 -0
  42. package/types/Common.d.ts +7 -3
  43. package/types/Divider.d.ts +13 -1
  44. package/types/DownloadApp.d.ts +19 -0
  45. package/types/ExpandCollapse.d.ts +16 -9
  46. package/types/ExpandCollapseMini.d.ts +16 -0
  47. package/types/Feedback.d.ts +16 -0
  48. package/types/Fieldset.d.ts +17 -0
  49. package/types/FileUpload.d.ts +6 -0
  50. package/types/FlexGrid.d.ts +57 -0
  51. package/types/Footnote.d.ts +18 -0
  52. package/types/HorizontalScroll.d.ts +15 -0
  53. package/types/HorizontalScrollButton.d.ts +17 -1
  54. package/types/Icon.d.ts +13 -2
  55. package/types/IconButton.d.ts +19 -0
  56. package/types/InputLabel.d.ts +12 -0
  57. package/types/InputSupports.d.ts +19 -0
  58. package/types/Link.d.ts +20 -4
  59. package/types/List.d.ts +11 -4
  60. package/types/Listbox.d.ts +13 -3
  61. package/types/Modal.d.ts +25 -0
  62. package/types/MultiSelectFilter.d.ts +24 -0
  63. package/types/Notification.d.ts +19 -0
  64. package/types/OrderedList.d.ts +17 -0
  65. package/types/Pagination.d.ts +25 -0
  66. package/types/Portal.d.ts +5 -1
  67. package/types/PriceLockup.d.ts +28 -0
  68. package/types/ProductCard.d.ts +20 -0
  69. package/types/ProductCardGroup.d.ts +21 -0
  70. package/types/Progress.d.ts +22 -0
  71. package/types/QuickLinks.d.ts +27 -0
  72. package/types/QuickLinksFeature.d.ts +27 -0
  73. package/types/Radio.d.ts +22 -0
  74. package/types/RadioCard.d.ts +21 -0
  75. package/types/Responsive.d.ts +14 -0
  76. package/types/Scroll.d.ts +2 -2
  77. package/types/Search.d.ts +15 -2
  78. package/types/Select.d.ts +12 -4
  79. package/types/SideNav.d.ts +38 -0
  80. package/types/Skeleton.d.ts +17 -0
  81. package/types/SkipLink.d.ts +14 -0
  82. package/types/Spacer.d.ts +8 -1
  83. package/types/StackView.d.ts +8 -2
  84. package/types/Status.d.ts +2 -3
  85. package/types/StepTracker.d.ts +16 -0
  86. package/types/TabBar.d.ts +25 -0
  87. package/types/Tabs.d.ts +12 -3
  88. package/types/Tags.d.ts +24 -0
  89. package/types/TextButton.d.ts +11 -4
  90. package/types/TextInput.d.ts +38 -0
  91. package/types/ThemeProvider.d.ts +29 -0
  92. package/types/Timeline.d.ts +16 -0
  93. package/types/ToggleSwitch.d.ts +11 -2
  94. package/types/ToolTip.d.ts +7 -3
  95. package/types/TooltipButton.d.ts +20 -0
  96. package/types/Typography.d.ts +8 -2
  97. package/types/Validator.d.ts +16 -0
  98. package/types/ViewportProvider.d.ts +12 -0
  99. package/types/index.d.ts +197 -50
package/CHANGELOG.md CHANGED
@@ -1,9 +1,24 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- This log was last generated on Fri, 17 Apr 2026 23:54:32 GMT and should not be manually modified.
3
+ This log was last generated on Thu, 21 May 2026 03:01:22 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 4.0.0-beta.0
8
+
9
+ Thu, 21 May 2026 03:01:22 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `Autocomplete`: Add scroll to overlay in mobile (david.melara1@telus.com)
14
+
15
+ ### Patches
16
+
17
+ - `TextInput`: Fix `onChangeText` returning `undefined` instead of an empty string when `isNumeric` is set and no text is provided (david.melara1@telus.com)
18
+ - `Autocomplete`: inside modal story fixed to show the close button properly (josue.higueroscalderon@telus.com)
19
+ - `TypeScript`: fix types (guillermo.peitzner@telus.com)
20
+ - `MultiSelectFilter`: position issues fixed in MFE (josue.higueroscalderon@telus.com)
21
+
7
22
  ## 4.0.0-alpha.2
8
23
 
9
24
  Fri, 17 Apr 2026 23:54:32 GMT
@@ -134,6 +134,7 @@ const Autocomplete = exports.Autocomplete = /*#__PURE__*/_react.default.forwardR
134
134
  value,
135
135
  helpText = '',
136
136
  loadingLabel,
137
+ dropdownPosition = 'bottom',
137
138
  tokens,
138
139
  ...rest
139
140
  } = _ref2;
@@ -189,6 +190,13 @@ const Autocomplete = exports.Autocomplete = /*#__PURE__*/_react.default.forwardR
189
190
  isShown: isExpanded || hintExpansionEnabled,
190
191
  offsets: {
191
192
  vertical: _Platform.default.OS !== 'web' && (hint || inputLabel) ? 28 : 4
193
+ },
194
+ align: dropdownPosition === 'top' ? {
195
+ center: 'center',
196
+ bottom: 'top'
197
+ } : {
198
+ center: 'center',
199
+ top: 'bottom'
192
200
  }
193
201
  });
194
202
  const targetRef = _react.default.useRef(null);
@@ -541,5 +549,9 @@ Autocomplete.propTypes = {
541
549
  /**
542
550
  * Input value for controlled usage
543
551
  */
544
- value: _propTypes.default.string
552
+ value: _propTypes.default.string,
553
+ /**
554
+ * Controls whether the dropdown renders above or below the input. Defaults to 'bottom'.
555
+ */
556
+ dropdownPosition: _propTypes.default.oneOf(['top', 'bottom'])
545
557
  };
@@ -6,6 +6,6 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.MIN_LISTBOX_WIDTH = exports.INPUT_LEFT_PADDING = exports.DEFAULT_MIN_TO_SUGGESTION = exports.DEFAULT_MAX_SUGGESTIONS = exports.DEFAULT_MAX_DROPDOWN_HEIGHT = void 0;
7
7
  const DEFAULT_MIN_TO_SUGGESTION = exports.DEFAULT_MIN_TO_SUGGESTION = 1;
8
8
  const DEFAULT_MAX_SUGGESTIONS = exports.DEFAULT_MAX_SUGGESTIONS = 5;
9
- const DEFAULT_MAX_DROPDOWN_HEIGHT = exports.DEFAULT_MAX_DROPDOWN_HEIGHT = 336; // Approximately 7 items (48px each)
9
+ const DEFAULT_MAX_DROPDOWN_HEIGHT = exports.DEFAULT_MAX_DROPDOWN_HEIGHT = 500;
10
10
  const INPUT_LEFT_PADDING = exports.INPUT_LEFT_PADDING = 16;
11
11
  const MIN_LISTBOX_WIDTH = exports.MIN_LISTBOX_WIDTH = 288;
@@ -9,6 +9,7 @@ var _propTypes = _interopRequireDefault(require("prop-types"));
9
9
  var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
10
10
  var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
11
11
  var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
12
+ var _ScrollView = _interopRequireDefault(require("react-native-web/dist/cjs/exports/ScrollView"));
12
13
  var _Portal = require("../Portal/Portal");
13
14
  var _useThemeTokens = require("../ThemeProvider/useThemeTokens");
14
15
  var _Card = require("../Card/Card");
@@ -67,7 +68,12 @@ const DropdownOverlay = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
67
68
  paddingLeft: paddingHorizontal,
68
69
  paddingRight: paddingHorizontal
69
70
  },
70
- children: children
71
+ children: _Platform.default.OS !== 'web' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ScrollView.default, {
72
+ style: {
73
+ maxHeight
74
+ },
75
+ children: children
76
+ }) : children
71
77
  })
72
78
  });
73
79
  });
@@ -10,6 +10,7 @@ var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Vi
10
10
  var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
11
11
  var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
12
12
  var _portal = require("@gorhom/portal");
13
+ var _Portal = require("../Portal/Portal");
13
14
  var _useCopy = require("../utils/useCopy");
14
15
  var _copyPropTypes = require("../utils/props/copyPropTypes");
15
16
  var _tokens = require("../utils/props/tokens");
@@ -125,7 +126,12 @@ const ModalOverlay = exports.ModalOverlay = /*#__PURE__*/_react.default.forwardR
125
126
  copy
126
127
  });
127
128
  const closeLabel = getCopy('closeButton');
128
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_portal.Portal, {
129
+
130
+ // On web, use the local Portal (always appends to document.body) to ensure correct
131
+ // absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
132
+ // outside the iframe's document, causing coordinate mismatches when scrolled.
133
+ const PortalComponent = _Platform.default.OS === 'web' ? _Portal.Portal : _portal.Portal;
134
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(PortalComponent, {
129
135
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
130
136
  ref: containerRef,
131
137
  onLayout: onLayout,
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.MultiSelectFilter = void 0;
7
7
  var _react = _interopRequireDefault(require("react"));
8
8
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
- var _portal = require("@gorhom/portal");
10
9
  var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
11
10
  var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
12
11
  var _Dimensions = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Dimensions"));
@@ -225,20 +224,6 @@ const MultiSelectFilter = exports.MultiSelectFilter = /*#__PURE__*/_react.defaul
225
224
  setIsOpen(false);
226
225
  onCancel();
227
226
  };
228
- const appRootRef = _react.default.useRef(null);
229
- const [rootOffsets, setRootOffsets] = _react.default.useState(null);
230
- _react.default.useEffect(() => {
231
- if (rootOffsets) return;
232
- appRootRef.current?.measureInWindow((x, y) => {
233
- // Only set offsets if they are positive
234
- // this is because we want to avoid negative offsets that could cause
235
- // the dropdown to be positioned incorrectly in some situations
236
- if (y > 0) setRootOffsets({
237
- horizontal: x,
238
- vertical: y
239
- });
240
- });
241
- }, [isOpen, rootOffsets]);
242
227
  const {
243
228
  align,
244
229
  offsets
@@ -255,8 +240,7 @@ const MultiSelectFilter = exports.MultiSelectFilter = /*#__PURE__*/_react.defaul
255
240
  left: 'left'
256
241
  },
257
242
  offsets: {
258
- vertical: 4 - (rootOffsets?.vertical || 0),
259
- horizontal: -rootOffsets?.horizontal || 0
243
+ vertical: 4
260
244
  }
261
245
  }
262
246
  });
@@ -393,12 +377,7 @@ const MultiSelectFilter = exports.MultiSelectFilter = /*#__PURE__*/_react.defaul
393
377
  })]
394
378
  });
395
379
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
396
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_portal.Portal, {
397
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
398
- ref: appRootRef,
399
- style: styles.appRootRef
400
- })
401
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_ButtonDropdown.ButtonDropdown, {
380
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ButtonDropdown.ButtonDropdown, {
402
381
  ref: sourceRef,
403
382
  ...pressHandlers,
404
383
  value: isOpen,
@@ -478,11 +457,6 @@ const styles = _StyleSheet.default.create({
478
457
  },
479
458
  scrollContainer: {
480
459
  padding: 1
481
- },
482
- appRootRef: {
483
- position: 'absolute',
484
- top: 0,
485
- left: 0
486
460
  }
487
461
  });
488
462
 
@@ -290,7 +290,8 @@ const TextInputBase = exports.TextInputBase = /*#__PURE__*/_react.default.forwar
290
290
  }, [element, pattern]);
291
291
  const handleChangeText = event => {
292
292
  const text = event.nativeEvent?.text ?? event.target?.value;
293
- let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') || undefined : text;
293
+ // Do NOT use `|| undefined`: empty string is falsy and would break the controlled contract
294
+ let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') ?? '' : text;
294
295
  if (type === 'card' && filteredText) {
295
296
  const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
296
297
  const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g');
@@ -61,46 +61,45 @@ function getOverlaidPosition(_ref2) {
61
61
  offsets = {},
62
62
  align
63
63
  } = _ref2;
64
- // Web-only: this will be difficult to mimic on native because there's no global scroll position.
65
- // TODO: wire something in e.g. a scroll ref accessible from a provider included in Allium provider
66
- // that can be passed to the appropriate ScrollView?
67
- const {
68
- scrollX = 0,
69
- scrollY = 0
70
- } = typeof window === 'object' ? window : {};
71
-
72
64
  // Will have top, bottom, left and/or right offsets depending on `align`
73
65
  const positioning = {};
74
66
  const verticalOffset = offsets.vertical ?? 0;
75
67
  const horizontalOffset = offsets.horizontal ?? 0;
76
68
  if (align.top) positioning.top = getPosition({
77
69
  edge: getEdgeType(align, 'top'),
78
- fromEdge: sourceLayout.y + scrollY + verticalOffset,
70
+ fromEdge: sourceLayout.y + verticalOffset,
79
71
  sourceSize: sourceLayout.height
80
72
  });
81
73
  if (align.middle) positioning.top = getPosition({
82
74
  edge: getEdgeType(align, 'middle'),
83
- fromEdge: sourceLayout.y + scrollY + verticalOffset - targetDimensions.height / 2,
84
- sourceSize: sourceLayout.height
85
- });
86
- if (align.bottom) positioning.bottom = getPosition({
87
- edge: getEdgeType(align, 'bottom'),
88
- fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - verticalOffset),
75
+ fromEdge: sourceLayout.y + verticalOffset - targetDimensions.height / 2,
89
76
  sourceSize: sourceLayout.height
90
77
  });
78
+ if (align.bottom) {
79
+ if (_Platform.default.OS !== 'web') {
80
+ // On native, position:absolute is parent-relative, so use negative top offset instead of window-based bottom math.
81
+ positioning.top = sourceLayout.y - targetDimensions.height - verticalOffset;
82
+ } else {
83
+ positioning.bottom = getPosition({
84
+ edge: getEdgeType(align, 'bottom'),
85
+ fromEdge: windowDimensions.height - (sourceLayout.y + sourceLayout.height - verticalOffset),
86
+ sourceSize: sourceLayout.height
87
+ });
88
+ }
89
+ }
91
90
  if (align.left) positioning.left = getPosition({
92
91
  edge: getEdgeType(align, 'left'),
93
- fromEdge: sourceLayout.x + scrollX + horizontalOffset,
92
+ fromEdge: sourceLayout.x + horizontalOffset,
94
93
  sourceSize: sourceLayout.width
95
94
  });
96
95
  if (align.center) positioning.left = getPosition({
97
96
  edge: getEdgeType(align, 'center'),
98
- fromEdge: sourceLayout.x + scrollX + horizontalOffset - targetDimensions.width / 2,
97
+ fromEdge: sourceLayout.x + horizontalOffset - targetDimensions.width / 2,
99
98
  sourceSize: sourceLayout.width
100
99
  });
101
100
  if (align.right) positioning.right = getPosition({
102
101
  edge: getEdgeType(align, 'right'),
103
- fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - horizontalOffset),
102
+ fromEdge: windowDimensions.width - (sourceLayout.x + sourceLayout.width - horizontalOffset),
104
103
  sourceSize: sourceLayout.width
105
104
  });
106
105
  if (!(align.left && align.right)) {
@@ -141,6 +140,32 @@ const useOverlaidPosition = _ref3 => {
141
140
  const targetRef = (0, _react.useRef)(null);
142
141
  const [targetDimensions, setTargetDimensions] = (0, _react.useState)(null);
143
142
  const [windowDimensions, setWindowDimensions] = (0, _react.useState)(null);
143
+ const hasRemeasuredRef = (0, _react.useRef)(false);
144
+ const applySourceLayout = (0, _react.useCallback)((dims, x, y, width, height) => {
145
+ setWindowDimensions(dims);
146
+ setSourceLayout({
147
+ x,
148
+ y,
149
+ width,
150
+ height
151
+ });
152
+ }, []);
153
+ const measureSourceOnWeb = (0, _react.useCallback)(windowDims => {
154
+ const el = sourceRef.current;
155
+ if (!el) return;
156
+ const domNode = el._nativeTag ?? el;
157
+ const dims = windowDims ?? _Dimensions.default.get('window');
158
+ const rect = typeof domNode.getBoundingClientRect === 'function' ? domNode.getBoundingClientRect() : null;
159
+ if (rect) {
160
+ const x = rect.left + (typeof window.scrollX === 'number' ? window.scrollX : 0);
161
+ const y = rect.top + (typeof window.scrollY === 'number' ? window.scrollY : 0);
162
+ applySourceLayout(dims, x, y, rect.width, rect.height);
163
+ } else {
164
+ el.measureInWindow((mx, my, width, height) => {
165
+ applySourceLayout(dims, mx, my, width, height);
166
+ });
167
+ }
168
+ }, [applySourceLayout]);
144
169
  const onTargetLayout = (0, _react.useCallback)(_ref4 => {
145
170
  let {
146
171
  nativeEvent: {
@@ -171,16 +196,19 @@ const useOverlaidPosition = _ref3 => {
171
196
  let {
172
197
  window
173
198
  } = _ref5;
174
- const measurementFunction = _Platform.default.OS === 'web' ? 'measureInWindow' : 'measure';
175
- sourceRef.current?.[measurementFunction]((x, y, width, height) => {
176
- setWindowDimensions(window);
177
- setSourceLayout({
178
- x,
179
- y,
180
- width,
181
- height
199
+ if (_Platform.default.OS === 'web') {
200
+ measureSourceOnWeb(window);
201
+ } else {
202
+ sourceRef.current?.measure((x, y, width, height) => {
203
+ setWindowDimensions(window);
204
+ setSourceLayout({
205
+ x,
206
+ y,
207
+ width,
208
+ height
209
+ });
182
210
  });
183
- });
211
+ }
184
212
  };
185
213
  let subscription;
186
214
  const unsubscribe = () => {
@@ -193,6 +221,7 @@ const useOverlaidPosition = _ref3 => {
193
221
  }
194
222
  setSourceLayout(null);
195
223
  setTargetDimensions(null);
224
+ hasRemeasuredRef.current = false;
196
225
  };
197
226
  if (readyToShow) {
198
227
  subscription = _Dimensions.default.addEventListener('change', handleDimensionsChange);
@@ -203,36 +232,48 @@ const useOverlaidPosition = _ref3 => {
203
232
  unsubscribe();
204
233
  }
205
234
  return unsubscribe;
206
- }, [readyToShow]);
235
+ }, [readyToShow, measureSourceOnWeb]);
236
+
237
+ // Re-measure source when targetDimensions first becomes available.
238
+ // Without this, there is a race condition: getBoundingClientRect() resolves faster than
239
+ // measureInWindow used to, so sourceLayout may be set before the dropdown has rendered
240
+ // and reported its dimensions via onTargetLayout. When targetDimensions finally arrives,
241
+ // sourceLayout is stale (no scroll has occurred to trigger handleScroll).
242
+ // Setting hasRemeasuredRef.current=true here also prevents the blink: isReady (on web)
243
+ // won't become true until this effect completes, guaranteeing the dropdown is always shown
244
+ // at its final correct position without an intermediate incorrect-position frame.
245
+ (0, _react.useEffect)(() => {
246
+ if (!isShown || !sourceRef.current || !targetDimensions || _Platform.default.OS !== 'web') return;
247
+ measureSourceOnWeb();
248
+ hasRemeasuredRef.current = true;
249
+ }, [targetDimensions, isShown, measureSourceOnWeb]);
207
250
  (0, _react.useEffect)(() => {
208
251
  if (_Platform.default.OS !== 'web') {
209
252
  return undefined;
210
253
  }
211
- const handleScroll = (0, _lodash.default)(() => {
212
- sourceRef.current?.measureInWindow((x, y, width, height) => {
213
- setWindowDimensions(window);
214
- setSourceLayout({
215
- x,
216
- y,
217
- width,
218
- height
219
- });
220
- });
221
- }, DEBOUNCE_DELAY);
254
+ const handleScroll = (0, _lodash.default)(measureSourceOnWeb, DEBOUNCE_DELAY);
222
255
  window.addEventListener('scroll', handleScroll);
223
256
  return () => {
224
257
  window.removeEventListener('scroll', handleScroll);
225
258
  handleScroll.cancel();
226
259
  };
227
- }, [sourceRef]);
228
- const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions);
260
+ }, [measureSourceOnWeb]);
261
+
262
+ // On web, require hasRemeasuredRef to be true before declaring isReady. This ensures
263
+ // the dropdown is never shown with an intermediate stale position (the blink).
264
+ // On native, hasRemeasuredRef stays false (the web-only effect never runs), so we skip
265
+ // that check to preserve the original native behaviour.
266
+ const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions && (_Platform.default.OS !== 'web' || hasRemeasuredRef.current));
229
267
  const overlaidPosition = isReady ? getOverlaidPosition({
230
268
  sourceLayout,
231
269
  targetDimensions,
232
270
  windowDimensions,
233
271
  offsets,
234
272
  align
235
- }) : {};
273
+ }) : {
274
+ top: 0,
275
+ left: 0
276
+ };
236
277
  return {
237
278
  overlaidPosition,
238
279
  sourceRef,
@@ -127,6 +127,7 @@ export const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
127
127
  value,
128
128
  helpText = '',
129
129
  loadingLabel,
130
+ dropdownPosition = 'bottom',
130
131
  tokens,
131
132
  ...rest
132
133
  } = _ref2;
@@ -182,6 +183,13 @@ export const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
182
183
  isShown: isExpanded || hintExpansionEnabled,
183
184
  offsets: {
184
185
  vertical: Platform.OS !== 'web' && (hint || inputLabel) ? 28 : 4
186
+ },
187
+ align: dropdownPosition === 'top' ? {
188
+ center: 'center',
189
+ bottom: 'top'
190
+ } : {
191
+ center: 'center',
192
+ top: 'bottom'
185
193
  }
186
194
  });
187
195
  const targetRef = React.useRef(null);
@@ -534,5 +542,9 @@ Autocomplete.propTypes = {
534
542
  /**
535
543
  * Input value for controlled usage
536
544
  */
537
- value: PropTypes.string
545
+ value: PropTypes.string,
546
+ /**
547
+ * Controls whether the dropdown renders above or below the input. Defaults to 'bottom'.
548
+ */
549
+ dropdownPosition: PropTypes.oneOf(['top', 'bottom'])
538
550
  };
@@ -1,5 +1,5 @@
1
1
  export const DEFAULT_MIN_TO_SUGGESTION = 1;
2
2
  export const DEFAULT_MAX_SUGGESTIONS = 5;
3
- export const DEFAULT_MAX_DROPDOWN_HEIGHT = 336; // Approximately 7 items (48px each)
3
+ export const DEFAULT_MAX_DROPDOWN_HEIGHT = 500;
4
4
  export const INPUT_LEFT_PADDING = 16;
5
5
  export const MIN_LISTBOX_WIDTH = 288;
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3
3
  import View from "react-native-web/dist/exports/View";
4
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
5
  import Platform from "react-native-web/dist/exports/Platform";
6
+ import ScrollView from "react-native-web/dist/exports/ScrollView";
6
7
  import { Portal } from '../Portal/Portal';
7
8
  import { useThemeTokens } from '../ThemeProvider/useThemeTokens';
8
9
  import { Card } from '../Card/Card';
@@ -60,7 +61,12 @@ const DropdownOverlay = /*#__PURE__*/React.forwardRef((_ref, ref) => {
60
61
  paddingLeft: paddingHorizontal,
61
62
  paddingRight: paddingHorizontal
62
63
  },
63
- children: children
64
+ children: Platform.OS !== 'web' ? /*#__PURE__*/_jsx(ScrollView, {
65
+ style: {
66
+ maxHeight
67
+ },
68
+ children: children
69
+ }) : children
64
70
  })
65
71
  });
66
72
  });
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
3
3
  import View from "react-native-web/dist/exports/View";
4
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
5
  import Platform from "react-native-web/dist/exports/Platform";
6
- import { Portal } from '@gorhom/portal';
6
+ import { Portal as GorhomPortal } from '@gorhom/portal';
7
+ import { Portal as LocalPortal } from '../Portal/Portal';
7
8
  import { useCopy } from '../utils/useCopy';
8
9
  import { copyPropTypes } from '../utils/props/copyPropTypes';
9
10
  import { getTokensPropType } from '../utils/props/tokens';
@@ -118,7 +119,12 @@ export const ModalOverlay = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
118
119
  copy
119
120
  });
120
121
  const closeLabel = getCopy('closeButton');
121
- return /*#__PURE__*/_jsx(Portal, {
122
+
123
+ // On web, use the local Portal (always appends to document.body) to ensure correct
124
+ // absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
125
+ // outside the iframe's document, causing coordinate mismatches when scrolled.
126
+ const PortalComponent = Platform.OS === 'web' ? LocalPortal : GorhomPortal;
127
+ return /*#__PURE__*/_jsx(PortalComponent, {
122
128
  children: /*#__PURE__*/_jsx(View, {
123
129
  ref: containerRef,
124
130
  onLayout: onLayout,
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Portal } from '@gorhom/portal';
4
3
  import View from "react-native-web/dist/exports/View";
5
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
6
5
  import Dimensions from "react-native-web/dist/exports/Dimensions";
@@ -218,20 +217,6 @@ export const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
218
217
  setIsOpen(false);
219
218
  onCancel();
220
219
  };
221
- const appRootRef = React.useRef(null);
222
- const [rootOffsets, setRootOffsets] = React.useState(null);
223
- React.useEffect(() => {
224
- if (rootOffsets) return;
225
- appRootRef.current?.measureInWindow((x, y) => {
226
- // Only set offsets if they are positive
227
- // this is because we want to avoid negative offsets that could cause
228
- // the dropdown to be positioned incorrectly in some situations
229
- if (y > 0) setRootOffsets({
230
- horizontal: x,
231
- vertical: y
232
- });
233
- });
234
- }, [isOpen, rootOffsets]);
235
220
  const {
236
221
  align,
237
222
  offsets
@@ -248,8 +233,7 @@ export const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
248
233
  left: 'left'
249
234
  },
250
235
  offsets: {
251
- vertical: 4 - (rootOffsets?.vertical || 0),
252
- horizontal: -rootOffsets?.horizontal || 0
236
+ vertical: 4
253
237
  }
254
238
  }
255
239
  });
@@ -386,12 +370,7 @@ export const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
386
370
  })]
387
371
  });
388
372
  return /*#__PURE__*/_jsxs(_Fragment, {
389
- children: [/*#__PURE__*/_jsx(Portal, {
390
- children: /*#__PURE__*/_jsx(View, {
391
- ref: appRootRef,
392
- style: styles.appRootRef
393
- })
394
- }), /*#__PURE__*/_jsx(ButtonDropdown, {
373
+ children: [/*#__PURE__*/_jsx(ButtonDropdown, {
395
374
  ref: sourceRef,
396
375
  ...pressHandlers,
397
376
  value: isOpen,
@@ -471,11 +450,6 @@ const styles = StyleSheet.create({
471
450
  },
472
451
  scrollContainer: {
473
452
  padding: 1
474
- },
475
- appRootRef: {
476
- position: 'absolute',
477
- top: 0,
478
- left: 0
479
453
  }
480
454
  });
481
455
 
@@ -283,7 +283,8 @@ export const TextInputBase = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
283
283
  }, [element, pattern]);
284
284
  const handleChangeText = event => {
285
285
  const text = event.nativeEvent?.text ?? event.target?.value;
286
- let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') || undefined : text;
286
+ // Do NOT use `|| undefined`: empty string is falsy and would break the controlled contract
287
+ let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') ?? '' : text;
287
288
  if (type === 'card' && filteredText) {
288
289
  const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
289
290
  const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g');