@mirohq/design-system-combobox 0.1.0-combobox.4 → 0.1.0-combobox.6

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
@@ -2,12 +2,15 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import React, { createContext, useRef, useState, useContext, useCallback, useEffect, useMemo } from 'react';
3
3
  import { Combobox as Combobox$1, ComboboxItem, ComboboxItemCheck, Group as Group$1, GroupLabel as GroupLabel$1, ComboboxProvider as ComboboxProvider$1 } from '@ariakit/react';
4
4
  import { useFormFieldContext, FloatingLabel } from '@mirohq/design-system-base-form';
5
+ import { stringAttrValue, booleanishAttrValue, mergeRefs, booleanify } from '@mirohq/design-system-utils';
5
6
  import * as RadixPopover from '@radix-ui/react-popover';
6
7
  import { Portal as Portal$1 } from '@radix-ui/react-popover';
7
- import { stringAttrValue, booleanishAttrValue, mergeRefs, booleanify } from '@mirohq/design-system-utils';
8
- import { styled } from '@mirohq/design-system-stitches';
8
+ import { styled, theme } from '@mirohq/design-system-stitches';
9
9
  import { Input } from '@mirohq/design-system-input';
10
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
10
11
  import { IconChevronDown, IconCross, IconCheckMark } from '@mirohq/design-system-icons';
12
+ import { ScrollArea } from '@mirohq/design-system-scroll-area';
13
+ import { mergeProps } from '@react-aria/utils';
11
14
  import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
12
15
  import { focus } from '@mirohq/design-system-styles';
13
16
  import { Primitive } from '@mirohq/design-system-primitive';
@@ -17,24 +20,29 @@ const StyledInput = styled(Input, {
17
20
  flexWrap: "wrap",
18
21
  flexGrow: 1,
19
22
  gap: "0 $50",
20
- "&&&": {
21
- height: "max-content"
23
+ overflowY: "scroll",
24
+ "&[data-valid], &[data-invalid]": {
25
+ // we don't need a bigger padding here as Input component will render its own icon
26
+ paddingRight: "$100"
22
27
  },
23
28
  "& input": {
24
- minWidth: "30px",
29
+ minWidth: "$8",
25
30
  flexBasis: 0,
26
- flexGrow: 1,
27
- marginRight: "$300"
31
+ flexGrow: 1
28
32
  },
29
33
  variants: {
30
34
  size: {
31
35
  large: {
32
36
  minHeight: "$10",
33
- padding: "5px $100"
37
+ height: "auto",
38
+ padding: "5px $100",
39
+ paddingRight: "$500"
34
40
  },
35
41
  "x-large": {
36
42
  minHeight: "$12",
37
- padding: "9px $150"
43
+ height: "auto",
44
+ padding: "5px $100",
45
+ paddingRight: "$500"
38
46
  }
39
47
  }
40
48
  },
@@ -46,19 +54,38 @@ const StyledInput = styled(Input, {
46
54
  const ComboboxContext = createContext({});
47
55
  const ComboboxProvider = ({
48
56
  children,
57
+ open: openProp,
49
58
  defaultOpen,
59
+ onOpen,
60
+ onClose,
50
61
  valid,
51
62
  value: valueProp,
52
63
  defaultValue: defaultValueProp,
64
+ onValueChange,
53
65
  onSearchValueChange,
54
66
  autoFilter = true,
55
67
  ...restProps
56
68
  }) => {
57
69
  const triggerRef = useRef(null);
70
+ const inputRef = useRef(null);
58
71
  const contentRef = useRef(null);
59
- const [openState, setOpenState] = useState(defaultOpen != null ? defaultOpen : false);
60
- const [value, setValue] = useState(valueProp != null ? valueProp : []);
61
72
  const [defaultValue, setDefaultValue] = useState(defaultValueProp);
73
+ const [openState = false, setOpenState] = useControllableState({
74
+ prop: openProp,
75
+ defaultProp: defaultOpen,
76
+ onChange: (state) => {
77
+ if (state) {
78
+ onOpen == null ? void 0 : onOpen();
79
+ } else {
80
+ onClose == null ? void 0 : onClose();
81
+ }
82
+ }
83
+ });
84
+ const [value, setValue] = useControllableState({
85
+ prop: valueProp,
86
+ defaultProp: defaultValueProp,
87
+ onChange: onValueChange
88
+ });
62
89
  const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
63
90
  const [searchValue, setSearchValue] = useState("");
64
91
  const { valid: formFieldValid } = useFormFieldContext();
@@ -76,6 +103,7 @@ const ComboboxProvider = ({
76
103
  defaultValue,
77
104
  onSearchValueChange,
78
105
  triggerRef,
106
+ inputRef,
79
107
  contentRef,
80
108
  autoFilter,
81
109
  searchValue,
@@ -112,8 +140,8 @@ const TriggerActionButton = ({
112
140
  clearActionLabel,
113
141
  size
114
142
  }) => {
115
- const { setOpenState, value, setValue } = useComboboxContext();
116
- const isEmpty = value === void 0 || value.length === 0;
143
+ const { setOpenState, value = [], setValue } = useComboboxContext();
144
+ const isEmpty = value.length === 0;
117
145
  const ActionButtonIcon = isEmpty ? IconChevronDown : IconCross;
118
146
  const label = isEmpty ? openActionLabel : clearActionLabel;
119
147
  const onActionButtonClick = useCallback(
@@ -121,7 +149,7 @@ const TriggerActionButton = ({
121
149
  if (!isEmpty) {
122
150
  setValue([]);
123
151
  } else {
124
- setOpenState((prevOpen) => !prevOpen);
152
+ setOpenState((prevOpen = false) => !prevOpen);
125
153
  }
126
154
  event.stopPropagation();
127
155
  },
@@ -147,8 +175,9 @@ const Trigger = React.forwardRef(
147
175
  "aria-disabled": ariaDisabled,
148
176
  valid: comboboxValid,
149
177
  disabled,
150
- value,
178
+ value = [],
151
179
  triggerRef,
180
+ inputRef,
152
181
  onSearchValueChange,
153
182
  searchValue,
154
183
  setSearchValue
@@ -175,41 +204,59 @@ const Trigger = React.forwardRef(
175
204
  disabled,
176
205
  invalid: booleanishAttrValue(valid),
177
206
  id: id != null ? id : formElementId,
178
- placeholder: (value == null ? void 0 : value.length) === 0 ? placeholder : void 0
207
+ placeholder: value.length === 0 ? placeholder : void 0
179
208
  };
180
209
  const shouldUseFloatingLabel = label !== null && isFloatingLabel;
181
- const isFloating = placeholder !== void 0 || (value == null ? void 0 : value.length) !== 0 || focused;
210
+ const isFloating = placeholder !== void 0 || value.length !== 0 || focused;
211
+ const scrollIntoView = (event) => {
212
+ var _a;
213
+ const trigger = triggerRef == null ? void 0 : triggerRef.current;
214
+ const baseInput = (_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement;
215
+ if (baseInput != null && trigger != null) {
216
+ event.preventDefault();
217
+ baseInput.scrollTo({
218
+ top: trigger.scrollHeight
219
+ });
220
+ }
221
+ if (restProps.onFocus !== void 0) {
222
+ restProps.onFocus(event);
223
+ }
224
+ };
182
225
  const onInputChange = (e) => {
183
226
  setSearchValue(e.target.value);
184
227
  onSearchValueChange == null ? void 0 : onSearchValueChange(e.target.value);
185
228
  onChange == null ? void 0 : onChange(e);
186
229
  };
187
- return /* @__PURE__ */ jsx(RadixPopover.Anchor, { ref: mergeRefs([triggerRef, forwardRef]), children: /* @__PURE__ */ jsx(
188
- Combobox$1,
189
- {
190
- render: /* @__PURE__ */ jsxs(
191
- StyledInput,
192
- {
193
- value: searchValue,
194
- onChange: onInputChange,
195
- ...inputProps,
196
- size,
197
- children: [
198
- shouldUseFloatingLabel && /* @__PURE__ */ jsx(FloatingLabel, { floating: isFloating, size, children: label }),
199
- children,
200
- /* @__PURE__ */ jsx(
201
- TriggerActionButton,
202
- {
203
- openActionLabel,
204
- clearActionLabel,
205
- size
206
- }
207
- )
208
- ]
209
- }
210
- )
211
- }
212
- ) });
230
+ return /* @__PURE__ */ jsxs(RadixPopover.Anchor, { ref: mergeRefs([triggerRef, forwardRef]), children: [
231
+ shouldUseFloatingLabel && /* @__PURE__ */ jsx(FloatingLabel, { floating: isFloating, size, children: label }),
232
+ /* @__PURE__ */ jsx(
233
+ Combobox$1,
234
+ {
235
+ render: /* @__PURE__ */ jsxs(
236
+ StyledInput,
237
+ {
238
+ ...inputProps,
239
+ value: searchValue,
240
+ size,
241
+ ref: inputRef,
242
+ onChange: onInputChange,
243
+ onFocus: scrollIntoView,
244
+ children: [
245
+ children,
246
+ /* @__PURE__ */ jsx(
247
+ TriggerActionButton,
248
+ {
249
+ openActionLabel,
250
+ clearActionLabel,
251
+ size
252
+ }
253
+ )
254
+ ]
255
+ }
256
+ )
257
+ }
258
+ )
259
+ ] });
213
260
  }
214
261
  );
215
262
 
@@ -223,7 +270,6 @@ const StyledContent = styled(RadixPopover.Content, {
223
270
  width: "var(--radix-popover-trigger-width)",
224
271
  zIndex: "$select",
225
272
  overflowY: "auto",
226
- marginTop: "$200",
227
273
  padding: "$150",
228
274
  boxSizing: "border-box",
229
275
  outline: "1px solid transparent"
@@ -269,20 +315,31 @@ const StyledItem = styled(ComboboxItem, {
269
315
  const Item = React.forwardRef(
270
316
  ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
271
317
  const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
272
- const { autoFilter, filteredItems } = useComboboxContext();
318
+ const { autoFilter, filteredItems, triggerRef, inputRef } = useComboboxContext();
273
319
  if (autoFilter !== false && !filteredItems.has(value)) {
274
320
  return null;
275
321
  }
322
+ const scrollIntoView = (event) => {
323
+ var _a;
324
+ if (((_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement) != null && (triggerRef == null ? void 0 : triggerRef.current) != null) {
325
+ inputRef.current.parentElement.scrollTo({
326
+ top: triggerRef.current.scrollHeight
327
+ });
328
+ }
329
+ if (restProps.onClick !== void 0) {
330
+ restProps.onClick(event);
331
+ }
332
+ };
276
333
  return /* @__PURE__ */ jsxs(
277
334
  StyledItem,
278
335
  {
279
- ...restProps,
280
- ...restAriaDisabledProps,
336
+ ...mergeProps(restProps, restAriaDisabledProps),
281
337
  focusable: true,
282
338
  accessibleWhenDisabled: ariaDisabled === true,
283
339
  disabled: ariaDisabled === true || disabled,
284
340
  ref: forwardRef,
285
341
  value,
342
+ onClick: scrollIntoView,
286
343
  children: [
287
344
  children,
288
345
  /* @__PURE__ */ jsx(
@@ -292,7 +349,13 @@ const Item = React.forwardRef(
292
349
  // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
293
350
  /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
294
351
  ),
295
- children: /* @__PURE__ */ jsx(IconCheckMark, { size: "small" })
352
+ children: /* @__PURE__ */ jsx(
353
+ IconCheckMark,
354
+ {
355
+ size: "small",
356
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-item-check" : void 0
357
+ }
358
+ )
296
359
  }
297
360
  )
298
361
  ]
@@ -323,48 +386,65 @@ const getChildrenItemValues = (componentChildren) => {
323
386
  return values;
324
387
  };
325
388
 
389
+ const CONTENT_OFFSET = parseInt(theme.space[50]);
326
390
  const isInsideRef = (element, ref) => {
327
391
  var _a, _b;
328
392
  return (_b = element != null && ((_a = ref.current) == null ? void 0 : _a.contains(element))) != null ? _b : false;
329
393
  };
330
- const Content = React.forwardRef(({ children, ...restProps }, forwardRef) => {
331
- const {
332
- triggerRef,
333
- contentRef,
334
- autoFilter,
335
- filteredItems,
336
- setFilteredItems,
337
- searchValue,
338
- noResultsText
339
- } = useComboboxContext();
340
- useEffect(() => {
341
- const childrenItemValues = getChildrenItemValues(children);
342
- setFilteredItems(
343
- new Set(
344
- autoFilter === false ? childrenItemValues : childrenItemValues.filter(
345
- (child) => child.toLowerCase().includes(searchValue.toLowerCase())
394
+ const Content = React.forwardRef(
395
+ ({ sideOffset = CONTENT_OFFSET, maxHeight, children, ...restProps }, forwardRef) => {
396
+ const {
397
+ triggerRef,
398
+ contentRef,
399
+ autoFilter,
400
+ filteredItems,
401
+ setFilteredItems,
402
+ searchValue,
403
+ noResultsText,
404
+ direction
405
+ } = useComboboxContext();
406
+ useEffect(() => {
407
+ const childrenItemValues = getChildrenItemValues(children);
408
+ setFilteredItems(
409
+ new Set(
410
+ autoFilter === false ? childrenItemValues : childrenItemValues.filter(
411
+ (child) => child.toLowerCase().includes(searchValue.toLowerCase())
412
+ )
346
413
  )
347
- )
414
+ );
415
+ }, [children, autoFilter, setFilteredItems, searchValue]);
416
+ const content = filteredItems.size === 0 ? noResultsText : children;
417
+ return /* @__PURE__ */ jsx(
418
+ StyledContent,
419
+ {
420
+ ...restProps,
421
+ sideOffset,
422
+ ref: mergeRefs([forwardRef, contentRef]),
423
+ onOpenAutoFocus: (event) => event.preventDefault(),
424
+ onInteractOutside: (event) => {
425
+ const target = event.target;
426
+ const isTrigger = isInsideRef(target, triggerRef);
427
+ const isContent = isInsideRef(target, contentRef);
428
+ if (isTrigger || isContent) {
429
+ event.preventDefault();
430
+ }
431
+ },
432
+ children: /* @__PURE__ */ jsxs(ScrollArea, { type: "always", dir: direction, children: [
433
+ /* @__PURE__ */ jsx(
434
+ ScrollArea.Viewport,
435
+ {
436
+ availableHeight: "var(--radix-popover-content-available-height)",
437
+ verticalGap: "calc(var(--space-150) * 2)",
438
+ maxHeight,
439
+ children: content
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsx(ScrollArea.Scrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollArea.Thumb, {}) })
443
+ ] })
444
+ }
348
445
  );
349
- }, [children, autoFilter, setFilteredItems, searchValue]);
350
- return /* @__PURE__ */ jsx(
351
- StyledContent,
352
- {
353
- ...restProps,
354
- ref: mergeRefs([forwardRef, contentRef]),
355
- onOpenAutoFocus: (event) => event.preventDefault(),
356
- onInteractOutside: (event) => {
357
- const target = event.target;
358
- const isTrigger = isInsideRef(target, triggerRef);
359
- const isContent = isInsideRef(target, contentRef);
360
- if (isTrigger || isContent) {
361
- event.preventDefault();
362
- }
363
- },
364
- children: filteredItems.size === 0 ? noResultsText : children
365
- }
366
- );
367
- });
446
+ }
447
+ );
368
448
 
369
449
  const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
370
450
 
@@ -444,17 +524,16 @@ const StyledValue = styled(Chip, {
444
524
 
445
525
  const Value = ({ removeChipAriaLabel }) => {
446
526
  const {
447
- value,
527
+ value = [],
448
528
  setValue,
449
529
  disabled,
450
530
  "aria-disabled": ariaDisabled
451
531
  } = useComboboxContext();
452
- const isEmpty = value === void 0 || value.length === 0;
453
532
  const isDisabled = ariaDisabled === true || disabled;
454
533
  const onItemRemove = (item) => {
455
- setValue((prevValue) => prevValue.filter((value2) => value2 !== item));
534
+ setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
456
535
  };
457
- if (isEmpty) {
536
+ if (value.length === 0) {
458
537
  return null;
459
538
  }
460
539
  return /* @__PURE__ */ jsx(Fragment, { children: value.map((item) => /* @__PURE__ */ jsx(
@@ -463,6 +542,7 @@ const Value = ({ removeChipAriaLabel }) => {
463
542
  onRemove: () => onItemRemove(item),
464
543
  disabled: isDisabled,
465
544
  removeChipAriaLabel,
545
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(item) : void 0,
466
546
  children: item
467
547
  },
468
548
  item
@@ -476,9 +556,18 @@ const StyledSeparator = styled(Primitive.div, {
476
556
  margin: "$100 0"
477
557
  });
478
558
 
479
- const Separator = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true }));
559
+ const Separator = React.forwardRef((props, forwardRef) => {
560
+ const { autoFilter, searchValue } = useComboboxContext();
561
+ if (autoFilter === true && searchValue.length > 0) {
562
+ return null;
563
+ }
564
+ return /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true });
565
+ });
480
566
 
481
- const StyledComboboxContent = styled(Primitive.div, {});
567
+ const StyledComboboxContent = styled(Primitive.div, {
568
+ position: "relative",
569
+ width: "100%"
570
+ });
482
571
 
483
572
  const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...restProps }, forwardRef) => {
484
573
  const {
@@ -510,8 +599,18 @@ const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...r
510
599
  setReadOnly
511
600
  ]);
512
601
  const onSetSelectedValue = (newValue) => {
513
- onValueChange == null ? void 0 : onValueChange(newValue);
514
- setValue(newValue);
602
+ setValue(typeof newValue === "string" ? [newValue] : newValue);
603
+ };
604
+ const comboboxProps = {
605
+ ...restProps,
606
+ onClick: (event) => {
607
+ if (!booleanify(disabled)) {
608
+ setOpenState(true);
609
+ }
610
+ if (restProps.onClick !== void 0) {
611
+ restProps.onClick(event);
612
+ }
613
+ }
515
614
  };
516
615
  return /* @__PURE__ */ jsx(RadixPopover.Root, { open: openState, onOpenChange: setOpenState, children: /* @__PURE__ */ jsx(
517
616
  ComboboxProvider$1,
@@ -521,7 +620,15 @@ const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...r
521
620
  defaultSelectedValue: defaultValue,
522
621
  selectedValue: value,
523
622
  setSelectedValue: onSetSelectedValue,
524
- children: /* @__PURE__ */ jsx(StyledComboboxContent, { ...restProps, ref: forwardRef, dir: direction, children })
623
+ children: /* @__PURE__ */ jsx(
624
+ StyledComboboxContent,
625
+ {
626
+ ...comboboxProps,
627
+ ref: forwardRef,
628
+ dir: direction,
629
+ children
630
+ }
631
+ )
525
632
  }
526
633
  ) });
527
634
  });
@@ -536,8 +643,9 @@ const Combobox = React.forwardRef(
536
643
  required,
537
644
  value,
538
645
  defaultValue,
539
- onSearchValueChange,
540
646
  onOpen,
647
+ onClose,
648
+ onSearchValueChange,
541
649
  onValueChange,
542
650
  direction = "ltr",
543
651
  autoFilter = true,
@@ -548,9 +656,12 @@ const Combobox = React.forwardRef(
548
656
  {
549
657
  defaultValue,
550
658
  value,
659
+ onValueChange,
551
660
  onSearchValueChange,
552
661
  defaultOpen,
553
662
  open,
663
+ onOpen,
664
+ onClose,
554
665
  valid,
555
666
  required,
556
667
  disabled,
@@ -559,15 +670,7 @@ const Combobox = React.forwardRef(
559
670
  direction,
560
671
  autoFilter,
561
672
  noResultsText,
562
- children: /* @__PURE__ */ jsx(
563
- Root,
564
- {
565
- ...restProps,
566
- noResultsText,
567
- value,
568
- ref: forwardRef
569
- }
570
- )
673
+ children: /* @__PURE__ */ jsx(Root, { ...restProps, value, ref: forwardRef })
571
674
  }
572
675
  )
573
676
  );