@mirohq/design-system-combobox 0.1.0-combobox.3 → 0.1.0-combobox.4

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,37 +1,45 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { createContext, useRef, useState, useContext, useEffect } from 'react';
3
- import * as Ariakit from '@ariakit/react';
4
- import { Combobox as Combobox$1, ComboboxItem, Group as Group$1, GroupLabel as GroupLabel$1 } from '@ariakit/react';
5
- import { useFormFieldContext } from '@mirohq/design-system-base-form';
2
+ import React, { createContext, useRef, useState, useContext, 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';
4
+ import { useFormFieldContext, FloatingLabel } from '@mirohq/design-system-base-form';
6
5
  import * as RadixPopover from '@radix-ui/react-popover';
7
6
  import { Portal as Portal$1 } from '@radix-ui/react-popover';
8
7
  import { stringAttrValue, booleanishAttrValue, mergeRefs, booleanify } from '@mirohq/design-system-utils';
9
- import { IconChevronDown, IconCross } from '@mirohq/design-system-icons';
10
8
  import { styled } from '@mirohq/design-system-stitches';
11
9
  import { Input } from '@mirohq/design-system-input';
10
+ import { IconChevronDown, IconCross, IconCheckMark } from '@mirohq/design-system-icons';
12
11
  import { useAriaDisabled } from '@mirohq/design-system-use-aria-disabled';
13
12
  import { focus } from '@mirohq/design-system-styles';
14
- import { BaseButton } from '@mirohq/design-system-base-button';
15
13
  import { Primitive } from '@mirohq/design-system-primitive';
14
+ import { BaseButton } from '@mirohq/design-system-base-button';
16
15
 
17
- const StyledActionButton = styled(Input.ActionButton, {
18
- position: "absolute",
19
- right: "$100",
20
- top: "10px"
21
- });
22
16
  const StyledInput = styled(Input, {
23
17
  flexWrap: "wrap",
24
18
  flexGrow: 1,
25
- gap: "$50",
26
- minHeight: "$12",
19
+ gap: "0 $50",
27
20
  "&&&": {
28
- height: "max-content",
29
- padding: "$100 $400 $100 $100"
21
+ height: "max-content"
30
22
  },
31
23
  "& input": {
32
24
  minWidth: "30px",
33
25
  flexBasis: 0,
34
- flexGrow: 1
26
+ flexGrow: 1,
27
+ marginRight: "$300"
28
+ },
29
+ variants: {
30
+ size: {
31
+ large: {
32
+ minHeight: "$10",
33
+ padding: "5px $100"
34
+ },
35
+ "x-large": {
36
+ minHeight: "$12",
37
+ padding: "9px $150"
38
+ }
39
+ }
40
+ },
41
+ defaultVariants: {
42
+ size: "large"
35
43
  }
36
44
  });
37
45
 
@@ -42,14 +50,17 @@ const ComboboxProvider = ({
42
50
  valid,
43
51
  value: valueProp,
44
52
  defaultValue: defaultValueProp,
53
+ onSearchValueChange,
54
+ autoFilter = true,
45
55
  ...restProps
46
56
  }) => {
47
57
  const triggerRef = useRef(null);
48
58
  const contentRef = useRef(null);
49
- const [openState, setOpenState] = useState(defaultOpen);
59
+ const [openState, setOpenState] = useState(defaultOpen != null ? defaultOpen : false);
50
60
  const [value, setValue] = useState(valueProp != null ? valueProp : []);
51
61
  const [defaultValue, setDefaultValue] = useState(defaultValueProp);
52
- const [searchValue, setSearchValue] = useState();
62
+ const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
63
+ const [searchValue, setSearchValue] = useState("");
53
64
  const { valid: formFieldValid } = useFormFieldContext();
54
65
  return /* @__PURE__ */ jsx(
55
66
  ComboboxContext.Provider,
@@ -63,10 +74,14 @@ const ComboboxProvider = ({
63
74
  setValue,
64
75
  setDefaultValue,
65
76
  defaultValue,
77
+ onSearchValueChange,
78
+ triggerRef,
79
+ contentRef,
80
+ autoFilter,
66
81
  searchValue,
67
82
  setSearchValue,
68
- triggerRef,
69
- contentRef
83
+ filteredItems,
84
+ setFilteredItems
70
85
  },
71
86
  children
72
87
  }
@@ -74,6 +89,47 @@ const ComboboxProvider = ({
74
89
  };
75
90
  const useComboboxContext = () => useContext(ComboboxContext);
76
91
 
92
+ const StyledActionButton = styled(Input.ActionButton, {
93
+ position: "absolute",
94
+ right: "$100",
95
+ variants: {
96
+ size: {
97
+ large: {
98
+ top: "5px"
99
+ },
100
+ "x-large": {
101
+ top: "9px"
102
+ }
103
+ }
104
+ },
105
+ defaultVariants: {
106
+ size: "large"
107
+ }
108
+ });
109
+
110
+ const TriggerActionButton = ({
111
+ openActionLabel,
112
+ clearActionLabel,
113
+ size
114
+ }) => {
115
+ const { setOpenState, value, setValue } = useComboboxContext();
116
+ const isEmpty = value === void 0 || value.length === 0;
117
+ const ActionButtonIcon = isEmpty ? IconChevronDown : IconCross;
118
+ const label = isEmpty ? openActionLabel : clearActionLabel;
119
+ const onActionButtonClick = useCallback(
120
+ (event) => {
121
+ if (!isEmpty) {
122
+ setValue([]);
123
+ } else {
124
+ setOpenState((prevOpen) => !prevOpen);
125
+ }
126
+ event.stopPropagation();
127
+ },
128
+ [isEmpty, setValue, setOpenState]
129
+ );
130
+ return /* @__PURE__ */ jsx(StyledActionButton, { label, size, onClick: onActionButtonClick, children: /* @__PURE__ */ jsx(ActionButtonIcon, { size: "small", weight: "thin" }) });
131
+ };
132
+
77
133
  const Trigger = React.forwardRef(
78
134
  ({
79
135
  id,
@@ -82,6 +138,9 @@ const Trigger = React.forwardRef(
82
138
  "aria-describedby": ariaDescribedBy,
83
139
  "aria-invalid": ariaInvalid,
84
140
  placeholder,
141
+ openActionLabel,
142
+ clearActionLabel,
143
+ onChange,
85
144
  ...restProps
86
145
  }, forwardRef) => {
87
146
  const {
@@ -89,13 +148,19 @@ const Trigger = React.forwardRef(
89
148
  valid: comboboxValid,
90
149
  disabled,
91
150
  value,
92
- triggerRef
151
+ triggerRef,
152
+ onSearchValueChange,
153
+ searchValue,
154
+ setSearchValue
93
155
  } = useComboboxContext();
94
156
  const {
95
157
  formElementId,
96
158
  ariaDescribedBy: formFieldContextDescribedBy,
97
159
  ariaInvalid: formFieldAriaInvalid,
98
- valid: formFieldValid
160
+ valid: formFieldValid,
161
+ label,
162
+ isFloatingLabel,
163
+ focused
99
164
  } = useFormFieldContext();
100
165
  const valid = formFieldValid != null ? formFieldValid : comboboxValid;
101
166
  const inputProps = {
@@ -112,16 +177,37 @@ const Trigger = React.forwardRef(
112
177
  id: id != null ? id : formElementId,
113
178
  placeholder: (value == null ? void 0 : value.length) === 0 ? placeholder : void 0
114
179
  };
115
- const variants = {
116
- size
180
+ const shouldUseFloatingLabel = label !== null && isFloatingLabel;
181
+ const isFloating = placeholder !== void 0 || (value == null ? void 0 : value.length) !== 0 || focused;
182
+ const onInputChange = (e) => {
183
+ setSearchValue(e.target.value);
184
+ onSearchValueChange == null ? void 0 : onSearchValueChange(e.target.value);
185
+ onChange == null ? void 0 : onChange(e);
117
186
  };
118
187
  return /* @__PURE__ */ jsx(RadixPopover.Anchor, { ref: mergeRefs([triggerRef, forwardRef]), children: /* @__PURE__ */ jsx(
119
188
  Combobox$1,
120
189
  {
121
- render: /* @__PURE__ */ jsxs(StyledInput, { ...inputProps, ...variants, children: [
122
- children,
123
- /* @__PURE__ */ jsx(StyledActionButton, { label: "custom label", children: /* @__PURE__ */ jsx(IconChevronDown, { size: "small", weight: "thin" }) })
124
- ] })
190
+ render: /* @__PURE__ */ jsxs(
191
+ StyledInput,
192
+ {
193
+ value: searchValue,
194
+ onChange: onInputChange,
195
+ ...inputProps,
196
+ size,
197
+ children: [
198
+ shouldUseFloatingLabel && /* @__PURE__ */ jsx(FloatingLabel, { floating: isFloating, size, children: label }),
199
+ children,
200
+ /* @__PURE__ */ jsx(
201
+ TriggerActionButton,
202
+ {
203
+ openActionLabel,
204
+ clearActionLabel,
205
+ size
206
+ }
207
+ )
208
+ ]
209
+ }
210
+ )
125
211
  }
126
212
  ) });
127
213
  }
@@ -134,37 +220,24 @@ const StyledContent = styled(RadixPopover.Content, {
134
220
  fontSize: "$175",
135
221
  fontWeight: "normal",
136
222
  lineHeight: "1.5",
137
- minWidth: "var(--radix-popover-trigger-width)",
223
+ width: "var(--radix-popover-trigger-width)",
138
224
  zIndex: "$select",
139
225
  overflowY: "auto",
140
- marginTop: "$200"
226
+ marginTop: "$200",
227
+ padding: "$150",
228
+ boxSizing: "border-box",
229
+ outline: "1px solid transparent"
141
230
  });
142
231
 
143
- const Content = React.forwardRef(({ children, ...restProps }, forwardRef) => {
144
- const { triggerRef, contentRef } = useComboboxContext();
145
- return /* @__PURE__ */ jsx(
146
- StyledContent,
147
- {
148
- ...restProps,
149
- ref: mergeRefs([forwardRef, contentRef]),
150
- onOpenAutoFocus: (event) => event.preventDefault(),
151
- onInteractOutside: (event) => {
152
- var _a, _b;
153
- const target = event.target;
154
- const isTrigger = target === triggerRef.current;
155
- const isContent = (_b = target != null && ((_a = contentRef.current) == null ? void 0 : _a.contains(target))) != null ? _b : false;
156
- if (isTrigger || isContent) {
157
- event.preventDefault();
158
- }
159
- },
160
- children
161
- }
162
- );
232
+ const StyledItemCheck = styled(Primitive.span, {
233
+ color: "$icon-primary",
234
+ paddingX: "$100"
163
235
  });
164
-
165
236
  const StyledItem = styled(ComboboxItem, {
166
237
  display: "flex",
167
238
  alignItems: "center",
239
+ justifyContent: "space-between",
240
+ gap: "$200",
168
241
  borderRadius: "$50",
169
242
  boxSizing: "border-box",
170
243
  color: "$text-neutrals",
@@ -173,15 +246,19 @@ const StyledItem = styled(ComboboxItem, {
173
246
  lineHeight: 1.5,
174
247
  position: "relative",
175
248
  userSelect: "none",
176
- padding: "6px 0",
177
- paddingInline: "$150 $100",
249
+ paddingX: "$100",
250
+ paddingY: "10px",
178
251
  ...focus.css({
179
- boxShadow: "$focus-small",
180
- outline: "1px solid transparent"
252
+ boxShadow: "$focus-small"
181
253
  }),
182
- '&:hover:not([aria-disabled="true"])': {
183
- background: "$background-primary-subtle-hover",
184
- color: "$text-primary-hover"
254
+ '&:not([aria-disabled="true"])': {
255
+ _hover: {
256
+ background: "$background-primary-subtle-hover",
257
+ color: "$text-primary-hover",
258
+ ["".concat(StyledItemCheck)]: {
259
+ color: "$icon-primary-hover"
260
+ }
261
+ }
185
262
  },
186
263
  "&:disabled, &[aria-disabled=true], &[data-disabled]": {
187
264
  cursor: "default",
@@ -192,6 +269,10 @@ const StyledItem = styled(ComboboxItem, {
192
269
  const Item = React.forwardRef(
193
270
  ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
194
271
  const { "aria-disabled": ariaDisabled, ...restAriaDisabledProps } = useAriaDisabled(restProps, { allowArrows: true });
272
+ const { autoFilter, filteredItems } = useComboboxContext();
273
+ if (autoFilter !== false && !filteredItems.has(value)) {
274
+ return null;
275
+ }
195
276
  return /* @__PURE__ */ jsxs(
196
277
  StyledItem,
197
278
  {
@@ -203,28 +284,124 @@ const Item = React.forwardRef(
203
284
  ref: forwardRef,
204
285
  value,
205
286
  children: [
206
- /* @__PURE__ */ jsx(Ariakit.ComboboxItemCheck, {}),
207
- children
287
+ children,
288
+ /* @__PURE__ */ jsx(
289
+ ComboboxItemCheck,
290
+ {
291
+ render: ({ style, ...props }) => (
292
+ // AriakitComboboxItemCheck adds its owm inline styles which we want to omit here
293
+ /* @__PURE__ */ jsx(StyledItemCheck, { ...props })
294
+ ),
295
+ children: /* @__PURE__ */ jsx(IconCheckMark, { size: "small" })
296
+ }
297
+ )
208
298
  ]
209
299
  }
210
300
  );
211
301
  }
212
302
  );
213
303
 
214
- const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
304
+ const itemType = React.createElement(Item).type;
305
+ const getChildrenItemValues = (componentChildren) => {
306
+ const values = [];
307
+ const recurse = (children) => {
308
+ React.Children.forEach(children, (child) => {
309
+ if (!React.isValidElement(child)) {
310
+ return;
311
+ }
312
+ if (child.type === itemType) {
313
+ const props = child.props;
314
+ values.push(props.value);
315
+ return;
316
+ }
317
+ if (child.props.children) {
318
+ recurse(child.props.children);
319
+ }
320
+ });
321
+ };
322
+ recurse(componentChildren);
323
+ return values;
324
+ };
325
+
326
+ const isInsideRef = (element, ref) => {
327
+ var _a, _b;
328
+ return (_b = element != null && ((_a = ref.current) == null ? void 0 : _a.contains(element))) != null ? _b : false;
329
+ };
330
+ const Content = React.forwardRef(({ children, ...restProps }, forwardRef) => {
331
+ const {
332
+ triggerRef,
333
+ contentRef,
334
+ autoFilter,
335
+ filteredItems,
336
+ setFilteredItems,
337
+ searchValue,
338
+ noResultsText
339
+ } = useComboboxContext();
340
+ useEffect(() => {
341
+ const childrenItemValues = getChildrenItemValues(children);
342
+ setFilteredItems(
343
+ new Set(
344
+ autoFilter === false ? childrenItemValues : childrenItemValues.filter(
345
+ (child) => child.toLowerCase().includes(searchValue.toLowerCase())
346
+ )
347
+ )
348
+ );
349
+ }, [children, autoFilter, setFilteredItems, searchValue]);
350
+ return /* @__PURE__ */ jsx(
351
+ StyledContent,
352
+ {
353
+ ...restProps,
354
+ ref: mergeRefs([forwardRef, contentRef]),
355
+ onOpenAutoFocus: (event) => event.preventDefault(),
356
+ onInteractOutside: (event) => {
357
+ const target = event.target;
358
+ const isTrigger = isInsideRef(target, triggerRef);
359
+ const isContent = isInsideRef(target, contentRef);
360
+ if (isTrigger || isContent) {
361
+ event.preventDefault();
362
+ }
363
+ },
364
+ children: filteredItems.size === 0 ? noResultsText : children
365
+ }
366
+ );
367
+ });
215
368
 
216
- const StyledGroup = styled(Group$1, {});
369
+ const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
217
370
 
218
- const Group = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroup, { ...props, ref: forwardRef }));
371
+ const Group = React.forwardRef(({ children, ...rest }, forwardRef) => {
372
+ const { autoFilter, filteredItems } = useComboboxContext();
373
+ const childValues = useMemo(
374
+ // don't perform calculation if auto filter is disabled
375
+ () => autoFilter !== false ? getChildrenItemValues(children) : [],
376
+ [children, autoFilter]
377
+ );
378
+ const hasVisibleChildren = useMemo(
379
+ () => (
380
+ // don't perform calculation if auto filter is disabled
381
+ autoFilter !== false ? childValues.some((value) => filteredItems.has(value)) : true
382
+ ),
383
+ [childValues, filteredItems, autoFilter]
384
+ );
385
+ if (!hasVisibleChildren) {
386
+ return null;
387
+ }
388
+ return /* @__PURE__ */ jsx(Group$1, { ...rest, ref: forwardRef, children });
389
+ });
219
390
 
220
- const StyledGroupLabel = styled(GroupLabel$1, {});
391
+ const StyledGroupLabel = styled(GroupLabel$1, {
392
+ padding: "$100",
393
+ color: "$text-neutrals-subtle",
394
+ fontSize: "$150",
395
+ textTransform: "uppercase",
396
+ fontWeight: 650
397
+ });
221
398
 
222
399
  const GroupLabel = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroupLabel, { ...props, ref: forwardRef }));
223
400
 
224
401
  const StyledChip = styled(Primitive.div, {
225
402
  fontSize: "$150",
226
403
  padding: "$50 $100",
227
- borderRadius: "$half",
404
+ borderRadius: "$round",
228
405
  display: "flex",
229
406
  alignItems: "center",
230
407
  gap: "$50",
@@ -233,6 +410,12 @@ const StyledChip = styled(Primitive.div, {
233
410
  background: "$gray-100",
234
411
  color: "$gray-900"
235
412
  });
413
+ const StyledChipButton = styled(BaseButton, {
414
+ ...focus.css({
415
+ boxShadow: "$focus-small-outline",
416
+ borderColor: "$blue-400 !important"
417
+ })
418
+ });
236
419
  const StyledChipContent = styled(Primitive.div, {
237
420
  textOverflow: "ellipsis",
238
421
  whiteSpace: "nowrap",
@@ -250,11 +433,15 @@ const LeftSlot = StyledLeftSlot;
250
433
  const Chip = React.forwardRef(
251
434
  ({ children, disabled = false, onRemove, removeChipAriaLabel, ...restProps }, forwardRef) => /* @__PURE__ */ jsxs(StyledChip, { ...restProps, ref: forwardRef, children: [
252
435
  /* @__PURE__ */ jsx(StyledChipContent, { children }),
253
- !booleanify(disabled) && /* @__PURE__ */ jsx(BaseButton, { onClick: onRemove, "aria-label": removeChipAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", color: "gray-500" }) })
436
+ !booleanify(disabled) && /* @__PURE__ */ jsx(StyledChipButton, { onClick: onRemove, "aria-label": removeChipAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", color: "gray-500" }) })
254
437
  ] })
255
438
  );
256
439
  Chip.LeftSlot = LeftSlot;
257
440
 
441
+ const StyledValue = styled(Chip, {
442
+ marginTop: "$50"
443
+ });
444
+
258
445
  const Value = ({ removeChipAriaLabel }) => {
259
446
  const {
260
447
  value,
@@ -271,7 +458,7 @@ const Value = ({ removeChipAriaLabel }) => {
271
458
  return null;
272
459
  }
273
460
  return /* @__PURE__ */ jsx(Fragment, { children: value.map((item) => /* @__PURE__ */ jsx(
274
- Chip,
461
+ StyledValue,
275
462
  {
276
463
  onRemove: () => onItemRemove(item),
277
464
  disabled: isDisabled,
@@ -282,6 +469,15 @@ const Value = ({ removeChipAriaLabel }) => {
282
469
  )) });
283
470
  };
284
471
 
472
+ const StyledSeparator = styled(Primitive.div, {
473
+ backgroundColor: "$border-neutrals-subtle",
474
+ height: "1px",
475
+ width: "100%",
476
+ margin: "$100 0"
477
+ });
478
+
479
+ const Separator = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledSeparator, { ...props, ref: forwardRef, "aria-hidden": true }));
480
+
285
481
  const StyledComboboxContent = styled(Primitive.div, {});
286
482
 
287
483
  const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...restProps }, forwardRef) => {
@@ -318,7 +514,7 @@ const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...r
318
514
  setValue(newValue);
319
515
  };
320
516
  return /* @__PURE__ */ jsx(RadixPopover.Root, { open: openState, onOpenChange: setOpenState, children: /* @__PURE__ */ jsx(
321
- Ariakit.ComboboxProvider,
517
+ ComboboxProvider$1,
322
518
  {
323
519
  open: openState,
324
520
  setOpen: setOpenState,
@@ -340,15 +536,19 @@ const Combobox = React.forwardRef(
340
536
  required,
341
537
  value,
342
538
  defaultValue,
539
+ onSearchValueChange,
343
540
  onOpen,
344
541
  onValueChange,
345
542
  direction = "ltr",
543
+ autoFilter = true,
544
+ noResultsText,
346
545
  ...restProps
347
546
  }, forwardRef) => /* @__PURE__ */ jsx(
348
547
  ComboboxProvider,
349
548
  {
350
549
  defaultValue,
351
550
  value,
551
+ onSearchValueChange,
352
552
  defaultOpen,
353
553
  open,
354
554
  valid,
@@ -357,7 +557,17 @@ const Combobox = React.forwardRef(
357
557
  readOnly,
358
558
  "aria-disabled": ariaDisabled,
359
559
  direction,
360
- children: /* @__PURE__ */ jsx(Root, { ...restProps, value, ref: forwardRef })
560
+ autoFilter,
561
+ noResultsText,
562
+ children: /* @__PURE__ */ jsx(
563
+ Root,
564
+ {
565
+ ...restProps,
566
+ noResultsText,
567
+ value,
568
+ ref: forwardRef
569
+ }
570
+ )
361
571
  }
362
572
  )
363
573
  );
@@ -368,6 +578,11 @@ Combobox.Item = Item;
368
578
  Combobox.Group = Group;
369
579
  Combobox.GroupLabel = GroupLabel;
370
580
  Combobox.Value = Value;
581
+ Combobox.Separator = Separator;
582
+
583
+ var types = /*#__PURE__*/Object.freeze({
584
+ __proto__: null
585
+ });
371
586
 
372
- export { Chip, Combobox };
587
+ export { Combobox, types as ComboboxTypes };
373
588
  //# sourceMappingURL=module.js.map