bits-ui 1.0.0-next.28 → 1.0.0-next.29

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.
@@ -20,6 +20,7 @@
20
20
  controlledOpen = false,
21
21
  controlledValue = false,
22
22
  items = [],
23
+ allowDeselect = true,
23
24
  children,
24
25
  }: ComboboxRootProps = $props();
25
26
 
@@ -63,6 +64,7 @@
63
64
  name: box.with(() => name),
64
65
  isCombobox: true,
65
66
  items: box.with(() => items),
67
+ allowDeselect: box.with(() => allowDeselect),
66
68
  });
67
69
  </script>
68
70
 
@@ -20,6 +20,7 @@
20
20
  controlledOpen = false,
21
21
  controlledValue = false,
22
22
  items = [],
23
+ allowDeselect = true,
23
24
  children,
24
25
  }: SelectRootProps = $props();
25
26
 
@@ -63,6 +64,7 @@
63
64
  name: box.with(() => name),
64
65
  isCombobox: false,
65
66
  items: box.with(() => items),
67
+ allowDeselect: box.with(() => allowDeselect),
66
68
  });
67
69
  </script>
68
70
 
@@ -18,6 +18,7 @@ type SelectBaseRootStateProps = ReadableBoxedValues<{
18
18
  label: string;
19
19
  disabled?: boolean;
20
20
  }[];
21
+ allowDeselect: boolean;
21
22
  }> & WritableBoxedValues<{
22
23
  open: boolean;
23
24
  }> & {
@@ -31,6 +32,7 @@ declare class SelectBaseRootState {
31
32
  open: SelectBaseRootStateProps["open"];
32
33
  scrollAlignment: SelectBaseRootStateProps["scrollAlignment"];
33
34
  items: SelectBaseRootStateProps["items"];
35
+ allowDeselect: SelectBaseRootStateProps["allowDeselect"];
34
36
  touchedInput: boolean;
35
37
  inputValue: string;
36
38
  inputNode: HTMLElement | null;
@@ -404,6 +406,7 @@ type InitSelectProps = {
404
406
  label: string;
405
407
  disabled?: boolean;
406
408
  }[];
409
+ allowDeselect: boolean;
407
410
  }> & WritableBoxedValues<{
408
411
  open: boolean;
409
412
  }> & {
@@ -24,6 +24,7 @@ class SelectBaseRootState {
24
24
  open;
25
25
  scrollAlignment;
26
26
  items;
27
+ allowDeselect;
27
28
  touchedInput = $state(false);
28
29
  inputValue = $state("");
29
30
  inputNode = $state(null);
@@ -59,6 +60,7 @@ class SelectBaseRootState {
59
60
  this.scrollAlignment = props.scrollAlignment;
60
61
  this.isCombobox = props.isCombobox;
61
62
  this.items = props.items;
63
+ this.allowDeselect = props.allowDeselect;
62
64
  this.bitsAttrs = getSelectBitsAttrs(this);
63
65
  $effect.pre(() => {
64
66
  if (!this.open.current) {
@@ -277,10 +279,15 @@ class SelectInputState {
277
279
  if (e.key === kbd.ENTER && !e.isComposing) {
278
280
  e.preventDefault();
279
281
  const highlightedValue = this.root.highlightedValue;
282
+ const isCurrentSelectedValue = highlightedValue === this.root.value.current;
283
+ if (!this.root.allowDeselect.current && isCurrentSelectedValue && !this.root.isMulti) {
284
+ this.root.handleClose();
285
+ return;
286
+ }
280
287
  if (highlightedValue) {
281
288
  this.root.toggleItem(highlightedValue, this.root.highlightedLabel ?? undefined);
282
289
  }
283
- if (!this.root.isMulti) {
290
+ if (!this.root.isMulti && !isCurrentSelectedValue) {
284
291
  this.root.handleClose();
285
292
  }
286
293
  }
@@ -328,7 +335,9 @@ class SelectInputState {
328
335
  };
329
336
  #oninput = (e) => {
330
337
  this.root.inputValue = e.currentTarget.value;
331
- this.root.setHighlightedToFirstCandidate();
338
+ afterTick(() => {
339
+ this.root.setHighlightedToFirstCandidate();
340
+ });
332
341
  };
333
342
  props = $derived.by(() => ({
334
343
  id: this.#id.current,
@@ -474,10 +483,15 @@ class SelectTriggerState {
474
483
  if ((e.key === kbd.ENTER || e.key === kbd.SPACE) && !e.isComposing) {
475
484
  e.preventDefault();
476
485
  const highlightedValue = this.root.highlightedValue;
486
+ const isCurrentSelectedValue = highlightedValue === this.root.value.current;
487
+ if (!this.root.allowDeselect.current && isCurrentSelectedValue && !this.root.isMulti) {
488
+ this.root.handleClose();
489
+ return;
490
+ }
477
491
  if (highlightedValue) {
478
492
  this.root.toggleItem(highlightedValue, this.root.highlightedLabel ?? undefined);
479
493
  }
480
- if (!this.root.isMulti) {
494
+ if (!this.root.isMulti && !isCurrentSelectedValue) {
481
495
  this.root.handleClose();
482
496
  }
483
497
  }
@@ -730,6 +744,13 @@ class SelectItemState {
730
744
  if (this.disabled.current)
731
745
  return;
732
746
  const isCurrentSelectedValue = this.value.current === this.root.value.current;
747
+ // if allowDeselect is false and the item is already selected and we're not in a
748
+ // multi select, do nothing and close the menu
749
+ if (!this.root.allowDeselect.current && isCurrentSelectedValue && !this.root.isMulti) {
750
+ this.root.handleClose();
751
+ return;
752
+ }
753
+ // otherwise, toggle the item and if we're not in a multi select, close the menu
733
754
  this.root.toggleItem(this.value.current, this.label.current);
734
755
  if (!this.root.isMulti && !isCurrentSelectedValue) {
735
756
  this.root.handleClose();
@@ -84,6 +84,11 @@ export type SelectBaseRootPropsWithoutHTML = WithChildren<{
84
84
  label: string;
85
85
  disabled?: boolean;
86
86
  }[];
87
+ /**
88
+ * Whether to allow the user to deselect an item by clicking on an already selected item.
89
+ * This is only applicable to `type="single"` selects/comboboxes.
90
+ */
91
+ allowDeselect?: boolean;
87
92
  }>;
88
93
  export type SelectSingleRootPropsWithoutHTML = {
89
94
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.0.0-next.28",
3
+ "version": "1.0.0-next.29",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",