@mirohq/design-system-combobox 0.3.5 → 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';
@@ -11,13 +11,13 @@ import { styled, theme } from '@mirohq/design-system-stitches';
11
11
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
12
12
  import { IconChevronDown, IconCross, IconCheckMark } from '@mirohq/design-system-icons';
13
13
  import { ScrollArea } from '@mirohq/design-system-scroll-area';
14
- import { contentStyles, itemStyles, StyledItemCheck, groupLabelStyles, separatorStyles } from '@mirohq/design-system-base-select';
15
- import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
16
- import { useLayoutEffect } from '@mirohq/design-system-use-layout-effect';
17
- import { createPortal } from 'react-dom';
14
+ import { itemsContainerStyles, contentStyles, itemStyles, StyledItemCheck, groupLabelStyles, separatorStyles } from '@mirohq/design-system-base-select';
18
15
  import { Primitive } from '@mirohq/design-system-primitive';
19
- import { BaseButton } from '@mirohq/design-system-base-button';
20
- import { focus } from '@mirohq/design-system-styles';
16
+ import { createPortal } from 'react-dom';
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,
@@ -325,6 +332,7 @@ const Trigger = React.forwardRef(
325
332
  }
326
333
  );
327
334
 
335
+ const StyledItemsContainer = styled(Primitive.div, itemsContainerStyles);
328
336
  const StyledContent = styled(RadixPopover.Content, {
329
337
  ...contentStyles,
330
338
  width: "var(--radix-popover-trigger-width)",
@@ -332,103 +340,6 @@ const StyledContent = styled(RadixPopover.Content, {
332
340
  boxSizing: "border-box"
333
341
  });
334
342
 
335
- const StyledItem = styled(ComboboxItem, itemStyles);
336
-
337
- const Item = React.forwardRef(
338
- ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
339
- const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
340
- const {
341
- autoFilter,
342
- filteredItems,
343
- setItemValueTextMap,
344
- triggerRef,
345
- inputRef,
346
- value: comboboxValue = []
347
- } = useComboboxContext();
348
- useLayoutEffect(() => {
349
- const textToSet = textValue !== void 0 ? textValue : typeof children === "string" ? children : "";
350
- setItemValueTextMap((prevState) => new Map(prevState.set(value, textToSet)));
351
- return () => {
352
- setItemValueTextMap((prevState) => {
353
- prevState.delete(value);
354
- return new Map(prevState);
355
- });
356
- };
357
- }, [setItemValueTextMap, value, textValue, children]);
358
- if (autoFilter !== false && !filteredItems.has(value)) {
359
- return null;
360
- }
361
- const scrollIntoView = (event) => {
362
- var _a;
363
- if (((_a = inputRef == null ? void 0 : inputRef.current) == null ? void 0 : _a.parentElement) != null && (triggerRef == null ? void 0 : triggerRef.current) != null) {
364
- inputRef.current.parentElement.scrollTo({
365
- top: triggerRef.current.scrollHeight
366
- });
367
- }
368
- if (restProps.onClick !== void 0) {
369
- restProps.onClick(event);
370
- }
371
- };
372
- const isSelected = comboboxValue.includes(value);
373
- return /* @__PURE__ */ jsxs(
374
- StyledItem,
375
- {
376
- ...mergeProps(restProps, restAriaDisabledProps),
377
- focusable: true,
378
- hideOnClick: false,
379
- accessibleWhenDisabled: booleanify(ariaDisabled),
380
- disabled: booleanify(ariaDisabled) || disabled,
381
- ref: forwardRef,
382
- value,
383
- onClick: scrollIntoView,
384
- "aria-selected": isSelected,
385
- children: [
386
- /* @__PURE__ */ jsx(
387
- ComboboxItemCheck,
388
- {
389
- checked: isSelected,
390
- render: ({ style, ...props }) => (
391
- // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
392
- /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
393
- ),
394
- children: /* @__PURE__ */ jsx(
395
- IconCheckMark,
396
- {
397
- size: "small",
398
- "data-testid": process.env.NODE_ENV === "test" ? "combobox-item-check" : void 0
399
- }
400
- )
401
- }
402
- ),
403
- children
404
- ]
405
- }
406
- );
407
- }
408
- );
409
-
410
- const itemType = React.createElement(Item).type;
411
- const getChildrenItemValues = (componentChildren) => {
412
- const values = [];
413
- const recurse = (children) => {
414
- React.Children.forEach(children, (child) => {
415
- if (!React.isValidElement(child)) {
416
- return;
417
- }
418
- if (child.type === itemType) {
419
- const props = child.props;
420
- values.push(props.value);
421
- return;
422
- }
423
- if (child.props.children) {
424
- recurse(child.props.children);
425
- }
426
- });
427
- };
428
- recurse(componentChildren);
429
- return values;
430
- };
431
-
432
343
  const useDocumentFragment = () => {
433
344
  const [fragment, setFragment] = React.useState();
434
345
  useLayoutEffect(() => {
@@ -446,6 +357,7 @@ const useInvisibleContent = () => {
446
357
  };
447
358
 
448
359
  const CONTENT_OFFSET = parseInt(theme.space[50]);
360
+ const RADIX_CONTENT_AVAILABLE_HEIGHT = "var(--radix-popover-content-available-height)";
449
361
  const isInsideRef = (element, ref) => {
450
362
  var _a, _b;
451
363
  return (_b = element != null && ((_a = ref.current) == null ? void 0 : _a.contains(element))) != null ? _b : false;
@@ -465,31 +377,15 @@ const Content = React.forwardRef(
465
377
  children,
466
378
  ...restProps
467
379
  }, forwardRef) => {
468
- const {
469
- triggerRef,
470
- contentRef,
471
- autoFilter,
472
- setFilteredItems,
473
- searchValue,
474
- direction,
475
- openState
476
- } = useComboboxContext();
477
- useEffect(() => {
478
- const childrenItemValues = getChildrenItemValues(children);
479
- const shouldFilter = autoFilter !== false && searchValue !== void 0 && searchValue.length > 0;
480
- const items = shouldFilter ? childrenItemValues.filter(
481
- (child) => child.toLowerCase().includes(searchValue.toLowerCase())
482
- ) : childrenItemValues;
483
- setFilteredItems(new Set(items));
484
- }, [children, autoFilter, setFilteredItems, searchValue]);
380
+ const { triggerRef, contentRef, direction, openState } = useComboboxContext();
485
381
  const getInvisibleContent = useInvisibleContent();
486
382
  if (!openState) {
487
383
  return getInvisibleContent(children);
488
384
  }
385
+ const content = /* @__PURE__ */ jsx(StyledItemsContainer, { children });
489
386
  return /* @__PURE__ */ jsx(
490
387
  StyledContent,
491
388
  {
492
- asChild: true,
493
389
  ...restProps,
494
390
  dir: direction,
495
391
  side,
@@ -514,14 +410,112 @@ const Content = React.forwardRef(
514
410
  /* @__PURE__ */ jsx(
515
411
  ScrollArea.Viewport,
516
412
  {
517
- availableHeight: "var(--radix-popover-content-available-height)",
518
- verticalGap: "var(--space-50) * 2",
519
- maxHeight,
520
- children
413
+ css: {
414
+ maxHeight: maxHeight !== void 0 ? "min(".concat(RADIX_CONTENT_AVAILABLE_HEIGHT, ", ").concat(typeof maxHeight === "number" ? "".concat(maxHeight, "px") : maxHeight, ")") : RADIX_CONTENT_AVAILABLE_HEIGHT
415
+ },
416
+ children: content
521
417
  }
522
418
  ),
523
419
  /* @__PURE__ */ jsx(ScrollArea.Scrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollArea.Thumb, {}) })
524
- ] }) : children })
420
+ ] }) : content })
421
+ }
422
+ );
423
+ }
424
+ );
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
+ ]
525
519
  }
526
520
  );
527
521
  }
@@ -532,79 +526,30 @@ const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
532
526
  const StyledGroup = styled(Group$1, {});
533
527
 
534
528
  const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
535
- const { autoFilter, filteredItems } = useComboboxContext();
536
- const childValues = useMemo(
537
- // don't perform calculation if auto filter is disabled
538
- () => autoFilter !== false ? getChildrenItemValues(children) : [],
539
- [children, autoFilter]
540
- );
541
- const hasVisibleChildren = useMemo(
542
- () => (
543
- // don't perform calculation if auto filter is disabled
544
- autoFilter !== false ? childValues.some((value) => filteredItems.has(value)) : true
545
- ),
546
- [childValues, filteredItems, autoFilter]
547
- );
529
+ const { autoFilter, searchValue, filteredItems } = useComboboxContext();
530
+ const id = useId();
548
531
  const getInvisibleContent = useInvisibleContent();
549
- if (!hasVisibleChildren) {
550
- return getInvisibleContent(children);
532
+ let hasVisibleContent = true;
533
+ if (autoFilter && searchValue.length > 0) {
534
+ hasVisibleContent = filteredItems.some((item) => item.groupId === id);
551
535
  }
552
- 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) });
553
537
  });
554
538
 
555
539
  const StyledGroupLabel = styled(GroupLabel$1, groupLabelStyles);
556
540
 
557
541
  const GroupLabel = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroupLabel, { ...props, ref: forwardRef }));
558
542
 
559
- const StyledChip = styled(Primitive.div, {
560
- fontSize: "$150",
561
- padding: "$50 $100",
562
- borderRadius: "$round",
563
- display: "flex",
564
- alignItems: "center",
565
- gap: "$50",
566
- whiteSpace: "nowrap",
567
- maxWidth: "$35",
568
- backgroundColor: "$background-neutrals-subtle",
569
- color: "$text-neutrals"
570
- });
571
- const StyledChipButton = styled(BaseButton, {
572
- color: "$icon-neutrals-inactive",
573
- ...focus.css({
574
- boxShadow: "$focus-small-outline"
575
- })
576
- });
577
- const StyledChipContent = styled(Primitive.div, {
578
- textOverflow: "ellipsis",
579
- whiteSpace: "nowrap",
580
- overflow: "hidden",
581
- lineHeight: 1.3
582
- });
583
-
584
- const StyledLeftSlot = styled(Primitive.span, {
585
- order: -1,
586
- marginRight: "$50"
587
- });
588
-
589
- const LeftSlot = StyledLeftSlot;
590
-
591
- const Chip = React.forwardRef(
592
- ({ children, disabled = false, onRemove, removeAriaLabel, ...restProps }, forwardRef) => /* @__PURE__ */ jsxs(StyledChip, { ...restProps, ref: forwardRef, children: [
593
- /* @__PURE__ */ jsx(StyledChipContent, { children }),
594
- !booleanify(disabled) && /* @__PURE__ */ jsx(StyledChipButton, { onClick: onRemove, "aria-label": removeAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", "aria-hidden": true }) })
595
- ] })
596
- );
597
- Chip.LeftSlot = LeftSlot;
598
-
599
543
  const Value = ({ unselectAriaLabel }) => {
600
544
  const {
601
545
  value = [],
602
546
  setValue,
603
547
  disabled,
548
+ readOnly,
604
549
  "aria-disabled": ariaDisabled,
605
- itemValueTextMap
550
+ itemsMap
606
551
  } = useComboboxContext();
607
- const isDisabled = ariaDisabled === true || disabled;
552
+ const canRemoveItem = !booleanify(ariaDisabled) && !booleanify(disabled) && !booleanify(readOnly);
608
553
  const onItemRemove = useCallback(
609
554
  (item) => {
610
555
  setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
@@ -613,8 +558,8 @@ const Value = ({ unselectAriaLabel }) => {
613
558
  );
614
559
  const getItemText = useCallback(
615
560
  (itemValue) => {
616
- const textValue = itemValueTextMap.get(itemValue);
617
- if (textValue === void 0 || textValue === "") {
561
+ const itemData = itemsMap.get(itemValue);
562
+ if (itemData === void 0 || itemData.displayedText === "") {
618
563
  return null;
619
564
  }
620
565
  return /* @__PURE__ */ jsx(
@@ -624,15 +569,15 @@ const Value = ({ unselectAriaLabel }) => {
624
569
  onItemRemove(itemValue);
625
570
  e.stopPropagation();
626
571
  },
627
- disabled: isDisabled,
628
- removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(textValue),
572
+ removable: canRemoveItem,
573
+ removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(itemData.displayedText),
629
574
  "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(itemValue) : void 0,
630
- children: textValue
575
+ children: itemData.displayedText
631
576
  },
632
577
  itemValue
633
578
  );
634
579
  },
635
- [isDisabled, itemValueTextMap, onItemRemove, unselectAriaLabel]
580
+ [canRemoveItem, itemsMap, onItemRemove, unselectAriaLabel]
636
581
  );
637
582
  return /* @__PURE__ */ jsx(Fragment, { children: value.map(getItemText) });
638
583
  };
@@ -641,7 +586,7 @@ const StyledSeparator = styled(Primitive.div, separatorStyles);
641
586
 
642
587
  const Separator = React.forwardRef((props, forwardRef) => {
643
588
  const { autoFilter, searchValue } = useComboboxContext();
644
- if (autoFilter === true && searchValue !== void 0 && searchValue.length > 0) {
589
+ if (autoFilter && searchValue.length > 0) {
645
590
  return null;
646
591
  }
647
592
  return /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true });
@@ -661,11 +606,13 @@ const StyledNoResult = styled(Primitive.div, {
661
606
  });
662
607
 
663
608
  const NoResult = React.forwardRef((props, forwardRef) => {
664
- const { filteredItems } = useComboboxContext();
665
- if (filteredItems.size !== 0) {
666
- 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 });
667
614
  }
668
- return /* @__PURE__ */ jsx(StyledNoResult, { ...props, ref: forwardRef });
615
+ return null;
669
616
  });
670
617
 
671
618
  const Root = React.forwardRef(
@@ -674,7 +621,6 @@ const Root = React.forwardRef(
674
621
  const {
675
622
  openState,
676
623
  setOpenState,
677
- defaultValue,
678
624
  value = [],
679
625
  setValue,
680
626
  required,
@@ -728,7 +674,6 @@ const Root = React.forwardRef(
728
674
  {
729
675
  open: openState,
730
676
  setOpen: onOpenChange,
731
- defaultSelectedValue: defaultValue,
732
677
  selectedValue: value,
733
678
  setSelectedValue: onSetSelectedValue,
734
679
  children: /* @__PURE__ */ jsxs(