@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.
@@ -16,6 +16,7 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
16
16
  function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
17
17
  function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
18
18
  import React, { Component } from 'react';
19
+ import { bind } from 'bind-event-listener';
19
20
  import { isAppleDevice } from '@atlaskit/ds-lib/device-check';
20
21
  import { isSafari } from '@atlaskit/ds-lib/is-safari';
21
22
  import __noop from '@atlaskit/ds-lib/noop';
@@ -29,6 +30,7 @@ import { createFilter } from './filters';
29
30
  import { classNames } from './internal/classnames';
30
31
  import { cleanValue } from './internal/clean-value';
31
32
  import { isDocumentElement } from './internal/is-document-el';
33
+ import { MenuPortalCloseContext } from './internal/menu-portal-close-context';
32
34
  import { multiValueAsValue } from './internal/multi-value-as-value';
33
35
  import { NotifyOpenLayerObserver } from './internal/notify-open-layer-observer';
34
36
  import RequiredInput from './internal/required-input';
@@ -327,7 +329,8 @@ var Select = /*#__PURE__*/function (_Component) {
327
329
  prevWasFocused: false,
328
330
  inputIsHiddenAfterUpdate: undefined,
329
331
  prevProps: undefined,
330
- instancePrefix: ''
332
+ instancePrefix: '',
333
+ controlElement: null
331
334
  });
332
335
  // Misc. Instance Properties
333
336
  // ------------------------------
@@ -338,11 +341,26 @@ var Select = /*#__PURE__*/function (_Component) {
338
341
  _defineProperty(_this, "initialTouchY", 0);
339
342
  _defineProperty(_this, "openAfterFocus", false);
340
343
  _defineProperty(_this, "scrollToFocusedOptionOnUpdate", false);
344
+ // Cleanup for a pending document `pointerup` listener registered by
345
+ // `openMenuAfterPointerUp`. See that method for the full rationale.
346
+ _defineProperty(_this, "deferredOpenMenuCleanup", null);
341
347
  // Refs
342
348
  // ------------------------------
343
349
  _defineProperty(_this, "controlRef", null);
344
350
  _defineProperty(_this, "getControlRef", function (ref) {
345
351
  _this.controlRef = ref;
352
+ // Mirror the ref into state on the top-layer path so
353
+ // `MenuPortalTopLayer`'s layout effects react to the anchor attaching.
354
+ // Skip the null-ref (unmount) case: setState during unmount is unsafe
355
+ // and the state is dropped with the instance anyway.
356
+ if (ref === null) {
357
+ return;
358
+ }
359
+ if (_this.state.controlElement !== ref && fg('platform-dst-top-layer')) {
360
+ _this.setState({
361
+ controlElement: ref
362
+ });
363
+ }
346
364
  });
347
365
  _defineProperty(_this, "focusedOptionRef", null);
348
366
  _defineProperty(_this, "getFocusedOptionRef", function (ref) {
@@ -553,7 +571,7 @@ var Select = /*#__PURE__*/function (_Component) {
553
571
  _this.focusInput();
554
572
  } else if (!_this.props.menuIsOpen) {
555
573
  if (openMenuOnClick) {
556
- _this.openMenu('first');
574
+ _this.openMenuAfterPointerUp('first');
557
575
  }
558
576
  } else {
559
577
  if (event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
@@ -582,7 +600,7 @@ var Select = /*#__PURE__*/function (_Component) {
582
600
  });
583
601
  _this.onMenuClose();
584
602
  } else {
585
- _this.openMenu('first');
603
+ _this.openMenuAfterPointerUp('first');
586
604
  }
587
605
  event.preventDefault();
588
606
  });
@@ -700,7 +718,14 @@ var Select = /*#__PURE__*/function (_Component) {
700
718
  isFocused: true
701
719
  });
702
720
  if (_this.openAfterFocus || _this.props.openMenuOnFocus) {
703
- _this.openMenu('first');
721
+ // `openAfterFocus` always follows a pointer gesture, so defer past
722
+ // pointerup. `openMenuOnFocus` alone can come from a keyboard tab
723
+ // with no pointer gesture in flight, so open synchronously.
724
+ if (_this.openAfterFocus) {
725
+ _this.openMenuAfterPointerUp('first');
726
+ } else {
727
+ _this.openMenu('first');
728
+ }
704
729
  }
705
730
  _this.openAfterFocus = false;
706
731
  });
@@ -991,6 +1016,7 @@ var Select = /*#__PURE__*/function (_Component) {
991
1016
  value: function componentWillUnmount() {
992
1017
  this.stopListeningComposition();
993
1018
  this.stopListeningToTouch();
1019
+ this.cancelDeferredOpenMenu();
994
1020
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
995
1021
  document.removeEventListener('scroll', this.onScroll, true);
996
1022
  }
@@ -1037,10 +1063,72 @@ var Select = /*#__PURE__*/function (_Component) {
1037
1063
  }
1038
1064
  this.inputRef.blur();
1039
1065
  }
1066
+ }, {
1067
+ key: "shouldDeferOpenPastPointerUp",
1068
+ value:
1069
+ /**
1070
+ * Whether to defer the menu open past the in-flight pointer gesture.
1071
+ * Any renderer that drives a `popover="auto"` element must, otherwise
1072
+ * the browser's light-dismiss runs on the matching `pointerup` and
1073
+ * closes the menu immediately. Today only the top-layer path needs it.
1074
+ */
1075
+ function shouldDeferOpenPastPointerUp() {
1076
+ return fg('platform-dst-top-layer');
1077
+ }
1078
+
1079
+ /**
1080
+ * Open the menu after the current pointer gesture, instead of
1081
+ * synchronously inside `mousedown`.
1082
+ *
1083
+ * On the top-layer path the menu is a `popover="auto"` element. The
1084
+ * browser captures the pointerdown target before the popover exists, so
1085
+ * opening synchronously gets immediately light-dismissed on pointerup
1086
+ * (and the matching `beforetoggle: closed` is not cancellable). Deferring
1087
+ * to the next `pointerup` avoids that.
1088
+ *
1089
+ * We listen for `pointerup` rather than `click` because `pointerup` is
1090
+ * the exact event the browser uses for light-dismiss (earliest safe
1091
+ * moment), always fires (`click` requires same down/up target), is hard
1092
+ * to lose to upstream `stopPropagation`, and is uniform across input
1093
+ * types. Off the top-layer path we open synchronously as before.
1094
+ */
1095
+ }, {
1096
+ key: "openMenuAfterPointerUp",
1097
+ value: function openMenuAfterPointerUp(focusOption) {
1098
+ var _this2 = this;
1099
+ if (!this.shouldDeferOpenPastPointerUp()) {
1100
+ this.openMenu(focusOption);
1101
+ return;
1102
+ }
1103
+ // A second pointerdown can land before the queued pointerup if the
1104
+ // user releases and re-clicks very quickly. Replace any pending
1105
+ // deferred open with the latest one so we never stack listeners.
1106
+ this.cancelDeferredOpenMenu();
1107
+ var handlePointerUp = function handlePointerUp() {
1108
+ _this2.deferredOpenMenuCleanup = null;
1109
+ _this2.openMenu(focusOption);
1110
+ };
1111
+ this.deferredOpenMenuCleanup = bind(document, {
1112
+ type: 'pointerup',
1113
+ listener: handlePointerUp,
1114
+ options: {
1115
+ capture: true,
1116
+ once: true
1117
+ }
1118
+ });
1119
+ }
1120
+ }, {
1121
+ key: "cancelDeferredOpenMenu",
1122
+ value: function cancelDeferredOpenMenu() {
1123
+ if (this.deferredOpenMenuCleanup) {
1124
+ this.deferredOpenMenuCleanup();
1125
+ this.deferredOpenMenuCleanup = null;
1126
+ }
1127
+ }
1040
1128
  }, {
1041
1129
  key: "openMenu",
1042
1130
  value: function openMenu(focusOption) {
1043
- var _this2 = this;
1131
+ var _this3 = this;
1044
1132
  var _this$state2 = this.state,
1045
1133
  selectValue = _this$state2.selectValue,
1046
1134
  isFocused = _this$state2.isFocused;
@@ -1062,25 +1150,25 @@ var Select = /*#__PURE__*/function (_Component) {
1062
1150
  focusedOption: focusedOption,
1063
1151
  focusedOptionId: this.getFocusedOptionId(focusedOption)
1064
1152
  }, function () {
1065
- return _this2.onMenuOpen();
1153
+ return _this3.onMenuOpen();
1066
1154
  });
1067
1155
  isSafari() && focusedOption && this.updateInputLabel(this.calculateInputLabel(focusedOption, openAtIndex));
1068
1156
  }
1069
1157
  }, {
1070
1158
  key: "updateInputLabel",
1071
1159
  value: function updateInputLabel(inputLabel) {
1072
- var _this3 = this;
1160
+ var _this4 = this;
1073
1161
  if (inputLabel) {
1074
1162
  var _this$inputRef;
1075
1163
  (_this$inputRef = this.inputRef) === null || _this$inputRef === void 0 || _this$inputRef.setAttribute('aria-label', inputLabel);
1076
1164
  setTimeout(function () {
1077
- var normalizedLabel = _this3.props['aria-label'] || _this3.props.label;
1165
+ var normalizedLabel = _this4.props['aria-label'] || _this4.props.label;
1078
1166
  if (normalizedLabel) {
1079
- var _this3$inputRef;
1080
- (_this3$inputRef = _this3.inputRef) === null || _this3$inputRef === void 0 || _this3$inputRef.setAttribute('aria-label', normalizedLabel);
1167
+ var _this4$inputRef;
1168
+ (_this4$inputRef = _this4.inputRef) === null || _this4$inputRef === void 0 || _this4$inputRef.setAttribute('aria-label', normalizedLabel);
1081
1169
  } else {
1082
- var _this3$inputRef2;
1083
- (_this3$inputRef2 = _this3.inputRef) === null || _this3$inputRef2 === void 0 || _this3$inputRef2.removeAttribute('aria-label');
1170
+ var _this4$inputRef2;
1171
+ (_this4$inputRef2 = _this4.inputRef) === null || _this4$inputRef2 === void 0 || _this4$inputRef2.removeAttribute('aria-label');
1084
1172
  }
1085
1173
  }, 500);
1086
1174
  }
@@ -1088,14 +1176,14 @@ var Select = /*#__PURE__*/function (_Component) {
1088
1176
  }, {
1089
1177
  key: "calculateInputLabel",
1090
1178
  value: function calculateInputLabel(focusedOption, optionIndex) {
1091
- var _this4 = this;
1179
+ var _this5 = this;
1092
1180
  var options = this.props.options;
1093
1181
  var isOptionsGrouped = options === null || options === void 0 ? void 0 : options.every(function (obj) {
1094
1182
  return _typeof(obj) === 'object' && obj !== null && 'options' in obj;
1095
1183
  });
1096
1184
  var inputLabel = this.getOptionLabel(focusedOption);
1097
1185
  var isOptionFocused = function isOptionFocused(option) {
1098
- return _this4.getOptionLabel(option) === inputLabel;
1186
+ return _this5.getOptionLabel(option) === inputLabel;
1099
1187
  };
1100
1188
  var groupData = options === null || options === void 0 ? void 0 : options.find(function (option) {
1101
1189
  var _groupCandidate$optio, _groupCandidate$optio2;
@@ -1483,7 +1571,7 @@ var Select = /*#__PURE__*/function (_Component) {
1483
1571
  }, {
1484
1572
  key: "renderPlaceholderOrValue",
1485
1573
  value: function renderPlaceholderOrValue() {
1486
- var _this5 = this;
1574
+ var _this6 = this;
1487
1575
  var _this$getComponents2 = this.getComponents(),
1488
1576
  MultiValue = _this$getComponents2.MultiValue,
1489
1577
  MultiValueContainer = _this$getComponents2.MultiValueContainer,
@@ -1518,7 +1606,7 @@ var Select = /*#__PURE__*/function (_Component) {
1518
1606
  if (isMulti) {
1519
1607
  return selectValue.map(function (opt, index) {
1520
1608
  var isOptionFocused = opt === focusedValue;
1521
- var key = "".concat(_this5.getOptionLabel(opt), "-").concat(_this5.getOptionValue(opt));
1609
+ var key = "".concat(_this6.getOptionLabel(opt), "-").concat(_this6.getOptionValue(opt));
1522
1610
  return /*#__PURE__*/React.createElement(MultiValue, _extends({}, commonProps, {
1523
1611
  components: {
1524
1612
  Container: MultiValueContainer,
@@ -1531,10 +1619,10 @@ var Select = /*#__PURE__*/function (_Component) {
1531
1619
  index: index,
1532
1620
  removeProps: _objectSpread(_objectSpread({
1533
1621
  onClick: function onClick() {
1534
- return _this5.removeValue(opt);
1622
+ return _this6.removeValue(opt);
1535
1623
  },
1536
1624
  onTouchEnd: function onTouchEnd() {
1537
- return _this5.removeValue(opt);
1625
+ return _this6.removeValue(opt);
1538
1626
  },
1539
1627
  onMouseDown: function onMouseDown(e) {
1540
1628
  e.preventDefault();
@@ -1542,15 +1630,15 @@ var Select = /*#__PURE__*/function (_Component) {
1542
1630
  }, testId && {
1543
1631
  'data-testid': "".concat(testId, "-select--multivalue-").concat(index, "-remove")
1544
1632
  }), {}, {
1545
- id: "".concat(_this5.getElementId('selected-value'), "-").concat(index, "-remove")
1633
+ id: "".concat(_this6.getElementId('selected-value'), "-").concat(index, "-remove")
1546
1634
  }),
1547
1635
  data: opt,
1548
1636
  innerProps: _objectSpread(_objectSpread({}, testId && {
1549
1637
  'data-testid': "".concat(testId, "-select--multivalue-").concat(index)
1550
1638
  }), {}, {
1551
- id: "".concat(_this5.getElementId('selected-value'), "-").concat(index)
1639
+ id: "".concat(_this6.getElementId('selected-value'), "-").concat(index)
1552
1640
  })
1553
- }), _this5.formatOptionLabel(opt, 'value'));
1641
+ }), _this6.formatOptionLabel(opt, 'value'));
1554
1642
  });
1555
1643
  }
1556
1644
  if (inputValue) {
@@ -1656,7 +1744,7 @@ var Select = /*#__PURE__*/function (_Component) {
1656
1744
  }, {
1657
1745
  key: "renderMenu",
1658
1746
  value: function renderMenu() {
1659
- var _this6 = this;
1747
+ var _this7 = this;
1660
1748
  var _this$getComponents6 = this.getComponents(),
1661
1749
  Group = _this$getComponents6.Group,
1662
1750
  GroupHeading = _this$getComponents6.GroupHeading,
@@ -1699,19 +1787,19 @@ var Select = /*#__PURE__*/function (_Component) {
1699
1787
  value = props.value;
1700
1788
  var isFocused = focusedOption === data;
1701
1789
  var onHover = isDisabled ? undefined : function () {
1702
- return _this6.onOptionHover(data);
1790
+ return _this7.onOptionHover(data);
1703
1791
  };
1704
1792
  var onSelect = isDisabled ? undefined : function () {
1705
- return _this6.selectOption(data);
1793
+ return _this7.selectOption(data);
1706
1794
  };
1707
- var optionId = "".concat(_this6.getElementId('option'), "-").concat(id);
1795
+ var optionId = "".concat(_this7.getElementId('option'), "-").concat(id);
1708
1796
  var innerProps = _objectSpread({
1709
1797
  id: optionId,
1710
1798
  onClick: onSelect,
1711
1799
  onMouseMove: onHover,
1712
1800
  onMouseOver: onHover,
1713
- role: _this6.props['UNSAFE_is_experimental_generic'] ? 'listitem' : 'option',
1714
- 'aria-selected': _this6.props['UNSAFE_is_experimental_generic'] ? undefined : isSelected,
1801
+ role: _this7.props['UNSAFE_is_experimental_generic'] ? 'listitem' : 'option',
1802
+ 'aria-selected': _this7.props['UNSAFE_is_experimental_generic'] ? undefined : isSelected,
1715
1803
  // We don't want aria-disabled if it's false. It's just noisy.
1716
1804
  'aria-disabled': !isDisabled ? undefined : isDisabled,
1717
1805
  'aria-describedby': headingId
@@ -1728,8 +1816,8 @@ var Select = /*#__PURE__*/function (_Component) {
1728
1816
  type: type,
1729
1817
  value: value,
1730
1818
  isFocused: isFocused,
1731
- innerRef: isFocused ? _this6.getFocusedOptionRef : undefined
1732
- }), _this6.formatOptionLabel(props.data, 'menu'));
1819
+ innerRef: isFocused ? _this7.getFocusedOptionRef : undefined
1820
+ }), _this7.formatOptionLabel(props.data, 'menu'));
1733
1821
  };
1734
1822
  var menuUI;
1735
1823
  if (this.hasOptions()) {
@@ -1739,7 +1827,7 @@ var Select = /*#__PURE__*/function (_Component) {
1739
1827
  var data = item.data,
1740
1828
  options = item.options,
1741
1829
  groupIndex = item.index;
1742
- var groupId = "".concat(_this6.getElementId('group'), "-").concat(groupIndex);
1830
+ var groupId = "".concat(_this7.getElementId('group'), "-").concat(groupIndex);
1743
1831
  var headingId = "".concat(groupId, "-heading");
1744
1832
  return /*#__PURE__*/React.createElement(Group, _extends({}, commonProps, {
1745
1833
  key: groupId,
@@ -1752,7 +1840,7 @@ var Select = /*#__PURE__*/function (_Component) {
1752
1840
  }, testId && {
1753
1841
  'data-testid': "".concat(testId, "-select--group-").concat(groupIndex, "-heading")
1754
1842
  }),
1755
- label: _this6.formatGroupLabel(item.data)
1843
+ label: _this7.formatGroupLabel(item.data)
1756
1844
  }), item.options.map(function (option) {
1757
1845
  return render(option, "".concat(groupIndex, "-").concat(option.index), headingId);
1758
1846
  }));
@@ -1799,9 +1887,9 @@ var Select = /*#__PURE__*/function (_Component) {
1799
1887
  return /*#__PURE__*/React.createElement(Menu, _extends({}, commonProps, menuPlacementProps, {
1800
1888
  innerRef: ref,
1801
1889
  innerProps: _objectSpread({
1802
- onMouseDown: _this6.onMenuMouseDown,
1803
- onMouseMove: _this6.onMenuMouseMove,
1804
- id: _this6.props.components.Menu ? _this6.getElementId('listbox') : undefined
1890
+ onMouseDown: _this7.onMenuMouseDown,
1891
+ onMouseMove: _this7.onMenuMouseMove,
1892
+ id: _this7.props.components.Menu ? _this7.getElementId('listbox') : undefined
1805
1893
  }, testId && {
1806
1894
  'data-testid': "".concat(testId, "-select--listbox-container")
1807
1895
  }),
@@ -1813,47 +1901,62 @@ var Select = /*#__PURE__*/function (_Component) {
1813
1901
  onBottomArrive: onMenuScrollToBottom,
1814
1902
  lockEnabled: menuShouldBlockScroll
1815
1903
  }, function (scrollTargetRef) {
1816
- var _this6$inputRef, _this6$inputRef2;
1904
+ var _this7$inputRef, _this7$inputRef2;
1817
1905
  return /*#__PURE__*/React.createElement(MenuList, _extends({}, commonProps, {
1818
1906
  innerRef: function innerRef(instance) {
1819
- _this6.getMenuListRef(instance);
1907
+ _this7.getMenuListRef(instance);
1820
1908
  scrollTargetRef(instance);
1821
1909
  },
1822
1910
  innerProps: _objectSpread(_objectSpread(_objectSpread({
1823
- role: _this6.props['UNSAFE_is_experimental_generic'] ? 'dialog' : 'listbox'
1824
- }, _this6.props['UNSAFE_is_experimental_generic'] && {
1825
- 'aria-labelledby': ((_this6$inputRef = _this6.inputRef) === null || _this6$inputRef === void 0 ? void 0 : _this6$inputRef.id) || _this6.getElementId('input')
1911
+ role: _this7.props['UNSAFE_is_experimental_generic'] ? 'dialog' : 'listbox'
1912
+ }, _this7.props['UNSAFE_is_experimental_generic'] && {
1913
+ 'aria-labelledby': ((_this7$inputRef = _this7.inputRef) === null || _this7$inputRef === void 0 ? void 0 : _this7$inputRef.id) || _this7.getElementId('input')
1826
1914
  }), {}, {
1827
- 'aria-multiselectable': !commonProps.isMulti || _this6.props['UNSAFE_is_experimental_generic'] ? undefined : commonProps.isMulti,
1828
- id: _this6.getElementId('listbox')
1915
+ 'aria-multiselectable': !commonProps.isMulti || _this7.props['UNSAFE_is_experimental_generic'] ? undefined : commonProps.isMulti,
1916
+ id: _this7.getElementId('listbox')
1829
1917
  }, testId && {
1830
1918
  'data-testid': "".concat(testId, "-select--listbox")
1831
- }), isSafari() && !_this6.props['UNSAFE_is_experimental_generic'] && {
1832
- 'aria-describedby': ((_this6$inputRef2 = _this6.inputRef) === null || _this6$inputRef2 === void 0 ? void 0 : _this6$inputRef2.id) || _this6.getElementId('input')
1919
+ }), isSafari() && !_this7.props['UNSAFE_is_experimental_generic'] && {
1920
+ 'aria-describedby': ((_this7$inputRef2 = _this7.inputRef) === null || _this7$inputRef2 === void 0 ? void 0 : _this7$inputRef2.id) || _this7.getElementId('input')
1833
1921
  }),
1834
1922
  isLoading: isLoading,
1835
1923
  maxHeight: maxHeight,
1836
1924
  focusedOption: focusedOption
1837
- }), _this6.props['UNSAFE_is_experimental_generic'] ? /*#__PURE__*/React.createElement("div", {
1925
+ }), _this7.props['UNSAFE_is_experimental_generic'] ? /*#__PURE__*/React.createElement("div", {
1838
1926
  role: "list"
1839
1927
  }, menuUI) : menuUI);
1840
1928
  }));
1841
1929
  });
1842
1930
 
1843
- // positioning behaviour is almost identical for portalled and fixed,
1844
- // so we use the same component. the actual portalling logic is forked
1845
- // within the component based on `menuPosition`
1846
- return menuPortalTarget || menuPosition === 'fixed' ? /*#__PURE__*/React.createElement(MenuPortal, _extends({}, commonProps, {
1931
+ // On the top-layer path the menu always portals (into the top layer)
1932
+ // regardless of consumer `menuPortalTarget` / `menuPosition`. Off the
1933
+ // flag we keep the legacy "portal only when needed" behaviour.
1934
+ var shouldPortal = fg('platform-dst-top-layer') || menuPortalTarget || menuPosition === 'fixed';
1935
+ if (!shouldPortal) {
1936
+ return menuElement;
1937
+ }
1938
+ // Top-layer path needs the state mirror so MenuPortalTopLayer re-renders
1939
+ // when the anchor attaches; legacy path keeps the direct ref read.
1940
+ var controlElementForPortal = fg('platform-dst-top-layer') ? this.state.controlElement : this.controlRef;
1941
+ var menuPortal = /*#__PURE__*/React.createElement(MenuPortal, _extends({}, commonProps, {
1847
1942
  appendTo: menuPortalTarget,
1848
- controlElement: this.controlRef,
1943
+ controlElement: controlElementForPortal,
1849
1944
  menuPlacement: menuPlacement,
1850
1945
  menuPosition: menuPosition
1851
- }), menuElement) : menuElement;
1946
+ }), menuElement);
1947
+ // The Provider plumbs the close signal to MenuPortalTopLayer; not
1948
+ // needed on the legacy path.
1949
+ if (!fg('platform-dst-top-layer')) {
1950
+ return menuPortal;
1951
+ }
1952
+ return /*#__PURE__*/React.createElement(MenuPortalCloseContext.Provider, {
1953
+ value: this.handleOpenLayerObserverCloseSignal
1954
+ }, menuPortal);
1852
1955
  }
1853
1956
  }, {
1854
1957
  key: "renderFormField",
1855
1958
  value: function renderFormField() {
1856
- var _this7 = this;
1959
+ var _this8 = this;
1857
1960
  var _this$props12 = this.props,
1858
1961
  delimiter = _this$props12.delimiter,
1859
1962
  isDisabled = _this$props12.isDisabled,
@@ -1873,7 +1976,7 @@ var Select = /*#__PURE__*/function (_Component) {
1873
1976
  if (isMulti) {
1874
1977
  if (delimiter) {
1875
1978
  var value = selectValue.map(function (opt) {
1876
- return _this7.getOptionValue(opt);
1979
+ return _this8.getOptionValue(opt);
1877
1980
  }).join(delimiter);
1878
1981
  return /*#__PURE__*/React.createElement("input", {
1879
1982
  name: name,
@@ -1886,7 +1989,7 @@ var Select = /*#__PURE__*/function (_Component) {
1886
1989
  key: "i-".concat(i),
1887
1990
  name: name,
1888
1991
  type: "hidden",
1889
- value: _this7.getOptionValue(opt)
1992
+ value: _this8.getOptionValue(opt)
1890
1993
  });
1891
1994
  }) : /*#__PURE__*/React.createElement("input", {
1892
1995
  name: name,
@@ -2001,7 +2104,7 @@ var Select = /*#__PURE__*/function (_Component) {
2001
2104
  innerProps: _objectSpread({}, testId && {
2002
2105
  'data-testid': "".concat(testId, "-select--indicators-container")
2003
2106
  })
2004
- }), this.renderClearIndicator(), this.renderLoadingIndicator(), this.renderDropdownIndicator())), this.renderMenu(), this.renderFormField(), /*#__PURE__*/React.createElement(NotifyOpenLayerObserver, {
2107
+ }), this.renderClearIndicator(), this.renderLoadingIndicator(), this.renderDropdownIndicator())), this.renderMenu(), this.renderFormField(), !fg('platform-dst-top-layer') && /*#__PURE__*/React.createElement(NotifyOpenLayerObserver, {
2005
2108
  isOpen: this.props.menuIsOpen,
2006
2109
  onClose: this.handleOpenLayerObserverCloseSignal
2007
2110
  })));
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ import type { GroupBase } from '../types';
7
+ import type { MenuPortalProps } from './menu-portal';
8
+ /**
9
+ * Top-layer host for react-select's menu. Hands positioning, flip, and width
10
+ * to `@atlaskit/top-layer`; ignores `appendTo` / `menuPortalTarget` /
11
+ * `menuPosition`. The browser-dismiss handler is supplied internally by
12
+ * `Select` via `MenuPortalCloseContext`.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <MenuPortalTopLayer controlElement={el} menuPlacement="bottom">
17
+ * <Menu />
18
+ * </MenuPortalTopLayer>
19
+ * ```
20
+ */
21
+ export declare function MenuPortalTopLayer<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(props: MenuPortalProps<Option, IsMulti, Group>): ReactNode;
@@ -20,4 +20,9 @@ export interface PortalStyleArgs {
20
20
  width: number;
21
21
  };
22
22
  }
23
- export declare const MenuPortal: <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(props: MenuPortalProps<Option, IsMulti, Group>) => JSX.Element | null;
23
+ /**
24
+ * Public-facing `MenuPortal` component. Routes between the legacy
25
+ * `createPortal`-based implementation and the top-layer-based
26
+ * `MenuPortalTopLayer` based on the `platform-dst-top-layer` feature flag.
27
+ */
28
+ export declare const MenuPortal: <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(props: MenuPortalProps<Option, IsMulti, Group>) => ReactNode;
@@ -0,0 +1,7 @@
1
+ import { type Context } from 'react';
2
+ /**
3
+ * Internal handoff from `Select` to `MenuPortalTopLayer` for the top-layer
4
+ * dismiss signal. Kept off `MenuPortalProps` to avoid widening the public
5
+ * subpath export at `@atlaskit/react-select/menu-portal`.
6
+ */
7
+ export declare const MenuPortalCloseContext: Context<(() => void) | undefined>;
@@ -454,6 +454,14 @@ interface State<Option, IsMulti extends boolean, Group extends GroupBase<Option>
454
454
  inputIsHiddenAfterUpdate: boolean | null | undefined;
455
455
  prevProps: SelectProps<Option, IsMulti, Group> | void;
456
456
  instancePrefix: string;
457
+ /**
458
+ * State mirror of `controlRef`, used only on the top-layer path so
459
+ * `MenuPortalTopLayer` re-renders when the anchor attaches (its layout
460
+ * effects depend on this prop). Legacy `MenuPortalLegacy` measures during
461
+ * render and keeps reading `controlRef` directly. Stays `null` when the
462
+ * `platform-dst-top-layer` flag is off.
463
+ */
464
+ controlElement: HTMLDivElement | null;
457
465
  }
458
466
  interface CategorizedOption<Option> {
459
467
  type: 'option';
@@ -485,6 +493,7 @@ export default class Select<Option = unknown, IsMulti extends boolean = false, G
485
493
  initialTouchY: number;
486
494
  openAfterFocus: boolean;
487
495
  scrollToFocusedOptionOnUpdate: boolean;
496
+ deferredOpenMenuCleanup: (() => void) | null;
488
497
  userIsDragging?: boolean;
489
498
  controlRef: HTMLDivElement | null;
490
499
  getControlRef: RefCallback<HTMLDivElement>;
@@ -518,6 +527,31 @@ export default class Select<Option = unknown, IsMulti extends boolean = false, G
518
527
  blurInput(): void;
519
528
  focus: () => void;
520
529
  blur: () => void;
530
+ /**
531
+ * Whether to defer the menu open past the in-flight pointer gesture.
532
+ * Any renderer that drives a `popover="auto"` element must, otherwise
533
+ * the browser's light-dismiss runs on the matching `pointerup` and
534
+ * closes the menu immediately. Today only the top-layer path needs it.
535
+ */
536
+ private shouldDeferOpenPastPointerUp;
537
+ /**
538
+ * Open the menu after the current pointer gesture, instead of
539
+ * synchronously inside `mousedown`.
540
+ *
541
+ * On the top-layer path the menu is a `popover="auto"` element. The
542
+ * browser captures the pointerdown target before the popover exists, so
543
+ * opening synchronously gets immediately light-dismissed on pointerup
544
+ * (and the matching `beforetoggle: closed` is not cancellable). Deferring
545
+ * to the next `pointerup` avoids that.
546
+ *
547
+ * We listen for `pointerup` rather than `click` because `pointerup` is
548
+ * the exact event the browser uses for light-dismiss (earliest safe
549
+ * moment), always fires (`click` requires same down/up target), is hard
550
+ * to lose to upstream `stopPropagation`, and is uniform across input
551
+ * types. Off the top-layer path we open synchronously as before.
552
+ */
553
+ private openMenuAfterPointerUp;
554
+ cancelDeferredOpenMenu(): void;
521
555
  openMenu(focusOption: 'first' | 'last'): void;
522
556
  updateInputLabel(inputLabel?: string): void;
523
557
  calculateInputLabel(focusedOption: Option, optionIndex: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/react-select",
3
- "version": "4.0.2",
3
+ "version": "4.1.1",
4
4
  "description": "A forked version of react-select to only be used in atlaskit/select",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -20,6 +20,7 @@
20
20
  ],
21
21
  "atlaskit:src": "src/index.tsx",
22
22
  "dependencies": {
23
+ "@atlaskit/browser-apis": "^1.0.0",
23
24
  "@atlaskit/ds-lib": "^8.0.0",
24
25
  "@atlaskit/icon": "^36.0.0",
25
26
  "@atlaskit/layering": "^4.0.0",
@@ -27,10 +28,12 @@
27
28
  "@atlaskit/primitives": "^20.0.0",
28
29
  "@atlaskit/spinner": "^20.0.0",
29
30
  "@atlaskit/tag": "^15.0.0",
30
- "@atlaskit/tokens": "^14.0.0",
31
+ "@atlaskit/tokens": "^15.0.0",
32
+ "@atlaskit/top-layer": "^1.0.0",
31
33
  "@babel/runtime": "^7.0.0",
32
34
  "@compiled/react": "^0.20.0",
33
35
  "@floating-ui/dom": "^1.0.1",
36
+ "bind-event-listener": "^3.0.0",
34
37
  "memoize-one": "^6.0.0",
35
38
  "use-isomorphic-layout-effect": "^1.1.2"
36
39
  },
@@ -40,14 +43,19 @@
40
43
  },
41
44
  "devDependencies": {
42
45
  "@af/accessibility-testing": "workspace:^",
46
+ "@af/integration-testing": "workspace:^",
47
+ "@af/visual-regression": "workspace:^",
48
+ "@atlaskit/button": "^24.1.0",
43
49
  "@atlaskit/checkbox": "^18.0.0",
44
50
  "@atlaskit/form": "^16.0.0",
51
+ "@atlaskit/modal-dialog": "^16.0.0",
45
52
  "@atlassian/feature-flags-test-utils": "^1.1.0",
46
53
  "@testing-library/react": "^16.3.0",
47
54
  "@testing-library/user-event": "^14.4.3",
48
55
  "jest-in-case": "^1.0.2",
49
56
  "react": "^18.2.0",
50
- "react-dom": "^18.2.0"
57
+ "react-dom": "^18.2.0",
58
+ "tiny-invariant": "^1.2.0"
51
59
  },
52
60
  "platform-feature-flags": {
53
61
  "custom-interactive-elements-not-keyboard-focusable": {
@@ -70,6 +78,9 @@
70
78
  },
71
79
  "platform-dst-lozenge-tag-badge-visual-uplifts": {
72
80
  "type": "boolean"
81
+ },
82
+ "platform-dst-top-layer": {
83
+ "type": "boolean"
73
84
  }
74
85
  },
75
86
  "techstack": {