@tamagui/select 1.0.0-beta.17 → 1.0.1-beta.102

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.
@@ -1,21 +1,47 @@
1
- var __defProp = Object.defineProperty;
2
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
1
  import {
2
+ FloatingFocusManager,
3
+ FloatingOverlay,
4
+ FloatingPortal,
5
+ autoUpdate,
6
+ detectOverflow,
7
+ flip,
4
8
  offset,
5
- useFloating
9
+ size,
10
+ useClick,
11
+ useDismiss,
12
+ useFloating,
13
+ useInteractions,
14
+ useListNavigation,
15
+ useRole,
16
+ useTypeahead
6
17
  } from "@floating-ui/react-dom-interactions";
7
18
  import { usePrevious } from "@radix-ui/react-use-previous";
8
19
  import { useComposedRefs } from "@tamagui/compose-refs";
9
- import { composeEventHandlers, useIsomorphicLayoutEffect } from "@tamagui/core";
20
+ import {
21
+ Theme,
22
+ styled,
23
+ useIsomorphicLayoutEffect,
24
+ useThemeName,
25
+ withStaticProperties
26
+ } from "@tamagui/core";
10
27
  import { useId } from "@tamagui/core";
11
28
  import { createContextScope } from "@tamagui/create-context";
29
+ import { ListItem } from "@tamagui/list-item";
12
30
  import { Separator } from "@tamagui/separator";
13
- import { XStack } from "@tamagui/stacks";
31
+ import { ThemeableStack, XStack, YStack } from "@tamagui/stacks";
32
+ import { Paragraph } from "@tamagui/text";
14
33
  import { useControllableState } from "@tamagui/use-controllable-state";
15
34
  import * as React from "react";
16
35
  import * as ReactDOM from "react-dom";
17
- const OPEN_KEYS = [" ", "Enter", "ArrowUp", "ArrowDown"];
18
- const SELECTION_KEYS = [" ", "Enter"];
36
+ const userAgent = typeof navigator !== "undefined" && navigator.userAgent || "";
37
+ const isFirefox = userAgent.toLowerCase().includes("firefox");
38
+ if (isFirefox) {
39
+ document.body.classList.add("firefox");
40
+ }
41
+ function getVisualOffsetTop() {
42
+ var _a;
43
+ return !/^((?!chrome|android).)*safari/i.test(userAgent) ? (_a = visualViewport == null ? void 0 : visualViewport.offsetTop) != null ? _a : 0 : 0;
44
+ }
19
45
  const SELECT_NAME = "Select";
20
46
  const WINDOW_PADDING = 8;
21
47
  const SCROLL_ARROW_VELOCITY = 8;
@@ -24,58 +50,6 @@ const MIN_HEIGHT = 80;
24
50
  const FALLBACK_THRESHOLD = 16;
25
51
  const [createSelectContext, createSelectScope] = createContextScope(SELECT_NAME);
26
52
  const [SelectProvider, useSelectContext] = createSelectContext(SELECT_NAME);
27
- const Select = /* @__PURE__ */ __name((props) => {
28
- const {
29
- __scopeSelect,
30
- children,
31
- open: openProp,
32
- defaultOpen,
33
- onOpenChange,
34
- value: valueProp,
35
- defaultValue,
36
- onValueChange,
37
- dir: dir2,
38
- name,
39
- autoComplete
40
- } = props;
41
- const [open, setOpen] = useControllableState({
42
- prop: openProp,
43
- defaultProp: defaultOpen || false,
44
- onChange: onOpenChange
45
- });
46
- const [value, setValue] = useControllableState({
47
- prop: valueProp,
48
- defaultProp: defaultValue || "",
49
- onChange: onValueChange
50
- });
51
- const selectedIndexRef = React.useRef(null);
52
- const activeIndexRef = React.useRef(null);
53
- const prevActiveIndex = usePrevious(activeIndex);
54
- const [showArrows, setShowArrows] = React.useState(false);
55
- const [scrollTop, setScrollTop] = React.useState(0);
56
- const [activeIndex, setActiveIndex] = React.useState(null);
57
- const [selectedIndex, setSelectedIndex] = React.useState(Math.max(0, listContentRef.current.indexOf(value)));
58
- useIsomorphicLayoutEffect(() => {
59
- selectedIndexRef.current = selectedIndex;
60
- activeIndexRef.current = activeIndex;
61
- });
62
- useEffect(() => {
63
- const frame = requestAnimationFrame(() => {
64
- setShowArrows(open);
65
- if (!open) {
66
- setScrollTop(0);
67
- setMiddlewareType("align");
68
- setActiveIndex(null);
69
- setControlledScrolling(false);
70
- }
71
- });
72
- return () => cancelAnimationFrame(frame);
73
- }, [open]);
74
- const showUpArrow = showArrows && scrollTop > SCROLL_ARROW_THRESHOLD;
75
- const showDownArrow = showArrows && floatingRef.current && scrollTop < floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD;
76
- return <SelectProvider scope={__scopeSelect} open={open}>{children}</SelectProvider>;
77
- }, "Select");
78
- Select.displayName = SELECT_NAME;
79
53
  const TRIGGER_NAME = "SelectTrigger";
80
54
  const SelectTrigger = React.forwardRef((props, forwardedRef) => {
81
55
  const {
@@ -85,217 +59,657 @@ const SelectTrigger = React.forwardRef((props, forwardedRef) => {
85
59
  ...triggerProps
86
60
  } = props;
87
61
  const context = useSelectContext(TRIGGER_NAME, __scopeSelect);
88
- const composedRefs = useComposedRefs(forwardedRef, context.onTriggerChange);
89
- const getItems = useCollection(__scopeSelect);
90
- const handleOpen = /* @__PURE__ */ __name(() => {
91
- if (!disabled) {
92
- context.onOpenChange(true);
93
- resetTypeahead();
94
- }
95
- }, "handleOpen");
96
- return <Primitive.button type="button" role="combobox" aria-controls={context.contentId} aria-expanded={context.open} aria-autocomplete="none" dir={context.dir} disabled={disabled} data-disabled={disabled ? "" : void 0} {...triggerProps} ref={composedRefs} onPointerDown={composeEventHandlers(triggerProps.onPointerDown, (event) => {
97
- ;
98
- event.target.releasePointerCapture(event.pointerId);
99
- if (event.button === 0 && event.ctrlKey === false) {
100
- handleOpen();
101
- context.triggerPointerDownPosRef.current = {
102
- x: Math.round(event.pageX),
103
- y: Math.round(event.pageY)
104
- };
105
- event.preventDefault();
106
- }
107
- })} onKeyDown={composeEventHandlers(triggerProps.onKeyDown, (event) => {
108
- const isTypingAhead = searchRef.current !== "";
109
- const isModifierKey = event.ctrlKey || event.altKey || event.metaKey;
110
- if (!isModifierKey && event.key.length === 1)
111
- handleTypeaheadSearch(event.key);
112
- if (isTypingAhead && event.key === " ")
113
- return;
114
- if (OPEN_KEYS.includes(event.key)) {
115
- handleOpen();
116
- event.preventDefault();
117
- }
118
- })} />;
62
+ return <ListItem backgrounded radiused hoverTheme pressTheme focusTheme focusable={false} borderWidth={1} componentName={TRIGGER_NAME} size={context.size} aria-expanded={context.open} aria-autocomplete="none" disabled={disabled} data-disabled={disabled ? "" : void 0} {...triggerProps} ref={forwardedRef} {...context.interactions.getReferenceProps()} />;
119
63
  });
120
64
  SelectTrigger.displayName = TRIGGER_NAME;
121
65
  const VALUE_NAME = "SelectValue";
122
- const SelectValue = React.forwardRef((props, forwardedRef) => {
123
- const { __scopeSelect, className, style, ...valueProps } = props;
124
- return <XStack {...valueProps} pointerEvents="none" />;
66
+ const SelectValueFrame = styled(Paragraph, {
67
+ name: VALUE_NAME,
68
+ selectable: false
125
69
  });
126
- const ICON_NAME = "SelectIcon";
127
- const SelectIcon = React.forwardRef((props, forwardedRef) => {
128
- const { __scopeSelect, children, ...iconProps } = props;
129
- return <XStack aria-hidden {...iconProps} ref={forwardedRef}>{children || "\u25BC"}</XStack>;
70
+ const SelectValue = SelectValueFrame.extractable(React.forwardRef(({ __scopeSelect, children, placeholder }, forwardedRef) => {
71
+ const context = useSelectContext(VALUE_NAME, __scopeSelect);
72
+ const { onValueNodeHasChildrenChange } = context;
73
+ const hasChildren = children !== void 0;
74
+ const composedRefs = useComposedRefs(forwardedRef, context.onValueNodeChange);
75
+ React.useLayoutEffect(() => {
76
+ onValueNodeHasChildrenChange(hasChildren);
77
+ }, [onValueNodeHasChildrenChange, hasChildren]);
78
+ return <SelectValueFrame size={context.size} ref={composedRefs} pointerEvents="none">{context.value === void 0 && placeholder !== void 0 ? placeholder : children}</SelectValueFrame>;
79
+ }));
80
+ SelectValue.displayName = VALUE_NAME;
81
+ const SelectIcon = styled(XStack, {
82
+ name: "SelectIcon",
83
+ "aria-hidden": true,
84
+ children: <Paragraph>{"\u25BC"}</Paragraph>
130
85
  });
131
- SelectIcon.displayName = ICON_NAME;
132
86
  const CONTENT_NAME = "SelectContent";
133
- const SelectContent = React.forwardRef((props, forwardedRef) => {
134
- const context = useSelectContext(CONTENT_NAME, props.__scopeSelect);
135
- return props.children;
136
- });
137
- const GROUP_NAME = "SelectGroup";
138
- const [SelectGroupContextProvider, useSelectGroupContext] = createSelectContext(GROUP_NAME);
139
- const SelectGroup = React.forwardRef((props, forwardedRef) => {
140
- const { __scopeSelect, ...groupProps } = props;
141
- const groupId = useId();
142
- return <SelectGroupContextProvider scope={__scopeSelect} id={groupId}><Primitive.div role="group" aria-labelledby={groupId} {...groupProps} ref={forwardedRef} /></SelectGroupContextProvider>;
87
+ const SelectContent = ({ children, __scopeSelect }) => {
88
+ const context = useSelectContext(CONTENT_NAME, __scopeSelect);
89
+ const themeName = useThemeName();
90
+ const contents = <Theme name={themeName}>{children}</Theme>;
91
+ return <FloatingPortal>{context.open ? <FloatingOverlay lockScroll>{contents}</FloatingOverlay> : <div style={{ display: "none" }}>{contents}</div>}</FloatingPortal>;
92
+ };
93
+ const VIEWPORT_NAME = "SelectViewport";
94
+ const SelectViewportFrame = styled(ThemeableStack, {
95
+ name: VIEWPORT_NAME,
96
+ backgroundColor: "$background",
97
+ elevate: true,
98
+ bordered: true,
99
+ overflow: "scroll",
100
+ userSelect: "none",
101
+ variants: {
102
+ size: {
103
+ "...size": (val, { tokens }) => {
104
+ var _a;
105
+ return {
106
+ borderRadius: (_a = tokens.radius[val]) != null ? _a : val
107
+ };
108
+ }
109
+ }
110
+ },
111
+ defaultVariants: {
112
+ size: "$2"
113
+ }
143
114
  });
144
- SelectGroup.displayName = GROUP_NAME;
145
- const LABEL_NAME = "SelectLabel";
146
- const SelectLabel = React.forwardRef((props, forwardedRef) => {
147
- const { __scopeSelect, ...labelProps } = props;
148
- const groupContext = useSelectGroupContext(LABEL_NAME, __scopeSelect);
149
- return <Primitive.div id={groupContext.id} {...labelProps} ref={forwardedRef} />;
115
+ const SelectViewport = React.forwardRef((props, forwardedRef) => {
116
+ const { __scopeSelect, children, ...viewportProps } = props;
117
+ const context = useSelectContext(VIEWPORT_NAME, __scopeSelect);
118
+ const { style, ...floatingProps } = context.interactions.getFloatingProps();
119
+ delete style["scrollbarWidth"];
120
+ delete style["listStyleType"];
121
+ return <>
122
+ <style dangerouslySetInnerHTML={{
123
+ __html: `[data-tamagui-select-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-tamagui-select-viewport]::-webkit-scrollbar{display:none}`
124
+ }} />
125
+ <FloatingFocusManager context={context.floatingContext} preventTabbing><SelectViewportFrame size={context.size} data-tamagui-select-viewport="" role="presentation" {...viewportProps} ref={forwardedRef} {...floatingProps} {...style}>{children}</SelectViewportFrame></FloatingFocusManager>
126
+ </>;
150
127
  });
151
- SelectLabel.displayName = LABEL_NAME;
128
+ SelectViewport.displayName = VIEWPORT_NAME;
152
129
  const ITEM_NAME = "SelectItem";
153
130
  const [SelectItemContextProvider, useSelectItemContext] = createSelectContext(ITEM_NAME);
154
131
  const SelectItem = React.forwardRef((props, forwardedRef) => {
155
- const { __scopeSelect, value, disabled = false, textValue: textValueProp, ...itemProps } = props;
132
+ const {
133
+ __scopeSelect,
134
+ value,
135
+ disabled = false,
136
+ textValue: textValueProp,
137
+ index,
138
+ ...itemProps
139
+ } = props;
156
140
  const context = useSelectContext(ITEM_NAME, __scopeSelect);
157
- const contentContext = useSelectContentContext(ITEM_NAME, __scopeSelect);
158
141
  const isSelected = context.value === value;
159
142
  const [textValue, setTextValue] = React.useState(textValueProp != null ? textValueProp : "");
160
143
  const [isFocused, setIsFocused] = React.useState(false);
161
- const composedRefs = useComposedRefs(forwardedRef, isSelected ? contentContext.onSelectedItemChange : void 0);
162
144
  const textId = useId();
163
- const handleSelect = /* @__PURE__ */ __name(() => {
164
- if (!disabled) {
165
- context.onValueChange(value);
166
- context.onOpenChange(false);
145
+ const {
146
+ selectedIndex,
147
+ setSelectedIndex,
148
+ listRef,
149
+ open,
150
+ setOpen,
151
+ onChange,
152
+ activeIndex,
153
+ setActiveIndex,
154
+ setValueAtIndex,
155
+ dataRef
156
+ } = context;
157
+ const composedRefs = useComposedRefs(forwardedRef, (node) => {
158
+ if (node instanceof HTMLElement) {
159
+ listRef.current[index] = node;
160
+ }
161
+ });
162
+ React.useEffect(() => {
163
+ setValueAtIndex(index, value);
164
+ }, [index]);
165
+ const timeoutRef = React.useRef();
166
+ const [allowMouseUp, setAllowMouseUp] = React.useState(false);
167
+ function handleSelect() {
168
+ setSelectedIndex(index);
169
+ onChange(value);
170
+ setOpen(false);
171
+ }
172
+ React.useLayoutEffect(() => {
173
+ clearTimeout(timeoutRef.current);
174
+ if (open) {
175
+ if (selectedIndex !== index) {
176
+ setAllowMouseUp(true);
177
+ } else {
178
+ timeoutRef.current = setTimeout(() => {
179
+ setAllowMouseUp(true);
180
+ }, 300);
181
+ }
182
+ } else {
183
+ setAllowMouseUp(false);
167
184
  }
168
- }, "handleSelect");
169
- return <SelectItemContextProvider scope={__scopeSelect} value={value} textId={textId} isSelected={isSelected} onItemTextChange={React.useCallback((node) => {
185
+ }, [open, index, setActiveIndex, selectedIndex]);
186
+ function handleKeyDown(event) {
187
+ if (event.key === "Enter" || event.key === " " && !dataRef.current.typing) {
188
+ event.preventDefault();
189
+ handleSelect();
190
+ }
191
+ }
192
+ const selectItemProps = context.interactions.getItemProps({
193
+ onClick: allowMouseUp ? handleSelect : void 0,
194
+ onMouseUp: allowMouseUp ? handleSelect : void 0,
195
+ onKeyDown: handleKeyDown
196
+ });
197
+ return <SelectItemContextProvider scope={__scopeSelect} value={value} textId={textId || ""} isSelected={isSelected} onItemTextChange={React.useCallback((node) => {
170
198
  setTextValue((prevTextValue) => {
171
199
  var _a;
172
200
  return prevTextValue || ((_a = node == null ? void 0 : node.textContent) != null ? _a : "").trim();
173
201
  });
174
- }, [])}><Collection.ItemSlot scope={__scopeSelect} value={value} disabled={disabled} textValue={textValue}><Primitive.div role="option" aria-labelledby={textId} aria-selected={isSelected && isFocused} data-state={isSelected ? "active" : "inactive"} aria-disabled={disabled || void 0} data-disabled={disabled ? "" : void 0} tabIndex={disabled ? void 0 : -1} {...itemProps} ref={composedRefs} onFocus={composeEventHandlers(itemProps.onFocus, () => setIsFocused(true))} onBlur={composeEventHandlers(itemProps.onBlur, () => setIsFocused(false))} onPointerUp={composeEventHandlers(itemProps.onPointerUp, handleSelect)} onPointerMove={composeEventHandlers(itemProps.onPointerMove, (event) => {
175
- if (disabled) {
176
- contentContext.onItemLeave();
177
- } else {
178
- event.currentTarget.focus({ preventScroll: true });
179
- }
180
- })} onPointerLeave={composeEventHandlers(itemProps.onPointerLeave, (event) => {
181
- if (event.currentTarget === document.activeElement) {
182
- contentContext.onItemLeave();
183
- }
184
- })} onKeyDown={composeEventHandlers(itemProps.onKeyDown, (event) => {
185
- const isTypingAhead = contentContext.searchRef.current !== "";
186
- if (isTypingAhead && event.key === " ")
187
- return;
188
- if (SELECTION_KEYS.includes(event.key))
189
- handleSelect();
190
- if (event.key === " ")
191
- event.preventDefault();
192
- })} /></Collection.ItemSlot></SelectItemContextProvider>;
202
+ }, [])}><ListItem backgrounded pressTheme focusTheme componentName={ITEM_NAME} ref={composedRefs} aria-labelledby={textId} aria-selected={isSelected && isFocused} data-state={isSelected ? "active" : "inactive"} aria-disabled={disabled || void 0} data-disabled={disabled ? "" : void 0} tabIndex={disabled ? void 0 : -1} size={context.size} {...itemProps} {...selectItemProps} /></SelectItemContextProvider>;
193
203
  });
194
204
  SelectItem.displayName = ITEM_NAME;
195
205
  const ITEM_TEXT_NAME = "SelectItemText";
206
+ const SelectItemTextFrame = styled(Paragraph, {
207
+ name: ITEM_TEXT_NAME,
208
+ selectable: false
209
+ });
196
210
  const SelectItemText = React.forwardRef((props, forwardedRef) => {
197
- var _a;
198
211
  const { __scopeSelect, className, style, ...itemTextProps } = props;
199
212
  const context = useSelectContext(ITEM_TEXT_NAME, __scopeSelect);
200
- const contentContext = useSelectContentContext(ITEM_TEXT_NAME, __scopeSelect);
201
213
  const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect);
202
214
  const ref = React.useRef(null);
203
- const composedRefs = useComposedRefs(forwardedRef, ref, itemContext.onItemTextChange, itemContext.isSelected ? contentContext.onSelectedItemTextChange : void 0);
215
+ const composedRefs = useComposedRefs(forwardedRef, ref, itemContext.onItemTextChange);
204
216
  return <>
205
- <Primitive.span id={itemContext.textId} {...itemTextProps} ref={composedRefs} />
217
+ <SelectItemTextFrame size={context.size} id={itemContext.textId} {...itemTextProps} ref={composedRefs} />
206
218
  {itemContext.isSelected && context.valueNode && !context.valueNodeHasChildren ? ReactDOM.createPortal(itemTextProps.children, context.valueNode) : null}
207
- {context.bubbleSelect ? ReactDOM.createPortal(<option value={itemContext.value}>{(_a = ref.current) == null ? void 0 : _a.textContent}</option>, context.bubbleSelect) : null}
208
219
  </>;
209
220
  });
210
221
  SelectItemText.displayName = ITEM_TEXT_NAME;
211
222
  const ITEM_INDICATOR_NAME = "SelectItemIndicator";
223
+ const SelectItemIndicatorFrame = styled(XStack, {
224
+ name: ITEM_TEXT_NAME
225
+ });
212
226
  const SelectItemIndicator = React.forwardRef((props, forwardedRef) => {
213
227
  const { __scopeSelect, ...itemIndicatorProps } = props;
214
228
  const itemContext = useSelectItemContext(ITEM_INDICATOR_NAME, __scopeSelect);
215
- return itemContext.isSelected ? <Primitive.span aria-hidden {...itemIndicatorProps} ref={forwardedRef} /> : null;
229
+ return itemContext.isSelected ? <SelectItemIndicatorFrame aria-hidden {...itemIndicatorProps} ref={forwardedRef} /> : null;
216
230
  });
217
231
  SelectItemIndicator.displayName = ITEM_INDICATOR_NAME;
232
+ const GROUP_NAME = "SelectGroup";
233
+ const [SelectGroupContextProvider, useSelectGroupContext] = createSelectContext(GROUP_NAME);
234
+ const SelectGroupFrame = styled(YStack, {
235
+ name: GROUP_NAME,
236
+ width: "100%"
237
+ });
238
+ const SelectGroup = React.forwardRef((props, forwardedRef) => {
239
+ const { __scopeSelect, ...groupProps } = props;
240
+ const groupId = useId();
241
+ return <SelectGroupContextProvider scope={__scopeSelect} id={groupId || ""}><SelectGroupFrame role="group" aria-labelledby={groupId} {...groupProps} ref={forwardedRef} /></SelectGroupContextProvider>;
242
+ });
243
+ SelectGroup.displayName = GROUP_NAME;
244
+ const LABEL_NAME = "SelectLabel";
245
+ const SelectLabel = React.forwardRef((props, forwardedRef) => {
246
+ const { __scopeSelect, ...labelProps } = props;
247
+ const context = useSelectContext(LABEL_NAME, __scopeSelect);
248
+ const groupContext = useSelectGroupContext(LABEL_NAME, __scopeSelect);
249
+ return <ListItem componentName={LABEL_NAME} fontWeight="800" id={groupContext.id} size={context.size} {...labelProps} ref={forwardedRef} />;
250
+ });
251
+ SelectLabel.displayName = LABEL_NAME;
218
252
  const SCROLL_UP_BUTTON_NAME = "SelectScrollUpButton";
219
253
  const SelectScrollUpButton = React.forwardRef((props, forwardedRef) => {
220
- const contentContext = useSelectContentContext(SCROLL_UP_BUTTON_NAME, props.__scopeSelect);
221
- const [canScrollUp, setCanScrollUp] = React.useState(false);
222
- const composedRefs = useComposedRefs(forwardedRef, contentContext.onScrollButtonChange);
223
- return canScrollUp ? <SelectScrollButtonImpl {...props} ref={composedRefs} onAutoScroll={() => {
224
- const { viewport, selectedItem } = contentContext;
225
- if (viewport && selectedItem) {
226
- viewport.scrollTop = viewport.scrollTop - selectedItem.offsetHeight;
227
- }
228
- }} /> : null;
254
+ return <SelectScrollButtonImpl componentName={SCROLL_UP_BUTTON_NAME} {...props} dir="up" ref={forwardedRef} />;
229
255
  });
230
256
  SelectScrollUpButton.displayName = SCROLL_UP_BUTTON_NAME;
231
257
  const SCROLL_DOWN_BUTTON_NAME = "SelectScrollDownButton";
232
258
  const SelectScrollDownButton = React.forwardRef((props, forwardedRef) => {
233
- const contentContext = useSelectContentContext(SCROLL_DOWN_BUTTON_NAME, props.__scopeSelect);
234
- const [canScrollDown, setCanScrollDown] = React.useState(false);
235
- const composedRefs = useComposedRefs(forwardedRef, contentContext.onScrollButtonChange);
236
- return canScrollDown ? <SelectScrollButtonImpl {...props} ref={composedRefs} onAutoScroll={() => {
237
- const { viewport, selectedItem } = contentContext;
238
- if (viewport && selectedItem) {
239
- viewport.scrollTop = viewport.scrollTop + selectedItem.offsetHeight;
240
- }
241
- }} /> : null;
259
+ return <SelectScrollButtonImpl componentName={SCROLL_DOWN_BUTTON_NAME} {...props} dir="down" ref={forwardedRef} />;
242
260
  });
243
261
  SelectScrollDownButton.displayName = SCROLL_DOWN_BUTTON_NAME;
244
262
  const SelectScrollButtonImpl = React.forwardRef((props, forwardedRef) => {
245
- const { __scopeSelect, onAutoScroll, ...scrollIndicatorProps } = props;
246
- const contentContext = useSelectContentContext("SelectScrollButton", __scopeSelect);
247
- const autoScrollTimerRef = React.useRef(null);
263
+ var _a, _b;
264
+ const { __scopeSelect, dir, componentName, ...scrollIndicatorProps } = props;
265
+ const { floatingRef, increaseHeight, forceUpdate, ...context } = useSelectContext(componentName, __scopeSelect);
248
266
  const intervalRef = React.useRef();
249
267
  const loopingRef = React.useRef(false);
268
+ const isVisible = context[dir === "down" ? "canScrollDown" : "canScrollUp"];
250
269
  const { x, y, reference, floating, strategy, update, refs } = useFloating({
251
270
  strategy: "fixed",
252
271
  placement: dir === "up" ? "top" : "bottom",
253
- middleware: [offset(({ floating: floating2 }) => -floating2.height)]
272
+ middleware: [offset(({ rects }) => -rects.floating.height)]
254
273
  });
255
- return <Primitive.div aria-hidden {...scrollIndicatorProps} ref={forwardedRef} style={{ flexShrink: 0, ...scrollIndicatorProps.style }} onPointerMove={composeEventHandlers(scrollIndicatorProps.onPointerMove, () => {
256
- contentContext.onItemLeave();
257
- if (autoScrollTimerRef.current === null) {
258
- autoScrollTimerRef.current = window.setInterval(onAutoScroll, 50);
274
+ const composedRefs = useComposedRefs(forwardedRef, floating);
275
+ React.useLayoutEffect(() => {
276
+ reference(floatingRef.current);
277
+ }, [reference, floatingRef.current]);
278
+ React.useEffect(() => {
279
+ if (!refs.reference.current || !refs.floating.current || !isVisible) {
280
+ return;
259
281
  }
260
- })} onPointerLeave={composeEventHandlers(scrollIndicatorProps.onPointerLeave, () => {
261
- clearAutoScrollTimer();
262
- })} />;
282
+ const cleanup = autoUpdate(refs.reference.current, refs.floating.current, update, {
283
+ animationFrame: true
284
+ });
285
+ return () => {
286
+ clearInterval(intervalRef.current);
287
+ loopingRef.current = false;
288
+ cleanup();
289
+ };
290
+ }, [isVisible, update, refs.floating, refs.reference]);
291
+ if (!isVisible) {
292
+ return null;
293
+ }
294
+ const handleScrollArrowChange = () => {
295
+ const floating2 = floatingRef.current;
296
+ const isUp = dir === "up";
297
+ if (floating2) {
298
+ const value = isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY;
299
+ const multi = isUp && floating2.scrollTop <= SCROLL_ARROW_THRESHOLD * 2 || !isUp && floating2.scrollTop >= floating2.scrollHeight - floating2.clientHeight - SCROLL_ARROW_THRESHOLD * 2 ? 2 : 1;
300
+ floating2.scrollTop += multi * (isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY);
301
+ increaseHeight(floating2, multi === 2 ? value * 2 : value);
302
+ forceUpdate();
303
+ }
304
+ };
305
+ return <YStack ref={composedRefs} componentName={componentName} aria-hidden {...scrollIndicatorProps} zIndex={1e3} position={strategy} left={x || 0} top={y || 0} width={`calc(${((_b = (_a = floatingRef.current) == null ? void 0 : _a.offsetWidth) != null ? _b : 0) - 2}px)`} onPointerMove={() => {
306
+ if (!loopingRef.current) {
307
+ intervalRef.current = setInterval(handleScrollArrowChange, 1e3 / 60);
308
+ loopingRef.current = true;
309
+ }
310
+ }} onPointerLeave={() => {
311
+ loopingRef.current = false;
312
+ clearInterval(intervalRef.current);
313
+ }} />;
263
314
  });
264
- const SEPARATOR_NAME = "SelectSeparator";
265
- const SelectSeparator = Separator;
266
- SelectSeparator.displayName = SEPARATOR_NAME;
267
- const BubbleSelect = React.forwardRef((props, forwardedRef) => {
268
- const { value, ...selectProps } = props;
269
- const ref = React.useRef(null);
270
- const composedRefs = useComposedRefs(forwardedRef, ref);
271
- const prevValue = usePrevious(value);
315
+ const SelectSeparator = styled(Separator, {
316
+ name: "SelectSeparator"
317
+ });
318
+ const Select = withStaticProperties((props) => {
319
+ const {
320
+ id,
321
+ __scopeSelect,
322
+ children,
323
+ open: openProp,
324
+ defaultOpen,
325
+ onOpenChange,
326
+ value: valueProp,
327
+ defaultValue,
328
+ onValueChange,
329
+ size: sizeProp,
330
+ dir,
331
+ name,
332
+ autoComplete
333
+ } = props;
334
+ const [open, setOpen] = useControllableState({
335
+ prop: openProp,
336
+ defaultProp: defaultOpen || false,
337
+ onChange: onOpenChange
338
+ });
339
+ const [value, setValue] = useControllableState({
340
+ prop: valueProp,
341
+ defaultProp: defaultValue || "",
342
+ onChange: onValueChange
343
+ });
344
+ const [activeIndex, setActiveIndex] = React.useState(null);
345
+ const selectedIndexRef = React.useRef(null);
346
+ const activeIndexRef = React.useRef(null);
347
+ const prevActiveIndex = usePrevious(activeIndex);
348
+ const [showArrows, setShowArrows] = React.useState(false);
349
+ const [scrollTop, setScrollTop] = React.useState(0);
350
+ const listItemsRef = React.useRef([]);
351
+ const listContentRef = React.useRef([]);
352
+ const [selectedIndex, setSelectedIndex] = React.useState(Math.max(0, listContentRef.current.indexOf(value)));
353
+ const [controlledScrolling, setControlledScrolling] = React.useState(false);
354
+ const [middlewareType, setMiddlewareType] = React.useState("align");
355
+ useIsomorphicLayoutEffect(() => {
356
+ selectedIndexRef.current = selectedIndex;
357
+ activeIndexRef.current = activeIndex;
358
+ });
272
359
  React.useEffect(() => {
273
- const select = ref.current;
274
- const selectProto = window.HTMLSelectElement.prototype;
275
- const descriptor = Object.getOwnPropertyDescriptor(selectProto, "value");
276
- const setValue = descriptor.set;
277
- if (prevValue !== value && setValue) {
278
- const event = new Event("change", { bubbles: true });
279
- setValue.call(select, value);
280
- select.dispatchEvent(event);
360
+ const frame = requestAnimationFrame(() => {
361
+ setShowArrows(open);
362
+ if (!open) {
363
+ setScrollTop(0);
364
+ setMiddlewareType("align");
365
+ setActiveIndex(null);
366
+ setControlledScrolling(false);
367
+ }
368
+ });
369
+ return () => {
370
+ cancelAnimationFrame(frame);
371
+ };
372
+ }, [open]);
373
+ function getFloatingPadding(floating2) {
374
+ var _a;
375
+ if (!floating2) {
376
+ return 0;
377
+ }
378
+ return Number((_a = getComputedStyle(floating2).paddingLeft) == null ? void 0 : _a.replace("px", ""));
379
+ }
380
+ const { x, y, reference, floating, strategy, context, refs, middlewareData, update } = useFloating({
381
+ open,
382
+ onOpenChange: setOpen,
383
+ placement: "bottom",
384
+ middleware: middlewareType === "align" ? [
385
+ offset(({ rects }) => {
386
+ var _a;
387
+ const index = (_a = activeIndexRef.current) != null ? _a : selectedIndexRef.current;
388
+ if (index == null) {
389
+ return 0;
390
+ }
391
+ const item = listItemsRef.current[index];
392
+ if (item == null) {
393
+ return 0;
394
+ }
395
+ const offsetTop = item.offsetTop;
396
+ const itemHeight = item.offsetHeight;
397
+ const height = rects.reference.height;
398
+ return -offsetTop - height - (itemHeight - height) / 2;
399
+ }),
400
+ {
401
+ name: "size",
402
+ async fn(args) {
403
+ var _a;
404
+ const {
405
+ elements: { floating: floating2 },
406
+ rects: { reference: reference2 },
407
+ middlewareData: middlewareData2
408
+ } = args;
409
+ const overflow = await detectOverflow(args, {
410
+ padding: WINDOW_PADDING
411
+ });
412
+ const top = Math.max(0, overflow.top);
413
+ const bottom = Math.max(0, overflow.bottom);
414
+ const nextY = args.y + top;
415
+ if ((_a = middlewareData2.size) == null ? void 0 : _a.skip) {
416
+ return {
417
+ y: nextY,
418
+ data: {
419
+ y: middlewareData2.size.y
420
+ }
421
+ };
422
+ }
423
+ Object.assign(floating2.style, {
424
+ maxHeight: `${floating2.scrollHeight - Math.abs(top + bottom)}px`,
425
+ minWidth: `${reference2.width + getFloatingPadding(floating2) * 2}px`
426
+ });
427
+ return {
428
+ y: nextY,
429
+ data: {
430
+ y: top,
431
+ skip: true
432
+ },
433
+ reset: {
434
+ rects: true
435
+ }
436
+ };
437
+ }
438
+ }
439
+ ] : [
440
+ offset(5),
441
+ flip(),
442
+ size({
443
+ apply({ rects, availableHeight, elements }) {
444
+ Object.assign(elements.floating.style, {
445
+ width: `${rects.reference.width}px`,
446
+ maxHeight: `${availableHeight}px`
447
+ });
448
+ },
449
+ padding: WINDOW_PADDING
450
+ })
451
+ ]
452
+ });
453
+ const floatingRef = refs.floating;
454
+ const forceUpdate = React.useReducer(() => ({}), {})[1];
455
+ const showUpArrow = showArrows && scrollTop > SCROLL_ARROW_THRESHOLD;
456
+ const showDownArrow = showArrows && floatingRef.current && scrollTop < floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD;
457
+ const interactions = useInteractions([
458
+ useClick(context, { pointerDown: true }),
459
+ useRole(context, { role: "listbox" }),
460
+ useDismiss(context),
461
+ useListNavigation(context, {
462
+ listRef: listItemsRef,
463
+ activeIndex,
464
+ selectedIndex,
465
+ onNavigate: setActiveIndex
466
+ }),
467
+ useTypeahead(context, {
468
+ listRef: listContentRef,
469
+ onMatch: open ? setActiveIndex : setSelectedIndex,
470
+ selectedIndex,
471
+ activeIndex
472
+ })
473
+ ]);
474
+ const increaseHeight = React.useCallback((floating2, amount = 0) => {
475
+ var _a;
476
+ if (middlewareType === "fallback") {
477
+ return;
478
+ }
479
+ const currentMaxHeight = Number(floating2.style.maxHeight.replace("px", ""));
480
+ const currentTop = Number(floating2.style.top.replace("px", ""));
481
+ const rect = floating2.getBoundingClientRect();
482
+ const rectTop = rect.top;
483
+ const rectBottom = rect.bottom;
484
+ const viewportHeight = (_a = visualViewport == null ? void 0 : visualViewport.height) != null ? _a : 0;
485
+ const visualMaxHeight = viewportHeight - WINDOW_PADDING * 2;
486
+ if (amount < 0 && selectedIndexRef.current != null && Math.round(rectBottom) < Math.round(viewportHeight + getVisualOffsetTop() - WINDOW_PADDING)) {
487
+ floating2.style.maxHeight = `${Math.min(visualMaxHeight, currentMaxHeight - amount)}px`;
488
+ }
489
+ if (amount > 0 && Math.round(rectTop) > Math.round(WINDOW_PADDING - getVisualOffsetTop()) && floating2.scrollHeight > floating2.offsetHeight) {
490
+ const nextTop = Math.max(WINDOW_PADDING + getVisualOffsetTop(), currentTop - amount);
491
+ const nextMaxHeight = Math.min(visualMaxHeight, currentMaxHeight + amount);
492
+ Object.assign(floating2.style, {
493
+ maxHeight: `${nextMaxHeight}px`,
494
+ top: `${nextTop}px`
495
+ });
496
+ if (nextTop - WINDOW_PADDING > getVisualOffsetTop()) {
497
+ floating2.scrollTop -= nextMaxHeight - currentMaxHeight + getFloatingPadding(floating2);
498
+ }
499
+ return currentTop - nextTop;
500
+ }
501
+ }, [middlewareType]);
502
+ const touchPageYRef = React.useRef(null);
503
+ const handleWheel = React.useCallback((event) => {
504
+ var _a, _b, _c;
505
+ const pinching = event.ctrlKey;
506
+ const currentTarget = event.currentTarget;
507
+ function isWheelEvent(event2) {
508
+ return typeof event2.deltaY === "number";
509
+ }
510
+ function isTouchEvent(event2) {
511
+ return event2.touches != null;
512
+ }
513
+ if (Math.abs(((_a = currentTarget == null ? void 0 : currentTarget.offsetHeight) != null ? _a : 0) - (((_b = visualViewport == null ? void 0 : visualViewport.height) != null ? _b : 0) - WINDOW_PADDING * 2)) > 1 && !pinching) {
514
+ event.preventDefault();
515
+ } else if (isWheelEvent(event) && isFirefox) {
516
+ currentTarget.scrollTop += event.deltaY;
517
+ }
518
+ if (!pinching) {
519
+ let delta = 5;
520
+ if (isTouchEvent(event)) {
521
+ const currentPageY = touchPageYRef.current;
522
+ const pageY = (_c = event.touches[0]) == null ? void 0 : _c.pageY;
523
+ if (pageY != null) {
524
+ touchPageYRef.current = pageY;
525
+ if (currentPageY != null) {
526
+ delta = currentPageY - pageY;
527
+ }
528
+ }
529
+ }
530
+ increaseHeight(currentTarget, isWheelEvent(event) ? event.deltaY : delta);
531
+ setScrollTop(currentTarget.scrollTop);
532
+ forceUpdate();
281
533
  }
282
- }, [prevValue, value]);
283
- return null;
284
- return <VisuallyHidden asChild><select {...selectProps} ref={composedRefs} defaultValue={value} /></VisuallyHidden>;
534
+ }, [increaseHeight, forceUpdate]);
535
+ React.useEffect(() => {
536
+ function onTouchEnd() {
537
+ touchPageYRef.current = null;
538
+ }
539
+ const floating2 = floatingRef.current;
540
+ if (open && floating2 && middlewareType === "align") {
541
+ floating2.addEventListener("wheel", handleWheel);
542
+ floating2.addEventListener("touchmove", handleWheel);
543
+ floating2.addEventListener("touchend", onTouchEnd, { passive: true });
544
+ return () => {
545
+ floating2.removeEventListener("wheel", handleWheel);
546
+ floating2.removeEventListener("touchmove", handleWheel);
547
+ floating2.removeEventListener("touchend", onTouchEnd);
548
+ };
549
+ }
550
+ }, [open, floatingRef, handleWheel, middlewareType]);
551
+ React.useEffect(() => {
552
+ window.addEventListener("resize", update);
553
+ return () => {
554
+ window.removeEventListener("resize", update);
555
+ };
556
+ }, [update]);
557
+ React.useLayoutEffect(() => {
558
+ var _a, _b, _c;
559
+ const floating2 = floatingRef.current;
560
+ if (open && controlledScrolling && floating2) {
561
+ const item = activeIndex != null ? listItemsRef.current[activeIndex] : selectedIndex != null ? listItemsRef.current[selectedIndex] : null;
562
+ if (item && prevActiveIndex != null) {
563
+ const itemHeight = (_b = (_a = listItemsRef.current[prevActiveIndex]) == null ? void 0 : _a.offsetHeight) != null ? _b : 0;
564
+ const floatingHeight = floating2.offsetHeight;
565
+ const top = item.offsetTop;
566
+ const bottom = top + itemHeight;
567
+ if (top < floating2.scrollTop + 20) {
568
+ const diff = floating2.scrollTop - top + 20;
569
+ floating2.scrollTop -= diff;
570
+ if (activeIndex != selectedIndex && activeIndex != null) {
571
+ increaseHeight(floating2, -diff);
572
+ }
573
+ } else if (bottom > floatingHeight + floating2.scrollTop - 20) {
574
+ const diff = bottom - floatingHeight - floating2.scrollTop + 20;
575
+ floating2.scrollTop += diff;
576
+ if (activeIndex != selectedIndex && activeIndex != null) {
577
+ floating2.scrollTop -= (_c = increaseHeight(floating2, diff)) != null ? _c : 0;
578
+ }
579
+ }
580
+ }
581
+ }
582
+ }, [
583
+ open,
584
+ controlledScrolling,
585
+ prevActiveIndex,
586
+ activeIndex,
587
+ selectedIndex,
588
+ floatingRef,
589
+ increaseHeight
590
+ ]);
591
+ React.useLayoutEffect(() => {
592
+ var _a, _b;
593
+ const floating2 = refs.floating.current;
594
+ const reference2 = refs.reference.current;
595
+ if (open && floating2 && reference2 && floating2.offsetHeight < floating2.scrollHeight) {
596
+ const referenceRect = reference2.getBoundingClientRect();
597
+ if (middlewareType === "fallback") {
598
+ const item = listItemsRef.current[selectedIndex];
599
+ if (item) {
600
+ floating2.scrollTop = item.offsetTop - floating2.clientHeight + referenceRect.height;
601
+ }
602
+ return;
603
+ }
604
+ floating2.scrollTop = (_a = middlewareData.size) == null ? void 0 : _a.y;
605
+ const closeToBottom = ((_b = visualViewport == null ? void 0 : visualViewport.height) != null ? _b : 0) + getVisualOffsetTop() - referenceRect.bottom < FALLBACK_THRESHOLD;
606
+ const closeToTop = referenceRect.top < FALLBACK_THRESHOLD;
607
+ if (floating2.offsetHeight < MIN_HEIGHT || closeToTop || closeToBottom) {
608
+ setMiddlewareType("fallback");
609
+ }
610
+ }
611
+ }, [
612
+ open,
613
+ increaseHeight,
614
+ selectedIndex,
615
+ middlewareType,
616
+ refs.floating,
617
+ refs.reference,
618
+ middlewareData
619
+ ]);
620
+ React.useLayoutEffect(() => {
621
+ if (open && selectedIndex != null) {
622
+ requestAnimationFrame(() => {
623
+ var _a;
624
+ (_a = listItemsRef.current[selectedIndex]) == null ? void 0 : _a.focus({ preventScroll: true });
625
+ });
626
+ }
627
+ }, [listItemsRef, selectedIndex, open]);
628
+ React.useEffect(() => {
629
+ const frame = requestAnimationFrame(() => {
630
+ setShowArrows(open);
631
+ if (!open) {
632
+ setScrollTop(0);
633
+ setMiddlewareType("align");
634
+ setActiveIndex(null);
635
+ setControlledScrolling(false);
636
+ }
637
+ });
638
+ return () => cancelAnimationFrame(frame);
639
+ }, [open]);
640
+ const [valueNode, setValueNode] = React.useState(null);
641
+ const [valueNodeHasChildren, setValueNodeHasChildren] = React.useState(false);
642
+ return <SelectProvider size={sizeProp} scope={__scopeSelect} increaseHeight={increaseHeight} forceUpdate={forceUpdate} floatingRef={floatingRef} valueNode={valueNode} onValueNodeChange={setValueNode} valueNodeHasChildren={valueNodeHasChildren} onValueNodeHasChildrenChange={setValueNodeHasChildren} setValueAtIndex={(index, value2) => {
643
+ listContentRef.current[index] = value2;
644
+ }} interactions={{
645
+ ...interactions,
646
+ getReferenceProps() {
647
+ return interactions.getReferenceProps({
648
+ ref: reference,
649
+ className: "SelectTrigger",
650
+ onKeyDown(event) {
651
+ if (event.key === "Enter" || event.key === " " && !context.dataRef.current.typing) {
652
+ event.preventDefault();
653
+ setOpen(true);
654
+ }
655
+ }
656
+ });
657
+ },
658
+ getFloatingProps(props2) {
659
+ return interactions.getFloatingProps({
660
+ ref: floating,
661
+ className: "Select",
662
+ ...props2,
663
+ style: {
664
+ position: strategy,
665
+ top: y != null ? y : "",
666
+ left: x != null ? x : "",
667
+ outline: 0,
668
+ listStyleType: "none",
669
+ scrollbarWidth: "none",
670
+ userSelect: "none",
671
+ ...props2 == null ? void 0 : props2.style
672
+ },
673
+ onPointerEnter() {
674
+ setControlledScrolling(false);
675
+ },
676
+ onPointerMove() {
677
+ setControlledScrolling(false);
678
+ },
679
+ onKeyDown() {
680
+ setControlledScrolling(true);
681
+ },
682
+ onScroll(event) {
683
+ setScrollTop(event.currentTarget.scrollTop);
684
+ }
685
+ });
686
+ }
687
+ }} floatingContext={context} activeIndex={activeIndex} canScrollDown={!!showDownArrow} canScrollUp={!!showUpArrow} controlledScrolling dataRef={context.dataRef} listRef={listItemsRef} onChange={setValue} selectedIndex={selectedIndex} setActiveIndex={setActiveIndex} setOpen={setOpen} setSelectedIndex={setSelectedIndex} value={value} open={open}>{children}</SelectProvider>;
688
+ }, {
689
+ Content: SelectContent,
690
+ Group: SelectGroup,
691
+ Icon: SelectIcon,
692
+ Item: SelectItem,
693
+ ItemIndicator: SelectItemIndicator,
694
+ ItemText: SelectItemText,
695
+ Label: SelectLabel,
696
+ ScrollDownButton: SelectScrollDownButton,
697
+ ScrollUpButton: SelectScrollUpButton,
698
+ Trigger: SelectTrigger,
699
+ Value: SelectValue,
700
+ Viewport: SelectViewport
285
701
  });
702
+ Select.displayName = SELECT_NAME;
286
703
  export {
287
704
  Select,
288
- SelectContent,
289
- SelectGroup,
705
+ SelectGroupFrame,
290
706
  SelectIcon,
291
707
  SelectItem,
292
- SelectItemIndicator,
293
- SelectItemText,
294
- SelectLabel,
295
- SelectScrollDownButton,
296
- SelectScrollUpButton,
708
+ SelectItemTextFrame,
297
709
  SelectSeparator,
298
710
  SelectTrigger,
299
- SelectValue,
711
+ SelectViewport,
712
+ SelectViewportFrame,
300
713
  createSelectScope
301
714
  };
715
+ //# sourceMappingURL=Select.js.map