@mirohq/design-system-combobox 0.1.0-combobox.1 → 0.1.0-combobox.10

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.
package/dist/module.js CHANGED
@@ -1,30 +1,98 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
2
- import React, { createContext, useState, useContext, useEffect } from 'react';
3
- import { useFormFieldContext } from '@mirohq/design-system-base-form';
4
- import { useHover } from '@react-aria/interactions';
5
- import { stringAttrValue, booleanishAttrValue, booleanify } from '@mirohq/design-system-utils';
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import React, { createContext, useRef, useState, useContext, useCallback, useEffect, useMemo } from 'react';
3
+ import { Combobox as Combobox$1, ComboboxItem, ComboboxItemCheck, ComboboxList, Group as Group$1, GroupLabel as GroupLabel$1, ComboboxProvider as ComboboxProvider$1 } from '@ariakit/react';
4
+ import { useFormFieldContext, FloatingLabel } from '@mirohq/design-system-base-form';
5
+ import { booleanishAttrValue, mergeRefs, booleanify } from '@mirohq/design-system-utils';
6
+ import * as RadixPopover from '@radix-ui/react-popover';
7
+ import { Trigger as Trigger$1, Anchor, Portal as Portal$1 } from '@radix-ui/react-popover';
8
+ import { styled, theme } from '@mirohq/design-system-stitches';
6
9
  import { Input } from '@mirohq/design-system-input';
7
- import { IconChevronDown, IconCheckMark } from '@mirohq/design-system-icons';
8
- import { styled } from '@mirohq/design-system-stitches';
10
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
11
+ import { IconChevronDown, IconCross, IconCheckMark } from '@mirohq/design-system-icons';
12
+ import { ScrollArea } from '@mirohq/design-system-scroll-area';
9
13
  import { Primitive } from '@mirohq/design-system-primitive';
10
- import { Portal as Portal$1 } from '@radix-ui/react-popover';
11
- import { Group as Group$1, GroupLabel as GroupLabel$1 } from '@ariakit/react';
14
+ import { mergeProps } from '@react-aria/utils';
15
+ import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
16
+ import { useLayoutEffect } from '@mirohq/design-system-use-layout-effect';
17
+ import { focus } from '@mirohq/design-system-styles';
18
+ import { createPortal } from 'react-dom';
19
+ import { BaseButton } from '@mirohq/design-system-base-button';
12
20
 
13
- const StyledTrigger = styled(Input, {});
21
+ const StyledInput = styled(Input, {
22
+ flexWrap: "wrap",
23
+ flexGrow: 1,
24
+ gap: "$50",
25
+ overflowY: "scroll",
26
+ "&[data-valid], &[data-invalid]": {
27
+ // we don't need a bigger padding here as Input component will render its own icon
28
+ paddingRight: "$100"
29
+ },
30
+ "& input": {
31
+ minWidth: "$8",
32
+ flexBasis: 0,
33
+ flexGrow: 1
34
+ },
35
+ variants: {
36
+ size: {
37
+ large: {
38
+ minHeight: "$10",
39
+ height: "auto",
40
+ padding: "5px $100",
41
+ paddingRight: "$500"
42
+ },
43
+ "x-large": {
44
+ minHeight: "$12",
45
+ height: "auto",
46
+ padding: "5px $100",
47
+ paddingRight: "$500"
48
+ }
49
+ }
50
+ },
51
+ defaultVariants: {
52
+ size: "large"
53
+ }
54
+ });
14
55
 
15
56
  const ComboboxContext = createContext({});
16
57
  const ComboboxProvider = ({
17
58
  children,
59
+ open: openProp,
18
60
  defaultOpen,
61
+ onOpen,
62
+ onClose,
19
63
  valid,
20
64
  value: valueProp,
21
65
  defaultValue: defaultValueProp,
66
+ onValueChange,
67
+ onSearchValueChange,
68
+ autoFilter = true,
22
69
  ...restProps
23
70
  }) => {
24
- const [openState, setOpenState] = useState(defaultOpen);
25
- const [value, setValue] = useState(valueProp);
71
+ const triggerRef = useRef(null);
72
+ const inputRef = useRef(null);
73
+ const contentRef = useRef(null);
26
74
  const [defaultValue, setDefaultValue] = useState(defaultValueProp);
27
- const [searchValue, setSearchValue] = useState();
75
+ const [openState = false, setOpenState] = useControllableState({
76
+ prop: openProp,
77
+ defaultProp: defaultOpen,
78
+ onChange: (state) => {
79
+ if (state) {
80
+ onOpen == null ? void 0 : onOpen();
81
+ } else {
82
+ onClose == null ? void 0 : onClose();
83
+ }
84
+ }
85
+ });
86
+ const [value, setValue] = useControllableState({
87
+ prop: valueProp,
88
+ defaultProp: defaultValueProp,
89
+ onChange: onValueChange
90
+ });
91
+ const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
92
+ const [searchValue, setSearchValue] = useState("");
93
+ const [size, setSize] = useState();
94
+ const [placeholder, setPlaceholder] = useState();
95
+ const [itemValueTextMap, setItemValueTextMap] = useState(/* @__PURE__ */ new Map());
28
96
  const { valid: formFieldValid } = useFormFieldContext();
29
97
  return /* @__PURE__ */ jsx(
30
98
  ComboboxContext.Provider,
@@ -38,8 +106,21 @@ const ComboboxProvider = ({
38
106
  setValue,
39
107
  setDefaultValue,
40
108
  defaultValue,
109
+ onSearchValueChange,
110
+ triggerRef,
111
+ inputRef,
112
+ contentRef,
113
+ autoFilter,
41
114
  searchValue,
42
- setSearchValue
115
+ setSearchValue,
116
+ filteredItems,
117
+ setFilteredItems,
118
+ itemValueTextMap,
119
+ setItemValueTextMap,
120
+ placeholder,
121
+ setPlaceholder,
122
+ size,
123
+ setSize
43
124
  },
44
125
  children
45
126
  }
@@ -47,6 +128,70 @@ const ComboboxProvider = ({
47
128
  };
48
129
  const useComboboxContext = () => useContext(ComboboxContext);
49
130
 
131
+ const StyledActionButton = styled(Input.ActionButton, {
132
+ position: "absolute",
133
+ right: "$100",
134
+ variants: {
135
+ size: {
136
+ large: {
137
+ top: "5px"
138
+ },
139
+ "x-large": {
140
+ top: "9px"
141
+ }
142
+ }
143
+ },
144
+ defaultVariants: {
145
+ size: "large"
146
+ }
147
+ });
148
+
149
+ const TriggerActionButton = ({
150
+ openActionLabel,
151
+ closeActionLabel,
152
+ clearActionLabel,
153
+ size
154
+ }) => {
155
+ const { openState, setOpenState, value = [], setValue } = useComboboxContext();
156
+ const isEmpty = value.length === 0;
157
+ const onToggleClick = useCallback(
158
+ (event) => {
159
+ if (openState) {
160
+ setOpenState(false);
161
+ }
162
+ event.stopPropagation();
163
+ },
164
+ [setOpenState, openState]
165
+ );
166
+ const onClearClick = useCallback(
167
+ (event) => {
168
+ setValue([]);
169
+ event.stopPropagation();
170
+ },
171
+ [setValue]
172
+ );
173
+ if (isEmpty) {
174
+ return /* @__PURE__ */ jsx(Trigger$1, { asChild: true, "aria-haspopup": "listbox", children: /* @__PURE__ */ jsx(
175
+ StyledActionButton,
176
+ {
177
+ label: openState ? closeActionLabel : openActionLabel,
178
+ size,
179
+ onClick: onToggleClick,
180
+ children: /* @__PURE__ */ jsx(IconChevronDown, { size: "small", weight: "thin" })
181
+ }
182
+ ) });
183
+ }
184
+ return /* @__PURE__ */ jsx(
185
+ StyledActionButton,
186
+ {
187
+ label: clearActionLabel,
188
+ size,
189
+ onClick: onClearClick,
190
+ children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin" })
191
+ }
192
+ );
193
+ };
194
+
50
195
  const Trigger = React.forwardRef(
51
196
  ({
52
197
  id,
@@ -55,142 +200,536 @@ const Trigger = React.forwardRef(
55
200
  "aria-describedby": ariaDescribedBy,
56
201
  "aria-invalid": ariaInvalid,
57
202
  placeholder,
58
- onHoverChange,
59
- onHoverStart,
60
- onHoverEnd,
203
+ openActionLabel,
204
+ closeActionLabel,
205
+ clearActionLabel,
206
+ onChange,
61
207
  ...restProps
62
208
  }, forwardRef) => {
63
209
  const {
64
210
  "aria-disabled": ariaDisabled,
65
211
  valid: comboboxValid,
66
212
  disabled,
67
- value
213
+ value = [],
214
+ readOnly,
215
+ triggerRef,
216
+ inputRef,
217
+ onSearchValueChange,
218
+ searchValue,
219
+ setSearchValue,
220
+ setOpenState,
221
+ setSize,
222
+ setPlaceholder
68
223
  } = useComboboxContext();
69
224
  const {
70
225
  formElementId,
71
- ariaDescribedBy: formFieldContextDescribedBy,
72
226
  ariaInvalid: formFieldAriaInvalid,
73
227
  valid: formFieldValid
74
228
  } = useFormFieldContext();
229
+ useEffect(() => {
230
+ setSize(size);
231
+ }, [size, setSize]);
232
+ useEffect(() => {
233
+ setPlaceholder(placeholder);
234
+ }, [setPlaceholder, placeholder]);
75
235
  const valid = formFieldValid != null ? formFieldValid : comboboxValid;
76
- const { hoverProps } = useHover({
77
- onHoverStart,
78
- onHoverEnd,
79
- onHoverChange: (isHovering) => {
80
- onHoverChange == null ? void 0 : onHoverChange(isHovering);
81
- }
82
- });
83
- const commonProps = {
236
+ const inputProps = {
84
237
  ...restProps,
85
- ref: forwardRef,
86
238
  "aria-disabled": ariaDisabled,
87
239
  "aria-invalid": ariaInvalid != null ? ariaInvalid : formFieldAriaInvalid,
88
- "aria-describedby": stringAttrValue(
89
- ariaDescribedBy,
90
- formFieldContextDescribedBy
91
- ),
240
+ // todo MDS-1011: use formFieldContextDescribedBy after removing form context from BaseInput
241
+ "aria-describedby": ariaDescribedBy,
92
242
  valid,
93
243
  disabled,
244
+ readOnly,
94
245
  invalid: booleanishAttrValue(valid),
95
246
  id: id != null ? id : formElementId,
96
- placeholder: value === void 0 ? placeholder : void 0
247
+ placeholder: value.length === 0 ? placeholder : void 0
248
+ };
249
+ const scrollIntoView = (event) => {
250
+ var _a;
251
+ const trigger = triggerRef == null ? void 0 : triggerRef.current;
252
+ const baseInput = (_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement;
253
+ if (baseInput != null && trigger != null) {
254
+ event.preventDefault();
255
+ baseInput.scrollTo({
256
+ top: trigger.scrollHeight
257
+ });
258
+ }
259
+ if (restProps.onFocus !== void 0) {
260
+ restProps.onFocus(event);
261
+ }
97
262
  };
98
- const variants = {
99
- size
263
+ const onInputChange = (e) => {
264
+ setSearchValue(e.target.value);
265
+ onSearchValueChange == null ? void 0 : onSearchValueChange(e.target.value);
266
+ onChange == null ? void 0 : onChange(e);
100
267
  };
101
- return /* @__PURE__ */ jsxs(StyledTrigger, { ...hoverProps, ...commonProps, ...variants, children: [
102
- children,
103
- /* @__PURE__ */ jsx(Input.ActionButton, { label: "custom label", children: /* @__PURE__ */ jsx(IconChevronDown, { size: "small", weight: "thin" }) })
104
- ] });
268
+ return /* @__PURE__ */ jsx(
269
+ Anchor,
270
+ {
271
+ ref: mergeRefs([triggerRef, forwardRef]),
272
+ onClick: () => {
273
+ if (!booleanify(disabled) && !booleanify(ariaDisabled) && !booleanify(readOnly)) {
274
+ setOpenState(true);
275
+ }
276
+ },
277
+ children: /* @__PURE__ */ jsx(
278
+ Combobox$1,
279
+ {
280
+ render: /* @__PURE__ */ jsxs(
281
+ StyledInput,
282
+ {
283
+ ...inputProps,
284
+ value: searchValue,
285
+ size,
286
+ ref: inputRef,
287
+ onChange: onInputChange,
288
+ onFocus: scrollIntoView,
289
+ children: [
290
+ children,
291
+ /* @__PURE__ */ jsx(
292
+ TriggerActionButton,
293
+ {
294
+ openActionLabel,
295
+ closeActionLabel,
296
+ clearActionLabel,
297
+ size
298
+ }
299
+ )
300
+ ]
301
+ }
302
+ )
303
+ }
304
+ )
305
+ }
306
+ );
105
307
  }
106
308
  );
107
309
 
108
- const StyledContent = styled(Primitive.div, {
310
+ const NoResultPlaceholder = styled(Primitive.div, {
311
+ padding: "$100"
312
+ });
313
+ const StyledContent = styled(RadixPopover.Content, {
109
314
  backgroundColor: "$background-neutrals-container",
110
315
  borderRadius: "$50",
111
316
  boxShadow: "$50",
112
- minWidth: "var(--radix-select-trigger-width)",
317
+ fontSize: "$175",
318
+ fontWeight: "normal",
319
+ lineHeight: "20px",
320
+ width: "var(--radix-popover-trigger-width)",
321
+ zIndex: "$select",
322
+ overflowY: "auto",
113
323
  padding: "$50",
114
- zIndex: "$select"
324
+ boxSizing: "border-box",
325
+ outline: "1px solid transparent"
115
326
  });
116
327
 
117
- const Content = React.forwardRef(({ children, ...restProps }, forwardRef) => {
118
- const { open, openState } = useComboboxContext();
119
- if (!booleanify(open != null ? open : openState))
120
- return null;
121
- return /* @__PURE__ */ jsx(StyledContent, { ...restProps, ref: forwardRef, children });
328
+ const StyledItemCheck = styled(Primitive.span, {
329
+ color: "$icon-primary"
122
330
  });
123
-
124
- const StyledItem = styled(Primitive.div, {
331
+ const StyledItem = styled(ComboboxItem, {
332
+ display: "grid",
333
+ gridTemplateColumns: "20px 1fr",
125
334
  borderRadius: "$50",
126
335
  boxSizing: "border-box",
127
336
  color: "$text-neutrals",
128
337
  cursor: "pointer",
129
338
  fontSize: "$175",
130
- lineHeight: 1.5,
339
+ lineHeight: "20px",
131
340
  position: "relative",
132
341
  userSelect: "none",
133
- padding: "6px 0",
134
- paddingInline: "$150 $100",
135
- '&:hover:not([aria-disabled="true"])': {
136
- background: "$background-primary-subtle-hover",
137
- color: "$text-primary-hover"
342
+ padding: "6px $100 6px $150",
343
+ ...focus.css({
344
+ boxShadow: "$focus-small"
345
+ }),
346
+ '&:not([aria-disabled="true"])': {
347
+ _hover: {
348
+ background: "$background-primary-subtle-hover",
349
+ color: "$text-primary-hover",
350
+ ["".concat(StyledItemCheck)]: {
351
+ color: "$icon-primary-hover"
352
+ }
353
+ }
354
+ },
355
+ "&:disabled, &[aria-disabled=true], &[data-disabled]": {
356
+ cursor: "default",
357
+ color: "$text-neutrals-disabled",
358
+ ["".concat(StyledItemCheck)]: {
359
+ color: "$icon-neutrals-disabled"
360
+ }
361
+ },
362
+ '&[aria-selected="true"]:not(:disabled,[aria-disabled=true],[data-disabled])': {
363
+ color: "$text-primary-selected"
138
364
  }
139
365
  });
140
366
 
141
367
  const Item = React.forwardRef(
142
- ({ value, textValue, children, ...restProps }, forwardRef) => {
143
- const { value: comboboxValue } = useComboboxContext();
144
- const isSelected = comboboxValue === value;
145
- return /* @__PURE__ */ jsxs(StyledItem, { ref: forwardRef, ...restProps, children: [
146
- isSelected && /* @__PURE__ */ jsx(IconCheckMark, { size: "small", weight: "thin" }),
147
- children
148
- ] });
368
+ ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
369
+ const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
370
+ const {
371
+ autoFilter,
372
+ filteredItems,
373
+ setItemValueTextMap,
374
+ triggerRef,
375
+ inputRef,
376
+ value: comboboxValue = []
377
+ } = useComboboxContext();
378
+ useLayoutEffect(() => {
379
+ const textToSet = textValue !== void 0 ? textValue : typeof children === "string" ? children : "";
380
+ setItemValueTextMap((prevState) => new Map(prevState.set(value, textToSet)));
381
+ return () => {
382
+ setItemValueTextMap((prevState) => {
383
+ prevState.delete(value);
384
+ return new Map(prevState);
385
+ });
386
+ };
387
+ }, [setItemValueTextMap, value, textValue, children]);
388
+ if (autoFilter !== false && !filteredItems.has(value)) {
389
+ return null;
390
+ }
391
+ const scrollIntoView = (event) => {
392
+ var _a;
393
+ if (((_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement) != null && (triggerRef == null ? void 0 : triggerRef.current) != null) {
394
+ inputRef.current.parentElement.scrollTo({
395
+ top: triggerRef.current.scrollHeight
396
+ });
397
+ }
398
+ if (restProps.onClick !== void 0) {
399
+ restProps.onClick(event);
400
+ }
401
+ };
402
+ const isSelected = comboboxValue.includes(value);
403
+ return /* @__PURE__ */ jsxs(
404
+ StyledItem,
405
+ {
406
+ ...mergeProps(restProps, restAriaDisabledProps),
407
+ focusable: true,
408
+ hideOnClick: false,
409
+ accessibleWhenDisabled: booleanify(ariaDisabled),
410
+ disabled: booleanify(ariaDisabled) || disabled,
411
+ ref: forwardRef,
412
+ value,
413
+ onClick: scrollIntoView,
414
+ "aria-selected": isSelected,
415
+ children: [
416
+ /* @__PURE__ */ jsx(
417
+ ComboboxItemCheck,
418
+ {
419
+ checked: isSelected,
420
+ render: ({ style, ...props }) => (
421
+ // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
422
+ /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
423
+ ),
424
+ children: /* @__PURE__ */ jsx(
425
+ IconCheckMark,
426
+ {
427
+ size: "small",
428
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-item-check" : void 0
429
+ }
430
+ )
431
+ }
432
+ ),
433
+ children
434
+ ]
435
+ }
436
+ );
437
+ }
438
+ );
439
+
440
+ const itemType = React.createElement(Item).type;
441
+ const getChildrenItemValues = (componentChildren) => {
442
+ const values = [];
443
+ const recurse = (children) => {
444
+ React.Children.forEach(children, (child) => {
445
+ if (!React.isValidElement(child)) {
446
+ return;
447
+ }
448
+ if (child.type === itemType) {
449
+ const props = child.props;
450
+ values.push(props.value);
451
+ return;
452
+ }
453
+ if (child.props.children) {
454
+ recurse(child.props.children);
455
+ }
456
+ });
457
+ };
458
+ recurse(componentChildren);
459
+ return values;
460
+ };
461
+
462
+ const useDocumentFragment = () => {
463
+ const [fragment, setFragment] = React.useState();
464
+ useLayoutEffect(() => {
465
+ setFragment(new DocumentFragment());
466
+ }, []);
467
+ return fragment;
468
+ };
469
+
470
+ const useInvisibleContent = () => {
471
+ const fragment = useDocumentFragment();
472
+ return useCallback(
473
+ (children) => fragment !== void 0 ? createPortal(/* @__PURE__ */ jsx("div", { children }), fragment) : null,
474
+ [fragment]
475
+ );
476
+ };
477
+
478
+ const CONTENT_OFFSET = parseInt(theme.space[50]);
479
+ const isInsideRef = (element, ref) => {
480
+ var _a, _b;
481
+ return (_b = element != null && ((_a = ref.current) == null ? void 0 : _a.contains(element))) != null ? _b : false;
482
+ };
483
+ const Content = React.forwardRef(
484
+ ({
485
+ side = "bottom",
486
+ sideOffset = CONTENT_OFFSET,
487
+ align = "center",
488
+ alignOffset = 0,
489
+ collisionPadding = 0,
490
+ avoidCollisions = true,
491
+ sticky = "partial",
492
+ hideWhenDetached = true,
493
+ overflow = "visible",
494
+ maxHeight,
495
+ children,
496
+ ...restProps
497
+ }, forwardRef) => {
498
+ const {
499
+ triggerRef,
500
+ contentRef,
501
+ autoFilter,
502
+ filteredItems,
503
+ setFilteredItems,
504
+ searchValue,
505
+ noResultsText,
506
+ direction,
507
+ openState
508
+ } = useComboboxContext();
509
+ useEffect(() => {
510
+ const childrenItemValues = getChildrenItemValues(children);
511
+ setFilteredItems(
512
+ new Set(
513
+ autoFilter === false ? childrenItemValues : childrenItemValues.filter(
514
+ (child) => child.toLowerCase().includes(searchValue.toLowerCase())
515
+ )
516
+ )
517
+ );
518
+ }, [children, autoFilter, setFilteredItems, searchValue]);
519
+ const getInvisibleContent = useInvisibleContent();
520
+ if (!openState) {
521
+ return getInvisibleContent(children);
522
+ }
523
+ const content = filteredItems.size === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
524
+ /* @__PURE__ */ jsx(NoResultPlaceholder, { children: noResultsText }),
525
+ getInvisibleContent(children)
526
+ ] }) : children;
527
+ return /* @__PURE__ */ jsx(
528
+ StyledContent,
529
+ {
530
+ asChild: true,
531
+ ...restProps,
532
+ dir: direction,
533
+ side,
534
+ sideOffset,
535
+ align,
536
+ alignOffset,
537
+ avoidCollisions,
538
+ collisionPadding,
539
+ sticky,
540
+ hideWhenDetached,
541
+ ref: mergeRefs([forwardRef, contentRef]),
542
+ onOpenAutoFocus: (event) => event.preventDefault(),
543
+ onInteractOutside: (event) => {
544
+ const target = event.target;
545
+ const isTrigger = isInsideRef(target, triggerRef);
546
+ const isContent = isInsideRef(target, contentRef);
547
+ if (isTrigger || isContent) {
548
+ event.preventDefault();
549
+ }
550
+ },
551
+ children: /* @__PURE__ */ jsx(ComboboxList, { role: "listbox", children: overflow === "auto" ? /* @__PURE__ */ jsxs(ScrollArea, { type: "always", dir: direction, children: [
552
+ /* @__PURE__ */ jsx(
553
+ ScrollArea.Viewport,
554
+ {
555
+ availableHeight: "var(--radix-popover-content-available-height)",
556
+ verticalGap: "var(--space-50) * 2",
557
+ maxHeight,
558
+ children: content
559
+ }
560
+ ),
561
+ /* @__PURE__ */ jsx(ScrollArea.Scrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollArea.Thumb, {}) })
562
+ ] }) : content })
563
+ }
564
+ );
149
565
  }
150
566
  );
151
567
 
152
568
  const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
153
569
 
154
- const StyledGroup = styled(Group$1, {});
570
+ const StyledGroup = styled(Group$1);
155
571
 
156
- const Group = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroup, { ...props, ref: forwardRef }));
572
+ const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
573
+ const { autoFilter, filteredItems } = useComboboxContext();
574
+ const childValues = useMemo(
575
+ // don't perform calculation if auto filter is disabled
576
+ () => autoFilter !== false ? getChildrenItemValues(children) : [],
577
+ [children, autoFilter]
578
+ );
579
+ const hasVisibleChildren = useMemo(
580
+ () => (
581
+ // don't perform calculation if auto filter is disabled
582
+ autoFilter !== false ? childValues.some((value) => filteredItems.has(value)) : true
583
+ ),
584
+ [childValues, filteredItems, autoFilter]
585
+ );
586
+ const getInvisibleContent = useInvisibleContent();
587
+ if (!hasVisibleChildren) {
588
+ return getInvisibleContent(children);
589
+ }
590
+ return /* @__PURE__ */ jsx(StyledGroup, { ...rest, ref: forwardRef, children });
591
+ });
157
592
 
158
- const StyledGroupLabel = styled(GroupLabel$1, {});
593
+ const StyledGroupLabel = styled(GroupLabel$1, {
594
+ padding: "6px $100",
595
+ color: "$text-neutrals-subtle"
596
+ });
159
597
 
160
598
  const GroupLabel = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroupLabel, { ...props, ref: forwardRef }));
161
599
 
162
- const StyledValue = styled(Primitive.span, {});
600
+ const StyledChip = styled(Primitive.div, {
601
+ fontSize: "$150",
602
+ padding: "$50 $100",
603
+ borderRadius: "$round",
604
+ display: "flex",
605
+ alignItems: "center",
606
+ gap: "$50",
607
+ whiteSpace: "nowrap",
608
+ maxWidth: "$35",
609
+ backgroundColor: "$background-neutrals-subtle",
610
+ color: "$text-neutrals"
611
+ });
612
+ const StyledChipButton = styled(BaseButton, {
613
+ color: "$icon-neutrals-inactive",
614
+ ...focus.css({
615
+ boxShadow: "$focus-small-outline"
616
+ })
617
+ });
618
+ const StyledChipContent = styled(Primitive.div, {
619
+ textOverflow: "ellipsis",
620
+ whiteSpace: "nowrap",
621
+ overflow: "hidden",
622
+ lineHeight: 1.3
623
+ });
163
624
 
164
- const Value = React.forwardRef(
165
- (props, forwardRef) => {
166
- const { value } = useComboboxContext();
167
- const isEmpty = value === void 0;
168
- if (isEmpty) {
169
- return null;
170
- }
171
- return /* @__PURE__ */ jsx(StyledValue, { ref: forwardRef, ...props, children: value });
172
- }
625
+ const StyledLeftSlot = styled(Primitive.span, {
626
+ order: -1,
627
+ marginRight: "$50"
628
+ });
629
+
630
+ const LeftSlot = StyledLeftSlot;
631
+
632
+ const Chip = React.forwardRef(
633
+ ({ children, disabled = false, onRemove, removeAriaLabel, ...restProps }, forwardRef) => /* @__PURE__ */ jsxs(StyledChip, { ...restProps, ref: forwardRef, children: [
634
+ /* @__PURE__ */ jsx(StyledChipContent, { children }),
635
+ !booleanify(disabled) && /* @__PURE__ */ jsx(StyledChipButton, { onClick: onRemove, "aria-label": removeAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", "aria-hidden": true }) })
636
+ ] })
173
637
  );
638
+ Chip.LeftSlot = LeftSlot;
639
+
640
+ const Value = ({ unselectAriaLabel }) => {
641
+ const {
642
+ value = [],
643
+ setValue,
644
+ disabled,
645
+ "aria-disabled": ariaDisabled,
646
+ itemValueTextMap
647
+ } = useComboboxContext();
648
+ const isDisabled = ariaDisabled === true || disabled;
649
+ const onItemRemove = useCallback(
650
+ (item) => {
651
+ setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
652
+ },
653
+ [setValue]
654
+ );
655
+ const getItemText = useCallback(
656
+ (itemValue) => {
657
+ const textValue = itemValueTextMap.get(itemValue);
658
+ if (textValue === void 0 || textValue === "") {
659
+ return null;
660
+ }
661
+ return /* @__PURE__ */ jsx(
662
+ Chip,
663
+ {
664
+ onRemove: (e) => {
665
+ onItemRemove(itemValue);
666
+ e.stopPropagation();
667
+ },
668
+ disabled: isDisabled,
669
+ removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(textValue),
670
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(itemValue) : void 0,
671
+ children: textValue
672
+ },
673
+ itemValue
674
+ );
675
+ },
676
+ [isDisabled, itemValueTextMap, onItemRemove, unselectAriaLabel]
677
+ );
678
+ return /* @__PURE__ */ jsx(Fragment, { children: value.map(getItemText) });
679
+ };
174
680
 
175
- const StyledComboboxContent = styled(Primitive.div, {});
681
+ const StyledSeparator = styled(Primitive.div, {
682
+ backgroundColor: "$border-neutrals-subtle",
683
+ height: "1px",
684
+ width: "100%",
685
+ margin: "$100 0"
686
+ });
687
+
688
+ const Separator = React.forwardRef((props, forwardRef) => {
689
+ const { autoFilter, searchValue } = useComboboxContext();
690
+ if (autoFilter === true && searchValue.length > 0) {
691
+ return null;
692
+ }
693
+ return /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true });
694
+ });
695
+
696
+ const StyledNativeSelect = styled(Primitive.select, {
697
+ // if we support autoComplete, we would have to use visually-hidden styles here
698
+ display: "none"
699
+ });
700
+ const StyledComboboxContent = styled(Primitive.div, {
701
+ position: "relative",
702
+ width: "100%"
703
+ });
176
704
 
177
705
  const Root = React.forwardRef(
178
- ({
179
- onOpen,
180
- onClose,
181
- onValueChange,
182
- value: valueProp,
183
- children,
184
- ...restProps
185
- }, forwardRef) => {
706
+ ({ value: valueProp, onValueChange, name, children, ...restProps }, forwardRef) => {
707
+ var _a;
186
708
  const {
709
+ openState,
710
+ setOpenState,
711
+ defaultValue,
712
+ value = [],
713
+ setValue,
187
714
  required,
188
715
  readOnly,
189
716
  "aria-disabled": ariaDisabled,
190
717
  disabled,
191
- direction
718
+ direction,
719
+ size,
720
+ placeholder,
721
+ triggerRef
192
722
  } = useComboboxContext();
193
- const { setRequired, setDisabled, setAriaDisabled, setReadOnly } = useFormFieldContext();
723
+ const {
724
+ setRequired,
725
+ setDisabled,
726
+ setAriaDisabled,
727
+ setReadOnly,
728
+ label,
729
+ isFloatingLabel,
730
+ focused,
731
+ formElementRef
732
+ } = useFormFieldContext();
194
733
  useEffect(() => {
195
734
  setRequired == null ? void 0 : setRequired(required);
196
735
  setDisabled == null ? void 0 : setDisabled(disabled);
@@ -206,7 +745,63 @@ const Root = React.forwardRef(
206
745
  setAriaDisabled,
207
746
  setReadOnly
208
747
  ]);
209
- return /* @__PURE__ */ jsx(StyledComboboxContent, { ...restProps, ref: forwardRef, dir: direction, children });
748
+ const shouldUseFloatingLabel = label !== null && isFloatingLabel;
749
+ const isFloating = placeholder !== void 0 || value.length !== 0 || focused;
750
+ const onSetSelectedValue = (newValue) => {
751
+ setValue(typeof newValue === "string" ? [newValue] : newValue);
752
+ };
753
+ const onOpenChange = (value2) => {
754
+ if (!booleanify(readOnly)) {
755
+ setOpenState(value2);
756
+ }
757
+ };
758
+ const isFormControl = Boolean((_a = triggerRef.current) == null ? void 0 : _a.closest("form"));
759
+ return /* @__PURE__ */ jsxs(RadixPopover.Root, { open: openState, onOpenChange, children: [
760
+ /* @__PURE__ */ jsx(
761
+ ComboboxProvider$1,
762
+ {
763
+ open: openState,
764
+ setOpen: onOpenChange,
765
+ defaultSelectedValue: defaultValue,
766
+ selectedValue: value,
767
+ setSelectedValue: onSetSelectedValue,
768
+ children: /* @__PURE__ */ jsxs(
769
+ StyledComboboxContent,
770
+ {
771
+ ref: forwardRef,
772
+ ...restProps,
773
+ dir: direction,
774
+ "data-form-element": "select",
775
+ children: [
776
+ shouldUseFloatingLabel && /* @__PURE__ */ jsx(FloatingLabel, { floating: isFloating, size, children: label }),
777
+ children
778
+ ]
779
+ }
780
+ )
781
+ }
782
+ ),
783
+ isFormControl && /* @__PURE__ */ jsx(
784
+ StyledNativeSelect,
785
+ {
786
+ multiple: true,
787
+ autoComplete: "off",
788
+ name,
789
+ tabIndex: -1,
790
+ "aria-hidden": "true",
791
+ ref: formElementRef,
792
+ required,
793
+ disabled,
794
+ "aria-disabled": ariaDisabled,
795
+ value,
796
+ onChange: () => {
797
+ },
798
+ children: value.length === 0 ? /* @__PURE__ */ jsx("option", { value: "" }) : (
799
+ // since we don't support autoComplete we can render here only selected values
800
+ value.map((itemValue) => /* @__PURE__ */ jsx("option", { value: itemValue, children: itemValue }, itemValue))
801
+ )
802
+ }
803
+ )
804
+ ] });
210
805
  }
211
806
  );
212
807
  const Combobox = React.forwardRef(
@@ -220,21 +815,33 @@ const Combobox = React.forwardRef(
220
815
  required,
221
816
  value,
222
817
  defaultValue,
818
+ onOpen,
819
+ onClose,
820
+ onSearchValueChange,
821
+ onValueChange,
223
822
  direction = "ltr",
823
+ autoFilter = true,
824
+ noResultsText,
224
825
  ...restProps
225
826
  }, forwardRef) => /* @__PURE__ */ jsx(
226
827
  ComboboxProvider,
227
828
  {
228
829
  defaultValue,
229
830
  value,
831
+ onValueChange,
832
+ onSearchValueChange,
230
833
  defaultOpen,
231
834
  open,
835
+ onOpen,
836
+ onClose,
232
837
  valid,
233
838
  required,
234
839
  disabled,
235
840
  readOnly,
236
841
  "aria-disabled": ariaDisabled,
237
842
  direction,
843
+ autoFilter,
844
+ noResultsText,
238
845
  children: /* @__PURE__ */ jsx(Root, { ...restProps, value, ref: forwardRef })
239
846
  }
240
847
  )
@@ -246,6 +853,11 @@ Combobox.Item = Item;
246
853
  Combobox.Group = Group;
247
854
  Combobox.GroupLabel = GroupLabel;
248
855
  Combobox.Value = Value;
856
+ Combobox.Separator = Separator;
857
+
858
+ var types = /*#__PURE__*/Object.freeze({
859
+ __proto__: null
860
+ });
249
861
 
250
- export { Combobox };
862
+ export { Combobox, types as ComboboxTypes };
251
863
  //# sourceMappingURL=module.js.map