@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.
@@ -0,0 +1,8 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * Internal handoff from `Select` to `MenuPortalTopLayer` for the top-layer
5
+ * dismiss signal. Kept off `MenuPortalProps` to avoid widening the public
6
+ * subpath export at `@atlaskit/react-select/menu-portal`.
7
+ */
8
+ export const MenuPortalCloseContext = /*#__PURE__*/createContext(undefined);
@@ -4,6 +4,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
4
  import "./select.compiled.css";
5
5
  import { ax, ix } from "@compiled/react/runtime";
6
6
  import React, { Component } from 'react';
7
+ import { bind } from 'bind-event-listener';
7
8
  import { isAppleDevice } from '@atlaskit/ds-lib/device-check';
8
9
  import { isSafari } from '@atlaskit/ds-lib/is-safari';
9
10
  import __noop from '@atlaskit/ds-lib/noop';
@@ -17,6 +18,7 @@ import { createFilter } from './filters';
17
18
  import { classNames } from './internal/classnames';
18
19
  import { cleanValue } from './internal/clean-value';
19
20
  import { isDocumentElement } from './internal/is-document-el';
21
+ import { MenuPortalCloseContext } from './internal/menu-portal-close-context';
20
22
  import { multiValueAsValue } from './internal/multi-value-as-value';
21
23
  import { NotifyOpenLayerObserver } from './internal/notify-open-layer-observer';
22
24
  import RequiredInput from './internal/required-input';
@@ -303,7 +305,8 @@ export default class Select extends Component {
303
305
  prevWasFocused: false,
304
306
  inputIsHiddenAfterUpdate: undefined,
305
307
  prevProps: undefined,
306
- instancePrefix: ''
308
+ instancePrefix: '',
309
+ controlElement: null
307
310
  });
308
311
  // Misc. Instance Properties
309
312
  // ------------------------------
@@ -314,11 +317,26 @@ export default class Select extends Component {
314
317
  _defineProperty(this, "initialTouchY", 0);
315
318
  _defineProperty(this, "openAfterFocus", false);
316
319
  _defineProperty(this, "scrollToFocusedOptionOnUpdate", false);
320
+ // Cleanup for a pending document `pointerup` listener registered by
321
+ // `openMenuAfterPointerUp`. See that method for the full rationale.
322
+ _defineProperty(this, "deferredOpenMenuCleanup", null);
317
323
  // Refs
318
324
  // ------------------------------
319
325
  _defineProperty(this, "controlRef", null);
320
326
  _defineProperty(this, "getControlRef", ref => {
321
327
  this.controlRef = ref;
328
+ // Mirror the ref into state on the top-layer path so
329
+ // `MenuPortalTopLayer`'s layout effects react to the anchor attaching.
330
+ // Skip the null-ref (unmount) case: setState during unmount is unsafe
331
+ // and the state is dropped with the instance anyway.
332
+ if (ref === null) {
333
+ return;
334
+ }
335
+ if (this.state.controlElement !== ref && fg('platform-dst-top-layer')) {
336
+ this.setState({
337
+ controlElement: ref
338
+ });
339
+ }
322
340
  });
323
341
  _defineProperty(this, "focusedOptionRef", null);
324
342
  _defineProperty(this, "getFocusedOptionRef", ref => {
@@ -528,7 +546,7 @@ export default class Select extends Component {
528
546
  this.focusInput();
529
547
  } else if (!this.props.menuIsOpen) {
530
548
  if (openMenuOnClick) {
531
- this.openMenu('first');
549
+ this.openMenuAfterPointerUp('first');
532
550
  }
533
551
  } else {
534
552
  if (event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
@@ -558,7 +576,7 @@ export default class Select extends Component {
558
576
  });
559
577
  this.onMenuClose();
560
578
  } else {
561
- this.openMenu('first');
579
+ this.openMenuAfterPointerUp('first');
562
580
  }
563
581
  event.preventDefault();
564
582
  });
@@ -678,7 +696,14 @@ export default class Select extends Component {
678
696
  isFocused: true
679
697
  });
680
698
  if (this.openAfterFocus || this.props.openMenuOnFocus) {
681
- this.openMenu('first');
699
+ // `openAfterFocus` always follows a pointer gesture, so defer past
700
+ // pointerup. `openMenuOnFocus` alone can come from a keyboard tab
701
+ // with no pointer gesture in flight, so open synchronously.
702
+ if (this.openAfterFocus) {
703
+ this.openMenuAfterPointerUp('first');
704
+ } else {
705
+ this.openMenu('first');
706
+ }
682
707
  }
683
708
  this.openAfterFocus = false;
684
709
  });
@@ -1033,6 +1058,7 @@ export default class Select extends Component {
1033
1058
  componentWillUnmount() {
1034
1059
  this.stopListeningComposition();
1035
1060
  this.stopListeningToTouch();
1061
+ this.cancelDeferredOpenMenu();
1036
1062
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
1037
1063
  document.removeEventListener('scroll', this.onScroll, true);
1038
1064
  }
@@ -1071,6 +1097,60 @@ export default class Select extends Component {
1071
1097
  }
1072
1098
  this.inputRef.blur();
1073
1099
  }
1100
+ /**
1101
+ * Whether to defer the menu open past the in-flight pointer gesture.
1102
+ * Any renderer that drives a `popover="auto"` element must, otherwise
1103
+ * the browser's light-dismiss runs on the matching `pointerup` and
1104
+ * closes the menu immediately. Today only the top-layer path needs it.
1105
+ */
1106
+ shouldDeferOpenPastPointerUp() {
1107
+ return fg('platform-dst-top-layer');
1108
+ }
1109
+
1110
+ /**
1111
+ * Open the menu after the current pointer gesture, instead of
1112
+ * synchronously inside `mousedown`.
1113
+ *
1114
+ * On the top-layer path the menu is a `popover="auto"` element. The
1115
+ * browser captures the pointerdown target before the popover exists, so
1116
+ * opening synchronously gets immediately light-dismissed on pointerup
1117
+ * (and the matching `beforetoggle: closed` is not cancellable). Deferring
1118
+ * to the next `pointerup` avoids that.
1119
+ *
1120
+ * We listen for `pointerup` rather than `click` because `pointerup` is
1121
+ * the exact event the browser uses for light-dismiss (earliest safe
1122
+ * moment), always fires (`click` requires same down/up target), is hard
1123
+ * to lose to upstream `stopPropagation`, and is uniform across input
1124
+ * types. Off the top-layer path we open synchronously as before.
1125
+ */
1126
+ openMenuAfterPointerUp(focusOption) {
1127
+ if (!this.shouldDeferOpenPastPointerUp()) {
1128
+ this.openMenu(focusOption);
1129
+ return;
1130
+ }
1131
+ // A second pointerdown can land before the queued pointerup if the
1132
+ // user releases and re-clicks very quickly. Replace any pending
1133
+ // deferred open with the latest one so we never stack listeners.
1134
+ this.cancelDeferredOpenMenu();
1135
+ const handlePointerUp = () => {
1136
+ this.deferredOpenMenuCleanup = null;
1137
+ this.openMenu(focusOption);
1138
+ };
1139
+ this.deferredOpenMenuCleanup = bind(document, {
1140
+ type: 'pointerup',
1141
+ listener: handlePointerUp,
1142
+ options: {
1143
+ capture: true,
1144
+ once: true
1145
+ }
1146
+ });
1147
+ }
1148
+ cancelDeferredOpenMenu() {
1149
+ if (this.deferredOpenMenuCleanup) {
1150
+ this.deferredOpenMenuCleanup();
1151
+ this.deferredOpenMenuCleanup = null;
1152
+ }
1153
+ }
1074
1154
  openMenu(focusOption) {
1075
1155
  const {
1076
1156
  selectValue,
@@ -1877,15 +1957,30 @@ export default class Select extends Component {
1877
1957
  }, menuUI) : menuUI);
1878
1958
  })));
1879
1959
 
1880
- // positioning behaviour is almost identical for portalled and fixed,
1881
- // so we use the same component. the actual portalling logic is forked
1882
- // within the component based on `menuPosition`
1883
- return menuPortalTarget || menuPosition === 'fixed' ? /*#__PURE__*/React.createElement(MenuPortal, _extends({}, commonProps, {
1960
+ // On the top-layer path the menu always portals (into the top layer)
1961
+ // regardless of consumer `menuPortalTarget` / `menuPosition`. Off the
1962
+ // flag we keep the legacy "portal only when needed" behaviour.
1963
+ const shouldPortal = fg('platform-dst-top-layer') || menuPortalTarget || menuPosition === 'fixed';
1964
+ if (!shouldPortal) {
1965
+ return menuElement;
1966
+ }
1967
+ // Top-layer path needs the state mirror so MenuPortalTopLayer re-renders
1968
+ // when the anchor attaches; legacy path keeps the direct ref read.
1969
+ const controlElementForPortal = fg('platform-dst-top-layer') ? this.state.controlElement : this.controlRef;
1970
+ const menuPortal = /*#__PURE__*/React.createElement(MenuPortal, _extends({}, commonProps, {
1884
1971
  appendTo: menuPortalTarget,
1885
- controlElement: this.controlRef,
1972
+ controlElement: controlElementForPortal,
1886
1973
  menuPlacement: menuPlacement,
1887
1974
  menuPosition: menuPosition
1888
- }), menuElement) : menuElement;
1975
+ }), menuElement);
1976
+ // The Provider plumbs the close signal to MenuPortalTopLayer; not
1977
+ // needed on the legacy path.
1978
+ if (!fg('platform-dst-top-layer')) {
1979
+ return menuPortal;
1980
+ }
1981
+ return /*#__PURE__*/React.createElement(MenuPortalCloseContext.Provider, {
1982
+ value: this.handleOpenLayerObserverCloseSignal
1983
+ }, menuPortal);
1889
1984
  }
1890
1985
  renderFormField() {
1891
1986
  const {
@@ -2040,7 +2135,7 @@ export default class Select extends Component {
2040
2135
  'data-testid': `${testId}-select--indicators-container`
2041
2136
  })
2042
2137
  }
2043
- }), this.renderClearIndicator(), this.renderLoadingIndicator(), this.renderDropdownIndicator())), this.renderMenu(), this.renderFormField(), /*#__PURE__*/React.createElement(NotifyOpenLayerObserver, {
2138
+ }), this.renderClearIndicator(), this.renderLoadingIndicator(), this.renderDropdownIndicator())), this.renderMenu(), this.renderFormField(), !fg('platform-dst-top-layer') && /*#__PURE__*/React.createElement(NotifyOpenLayerObserver, {
2044
2139
  isOpen: this.props.menuIsOpen,
2045
2140
  onClose: this.handleOpenLayerObserverCloseSignal
2046
2141
  })));
@@ -4,6 +4,7 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
4
4
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
5
5
  import { useContext, useLayoutEffect, useRef, useState } from 'react';
6
6
  import __noop from '@atlaskit/ds-lib/noop';
7
+ import { fg } from '@atlaskit/platform-feature-flags';
7
8
  import { PortalPlacementContext } from '../internal/portal-placement-context';
8
9
  var noop = __noop;
9
10
  function getScrollParent(element) {
@@ -262,6 +263,12 @@ var MenuPlacer = function MenuPlacer(props) {
262
263
  // The minimum height of the control
263
264
  var controlHeight = 38;
264
265
  useLayoutEffect(function () {
266
+ // When the menu is hosted in the browser top layer, positioning, flipping
267
+ // and viewport-fit are all handled by `@atlaskit/top-layer`. The placer
268
+ // becomes a pass-through that only forwards `maxMenuHeight` as a cap.
269
+ if (fg('platform-dst-top-layer')) {
270
+ return;
271
+ }
265
272
  var menuEl = ref.current;
266
273
  if (!menuEl) {
267
274
  return;
@@ -0,0 +1 @@
1
+ ._ofie1496{max-block-size:100dvh}
@@ -0,0 +1,142 @@
1
+ /* menu-portal-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import _extends from "@babel/runtime/helpers/extends";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ import "./menu-portal-top-layer.compiled.css";
5
+ import * as React from 'react';
6
+ import { ax, ix } from "@compiled/react/runtime";
7
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
8
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
9
+ import { useCallback, useContext, useRef } from 'react';
10
+ import { cx } from '@compiled/react';
11
+ import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
12
+ import { Popover } from '@atlaskit/top-layer/popover';
13
+ import { useAnchorPosition } from '@atlaskit/top-layer/use-anchor-position';
14
+ import { useWidthFromAnchor } from '@atlaskit/top-layer/use-width-from-anchor';
15
+ import { getStyleProps } from '../get-style-props';
16
+ import { MenuPortalCloseContext } from '../internal/menu-portal-close-context';
17
+ // `'auto'` falls through to `'end'`; top-layer's `position-try-fallbacks` flips it if needed.
18
+ function reactSelectEdgeToTopLayerEdge(menuPlacement) {
19
+ return menuPlacement === 'top' ? 'start' : 'end';
20
+ }
21
+ var menuPortalStyles = {
22
+ root: "_ofie1496"
23
+ };
24
+
25
+ /**
26
+ * Top-layer host for react-select's menu. Hands positioning, flip, and width
27
+ * to `@atlaskit/top-layer`; ignores `appendTo` / `menuPortalTarget` /
28
+ * `menuPosition`. The browser-dismiss handler is supplied internally by
29
+ * `Select` via `MenuPortalCloseContext`.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * <MenuPortalTopLayer controlElement={el} menuPlacement="bottom">
34
+ * <Menu />
35
+ * </MenuPortalTopLayer>
36
+ * ```
37
+ */
38
+ export function MenuPortalTopLayer(props) {
39
+ var children = props.children,
40
+ controlElement = props.controlElement,
41
+ innerProps = props.innerProps,
42
+ menuPlacement = props.menuPlacement,
43
+ menuPosition = props.menuPosition,
44
+ xcss = props.xcss;
45
+ // Select's "close the menu" callback, distinct from `Popover.onClose`.
46
+ var closeSelect = useContext(MenuPortalCloseContext);
47
+ var popoverRef = useRef(null);
48
+
49
+ // Top-layer hooks need a RefObject; in-render mutation keeps it in sync
50
+ // with the prop without an effect. The hooks re-read `.current` from
51
+ // `isOpen`-keyed layout effects, so a stable ref identity is fine.
52
+ var anchorRef = useRef(null);
53
+ anchorRef.current = controlElement;
54
+
55
+ // `controlElement` is null on initial mount / SSR until `Select` mirrors
56
+ // its ref into state. While null, every hook below no-ops to avoid DOM
57
+ // reads or registering an unpositioned popover.
58
+ var isAnchored = controlElement !== null;
59
+
60
+ // `gap: 0` matches the legacy MenuPortal (no trigger-to-menu gap; the
61
+ // menu root already declares its own `marginBlockStart`). Without this
62
+ // override `useAnchorPosition`'s default 8px gap would diverge visually
63
+ // from the legacy path.
64
+ useAnchorPosition({
65
+ anchorRef: anchorRef,
66
+ popoverRef: popoverRef,
67
+ placement: {
68
+ axis: 'block',
69
+ edge: reactSelectEdgeToTopLayerEdge(menuPlacement),
70
+ offset: {
71
+ gap: 0
72
+ }
73
+ },
74
+ isOpen: isAnchored
75
+ });
76
+ useWidthFromAnchor({
77
+ anchorRef: anchorRef,
78
+ popoverRef: popoverRef,
79
+ mode: 'match-anchor',
80
+ isOpen: isAnchored
81
+ });
82
+ var handlePopoverClose = useCallback(function () {
83
+ if (closeSelect) {
84
+ closeSelect();
85
+ }
86
+ }, [closeSelect]);
87
+
88
+ // Explicit observer registration: the outer Popover is intentionally
89
+ // roleless (see Popover comment below), so Popover cannot auto-register
90
+ // from its role. This lets `closeLayers()` (Modal / Drawer) dismiss us.
91
+ useNotifyOpenLayerObserver({
92
+ type: 'popup',
93
+ isOpen: isAnchored,
94
+ onClose: handlePopoverClose
95
+ });
96
+
97
+ // Top-layer owns positioning; zero offset/rect preserves the consumer
98
+ // styles call shape.
99
+ var _getStyleProps = getStyleProps(_objectSpread(_objectSpread({}, props), {}, {
100
+ offset: 0,
101
+ position: menuPosition,
102
+ rect: {
103
+ left: 0,
104
+ width: 0
105
+ }
106
+ }), 'menuPortal', {
107
+ 'menu-portal': true
108
+ }),
109
+ className = _getStyleProps.className;
110
+
111
+ // Popover stays mounted and is driven by `isOpen` so its layout-effect
112
+ // teardown (`hidePopover`, observer cleanup, position-hook style reset)
113
+ // runs against a live element. Conditional render would skip that path.
114
+ //
115
+ // `mode="manual"` opts out of native light-dismiss: react-select already
116
+ // owns outside-click and Escape via its own handlers, and the combobox
117
+ // trigger lives in a separate DOM subtree that the spec algorithm cannot
118
+ // see. Matches the pattern in `@atlaskit/datetime-picker`'s MenuTopLayer.
119
+ //
120
+ // The Popover host is intentionally roleless: the inner `MenuList`
121
+ // keeps `role="listbox"` and the id referenced by `aria-controls`.
122
+ // Putting the role on the outer host caused Playwright `toBeVisible`
123
+ // and some SR hit-testing to treat it as hidden (zero bounding rect
124
+ // while `styles.root` opts out of UA `[popover]` positioning).
125
+ //
126
+ // TODO: add open / close animation. Select unmounts MenuPortalTopLayer
127
+ // the moment `menuIsOpen` flips false, so Popover's `animate` exit
128
+ // transition never gets a frame. Needs keeping the portal mounted
129
+ // through the exit (`onExitFinish`) on the Select side.
130
+ return /*#__PURE__*/React.createElement(Popover, {
131
+ ref: popoverRef,
132
+ mode: "manual",
133
+ isOpen: isAnchored,
134
+ onClose: handlePopoverClose
135
+ }, /*#__PURE__*/React.createElement("div", _extends({
136
+ // `className` carries consumer `styles.menuPortal({...})` output,
137
+ // `xcss` carries caller compiled atomic classes, and `-MenuPortal`
138
+ // is a legacy selector hook kept for parity with MenuPortalLegacy.
139
+ // 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
140
+ className: ax([menuPortalStyles.root, cx(className, xcss, '-MenuPortal')])
141
+ }, innerProps), children));
142
+ }
@@ -11,8 +11,10 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
11
11
  import { cx } from '@compiled/react';
12
12
  import { autoUpdate } from '@floating-ui/dom';
13
13
  import { createPortal } from 'react-dom';
14
+ import { fg } from '@atlaskit/platform-feature-flags';
14
15
  import { getStyleProps } from '../get-style-props';
15
16
  import { PortalPlacementContext } from '../internal/portal-placement-context';
17
+ import { MenuPortalTopLayer } from './menu-portal-top-layer';
16
18
  function getBoundingClientObj(element) {
17
19
  var rect = element.getBoundingClientRect();
18
20
  return {
@@ -31,7 +33,7 @@ var menuPortalStyles = {
31
33
  root: "_1pbykb7n _1e02a1vk _kqswcp1v _152t1nmo _1bsb1qxj"
32
34
  };
33
35
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
34
- export var MenuPortal = function MenuPortal(props) {
36
+ function MenuPortalLegacy(props) {
35
37
  var appendTo = props.appendTo,
36
38
  children = props.children,
37
39
  controlElement = props.controlElement,
@@ -90,7 +92,10 @@ export var MenuPortal = function MenuPortal(props) {
90
92
  runAutoUpdate();
91
93
  }, [runAutoUpdate]);
92
94
 
93
- // bail early if required elements aren't present
95
+ // Legacy quirk: `computedPosition` is null until the layout effect runs,
96
+ // so the first render returns null even with `defaultMenuIsOpen` set.
97
+ // Synchronous observers (VR snapshots) see a one-frame "closed" state.
98
+ // Left as-is; the top-layer path supersedes this and positions declaratively.
94
99
  if (!appendTo && menuPosition !== 'fixed' || !computedPosition) {
95
100
  return null;
96
101
  }
@@ -120,4 +125,19 @@ export var MenuPortal = function MenuPortal(props) {
120
125
  return /*#__PURE__*/React.createElement(PortalPlacementContext.Provider, {
121
126
  value: portalPlacementContext
122
127
  }, appendTo ? /*#__PURE__*/createPortal(menuWrapper, appendTo) : menuWrapper);
128
+ }
129
+
130
+ /**
131
+ * Public-facing `MenuPortal` component. Routes between the legacy
132
+ * `createPortal`-based implementation and the top-layer-based
133
+ * `MenuPortalTopLayer` based on the `platform-dst-top-layer` feature flag.
134
+ */
135
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
136
+ export var MenuPortal = function MenuPortal(props) {
137
+ if (fg('platform-dst-top-layer')) {
138
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
139
+ return /*#__PURE__*/React.createElement(MenuPortalTopLayer, props);
140
+ }
141
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
142
+ return /*#__PURE__*/React.createElement(MenuPortalLegacy, props);
123
143
  };
@@ -0,0 +1,8 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * Internal handoff from `Select` to `MenuPortalTopLayer` for the top-layer
5
+ * dismiss signal. Kept off `MenuPortalProps` to avoid widening the public
6
+ * subpath export at `@atlaskit/react-select/menu-portal`.
7
+ */
8
+ export var MenuPortalCloseContext = /*#__PURE__*/createContext(undefined);