@atlaskit/react-select 2.1.0 → 2.2.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 (109) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/emotion/components/containers.js +111 -0
  3. package/dist/cjs/emotion/components/control.js +110 -0
  4. package/dist/cjs/emotion/components/group.js +71 -0
  5. package/dist/cjs/emotion/components/index.js +52 -0
  6. package/dist/cjs/emotion/components/indicators.js +137 -0
  7. package/dist/cjs/emotion/components/input.js +94 -0
  8. package/dist/cjs/emotion/components/internal/a11y-text.js +36 -0
  9. package/dist/cjs/emotion/components/internal/dummy-input.js +44 -0
  10. package/dist/cjs/emotion/components/internal/index.js +34 -0
  11. package/dist/cjs/emotion/components/internal/notify-open-layer-observer.js +21 -0
  12. package/dist/cjs/emotion/components/internal/required-input.js +43 -0
  13. package/dist/cjs/emotion/components/internal/scroll-manager.js +57 -0
  14. package/dist/cjs/emotion/components/internal/use-scroll-capture.js +132 -0
  15. package/dist/cjs/emotion/components/internal/use-scroll-lock.js +149 -0
  16. package/dist/cjs/emotion/components/live-region.js +182 -0
  17. package/dist/cjs/emotion/components/menu.js +456 -0
  18. package/dist/cjs/emotion/components/multi-value.js +224 -0
  19. package/dist/cjs/emotion/components/option.js +82 -0
  20. package/dist/cjs/emotion/components/placeholder.js +34 -0
  21. package/dist/cjs/emotion/components/single-value.js +40 -0
  22. package/dist/cjs/select.js +11 -8
  23. package/dist/es2019/emotion/components/containers.js +109 -0
  24. package/dist/es2019/emotion/components/control.js +107 -0
  25. package/dist/es2019/emotion/components/group.js +59 -0
  26. package/dist/es2019/emotion/components/index.js +41 -0
  27. package/dist/es2019/emotion/components/indicators.js +128 -0
  28. package/dist/es2019/emotion/components/input.js +86 -0
  29. package/dist/es2019/emotion/components/internal/a11y-text.js +27 -0
  30. package/dist/es2019/emotion/components/internal/dummy-input.js +37 -0
  31. package/dist/es2019/emotion/components/internal/index.js +4 -0
  32. package/dist/es2019/emotion/components/internal/notify-open-layer-observer.js +16 -0
  33. package/dist/es2019/emotion/components/internal/required-input.js +35 -0
  34. package/dist/es2019/emotion/components/internal/scroll-manager.js +49 -0
  35. package/dist/es2019/emotion/components/internal/use-scroll-capture.js +128 -0
  36. package/dist/es2019/emotion/components/internal/use-scroll-lock.js +143 -0
  37. package/dist/es2019/emotion/components/live-region.js +178 -0
  38. package/dist/es2019/emotion/components/menu.js +450 -0
  39. package/dist/es2019/emotion/components/multi-value.js +227 -0
  40. package/dist/es2019/emotion/components/option.js +78 -0
  41. package/dist/es2019/emotion/components/placeholder.js +28 -0
  42. package/dist/es2019/emotion/components/single-value.js +34 -0
  43. package/dist/es2019/select.js +11 -8
  44. package/dist/esm/emotion/components/containers.js +105 -0
  45. package/dist/esm/emotion/components/control.js +103 -0
  46. package/dist/esm/emotion/components/group.js +65 -0
  47. package/dist/esm/emotion/components/index.js +43 -0
  48. package/dist/esm/emotion/components/indicators.js +132 -0
  49. package/dist/esm/emotion/components/input.js +89 -0
  50. package/dist/esm/emotion/components/internal/a11y-text.js +29 -0
  51. package/dist/esm/emotion/components/internal/dummy-input.js +38 -0
  52. package/dist/esm/emotion/components/internal/index.js +4 -0
  53. package/dist/esm/emotion/components/internal/notify-open-layer-observer.js +15 -0
  54. package/dist/esm/emotion/components/internal/required-input.js +36 -0
  55. package/dist/esm/emotion/components/internal/scroll-manager.js +49 -0
  56. package/dist/esm/emotion/components/internal/use-scroll-capture.js +126 -0
  57. package/dist/esm/emotion/components/internal/use-scroll-lock.js +143 -0
  58. package/dist/esm/emotion/components/live-region.js +175 -0
  59. package/dist/esm/emotion/components/menu.js +454 -0
  60. package/dist/esm/emotion/components/multi-value.js +217 -0
  61. package/dist/esm/emotion/components/option.js +75 -0
  62. package/dist/esm/emotion/components/placeholder.js +27 -0
  63. package/dist/esm/emotion/components/single-value.js +33 -0
  64. package/dist/esm/select.js +11 -8
  65. package/dist/types/components/internal/notify-open-layer-observer.d.ts +4 -2
  66. package/dist/types/emotion/components/containers.d.ts +54 -0
  67. package/dist/types/emotion/components/control.d.ts +42 -0
  68. package/dist/types/emotion/components/group.d.ts +52 -0
  69. package/dist/types/emotion/components/index.d.ts +67 -0
  70. package/dist/types/emotion/components/indicators.d.ts +73 -0
  71. package/dist/types/emotion/components/input.d.ts +37 -0
  72. package/dist/types/emotion/components/internal/a11y-text.d.ts +8 -0
  73. package/dist/types/emotion/components/internal/dummy-input.d.ts +9 -0
  74. package/dist/types/emotion/components/internal/index.d.ts +4 -0
  75. package/dist/types/emotion/components/internal/notify-open-layer-observer.d.ts +11 -0
  76. package/dist/types/emotion/components/internal/required-input.d.ts +10 -0
  77. package/dist/types/emotion/components/internal/scroll-manager.d.ts +17 -0
  78. package/dist/types/emotion/components/internal/use-scroll-capture.d.ts +12 -0
  79. package/dist/types/emotion/components/internal/use-scroll-lock.d.ts +9 -0
  80. package/dist/types/emotion/components/live-region.d.ts +25 -0
  81. package/dist/types/emotion/components/menu.d.ts +116 -0
  82. package/dist/types/emotion/components/multi-value.d.ts +47 -0
  83. package/dist/types/emotion/components/option.d.ts +49 -0
  84. package/dist/types/emotion/components/placeholder.d.ts +22 -0
  85. package/dist/types/emotion/components/single-value.d.ts +28 -0
  86. package/dist/types/select.d.ts +7 -4
  87. package/dist/types-ts4.5/components/internal/notify-open-layer-observer.d.ts +4 -2
  88. package/dist/types-ts4.5/emotion/components/containers.d.ts +54 -0
  89. package/dist/types-ts4.5/emotion/components/control.d.ts +42 -0
  90. package/dist/types-ts4.5/emotion/components/group.d.ts +52 -0
  91. package/dist/types-ts4.5/emotion/components/index.d.ts +67 -0
  92. package/dist/types-ts4.5/emotion/components/indicators.d.ts +73 -0
  93. package/dist/types-ts4.5/emotion/components/input.d.ts +37 -0
  94. package/dist/types-ts4.5/emotion/components/internal/a11y-text.d.ts +8 -0
  95. package/dist/types-ts4.5/emotion/components/internal/dummy-input.d.ts +9 -0
  96. package/dist/types-ts4.5/emotion/components/internal/index.d.ts +4 -0
  97. package/dist/types-ts4.5/emotion/components/internal/notify-open-layer-observer.d.ts +11 -0
  98. package/dist/types-ts4.5/emotion/components/internal/required-input.d.ts +10 -0
  99. package/dist/types-ts4.5/emotion/components/internal/scroll-manager.d.ts +17 -0
  100. package/dist/types-ts4.5/emotion/components/internal/use-scroll-capture.d.ts +12 -0
  101. package/dist/types-ts4.5/emotion/components/internal/use-scroll-lock.d.ts +9 -0
  102. package/dist/types-ts4.5/emotion/components/live-region.d.ts +25 -0
  103. package/dist/types-ts4.5/emotion/components/menu.d.ts +116 -0
  104. package/dist/types-ts4.5/emotion/components/multi-value.d.ts +47 -0
  105. package/dist/types-ts4.5/emotion/components/option.d.ts +49 -0
  106. package/dist/types-ts4.5/emotion/components/placeholder.d.ts +22 -0
  107. package/dist/types-ts4.5/emotion/components/single-value.d.ts +28 -0
  108. package/dist/types-ts4.5/select.d.ts +7 -4
  109. package/package.json +3 -3
@@ -0,0 +1,178 @@
1
+ /* eslint-disable @atlaskit/platform/ensure-feature-flag-prefix */
2
+ /**
3
+ * @jsxRuntime classic
4
+ * @jsx jsx
5
+ * @jsxFrag React.Fragment
6
+ */
7
+ import React, { Fragment, useMemo, useRef } from 'react';
8
+ import { jsx } from '@emotion/react';
9
+ import { fg } from '@atlaskit/platform-feature-flags';
10
+ import { defaultAriaLiveMessages } from '../../accessibility';
11
+ import A11yText from './internal/a11y-text';
12
+
13
+ // ==============================
14
+ // Root Container
15
+ // ==============================
16
+
17
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
18
+ const LiveRegion = props => {
19
+ const {
20
+ ariaSelection,
21
+ focusedOption,
22
+ focusedValue,
23
+ focusableOptions,
24
+ isFocused,
25
+ selectValue,
26
+ selectProps,
27
+ id,
28
+ isAppleDevice
29
+ } = props;
30
+ const {
31
+ ariaLiveMessages,
32
+ getOptionLabel,
33
+ inputValue,
34
+ isMulti,
35
+ isOptionDisabled,
36
+ isSearchable,
37
+ label,
38
+ menuIsOpen,
39
+ options,
40
+ screenReaderStatus,
41
+ tabSelectsValue,
42
+ isLoading
43
+ } = selectProps;
44
+ const ariaLabel = selectProps['aria-label'] || label;
45
+ const ariaLive = selectProps['aria-live'];
46
+
47
+ // for safari, we will use minimum support from aria-live region
48
+ const isA11yImprovementEnabled = fg('design_system_select-a11y-improvement') && !isAppleDevice;
49
+
50
+ // Update aria live message configuration when prop changes
51
+ const messages = useMemo(() => ({
52
+ ...defaultAriaLiveMessages,
53
+ ...(ariaLiveMessages || {})
54
+ }), [ariaLiveMessages]);
55
+
56
+ // Update aria live selected option when prop changes
57
+ const ariaSelected = useMemo(() => {
58
+ let message = '';
59
+ if (isA11yImprovementEnabled && menuIsOpen) {
60
+ // we don't need to have selected message when the menu is open
61
+ return '';
62
+ }
63
+ if (ariaSelection && messages.onChange) {
64
+ const {
65
+ option,
66
+ options: selectedOptions,
67
+ removedValue,
68
+ removedValues,
69
+ value
70
+ } = ariaSelection;
71
+ // select-option when !isMulti does not return option so we assume selected option is value
72
+ const asOption = val => !Array.isArray(val) ? val : null;
73
+
74
+ // If there is just one item from the action then get its label
75
+ const selected = removedValue || option || asOption(value);
76
+ const label = selected ? getOptionLabel(selected) : '';
77
+
78
+ // If there are multiple items from the action then return an array of labels
79
+ const multiSelected = selectedOptions || removedValues || undefined;
80
+ const labels = multiSelected ? multiSelected.map(getOptionLabel) : [];
81
+ if (isA11yImprovementEnabled && !label && !labels.length) {
82
+ // return empty string if no labels provided
83
+ return '';
84
+ }
85
+ const onChangeProps = {
86
+ // multiSelected items are usually items that have already been selected
87
+ // or set by the user as a default value so we assume they are not disabled
88
+ isDisabled: selected && isOptionDisabled(selected, selectValue),
89
+ label,
90
+ labels,
91
+ ...ariaSelection
92
+ };
93
+ message = messages.onChange(onChangeProps);
94
+ }
95
+ return message;
96
+ }, [ariaSelection, messages, isOptionDisabled, selectValue, getOptionLabel, isA11yImprovementEnabled, menuIsOpen]);
97
+ const prevInputValue = useRef('');
98
+ const ariaFocused = useMemo(() => {
99
+ let focusMsg = '';
100
+ const focused = focusedOption || focusedValue;
101
+ const isSelected = !!(focusedOption && selectValue && selectValue.includes(focusedOption));
102
+ if (inputValue === prevInputValue.current && isA11yImprovementEnabled) {
103
+ // only announce focus option when searching when ff is on and the input value changed
104
+ // for safari, we will announce for all
105
+ return '';
106
+ }
107
+ if (focused && messages.onFocus) {
108
+ const onFocusProps = {
109
+ focused,
110
+ label: getOptionLabel(focused),
111
+ isDisabled: isOptionDisabled(focused, selectValue),
112
+ isSelected,
113
+ options: focusableOptions,
114
+ context: focused === focusedOption ? 'menu' : 'value',
115
+ selectValue,
116
+ isMulti
117
+ };
118
+ focusMsg = messages.onFocus(onFocusProps);
119
+ }
120
+ prevInputValue.current = inputValue;
121
+ return focusMsg;
122
+ }, [inputValue, focusedOption, focusedValue, getOptionLabel, isOptionDisabled, messages, focusableOptions, selectValue, isA11yImprovementEnabled, isMulti]);
123
+ const ariaResults = useMemo(() => {
124
+ let resultsMsg = '';
125
+ if (menuIsOpen && options.length && !isLoading && messages.onFilter) {
126
+ const resultsMessage = screenReaderStatus({
127
+ count: focusableOptions.length
128
+ });
129
+ resultsMsg = messages.onFilter({
130
+ inputValue,
131
+ resultsMessage
132
+ });
133
+ }
134
+ return resultsMsg;
135
+ }, [focusableOptions, inputValue, menuIsOpen, messages, options, screenReaderStatus, isLoading]);
136
+ const isInitialFocus = (ariaSelection === null || ariaSelection === void 0 ? void 0 : ariaSelection.action) === 'initial-input-focus';
137
+ const ariaGuidance = useMemo(() => {
138
+ if (fg('design_system_select-a11y-improvement')) {
139
+ // don't announce guidance at all when ff is on
140
+ return '';
141
+ }
142
+ let guidanceMsg = '';
143
+ if (messages.guidance) {
144
+ const context = focusedValue ? 'value' : menuIsOpen ? 'menu' : 'input';
145
+ guidanceMsg = messages.guidance({
146
+ 'aria-label': ariaLabel,
147
+ context,
148
+ isDisabled: focusedOption && isOptionDisabled(focusedOption, selectValue),
149
+ isMulti,
150
+ isSearchable,
151
+ tabSelectsValue,
152
+ isInitialFocus
153
+ });
154
+ }
155
+ return guidanceMsg;
156
+ }, [ariaLabel, focusedOption, focusedValue, isMulti, isOptionDisabled, isSearchable, menuIsOpen, messages, selectValue, tabSelectsValue, isInitialFocus]);
157
+ const ScreenReaderText = jsx(Fragment, null, jsx("span", {
158
+ id: "aria-selection"
159
+ }, ariaSelected), jsx("span", {
160
+ id: "aria-results"
161
+ }, ariaResults), !fg('design_system_select-a11y-improvement') && jsx(React.Fragment, null, jsx("span", {
162
+ id: "aria-focused"
163
+ }, ariaFocused), jsx("span", {
164
+ id: "aria-guidance"
165
+ }, ariaGuidance)));
166
+ return jsx(Fragment, null, jsx(A11yText, {
167
+ id: id
168
+ }, isInitialFocus && ScreenReaderText), jsx(A11yText, {
169
+ "aria-live": ariaLive // Should be undefined by default unless a specific use case requires it
170
+ ,
171
+ "aria-atomic": fg('design_system_select-a11y-improvement') ? undefined : 'false',
172
+ "aria-relevant": fg('design_system_select-a11y-improvement') ? undefined : 'additions text',
173
+ role: fg('design_system_select-a11y-improvement') ? 'status' : 'log'
174
+ }, isFocused && !isInitialFocus && ScreenReaderText));
175
+ };
176
+
177
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
178
+ export default LiveRegion;
@@ -0,0 +1,450 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ /**
3
+ * @jsxRuntime classic
4
+ * @jsx jsx
5
+ */
6
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
7
+ import { jsx } from '@emotion/react';
8
+ import { autoUpdate } from '@floating-ui/dom';
9
+ import { createPortal } from 'react-dom';
10
+ import useLayoutEffect from 'use-isomorphic-layout-effect';
11
+ import { fg } from '@atlaskit/platform-feature-flags';
12
+ import { Text } from '@atlaskit/primitives';
13
+ import { animatedScrollTo, getBoundingClientObj, getScrollParent, getScrollTop, getStyleProps, normalizedHeight, scrollTo } from '../../utils';
14
+
15
+ // ==============================
16
+ // Menu
17
+ // ==============================
18
+
19
+ // Get Menu Placement
20
+ // ------------------------------
21
+
22
+ function getMenuPlacement({
23
+ maxHeight: preferredMaxHeight,
24
+ menuEl,
25
+ minHeight,
26
+ placement: preferredPlacement,
27
+ shouldScroll,
28
+ isFixedPosition,
29
+ controlHeight
30
+ }) {
31
+ const scrollParent = getScrollParent(menuEl);
32
+ const defaultState = {
33
+ placement: 'bottom',
34
+ maxHeight: preferredMaxHeight
35
+ };
36
+
37
+ // something went wrong, return default state
38
+ if (!menuEl || !menuEl.offsetParent) {
39
+ return defaultState;
40
+ }
41
+
42
+ // we can't trust `scrollParent.scrollHeight` --> it may increase when
43
+ // the menu is rendered
44
+ const {
45
+ height: scrollHeight,
46
+ top: scrollParentTop
47
+ } = scrollParent.getBoundingClientRect();
48
+ const {
49
+ bottom: menuBottom,
50
+ height: menuHeight,
51
+ top: menuTop
52
+ } = menuEl.getBoundingClientRect();
53
+ const {
54
+ top: containerTop
55
+ } = menuEl.offsetParent.getBoundingClientRect();
56
+ const viewHeight = isFixedPosition ? window.innerHeight : normalizedHeight(scrollParent);
57
+ const scrollTop = getScrollTop(scrollParent);
58
+ // use menuTop - scrollParentTop for the actual top space of menu in the scroll container
59
+ const menuTopFromParent = fg('design-system-select-fix-placement') ? menuTop - scrollParentTop : menuTop;
60
+ const marginBottom = parseInt(getComputedStyle(menuEl).marginBottom, 10);
61
+ const marginTop = parseInt(getComputedStyle(menuEl).marginTop, 10);
62
+ const viewSpaceAbove = containerTop - marginTop;
63
+ const viewSpaceBelow = viewHeight - menuTopFromParent;
64
+ const scrollSpaceAbove = viewSpaceAbove + scrollTop;
65
+ const scrollSpaceBelow = scrollHeight - scrollTop - menuTopFromParent;
66
+ const scrollDown = menuBottom - viewHeight + scrollTop + marginBottom;
67
+ const scrollUp = scrollTop + menuTop - marginTop;
68
+ const scrollDuration = 160;
69
+ switch (preferredPlacement) {
70
+ case 'auto':
71
+ case 'bottom':
72
+ // 1: the menu will fit, do nothing
73
+ if (viewSpaceBelow >= menuHeight) {
74
+ return {
75
+ placement: 'bottom',
76
+ maxHeight: preferredMaxHeight
77
+ };
78
+ }
79
+
80
+ // 2: the menu will fit, if scrolled
81
+ if (scrollSpaceBelow >= menuHeight && !isFixedPosition) {
82
+ if (shouldScroll) {
83
+ animatedScrollTo(scrollParent, scrollDown, scrollDuration);
84
+ }
85
+ return {
86
+ placement: 'bottom',
87
+ maxHeight: preferredMaxHeight
88
+ };
89
+ }
90
+
91
+ // 3: the menu will fit, if constrained
92
+ if (!isFixedPosition && scrollSpaceBelow >= minHeight || isFixedPosition && viewSpaceBelow >= minHeight) {
93
+ if (shouldScroll) {
94
+ animatedScrollTo(scrollParent, scrollDown, scrollDuration);
95
+ }
96
+
97
+ // we want to provide as much of the menu as possible to the user,
98
+ // so give them whatever is available below rather than the minHeight.
99
+ const constrainedHeight = isFixedPosition ? viewSpaceBelow - marginBottom : scrollSpaceBelow - marginBottom;
100
+ return {
101
+ placement: 'bottom',
102
+ maxHeight: constrainedHeight
103
+ };
104
+ }
105
+
106
+ // 4. Forked beviour when there isn't enough space below
107
+
108
+ // AUTO: flip the menu, render above
109
+ if (preferredPlacement === 'auto' || isFixedPosition) {
110
+ // may need to be constrained after flipping
111
+ let constrainedHeight = preferredMaxHeight;
112
+ const spaceAbove = isFixedPosition ? viewSpaceAbove : scrollSpaceAbove;
113
+ if (spaceAbove >= minHeight) {
114
+ constrainedHeight = Math.min(spaceAbove - marginBottom - controlHeight, preferredMaxHeight);
115
+ }
116
+ return {
117
+ placement: 'top',
118
+ maxHeight: constrainedHeight
119
+ };
120
+ }
121
+
122
+ // BOTTOM: allow browser to increase scrollable area and immediately set scroll
123
+ if (preferredPlacement === 'bottom') {
124
+ if (shouldScroll) {
125
+ scrollTo(scrollParent, scrollDown);
126
+ }
127
+ return {
128
+ placement: 'bottom',
129
+ maxHeight: preferredMaxHeight
130
+ };
131
+ }
132
+ break;
133
+ case 'top':
134
+ // 1: the menu will fit, do nothing
135
+ if (viewSpaceAbove >= menuHeight) {
136
+ return {
137
+ placement: 'top',
138
+ maxHeight: preferredMaxHeight
139
+ };
140
+ }
141
+
142
+ // 2: the menu will fit, if scrolled
143
+ if (scrollSpaceAbove >= menuHeight && !isFixedPosition) {
144
+ if (shouldScroll) {
145
+ animatedScrollTo(scrollParent, scrollUp, scrollDuration);
146
+ }
147
+ return {
148
+ placement: 'top',
149
+ maxHeight: preferredMaxHeight
150
+ };
151
+ }
152
+
153
+ // 3: the menu will fit, if constrained
154
+ if (!isFixedPosition && scrollSpaceAbove >= minHeight || isFixedPosition && viewSpaceAbove >= minHeight) {
155
+ let constrainedHeight = preferredMaxHeight;
156
+
157
+ // we want to provide as much of the menu as possible to the user,
158
+ // so give them whatever is available below rather than the minHeight.
159
+ if (!isFixedPosition && scrollSpaceAbove >= minHeight || isFixedPosition && viewSpaceAbove >= minHeight) {
160
+ constrainedHeight = isFixedPosition ? viewSpaceAbove - marginTop : scrollSpaceAbove - marginTop;
161
+ }
162
+ if (shouldScroll) {
163
+ animatedScrollTo(scrollParent, scrollUp, scrollDuration);
164
+ }
165
+ return {
166
+ placement: 'top',
167
+ maxHeight: constrainedHeight
168
+ };
169
+ }
170
+
171
+ // 4. not enough space, the browser WILL NOT increase scrollable area when
172
+ // absolutely positioned element rendered above the viewport (only below).
173
+ // Flip the menu, render below
174
+ return {
175
+ placement: 'bottom',
176
+ maxHeight: preferredMaxHeight
177
+ };
178
+ default:
179
+ throw new Error(`Invalid placement provided "${preferredPlacement}".`);
180
+ }
181
+ return defaultState;
182
+ }
183
+
184
+ // Menu Component
185
+ // ------------------------------
186
+
187
+ function alignToControl(placement) {
188
+ const placementToCSSProp = {
189
+ bottom: 'top',
190
+ top: 'bottom'
191
+ };
192
+ return placement ? placementToCSSProp[placement] : 'bottom';
193
+ }
194
+ const coercePlacement = p => p === 'auto' ? 'bottom' : p;
195
+ export const menuCSS = ({
196
+ placement
197
+ }) => ({
198
+ label: 'menu',
199
+ [alignToControl(placement)]: '100%',
200
+ position: 'absolute',
201
+ width: '100%',
202
+ zIndex: 1,
203
+ borderRadius: "var(--ds-border-radius, 4px)",
204
+ marginBottom: "var(--ds-space-100, 8px)",
205
+ marginTop: "var(--ds-space-100, 8px)",
206
+ backgroundColor: "var(--ds-surface-overlay, white)",
207
+ boxShadow: "var(--ds-shadow-overlay, 0 0 0 1px hsl(0deg 0% 0% / 10%), 0 4px 11px hsl(0deg 0% 0% / 10%))"
208
+ });
209
+ const PortalPlacementContext = /*#__PURE__*/createContext(null);
210
+
211
+ // NOTE: internal only
212
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
213
+ export const MenuPlacer = props => {
214
+ const {
215
+ children,
216
+ minMenuHeight,
217
+ maxMenuHeight,
218
+ menuPlacement,
219
+ menuPosition,
220
+ menuShouldScrollIntoView
221
+ } = props;
222
+ const {
223
+ setPortalPlacement
224
+ } = useContext(PortalPlacementContext) || {};
225
+ const ref = useRef(null);
226
+ const [maxHeight, setMaxHeight] = useState(maxMenuHeight);
227
+ const [placement, setPlacement] = useState(null);
228
+ // The minimum height of the control
229
+ const controlHeight = 38;
230
+ useLayoutEffect(() => {
231
+ const menuEl = ref.current;
232
+ if (!menuEl) {
233
+ return;
234
+ }
235
+
236
+ // DO NOT scroll if position is fixed
237
+ const isFixedPosition = menuPosition === 'fixed';
238
+ const shouldScroll = menuShouldScrollIntoView && !isFixedPosition;
239
+ const state = getMenuPlacement({
240
+ maxHeight: maxMenuHeight,
241
+ menuEl,
242
+ minHeight: minMenuHeight,
243
+ placement: menuPlacement,
244
+ shouldScroll,
245
+ isFixedPosition,
246
+ controlHeight
247
+ });
248
+ setMaxHeight(state.maxHeight);
249
+ setPlacement(state.placement);
250
+ setPortalPlacement === null || setPortalPlacement === void 0 ? void 0 : setPortalPlacement(state.placement);
251
+ }, [maxMenuHeight, menuPlacement, menuPosition, menuShouldScrollIntoView, minMenuHeight, setPortalPlacement, controlHeight]);
252
+ return children({
253
+ ref,
254
+ placerProps: {
255
+ ...props,
256
+ placement: placement || coercePlacement(menuPlacement),
257
+ maxHeight
258
+ }
259
+ });
260
+ };
261
+ const Menu = props => {
262
+ const {
263
+ children,
264
+ innerRef,
265
+ innerProps
266
+ } = props;
267
+ return jsx("div", _extends({}, getStyleProps(props, 'menu', {
268
+ menu: true
269
+ }), {
270
+ ref: innerRef
271
+ }, innerProps), children);
272
+ };
273
+
274
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
275
+ export default Menu;
276
+
277
+ // ==============================
278
+ // Menu List
279
+ // ==============================
280
+
281
+ export const menuListCSS = ({
282
+ maxHeight
283
+ }) => ({
284
+ maxHeight,
285
+ overflowY: 'auto',
286
+ position: 'relative',
287
+ // required for offset[Height, Top] > keyboard scroll
288
+ WebkitOverflowScrolling: 'touch',
289
+ paddingTop: "var(--ds-space-100, 8px)",
290
+ paddingBottom: "var(--ds-space-100, 8px)"
291
+ });
292
+
293
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
294
+ export const MenuList = props => {
295
+ const {
296
+ children,
297
+ innerProps,
298
+ innerRef,
299
+ isMulti
300
+ } = props;
301
+ return jsx("div", _extends({}, getStyleProps(props, 'menuList', {
302
+ 'menu-list': true,
303
+ 'menu-list--is-multi': isMulti
304
+ }), {
305
+ ref: innerRef
306
+ }, innerProps, {
307
+ tabIndex: -1
308
+ }), children);
309
+ };
310
+
311
+ // ==============================
312
+ // Menu Notices
313
+ // ==============================
314
+
315
+ const noticeCSS = ({}) => ({
316
+ textAlign: 'center',
317
+ padding: `${"var(--ds-space-100, 8px)"} ${"var(--ds-space-150, 12px)"}`
318
+ });
319
+ export const noOptionsMessageCSS = noticeCSS;
320
+ export const loadingMessageCSS = noticeCSS;
321
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
322
+ export const NoOptionsMessage = ({
323
+ children = 'No options',
324
+ innerProps,
325
+ ...restProps
326
+ }) => {
327
+ return jsx("div", _extends({}, getStyleProps({
328
+ ...restProps,
329
+ children,
330
+ innerProps
331
+ }, 'noOptionsMessage', {
332
+ 'menu-notice': true,
333
+ 'menu-notice--no-options': true
334
+ }), {
335
+ // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
336
+ role: "option"
337
+ }, innerProps), jsx(Text, {
338
+ color: "color.text.subtle"
339
+ }, children));
340
+ };
341
+
342
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
343
+ export const LoadingMessage = ({
344
+ children = 'Loading...',
345
+ innerProps,
346
+ ...restProps
347
+ }) => {
348
+ return jsx("div", _extends({}, getStyleProps({
349
+ ...restProps,
350
+ children,
351
+ innerProps
352
+ }, 'loadingMessage', {
353
+ 'menu-notice': true,
354
+ 'menu-notice--loading': true
355
+ }), innerProps, {
356
+ // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
357
+ role: "option"
358
+ }), jsx(Text, {
359
+ color: "color.text.subtle"
360
+ }, children));
361
+ };
362
+
363
+ // ==============================
364
+ // Menu Portal
365
+ // ==============================
366
+
367
+ export const menuPortalCSS = ({
368
+ rect,
369
+ offset,
370
+ position
371
+ }) => ({
372
+ left: rect.left,
373
+ position: position,
374
+ top: offset,
375
+ width: rect.width,
376
+ zIndex: 1
377
+ });
378
+ // eslint-disable-next-line @repo/internal/react/require-jsdoc
379
+ export const MenuPortal = props => {
380
+ const {
381
+ appendTo,
382
+ children,
383
+ controlElement,
384
+ innerProps,
385
+ menuPlacement,
386
+ menuPosition
387
+ } = props;
388
+ const menuPortalRef = useRef(null);
389
+ const cleanupRef = useRef(null);
390
+ const [placement, setPortalPlacement] = useState(coercePlacement(menuPlacement));
391
+ const portalPlacementContext = useMemo(() => ({
392
+ setPortalPlacement
393
+ }), []);
394
+ const [computedPosition, setComputedPosition] = useState(null);
395
+ const updateComputedPosition = useCallback(() => {
396
+ if (!controlElement) {
397
+ return;
398
+ }
399
+ const rect = getBoundingClientObj(controlElement);
400
+ const scrollDistance = menuPosition === 'fixed' ? 0 : window.pageYOffset;
401
+ const offset = rect[placement] + scrollDistance;
402
+ if (offset !== (computedPosition === null || computedPosition === void 0 ? void 0 : computedPosition.offset) || rect.left !== (computedPosition === null || computedPosition === void 0 ? void 0 : computedPosition.rect.left) || rect.width !== (computedPosition === null || computedPosition === void 0 ? void 0 : computedPosition.rect.width)) {
403
+ setComputedPosition({
404
+ offset,
405
+ rect
406
+ });
407
+ }
408
+ }, [controlElement, menuPosition, placement, computedPosition === null || computedPosition === void 0 ? void 0 : computedPosition.offset, computedPosition === null || computedPosition === void 0 ? void 0 : computedPosition.rect.left, computedPosition === null || computedPosition === void 0 ? void 0 : computedPosition.rect.width]);
409
+ useLayoutEffect(() => {
410
+ updateComputedPosition();
411
+ }, [updateComputedPosition]);
412
+ const runAutoUpdate = useCallback(() => {
413
+ if (typeof cleanupRef.current === 'function') {
414
+ cleanupRef.current();
415
+ cleanupRef.current = null;
416
+ }
417
+ if (controlElement && menuPortalRef.current) {
418
+ cleanupRef.current = autoUpdate(controlElement, menuPortalRef.current, updateComputedPosition, {
419
+ elementResize: 'ResizeObserver' in window
420
+ });
421
+ }
422
+ }, [controlElement, updateComputedPosition]);
423
+ useLayoutEffect(() => {
424
+ runAutoUpdate();
425
+ }, [runAutoUpdate]);
426
+ const setMenuPortalElement = useCallback(menuPortalElement => {
427
+ menuPortalRef.current = menuPortalElement;
428
+ runAutoUpdate();
429
+ }, [runAutoUpdate]);
430
+
431
+ // bail early if required elements aren't present
432
+ if (!appendTo && menuPosition !== 'fixed' || !computedPosition) {
433
+ return null;
434
+ }
435
+
436
+ // same wrapper element whether fixed or portalled
437
+ const menuWrapper = jsx("div", _extends({
438
+ ref: setMenuPortalElement
439
+ }, getStyleProps({
440
+ ...props,
441
+ offset: computedPosition.offset,
442
+ position: menuPosition,
443
+ rect: computedPosition.rect
444
+ }, 'menuPortal', {
445
+ 'menu-portal': true
446
+ }), innerProps), children);
447
+ return jsx(PortalPlacementContext.Provider, {
448
+ value: portalPlacementContext
449
+ }, appendTo ? /*#__PURE__*/createPortal(menuWrapper, appendTo) : menuWrapper);
450
+ };