@mirohq/design-system-combobox 0.1.0-combobox.7 → 0.1.0-combobox.9

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,21 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import React, { createContext, useContext, useRef, useState, useCallback, useEffect, useMemo } from 'react';
3
- import { Combobox as Combobox$1, ComboboxItem, ComboboxItemCheck, Group as Group$1, GroupLabel as GroupLabel$1, ComboboxProvider as ComboboxProvider$1 } from '@ariakit/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';
4
4
  import { useFormFieldContext, FloatingLabel } from '@mirohq/design-system-base-form';
5
5
  import * as RadixPopover from '@radix-ui/react-popover';
6
- import { Anchor, Portal as Portal$1 } from '@radix-ui/react-popover';
7
- import { stringAttrValue, booleanishAttrValue, mergeRefs, booleanify } from '@mirohq/design-system-utils';
6
+ import { Anchor, Trigger as Trigger$1, Portal as Portal$1 } from '@radix-ui/react-popover';
7
+ import { booleanishAttrValue, mergeRefs, booleanify } from '@mirohq/design-system-utils';
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';
11
11
  import { IconChevronDown, IconCross, IconCheckMark } from '@mirohq/design-system-icons';
12
12
  import { ScrollArea } from '@mirohq/design-system-scroll-area';
13
+ import { Primitive } from '@mirohq/design-system-primitive';
13
14
  import { mergeProps } from '@react-aria/utils';
14
15
  import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
16
+ import { useLayoutEffect } from '@mirohq/design-system-use-layout-effect';
15
17
  import { focus } from '@mirohq/design-system-styles';
16
- import { Primitive } from '@mirohq/design-system-primitive';
18
+ import { createPortal } from 'react-dom';
17
19
  import { BaseButton } from '@mirohq/design-system-base-button';
18
20
 
19
21
  const StyledAnchor = styled(Anchor, {
@@ -92,6 +94,7 @@ const ComboboxProvider = ({
92
94
  });
93
95
  const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
94
96
  const [searchValue, setSearchValue] = useState("");
97
+ const [itemValueTextMap, setItemValueTextMap] = useState(/* @__PURE__ */ new Map());
95
98
  const { valid: formFieldValid } = useFormFieldContext();
96
99
  return /* @__PURE__ */ jsx(
97
100
  ComboboxContext.Provider,
@@ -113,7 +116,9 @@ const ComboboxProvider = ({
113
116
  searchValue,
114
117
  setSearchValue,
115
118
  filteredItems,
116
- setFilteredItems
119
+ setFilteredItems,
120
+ itemValueTextMap,
121
+ setItemValueTextMap
117
122
  },
118
123
  children
119
124
  }
@@ -141,25 +146,48 @@ const StyledActionButton = styled(Input.ActionButton, {
141
146
 
142
147
  const TriggerActionButton = ({
143
148
  openActionLabel,
149
+ closeActionLabel,
144
150
  clearActionLabel,
145
151
  size
146
152
  }) => {
147
- const { setOpenState, value = [], setValue } = useComboboxContext();
153
+ const { openState, setOpenState, value = [], setValue } = useComboboxContext();
148
154
  const isEmpty = value.length === 0;
149
- const ActionButtonIcon = isEmpty ? IconChevronDown : IconCross;
150
- const label = isEmpty ? openActionLabel : clearActionLabel;
151
- const onActionButtonClick = useCallback(
155
+ const onToggleClick = useCallback(
152
156
  (event) => {
153
- if (!isEmpty) {
154
- setValue([]);
155
- } else {
156
- setOpenState((prevOpen = false) => !prevOpen);
157
+ if (openState) {
158
+ setOpenState(false);
157
159
  }
158
160
  event.stopPropagation();
159
161
  },
160
- [isEmpty, setValue, setOpenState]
162
+ [setOpenState, openState]
163
+ );
164
+ const onClearClick = useCallback(
165
+ (event) => {
166
+ setValue([]);
167
+ event.stopPropagation();
168
+ },
169
+ [setValue]
170
+ );
171
+ if (isEmpty) {
172
+ return /* @__PURE__ */ jsx(Trigger$1, { asChild: true, "aria-haspopup": "listbox", children: /* @__PURE__ */ jsx(
173
+ StyledActionButton,
174
+ {
175
+ label: openState ? closeActionLabel : openActionLabel,
176
+ size,
177
+ onClick: onToggleClick,
178
+ children: /* @__PURE__ */ jsx(IconChevronDown, { size: "small", weight: "thin" })
179
+ }
180
+ ) });
181
+ }
182
+ return /* @__PURE__ */ jsx(
183
+ StyledActionButton,
184
+ {
185
+ label: clearActionLabel,
186
+ size,
187
+ onClick: onClearClick,
188
+ children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin" })
189
+ }
161
190
  );
162
- return /* @__PURE__ */ jsx(StyledActionButton, { label, size, onClick: onActionButtonClick, children: /* @__PURE__ */ jsx(ActionButtonIcon, { size: "small", weight: "thin" }) });
163
191
  };
164
192
 
165
193
  const Trigger = React.forwardRef(
@@ -171,6 +199,7 @@ const Trigger = React.forwardRef(
171
199
  "aria-invalid": ariaInvalid,
172
200
  placeholder,
173
201
  openActionLabel,
202
+ closeActionLabel,
174
203
  clearActionLabel,
175
204
  onChange,
176
205
  css,
@@ -191,7 +220,6 @@ const Trigger = React.forwardRef(
191
220
  } = useComboboxContext();
192
221
  const {
193
222
  formElementId,
194
- ariaDescribedBy: formFieldContextDescribedBy,
195
223
  ariaInvalid: formFieldAriaInvalid,
196
224
  valid: formFieldValid,
197
225
  label,
@@ -203,10 +231,8 @@ const Trigger = React.forwardRef(
203
231
  ...restProps,
204
232
  "aria-disabled": ariaDisabled,
205
233
  "aria-invalid": ariaInvalid != null ? ariaInvalid : formFieldAriaInvalid,
206
- "aria-describedby": stringAttrValue(
207
- ariaDescribedBy,
208
- formFieldContextDescribedBy
209
- ),
234
+ // todo MDS-1011: use formFieldContextDescribedBy after removing form context from BaseInput
235
+ "aria-describedby": ariaDescribedBy,
210
236
  valid,
211
237
  disabled,
212
238
  readOnly,
@@ -265,6 +291,7 @@ const Trigger = React.forwardRef(
265
291
  TriggerActionButton,
266
292
  {
267
293
  openActionLabel,
294
+ closeActionLabel,
268
295
  clearActionLabel,
269
296
  size
270
297
  }
@@ -280,6 +307,9 @@ const Trigger = React.forwardRef(
280
307
  }
281
308
  );
282
309
 
310
+ const NoResultPlaceholder = styled(Primitive.div, {
311
+ padding: "$100"
312
+ });
283
313
  const StyledContent = styled(RadixPopover.Content, {
284
314
  backgroundColor: "$background-neutrals-container",
285
315
  borderRadius: "$50",
@@ -337,7 +367,23 @@ const StyledItem = styled(ComboboxItem, {
337
367
  const Item = React.forwardRef(
338
368
  ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
339
369
  const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
340
- const { autoFilter, filteredItems, triggerRef, inputRef } = useComboboxContext();
370
+ const {
371
+ autoFilter,
372
+ filteredItems,
373
+ setItemValueTextMap,
374
+ triggerRef,
375
+ inputRef
376
+ } = useComboboxContext();
377
+ useLayoutEffect(() => {
378
+ const textToSet = textValue !== void 0 ? textValue : typeof children === "string" ? children : "";
379
+ setItemValueTextMap((prevState) => new Map(prevState.set(value, textToSet)));
380
+ return () => {
381
+ setItemValueTextMap((prevState) => {
382
+ prevState.delete(value);
383
+ return new Map(prevState);
384
+ });
385
+ };
386
+ }, [setItemValueTextMap, value, textValue, children]);
341
387
  if (autoFilter !== false && !filteredItems.has(value)) {
342
388
  return null;
343
389
  }
@@ -409,13 +455,35 @@ const getChildrenItemValues = (componentChildren) => {
409
455
  return values;
410
456
  };
411
457
 
458
+ const useDocumentFragment = () => {
459
+ const [fragment, setFragment] = React.useState();
460
+ useLayoutEffect(() => {
461
+ setFragment(new DocumentFragment());
462
+ }, []);
463
+ return fragment;
464
+ };
465
+
466
+ const useInvisibleContent = () => {
467
+ const fragment = useDocumentFragment();
468
+ return useCallback(
469
+ (children) => fragment !== void 0 ? createPortal(/* @__PURE__ */ jsx("div", { children }), fragment) : null,
470
+ [fragment]
471
+ );
472
+ };
473
+
412
474
  const CONTENT_OFFSET = parseInt(theme.space[50]);
413
475
  const isInsideRef = (element, ref) => {
414
476
  var _a, _b;
415
477
  return (_b = element != null && ((_a = ref.current) == null ? void 0 : _a.contains(element))) != null ? _b : false;
416
478
  };
417
479
  const Content = React.forwardRef(
418
- ({ sideOffset = CONTENT_OFFSET, maxHeight, children, ...restProps }, forwardRef) => {
480
+ ({
481
+ sideOffset = CONTENT_OFFSET,
482
+ maxHeight,
483
+ overflow,
484
+ children,
485
+ ...restProps
486
+ }, forwardRef) => {
419
487
  const {
420
488
  triggerRef,
421
489
  contentRef,
@@ -424,7 +492,8 @@ const Content = React.forwardRef(
424
492
  setFilteredItems,
425
493
  searchValue,
426
494
  noResultsText,
427
- direction
495
+ direction,
496
+ openState
428
497
  } = useComboboxContext();
429
498
  useEffect(() => {
430
499
  const childrenItemValues = getChildrenItemValues(children);
@@ -436,11 +505,20 @@ const Content = React.forwardRef(
436
505
  )
437
506
  );
438
507
  }, [children, autoFilter, setFilteredItems, searchValue]);
439
- const content = filteredItems.size === 0 ? noResultsText : children;
508
+ const getInvisibleContent = useInvisibleContent();
509
+ if (!openState) {
510
+ return getInvisibleContent(children);
511
+ }
512
+ const content = filteredItems.size === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
513
+ /* @__PURE__ */ jsx(NoResultPlaceholder, { children: noResultsText }),
514
+ getInvisibleContent(children)
515
+ ] }) : children;
440
516
  return /* @__PURE__ */ jsx(
441
517
  StyledContent,
442
518
  {
519
+ asChild: true,
443
520
  ...restProps,
521
+ dir: direction,
444
522
  sideOffset,
445
523
  ref: mergeRefs([forwardRef, contentRef]),
446
524
  onOpenAutoFocus: (event) => event.preventDefault(),
@@ -452,7 +530,7 @@ const Content = React.forwardRef(
452
530
  event.preventDefault();
453
531
  }
454
532
  },
455
- children: /* @__PURE__ */ jsxs(ScrollArea, { type: "always", dir: direction, children: [
533
+ children: /* @__PURE__ */ jsx(ComboboxList, { role: "listbox", children: overflow === "auto" ? /* @__PURE__ */ jsxs(ScrollArea, { type: "always", dir: direction, children: [
456
534
  /* @__PURE__ */ jsx(
457
535
  ScrollArea.Viewport,
458
536
  {
@@ -463,7 +541,7 @@ const Content = React.forwardRef(
463
541
  }
464
542
  ),
465
543
  /* @__PURE__ */ jsx(ScrollArea.Scrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollArea.Thumb, {}) })
466
- ] })
544
+ ] }) : content })
467
545
  }
468
546
  );
469
547
  }
@@ -471,6 +549,8 @@ const Content = React.forwardRef(
471
549
 
472
550
  const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
473
551
 
552
+ const StyledGroup = styled(Group$1);
553
+
474
554
  const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
475
555
  const { autoFilter, filteredItems } = useComboboxContext();
476
556
  const childValues = useMemo(
@@ -485,10 +565,11 @@ const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
485
565
  ),
486
566
  [childValues, filteredItems, autoFilter]
487
567
  );
568
+ const getInvisibleContent = useInvisibleContent();
488
569
  if (!hasVisibleChildren) {
489
- return null;
570
+ return getInvisibleContent(children);
490
571
  }
491
- return /* @__PURE__ */ jsx(Group$1, { ...rest, ref: forwardRef, children });
572
+ return /* @__PURE__ */ jsx(StyledGroup, { ...rest, ref: forwardRef, children });
492
573
  });
493
574
 
494
575
  const StyledGroupLabel = styled(GroupLabel$1, {
@@ -507,13 +588,13 @@ const StyledChip = styled(Primitive.div, {
507
588
  gap: "$50",
508
589
  whiteSpace: "nowrap",
509
590
  maxWidth: "$35",
510
- background: "$gray-100",
511
- color: "$gray-900"
591
+ backgroundColor: "$background-neutrals-subtle",
592
+ color: "$text-neutrals"
512
593
  });
513
594
  const StyledChipButton = styled(BaseButton, {
595
+ color: "$icon-neutrals-inactive",
514
596
  ...focus.css({
515
- boxShadow: "$focus-small-outline",
516
- borderColor: "$blue-400 !important"
597
+ boxShadow: "$focus-small-outline"
517
598
  })
518
599
  });
519
600
  const StyledChipContent = styled(Primitive.div, {
@@ -531,9 +612,9 @@ const StyledLeftSlot = styled(Primitive.span, {
531
612
  const LeftSlot = StyledLeftSlot;
532
613
 
533
614
  const Chip = React.forwardRef(
534
- ({ children, disabled = false, onRemove, removeChipAriaLabel, ...restProps }, forwardRef) => /* @__PURE__ */ jsxs(StyledChip, { ...restProps, ref: forwardRef, children: [
615
+ ({ children, disabled = false, onRemove, removeAriaLabel, ...restProps }, forwardRef) => /* @__PURE__ */ jsxs(StyledChip, { ...restProps, ref: forwardRef, children: [
535
616
  /* @__PURE__ */ jsx(StyledChipContent, { children }),
536
- !booleanify(disabled) && /* @__PURE__ */ jsx(StyledChipButton, { onClick: onRemove, "aria-label": removeChipAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", color: "gray-500" }) })
617
+ !booleanify(disabled) && /* @__PURE__ */ jsx(StyledChipButton, { onClick: onRemove, "aria-label": removeAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", "aria-hidden": true }) })
537
618
  ] })
538
619
  );
539
620
  Chip.LeftSlot = LeftSlot;
@@ -542,31 +623,42 @@ const StyledValue = styled(Chip, {
542
623
  marginTop: "$50"
543
624
  });
544
625
 
545
- const Value = ({ removeChipAriaLabel }) => {
626
+ const Value = ({ unselectAriaLabel }) => {
546
627
  const {
547
628
  value = [],
548
629
  setValue,
549
630
  disabled,
550
- "aria-disabled": ariaDisabled
631
+ "aria-disabled": ariaDisabled,
632
+ itemValueTextMap
551
633
  } = useComboboxContext();
552
634
  const isDisabled = ariaDisabled === true || disabled;
553
- const onItemRemove = (item) => {
554
- setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
555
- };
556
- if (value.length === 0) {
557
- return null;
558
- }
559
- return /* @__PURE__ */ jsx(Fragment, { children: value.map((item) => /* @__PURE__ */ jsx(
560
- StyledValue,
561
- {
562
- onRemove: () => onItemRemove(item),
563
- disabled: isDisabled,
564
- removeChipAriaLabel,
565
- "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(item) : void 0,
566
- children: item
635
+ const onItemRemove = useCallback(
636
+ (item) => {
637
+ setValue((prevValue) => prevValue == null ? void 0 : prevValue.filter((value2) => value2 !== item));
567
638
  },
568
- item
569
- )) });
639
+ [setValue]
640
+ );
641
+ const getItemText = useCallback(
642
+ (itemValue) => {
643
+ const textValue = itemValueTextMap.get(itemValue);
644
+ if (textValue === void 0 || textValue === "") {
645
+ return null;
646
+ }
647
+ return /* @__PURE__ */ jsx(
648
+ StyledValue,
649
+ {
650
+ onRemove: () => onItemRemove(itemValue),
651
+ disabled: isDisabled,
652
+ removeAriaLabel: "".concat(unselectAriaLabel, " ").concat(textValue),
653
+ "data-testid": process.env.NODE_ENV === "test" ? "combobox-value-".concat(itemValue) : void 0,
654
+ children: textValue
655
+ },
656
+ itemValue
657
+ );
658
+ },
659
+ [isDisabled, itemValueTextMap, onItemRemove, unselectAriaLabel]
660
+ );
661
+ return /* @__PURE__ */ jsx(Fragment, { children: value.map(getItemText) });
570
662
  };
571
663
 
572
664
  const StyledSeparator = styled(Primitive.div, {