bits-ui 1.3.11 → 1.3.13

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.
@@ -26,6 +26,7 @@ declare class CommandRootState {
26
26
  labelNode: HTMLElement | null;
27
27
  commandState: CommandState;
28
28
  _commandState: CommandState;
29
+ searchHasHadValue: boolean;
29
30
  setState<K extends keyof CommandState>(key: K, value: CommandState[K], opts?: boolean): void;
30
31
  constructor(opts: CommandRootStateProps);
31
32
  /**
@@ -266,7 +267,7 @@ declare class CommandSeparatorState {
266
267
  constructor(opts: CommandSeparatorStateProps, root: CommandRootState);
267
268
  props: {
268
269
  readonly id: string;
269
- readonly role: "separator";
270
+ readonly "aria-hidden": "true";
270
271
  readonly "data-command-separator": "";
271
272
  };
272
273
  }
@@ -44,6 +44,8 @@ class CommandRootState {
44
44
  commandState = $state.raw(null);
45
45
  // internal state that we mutate in batches and publish to the `state` at once
46
46
  _commandState = $state(null);
47
+ // whether the search has had a value other than ""
48
+ searchHasHadValue = $state(false);
47
49
  #snapshot() {
48
50
  return $state.snapshot(this._commandState);
49
51
  }
@@ -70,6 +72,9 @@ class CommandRootState {
70
72
  this.#filterItems();
71
73
  this.#sort();
72
74
  this.#selectFirstItem();
75
+ afterTick(() => {
76
+ this.#selectFirstItem();
77
+ });
73
78
  }
74
79
  else if (key === "value") {
75
80
  // opts is a boolean referring to whether it should NOT be scrolled into view
@@ -100,6 +105,11 @@ class CommandRootState {
100
105
  this.commandState = defaultState;
101
106
  useRefById(opts);
102
107
  this.onkeydown = this.onkeydown.bind(this);
108
+ $effect(() => {
109
+ if (this._commandState.search !== "") {
110
+ this.searchHasHadValue = true;
111
+ }
112
+ });
103
113
  }
104
114
  /**
105
115
  * Calculates score for an item based on search text and keywords.
@@ -268,7 +278,7 @@ class CommandRootState {
268
278
  * Special handling for first items in groups.
269
279
  */
270
280
  #scrollSelectedIntoView() {
271
- afterSleep(1, () => {
281
+ afterTick(() => {
272
282
  const item = this.#getSelectedItem();
273
283
  if (!item)
274
284
  return;
@@ -277,10 +287,10 @@ class CommandRootState {
277
287
  return;
278
288
  const firstChildOfParent = getFirstNonCommentChild(grandparent);
279
289
  if (firstChildOfParent && firstChildOfParent.dataset?.value === item.dataset?.value) {
280
- item
290
+ const closestGroupHeader = item
281
291
  ?.closest(COMMAND_GROUP_SELECTOR)
282
- ?.querySelector(COMMAND_GROUP_HEADING_SELECTOR)
283
- ?.scrollIntoView({ block: "nearest" });
292
+ ?.querySelector(COMMAND_GROUP_HEADING_SELECTOR);
293
+ closestGroupHeader?.scrollIntoView({ block: "nearest" });
284
294
  return;
285
295
  }
286
296
  item.scrollIntoView({ block: "nearest" });
@@ -419,15 +429,16 @@ class CommandRootState {
419
429
  this.#sort();
420
430
  this.#scheduleUpdate();
421
431
  return () => {
432
+ const selectedItem = this.#getSelectedItem();
422
433
  this.allIds.delete(id);
423
434
  this.allItems.delete(id);
424
435
  this.commandState.filtered.items.delete(id);
425
- const selectedItem = this.#getSelectedItem();
426
436
  this.#filterItems();
427
437
  // The item removed have been the selected one,
428
438
  // so selection should be moved to the first
429
- if (selectedItem?.getAttribute("id") === id)
439
+ if (selectedItem?.getAttribute("id") === id) {
430
440
  this.#selectFirstItem();
441
+ }
431
442
  this.#scheduleUpdate();
432
443
  };
433
444
  }
@@ -558,12 +569,16 @@ class CommandEmptyState {
558
569
  opts;
559
570
  root;
560
571
  #isInitialRender = true;
561
- shouldRender = $derived.by(() => (this.root._commandState.filtered.count === 0 && this.#isInitialRender === false) ||
562
- this.opts.forceMount.current);
572
+ shouldRender = $derived.by(() => {
573
+ return ((this.root._commandState.filtered.count === 0 &&
574
+ this.#isInitialRender === false &&
575
+ this.root.searchHasHadValue) ||
576
+ this.opts.forceMount.current);
577
+ });
563
578
  constructor(opts, root) {
564
579
  this.opts = opts;
565
580
  this.root = root;
566
- $effect(() => {
581
+ $effect.pre(() => {
567
582
  this.#isInitialRender = false;
568
583
  });
569
584
  useRefById({
@@ -599,7 +614,7 @@ class CommandGroupContainerState {
599
614
  ...opts,
600
615
  deps: () => this.shouldRender,
601
616
  });
602
- $effect(() => {
617
+ watch(() => this.opts.id.current, () => {
603
618
  return this.root.registerGroup(this.opts.id.current);
604
619
  });
605
620
  $effect(() => {
@@ -712,6 +727,7 @@ class CommandItemState {
712
727
  });
713
728
  trueValue = $state("");
714
729
  shouldRender = $derived.by(() => {
730
+ this.opts.ref.current;
715
731
  if (this.#trueForceMount ||
716
732
  this.root.opts.shouldFilter.current === false ||
717
733
  !this.root.commandState.search) {
@@ -736,6 +752,7 @@ class CommandItemState {
736
752
  () => this.opts.id.current,
737
753
  () => this.#group?.opts.id.current,
738
754
  () => this.opts.forceMount.current,
755
+ () => this.opts.ref.current,
739
756
  ], () => {
740
757
  if (this.opts.forceMount.current)
741
758
  return;
@@ -807,7 +824,7 @@ class CommandLoadingState {
807
824
  class CommandSeparatorState {
808
825
  opts;
809
826
  root;
810
- shouldRender = $derived.by(() => !this.root.commandState.search || this.opts.forceMount.current);
827
+ shouldRender = $derived.by(() => !this.root._commandState.search || this.opts.forceMount.current);
811
828
  constructor(opts, root) {
812
829
  this.opts = opts;
813
830
  this.root = root;
@@ -818,7 +835,8 @@ class CommandSeparatorState {
818
835
  }
819
836
  props = $derived.by(() => ({
820
837
  id: this.opts.id.current,
821
- role: "separator",
838
+ // role="separator" cannot belong to a role="listbox"
839
+ "aria-hidden": "true",
822
840
  [COMMAND_SEPARATOR_ATTR]: "",
823
841
  }));
824
842
  }
@@ -35,7 +35,7 @@
35
35
  </script>
36
36
 
37
37
  {#key itemState.root.key}
38
- <div style="display: contents;" data-item-wrapper data-value={value}>
38
+ <div style="display: contents;" data-item-wrapper data-value={itemState.trueValue}>
39
39
  {#if itemState.shouldRender}
40
40
  {#if child}
41
41
  {@render child({ props: mergedProps })}
@@ -25,10 +25,12 @@
25
25
  const mergedProps = $derived(mergeProps(restProps, listState.props));
26
26
  </script>
27
27
 
28
- {#if child}
29
- {@render child({ props: mergedProps })}
30
- {:else}
31
- <div {...mergedProps}>
32
- {@render children?.()}
33
- </div>
34
- {/if}
28
+ {#key listState.root._commandState.search === ""}
29
+ {#if child}
30
+ {@render child({ props: mergedProps })}
31
+ {:else}
32
+ <div {...mergedProps}>
33
+ {@render children?.()}
34
+ </div>
35
+ {/if}
36
+ {/key}
@@ -23,6 +23,7 @@
23
23
  // the default menu behavior of handling outside interactions on the trigger
24
24
  onEscapeKeydown = noop,
25
25
  forceMount = false,
26
+ trapFocus = true,
26
27
  ...restProps
27
28
  }: ContextMenuContentProps = $props();
28
29
 
@@ -76,7 +77,7 @@
76
77
  onEscapeKeydown={handleEscapeKeydown}
77
78
  {onOpenAutoFocus}
78
79
  {isValidEvent}
79
- trapFocus
80
+ {trapFocus}
80
81
  {loop}
81
82
  {id}
82
83
  >
@@ -109,7 +110,7 @@
109
110
  onEscapeKeydown={handleEscapeKeydown}
110
111
  {onOpenAutoFocus}
111
112
  {isValidEvent}
112
- trapFocus
113
+ {trapFocus}
113
114
  {loop}
114
115
  {id}
115
116
  >
@@ -25,6 +25,7 @@
25
25
  onOpenAutoFocus: onOpenAutoFocusProp = noop,
26
26
  onCloseAutoFocus: onCloseAutoFocusProp = noop,
27
27
  onFocusOutside = noop,
28
+ trapFocus = false,
28
29
  ...restProps
29
30
  }: MenuSubContentStaticProps = $props();
30
31
 
@@ -115,7 +116,7 @@
115
116
  onFocusOutside={handleOnFocusOutside}
116
117
  preventScroll={false}
117
118
  {loop}
118
- trapFocus={false}
119
+ {trapFocus}
119
120
  isStatic
120
121
  >
121
122
  {#snippet popper({ props })}
@@ -145,7 +146,7 @@
145
146
  onFocusOutside={handleOnFocusOutside}
146
147
  preventScroll={false}
147
148
  {loop}
148
- trapFocus={false}
149
+ {trapFocus}
149
150
  isStatic
150
151
  >
151
152
  {#snippet popper({ props })}
@@ -26,6 +26,7 @@
26
26
  onCloseAutoFocus: onCloseAutoFocusProp = noop,
27
27
  onFocusOutside = noop,
28
28
  side = "right",
29
+ trapFocus = false,
29
30
  ...restProps
30
31
  }: MenuSubContentProps = $props();
31
32
 
@@ -117,7 +118,7 @@
117
118
  onFocusOutside={handleOnFocusOutside}
118
119
  preventScroll={false}
119
120
  {loop}
120
- trapFocus={false}
121
+ {trapFocus}
121
122
  >
122
123
  {#snippet popper({ props, wrapperProps })}
123
124
  {@const finalProps = mergeProps(props, mergedProps, {
@@ -152,7 +153,7 @@
152
153
  onFocusOutside={handleOnFocusOutside}
153
154
  preventScroll={false}
154
155
  {loop}
155
- trapFocus={false}
156
+ {trapFocus}
156
157
  >
157
158
  {#snippet popper({ props, wrapperProps })}
158
159
  {@const finalProps = mergeProps(props, mergedProps, {
@@ -1,5 +1,5 @@
1
1
  import { useRefById } from "svelte-toolbelt";
2
- import { Context } from "runed";
2
+ import { Context, watch } from "runed";
3
3
  import { getAriaChecked, getAriaRequired, getDataDisabled } from "../../internal/attrs.js";
4
4
  import { useRovingFocus, } from "../../internal/use-roving-focus.svelte.js";
5
5
  import { kbd } from "../../internal/kbd.js";
@@ -43,6 +43,7 @@ class RadioGroupItemState {
43
43
  checked = $derived.by(() => this.root.opts.value.current === this.opts.value.current);
44
44
  #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current);
45
45
  #isChecked = $derived.by(() => this.root.isChecked(this.opts.value.current));
46
+ #tabIndex = $state(-1);
46
47
  constructor(opts, root) {
47
48
  this.opts = opts;
48
49
  this.root = root;
@@ -57,6 +58,12 @@ class RadioGroupItemState {
57
58
  $effect(() => {
58
59
  this.#tabIndex = this.root.rovingFocusGroup.getTabIndex(this.opts.ref.current);
59
60
  });
61
+ watch([() => this.opts.value.current, () => this.root.opts.value.current], () => {
62
+ if (this.opts.value.current === this.root.opts.value.current) {
63
+ this.root.rovingFocusGroup.setCurrentTabStopId(this.opts.id.current);
64
+ this.#tabIndex = 0;
65
+ }
66
+ });
60
67
  this.onclick = this.onclick.bind(this);
61
68
  this.onkeydown = this.onkeydown.bind(this);
62
69
  this.onfocus = this.onfocus.bind(this);
@@ -81,7 +88,6 @@ class RadioGroupItemState {
81
88
  }
82
89
  this.root.rovingFocusGroup.handleKeydown(this.opts.ref.current, e, true);
83
90
  }
84
- #tabIndex = $state(-1);
85
91
  snippetProps = $derived.by(() => ({ checked: this.#isChecked }));
86
92
  props = $derived.by(() => ({
87
93
  id: this.opts.id.current,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",