@mirohq/design-system-combobox 0.3.6 → 0.4.0

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",
@@ -34,6 +34,13 @@ const StyledBaseInput = styled(BaseInput, {
34
34
  },
35
35
  variants: {
36
36
  size: {
37
+ medium: {
38
+ minHeight: "$8",
39
+ height: "auto",
40
+ padding: "5px $100",
41
+ paddingRight: "$500",
42
+ fontSize: "$175"
43
+ },
37
44
  large: {
38
45
  minHeight: "$10",
39
46
  height: "auto",
@@ -53,6 +60,10 @@ const StyledBaseInput = styled(BaseInput, {
53
60
  }
54
61
  });
55
62
 
63
+ function searchQueryMatch(displayedText, searchValue) {
64
+ return displayedText.toLowerCase().includes(searchValue.toLowerCase());
65
+ }
66
+
56
67
  const ComboboxContext = createContext({});
57
68
  const ComboboxProvider = ({
58
69
  children,
@@ -66,13 +77,12 @@ const ComboboxProvider = ({
66
77
  onValueChange,
67
78
  searchValue: searchValueProp,
68
79
  onSearchValueChange,
69
- autoFilter = true,
80
+ autoFilter,
70
81
  ...restProps
71
82
  }) => {
72
83
  const triggerRef = useRef(null);
73
84
  const inputRef = useRef(null);
74
85
  const contentRef = useRef(null);
75
- const [defaultValue, setDefaultValue] = useState(defaultValueProp);
76
86
  const [openState = false, setOpenState] = useControllableState({
77
87
  prop: openProp,
78
88
  defaultProp: defaultOpen,
@@ -89,16 +99,23 @@ const ComboboxProvider = ({
89
99
  defaultProp: defaultValueProp,
90
100
  onChange: onValueChange
91
101
  });
92
- const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
93
- const [searchValue, setSearchValue] = useControllableState({
102
+ const [searchValue = "", setSearchValue] = useControllableState({
94
103
  prop: searchValueProp,
95
104
  defaultProp: "",
96
105
  onChange: onSearchValueChange
97
106
  });
98
107
  const [size, setSize] = useState();
99
108
  const [placeholder, setPlaceholder] = useState();
100
- const [itemValueTextMap, setItemValueTextMap] = useState(/* @__PURE__ */ new Map());
109
+ const [itemsMap, setItemsMap] = useState(/* @__PURE__ */ new Map());
101
110
  const { valid: formFieldValid } = useFormFieldContext();
111
+ const filteredItems = useMemo(() => {
112
+ if (searchValue.length > 0) {
113
+ return Array.from(itemsMap.values()).filter(
114
+ (item) => searchQueryMatch(item.displayedText, searchValue)
115
+ );
116
+ }
117
+ return [];
118
+ }, [itemsMap, searchValue]);
102
119
  return /* @__PURE__ */ jsx(
103
120
  ComboboxContext.Provider,
104
121
  {
@@ -109,18 +126,15 @@ const ComboboxProvider = ({
109
126
  setOpenState,
110
127
  value,
111
128
  setValue,
112
- setDefaultValue,
113
- defaultValue,
114
129
  triggerRef,
115
130
  inputRef,
116
131
  contentRef,
117
132
  autoFilter,
118
133
  searchValue,
119
134
  setSearchValue,
135
+ itemsMap,
136
+ setItemsMap,
120
137
  filteredItems,
121
- setFilteredItems,
122
- itemValueTextMap,
123
- setItemValueTextMap,
124
138
  placeholder,
125
139
  setPlaceholder,
126
140
  size,
@@ -137,6 +151,9 @@ const StyledActionButton = styled(BaseInput.ActionButton, {
137
151
  right: "$100",
138
152
  variants: {
139
153
  size: {
154
+ medium: {
155
+ top: "3px"
156
+ },
140
157
  large: {
141
158
  top: "5px"
142
159
  },
@@ -207,7 +224,7 @@ const Trigger = React.forwardRef(
207
224
  placeholder,
208
225
  openActionLabel,
209
226
  closeActionLabel,
210
- clearable,
227
+ clearable = true,
211
228
  clearActionLabel,
212
229
  onChange,
213
230
  onFocus,
@@ -333,103 +350,6 @@ const StyledContent = styled(RadixPopover.Content, {
333
350
  boxSizing: "border-box"
334
351
  });
335
352
 
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
353
  const useDocumentFragment = () => {
434
354
  const [fragment, setFragment] = React.useState();
435
355
  useLayoutEffect(() => {
@@ -467,23 +387,7 @@ const Content = React.forwardRef(
467
387
  children,
468
388
  ...restProps
469
389
  }, 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]);
390
+ const { triggerRef, contentRef, direction, openState } = useComboboxContext();
487
391
  const getInvisibleContent = useInvisibleContent();
488
392
  if (!openState) {
489
393
  return getInvisibleContent(children);
@@ -529,84 +433,133 @@ const Content = React.forwardRef(
529
433
  }
530
434
  );
531
435
 
436
+ const StyledItem = styled(ComboboxItem, itemStyles);
437
+
438
+ const GroupContext = createContext({});
439
+ const GroupProvider = ({
440
+ children,
441
+ ...restProps
442
+ }) => /* @__PURE__ */ jsx(
443
+ GroupContext.Provider,
444
+ {
445
+ value: {
446
+ ...restProps
447
+ },
448
+ children
449
+ }
450
+ );
451
+ const useGroupContext = () => useContext(GroupContext);
452
+
453
+ const Item = React.forwardRef(
454
+ ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
455
+ const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
456
+ const {
457
+ searchValue,
458
+ autoFilter,
459
+ setItemsMap,
460
+ triggerRef,
461
+ inputRef,
462
+ value: comboboxValue = []
463
+ } = useComboboxContext();
464
+ const { groupId } = useGroupContext();
465
+ const displayedText = useMemo(() => {
466
+ if (textValue !== void 0) {
467
+ return textValue;
468
+ }
469
+ return typeof children === "string" ? children : "";
470
+ }, [textValue, children]);
471
+ useLayoutEffect(() => {
472
+ setItemsMap(
473
+ (prevState) => new Map(prevState.set(value, { displayedText, groupId }))
474
+ );
475
+ return () => {
476
+ setItemsMap((prevState) => {
477
+ prevState.delete(value);
478
+ return new Map(prevState);
479
+ });
480
+ };
481
+ }, [setItemsMap, groupId, value, displayedText]);
482
+ if (autoFilter && searchValue.length > 0 && !searchQueryMatch(displayedText, searchValue)) {
483
+ return null;
484
+ }
485
+ const scrollIntoView = (event) => {
486
+ var _a;
487
+ if (((_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement) != null && (triggerRef == null ? void 0 : triggerRef.current) != null) {
488
+ inputRef.current.parentElement.scrollTo({
489
+ top: triggerRef.current.scrollHeight
490
+ });
491
+ }
492
+ if (restProps.onClick !== void 0) {
493
+ restProps.onClick(event);
494
+ }
495
+ };
496
+ const isSelected = comboboxValue.includes(value);
497
+ return /* @__PURE__ */ jsxs(
498
+ StyledItem,
499
+ {
500
+ ...mergeProps(restProps, restAriaDisabledProps),
501
+ focusable: true,
502
+ hideOnClick: false,
503
+ accessibleWhenDisabled: booleanify(ariaDisabled),
504
+ disabled: booleanify(ariaDisabled) || disabled,
505
+ ref: forwardRef,
506
+ value,
507
+ onClick: scrollIntoView,
508
+ "aria-selected": isSelected,
509
+ children: [
510
+ /* @__PURE__ */ jsx(
511
+ ComboboxItemCheck,
512
+ {
513
+ checked: isSelected,
514
+ render: ({ style, ...props }) => (
515
+ // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
516
+ /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
517
+ ),
518
+ children: /* @__PURE__ */ jsx(
519
+ IconCheckMark,
520
+ {
521
+ size: "small",
522
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-item-check" : void 0
523
+ }
524
+ )
525
+ }
526
+ ),
527
+ children
528
+ ]
529
+ }
530
+ );
531
+ }
532
+ );
533
+
532
534
  const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
533
535
 
534
536
  const StyledGroup = styled(Group$1, {});
535
537
 
536
538
  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
- );
539
+ const { autoFilter, searchValue, filteredItems } = useComboboxContext();
540
+ const id = useId();
550
541
  const getInvisibleContent = useInvisibleContent();
551
- if (!hasVisibleChildren) {
552
- return getInvisibleContent(children);
542
+ let hasVisibleContent = true;
543
+ if (autoFilter && searchValue.length > 0) {
544
+ hasVisibleContent = filteredItems.some((item) => item.groupId === id);
553
545
  }
554
- return /* @__PURE__ */ jsx(StyledGroup, { ...rest, ref: forwardRef, children });
546
+ return /* @__PURE__ */ jsx(GroupProvider, { groupId: id, children: hasVisibleContent ? /* @__PURE__ */ jsx(StyledGroup, { ...rest, ref: forwardRef, children }) : getInvisibleContent(children) });
555
547
  });
556
548
 
557
549
  const StyledGroupLabel = styled(GroupLabel$1, groupLabelStyles);
558
550
 
559
551
  const GroupLabel = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroupLabel, { ...props, ref: forwardRef }));
560
552
 
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
553
  const Value = ({ unselectAriaLabel }) => {
602
554
  const {
603
555
  value = [],
604
556
  setValue,
605
557
  disabled,
558
+ readOnly,
606
559
  "aria-disabled": ariaDisabled,
607
- itemValueTextMap
560
+ itemsMap
608
561
  } = useComboboxContext();
609
- const isDisabled = ariaDisabled === true || disabled;
562
+ const canRemoveItem = !booleanify(ariaDisabled) && !booleanify(disabled) && !booleanify(readOnly);
610
563
  const onItemRemove = useCallback(
611
564
  (item) => {
612
565
  setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
@@ -615,8 +568,8 @@ const Value = ({ unselectAriaLabel }) => {
615
568
  );
616
569
  const getItemText = useCallback(
617
570
  (itemValue) => {
618
- const textValue = itemValueTextMap.get(itemValue);
619
- if (textValue === void 0 || textValue === "") {
571
+ const itemData = itemsMap.get(itemValue);
572
+ if (itemData === void 0 || itemData.displayedText === "") {
620
573
  return null;
621
574
  }
622
575
  return /* @__PURE__ */ jsx(
@@ -626,15 +579,15 @@ const Value = ({ unselectAriaLabel }) => {
626
579
  onItemRemove(itemValue);
627
580
  e.stopPropagation();
628
581
  },
629
- disabled: isDisabled,
630
- removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(textValue),
582
+ removable: canRemoveItem,
583
+ removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(itemData.displayedText),
631
584
  "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(itemValue) : void 0,
632
- children: textValue
585
+ children: itemData.displayedText
633
586
  },
634
587
  itemValue
635
588
  );
636
589
  },
637
- [isDisabled, itemValueTextMap, onItemRemove, unselectAriaLabel]
590
+ [canRemoveItem, itemsMap, onItemRemove, unselectAriaLabel]
638
591
  );
639
592
  return /* @__PURE__ */ jsx(Fragment, { children: value.map(getItemText) });
640
593
  };
@@ -643,7 +596,7 @@ const StyledSeparator = styled(Primitive.div, separatorStyles);
643
596
 
644
597
  const Separator = React.forwardRef((props, forwardRef) => {
645
598
  const { autoFilter, searchValue } = useComboboxContext();
646
- if (autoFilter === true && searchValue !== void 0 && searchValue.length > 0) {
599
+ if (autoFilter && searchValue.length > 0) {
647
600
  return null;
648
601
  }
649
602
  return /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true });
@@ -663,11 +616,13 @@ const StyledNoResult = styled(Primitive.div, {
663
616
  });
664
617
 
665
618
  const NoResult = React.forwardRef((props, forwardRef) => {
666
- const { filteredItems } = useComboboxContext();
667
- if (filteredItems.size !== 0) {
668
- return null;
619
+ const { autoFilter, searchValue, filteredItems, itemsMap } = useComboboxContext();
620
+ const noActiveFiltering = !autoFilter || autoFilter && searchValue.length === 0;
621
+ const isVisible = noActiveFiltering ? itemsMap.size === 0 : filteredItems.length === 0;
622
+ if (isVisible) {
623
+ return /* @__PURE__ */ jsx(StyledNoResult, { ...props, ref: forwardRef });
669
624
  }
670
- return /* @__PURE__ */ jsx(StyledNoResult, { ...props, ref: forwardRef });
625
+ return null;
671
626
  });
672
627
 
673
628
  const Root = React.forwardRef(
@@ -676,7 +631,6 @@ const Root = React.forwardRef(
676
631
  const {
677
632
  openState,
678
633
  setOpenState,
679
- defaultValue,
680
634
  value = [],
681
635
  setValue,
682
636
  required,
@@ -730,7 +684,6 @@ const Root = React.forwardRef(
730
684
  {
731
685
  open: openState,
732
686
  setOpen: onOpenChange,
733
- defaultSelectedValue: defaultValue,
734
687
  selectedValue: value,
735
688
  setSelectedValue: onSetSelectedValue,
736
689
  children: /* @__PURE__ */ jsxs(