@atlaskit/react-select 4.0.2 → 4.1.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.
@@ -20,6 +20,7 @@ var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits
20
20
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
21
21
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
22
22
  var _react = _interopRequireWildcard(require("react"));
23
+ var _bindEventListener = require("bind-event-listener");
23
24
  var _deviceCheck = require("@atlaskit/ds-lib/device-check");
24
25
  var _isSafari = require("@atlaskit/ds-lib/is-safari");
25
26
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
@@ -33,6 +34,7 @@ var _filters = require("./filters");
33
34
  var _classnames = require("./internal/classnames");
34
35
  var _cleanValue = require("./internal/clean-value");
35
36
  var _isDocumentEl = require("./internal/is-document-el");
37
+ var _menuPortalCloseContext = require("./internal/menu-portal-close-context");
36
38
  var _multiValueAsValue = require("./internal/multi-value-as-value");
37
39
  var _notifyOpenLayerObserver = require("./internal/notify-open-layer-observer");
38
40
  var _requiredInput = _interopRequireDefault(require("./internal/required-input"));
@@ -336,7 +338,8 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
336
338
  prevWasFocused: false,
337
339
  inputIsHiddenAfterUpdate: undefined,
338
340
  prevProps: undefined,
339
- instancePrefix: ''
341
+ instancePrefix: '',
342
+ controlElement: null
340
343
  });
341
344
  // Misc. Instance Properties
342
345
  // ------------------------------
@@ -347,11 +350,26 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
347
350
  (0, _defineProperty2.default)(_this, "initialTouchY", 0);
348
351
  (0, _defineProperty2.default)(_this, "openAfterFocus", false);
349
352
  (0, _defineProperty2.default)(_this, "scrollToFocusedOptionOnUpdate", false);
353
+ // Cleanup for a pending document `pointerup` listener registered by
354
+ // `openMenuAfterPointerUp`. See that method for the full rationale.
355
+ (0, _defineProperty2.default)(_this, "deferredOpenMenuCleanup", null);
350
356
  // Refs
351
357
  // ------------------------------
352
358
  (0, _defineProperty2.default)(_this, "controlRef", null);
353
359
  (0, _defineProperty2.default)(_this, "getControlRef", function (ref) {
354
360
  _this.controlRef = ref;
361
+ // Mirror the ref into state on the top-layer path so
362
+ // `MenuPortalTopLayer`'s layout effects react to the anchor attaching.
363
+ // Skip the null-ref (unmount) case: setState during unmount is unsafe
364
+ // and the state is dropped with the instance anyway.
365
+ if (ref === null) {
366
+ return;
367
+ }
368
+ if (_this.state.controlElement !== ref && (0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
369
+ _this.setState({
370
+ controlElement: ref
371
+ });
372
+ }
355
373
  });
356
374
  (0, _defineProperty2.default)(_this, "focusedOptionRef", null);
357
375
  (0, _defineProperty2.default)(_this, "getFocusedOptionRef", function (ref) {
@@ -562,7 +580,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
562
580
  _this.focusInput();
563
581
  } else if (!_this.props.menuIsOpen) {
564
582
  if (openMenuOnClick) {
565
- _this.openMenu('first');
583
+ _this.openMenuAfterPointerUp('first');
566
584
  }
567
585
  } else {
568
586
  if (event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
@@ -591,7 +609,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
591
609
  });
592
610
  _this.onMenuClose();
593
611
  } else {
594
- _this.openMenu('first');
612
+ _this.openMenuAfterPointerUp('first');
595
613
  }
596
614
  event.preventDefault();
597
615
  });
@@ -709,7 +727,14 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
709
727
  isFocused: true
710
728
  });
711
729
  if (_this.openAfterFocus || _this.props.openMenuOnFocus) {
712
- _this.openMenu('first');
730
+ // `openAfterFocus` always follows a pointer gesture, so defer past
731
+ // pointerup. `openMenuOnFocus` alone can come from a keyboard tab
732
+ // with no pointer gesture in flight, so open synchronously.
733
+ if (_this.openAfterFocus) {
734
+ _this.openMenuAfterPointerUp('first');
735
+ } else {
736
+ _this.openMenu('first');
737
+ }
713
738
  }
714
739
  _this.openAfterFocus = false;
715
740
  });
@@ -1000,6 +1025,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1000
1025
  value: function componentWillUnmount() {
1001
1026
  this.stopListeningComposition();
1002
1027
  this.stopListeningToTouch();
1028
+ this.cancelDeferredOpenMenu();
1003
1029
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
1004
1030
  document.removeEventListener('scroll', this.onScroll, true);
1005
1031
  }
@@ -1046,10 +1072,72 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1046
1072
  }
1047
1073
  this.inputRef.blur();
1048
1074
  }
1075
+ }, {
1076
+ key: "shouldDeferOpenPastPointerUp",
1077
+ value:
1078
+ /**
1079
+ * Whether to defer the menu open past the in-flight pointer gesture.
1080
+ * Any renderer that drives a `popover="auto"` element must, otherwise
1081
+ * the browser's light-dismiss runs on the matching `pointerup` and
1082
+ * closes the menu immediately. Today only the top-layer path needs it.
1083
+ */
1084
+ function shouldDeferOpenPastPointerUp() {
1085
+ return (0, _platformFeatureFlags.fg)('platform-dst-top-layer');
1086
+ }
1087
+
1088
+ /**
1089
+ * Open the menu after the current pointer gesture, instead of
1090
+ * synchronously inside `mousedown`.
1091
+ *
1092
+ * On the top-layer path the menu is a `popover="auto"` element. The
1093
+ * browser captures the pointerdown target before the popover exists, so
1094
+ * opening synchronously gets immediately light-dismissed on pointerup
1095
+ * (and the matching `beforetoggle: closed` is not cancellable). Deferring
1096
+ * to the next `pointerup` avoids that.
1097
+ *
1098
+ * We listen for `pointerup` rather than `click` because `pointerup` is
1099
+ * the exact event the browser uses for light-dismiss (earliest safe
1100
+ * moment), always fires (`click` requires same down/up target), is hard
1101
+ * to lose to upstream `stopPropagation`, and is uniform across input
1102
+ * types. Off the top-layer path we open synchronously as before.
1103
+ */
1104
+ }, {
1105
+ key: "openMenuAfterPointerUp",
1106
+ value: function openMenuAfterPointerUp(focusOption) {
1107
+ var _this2 = this;
1108
+ if (!this.shouldDeferOpenPastPointerUp()) {
1109
+ this.openMenu(focusOption);
1110
+ return;
1111
+ }
1112
+ // A second pointerdown can land before the queued pointerup if the
1113
+ // user releases and re-clicks very quickly. Replace any pending
1114
+ // deferred open with the latest one so we never stack listeners.
1115
+ this.cancelDeferredOpenMenu();
1116
+ var handlePointerUp = function handlePointerUp() {
1117
+ _this2.deferredOpenMenuCleanup = null;
1118
+ _this2.openMenu(focusOption);
1119
+ };
1120
+ this.deferredOpenMenuCleanup = (0, _bindEventListener.bind)(document, {
1121
+ type: 'pointerup',
1122
+ listener: handlePointerUp,
1123
+ options: {
1124
+ capture: true,
1125
+ once: true
1126
+ }
1127
+ });
1128
+ }
1129
+ }, {
1130
+ key: "cancelDeferredOpenMenu",
1131
+ value: function cancelDeferredOpenMenu() {
1132
+ if (this.deferredOpenMenuCleanup) {
1133
+ this.deferredOpenMenuCleanup();
1134
+ this.deferredOpenMenuCleanup = null;
1135
+ }
1136
+ }
1049
1137
  }, {
1050
1138
  key: "openMenu",
1051
1139
  value: function openMenu(focusOption) {
1052
- var _this2 = this;
1140
+ var _this3 = this;
1053
1141
  var _this$state2 = this.state,
1054
1142
  selectValue = _this$state2.selectValue,
1055
1143
  isFocused = _this$state2.isFocused;
@@ -1071,25 +1159,25 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1071
1159
  focusedOption: focusedOption,
1072
1160
  focusedOptionId: this.getFocusedOptionId(focusedOption)
1073
1161
  }, function () {
1074
- return _this2.onMenuOpen();
1162
+ return _this3.onMenuOpen();
1075
1163
  });
1076
1164
  (0, _isSafari.isSafari)() && focusedOption && this.updateInputLabel(this.calculateInputLabel(focusedOption, openAtIndex));
1077
1165
  }
1078
1166
  }, {
1079
1167
  key: "updateInputLabel",
1080
1168
  value: function updateInputLabel(inputLabel) {
1081
- var _this3 = this;
1169
+ var _this4 = this;
1082
1170
  if (inputLabel) {
1083
1171
  var _this$inputRef;
1084
1172
  (_this$inputRef = this.inputRef) === null || _this$inputRef === void 0 || _this$inputRef.setAttribute('aria-label', inputLabel);
1085
1173
  setTimeout(function () {
1086
- var normalizedLabel = _this3.props['aria-label'] || _this3.props.label;
1174
+ var normalizedLabel = _this4.props['aria-label'] || _this4.props.label;
1087
1175
  if (normalizedLabel) {
1088
- var _this3$inputRef;
1089
- (_this3$inputRef = _this3.inputRef) === null || _this3$inputRef === void 0 || _this3$inputRef.setAttribute('aria-label', normalizedLabel);
1176
+ var _this4$inputRef;
1177
+ (_this4$inputRef = _this4.inputRef) === null || _this4$inputRef === void 0 || _this4$inputRef.setAttribute('aria-label', normalizedLabel);
1090
1178
  } else {
1091
- var _this3$inputRef2;
1092
- (_this3$inputRef2 = _this3.inputRef) === null || _this3$inputRef2 === void 0 || _this3$inputRef2.removeAttribute('aria-label');
1179
+ var _this4$inputRef2;
1180
+ (_this4$inputRef2 = _this4.inputRef) === null || _this4$inputRef2 === void 0 || _this4$inputRef2.removeAttribute('aria-label');
1093
1181
  }
1094
1182
  }, 500);
1095
1183
  }
@@ -1097,14 +1185,14 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1097
1185
  }, {
1098
1186
  key: "calculateInputLabel",
1099
1187
  value: function calculateInputLabel(focusedOption, optionIndex) {
1100
- var _this4 = this;
1188
+ var _this5 = this;
1101
1189
  var options = this.props.options;
1102
1190
  var isOptionsGrouped = options === null || options === void 0 ? void 0 : options.every(function (obj) {
1103
1191
  return (0, _typeof2.default)(obj) === 'object' && obj !== null && 'options' in obj;
1104
1192
  });
1105
1193
  var inputLabel = this.getOptionLabel(focusedOption);
1106
1194
  var isOptionFocused = function isOptionFocused(option) {
1107
- return _this4.getOptionLabel(option) === inputLabel;
1195
+ return _this5.getOptionLabel(option) === inputLabel;
1108
1196
  };
1109
1197
  var groupData = options === null || options === void 0 ? void 0 : options.find(function (option) {
1110
1198
  var _groupCandidate$optio, _groupCandidate$optio2;
@@ -1492,7 +1580,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1492
1580
  }, {
1493
1581
  key: "renderPlaceholderOrValue",
1494
1582
  value: function renderPlaceholderOrValue() {
1495
- var _this5 = this;
1583
+ var _this6 = this;
1496
1584
  var _this$getComponents2 = this.getComponents(),
1497
1585
  MultiValue = _this$getComponents2.MultiValue,
1498
1586
  MultiValueContainer = _this$getComponents2.MultiValueContainer,
@@ -1527,7 +1615,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1527
1615
  if (isMulti) {
1528
1616
  return selectValue.map(function (opt, index) {
1529
1617
  var isOptionFocused = opt === focusedValue;
1530
- var key = "".concat(_this5.getOptionLabel(opt), "-").concat(_this5.getOptionValue(opt));
1618
+ var key = "".concat(_this6.getOptionLabel(opt), "-").concat(_this6.getOptionValue(opt));
1531
1619
  return /*#__PURE__*/_react.default.createElement(MultiValue, (0, _extends2.default)({}, commonProps, {
1532
1620
  components: {
1533
1621
  Container: MultiValueContainer,
@@ -1540,10 +1628,10 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1540
1628
  index: index,
1541
1629
  removeProps: _objectSpread(_objectSpread({
1542
1630
  onClick: function onClick() {
1543
- return _this5.removeValue(opt);
1631
+ return _this6.removeValue(opt);
1544
1632
  },
1545
1633
  onTouchEnd: function onTouchEnd() {
1546
- return _this5.removeValue(opt);
1634
+ return _this6.removeValue(opt);
1547
1635
  },
1548
1636
  onMouseDown: function onMouseDown(e) {
1549
1637
  e.preventDefault();
@@ -1551,15 +1639,15 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1551
1639
  }, testId && {
1552
1640
  'data-testid': "".concat(testId, "-select--multivalue-").concat(index, "-remove")
1553
1641
  }), {}, {
1554
- id: "".concat(_this5.getElementId('selected-value'), "-").concat(index, "-remove")
1642
+ id: "".concat(_this6.getElementId('selected-value'), "-").concat(index, "-remove")
1555
1643
  }),
1556
1644
  data: opt,
1557
1645
  innerProps: _objectSpread(_objectSpread({}, testId && {
1558
1646
  'data-testid': "".concat(testId, "-select--multivalue-").concat(index)
1559
1647
  }), {}, {
1560
- id: "".concat(_this5.getElementId('selected-value'), "-").concat(index)
1648
+ id: "".concat(_this6.getElementId('selected-value'), "-").concat(index)
1561
1649
  })
1562
- }), _this5.formatOptionLabel(opt, 'value'));
1650
+ }), _this6.formatOptionLabel(opt, 'value'));
1563
1651
  });
1564
1652
  }
1565
1653
  if (inputValue) {
@@ -1665,7 +1753,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1665
1753
  }, {
1666
1754
  key: "renderMenu",
1667
1755
  value: function renderMenu() {
1668
- var _this6 = this;
1756
+ var _this7 = this;
1669
1757
  var _this$getComponents6 = this.getComponents(),
1670
1758
  Group = _this$getComponents6.Group,
1671
1759
  GroupHeading = _this$getComponents6.GroupHeading,
@@ -1708,19 +1796,19 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1708
1796
  value = props.value;
1709
1797
  var isFocused = focusedOption === data;
1710
1798
  var onHover = isDisabled ? undefined : function () {
1711
- return _this6.onOptionHover(data);
1799
+ return _this7.onOptionHover(data);
1712
1800
  };
1713
1801
  var onSelect = isDisabled ? undefined : function () {
1714
- return _this6.selectOption(data);
1802
+ return _this7.selectOption(data);
1715
1803
  };
1716
- var optionId = "".concat(_this6.getElementId('option'), "-").concat(id);
1804
+ var optionId = "".concat(_this7.getElementId('option'), "-").concat(id);
1717
1805
  var innerProps = _objectSpread({
1718
1806
  id: optionId,
1719
1807
  onClick: onSelect,
1720
1808
  onMouseMove: onHover,
1721
1809
  onMouseOver: onHover,
1722
- role: _this6.props['UNSAFE_is_experimental_generic'] ? 'listitem' : 'option',
1723
- 'aria-selected': _this6.props['UNSAFE_is_experimental_generic'] ? undefined : isSelected,
1810
+ role: _this7.props['UNSAFE_is_experimental_generic'] ? 'listitem' : 'option',
1811
+ 'aria-selected': _this7.props['UNSAFE_is_experimental_generic'] ? undefined : isSelected,
1724
1812
  // We don't want aria-disabled if it's false. It's just noisy.
1725
1813
  'aria-disabled': !isDisabled ? undefined : isDisabled,
1726
1814
  'aria-describedby': headingId
@@ -1737,8 +1825,8 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1737
1825
  type: type,
1738
1826
  value: value,
1739
1827
  isFocused: isFocused,
1740
- innerRef: isFocused ? _this6.getFocusedOptionRef : undefined
1741
- }), _this6.formatOptionLabel(props.data, 'menu'));
1828
+ innerRef: isFocused ? _this7.getFocusedOptionRef : undefined
1829
+ }), _this7.formatOptionLabel(props.data, 'menu'));
1742
1830
  };
1743
1831
  var menuUI;
1744
1832
  if (this.hasOptions()) {
@@ -1748,7 +1836,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1748
1836
  var data = item.data,
1749
1837
  options = item.options,
1750
1838
  groupIndex = item.index;
1751
- var groupId = "".concat(_this6.getElementId('group'), "-").concat(groupIndex);
1839
+ var groupId = "".concat(_this7.getElementId('group'), "-").concat(groupIndex);
1752
1840
  var headingId = "".concat(groupId, "-heading");
1753
1841
  return /*#__PURE__*/_react.default.createElement(Group, (0, _extends2.default)({}, commonProps, {
1754
1842
  key: groupId,
@@ -1761,7 +1849,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1761
1849
  }, testId && {
1762
1850
  'data-testid': "".concat(testId, "-select--group-").concat(groupIndex, "-heading")
1763
1851
  }),
1764
- label: _this6.formatGroupLabel(item.data)
1852
+ label: _this7.formatGroupLabel(item.data)
1765
1853
  }), item.options.map(function (option) {
1766
1854
  return render(option, "".concat(groupIndex, "-").concat(option.index), headingId);
1767
1855
  }));
@@ -1808,9 +1896,9 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1808
1896
  return /*#__PURE__*/_react.default.createElement(Menu, (0, _extends2.default)({}, commonProps, menuPlacementProps, {
1809
1897
  innerRef: ref,
1810
1898
  innerProps: _objectSpread({
1811
- onMouseDown: _this6.onMenuMouseDown,
1812
- onMouseMove: _this6.onMenuMouseMove,
1813
- id: _this6.props.components.Menu ? _this6.getElementId('listbox') : undefined
1899
+ onMouseDown: _this7.onMenuMouseDown,
1900
+ onMouseMove: _this7.onMenuMouseMove,
1901
+ id: _this7.props.components.Menu ? _this7.getElementId('listbox') : undefined
1814
1902
  }, testId && {
1815
1903
  'data-testid': "".concat(testId, "-select--listbox-container")
1816
1904
  }),
@@ -1822,47 +1910,62 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1822
1910
  onBottomArrive: onMenuScrollToBottom,
1823
1911
  lockEnabled: menuShouldBlockScroll
1824
1912
  }, function (scrollTargetRef) {
1825
- var _this6$inputRef, _this6$inputRef2;
1913
+ var _this7$inputRef, _this7$inputRef2;
1826
1914
  return /*#__PURE__*/_react.default.createElement(MenuList, (0, _extends2.default)({}, commonProps, {
1827
1915
  innerRef: function innerRef(instance) {
1828
- _this6.getMenuListRef(instance);
1916
+ _this7.getMenuListRef(instance);
1829
1917
  scrollTargetRef(instance);
1830
1918
  },
1831
1919
  innerProps: _objectSpread(_objectSpread(_objectSpread({
1832
- role: _this6.props['UNSAFE_is_experimental_generic'] ? 'dialog' : 'listbox'
1833
- }, _this6.props['UNSAFE_is_experimental_generic'] && {
1834
- 'aria-labelledby': ((_this6$inputRef = _this6.inputRef) === null || _this6$inputRef === void 0 ? void 0 : _this6$inputRef.id) || _this6.getElementId('input')
1920
+ role: _this7.props['UNSAFE_is_experimental_generic'] ? 'dialog' : 'listbox'
1921
+ }, _this7.props['UNSAFE_is_experimental_generic'] && {
1922
+ 'aria-labelledby': ((_this7$inputRef = _this7.inputRef) === null || _this7$inputRef === void 0 ? void 0 : _this7$inputRef.id) || _this7.getElementId('input')
1835
1923
  }), {}, {
1836
- 'aria-multiselectable': !commonProps.isMulti || _this6.props['UNSAFE_is_experimental_generic'] ? undefined : commonProps.isMulti,
1837
- id: _this6.getElementId('listbox')
1924
+ 'aria-multiselectable': !commonProps.isMulti || _this7.props['UNSAFE_is_experimental_generic'] ? undefined : commonProps.isMulti,
1925
+ id: _this7.getElementId('listbox')
1838
1926
  }, testId && {
1839
1927
  'data-testid': "".concat(testId, "-select--listbox")
1840
- }), (0, _isSafari.isSafari)() && !_this6.props['UNSAFE_is_experimental_generic'] && {
1841
- 'aria-describedby': ((_this6$inputRef2 = _this6.inputRef) === null || _this6$inputRef2 === void 0 ? void 0 : _this6$inputRef2.id) || _this6.getElementId('input')
1928
+ }), (0, _isSafari.isSafari)() && !_this7.props['UNSAFE_is_experimental_generic'] && {
1929
+ 'aria-describedby': ((_this7$inputRef2 = _this7.inputRef) === null || _this7$inputRef2 === void 0 ? void 0 : _this7$inputRef2.id) || _this7.getElementId('input')
1842
1930
  }),
1843
1931
  isLoading: isLoading,
1844
1932
  maxHeight: maxHeight,
1845
1933
  focusedOption: focusedOption
1846
- }), _this6.props['UNSAFE_is_experimental_generic'] ? /*#__PURE__*/_react.default.createElement("div", {
1934
+ }), _this7.props['UNSAFE_is_experimental_generic'] ? /*#__PURE__*/_react.default.createElement("div", {
1847
1935
  role: "list"
1848
1936
  }, menuUI) : menuUI);
1849
1937
  }));
1850
1938
  });
1851
1939
 
1852
- // positioning behaviour is almost identical for portalled and fixed,
1853
- // so we use the same component. the actual portalling logic is forked
1854
- // within the component based on `menuPosition`
1855
- return menuPortalTarget || menuPosition === 'fixed' ? /*#__PURE__*/_react.default.createElement(MenuPortal, (0, _extends2.default)({}, commonProps, {
1940
+ // On the top-layer path the menu always portals (into the top layer)
1941
+ // regardless of consumer `menuPortalTarget` / `menuPosition`. Off the
1942
+ // flag we keep the legacy "portal only when needed" behaviour.
1943
+ var shouldPortal = (0, _platformFeatureFlags.fg)('platform-dst-top-layer') || menuPortalTarget || menuPosition === 'fixed';
1944
+ if (!shouldPortal) {
1945
+ return menuElement;
1946
+ }
1947
+ // Top-layer path needs the state mirror so MenuPortalTopLayer re-renders
1948
+ // when the anchor attaches; legacy path keeps the direct ref read.
1949
+ var controlElementForPortal = (0, _platformFeatureFlags.fg)('platform-dst-top-layer') ? this.state.controlElement : this.controlRef;
1950
+ var menuPortal = /*#__PURE__*/_react.default.createElement(MenuPortal, (0, _extends2.default)({}, commonProps, {
1856
1951
  appendTo: menuPortalTarget,
1857
- controlElement: this.controlRef,
1952
+ controlElement: controlElementForPortal,
1858
1953
  menuPlacement: menuPlacement,
1859
1954
  menuPosition: menuPosition
1860
- }), menuElement) : menuElement;
1955
+ }), menuElement);
1956
+ // The Provider plumbs the close signal to MenuPortalTopLayer; not
1957
+ // needed on the legacy path.
1958
+ if (!(0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
1959
+ return menuPortal;
1960
+ }
1961
+ return /*#__PURE__*/_react.default.createElement(_menuPortalCloseContext.MenuPortalCloseContext.Provider, {
1962
+ value: this.handleOpenLayerObserverCloseSignal
1963
+ }, menuPortal);
1861
1964
  }
1862
1965
  }, {
1863
1966
  key: "renderFormField",
1864
1967
  value: function renderFormField() {
1865
- var _this7 = this;
1968
+ var _this8 = this;
1866
1969
  var _this$props12 = this.props,
1867
1970
  delimiter = _this$props12.delimiter,
1868
1971
  isDisabled = _this$props12.isDisabled,
@@ -1882,7 +1985,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1882
1985
  if (isMulti) {
1883
1986
  if (delimiter) {
1884
1987
  var value = selectValue.map(function (opt) {
1885
- return _this7.getOptionValue(opt);
1988
+ return _this8.getOptionValue(opt);
1886
1989
  }).join(delimiter);
1887
1990
  return /*#__PURE__*/_react.default.createElement("input", {
1888
1991
  name: name,
@@ -1895,7 +1998,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
1895
1998
  key: "i-".concat(i),
1896
1999
  name: name,
1897
2000
  type: "hidden",
1898
- value: _this7.getOptionValue(opt)
2001
+ value: _this8.getOptionValue(opt)
1899
2002
  });
1900
2003
  }) : /*#__PURE__*/_react.default.createElement("input", {
1901
2004
  name: name,
@@ -2010,7 +2113,7 @@ var Select = exports.default = /*#__PURE__*/function (_Component) {
2010
2113
  innerProps: _objectSpread({}, testId && {
2011
2114
  'data-testid': "".concat(testId, "-select--indicators-container")
2012
2115
  })
2013
- }), this.renderClearIndicator(), this.renderLoadingIndicator(), this.renderDropdownIndicator())), this.renderMenu(), this.renderFormField(), /*#__PURE__*/_react.default.createElement(_notifyOpenLayerObserver.NotifyOpenLayerObserver, {
2116
+ }), this.renderClearIndicator(), this.renderLoadingIndicator(), this.renderDropdownIndicator())), this.renderMenu(), this.renderFormField(), !(0, _platformFeatureFlags.fg)('platform-dst-top-layer') && /*#__PURE__*/_react.default.createElement(_notifyOpenLayerObserver.NotifyOpenLayerObserver, {
2014
2117
  isOpen: this.props.menuIsOpen,
2015
2118
  onClose: this.handleOpenLayerObserverCloseSignal
2016
2119
  })));
@@ -1,5 +1,6 @@
1
1
  import { useContext, useLayoutEffect, useRef, useState } from 'react';
2
2
  import __noop from '@atlaskit/ds-lib/noop';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
3
4
  import { PortalPlacementContext } from '../internal/portal-placement-context';
4
5
  const noop = __noop;
5
6
  function getScrollParent(element) {
@@ -255,6 +256,12 @@ const MenuPlacer = props => {
255
256
  // The minimum height of the control
256
257
  const controlHeight = 38;
257
258
  useLayoutEffect(() => {
259
+ // When the menu is hosted in the browser top layer, positioning, flipping
260
+ // and viewport-fit are all handled by `@atlaskit/top-layer`. The placer
261
+ // becomes a pass-through that only forwards `maxMenuHeight` as a cap.
262
+ if (fg('platform-dst-top-layer')) {
263
+ return;
264
+ }
258
265
  const menuEl = ref.current;
259
266
  if (!menuEl) {
260
267
  return;
@@ -0,0 +1 @@
1
+ ._ofie1496{max-block-size:100dvh}
@@ -0,0 +1,143 @@
1
+ /* menu-portal-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import _extends from "@babel/runtime/helpers/extends";
3
+ import "./menu-portal-top-layer.compiled.css";
4
+ import * as React from 'react';
5
+ import { ax, ix } from "@compiled/react/runtime";
6
+ import { useCallback, useContext, useRef } from 'react';
7
+ import { cx } from '@compiled/react';
8
+ import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
9
+ import { Popover } from '@atlaskit/top-layer/popover';
10
+ import { useAnchorPosition } from '@atlaskit/top-layer/use-anchor-position';
11
+ import { useWidthFromAnchor } from '@atlaskit/top-layer/use-width-from-anchor';
12
+ import { getStyleProps } from '../get-style-props';
13
+ import { MenuPortalCloseContext } from '../internal/menu-portal-close-context';
14
+ // `'auto'` falls through to `'end'`; top-layer's `position-try-fallbacks` flips it if needed.
15
+ function reactSelectEdgeToTopLayerEdge(menuPlacement) {
16
+ return menuPlacement === 'top' ? 'start' : 'end';
17
+ }
18
+ const menuPortalStyles = {
19
+ root: "_ofie1496"
20
+ };
21
+
22
+ /**
23
+ * Top-layer host for react-select's menu. Hands positioning, flip, and width
24
+ * to `@atlaskit/top-layer`; ignores `appendTo` / `menuPortalTarget` /
25
+ * `menuPosition`. The browser-dismiss handler is supplied internally by
26
+ * `Select` via `MenuPortalCloseContext`.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <MenuPortalTopLayer controlElement={el} menuPlacement="bottom">
31
+ * <Menu />
32
+ * </MenuPortalTopLayer>
33
+ * ```
34
+ */
35
+ export function MenuPortalTopLayer(props) {
36
+ const {
37
+ children,
38
+ controlElement,
39
+ innerProps,
40
+ menuPlacement,
41
+ menuPosition,
42
+ xcss
43
+ } = props;
44
+ // Select's "close the menu" callback, distinct from `Popover.onClose`.
45
+ const closeSelect = useContext(MenuPortalCloseContext);
46
+ const popoverRef = useRef(null);
47
+
48
+ // Top-layer hooks need a RefObject; in-render mutation keeps it in sync
49
+ // with the prop without an effect. The hooks re-read `.current` from
50
+ // `isOpen`-keyed layout effects, so a stable ref identity is fine.
51
+ const anchorRef = useRef(null);
52
+ anchorRef.current = controlElement;
53
+
54
+ // `controlElement` is null on initial mount / SSR until `Select` mirrors
55
+ // its ref into state. While null, every hook below no-ops to avoid DOM
56
+ // reads or registering an unpositioned popover.
57
+ const isAnchored = controlElement !== null;
58
+
59
+ // `gap: 0` matches the legacy MenuPortal (no trigger-to-menu gap; the
60
+ // menu root already declares its own `marginBlockStart`). Without this
61
+ // override `useAnchorPosition`'s default 8px gap would diverge visually
62
+ // from the legacy path.
63
+ useAnchorPosition({
64
+ anchorRef,
65
+ popoverRef,
66
+ placement: {
67
+ axis: 'block',
68
+ edge: reactSelectEdgeToTopLayerEdge(menuPlacement),
69
+ offset: {
70
+ gap: 0
71
+ }
72
+ },
73
+ isOpen: isAnchored
74
+ });
75
+ useWidthFromAnchor({
76
+ anchorRef,
77
+ popoverRef,
78
+ mode: 'match-anchor',
79
+ isOpen: isAnchored
80
+ });
81
+ const handlePopoverClose = useCallback(() => {
82
+ if (closeSelect) {
83
+ closeSelect();
84
+ }
85
+ }, [closeSelect]);
86
+
87
+ // Explicit observer registration: the outer Popover is intentionally
88
+ // roleless (see Popover comment below), so Popover cannot auto-register
89
+ // from its role. This lets `closeLayers()` (Modal / Drawer) dismiss us.
90
+ useNotifyOpenLayerObserver({
91
+ type: 'popup',
92
+ isOpen: isAnchored,
93
+ onClose: handlePopoverClose
94
+ });
95
+
96
+ // Top-layer owns positioning; zero offset/rect preserves the consumer
97
+ // styles call shape.
98
+ const {
99
+ className
100
+ } = getStyleProps({
101
+ ...props,
102
+ offset: 0,
103
+ position: menuPosition,
104
+ rect: {
105
+ left: 0,
106
+ width: 0
107
+ }
108
+ }, 'menuPortal', {
109
+ 'menu-portal': true
110
+ });
111
+
112
+ // Popover stays mounted and is driven by `isOpen` so its layout-effect
113
+ // teardown (`hidePopover`, observer cleanup, position-hook style reset)
114
+ // runs against a live element. Conditional render would skip that path.
115
+ //
116
+ // `mode="manual"` opts out of native light-dismiss: react-select already
117
+ // owns outside-click and Escape via its own handlers, and the combobox
118
+ // trigger lives in a separate DOM subtree that the spec algorithm cannot
119
+ // see. Matches the pattern in `@atlaskit/datetime-picker`'s MenuTopLayer.
120
+ //
121
+ // The Popover host is intentionally roleless: the inner `MenuList`
122
+ // keeps `role="listbox"` and the id referenced by `aria-controls`.
123
+ // Putting the role on the outer host caused Playwright `toBeVisible`
124
+ // and some SR hit-testing to treat it as hidden (zero bounding rect
125
+ // while `styles.root` opts out of UA `[popover]` positioning).
126
+ //
127
+ // TODO: add open / close animation. Select unmounts MenuPortalTopLayer
128
+ // the moment `menuIsOpen` flips false, so Popover's `animate` exit
129
+ // transition never gets a frame. Needs keeping the portal mounted
130
+ // through the exit (`onExitFinish`) on the Select side.
131
+ return /*#__PURE__*/React.createElement(Popover, {
132
+ ref: popoverRef,
133
+ mode: "manual",
134
+ isOpen: isAnchored,
135
+ onClose: handlePopoverClose
136
+ }, /*#__PURE__*/React.createElement("div", _extends({
137
+ // `className` carries consumer `styles.menuPortal({...})` output,
138
+ // `xcss` carries caller compiled atomic classes, and `-MenuPortal`
139
+ // is a legacy selector hook kept for parity with MenuPortalLegacy.
140
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop, @atlaskit/ui-styling-standard/local-cx-xcss, @compiled/local-cx-xcss, @typescript-eslint/no-explicit-any
141
+ className: ax([menuPortalStyles.root, cx(className, xcss, '-MenuPortal')])
142
+ }, innerProps), children));
143
+ }
@@ -7,8 +7,10 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
7
7
  import { cx } from '@compiled/react';
8
8
  import { autoUpdate } from '@floating-ui/dom';
9
9
  import { createPortal } from 'react-dom';
10
+ import { fg } from '@atlaskit/platform-feature-flags';
10
11
  import { getStyleProps } from '../get-style-props';
11
12
  import { PortalPlacementContext } from '../internal/portal-placement-context';
13
+ import { MenuPortalTopLayer } from './menu-portal-top-layer';
12
14
  function getBoundingClientObj(element) {
13
15
  const rect = element.getBoundingClientRect();
14
16
  return {
@@ -25,7 +27,7 @@ const menuPortalStyles = {
25
27
  root: "_1pbykb7n _1e02a1vk _kqswcp1v _152t1nmo _1bsb1qxj"
26
28
  };
27
29
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
28
- export const MenuPortal = props => {
30
+ function MenuPortalLegacy(props) {
29
31
  const {
30
32
  appendTo,
31
33
  children,
@@ -78,7 +80,10 @@ export const MenuPortal = props => {
78
80
  runAutoUpdate();
79
81
  }, [runAutoUpdate]);
80
82
 
81
- // bail early if required elements aren't present
83
+ // Legacy quirk: `computedPosition` is null until the layout effect runs,
84
+ // so the first render returns null even with `defaultMenuIsOpen` set.
85
+ // Synchronous observers (VR snapshots) see a one-frame "closed" state.
86
+ // Left as-is; the top-layer path supersedes this and positions declaratively.
82
87
  if (!appendTo && menuPosition !== 'fixed' || !computedPosition) {
83
88
  return null;
84
89
  }
@@ -112,4 +117,19 @@ export const MenuPortal = props => {
112
117
  return /*#__PURE__*/React.createElement(PortalPlacementContext.Provider, {
113
118
  value: portalPlacementContext
114
119
  }, appendTo ? /*#__PURE__*/createPortal(menuWrapper, appendTo) : menuWrapper);
120
+ }
121
+
122
+ /**
123
+ * Public-facing `MenuPortal` component. Routes between the legacy
124
+ * `createPortal`-based implementation and the top-layer-based
125
+ * `MenuPortalTopLayer` based on the `platform-dst-top-layer` feature flag.
126
+ */
127
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
128
+ export const MenuPortal = props => {
129
+ if (fg('platform-dst-top-layer')) {
130
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
131
+ return /*#__PURE__*/React.createElement(MenuPortalTopLayer, props);
132
+ }
133
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
134
+ return /*#__PURE__*/React.createElement(MenuPortalLegacy, props);
115
135
  };