@telus-uds/components-base 4.0.0-alpha.2 → 4.0.0-beta.1

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 (120) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
  3. package/lib/cjs/Autocomplete/constants.js +1 -1
  4. package/lib/cjs/Button/ButtonGroup.js +91 -23
  5. package/lib/cjs/Card/CardBase.js +75 -47
  6. package/lib/cjs/ColourToggle/ColourBubble.js +66 -13
  7. package/lib/cjs/ColourToggle/ColourToggle.js +41 -11
  8. package/lib/cjs/ColourToggle/constants.js +15 -0
  9. package/lib/cjs/ColourToggle/dictionary.js +14 -0
  10. package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
  11. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +7 -1
  12. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -28
  13. package/lib/cjs/TextInput/TextInputBase.js +2 -1
  14. package/lib/cjs/Validator/Validator.js +5 -1
  15. package/lib/cjs/utils/useOverlaidPosition.js +83 -42
  16. package/lib/esm/Autocomplete/Autocomplete.js +13 -1
  17. package/lib/esm/Autocomplete/constants.js +1 -1
  18. package/lib/esm/Button/ButtonGroup.js +91 -23
  19. package/lib/esm/Card/CardBase.js +75 -47
  20. package/lib/esm/ColourToggle/ColourBubble.js +67 -14
  21. package/lib/esm/ColourToggle/ColourToggle.js +41 -11
  22. package/lib/esm/ColourToggle/constants.js +8 -0
  23. package/lib/esm/ColourToggle/dictionary.js +8 -0
  24. package/lib/esm/Listbox/ListboxOverlay.js +7 -1
  25. package/lib/esm/MultiSelectFilter/ModalOverlay.js +8 -2
  26. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -28
  27. package/lib/esm/TextInput/TextInputBase.js +2 -1
  28. package/lib/esm/Validator/Validator.js +5 -1
  29. package/lib/esm/utils/useOverlaidPosition.js +83 -42
  30. package/lib/package.json +3 -3
  31. package/package.json +3 -3
  32. package/src/Autocomplete/Autocomplete.jsx +11 -2
  33. package/src/Autocomplete/constants.js +1 -1
  34. package/src/Button/ButtonGroup.jsx +93 -24
  35. package/src/Card/CardBase.jsx +94 -75
  36. package/src/ColourToggle/ColourBubble.jsx +62 -7
  37. package/src/ColourToggle/ColourToggle.jsx +43 -9
  38. package/src/ColourToggle/constants.js +10 -0
  39. package/src/ColourToggle/dictionary.js +4 -0
  40. package/src/Listbox/ListboxOverlay.jsx +6 -2
  41. package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
  42. package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
  43. package/src/TextInput/TextInputBase.jsx +2 -1
  44. package/src/Validator/Validator.jsx +5 -1
  45. package/src/utils/useOverlaidPosition.js +84 -34
  46. package/types/A11yInfoProvider.d.ts +9 -0
  47. package/types/A11yText.d.ts +13 -0
  48. package/types/ActionCard.d.ts +19 -0
  49. package/types/ActivityIndicator.d.ts +13 -0
  50. package/types/Autocomplete.d.ts +27 -0
  51. package/types/Badge.d.ts +15 -4
  52. package/types/BaseProvider.d.ts +26 -0
  53. package/types/Box.d.ts +13 -3
  54. package/types/Button.d.ts +35 -0
  55. package/types/Card.d.ts +19 -0
  56. package/types/CardGroup.d.ts +23 -0
  57. package/types/Carousel.d.ts +25 -0
  58. package/types/Checkbox.d.ts +15 -2
  59. package/types/CheckboxCard.d.ts +20 -0
  60. package/types/CheckboxCardGroup.d.ts +10 -4
  61. package/types/ChevronLink.d.ts +9 -2
  62. package/types/ColourToggle.d.ts +16 -0
  63. package/types/Common.d.ts +7 -3
  64. package/types/Divider.d.ts +13 -1
  65. package/types/DownloadApp.d.ts +19 -0
  66. package/types/ExpandCollapse.d.ts +16 -9
  67. package/types/ExpandCollapseMini.d.ts +16 -0
  68. package/types/Feedback.d.ts +16 -0
  69. package/types/Fieldset.d.ts +17 -0
  70. package/types/FileUpload.d.ts +6 -0
  71. package/types/FlexGrid.d.ts +57 -0
  72. package/types/Footnote.d.ts +18 -0
  73. package/types/HorizontalScroll.d.ts +15 -0
  74. package/types/HorizontalScrollButton.d.ts +17 -1
  75. package/types/Icon.d.ts +13 -2
  76. package/types/IconButton.d.ts +19 -0
  77. package/types/InputLabel.d.ts +12 -0
  78. package/types/InputSupports.d.ts +19 -0
  79. package/types/Link.d.ts +20 -4
  80. package/types/List.d.ts +11 -4
  81. package/types/Listbox.d.ts +13 -3
  82. package/types/Modal.d.ts +25 -0
  83. package/types/MultiSelectFilter.d.ts +24 -0
  84. package/types/Notification.d.ts +19 -0
  85. package/types/OrderedList.d.ts +17 -0
  86. package/types/Pagination.d.ts +25 -0
  87. package/types/Portal.d.ts +5 -1
  88. package/types/PriceLockup.d.ts +28 -0
  89. package/types/ProductCard.d.ts +20 -0
  90. package/types/ProductCardGroup.d.ts +21 -0
  91. package/types/Progress.d.ts +22 -0
  92. package/types/QuickLinks.d.ts +27 -0
  93. package/types/QuickLinksFeature.d.ts +27 -0
  94. package/types/Radio.d.ts +22 -0
  95. package/types/RadioCard.d.ts +21 -0
  96. package/types/Responsive.d.ts +14 -0
  97. package/types/Scroll.d.ts +2 -2
  98. package/types/Search.d.ts +15 -2
  99. package/types/Select.d.ts +12 -4
  100. package/types/SideNav.d.ts +38 -0
  101. package/types/Skeleton.d.ts +17 -0
  102. package/types/SkipLink.d.ts +14 -0
  103. package/types/Spacer.d.ts +8 -1
  104. package/types/StackView.d.ts +8 -2
  105. package/types/Status.d.ts +2 -3
  106. package/types/StepTracker.d.ts +16 -0
  107. package/types/TabBar.d.ts +25 -0
  108. package/types/Tabs.d.ts +12 -3
  109. package/types/Tags.d.ts +24 -0
  110. package/types/TextButton.d.ts +11 -4
  111. package/types/TextInput.d.ts +38 -0
  112. package/types/ThemeProvider.d.ts +29 -0
  113. package/types/Timeline.d.ts +16 -0
  114. package/types/ToggleSwitch.d.ts +11 -2
  115. package/types/ToolTip.d.ts +7 -3
  116. package/types/TooltipButton.d.ts +20 -0
  117. package/types/Typography.d.ts +8 -2
  118. package/types/Validator.d.ts +16 -0
  119. package/types/ViewportProvider.d.ts +12 -0
  120. package/types/index.d.ts +197 -50
@@ -13,9 +13,12 @@ var _tokens = require("../utils/props/tokens");
13
13
  var _selectSystemProps = require("../utils/props/selectSystemProps");
14
14
  var _variantProp = require("../utils/props/variantProp");
15
15
  var _viewProps = require("../utils/props/viewProps");
16
+ var _useCopy = require("../utils/useCopy");
16
17
  var _StackWrap = require("../StackView/StackWrap");
17
18
  var _Typography = require("../Typography/Typography");
18
19
  var _ColourBubble = require("./ColourBubble");
20
+ var _constants = require("./constants");
21
+ var _dictionary = require("./dictionary");
19
22
  var _jsxRuntime = require("react/jsx-runtime");
20
23
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
21
24
  const [selectProps, selectedSystemPropTypes] = (0, _selectSystemProps.selectSystemProps)([_a11yProps.a11yProps, _viewProps.viewProps]);
@@ -27,37 +30,49 @@ const ColourToggle = exports.ColourToggle = /*#__PURE__*/_react.default.forwardR
27
30
  items,
28
31
  onChange,
29
32
  showTooltips,
33
+ copy = 'en',
34
+ dictionary = _dictionary.DEFAULT_COLOUR_TOGGLE_DICTIONARY,
30
35
  ...rest
31
36
  } = _ref;
32
37
  const [currentColourId, setCurrentColourId] = _react.default.useState(defaultColourId);
33
38
  const getTokens = (0, _useThemeTokens.useThemeTokensCallback)('ColourToggle', tokens, variant);
39
+ const getCopy = (0, _useCopy.useCopy)({
40
+ dictionary,
41
+ copy
42
+ });
34
43
  const {
35
44
  space
36
45
  } = getTokens();
37
- const {
38
- colourName: currentColourName = ''
39
- } = items.find(item => item.id === currentColourId) || '';
46
+ const currentItem = items.find(_ref2 => {
47
+ let {
48
+ id
49
+ } = _ref2;
50
+ return id === currentColourId;
51
+ });
40
52
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_View.default, {
41
53
  ref: ref,
42
54
  ...selectProps(rest),
43
55
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Typography.Typography, {
44
- children: currentColourName
56
+ children: currentItem?.unavailable ? `${currentItem.colourName} ${getCopy('unavailable')}` : currentItem?.colourName ?? ''
45
57
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_StackWrap.StackWrap, {
46
58
  space: space,
47
59
  accessibilityRole: "radiogroup",
48
- children: items.map((_ref2, index) => {
60
+ children: items.map((_ref3, index) => {
49
61
  let {
50
62
  id,
51
63
  colourHexCode,
52
- colourName
53
- } = _ref2;
64
+ colourName,
65
+ unavailable,
66
+ disabled
67
+ } = _ref3;
54
68
  const colourBubbleId = id || `ColourBubble[${index}]`;
55
69
  const handleChangeColour = event => {
56
70
  setCurrentColourId(id);
57
71
  onChange?.(event, {
58
72
  id,
59
73
  colourHexCode,
60
- colourName
74
+ colourName,
75
+ unavailable
61
76
  });
62
77
  };
63
78
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ColourBubble.ColourBubble, {
@@ -67,7 +82,9 @@ const ColourToggle = exports.ColourToggle = /*#__PURE__*/_react.default.forwardR
67
82
  colourHexCode: colourHexCode,
68
83
  colourName: colourName,
69
84
  onPress: handleChangeColour,
70
- showTooltip: showTooltips
85
+ showTooltip: showTooltips,
86
+ unavailable: unavailable,
87
+ isDisabled: !!disabled
71
88
  }, colourBubbleId);
72
89
  })
73
90
  })]
@@ -94,7 +111,9 @@ ColourToggle.propTypes = {
94
111
  items: _propTypes.default.arrayOf(_propTypes.default.exact({
95
112
  colourHexCode: _propTypes.default.string,
96
113
  colourName: _propTypes.default.string,
97
- id: _propTypes.default.string
114
+ id: _propTypes.default.string,
115
+ unavailable: _propTypes.default.oneOf(Object.values(_constants.UNAVAILABLE_VARIANT)),
116
+ disabled: _propTypes.default.bool
98
117
  })),
99
118
  /**
100
119
  * If provided, this function is called when the current selection of the color is changed of all currently `items`. Receives two parameters: item object selected and the event
@@ -103,5 +122,16 @@ ColourToggle.propTypes = {
103
122
  /**
104
123
  * When true, displays each colour's name as a tooltip on hover (web only).
105
124
  */
106
- showTooltips: _propTypes.default.bool
125
+ showTooltips: _propTypes.default.bool,
126
+ /**
127
+ * Select English or French copy.
128
+ */
129
+ copy: _propTypes.default.oneOf(['en', 'fr']),
130
+ /**
131
+ * Override default labels.
132
+ */
133
+ dictionary: _propTypes.default.shape({
134
+ en: _constants.DICTIONARY_CONTENT_SHAPE,
135
+ fr: _constants.DICTIONARY_CONTENT_SHAPE
136
+ })
107
137
  };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.UNAVAILABLE_VARIANT = exports.DICTIONARY_CONTENT_SHAPE = void 0;
7
+ var _propTypes = _interopRequireDefault(require("prop-types"));
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
+ const UNAVAILABLE_VARIANT = exports.UNAVAILABLE_VARIANT = {
10
+ DARK: 'dark',
11
+ LIGHT: 'light'
12
+ };
13
+ const DICTIONARY_CONTENT_SHAPE = exports.DICTIONARY_CONTENT_SHAPE = _propTypes.default.shape({
14
+ unavailable: _propTypes.default.string
15
+ });
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.DEFAULT_COLOUR_TOGGLE_DICTIONARY = void 0;
7
+ const DEFAULT_COLOUR_TOGGLE_DICTIONARY = exports.DEFAULT_COLOUR_TOGGLE_DICTIONARY = {
8
+ en: {
9
+ unavailable: '(Out of stock)'
10
+ },
11
+ fr: {
12
+ unavailable: '(En rupture de stock)'
13
+ }
14
+ };
@@ -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');
@@ -207,7 +207,11 @@ const Validator = exports.Validator = /*#__PURE__*/_react.default.forwardRef((_r
207
207
 
208
208
  // Sync external value prop to internal state
209
209
  _react.default.useEffect(() => {
210
- if (value && Number(value).toString() !== 'NaN') {
210
+ if (value === '') {
211
+ setCodes(Array(validatorsLength).fill(''));
212
+ return;
213
+ }
214
+ if (Number(value).toString() !== 'NaN') {
211
215
  const digits = value.split('').slice(0, validatorsLength);
212
216
  const newCodes = Array(validatorsLength).fill('');
213
217
  digits.forEach((digit, i) => {
@@ -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;