@mirohq/design-system-combobox 0.1.0-combobox.2 → 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,22 +1,46 @@
1
- import { jsx, jsxs } 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';
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, 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
- import { stringAttrValue, booleanishAttrValue, mergeRefs } from '@mirohq/design-system-utils';
9
- import { IconChevronDown } from '@mirohq/design-system-icons';
10
- import { Input } from '@mirohq/design-system-input';
7
+ import { stringAttrValue, booleanishAttrValue, mergeRefs, booleanify } from '@mirohq/design-system-utils';
11
8
  import { styled } from '@mirohq/design-system-stitches';
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
13
  import { Primitive } from '@mirohq/design-system-primitive';
14
+ import { BaseButton } from '@mirohq/design-system-base-button';
15
15
 
16
- const StyledActionButton = styled(Input.ActionButton, {
17
- position: "absolute",
18
- right: "$100",
19
- top: "6px"
16
+ const StyledInput = styled(Input, {
17
+ flexWrap: "wrap",
18
+ flexGrow: 1,
19
+ gap: "0 $50",
20
+ "&&&": {
21
+ height: "max-content"
22
+ },
23
+ "& input": {
24
+ minWidth: "30px",
25
+ flexBasis: 0,
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"
43
+ }
20
44
  });
21
45
 
22
46
  const ComboboxContext = createContext({});
@@ -26,14 +50,17 @@ const ComboboxProvider = ({
26
50
  valid,
27
51
  value: valueProp,
28
52
  defaultValue: defaultValueProp,
53
+ onSearchValueChange,
54
+ autoFilter = true,
29
55
  ...restProps
30
56
  }) => {
31
57
  const triggerRef = useRef(null);
32
58
  const contentRef = useRef(null);
33
- const [openState, setOpenState] = useState(defaultOpen);
59
+ const [openState, setOpenState] = useState(defaultOpen != null ? defaultOpen : false);
34
60
  const [value, setValue] = useState(valueProp != null ? valueProp : []);
35
61
  const [defaultValue, setDefaultValue] = useState(defaultValueProp);
36
- const [searchValue, setSearchValue] = useState();
62
+ const [filteredItems, setFilteredItems] = useState(/* @__PURE__ */ new Set());
63
+ const [searchValue, setSearchValue] = useState("");
37
64
  const { valid: formFieldValid } = useFormFieldContext();
38
65
  return /* @__PURE__ */ jsx(
39
66
  ComboboxContext.Provider,
@@ -47,10 +74,14 @@ const ComboboxProvider = ({
47
74
  setValue,
48
75
  setDefaultValue,
49
76
  defaultValue,
77
+ onSearchValueChange,
78
+ triggerRef,
79
+ contentRef,
80
+ autoFilter,
50
81
  searchValue,
51
82
  setSearchValue,
52
- triggerRef,
53
- contentRef
83
+ filteredItems,
84
+ setFilteredItems
54
85
  },
55
86
  children
56
87
  }
@@ -58,6 +89,47 @@ const ComboboxProvider = ({
58
89
  };
59
90
  const useComboboxContext = () => useContext(ComboboxContext);
60
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
+
61
133
  const Trigger = React.forwardRef(
62
134
  ({
63
135
  id,
@@ -66,6 +138,9 @@ const Trigger = React.forwardRef(
66
138
  "aria-describedby": ariaDescribedBy,
67
139
  "aria-invalid": ariaInvalid,
68
140
  placeholder,
141
+ openActionLabel,
142
+ clearActionLabel,
143
+ onChange,
69
144
  ...restProps
70
145
  }, forwardRef) => {
71
146
  const {
@@ -73,13 +148,19 @@ const Trigger = React.forwardRef(
73
148
  valid: comboboxValid,
74
149
  disabled,
75
150
  value,
76
- triggerRef
151
+ triggerRef,
152
+ onSearchValueChange,
153
+ searchValue,
154
+ setSearchValue
77
155
  } = useComboboxContext();
78
156
  const {
79
157
  formElementId,
80
158
  ariaDescribedBy: formFieldContextDescribedBy,
81
159
  ariaInvalid: formFieldAriaInvalid,
82
- valid: formFieldValid
160
+ valid: formFieldValid,
161
+ label,
162
+ isFloatingLabel,
163
+ focused
83
164
  } = useFormFieldContext();
84
165
  const valid = formFieldValid != null ? formFieldValid : comboboxValid;
85
166
  const inputProps = {
@@ -96,16 +177,37 @@ const Trigger = React.forwardRef(
96
177
  id: id != null ? id : formElementId,
97
178
  placeholder: (value == null ? void 0 : value.length) === 0 ? placeholder : void 0
98
179
  };
99
- const variants = {
100
- 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);
101
186
  };
102
187
  return /* @__PURE__ */ jsx(RadixPopover.Anchor, { ref: mergeRefs([triggerRef, forwardRef]), children: /* @__PURE__ */ jsx(
103
188
  Combobox$1,
104
189
  {
105
- render: /* @__PURE__ */ jsxs(Input, { ...inputProps, ...variants, children: [
106
- children,
107
- /* @__PURE__ */ jsx(StyledActionButton, { label: "custom label", children: /* @__PURE__ */ jsx(IconChevronDown, { size: "small", weight: "thin" }) })
108
- ] })
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
+ )
109
211
  }
110
212
  ) });
111
213
  }
@@ -118,37 +220,24 @@ const StyledContent = styled(RadixPopover.Content, {
118
220
  fontSize: "$175",
119
221
  fontWeight: "normal",
120
222
  lineHeight: "1.5",
121
- minWidth: "var(--radix-popover-trigger-width)",
223
+ width: "var(--radix-popover-trigger-width)",
122
224
  zIndex: "$select",
123
225
  overflowY: "auto",
124
- marginTop: "$200"
226
+ marginTop: "$200",
227
+ padding: "$150",
228
+ boxSizing: "border-box",
229
+ outline: "1px solid transparent"
125
230
  });
126
231
 
127
- const Content = React.forwardRef(({ children, ...restProps }, forwardRef) => {
128
- const { triggerRef, contentRef } = useComboboxContext();
129
- return /* @__PURE__ */ jsx(
130
- StyledContent,
131
- {
132
- ...restProps,
133
- ref: mergeRefs([forwardRef, contentRef]),
134
- onOpenAutoFocus: (event) => event.preventDefault(),
135
- onInteractOutside: (event) => {
136
- var _a, _b;
137
- const target = event.target;
138
- const isTrigger = target === triggerRef.current;
139
- const isContent = (_b = target != null && ((_a = contentRef.current) == null ? void 0 : _a.contains(target))) != null ? _b : false;
140
- if (isTrigger || isContent) {
141
- event.preventDefault();
142
- }
143
- },
144
- children
145
- }
146
- );
232
+ const StyledItemCheck = styled(Primitive.span, {
233
+ color: "$icon-primary",
234
+ paddingX: "$100"
147
235
  });
148
-
149
236
  const StyledItem = styled(ComboboxItem, {
150
237
  display: "flex",
151
238
  alignItems: "center",
239
+ justifyContent: "space-between",
240
+ gap: "$200",
152
241
  borderRadius: "$50",
153
242
  boxSizing: "border-box",
154
243
  color: "$text-neutrals",
@@ -157,15 +246,19 @@ const StyledItem = styled(ComboboxItem, {
157
246
  lineHeight: 1.5,
158
247
  position: "relative",
159
248
  userSelect: "none",
160
- padding: "6px 0",
161
- paddingInline: "$150 $100",
249
+ paddingX: "$100",
250
+ paddingY: "10px",
162
251
  ...focus.css({
163
- boxShadow: "$focus-small",
164
- outline: "1px solid transparent"
252
+ boxShadow: "$focus-small"
165
253
  }),
166
- '&:hover:not([aria-disabled="true"])': {
167
- background: "$background-primary-subtle-hover",
168
- 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
+ }
169
262
  },
170
263
  "&:disabled, &[aria-disabled=true], &[data-disabled]": {
171
264
  cursor: "default",
@@ -176,6 +269,10 @@ const StyledItem = styled(ComboboxItem, {
176
269
  const Item = React.forwardRef(
177
270
  ({ disabled = false, value, textValue, children, ...restProps }, forwardRef) => {
178
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
+ }
179
276
  return /* @__PURE__ */ jsxs(
180
277
  StyledItem,
181
278
  {
@@ -187,36 +284,199 @@ const Item = React.forwardRef(
187
284
  ref: forwardRef,
188
285
  value,
189
286
  children: [
190
- /* @__PURE__ */ jsx(Ariakit.ComboboxItemCheck, {}),
191
- 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
+ )
192
298
  ]
193
299
  }
194
300
  );
195
301
  }
196
302
  );
197
303
 
198
- 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
+ };
199
325
 
200
- const StyledGroup = styled(Group$1, {});
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
+ });
368
+
369
+ const Portal = (props) => /* @__PURE__ */ jsx(Portal$1, { ...props });
201
370
 
202
- 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
+ });
203
390
 
204
- 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
+ });
205
398
 
206
399
  const GroupLabel = React.forwardRef((props, forwardRef) => /* @__PURE__ */ jsx(StyledGroupLabel, { ...props, ref: forwardRef }));
207
400
 
208
- const StyledValue = styled(Primitive.span, {});
401
+ const StyledChip = styled(Primitive.div, {
402
+ fontSize: "$150",
403
+ padding: "$50 $100",
404
+ borderRadius: "$round",
405
+ display: "flex",
406
+ alignItems: "center",
407
+ gap: "$50",
408
+ whiteSpace: "nowrap",
409
+ maxWidth: "$35",
410
+ background: "$gray-100",
411
+ color: "$gray-900"
412
+ });
413
+ const StyledChipButton = styled(BaseButton, {
414
+ ...focus.css({
415
+ boxShadow: "$focus-small-outline",
416
+ borderColor: "$blue-400 !important"
417
+ })
418
+ });
419
+ const StyledChipContent = styled(Primitive.div, {
420
+ textOverflow: "ellipsis",
421
+ whiteSpace: "nowrap",
422
+ overflow: "hidden",
423
+ lineHeight: 1.3
424
+ });
425
+
426
+ const StyledLeftSlot = styled(Primitive.span, {
427
+ order: -1,
428
+ marginRight: "$50"
429
+ });
209
430
 
210
- const Value = React.forwardRef(
211
- (props, forwardRef) => {
212
- const { value } = useComboboxContext();
213
- const isEmpty = (value == null ? void 0 : value.length) === 0;
214
- if (isEmpty) {
215
- return null;
216
- }
217
- return /* @__PURE__ */ jsx(StyledValue, { ref: forwardRef, ...props, children: value });
218
- }
431
+ const LeftSlot = StyledLeftSlot;
432
+
433
+ const Chip = React.forwardRef(
434
+ ({ children, disabled = false, onRemove, removeChipAriaLabel, ...restProps }, forwardRef) => /* @__PURE__ */ jsxs(StyledChip, { ...restProps, ref: forwardRef, children: [
435
+ /* @__PURE__ */ jsx(StyledChipContent, { children }),
436
+ !booleanify(disabled) && /* @__PURE__ */ jsx(StyledChipButton, { onClick: onRemove, "aria-label": removeChipAriaLabel, children: /* @__PURE__ */ jsx(IconCross, { size: "small", weight: "thin", color: "gray-500" }) })
437
+ ] })
219
438
  );
439
+ Chip.LeftSlot = LeftSlot;
440
+
441
+ const StyledValue = styled(Chip, {
442
+ marginTop: "$50"
443
+ });
444
+
445
+ const Value = ({ removeChipAriaLabel }) => {
446
+ const {
447
+ value,
448
+ setValue,
449
+ disabled,
450
+ "aria-disabled": ariaDisabled
451
+ } = useComboboxContext();
452
+ const isEmpty = value === void 0 || value.length === 0;
453
+ const isDisabled = ariaDisabled === true || disabled;
454
+ const onItemRemove = (item) => {
455
+ setValue((prevValue) => prevValue.filter((value2) => value2 !== item));
456
+ };
457
+ if (isEmpty) {
458
+ return null;
459
+ }
460
+ return /* @__PURE__ */ jsx(Fragment, { children: value.map((item) => /* @__PURE__ */ jsx(
461
+ StyledValue,
462
+ {
463
+ onRemove: () => onItemRemove(item),
464
+ disabled: isDisabled,
465
+ removeChipAriaLabel,
466
+ children: item
467
+ },
468
+ item
469
+ )) });
470
+ };
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 }));
220
480
 
221
481
  const StyledComboboxContent = styled(Primitive.div, {});
222
482
 
@@ -254,7 +514,7 @@ const Root = React.forwardRef(({ value: valueProp, onValueChange, children, ...r
254
514
  setValue(newValue);
255
515
  };
256
516
  return /* @__PURE__ */ jsx(RadixPopover.Root, { open: openState, onOpenChange: setOpenState, children: /* @__PURE__ */ jsx(
257
- Ariakit.ComboboxProvider,
517
+ ComboboxProvider$1,
258
518
  {
259
519
  open: openState,
260
520
  setOpen: setOpenState,
@@ -276,15 +536,19 @@ const Combobox = React.forwardRef(
276
536
  required,
277
537
  value,
278
538
  defaultValue,
539
+ onSearchValueChange,
279
540
  onOpen,
280
541
  onValueChange,
281
542
  direction = "ltr",
543
+ autoFilter = true,
544
+ noResultsText,
282
545
  ...restProps
283
546
  }, forwardRef) => /* @__PURE__ */ jsx(
284
547
  ComboboxProvider,
285
548
  {
286
549
  defaultValue,
287
550
  value,
551
+ onSearchValueChange,
288
552
  defaultOpen,
289
553
  open,
290
554
  valid,
@@ -293,7 +557,17 @@ const Combobox = React.forwardRef(
293
557
  readOnly,
294
558
  "aria-disabled": ariaDisabled,
295
559
  direction,
296
- 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
+ )
297
571
  }
298
572
  )
299
573
  );
@@ -304,6 +578,11 @@ Combobox.Item = Item;
304
578
  Combobox.Group = Group;
305
579
  Combobox.GroupLabel = GroupLabel;
306
580
  Combobox.Value = Value;
581
+ Combobox.Separator = Separator;
582
+
583
+ var types = /*#__PURE__*/Object.freeze({
584
+ __proto__: null
585
+ });
307
586
 
308
- export { Combobox };
587
+ export { Combobox, types as ComboboxTypes };
309
588
  //# sourceMappingURL=module.js.map