@mirohq/design-system-combobox 0.1.0-combobox.8 → 0.1.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,10 +1,10 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { createContext, useContext, useRef, useState, useCallback, useEffect, useMemo } from 'react';
2
+ import React, { createContext, useRef, useState, useContext, useCallback, useEffect, useMemo } from 'react';
3
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
4
  import { useFormFieldContext, FloatingLabel } from '@mirohq/design-system-base-form';
5
- import * as RadixPopover from '@radix-ui/react-popover';
6
- import { Anchor, Trigger as Trigger$1, Portal as Portal$1 } from '@radix-ui/react-popover';
7
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
8
  import { styled, theme } from '@mirohq/design-system-stitches';
9
9
  import { Input } from '@mirohq/design-system-input';
10
10
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
@@ -13,17 +13,15 @@ import { ScrollArea } from '@mirohq/design-system-scroll-area';
13
13
  import { Primitive } from '@mirohq/design-system-primitive';
14
14
  import { mergeProps } from '@react-aria/utils';
15
15
  import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
16
+ import { useLayoutEffect } from '@mirohq/design-system-use-layout-effect';
16
17
  import { focus } from '@mirohq/design-system-styles';
18
+ import { createPortal } from 'react-dom';
17
19
  import { BaseButton } from '@mirohq/design-system-base-button';
18
20
 
19
- const StyledAnchor = styled(Anchor, {
20
- position: "relative",
21
- width: "100%"
22
- });
23
21
  const StyledInput = styled(Input, {
24
22
  flexWrap: "wrap",
25
23
  flexGrow: 1,
26
- gap: "0 $50",
24
+ gap: "$50",
27
25
  overflowY: "scroll",
28
26
  "&[data-valid], &[data-invalid]": {
29
27
  // we don't need a bigger padding here as Input component will render its own icon
@@ -92,6 +90,9 @@ const ComboboxProvider = ({
92
90
  });
93
91
  const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
94
92
  const [searchValue, setSearchValue] = useState("");
93
+ const [size, setSize] = useState();
94
+ const [placeholder, setPlaceholder] = useState();
95
+ const [itemValueTextMap, setItemValueTextMap] = useState(/* @__PURE__ */ new Map());
95
96
  const { valid: formFieldValid } = useFormFieldContext();
96
97
  return /* @__PURE__ */ jsx(
97
98
  ComboboxContext.Provider,
@@ -113,7 +114,13 @@ const ComboboxProvider = ({
113
114
  searchValue,
114
115
  setSearchValue,
115
116
  filteredItems,
116
- setFilteredItems
117
+ setFilteredItems,
118
+ itemValueTextMap,
119
+ setItemValueTextMap,
120
+ placeholder,
121
+ setPlaceholder,
122
+ size,
123
+ setSize
117
124
  },
118
125
  children
119
126
  }
@@ -197,7 +204,6 @@ const Trigger = React.forwardRef(
197
204
  closeActionLabel,
198
205
  clearActionLabel,
199
206
  onChange,
200
- css,
201
207
  ...restProps
202
208
  }, forwardRef) => {
203
209
  const {
@@ -211,16 +217,21 @@ const Trigger = React.forwardRef(
211
217
  onSearchValueChange,
212
218
  searchValue,
213
219
  setSearchValue,
214
- setOpenState
220
+ setOpenState,
221
+ setSize,
222
+ setPlaceholder
215
223
  } = useComboboxContext();
216
224
  const {
217
225
  formElementId,
218
226
  ariaInvalid: formFieldAriaInvalid,
219
- valid: formFieldValid,
220
- label,
221
- isFloatingLabel,
222
- focused
227
+ valid: formFieldValid
223
228
  } = useFormFieldContext();
229
+ useEffect(() => {
230
+ setSize(size);
231
+ }, [size, setSize]);
232
+ useEffect(() => {
233
+ setPlaceholder(placeholder);
234
+ }, [setPlaceholder, placeholder]);
224
235
  const valid = formFieldValid != null ? formFieldValid : comboboxValid;
225
236
  const inputProps = {
226
237
  ...restProps,
@@ -235,8 +246,6 @@ const Trigger = React.forwardRef(
235
246
  id: id != null ? id : formElementId,
236
247
  placeholder: value.length === 0 ? placeholder : void 0
237
248
  };
238
- const shouldUseFloatingLabel = label !== null && isFloatingLabel;
239
- const isFloating = placeholder !== void 0 || value.length !== 0 || focused || searchValue !== "";
240
249
  const scrollIntoView = (event) => {
241
250
  var _a;
242
251
  const trigger = triggerRef == null ? void 0 : triggerRef.current;
@@ -256,47 +265,43 @@ const Trigger = React.forwardRef(
256
265
  onSearchValueChange == null ? void 0 : onSearchValueChange(e.target.value);
257
266
  onChange == null ? void 0 : onChange(e);
258
267
  };
259
- return /* @__PURE__ */ jsxs(
260
- StyledAnchor,
268
+ return /* @__PURE__ */ jsx(
269
+ Anchor,
261
270
  {
262
271
  ref: mergeRefs([triggerRef, forwardRef]),
263
- css,
264
272
  onClick: () => {
265
273
  if (!booleanify(disabled) && !booleanify(ariaDisabled) && !booleanify(readOnly)) {
266
274
  setOpenState(true);
267
275
  }
268
276
  },
269
- children: [
270
- shouldUseFloatingLabel && /* @__PURE__ */ jsx(FloatingLabel, { floating: isFloating, size, children: label }),
271
- /* @__PURE__ */ jsx(
272
- Combobox$1,
273
- {
274
- render: /* @__PURE__ */ jsxs(
275
- StyledInput,
276
- {
277
- ...inputProps,
278
- value: searchValue,
279
- size,
280
- ref: inputRef,
281
- onChange: onInputChange,
282
- onFocus: scrollIntoView,
283
- children: [
284
- children,
285
- /* @__PURE__ */ jsx(
286
- TriggerActionButton,
287
- {
288
- openActionLabel,
289
- closeActionLabel,
290
- clearActionLabel,
291
- size
292
- }
293
- )
294
- ]
295
- }
296
- )
297
- }
298
- )
299
- ]
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
+ )
300
305
  }
301
306
  );
302
307
  }
@@ -362,7 +367,24 @@ const StyledItem = styled(ComboboxItem, {
362
367
  const Item = React.forwardRef(
363
368
  ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
364
369
  const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
365
- const { autoFilter, filteredItems, triggerRef, inputRef } = useComboboxContext();
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]);
366
388
  if (autoFilter !== false && !filteredItems.has(value)) {
367
389
  return null;
368
390
  }
@@ -377,6 +399,7 @@ const Item = React.forwardRef(
377
399
  restProps.onClick(event);
378
400
  }
379
401
  };
402
+ const isSelected = comboboxValue.includes(value);
380
403
  return /* @__PURE__ */ jsxs(
381
404
  StyledItem,
382
405
  {
@@ -388,10 +411,12 @@ const Item = React.forwardRef(
388
411
  ref: forwardRef,
389
412
  value,
390
413
  onClick: scrollIntoView,
414
+ "aria-selected": isSelected,
391
415
  children: [
392
416
  /* @__PURE__ */ jsx(
393
417
  ComboboxItemCheck,
394
418
  {
419
+ checked: isSelected,
395
420
  render: ({ style, ...props }) => (
396
421
  // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
397
422
  /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
@@ -434,6 +459,22 @@ const getChildrenItemValues = (componentChildren) => {
434
459
  return values;
435
460
  };
436
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
+
437
478
  const CONTENT_OFFSET = parseInt(theme.space[50]);
438
479
  const isInsideRef = (element, ref) => {
439
480
  var _a, _b;
@@ -441,9 +482,16 @@ const isInsideRef = (element, ref) => {
441
482
  };
442
483
  const Content = React.forwardRef(
443
484
  ({
485
+ side = "bottom",
444
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",
445
494
  maxHeight,
446
- overflow,
447
495
  children,
448
496
  ...restProps
449
497
  }, forwardRef) => {
@@ -455,7 +503,8 @@ const Content = React.forwardRef(
455
503
  setFilteredItems,
456
504
  searchValue,
457
505
  noResultsText,
458
- direction
506
+ direction,
507
+ openState
459
508
  } = useComboboxContext();
460
509
  useEffect(() => {
461
510
  const childrenItemValues = getChildrenItemValues(children);
@@ -467,14 +516,28 @@ const Content = React.forwardRef(
467
516
  )
468
517
  );
469
518
  }, [children, autoFilter, setFilteredItems, searchValue]);
470
- const content = filteredItems.size === 0 ? /* @__PURE__ */ jsx(NoResultPlaceholder, { children: noResultsText }) : children;
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;
471
527
  return /* @__PURE__ */ jsx(
472
528
  StyledContent,
473
529
  {
474
530
  asChild: true,
475
531
  ...restProps,
476
532
  dir: direction,
533
+ side,
477
534
  sideOffset,
535
+ align,
536
+ alignOffset,
537
+ avoidCollisions,
538
+ collisionPadding,
539
+ sticky,
540
+ hideWhenDetached,
478
541
  ref: mergeRefs([forwardRef, contentRef]),
479
542
  onOpenAutoFocus: (event) => event.preventDefault(),
480
543
  onInteractOutside: (event) => {
@@ -520,8 +583,9 @@ const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
520
583
  ),
521
584
  [childValues, filteredItems, autoFilter]
522
585
  );
586
+ const getInvisibleContent = useInvisibleContent();
523
587
  if (!hasVisibleChildren) {
524
- return null;
588
+ return getInvisibleContent(children);
525
589
  }
526
590
  return /* @__PURE__ */ jsx(StyledGroup, { ...rest, ref: forwardRef, children });
527
591
  });
@@ -573,35 +637,45 @@ const Chip = React.forwardRef(
573
637
  );
574
638
  Chip.LeftSlot = LeftSlot;
575
639
 
576
- const StyledValue = styled(Chip, {
577
- marginTop: "$50"
578
- });
579
-
580
640
  const Value = ({ unselectAriaLabel }) => {
581
641
  const {
582
642
  value = [],
583
643
  setValue,
584
644
  disabled,
585
- "aria-disabled": ariaDisabled
645
+ "aria-disabled": ariaDisabled,
646
+ itemValueTextMap
586
647
  } = useComboboxContext();
587
648
  const isDisabled = ariaDisabled === true || disabled;
588
- const onItemRemove = (item) => {
589
- setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
590
- };
591
- if (value.length === 0) {
592
- return null;
593
- }
594
- return /* @__PURE__ */ jsx(Fragment, { children: value.map((item) => /* @__PURE__ */ jsx(
595
- StyledValue,
596
- {
597
- onRemove: () => onItemRemove(item),
598
- disabled: isDisabled,
599
- removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(item),
600
- "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(item) : void 0,
601
- children: item
649
+ const onItemRemove = useCallback(
650
+ (item) => {
651
+ setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
602
652
  },
603
- item
604
- )) });
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) });
605
679
  };
606
680
 
607
681
  const StyledSeparator = styled(Primitive.div, {
@@ -619,53 +693,71 @@ const Separator = React.forwardRef((props, forwardRef) => {
619
693
  return /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true });
620
694
  });
621
695
 
622
- const Root = ({
623
- value: valueProp,
624
- children,
625
- ...restProps
626
- }) => {
627
- const {
628
- openState,
629
- setOpenState,
630
- defaultValue,
631
- value,
632
- setValue,
633
- required,
634
- readOnly,
635
- "aria-disabled": ariaDisabled,
636
- disabled
637
- } = useComboboxContext();
638
- const { setRequired, setDisabled, setAriaDisabled, setReadOnly } = useFormFieldContext();
639
- useEffect(() => {
640
- setRequired == null ? void 0 : setRequired(required);
641
- setDisabled == null ? void 0 : setDisabled(disabled);
642
- setAriaDisabled == null ? void 0 : setAriaDisabled(ariaDisabled);
643
- setReadOnly == null ? void 0 : setReadOnly(readOnly);
644
- }, [
645
- readOnly,
646
- disabled,
647
- ariaDisabled,
648
- required,
649
- setRequired,
650
- setDisabled,
651
- setAriaDisabled,
652
- setReadOnly
653
- ]);
654
- const onSetSelectedValue = (newValue) => {
655
- setValue(typeof newValue === "string" ? [newValue] : newValue);
656
- };
657
- const onOpenChange = (value2) => {
658
- if (!booleanify(readOnly)) {
659
- setOpenState(value2);
660
- }
661
- };
662
- return /* @__PURE__ */ jsx(
663
- RadixPopover.Root,
664
- {
665
- open: openState,
666
- onOpenChange,
667
- ...restProps,
668
- children: /* @__PURE__ */ jsx(
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
+ });
704
+
705
+ const Root = React.forwardRef(
706
+ ({ value: valueProp, onValueChange, name, children, ...restProps }, forwardRef) => {
707
+ var _a;
708
+ const {
709
+ openState,
710
+ setOpenState,
711
+ defaultValue,
712
+ value = [],
713
+ setValue,
714
+ required,
715
+ readOnly,
716
+ "aria-disabled": ariaDisabled,
717
+ disabled,
718
+ direction,
719
+ size,
720
+ placeholder,
721
+ triggerRef
722
+ } = useComboboxContext();
723
+ const {
724
+ setRequired,
725
+ setDisabled,
726
+ setAriaDisabled,
727
+ setReadOnly,
728
+ label,
729
+ isFloatingLabel,
730
+ focused,
731
+ formElementRef
732
+ } = useFormFieldContext();
733
+ useEffect(() => {
734
+ setRequired == null ? void 0 : setRequired(required);
735
+ setDisabled == null ? void 0 : setDisabled(disabled);
736
+ setAriaDisabled == null ? void 0 : setAriaDisabled(ariaDisabled);
737
+ setReadOnly == null ? void 0 : setReadOnly(readOnly);
738
+ }, [
739
+ readOnly,
740
+ disabled,
741
+ ariaDisabled,
742
+ required,
743
+ setRequired,
744
+ setDisabled,
745
+ setAriaDisabled,
746
+ setReadOnly
747
+ ]);
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(
669
761
  ComboboxProvider$1,
670
762
  {
671
763
  open: openState,
@@ -673,51 +765,86 @@ const Root = ({
673
765
  defaultSelectedValue: defaultValue,
674
766
  selectedValue: value,
675
767
  setSelectedValue: onSetSelectedValue,
676
- children
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
+ )
677
802
  }
678
803
  )
679
- }
680
- );
681
- };
682
- const Combobox = ({
683
- "aria-disabled": ariaDisabled,
684
- defaultOpen = false,
685
- open,
686
- valid,
687
- disabled,
688
- readOnly,
689
- required,
690
- value,
691
- defaultValue,
692
- onOpen,
693
- onClose,
694
- onSearchValueChange,
695
- onValueChange,
696
- direction = "ltr",
697
- autoFilter = true,
698
- noResultsText,
699
- ...restProps
700
- }) => /* @__PURE__ */ jsx(
701
- ComboboxProvider,
702
- {
703
- defaultValue,
704
- value,
705
- onValueChange,
706
- onSearchValueChange,
707
- defaultOpen,
804
+ ] });
805
+ }
806
+ );
807
+ const Combobox = React.forwardRef(
808
+ ({
809
+ "aria-disabled": ariaDisabled,
810
+ defaultOpen = false,
708
811
  open,
709
- onOpen,
710
- onClose,
711
812
  valid,
712
- required,
713
813
  disabled,
714
814
  readOnly,
715
- "aria-disabled": ariaDisabled,
716
- direction,
717
- autoFilter,
815
+ required,
816
+ value,
817
+ defaultValue,
818
+ onOpen,
819
+ onClose,
820
+ onSearchValueChange,
821
+ onValueChange,
822
+ direction = "ltr",
823
+ autoFilter = true,
718
824
  noResultsText,
719
- children: /* @__PURE__ */ jsx(Root, { ...restProps, value })
720
- }
825
+ ...restProps
826
+ }, forwardRef) => /* @__PURE__ */ jsx(
827
+ ComboboxProvider,
828
+ {
829
+ defaultValue,
830
+ value,
831
+ onValueChange,
832
+ onSearchValueChange,
833
+ defaultOpen,
834
+ open,
835
+ onOpen,
836
+ onClose,
837
+ valid,
838
+ required,
839
+ disabled,
840
+ readOnly,
841
+ "aria-disabled": ariaDisabled,
842
+ direction,
843
+ autoFilter,
844
+ noResultsText,
845
+ children: /* @__PURE__ */ jsx(Root, { ...restProps, value, ref: forwardRef })
846
+ }
847
+ )
721
848
  );
722
849
  Combobox.Portal = Portal;
723
850
  Combobox.Trigger = Trigger;