@telus-uds/components-base 4.0.0-alpha.1 → 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 (129) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
  3. package/lib/cjs/Autocomplete/constants.js +1 -1
  4. package/lib/cjs/Footnote/FootnoteLink.js +18 -17
  5. package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
  6. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +7 -1
  7. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -28
  8. package/lib/cjs/Progress/Progress.js +30 -5
  9. package/lib/cjs/Scroll/Scroll.js +472 -0
  10. package/lib/cjs/Scroll/ScrollContext.js +55 -0
  11. package/lib/cjs/Scroll/ScrollItem.js +106 -0
  12. package/lib/cjs/Scroll/ScrollProgress.js +100 -0
  13. package/lib/cjs/Scroll/dictionary.js +18 -0
  14. package/lib/cjs/TextInput/TextInputBase.js +2 -1
  15. package/lib/cjs/index.js +14 -0
  16. package/lib/cjs/utils/animation/useAnimatedValue.js +46 -0
  17. package/lib/cjs/utils/children.js +2 -1
  18. package/lib/cjs/utils/useOverlaidPosition.js +83 -42
  19. package/lib/esm/Autocomplete/Autocomplete.js +13 -1
  20. package/lib/esm/Autocomplete/constants.js +1 -1
  21. package/lib/esm/Footnote/FootnoteLink.js +18 -17
  22. package/lib/esm/Listbox/ListboxOverlay.js +7 -1
  23. package/lib/esm/MultiSelectFilter/ModalOverlay.js +8 -2
  24. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -28
  25. package/lib/esm/Progress/Progress.js +30 -5
  26. package/lib/esm/Scroll/Scroll.js +465 -0
  27. package/lib/esm/Scroll/ScrollContext.js +46 -0
  28. package/lib/esm/Scroll/ScrollItem.js +100 -0
  29. package/lib/esm/Scroll/ScrollProgress.js +93 -0
  30. package/lib/esm/Scroll/dictionary.js +12 -0
  31. package/lib/esm/TextInput/TextInputBase.js +2 -1
  32. package/lib/esm/index.js +2 -0
  33. package/lib/esm/utils/animation/useAnimatedValue.js +38 -0
  34. package/lib/esm/utils/children.js +2 -1
  35. package/lib/esm/utils/useOverlaidPosition.js +83 -42
  36. package/lib/package.json +2 -2
  37. package/package.json +2 -2
  38. package/src/Autocomplete/Autocomplete.jsx +11 -2
  39. package/src/Autocomplete/constants.js +1 -1
  40. package/src/Footnote/FootnoteLink.jsx +17 -12
  41. package/src/Listbox/ListboxOverlay.jsx +6 -2
  42. package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
  43. package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
  44. package/src/Progress/Progress.jsx +22 -3
  45. package/src/Scroll/Scroll.jsx +483 -0
  46. package/src/Scroll/ScrollContext.jsx +39 -0
  47. package/src/Scroll/ScrollItem.jsx +87 -0
  48. package/src/Scroll/ScrollProgress.jsx +74 -0
  49. package/src/Scroll/dictionary.js +12 -0
  50. package/src/TextInput/TextInputBase.jsx +2 -1
  51. package/src/index.js +2 -0
  52. package/src/utils/animation/useAnimatedValue.js +35 -0
  53. package/src/utils/children.jsx +4 -1
  54. package/src/utils/useOverlaidPosition.js +84 -34
  55. package/types/A11yInfoProvider.d.ts +9 -0
  56. package/types/A11yText.d.ts +13 -0
  57. package/types/ActionCard.d.ts +19 -0
  58. package/types/ActivityIndicator.d.ts +13 -0
  59. package/types/Autocomplete.d.ts +27 -0
  60. package/types/Badge.d.ts +15 -4
  61. package/types/BaseProvider.d.ts +26 -0
  62. package/types/Box.d.ts +13 -3
  63. package/types/Button.d.ts +35 -0
  64. package/types/Card.d.ts +19 -0
  65. package/types/CardGroup.d.ts +23 -0
  66. package/types/Carousel.d.ts +25 -0
  67. package/types/Checkbox.d.ts +15 -2
  68. package/types/CheckboxCard.d.ts +20 -0
  69. package/types/CheckboxCardGroup.d.ts +10 -4
  70. package/types/ChevronLink.d.ts +9 -2
  71. package/types/ColourToggle.d.ts +16 -0
  72. package/types/Common.d.ts +7 -3
  73. package/types/Divider.d.ts +13 -1
  74. package/types/DownloadApp.d.ts +19 -0
  75. package/types/ExpandCollapse.d.ts +16 -9
  76. package/types/ExpandCollapseMini.d.ts +16 -0
  77. package/types/Feedback.d.ts +16 -0
  78. package/types/Fieldset.d.ts +17 -0
  79. package/types/FileUpload.d.ts +6 -0
  80. package/types/FlexGrid.d.ts +57 -0
  81. package/types/Footnote.d.ts +18 -0
  82. package/types/HorizontalScroll.d.ts +15 -0
  83. package/types/HorizontalScrollButton.d.ts +17 -1
  84. package/types/Icon.d.ts +13 -2
  85. package/types/IconButton.d.ts +19 -0
  86. package/types/InputLabel.d.ts +12 -0
  87. package/types/InputSupports.d.ts +19 -0
  88. package/types/Link.d.ts +20 -4
  89. package/types/List.d.ts +11 -4
  90. package/types/Listbox.d.ts +13 -3
  91. package/types/Modal.d.ts +25 -0
  92. package/types/MultiSelectFilter.d.ts +24 -0
  93. package/types/Notification.d.ts +19 -0
  94. package/types/OrderedList.d.ts +17 -0
  95. package/types/Pagination.d.ts +25 -0
  96. package/types/Portal.d.ts +5 -1
  97. package/types/PriceLockup.d.ts +28 -0
  98. package/types/ProductCard.d.ts +20 -0
  99. package/types/ProductCardGroup.d.ts +21 -0
  100. package/types/Progress.d.ts +22 -0
  101. package/types/QuickLinks.d.ts +27 -0
  102. package/types/QuickLinksFeature.d.ts +27 -0
  103. package/types/Radio.d.ts +22 -0
  104. package/types/RadioCard.d.ts +21 -0
  105. package/types/Responsive.d.ts +14 -0
  106. package/types/Scroll.d.ts +66 -0
  107. package/types/Search.d.ts +15 -2
  108. package/types/Select.d.ts +12 -4
  109. package/types/SideNav.d.ts +38 -0
  110. package/types/Skeleton.d.ts +17 -0
  111. package/types/SkipLink.d.ts +14 -0
  112. package/types/Spacer.d.ts +8 -1
  113. package/types/StackView.d.ts +8 -2
  114. package/types/Status.d.ts +2 -3
  115. package/types/StepTracker.d.ts +16 -0
  116. package/types/TabBar.d.ts +25 -0
  117. package/types/Tabs.d.ts +12 -3
  118. package/types/Tags.d.ts +24 -0
  119. package/types/TextButton.d.ts +11 -4
  120. package/types/TextInput.d.ts +38 -0
  121. package/types/ThemeProvider.d.ts +29 -0
  122. package/types/Timeline.d.ts +16 -0
  123. package/types/ToggleSwitch.d.ts +11 -2
  124. package/types/ToolTip.d.ts +7 -3
  125. package/types/TooltipButton.d.ts +20 -0
  126. package/types/Typography.d.ts +8 -2
  127. package/types/Validator.d.ts +16 -0
  128. package/types/ViewportProvider.d.ts +12 -0
  129. package/types/index.d.ts +205 -49
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ScrollProgress = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _propTypes = _interopRequireDefault(require("prop-types"));
9
+ var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
10
+ var _pressability = require("../utils/pressability");
11
+ var _useAnimatedValue = require("../utils/animation/useAnimatedValue");
12
+ var _Progress = require("../Progress/Progress");
13
+ var _ProgressBar = require("../Progress/ProgressBar");
14
+ var _jsxRuntime = require("react/jsx-runtime");
15
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
+ const MAX_PERCENTAGE = 100;
17
+ const BORDER_RADIUS_DIVISOR = 2;
18
+ const staticProgressTokens = {
19
+ borderWidth: 0,
20
+ borderColor: 'transparent'
21
+ };
22
+ const staticProgressBarTokens = {
23
+ gradient: null,
24
+ outlineWidth: 0
25
+ };
26
+ const selectProgressTokens = _ref => {
27
+ let {
28
+ progressTrackHeight,
29
+ progressBarHeight,
30
+ progressTrackColor
31
+ } = _ref;
32
+ return {
33
+ height: progressTrackHeight,
34
+ barHeight: progressBarHeight,
35
+ backgroundColor: progressTrackColor,
36
+ borderRadius: progressTrackHeight / BORDER_RADIUS_DIVISOR
37
+ };
38
+ };
39
+ const selectBarTokens = _ref2 => {
40
+ let {
41
+ progressBarColor,
42
+ progressBarHeight
43
+ } = _ref2;
44
+ return {
45
+ backgroundColor: progressBarColor,
46
+ borderRadius: progressBarHeight / BORDER_RADIUS_DIVISOR
47
+ };
48
+ };
49
+ const ScrollProgress = exports.ScrollProgress = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
50
+ let {
51
+ pressableState,
52
+ getTokens,
53
+ thumbWrapperStyle,
54
+ getCopy
55
+ } = _ref3;
56
+ const state = (0, _pressability.resolvePressableState)(pressableState);
57
+ const themeTokens = getTokens(state);
58
+ const animatedBarHeight = (0, _useAnimatedValue.useAnimatedValue)(themeTokens.progressBarHeight);
59
+ const animatedTokens = {
60
+ ...themeTokens,
61
+ progressBarHeight: animatedBarHeight
62
+ };
63
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Progress.Progress, {
64
+ tokens: {
65
+ ...staticProgressTokens,
66
+ ...selectProgressTokens(animatedTokens)
67
+ },
68
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
69
+ ref: ref,
70
+ style: thumbWrapperStyle,
71
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_ProgressBar.ProgressBar, {
72
+ percentage: MAX_PERCENTAGE,
73
+ tokens: {
74
+ ...staticProgressBarTokens,
75
+ ...selectBarTokens(animatedTokens)
76
+ },
77
+ accessibilityLabel: getCopy('progressBarLabel')
78
+ })
79
+ })
80
+ });
81
+ });
82
+ ScrollProgress.displayName = 'ScrollProgress';
83
+ ScrollProgress.propTypes = {
84
+ /**
85
+ * State object from Pressable's render callback containing hovered, pressed, and focused states
86
+ */
87
+ pressableState: _propTypes.default.object.isRequired,
88
+ /**
89
+ * Theme token resolver callback from useThemeTokensCallback that accepts appearance state
90
+ */
91
+ getTokens: _propTypes.default.func.isRequired,
92
+ /**
93
+ * Style array for the thumb wrapper containing static and dynamic position styles
94
+ */
95
+ thumbWrapperStyle: _propTypes.default.oneOfType([_propTypes.default.object, _propTypes.default.array]).isRequired,
96
+ /**
97
+ * Copy resolver function from useCopy for retrieving localized strings
98
+ */
99
+ getCopy: _propTypes.default.func.isRequired
100
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.dictionary = void 0;
7
+ const dictionary = exports.dictionary = {
8
+ en: {
9
+ accessibilityLabel: 'Scrollable content',
10
+ instructionText: 'Use Tab, arrow keys, or space bar to navigate',
11
+ progressBarLabel: 'Scroll position'
12
+ },
13
+ fr: {
14
+ accessibilityLabel: 'Contenu défilable',
15
+ instructionText: "Utilisez Tab, les touches fléchées ou la barre d'espace pour naviguer",
16
+ progressBarLabel: 'Position de défilement'
17
+ }
18
+ };
@@ -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');
package/lib/cjs/index.js CHANGED
@@ -471,6 +471,18 @@ Object.defineProperty(exports, "Responsive", {
471
471
  return _Responsive.Responsive;
472
472
  }
473
473
  });
474
+ Object.defineProperty(exports, "Scroll", {
475
+ enumerable: true,
476
+ get: function () {
477
+ return _Scroll.Scroll;
478
+ }
479
+ });
480
+ Object.defineProperty(exports, "ScrollItem", {
481
+ enumerable: true,
482
+ get: function () {
483
+ return _ScrollItem.ScrollItem;
484
+ }
485
+ });
474
486
  Object.defineProperty(exports, "Search", {
475
487
  enumerable: true,
476
488
  get: function () {
@@ -1272,6 +1284,8 @@ var _RadioGroup = require("./Radio/RadioGroup");
1272
1284
  var _RadioCard = require("./RadioCard/RadioCard");
1273
1285
  var _RadioCardGroup = require("./RadioCard/RadioCardGroup");
1274
1286
  var _Responsive = require("./Responsive/Responsive");
1287
+ var _Scroll = require("./Scroll/Scroll");
1288
+ var _ScrollItem = require("./Scroll/ScrollItem");
1275
1289
  var _Search = require("./Search/Search");
1276
1290
  var _Select = require("./Select/Select");
1277
1291
  var _SelectItem = require("./Select/SelectItem");
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useAnimatedValue = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _Animated = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Animated"));
9
+ var _Easing = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Easing"));
10
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
+ const ANIMATION_DURATION = 200;
12
+
13
+ /**
14
+ * Smoothly animates a numeric value to a new target using a quadratic ease-in-out
15
+ * curve. Returns the current interpolated value as a plain number so
16
+ * consumers can use it in non-Animated style props.
17
+ *
18
+ * @param {number} targetValue - The value to animate towards.
19
+ * @param {number} [duration=ANIMATION_DURATION] - Animation duration in ms.
20
+ */
21
+ const useAnimatedValue = function (targetValue) {
22
+ let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ANIMATION_DURATION;
23
+ const [currentValue, setCurrentValue] = _react.default.useState(targetValue);
24
+ const animatedValue = _react.default.useRef(new _Animated.default.Value(targetValue)).current;
25
+ _react.default.useEffect(() => {
26
+ const id = animatedValue.addListener(_ref => {
27
+ let {
28
+ value
29
+ } = _ref;
30
+ return setCurrentValue(value);
31
+ });
32
+ return () => animatedValue.removeListener(id);
33
+ }, [animatedValue]);
34
+ _react.default.useEffect(() => {
35
+ const animation = _Animated.default.timing(animatedValue, {
36
+ toValue: targetValue,
37
+ duration,
38
+ easing: _Easing.default.inOut(_Easing.default.quad),
39
+ useNativeDriver: false
40
+ });
41
+ animation.start();
42
+ return () => animation.stop();
43
+ }, [targetValue, animatedValue, duration]);
44
+ return currentValue;
45
+ };
46
+ exports.useAnimatedValue = useAnimatedValue;
@@ -54,7 +54,8 @@ const unpackFragment = child => {
54
54
  exports.unpackFragment = unpackFragment;
55
55
  const isStringOrNumber = child => typeof child === 'string' || typeof child === 'number';
56
56
  // Wrap an A11yText with neighouring text strings so it doesn't split them into multiple <Text>s
57
- const isWrapable = child => isStringOrNumber(child) || child.type === _A11yText.A11yText || child.type?.name === 'FootnoteLink';
57
+ // Check displayName first as it is explicitly set and reliable regardless of export style (named vs default)
58
+ const isWrapable = child => isStringOrNumber(child) || child.type === _A11yText.A11yText || child.type?.__UDS_COMPONENT_NAME__ === 'FootnoteLink';
58
59
  const combineKeys = childrenArray => childrenArray.reduce((newKey, child) => `${newKey}${child.key || ''}`, '');
59
60
 
60
61
  // Group wrappable children for one `<Text>` parent, merging adjacent text nodes
@@ -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;
@@ -15,28 +15,28 @@ import { applyTextStyles } from '../ThemeProvider/utils/styles';
15
15
  import { useThemeTokens } from '../ThemeProvider/useThemeTokens';
16
16
  import { jsx as _jsx } from "react/jsx-runtime";
17
17
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps]);
18
-
19
- // The top property varies between devices due to how devices render the viewport.
18
+ const HALF_FONT_SIZE = 2;
19
+ const QUARTER_FONT_SIZE = 4;
20
+ const DEFAULT_LINE_HEIGHT = 1;
21
+ const LINE_HEIGHT_MULTIPLIER = 2;
20
22
  const selectTextStyle = _ref => {
21
23
  let {
22
24
  fontSize,
23
25
  lineHeight
24
26
  } = _ref;
25
- return {
26
- fontSize,
27
- lineHeight: lineHeight !== 1 ? lineHeight : fontSize * 2,
28
- ...Platform.select({
29
- ios: {
30
- top: -fontSize / 2
31
- },
32
- android: {
33
- top: fontSize / 4
34
- },
35
- web: {
36
- top: -fontSize / 2.8
37
- }
38
- })
39
- };
27
+ return Platform.select({
28
+ native: {
29
+ fontSize: fontSize / HALF_FONT_SIZE,
30
+ lineHeight: fontSize,
31
+ position: 'relative',
32
+ top: -(fontSize * lineHeight) / HALF_FONT_SIZE
33
+ },
34
+ web: {
35
+ fontSize,
36
+ lineHeight: lineHeight !== DEFAULT_LINE_HEIGHT ? lineHeight : fontSize * LINE_HEIGHT_MULTIPLIER,
37
+ top: -fontSize / QUARTER_FONT_SIZE
38
+ }
39
+ });
40
40
  };
41
41
  export const FootnoteLink = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
42
42
  let {
@@ -80,6 +80,7 @@ export const FootnoteLink = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
80
80
  });
81
81
  });
82
82
  FootnoteLink.displayName = 'FootnoteLink';
83
+ FootnoteLink.__UDS_COMPONENT_NAME__ = 'FootnoteLink';
83
84
  const copyShape = PropTypes.shape({
84
85
  a11yLabel: PropTypes.string.isRequired
85
86
  });
@@ -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