bits-ui 1.3.10 → 1.3.12

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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { box, mergeProps } from "svelte-toolbelt";
2
+ import { afterTick, box, mergeProps } from "svelte-toolbelt";
3
3
  import type { AlertDialogContentProps } from "../types.js";
4
4
  import DismissibleLayer from "../../utilities/dismissible-layer/dismissible-layer.svelte";
5
5
  import EscapeLayer from "../../utilities/escape-layer/escape-layer.svelte";
@@ -64,7 +64,9 @@
64
64
  onOpenAutoFocus(e);
65
65
  if (e.defaultPrevented) return;
66
66
  e.preventDefault();
67
- contentState.root.cancelNode?.focus();
67
+ afterTick(() => {
68
+ contentState.opts.ref.current?.focus();
69
+ });
68
70
  }}
69
71
  >
70
72
  {#snippet focusScope({ props: focusScopeProps })}
@@ -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.
@@ -258,7 +268,7 @@ class CommandRootState {
258
268
  const node = this.opts.ref.current;
259
269
  if (!node)
260
270
  return;
261
- const selectedNode = node.querySelector(`${COMMAND_VALID_ITEM_SELECTOR}[aria-selected="true"]`);
271
+ const selectedNode = node.querySelector(`${COMMAND_VALID_ITEM_SELECTOR}[data-selected]`);
262
272
  if (!selectedNode)
263
273
  return;
264
274
  return selectedNode;
@@ -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(() => {
@@ -662,7 +677,7 @@ class CommandInputState {
662
677
  opts;
663
678
  root;
664
679
  #selectedItemId = $derived.by(() => {
665
- const item = this.root.viewportNode?.querySelector(`${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(this.opts.value.current)}"]`);
680
+ const item = this.root.viewportNode?.querySelector(`${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(this.root.opts.value.current)}"]`);
666
681
  if (!item)
667
682
  return;
668
683
  return item?.getAttribute("id") ?? undefined;
@@ -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}
@@ -17,6 +17,7 @@
17
17
  loop = true,
18
18
  onInteractOutside = noop,
19
19
  onCloseAutoFocus = noop,
20
+ onOpenAutoFocus = noop,
20
21
  preventScroll = true,
21
22
  // we need to explicitly pass this prop to the PopperLayer to override
22
23
  // the default menu behavior of handling outside interactions on the trigger
@@ -73,6 +74,7 @@
73
74
  {preventScroll}
74
75
  onInteractOutside={handleInteractOutside}
75
76
  onEscapeKeydown={handleEscapeKeydown}
77
+ {onOpenAutoFocus}
76
78
  {isValidEvent}
77
79
  trapFocus
78
80
  {loop}
@@ -105,6 +107,7 @@
105
107
  {preventScroll}
106
108
  onInteractOutside={handleInteractOutside}
107
109
  onEscapeKeydown={handleEscapeKeydown}
110
+ {onOpenAutoFocus}
108
111
  {isValidEvent}
109
112
  trapFocus
110
113
  {loop}
@@ -2,7 +2,7 @@ import type { MenuContentProps, MenuContentPropsWithoutHTML } from "../menu/type
2
2
  import type { WithChild, Without } from "../../internal/types.js";
3
3
  import type { BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
4
4
  export type ContextMenuContentPropsWithoutHTML = Omit<MenuContentPropsWithoutHTML, "align" | "side" | "sideOffset">;
5
- export type ContextMenuContentProps = Omit<MenuContentProps, "side" | "onOpenAutoFocus" | "sideOffset" | "align">;
5
+ export type ContextMenuContentProps = Omit<MenuContentProps, "side" | "sideOffset" | "align">;
6
6
  export type ContextMenuTriggerPropsWithoutHTML = WithChild<{
7
7
  /**
8
8
  * Whether the context menu trigger is disabled. If disabled, the trigger will not
@@ -71,6 +71,7 @@ declare class DialogCloseState {
71
71
  readonly onclick: (e: BitsMouseEvent) => void;
72
72
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
73
73
  readonly disabled: true | undefined;
74
+ readonly tabindex: 0;
74
75
  };
75
76
  }
76
77
  type DialogActionStateProps = WithRefProps;
@@ -120,11 +121,14 @@ declare class DialogContentState {
120
121
  readonly "data-state": "open" | "closed";
121
122
  readonly id: string;
122
123
  readonly role: "dialog" | "alertdialog";
124
+ readonly "aria-modal": "true";
123
125
  readonly "aria-describedby": string | undefined;
124
126
  readonly "aria-labelledby": string | undefined;
125
127
  readonly style: {
126
128
  readonly pointerEvents: "auto";
129
+ readonly outline: "none" | undefined;
127
130
  };
131
+ readonly tabindex: -1 | undefined;
128
132
  };
129
133
  }
130
134
  type DialogOverlayStateProps = WithRefProps;
@@ -157,6 +161,7 @@ declare class AlertDialogCancelState {
157
161
  readonly id: string;
158
162
  readonly onclick: (e: BitsMouseEvent) => void;
159
163
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
164
+ readonly tabindex: 0;
160
165
  };
161
166
  }
162
167
  export declare function useDialogRoot(props: DialogRootStateProps): DialogRootState;
@@ -123,6 +123,7 @@ class DialogCloseState {
123
123
  onclick: this.onclick,
124
124
  onkeydown: this.onkeydown,
125
125
  disabled: this.opts.disabled.current ? true : undefined,
126
+ tabindex: 0,
126
127
  ...this.root.sharedProps,
127
128
  }));
128
129
  }
@@ -204,12 +205,15 @@ class DialogContentState {
204
205
  props = $derived.by(() => ({
205
206
  id: this.opts.id.current,
206
207
  role: this.root.opts.variant.current === "alert-dialog" ? "alertdialog" : "dialog",
208
+ "aria-modal": "true",
207
209
  "aria-describedby": this.root.descriptionId,
208
210
  "aria-labelledby": this.root.titleId,
209
211
  [this.root.attrs.content]: "",
210
212
  style: {
211
213
  pointerEvents: "auto",
214
+ outline: this.root.opts.variant.current === "alert-dialog" ? "none" : undefined,
212
215
  },
216
+ tabindex: this.root.opts.variant.current === "alert-dialog" ? -1 : undefined,
213
217
  ...this.root.sharedProps,
214
218
  }));
215
219
  }
@@ -270,6 +274,7 @@ class AlertDialogCancelState {
270
274
  [this.root.attrs.cancel]: "",
271
275
  onclick: this.onclick,
272
276
  onkeydown: this.onkeydown,
277
+ tabindex: 0,
273
278
  ...this.root.sharedProps,
274
279
  }));
275
280
  }
@@ -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,
@@ -138,6 +138,7 @@ declare class SelectTriggerState {
138
138
  readonly id: string;
139
139
  readonly disabled: true | undefined;
140
140
  readonly "aria-haspopup": "listbox";
141
+ readonly "aria-expanded": "true" | "false";
141
142
  readonly "aria-activedescendant": string | undefined;
142
143
  readonly "data-state": "open" | "closed";
143
144
  readonly "data-disabled": "" | undefined;
@@ -174,9 +175,10 @@ declare class SelectContentState {
174
175
  readonly outline: "none";
175
176
  readonly boxSizing: "border-box";
176
177
  readonly pointerEvents: "auto";
177
- };
178
+ } | undefined;
178
179
  readonly id: string;
179
180
  readonly role: "listbox";
181
+ readonly "aria-multiselectable": "true" | undefined;
180
182
  readonly "data-state": "open" | "closed";
181
183
  readonly style: {
182
184
  readonly display: "flex";
@@ -589,6 +589,7 @@ class SelectTriggerState {
589
589
  id: this.opts.id.current,
590
590
  disabled: this.root.opts.disabled.current ? true : undefined,
591
591
  "aria-haspopup": "listbox",
592
+ "aria-expanded": getAriaExpanded(this.root.opts.open.current),
592
593
  "aria-activedescendant": this.root.highlightedId,
593
594
  "data-state": getDataOpenClosed(this.root.opts.open.current),
594
595
  "data-disabled": getDataDisabled(this.root.opts.disabled.current),
@@ -665,6 +666,7 @@ class SelectContentState {
665
666
  props = $derived.by(() => ({
666
667
  id: this.opts.id.current,
667
668
  role: "listbox",
669
+ "aria-multiselectable": this.root.isMulti ? "true" : undefined,
668
670
  "data-state": getDataOpenClosed(this.root.opts.open.current),
669
671
  [this.root.bitsAttrs.content]: "",
670
672
  style: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",