@telus-uds/components-base 3.29.1 → 3.31.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 (71) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
  3. package/lib/cjs/Autocomplete/constants.js +1 -1
  4. package/lib/cjs/Carousel/Carousel.js +6 -1
  5. package/lib/cjs/Carousel/CarouselThumbnail.js +123 -31
  6. package/lib/cjs/Carousel/CarouselThumbnailNavigation.js +8 -1
  7. package/lib/cjs/Footnote/FootnoteLink.js +18 -17
  8. package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
  9. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +7 -1
  10. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -28
  11. package/lib/cjs/Progress/Progress.js +30 -5
  12. package/lib/cjs/Scroll/Scroll.js +466 -0
  13. package/lib/cjs/Scroll/ScrollContext.js +55 -0
  14. package/lib/cjs/Scroll/ScrollItem.js +103 -0
  15. package/lib/cjs/Scroll/ScrollProgress.js +99 -0
  16. package/lib/cjs/Scroll/dictionary.js +18 -0
  17. package/lib/cjs/Scroll/index.js +29 -0
  18. package/lib/cjs/TextInput/TextInputBase.js +2 -1
  19. package/lib/cjs/index.js +15 -0
  20. package/lib/cjs/utils/animation/index.js +7 -0
  21. package/lib/cjs/utils/animation/useAnimatedValue.js +46 -0
  22. package/lib/cjs/utils/children.js +2 -1
  23. package/lib/cjs/utils/useOverlaidPosition.js +83 -42
  24. package/lib/esm/Autocomplete/Autocomplete.js +13 -1
  25. package/lib/esm/Autocomplete/constants.js +1 -1
  26. package/lib/esm/Carousel/Carousel.js +6 -1
  27. package/lib/esm/Carousel/CarouselThumbnail.js +124 -32
  28. package/lib/esm/Carousel/CarouselThumbnailNavigation.js +8 -1
  29. package/lib/esm/Footnote/FootnoteLink.js +18 -17
  30. package/lib/esm/Listbox/ListboxOverlay.js +7 -1
  31. package/lib/esm/MultiSelectFilter/ModalOverlay.js +8 -2
  32. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -28
  33. package/lib/esm/Progress/Progress.js +30 -5
  34. package/lib/esm/Scroll/Scroll.js +459 -0
  35. package/lib/esm/Scroll/ScrollContext.js +47 -0
  36. package/lib/esm/Scroll/ScrollItem.js +96 -0
  37. package/lib/esm/Scroll/ScrollProgress.js +92 -0
  38. package/lib/esm/Scroll/dictionary.js +12 -0
  39. package/lib/esm/Scroll/index.js +4 -0
  40. package/lib/esm/TextInput/TextInputBase.js +2 -1
  41. package/lib/esm/index.js +1 -0
  42. package/lib/esm/utils/animation/index.js +1 -1
  43. package/lib/esm/utils/animation/useAnimatedValue.js +39 -0
  44. package/lib/esm/utils/children.js +2 -1
  45. package/lib/esm/utils/useOverlaidPosition.js +83 -42
  46. package/lib/package.json +2 -2
  47. package/package.json +2 -2
  48. package/src/Autocomplete/Autocomplete.jsx +11 -2
  49. package/src/Autocomplete/constants.js +1 -1
  50. package/src/Carousel/Carousel.jsx +6 -1
  51. package/src/Carousel/CarouselThumbnail.jsx +153 -64
  52. package/src/Carousel/CarouselThumbnailNavigation.jsx +8 -2
  53. package/src/Footnote/FootnoteLink.jsx +17 -12
  54. package/src/Listbox/ListboxOverlay.jsx +6 -2
  55. package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
  56. package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
  57. package/src/Progress/Progress.jsx +22 -3
  58. package/src/Scroll/Scroll.jsx +488 -0
  59. package/src/Scroll/ScrollContext.jsx +41 -0
  60. package/src/Scroll/ScrollItem.jsx +89 -0
  61. package/src/Scroll/ScrollProgress.jsx +75 -0
  62. package/src/Scroll/dictionary.js +12 -0
  63. package/src/Scroll/index.js +5 -0
  64. package/src/TextInput/TextInputBase.jsx +2 -1
  65. package/src/index.js +1 -0
  66. package/src/utils/animation/index.js +1 -1
  67. package/src/utils/animation/useAnimatedValue.js +37 -0
  68. package/src/utils/children.jsx +4 -1
  69. package/src/utils/useOverlaidPosition.js +84 -34
  70. package/types/Scroll.d.ts +66 -0
  71. package/types/index.d.ts +9 -0
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = 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 _utils = require("../utils");
11
+ var _Progress = _interopRequireDefault(require("../Progress"));
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ const MAX_PERCENTAGE = 100;
15
+ const BORDER_RADIUS_DIVISOR = 2;
16
+ const staticProgressTokens = {
17
+ borderWidth: 0,
18
+ borderColor: 'transparent'
19
+ };
20
+ const staticProgressBarTokens = {
21
+ gradient: null,
22
+ outlineWidth: 0
23
+ };
24
+ const selectProgressTokens = _ref => {
25
+ let {
26
+ progressTrackHeight,
27
+ progressBarHeight,
28
+ progressTrackColor
29
+ } = _ref;
30
+ return {
31
+ height: progressTrackHeight,
32
+ barHeight: progressBarHeight,
33
+ backgroundColor: progressTrackColor,
34
+ borderRadius: progressTrackHeight / BORDER_RADIUS_DIVISOR
35
+ };
36
+ };
37
+ const selectBarTokens = _ref2 => {
38
+ let {
39
+ progressBarColor,
40
+ progressBarHeight
41
+ } = _ref2;
42
+ return {
43
+ backgroundColor: progressBarColor,
44
+ borderRadius: progressBarHeight / BORDER_RADIUS_DIVISOR
45
+ };
46
+ };
47
+ const ScrollProgress = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
48
+ let {
49
+ pressableState,
50
+ getTokens,
51
+ thumbWrapperStyle,
52
+ getCopy
53
+ } = _ref3;
54
+ const state = (0, _utils.resolvePressableState)(pressableState);
55
+ const themeTokens = getTokens(state);
56
+ const animatedBarHeight = (0, _utils.useAnimatedValue)(themeTokens.progressBarHeight);
57
+ const animatedTokens = {
58
+ ...themeTokens,
59
+ progressBarHeight: animatedBarHeight
60
+ };
61
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Progress.default, {
62
+ tokens: {
63
+ ...staticProgressTokens,
64
+ ...selectProgressTokens(animatedTokens)
65
+ },
66
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
67
+ ref: ref,
68
+ style: thumbWrapperStyle,
69
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Progress.default.Bar, {
70
+ percentage: MAX_PERCENTAGE,
71
+ tokens: {
72
+ ...staticProgressBarTokens,
73
+ ...selectBarTokens(animatedTokens)
74
+ },
75
+ accessibilityLabel: getCopy('progressBarLabel')
76
+ })
77
+ })
78
+ });
79
+ });
80
+ ScrollProgress.displayName = 'ScrollProgress';
81
+ ScrollProgress.propTypes = {
82
+ /**
83
+ * State object from Pressable's render callback containing hovered, pressed, and focused states
84
+ */
85
+ pressableState: _propTypes.default.object.isRequired,
86
+ /**
87
+ * Theme token resolver callback from useThemeTokensCallback that accepts appearance state
88
+ */
89
+ getTokens: _propTypes.default.func.isRequired,
90
+ /**
91
+ * Style array for the thumb wrapper containing static and dynamic position styles
92
+ */
93
+ thumbWrapperStyle: _propTypes.default.oneOfType([_propTypes.default.object, _propTypes.default.array]).isRequired,
94
+ /**
95
+ * Copy resolver function from useCopy for retrieving localized strings
96
+ */
97
+ getCopy: _propTypes.default.func.isRequired
98
+ };
99
+ var _default = exports.default = ScrollProgress;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _default = exports.default = {
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
+ };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "ScrollItem", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _ScrollItem.default;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "ScrollProvider", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _ScrollContext.ScrollProvider;
16
+ }
17
+ });
18
+ exports.default = void 0;
19
+ Object.defineProperty(exports, "useScroll", {
20
+ enumerable: true,
21
+ get: function () {
22
+ return _ScrollContext.useScroll;
23
+ }
24
+ });
25
+ var _Scroll = _interopRequireDefault(require("./Scroll"));
26
+ var _ScrollItem = _interopRequireDefault(require("./ScrollItem"));
27
+ var _ScrollContext = require("./ScrollContext");
28
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
29
+ var _default = exports.default = _Scroll.default;
@@ -279,7 +279,8 @@ const TextInputBase = /*#__PURE__*/_react.default.forwardRef((_ref8, ref) => {
279
279
  }, [element, pattern]);
280
280
  const handleChangeText = event => {
281
281
  const text = event.nativeEvent?.text ?? event.target?.value;
282
- let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') || undefined : text;
282
+ // Do NOT use `|| undefined`: empty string is falsy and would break the controlled contract
283
+ let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') ?? '' : text;
283
284
  if (type === 'card' && filteredText) {
284
285
  const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
285
286
  const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g');
package/lib/cjs/index.js CHANGED
@@ -57,6 +57,8 @@ var _exportNames = {
57
57
  RadioCard: true,
58
58
  RadioCardGroup: true,
59
59
  Responsive: true,
60
+ Scroll: true,
61
+ ScrollItem: true,
60
62
  Search: true,
61
63
  Select: true,
62
64
  Shortcuts: true,
@@ -422,6 +424,18 @@ Object.defineProperty(exports, "Responsive", {
422
424
  return _Responsive.default;
423
425
  }
424
426
  });
427
+ Object.defineProperty(exports, "Scroll", {
428
+ enumerable: true,
429
+ get: function () {
430
+ return _Scroll.default;
431
+ }
432
+ });
433
+ Object.defineProperty(exports, "ScrollItem", {
434
+ enumerable: true,
435
+ get: function () {
436
+ return _Scroll.ScrollItem;
437
+ }
438
+ });
425
439
  Object.defineProperty(exports, "Search", {
426
440
  enumerable: true,
427
441
  get: function () {
@@ -756,6 +770,7 @@ var _QuickLinksFeature = _interopRequireDefault(require("./QuickLinksFeature"));
756
770
  var _Radio = _interopRequireWildcard(require("./Radio"));
757
771
  var _RadioCard = _interopRequireWildcard(require("./RadioCard"));
758
772
  var _Responsive = _interopRequireDefault(require("./Responsive"));
773
+ var _Scroll = _interopRequireWildcard(require("./Scroll"));
759
774
  var _Search = _interopRequireDefault(require("./Search"));
760
775
  var _Select = _interopRequireDefault(require("./Select"));
761
776
  var _Shortcuts = _interopRequireWildcard(require("./Shortcuts"));
@@ -3,11 +3,18 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "useAnimatedValue", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _useAnimatedValue.default;
10
+ }
11
+ });
6
12
  Object.defineProperty(exports, "useVerticalExpandAnimation", {
7
13
  enumerable: true,
8
14
  get: function () {
9
15
  return _useVerticalExpandAnimation.default;
10
16
  }
11
17
  });
18
+ var _useAnimatedValue = _interopRequireDefault(require("./useAnimatedValue"));
12
19
  var _useVerticalExpandAnimation = _interopRequireDefault(require("./useVerticalExpandAnimation"));
13
20
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = 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
+ var _default = exports.default = 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.default || 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.default || 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,
@@ -119,6 +119,7 @@ const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
119
119
  value,
120
120
  helpText = '',
121
121
  loadingLabel,
122
+ dropdownPosition = 'bottom',
122
123
  tokens,
123
124
  ...rest
124
125
  } = _ref2;
@@ -174,6 +175,13 @@ const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
174
175
  isShown: isExpanded || hintExpansionEnabled,
175
176
  offsets: {
176
177
  vertical: Platform.OS !== 'web' && (hint || inputLabel) ? 28 : 4
178
+ },
179
+ align: dropdownPosition === 'top' ? {
180
+ center: 'center',
181
+ bottom: 'top'
182
+ } : {
183
+ center: 'center',
184
+ top: 'bottom'
177
185
  }
178
186
  });
179
187
  const targetRef = React.useRef(null);
@@ -526,6 +534,10 @@ Autocomplete.propTypes = {
526
534
  /**
527
535
  * Input value for controlled usage
528
536
  */
529
- value: PropTypes.string
537
+ value: PropTypes.string,
538
+ /**
539
+ * Controls whether the dropdown renders above or below the input. Defaults to 'bottom'.
540
+ */
541
+ dropdownPosition: PropTypes.oneOf(['top', 'bottom'])
530
542
  };
531
543
  export default Autocomplete;
@@ -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;
@@ -1170,9 +1170,14 @@ Carousel.propTypes = {
1170
1170
  * An array of objects containing information on the thumbnails to be rendered as navigation panel
1171
1171
  */
1172
1172
  thumbnails: PropTypes.arrayOf(PropTypes.shape({
1173
+ /** Accessibility label for the thumbnail image, used by assistive technologies. */
1173
1174
  accessibilityLabel: PropTypes.string,
1175
+ /** Alternative text for the thumbnail image, displayed when the image cannot be rendered. */
1174
1176
  alt: PropTypes.string,
1175
- src: PropTypes.string
1177
+ /** When true, renders a play icon overlay on the thumbnail to indicate that the slide contains a video. */
1178
+ isVideo: PropTypes.bool,
1179
+ /** URL or path of the thumbnail image. When used with `isVideo`, this should be the video's poster/preview image. */
1180
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
1176
1181
  })),
1177
1182
  /**
1178
1183
  * Minimal part of slide width must be swiped for changing index.