@mirohq/design-system-combobox 0.3.6 → 0.3.7

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,6 +1,6 @@
1
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';
2
+ import React, { createContext, useRef, useState, useMemo, useContext, useCallback, useEffect } from 'react';
3
+ import { Combobox as Combobox$1, ComboboxList, 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
5
  import { mergeRefs, booleanify, stringAttrValue } from '@mirohq/design-system-utils';
6
6
  import * as RadixPopover from '@radix-ui/react-popover';
@@ -13,11 +13,11 @@ import { IconChevronDown, IconCross, IconCheckMark } from '@mirohq/design-system
13
13
  import { ScrollArea } from '@mirohq/design-system-scroll-area';
14
14
  import { itemsContainerStyles, contentStyles, itemStyles, StyledItemCheck, groupLabelStyles, separatorStyles } from '@mirohq/design-system-base-select';
15
15
  import { Primitive } from '@mirohq/design-system-primitive';
16
- import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
17
- import { useLayoutEffect } from '@mirohq/design-system-use-layout-effect';
18
16
  import { createPortal } from 'react-dom';
19
- import { BaseButton } from '@mirohq/design-system-base-button';
20
- import { focus } from '@mirohq/design-system-styles';
17
+ import { useLayoutEffect } from '@mirohq/design-system-use-layout-effect';
18
+ import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
19
+ import { useId } from '@mirohq/design-system-use-id';
20
+ import { Chip } from '@mirohq/design-system-chip';
21
21
 
22
22
  const StyledBaseInput = styled(BaseInput, {
23
23
  flexWrap: "wrap",
@@ -53,6 +53,10 @@ const StyledBaseInput = styled(BaseInput, {
53
53
  }
54
54
  });
55
55
 
56
+ function searchQueryMatch(displayedText, searchValue) {
57
+ return displayedText.toLowerCase().includes(searchValue.toLowerCase());
58
+ }
59
+
56
60
  const ComboboxContext = createContext({});
57
61
  const ComboboxProvider = ({
58
62
  children,
@@ -66,13 +70,12 @@ const ComboboxProvider = ({
66
70
  onValueChange,
67
71
  searchValue: searchValueProp,
68
72
  onSearchValueChange,
69
- autoFilter = true,
73
+ autoFilter,
70
74
  ...restProps
71
75
  }) => {
72
76
  const triggerRef = useRef(null);
73
77
  const inputRef = useRef(null);
74
78
  const contentRef = useRef(null);
75
- const [defaultValue, setDefaultValue] = useState(defaultValueProp);
76
79
  const [openState = false, setOpenState] = useControllableState({
77
80
  prop: openProp,
78
81
  defaultProp: defaultOpen,
@@ -89,16 +92,23 @@ const ComboboxProvider = ({
89
92
  defaultProp: defaultValueProp,
90
93
  onChange: onValueChange
91
94
  });
92
- const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
93
- const [searchValue, setSearchValue] = useControllableState({
95
+ const [searchValue = "", setSearchValue] = useControllableState({
94
96
  prop: searchValueProp,
95
97
  defaultProp: "",
96
98
  onChange: onSearchValueChange
97
99
  });
98
100
  const [size, setSize] = useState();
99
101
  const [placeholder, setPlaceholder] = useState();
100
- const [itemValueTextMap, setItemValueTextMap] = useState(/* @__PURE__ */ new Map());
102
+ const [itemsMap, setItemsMap] = useState(/* @__PURE__ */ new Map());
101
103
  const { valid: formFieldValid } = useFormFieldContext();
104
+ const filteredItems = useMemo(() => {
105
+ if (searchValue.length > 0) {
106
+ return Array.from(itemsMap.values()).filter(
107
+ (item) => searchQueryMatch(item.displayedText, searchValue)
108
+ );
109
+ }
110
+ return [];
111
+ }, [itemsMap, searchValue]);
102
112
  return /* @__PURE__ */ jsx(
103
113
  ComboboxContext.Provider,
104
114
  {
@@ -109,18 +119,15 @@ const ComboboxProvider = ({
109
119
  setOpenState,
110
120
  value,
111
121
  setValue,
112
- setDefaultValue,
113
- defaultValue,
114
122
  triggerRef,
115
123
  inputRef,
116
124
  contentRef,
117
125
  autoFilter,
118
126
  searchValue,
119
127
  setSearchValue,
128
+ itemsMap,
129
+ setItemsMap,
120
130
  filteredItems,
121
- setFilteredItems,
122
- itemValueTextMap,
123
- setItemValueTextMap,
124
131
  placeholder,
125
132
  setPlaceholder,
126
133
  size,
@@ -207,7 +214,7 @@ const Trigger = React.forwardRef(
207
214
  placeholder,
208
215
  openActionLabel,
209
216
  closeActionLabel,
210
- clearable,
217
+ clearable = true,
211
218
  clearActionLabel,
212
219
  onChange,
213
220
  onFocus,
@@ -333,103 +340,6 @@ const StyledContent = styled(RadixPopover.Content, {
333
340
  boxSizing: "border-box"
334
341
  });
335
342
 
336
- const StyledItem = styled(ComboboxItem, itemStyles);
337
-
338
- const Item = React.forwardRef(
339
- ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
340
- const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
341
- const {
342
- autoFilter,
343
- filteredItems,
344
- setItemValueTextMap,
345
- triggerRef,
346
- inputRef,
347
- value: comboboxValue = []
348
- } = useComboboxContext();
349
- useLayoutEffect(() => {
350
- const textToSet = textValue !== void 0 ? textValue : typeof children === "string" ? children : "";
351
- setItemValueTextMap((prevState) => new Map(prevState.set(value, textToSet)));
352
- return () => {
353
- setItemValueTextMap((prevState) => {
354
- prevState.delete(value);
355
- return new Map(prevState);
356
- });
357
- };
358
- }, [setItemValueTextMap, value, textValue, children]);
359
- if (autoFilter !== false && !filteredItems.has(value)) {
360
- return null;
361
- }
362
- const scrollIntoView = (event) => {
363
- var _a;
364
- if (((_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement) != null && (triggerRef == null ? void 0 : triggerRef.current) != null) {
365
- inputRef.current.parentElement.scrollTo({
366
- top: triggerRef.current.scrollHeight
367
- });
368
- }
369
- if (restProps.onClick !== void 0) {
370
- restProps.onClick(event);
371
- }
372
- };
373
- const isSelected = comboboxValue.includes(value);
374
- return /* @__PURE__ */ jsxs(
375
- StyledItem,
376
- {
377
- ...mergeProps(restProps, restAriaDisabledProps),
378
- focusable: true,
379
- hideOnClick: false,
380
- accessibleWhenDisabled: booleanify(ariaDisabled),
381
- disabled: booleanify(ariaDisabled) || disabled,
382
- ref: forwardRef,
383
- value,
384
- onClick: scrollIntoView,
385
- "aria-selected": isSelected,
386
- children: [
387
- /* @__PURE__ */ jsx(
388
- ComboboxItemCheck,
389
- {
390
- checked: isSelected,
391
- render: ({ style, ...props }) => (
392
- // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
393
- /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
394
- ),
395
- children: /* @__PURE__ */ jsx(
396
- IconCheckMark,
397
- {
398
- size: "small",
399
- "data-testid": process.env.NODE_ENV === "test" ? "combobox-item-check" : void 0
400
- }
401
- )
402
- }
403
- ),
404
- children
405
- ]
406
- }
407
- );
408
- }
409
- );
410
-
411
- const itemType = React.createElement(Item).type;
412
- const getChildrenItemValues = (componentChildren) => {
413
- const values = [];
414
- const recurse = (children) => {
415
- React.Children.forEach(children, (child) => {
416
- if (!React.isValidElement(child)) {
417
- return;
418
- }
419
- if (child.type === itemType) {
420
- const props = child.props;
421
- values.push(props.value);
422
- return;
423
- }
424
- if (child.props.children) {
425
- recurse(child.props.children);
426
- }
427
- });
428
- };
429
- recurse(componentChildren);
430
- return values;
431
- };
432
-
433
343
  const useDocumentFragment = () => {
434
344
  const [fragment, setFragment] = React.useState();
435
345
  useLayoutEffect(() => {
@@ -467,23 +377,7 @@ const Content = React.forwardRef(
467
377
  children,
468
378
  ...restProps
469
379
  }, forwardRef) => {
470
- const {
471
- triggerRef,
472
- contentRef,
473
- autoFilter,
474
- setFilteredItems,
475
- searchValue,
476
- direction,
477
- openState
478
- } = useComboboxContext();
479
- useEffect(() => {
480
- const childrenItemValues = getChildrenItemValues(children);
481
- const shouldFilter = autoFilter !== false && searchValue !== void 0 && searchValue.length > 0;
482
- const items = shouldFilter ? childrenItemValues.filter(
483
- (child) => child.toLowerCase().includes(searchValue.toLowerCase())
484
- ) : childrenItemValues;
485
- setFilteredItems(new Set(items));
486
- }, [children, autoFilter, setFilteredItems, searchValue]);
380
+ const { triggerRef, contentRef, direction, openState } = useComboboxContext();
487
381
  const getInvisibleContent = useInvisibleContent();
488
382
  if (!openState) {
489
383
  return getInvisibleContent(children);
@@ -529,84 +423,133 @@ const Content = React.forwardRef(
529
423
  }
530
424
  );
531
425
 
426
+ const StyledItem = styled(ComboboxItem, itemStyles);
427
+
428
+ const GroupContext = createContext({});
429
+ const GroupProvider = ({
430
+ children,
431
+ ...restProps
432
+ }) => /* @__PURE__ */ jsx(
433
+ GroupContext.Provider,
434
+ {
435
+ value: {
436
+ ...restProps
437
+ },
438
+ children
439
+ }
440
+ );
441
+ const useGroupContext = () => useContext(GroupContext);
442
+
443
+ const Item = React.forwardRef(
444
+ ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
445
+ const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
446
+ const {
447
+ searchValue,
448
+ autoFilter,
449
+ setItemsMap,
450
+ triggerRef,
451
+ inputRef,
452
+ value: comboboxValue = []
453
+ } = useComboboxContext();
454
+ const { groupId } = useGroupContext();
455
+ const displayedText = useMemo(() => {
456
+ if (textValue !== void 0) {
457
+ return textValue;
458
+ }
459
+ return typeof children === "string" ? children : "";
460
+ }, [textValue, children]);
461
+ useLayoutEffect(() => {
462
+ setItemsMap(
463
+ (prevState) => new Map(prevState.set(value, { displayedText, groupId }))
464
+ );
465
+ return () => {
466
+ setItemsMap((prevState) => {
467
+ prevState.delete(value);
468
+ return new Map(prevState);
469
+ });
470
+ };
471
+ }, [setItemsMap, groupId, value, displayedText]);
472
+ if (autoFilter && searchValue.length > 0 && !searchQueryMatch(displayedText, searchValue)) {
473
+ return null;
474
+ }
475
+ const scrollIntoView = (event) => {
476
+ var _a;
477
+ if (((_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement) != null && (triggerRef == null ? void 0 : triggerRef.current) != null) {
478
+ inputRef.current.parentElement.scrollTo({
479
+ top: triggerRef.current.scrollHeight
480
+ });
481
+ }
482
+ if (restProps.onClick !== void 0) {
483
+ restProps.onClick(event);
484
+ }
485
+ };
486
+ const isSelected = comboboxValue.includes(value);
487
+ return /* @__PURE__ */ jsxs(
488
+ StyledItem,
489
+ {
490
+ ...mergeProps(restProps, restAriaDisabledProps),
491
+ focusable: true,
492
+ hideOnClick: false,
493
+ accessibleWhenDisabled: booleanify(ariaDisabled),
494
+ disabled: booleanify(ariaDisabled) || disabled,
495
+ ref: forwardRef,
496
+ value,
497
+ onClick: scrollIntoView,
498
+ "aria-selected": isSelected,
499
+ children: [
500
+ /* @__PURE__ */ jsx(
501
+ ComboboxItemCheck,
502
+ {
503
+ checked: isSelected,
504
+ render: ({ style, ...props }) => (
505
+ // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
506
+ /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
507
+ ),
508
+ children: /* @__PURE__ */ jsx(
509
+ IconCheckMark,
510
+ {
511
+ size: "small",
512
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-item-check" : void 0
513
+ }
514
+ )
515
+ }
516
+ ),
517
+ children
518
+ ]
519
+ }
520
+ );
521
+ }
522
+ );
523
+
532
524
  const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
533
525
 
534
526
  const StyledGroup = styled(Group$1, {});
535
527
 
536
528
  const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
537
- const { autoFilter, filteredItems } = useComboboxContext();
538
- const childValues = useMemo(
539
- // don't perform calculation if auto filter is disabled
540
- () => autoFilter !== false ? getChildrenItemValues(children) : [],
541
- [children, autoFilter]
542
- );
543
- const hasVisibleChildren = useMemo(
544
- () => (
545
- // don't perform calculation if auto filter is disabled
546
- autoFilter !== false ? childValues.some((value) => filteredItems.has(value)) : true
547
- ),
548
- [childValues, filteredItems, autoFilter]
549
- );
529
+ const { autoFilter, searchValue, filteredItems } = useComboboxContext();
530
+ const id = useId();
550
531
  const getInvisibleContent = useInvisibleContent();
551
- if (!hasVisibleChildren) {
552
- return getInvisibleContent(children);
532
+ let hasVisibleContent = true;
533
+ if (autoFilter && searchValue.length > 0) {
534
+ hasVisibleContent = filteredItems.some((item) => item.groupId === id);
553
535
  }
554
- return /* @__PURE__ */ jsx(StyledGroup, { ...rest, ref: forwardRef, children });
536
+ return /* @__PURE__ */ jsx(GroupProvider, { groupId: id, children: hasVisibleContent ? /* @__PURE__ */ jsx(StyledGroup, { ...rest, ref: forwardRef, children }) : getInvisibleContent(children) });
555
537
  });
556
538
 
557
539
  const StyledGroupLabel = styled(GroupLabel$1, groupLabelStyles);
558
540
 
559
541
  const GroupLabel = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroupLabel, { ...props, ref: forwardRef }));
560
542
 
561
- const StyledChip = styled(Primitive.div, {
562
- fontSize: "$150",
563
- padding: "$50 $100",
564
- borderRadius: "$round",
565
- display: "flex",
566
- alignItems: "center",
567
- gap: "$50",
568
- whiteSpace: "nowrap",
569
- maxWidth: "$35",
570
- backgroundColor: "$background-neutrals-subtle",
571
- color: "$text-neutrals"
572
- });
573
- const StyledChipButton = styled(BaseButton, {
574
- color: "$icon-neutrals-inactive",
575
- ...focus.css({
576
- boxShadow: "$focus-small-outline"
577
- })
578
- });
579
- const StyledChipContent = styled(Primitive.div, {
580
- textOverflow: "ellipsis",
581
- whiteSpace: "nowrap",
582
- overflow: "hidden",
583
- lineHeight: 1.3
584
- });
585
-
586
- const StyledLeftSlot = styled(Primitive.span, {
587
- order: -1,
588
- marginRight: "$50"
589
- });
590
-
591
- const LeftSlot = StyledLeftSlot;
592
-
593
- const Chip = React.forwardRef(
594
- ({ children, disabled = false, onRemove, removeAriaLabel, ...restProps }, forwardRef) => /* @__PURE__ */ jsxs(StyledChip, { ...restProps, ref: forwardRef, children: [
595
- /* @__PURE__ */ jsx(StyledChipContent, { children }),
596
- !booleanify(disabled) && /* @__PURE__ */ jsx(StyledChipButton, { onClick: onRemove, "aria-label": removeAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", "aria-hidden": true }) })
597
- ] })
598
- );
599
- Chip.LeftSlot = LeftSlot;
600
-
601
543
  const Value = ({ unselectAriaLabel }) => {
602
544
  const {
603
545
  value = [],
604
546
  setValue,
605
547
  disabled,
548
+ readOnly,
606
549
  "aria-disabled": ariaDisabled,
607
- itemValueTextMap
550
+ itemsMap
608
551
  } = useComboboxContext();
609
- const isDisabled = ariaDisabled === true || disabled;
552
+ const canRemoveItem = !booleanify(ariaDisabled) && !booleanify(disabled) && !booleanify(readOnly);
610
553
  const onItemRemove = useCallback(
611
554
  (item) => {
612
555
  setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
@@ -615,8 +558,8 @@ const Value = ({ unselectAriaLabel }) => {
615
558
  );
616
559
  const getItemText = useCallback(
617
560
  (itemValue) => {
618
- const textValue = itemValueTextMap.get(itemValue);
619
- if (textValue === void 0 || textValue === "") {
561
+ const itemData = itemsMap.get(itemValue);
562
+ if (itemData === void 0 || itemData.displayedText === "") {
620
563
  return null;
621
564
  }
622
565
  return /* @__PURE__ */ jsx(
@@ -626,15 +569,15 @@ const Value = ({ unselectAriaLabel }) => {
626
569
  onItemRemove(itemValue);
627
570
  e.stopPropagation();
628
571
  },
629
- disabled: isDisabled,
630
- removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(textValue),
572
+ removable: canRemoveItem,
573
+ removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(itemData.displayedText),
631
574
  "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(itemValue) : void 0,
632
- children: textValue
575
+ children: itemData.displayedText
633
576
  },
634
577
  itemValue
635
578
  );
636
579
  },
637
- [isDisabled, itemValueTextMap, onItemRemove, unselectAriaLabel]
580
+ [canRemoveItem, itemsMap, onItemRemove, unselectAriaLabel]
638
581
  );
639
582
  return /* @__PURE__ */ jsx(Fragment, { children: value.map(getItemText) });
640
583
  };
@@ -643,7 +586,7 @@ const StyledSeparator = styled(Primitive.div, separatorStyles);
643
586
 
644
587
  const Separator = React.forwardRef((props, forwardRef) => {
645
588
  const { autoFilter, searchValue } = useComboboxContext();
646
- if (autoFilter === true && searchValue !== void 0 && searchValue.length > 0) {
589
+ if (autoFilter && searchValue.length > 0) {
647
590
  return null;
648
591
  }
649
592
  return /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true });
@@ -663,11 +606,13 @@ const StyledNoResult = styled(Primitive.div, {
663
606
  });
664
607
 
665
608
  const NoResult = React.forwardRef((props, forwardRef) => {
666
- const { filteredItems } = useComboboxContext();
667
- if (filteredItems.size !== 0) {
668
- return null;
609
+ const { autoFilter, searchValue, filteredItems, itemsMap } = useComboboxContext();
610
+ const noActiveFiltering = !autoFilter || autoFilter && searchValue.length === 0;
611
+ const isVisible = noActiveFiltering ? itemsMap.size === 0 : filteredItems.length === 0;
612
+ if (isVisible) {
613
+ return /* @__PURE__ */ jsx(StyledNoResult, { ...props, ref: forwardRef });
669
614
  }
670
- return /* @__PURE__ */ jsx(StyledNoResult, { ...props, ref: forwardRef });
615
+ return null;
671
616
  });
672
617
 
673
618
  const Root = React.forwardRef(
@@ -676,7 +621,6 @@ const Root = React.forwardRef(
676
621
  const {
677
622
  openState,
678
623
  setOpenState,
679
- defaultValue,
680
624
  value = [],
681
625
  setValue,
682
626
  required,
@@ -730,7 +674,6 @@ const Root = React.forwardRef(
730
674
  {
731
675
  open: openState,
732
676
  setOpen: onOpenChange,
733
- defaultSelectedValue: defaultValue,
734
677
  selectedValue: value,
735
678
  setSelectedValue: onSetSelectedValue,
736
679
  children: /* @__PURE__ */ jsxs(