bits-ui 1.0.0-next.27 → 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
 
@@ -153,9 +153,8 @@ declare class CommandItemState {
153
153
  readonly "data-selected": "" | undefined;
154
154
  readonly "data-command-item": "";
155
155
  readonly role: "option";
156
- readonly onpointerdown: (e: PointerEvent) => void;
157
156
  readonly onpointermove: () => void;
158
- readonly onpointerup: (e: PointerEvent) => void;
157
+ readonly onclick: () => void;
159
158
  };
160
159
  }
161
160
  type CommandLoadingStateProps = WithRefProps<ReadableBoxedValues<{
@@ -236,7 +236,7 @@ class CommandRootState {
236
236
  if (!grandparent)
237
237
  return;
238
238
  const firstChildOfParent = getFirstNonCommentChild(grandparent);
239
- if (firstChildOfParent && firstChildOfParent.dataset.value === item.dataset.value) {
239
+ if (firstChildOfParent && firstChildOfParent.dataset?.value === item.dataset?.value) {
240
240
  item
241
241
  ?.closest(GROUP_SELECTOR)
242
242
  ?.querySelector(GROUP_HEADING_SELECTOR)
@@ -696,21 +696,11 @@ class CommandItemState {
696
696
  return;
697
697
  this.#select();
698
698
  };
699
- #onpointerdown = (e) => {
699
+ #onclick = () => {
700
700
  if (this.#disabled.current)
701
701
  return;
702
- if (e.pointerType === "touch" || e.button !== 0)
703
- return e.preventDefault();
704
702
  this.#onSelect();
705
703
  };
706
- #onpointerup = (e) => {
707
- if (this.#disabled.current)
708
- return;
709
- if (e.pointerType === "touch") {
710
- e.preventDefault();
711
- this.#onSelect();
712
- }
713
- };
714
704
  props = $derived.by(() => ({
715
705
  id: this.id.current,
716
706
  "aria-disabled": getAriaDisabled(this.#disabled.current),
@@ -719,9 +709,8 @@ class CommandItemState {
719
709
  "data-selected": getDataSelected(this.isSelected),
720
710
  [ITEM_ATTR]: "",
721
711
  role: "option",
722
- onpointerdown: this.#onpointerdown,
723
712
  onpointermove: this.#onpointermove,
724
- onpointerup: this.#onpointerup,
713
+ onclick: this.#onclick,
725
714
  }));
726
715
  }
727
716
  class CommandLoadingState {
@@ -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.27",
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",