@mirohq/design-system-combobox 0.1.0-combobox.5 → 0.1.0-combobox.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,19 +1,25 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { createContext, useRef, useState, useContext, useCallback, useEffect, useMemo } from 'react';
2
+ import React, { createContext, useContext, useRef, useState, 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
5
  import * as RadixPopover from '@radix-ui/react-popover';
6
- import { Portal as Portal$1 } from '@radix-ui/react-popover';
6
+ import { Anchor, Portal as Portal$1 } from '@radix-ui/react-popover';
7
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';
11
13
  import { mergeProps } from '@react-aria/utils';
12
14
  import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
13
15
  import { focus } from '@mirohq/design-system-styles';
14
16
  import { Primitive } from '@mirohq/design-system-primitive';
15
17
  import { BaseButton } from '@mirohq/design-system-base-button';
16
18
 
19
+ const StyledAnchor = styled(Anchor, {
20
+ position: "relative",
21
+ width: "100%"
22
+ });
17
23
  const StyledInput = styled(Input, {
18
24
  flexWrap: "wrap",
19
25
  flexGrow: 1,
@@ -52,10 +58,14 @@ const StyledInput = styled(Input, {
52
58
  const ComboboxContext = createContext({});
53
59
  const ComboboxProvider = ({
54
60
  children,
61
+ open: openProp,
55
62
  defaultOpen,
63
+ onOpen,
64
+ onClose,
56
65
  valid,
57
66
  value: valueProp,
58
67
  defaultValue: defaultValueProp,
68
+ onValueChange,
59
69
  onSearchValueChange,
60
70
  autoFilter = true,
61
71
  ...restProps
@@ -63,9 +73,23 @@ const ComboboxProvider = ({
63
73
  const triggerRef = useRef(null);
64
74
  const inputRef = useRef(null);
65
75
  const contentRef = useRef(null);
66
- const [openState, setOpenState] = useState(defaultOpen != null ? defaultOpen : false);
67
- const [value, setValue] = useState(valueProp != null ? valueProp : []);
68
76
  const [defaultValue, setDefaultValue] = useState(defaultValueProp);
77
+ const [openState = false, setOpenState] = useControllableState({
78
+ prop: openProp,
79
+ defaultProp: defaultOpen,
80
+ onChange: (state) => {
81
+ if (state) {
82
+ onOpen == null ? void 0 : onOpen();
83
+ } else {
84
+ onClose == null ? void 0 : onClose();
85
+ }
86
+ }
87
+ });
88
+ const [value, setValue] = useControllableState({
89
+ prop: valueProp,
90
+ defaultProp: defaultValueProp,
91
+ onChange: onValueChange
92
+ });
69
93
  const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
70
94
  const [searchValue, setSearchValue] = useState("");
71
95
  const { valid: formFieldValid } = useFormFieldContext();
@@ -120,7 +144,7 @@ const TriggerActionButton = ({
120
144
  clearActionLabel,
121
145
  size
122
146
  }) => {
123
- const { setOpenState, value, setValue } = useComboboxContext();
147
+ const { setOpenState, value = [], setValue } = useComboboxContext();
124
148
  const isEmpty = value.length === 0;
125
149
  const ActionButtonIcon = isEmpty ? IconChevronDown : IconCross;
126
150
  const label = isEmpty ? openActionLabel : clearActionLabel;
@@ -129,7 +153,7 @@ const TriggerActionButton = ({
129
153
  if (!isEmpty) {
130
154
  setValue([]);
131
155
  } else {
132
- setOpenState((prevOpen) => !prevOpen);
156
+ setOpenState((prevOpen = false) => !prevOpen);
133
157
  }
134
158
  event.stopPropagation();
135
159
  },
@@ -149,18 +173,21 @@ const Trigger = React.forwardRef(
149
173
  openActionLabel,
150
174
  clearActionLabel,
151
175
  onChange,
176
+ css,
152
177
  ...restProps
153
178
  }, forwardRef) => {
154
179
  const {
155
180
  "aria-disabled": ariaDisabled,
156
181
  valid: comboboxValid,
157
182
  disabled,
158
- value,
183
+ value = [],
184
+ readOnly,
159
185
  triggerRef,
160
186
  inputRef,
161
187
  onSearchValueChange,
162
188
  searchValue,
163
- setSearchValue
189
+ setSearchValue,
190
+ setOpenState
164
191
  } = useComboboxContext();
165
192
  const {
166
193
  formElementId,
@@ -182,12 +209,13 @@ const Trigger = React.forwardRef(
182
209
  ),
183
210
  valid,
184
211
  disabled,
212
+ readOnly,
185
213
  invalid: booleanishAttrValue(valid),
186
214
  id: id != null ? id : formElementId,
187
215
  placeholder: value.length === 0 ? placeholder : void 0
188
216
  };
189
217
  const shouldUseFloatingLabel = label !== null && isFloatingLabel;
190
- const isFloating = placeholder !== void 0 || value.length !== 0 || focused;
218
+ const isFloating = placeholder !== void 0 || value.length !== 0 || focused || searchValue !== "";
191
219
  const scrollIntoView = (event) => {
192
220
  var _a;
193
221
  const trigger = triggerRef == null ? void 0 : triggerRef.current;
@@ -207,36 +235,48 @@ const Trigger = React.forwardRef(
207
235
  onSearchValueChange == null ? void 0 : onSearchValueChange(e.target.value);
208
236
  onChange == null ? void 0 : onChange(e);
209
237
  };
210
- return /* @__PURE__ */ jsxs(RadixPopover.Anchor, { ref: mergeRefs([triggerRef, forwardRef]), children: [
211
- shouldUseFloatingLabel && /* @__PURE__ */ jsx(FloatingLabel, { floating: isFloating, size, children: label }),
212
- /* @__PURE__ */ jsx(
213
- Combobox$1,
214
- {
215
- render: /* @__PURE__ */ jsxs(
216
- StyledInput,
238
+ return /* @__PURE__ */ jsxs(
239
+ StyledAnchor,
240
+ {
241
+ ref: mergeRefs([triggerRef, forwardRef]),
242
+ css,
243
+ onClick: () => {
244
+ if (!booleanify(disabled) && !booleanify(ariaDisabled) && !booleanify(readOnly)) {
245
+ setOpenState(true);
246
+ }
247
+ },
248
+ children: [
249
+ shouldUseFloatingLabel && /* @__PURE__ */ jsx(FloatingLabel, { floating: isFloating, size, children: label }),
250
+ /* @__PURE__ */ jsx(
251
+ Combobox$1,
217
252
  {
218
- ...inputProps,
219
- value: searchValue,
220
- size,
221
- ref: inputRef,
222
- onChange: onInputChange,
223
- onFocus: scrollIntoView,
224
- children: [
225
- children,
226
- /* @__PURE__ */ jsx(
227
- TriggerActionButton,
228
- {
229
- openActionLabel,
230
- clearActionLabel,
231
- size
232
- }
233
- )
234
- ]
253
+ render: /* @__PURE__ */ jsxs(
254
+ StyledInput,
255
+ {
256
+ ...inputProps,
257
+ value: searchValue,
258
+ size,
259
+ ref: inputRef,
260
+ onChange: onInputChange,
261
+ onFocus: scrollIntoView,
262
+ children: [
263
+ children,
264
+ /* @__PURE__ */ jsx(
265
+ TriggerActionButton,
266
+ {
267
+ openActionLabel,
268
+ clearActionLabel,
269
+ size
270
+ }
271
+ )
272
+ ]
273
+ }
274
+ )
235
275
  }
236
276
  )
237
- }
238
- )
239
- ] });
277
+ ]
278
+ }
279
+ );
240
280
  }
241
281
  );
242
282
 
@@ -246,35 +286,30 @@ const StyledContent = styled(RadixPopover.Content, {
246
286
  boxShadow: "$50",
247
287
  fontSize: "$175",
248
288
  fontWeight: "normal",
249
- lineHeight: "1.5",
289
+ lineHeight: "20px",
250
290
  width: "var(--radix-popover-trigger-width)",
251
291
  zIndex: "$select",
252
292
  overflowY: "auto",
253
- marginTop: "$200",
254
- padding: "$150",
293
+ padding: "$50",
255
294
  boxSizing: "border-box",
256
295
  outline: "1px solid transparent"
257
296
  });
258
297
 
259
298
  const StyledItemCheck = styled(Primitive.span, {
260
- color: "$icon-primary",
261
- paddingX: "$100"
299
+ color: "$icon-primary"
262
300
  });
263
301
  const StyledItem = styled(ComboboxItem, {
264
- display: "flex",
265
- alignItems: "center",
266
- justifyContent: "space-between",
267
- gap: "$200",
302
+ display: "grid",
303
+ gridTemplateColumns: "20px 1fr",
268
304
  borderRadius: "$50",
269
305
  boxSizing: "border-box",
270
306
  color: "$text-neutrals",
271
307
  cursor: "pointer",
272
308
  fontSize: "$175",
273
- lineHeight: 1.5,
309
+ lineHeight: "20px",
274
310
  position: "relative",
275
311
  userSelect: "none",
276
- paddingX: "$100",
277
- paddingY: "10px",
312
+ padding: "6px $100 6px $150",
278
313
  ...focus.css({
279
314
  boxShadow: "$focus-small"
280
315
  }),
@@ -289,7 +324,13 @@ const StyledItem = styled(ComboboxItem, {
289
324
  },
290
325
  "&:disabled, &[aria-disabled=true], &[data-disabled]": {
291
326
  cursor: "default",
292
- color: "$text-neutrals-disabled"
327
+ color: "$text-neutrals-disabled",
328
+ ["".concat(StyledItemCheck)]: {
329
+ color: "$icon-neutrals-disabled"
330
+ }
331
+ },
332
+ '&[aria-selected="true"]:not(:disabled,[aria-disabled=true],[data-disabled])': {
333
+ color: "$text-primary-selected"
293
334
  }
294
335
  });
295
336
 
@@ -316,13 +357,13 @@ const Item = React.forwardRef(
316
357
  {
317
358
  ...mergeProps(restProps, restAriaDisabledProps),
318
359
  focusable: true,
319
- accessibleWhenDisabled: ariaDisabled === true,
320
- disabled: ariaDisabled === true || disabled,
360
+ hideOnClick: false,
361
+ accessibleWhenDisabled: booleanify(ariaDisabled),
362
+ disabled: booleanify(ariaDisabled) || disabled,
321
363
  ref: forwardRef,
322
364
  value,
323
365
  onClick: scrollIntoView,
324
366
  children: [
325
- children,
326
367
  /* @__PURE__ */ jsx(
327
368
  ComboboxItemCheck,
328
369
  {
@@ -330,9 +371,16 @@ const Item = React.forwardRef(
330
371
  // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
331
372
  /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
332
373
  ),
333
- children: /* @__PURE__ */ jsx(IconCheckMark, { size: "small" })
374
+ children: /* @__PURE__ */ jsx(
375
+ IconCheckMark,
376
+ {
377
+ size: "small",
378
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-item-check" : void 0
379
+ }
380
+ )
334
381
  }
335
- )
382
+ ),
383
+ children
336
384
  ]
337
385
  }
338
386
  );
@@ -361,48 +409,65 @@ const getChildrenItemValues = (componentChildren) => {
361
409
  return values;
362
410
  };
363
411
 
412
+ const CONTENT_OFFSET = parseInt(theme.space[50]);
364
413
  const isInsideRef = (element, ref) => {
365
414
  var _a, _b;
366
415
  return (_b = element != null && ((_a = ref.current) == null ? void 0 : _a.contains(element))) != null ? _b : false;
367
416
  };
368
- const Content = React.forwardRef(({ children, ...restProps }, forwardRef) => {
369
- const {
370
- triggerRef,
371
- contentRef,
372
- autoFilter,
373
- filteredItems,
374
- setFilteredItems,
375
- searchValue,
376
- noResultsText
377
- } = useComboboxContext();
378
- useEffect(() => {
379
- const childrenItemValues = getChildrenItemValues(children);
380
- setFilteredItems(
381
- new Set(
382
- autoFilter === false ? childrenItemValues : childrenItemValues.filter(
383
- (child) => child.toLowerCase().includes(searchValue.toLowerCase())
417
+ const Content = React.forwardRef(
418
+ ({ sideOffset = CONTENT_OFFSET, maxHeight, children, ...restProps }, forwardRef) => {
419
+ const {
420
+ triggerRef,
421
+ contentRef,
422
+ autoFilter,
423
+ filteredItems,
424
+ setFilteredItems,
425
+ searchValue,
426
+ noResultsText,
427
+ direction
428
+ } = useComboboxContext();
429
+ useEffect(() => {
430
+ const childrenItemValues = getChildrenItemValues(children);
431
+ setFilteredItems(
432
+ new Set(
433
+ autoFilter === false ? childrenItemValues : childrenItemValues.filter(
434
+ (child) => child.toLowerCase().includes(searchValue.toLowerCase())
435
+ )
384
436
  )
385
- )
437
+ );
438
+ }, [children, autoFilter, setFilteredItems, searchValue]);
439
+ const content = filteredItems.size === 0 ? noResultsText : children;
440
+ return /* @__PURE__ */ jsx(
441
+ StyledContent,
442
+ {
443
+ ...restProps,
444
+ sideOffset,
445
+ ref: mergeRefs([forwardRef, contentRef]),
446
+ onOpenAutoFocus: (event) => event.preventDefault(),
447
+ onInteractOutside: (event) => {
448
+ const target = event.target;
449
+ const isTrigger = isInsideRef(target, triggerRef);
450
+ const isContent = isInsideRef(target, contentRef);
451
+ if (isTrigger || isContent) {
452
+ event.preventDefault();
453
+ }
454
+ },
455
+ children: /* @__PURE__ */ jsxs(ScrollArea, { type: "always", dir: direction, children: [
456
+ /* @__PURE__ */ jsx(
457
+ ScrollArea.Viewport,
458
+ {
459
+ availableHeight: "var(--radix-popover-content-available-height)",
460
+ verticalGap: "var(--space-50) * 2",
461
+ maxHeight,
462
+ children: content
463
+ }
464
+ ),
465
+ /* @__PURE__ */ jsx(ScrollArea.Scrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollArea.Thumb, {}) })
466
+ ] })
467
+ }
386
468
  );
387
- }, [children, autoFilter, setFilteredItems, searchValue]);
388
- return /* @__PURE__ */ jsx(
389
- StyledContent,
390
- {
391
- ...restProps,
392
- ref: mergeRefs([forwardRef, contentRef]),
393
- onOpenAutoFocus: (event) => event.preventDefault(),
394
- onInteractOutside: (event) => {
395
- const target = event.target;
396
- const isTrigger = isInsideRef(target, triggerRef);
397
- const isContent = isInsideRef(target, contentRef);
398
- if (isTrigger || isContent) {
399
- event.preventDefault();
400
- }
401
- },
402
- children: filteredItems.size === 0 ? noResultsText : children
403
- }
404
- );
405
- });
469
+ }
470
+ );
406
471
 
407
472
  const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
408
473
 
@@ -427,11 +492,8 @@ const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
427
492
  });
428
493
 
429
494
  const StyledGroupLabel = styled(GroupLabel$1, {
430
- padding: "$100",
431
- color: "$text-neutrals-subtle",
432
- fontSize: "$150",
433
- textTransform: "uppercase",
434
- fontWeight: 650
495
+ padding: "6px $100",
496
+ color: "$text-neutrals-subtle"
435
497
  });
436
498
 
437
499
  const GroupLabel = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroupLabel, { ...props, ref: forwardRef }));
@@ -482,14 +544,14 @@ const StyledValue = styled(Chip, {
482
544
 
483
545
  const Value = ({ removeChipAriaLabel }) => {
484
546
  const {
485
- value,
547
+ value = [],
486
548
  setValue,
487
549
  disabled,
488
550
  "aria-disabled": ariaDisabled
489
551
  } = useComboboxContext();
490
552
  const isDisabled = ariaDisabled === true || disabled;
491
553
  const onItemRemove = (item) => {
492
- setValue((prevValue) => prevValue.filter((value2) => value2 !== item));
554
+ setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
493
555
  };
494
556
  if (value.length === 0) {
495
557
  return null;
@@ -500,6 +562,7 @@ const Value = ({ removeChipAriaLabel }) => {
500
562
  onRemove: () => onItemRemove(item),
501
563
  disabled: isDisabled,
502
564
  removeChipAriaLabel,
565
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(item) : void 0,
503
566
  children: item
504
567
  },
505
568
  item
@@ -513,13 +576,19 @@ const StyledSeparator = styled(Primitive.div, {
513
576
  margin: "$100 0"
514
577
  });
515
578
 
516
- const Separator = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true }));
517
-
518
- const StyledComboboxContent = styled(Primitive.div, {
519
- position: "relative"
579
+ const Separator = React.forwardRef((props, forwardRef) => {
580
+ const { autoFilter, searchValue } = useComboboxContext();
581
+ if (autoFilter === true && searchValue.length > 0) {
582
+ return null;
583
+ }
584
+ return /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true });
520
585
  });
521
586
 
522
- const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...restProps }, forwardRef) => {
587
+ const Root = ({
588
+ value: valueProp,
589
+ children,
590
+ ...restProps
591
+ }) => {
523
592
  const {
524
593
  openState,
525
594
  setOpenState,
@@ -529,8 +598,7 @@ const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...r
529
598
  required,
530
599
  readOnly,
531
600
  "aria-disabled": ariaDisabled,
532
- disabled,
533
- direction
601
+ disabled
534
602
  } = useComboboxContext();
535
603
  const { setRequired, setDisabled, setAriaDisabled, setReadOnly } = useFormFieldContext();
536
604
  useEffect(() => {
@@ -549,66 +617,72 @@ const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...r
549
617
  setReadOnly
550
618
  ]);
551
619
  const onSetSelectedValue = (newValue) => {
552
- onValueChange == null ? void 0 : onValueChange(newValue);
553
- setValue(newValue);
620
+ setValue(typeof newValue === "string" ? [newValue] : newValue);
554
621
  };
555
- return /* @__PURE__ */ jsx(RadixPopover.Root, { open: openState, onOpenChange: setOpenState, children: /* @__PURE__ */ jsx(
556
- ComboboxProvider$1,
622
+ const onOpenChange = (value2) => {
623
+ if (!booleanify(readOnly)) {
624
+ setOpenState(value2);
625
+ }
626
+ };
627
+ return /* @__PURE__ */ jsx(
628
+ RadixPopover.Root,
557
629
  {
558
630
  open: openState,
559
- setOpen: setOpenState,
560
- defaultSelectedValue: defaultValue,
561
- selectedValue: value,
562
- setSelectedValue: onSetSelectedValue,
563
- children: /* @__PURE__ */ jsx(StyledComboboxContent, { ...restProps, ref: forwardRef, dir: direction, children })
631
+ onOpenChange,
632
+ ...restProps,
633
+ children: /* @__PURE__ */ jsx(
634
+ ComboboxProvider$1,
635
+ {
636
+ open: openState,
637
+ setOpen: onOpenChange,
638
+ defaultSelectedValue: defaultValue,
639
+ selectedValue: value,
640
+ setSelectedValue: onSetSelectedValue,
641
+ children
642
+ }
643
+ )
564
644
  }
565
- ) });
566
- });
567
- const Combobox = React.forwardRef(
568
- ({
569
- "aria-disabled": ariaDisabled,
570
- defaultOpen = false,
645
+ );
646
+ };
647
+ const Combobox = ({
648
+ "aria-disabled": ariaDisabled,
649
+ defaultOpen = false,
650
+ open,
651
+ valid,
652
+ disabled,
653
+ readOnly,
654
+ required,
655
+ value,
656
+ defaultValue,
657
+ onOpen,
658
+ onClose,
659
+ onSearchValueChange,
660
+ onValueChange,
661
+ direction = "ltr",
662
+ autoFilter = true,
663
+ noResultsText,
664
+ ...restProps
665
+ }) => /* @__PURE__ */ jsx(
666
+ ComboboxProvider,
667
+ {
668
+ defaultValue,
669
+ value,
670
+ onValueChange,
671
+ onSearchValueChange,
672
+ defaultOpen,
571
673
  open,
674
+ onOpen,
675
+ onClose,
572
676
  valid,
677
+ required,
573
678
  disabled,
574
679
  readOnly,
575
- required,
576
- value,
577
- defaultValue,
578
- onSearchValueChange,
579
- onOpen,
580
- onValueChange,
581
- direction = "ltr",
582
- autoFilter = true,
680
+ "aria-disabled": ariaDisabled,
681
+ direction,
682
+ autoFilter,
583
683
  noResultsText,
584
- ...restProps
585
- }, forwardRef) => /* @__PURE__ */ jsx(
586
- ComboboxProvider,
587
- {
588
- defaultValue,
589
- value,
590
- onSearchValueChange,
591
- defaultOpen,
592
- open,
593
- valid,
594
- required,
595
- disabled,
596
- readOnly,
597
- "aria-disabled": ariaDisabled,
598
- direction,
599
- autoFilter,
600
- noResultsText,
601
- children: /* @__PURE__ */ jsx(
602
- Root,
603
- {
604
- ...restProps,
605
- noResultsText,
606
- value,
607
- ref: forwardRef
608
- }
609
- )
610
- }
611
- )
684
+ children: /* @__PURE__ */ jsx(Root, { ...restProps, value })
685
+ }
612
686
  );
613
687
  Combobox.Portal = Portal;
614
688
  Combobox.Trigger = Trigger;