bits-ui 2.11.7 → 2.12.0

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.
@@ -129,8 +129,11 @@ export class CommandRootState {
129
129
  */
130
130
  #sort() {
131
131
  if (!this._commandState.search || this.opts.shouldFilter.current === false) {
132
- // If no search and no selection yet, select first item
133
- this.#selectFirstItem();
132
+ // if no search and no initial value set or when clearing search,
133
+ // we select the first item.
134
+ if (!this._commandState.value || !this.#isInitialMount) {
135
+ this.#selectFirstItem();
136
+ }
134
137
  return;
135
138
  }
136
139
  const scores = this._commandState.filtered.items;
@@ -20,10 +20,15 @@ export declare class DialogRootState {
20
20
  triggerId: string | undefined;
21
21
  descriptionId: string | undefined;
22
22
  cancelNode: HTMLElement | null;
23
- constructor(opts: DialogRootStateOpts);
23
+ nestedOpenCount: number;
24
+ readonly depth: number;
25
+ readonly parent: DialogRootState | null;
26
+ constructor(opts: DialogRootStateOpts, parent: DialogRootState | null);
24
27
  handleOpen(): void;
25
28
  handleClose(): void;
26
29
  getBitsAttr: typeof dialogAttrs.getAttr;
30
+ incrementNested(): void;
31
+ decrementNested(): void;
27
32
  readonly sharedProps: {
28
33
  readonly "data-state": "open" | "closed";
29
34
  };
@@ -137,8 +142,12 @@ export declare class DialogContentState {
137
142
  readonly style: {
138
143
  readonly pointerEvents: "auto";
139
144
  readonly outline: "none" | undefined;
145
+ readonly "--bits-dialog-depth": number;
146
+ readonly "--bits-dialog-nested-count": number;
140
147
  };
141
148
  readonly tabindex: -1 | undefined;
149
+ readonly "data-nested-open": "" | undefined;
150
+ readonly "data-nested": "" | undefined;
142
151
  };
143
152
  }
144
153
  interface DialogOverlayStateOpts extends WithRefOpts {
@@ -157,7 +166,11 @@ export declare class DialogOverlayState {
157
166
  readonly id: string;
158
167
  readonly style: {
159
168
  readonly pointerEvents: "auto";
169
+ readonly "--bits-dialog-depth": number;
170
+ readonly "--bits-dialog-nested-count": number;
160
171
  };
172
+ readonly "data-nested-open": "" | undefined;
173
+ readonly "data-nested": "" | undefined;
161
174
  };
162
175
  }
163
176
  interface AlertDialogCancelStateOpts extends WithRefOpts, ReadableBoxedValues<{
@@ -1,6 +1,6 @@
1
- import { attachRef, boxWith, } from "svelte-toolbelt";
1
+ import { attachRef, boxWith, onDestroyEffect, } from "svelte-toolbelt";
2
2
  import { Context, watch } from "runed";
3
- import { createBitsAttrs, boolToStr, getDataOpenClosed } from "../../internal/attrs.js";
3
+ import { createBitsAttrs, boolToStr, getDataOpenClosed, boolToEmptyStrOrUndef, } from "../../internal/attrs.js";
4
4
  import { kbd } from "../../internal/kbd.js";
5
5
  import { OpenChangeComplete } from "../../internal/open-change-complete.js";
6
6
  const dialogAttrs = createBitsAttrs({
@@ -10,7 +10,8 @@ const dialogAttrs = createBitsAttrs({
10
10
  const DialogRootContext = new Context("Dialog.Root | AlertDialog.Root");
11
11
  export class DialogRootState {
12
12
  static create(opts) {
13
- return DialogRootContext.set(new DialogRootState(opts));
13
+ const parent = DialogRootContext.getOr(null);
14
+ return DialogRootContext.set(new DialogRootState(opts, parent));
14
15
  }
15
16
  opts;
16
17
  triggerNode = $state(null);
@@ -21,8 +22,13 @@ export class DialogRootState {
21
22
  triggerId = $state(undefined);
22
23
  descriptionId = $state(undefined);
23
24
  cancelNode = $state(null);
24
- constructor(opts) {
25
+ nestedOpenCount = $state(0);
26
+ depth;
27
+ parent;
28
+ constructor(opts, parent) {
25
29
  this.opts = opts;
30
+ this.parent = parent;
31
+ this.depth = parent ? parent.depth + 1 : 0;
26
32
  this.handleOpen = this.handleOpen.bind(this);
27
33
  this.handleClose = this.handleClose.bind(this);
28
34
  new OpenChangeComplete({
@@ -33,6 +39,21 @@ export class DialogRootState {
33
39
  this.opts.onOpenChangeComplete.current(this.opts.open.current);
34
40
  },
35
41
  });
42
+ watch(() => this.opts.open.current, (isOpen) => {
43
+ if (!this.parent)
44
+ return;
45
+ if (isOpen) {
46
+ this.parent.incrementNested();
47
+ }
48
+ else {
49
+ this.parent.decrementNested();
50
+ }
51
+ }, { lazy: true });
52
+ onDestroyEffect(() => {
53
+ if (this.opts.open.current) {
54
+ this.parent?.decrementNested();
55
+ }
56
+ });
36
57
  }
37
58
  handleOpen() {
38
59
  if (this.opts.open.current)
@@ -47,6 +68,16 @@ export class DialogRootState {
47
68
  getBitsAttr = (part) => {
48
69
  return dialogAttrs.getAttr(part, this.opts.variant.current);
49
70
  };
71
+ incrementNested() {
72
+ this.nestedOpenCount++;
73
+ this.parent?.incrementNested();
74
+ }
75
+ decrementNested() {
76
+ if (this.nestedOpenCount === 0)
77
+ return;
78
+ this.nestedOpenCount--;
79
+ this.parent?.decrementNested();
80
+ }
50
81
  sharedProps = $derived.by(() => ({
51
82
  "data-state": getDataOpenClosed(this.opts.open.current),
52
83
  }));
@@ -231,8 +262,12 @@ export class DialogContentState {
231
262
  style: {
232
263
  pointerEvents: "auto",
233
264
  outline: this.root.opts.variant.current === "alert-dialog" ? "none" : undefined,
265
+ "--bits-dialog-depth": this.root.depth,
266
+ "--bits-dialog-nested-count": this.root.nestedOpenCount,
234
267
  },
235
268
  tabindex: this.root.opts.variant.current === "alert-dialog" ? -1 : undefined,
269
+ "data-nested-open": boolToEmptyStrOrUndef(this.root.nestedOpenCount > 0),
270
+ "data-nested": boolToEmptyStrOrUndef(this.root.parent !== null),
236
271
  ...this.root.sharedProps,
237
272
  ...this.attachment,
238
273
  }));
@@ -255,7 +290,11 @@ export class DialogOverlayState {
255
290
  [this.root.getBitsAttr("overlay")]: "",
256
291
  style: {
257
292
  pointerEvents: "auto",
293
+ "--bits-dialog-depth": this.root.depth,
294
+ "--bits-dialog-nested-count": this.root.nestedOpenCount,
258
295
  },
296
+ "data-nested-open": boolToEmptyStrOrUndef(this.root.nestedOpenCount > 0),
297
+ "data-nested": boolToEmptyStrOrUndef(this.root.parent !== null),
259
298
  ...this.root.sharedProps,
260
299
  ...this.attachment,
261
300
  }));
@@ -32,6 +32,7 @@ declare abstract class SelectBaseRootState {
32
32
  touchedInput: boolean;
33
33
  inputNode: HTMLElement | null;
34
34
  contentNode: HTMLElement | null;
35
+ viewportNode: HTMLElement | null;
35
36
  triggerNode: HTMLElement | null;
36
37
  valueId: string;
37
38
  highlightedNode: HTMLElement | null;
@@ -44,7 +45,7 @@ declare abstract class SelectBaseRootState {
44
45
  constructor(opts: SelectBaseRootStateOpts);
45
46
  setHighlightedNode(node: HTMLElement | null, initial?: boolean): void;
46
47
  getCandidateNodes(): HTMLElement[];
47
- setHighlightedToFirstCandidate(): void;
48
+ setHighlightedToFirstCandidate(initial?: boolean): void;
48
49
  getNodeByValue(value: string): HTMLElement | null;
49
50
  setOpen(open: boolean): void;
50
51
  toggleOpen(): void;
@@ -195,7 +196,6 @@ export declare class SelectContentState {
195
196
  readonly opts: SelectContentStateOpts;
196
197
  readonly root: SelectRoot;
197
198
  readonly attachment: RefAttachment;
198
- viewportNode: HTMLElement | null;
199
199
  isPositioned: boolean;
200
200
  domContext: DOMContext;
201
201
  constructor(opts: SelectContentStateOpts, root: SelectRoot);
@@ -45,6 +45,7 @@ class SelectBaseRootState {
45
45
  touchedInput = $state(false);
46
46
  inputNode = $state(null);
47
47
  contentNode = $state(null);
48
+ viewportNode = $state(null);
48
49
  triggerNode = $state(null);
49
50
  valueId = $state("");
50
51
  highlightedNode = $state(null);
@@ -94,12 +95,26 @@ class SelectBaseRootState {
94
95
  return [];
95
96
  return Array.from(node.querySelectorAll(`[${this.getBitsAttr("item")}]:not([data-disabled])`));
96
97
  }
97
- setHighlightedToFirstCandidate() {
98
+ setHighlightedToFirstCandidate(initial = false) {
98
99
  this.setHighlightedNode(null);
99
- const candidateNodes = this.getCandidateNodes();
100
- if (!candidateNodes.length)
100
+ let nodes = this.getCandidateNodes();
101
+ if (!nodes.length)
101
102
  return;
102
- this.setHighlightedNode(candidateNodes[0]);
103
+ // don't consider nodes that aren't visible within the viewport
104
+ if (this.viewportNode) {
105
+ const viewportRect = this.viewportNode.getBoundingClientRect();
106
+ nodes = nodes.filter((node) => {
107
+ if (!this.viewportNode)
108
+ return false;
109
+ const nodeRect = node.getBoundingClientRect();
110
+ const isNodeFullyVisible = nodeRect.right < viewportRect.right &&
111
+ nodeRect.left > viewportRect.left &&
112
+ nodeRect.bottom < viewportRect.bottom &&
113
+ nodeRect.top > viewportRect.top;
114
+ return isNodeFullyVisible;
115
+ });
116
+ }
117
+ this.setHighlightedNode(nodes[0], initial);
103
118
  }
104
119
  getNodeByValue(value) {
105
120
  const candidateNodes = this.getCandidateNodes();
@@ -185,10 +200,7 @@ export class SelectSingleRootState extends SelectBaseRootState {
185
200
  }
186
201
  }
187
202
  // if no value is set, we want to highlight the first item
188
- const firstCandidate = this.getCandidateNodes()[0];
189
- if (!firstCandidate)
190
- return;
191
- this.setHighlightedNode(firstCandidate, true);
203
+ this.setHighlightedToFirstCandidate(true);
192
204
  });
193
205
  }
194
206
  }
@@ -237,10 +249,7 @@ class SelectMultipleRootState extends SelectBaseRootState {
237
249
  }
238
250
  }
239
251
  // if no value is set, we want to highlight the first item
240
- const firstCandidate = this.getCandidateNodes()[0];
241
- if (!firstCandidate)
242
- return;
243
- this.setHighlightedNode(firstCandidate, true);
252
+ this.setHighlightedToFirstCandidate(true);
244
253
  });
245
254
  }
246
255
  }
@@ -691,7 +700,6 @@ export class SelectContentState {
691
700
  opts;
692
701
  root;
693
702
  attachment;
694
- viewportNode = $state(null);
695
703
  isPositioned = $state(false);
696
704
  domContext;
697
705
  constructor(opts, root) {
@@ -976,7 +984,9 @@ export class SelectViewportState {
976
984
  this.opts = opts;
977
985
  this.content = content;
978
986
  this.root = content.root;
979
- this.attachment = attachRef(opts.ref, (v) => (this.content.viewportNode = v));
987
+ this.attachment = attachRef(opts.ref, (v) => {
988
+ this.root.viewportNode = v;
989
+ });
980
990
  }
981
991
  props = $derived.by(() => ({
982
992
  id: this.opts.id.current,
@@ -1079,11 +1089,11 @@ export class SelectScrollDownButtonState {
1079
1089
  this.content = scrollButtonState.content;
1080
1090
  this.root = scrollButtonState.root;
1081
1091
  this.scrollButtonState.onAutoScroll = this.handleAutoScroll;
1082
- watch([() => this.content.viewportNode, () => this.content.isPositioned], () => {
1083
- if (!this.content.viewportNode || !this.content.isPositioned)
1092
+ watch([() => this.root.viewportNode, () => this.content.isPositioned], () => {
1093
+ if (!this.root.viewportNode || !this.content.isPositioned)
1084
1094
  return;
1085
1095
  this.handleScroll(true);
1086
- return on(this.content.viewportNode, "scroll", () => this.handleScroll());
1096
+ return on(this.root.viewportNode, "scroll", () => this.handleScroll());
1087
1097
  });
1088
1098
  /**
1089
1099
  * If the input value changes, this means that the filtered items may have changed,
@@ -1091,10 +1101,10 @@ export class SelectScrollDownButtonState {
1091
1101
  */
1092
1102
  watch([
1093
1103
  () => this.root.opts.inputValue.current,
1094
- () => this.content.viewportNode,
1104
+ () => this.root.viewportNode,
1095
1105
  () => this.content.isPositioned,
1096
1106
  ], () => {
1097
- if (!this.content.viewportNode || !this.content.isPositioned)
1107
+ if (!this.root.viewportNode || !this.content.isPositioned)
1098
1108
  return;
1099
1109
  this.handleScroll(true);
1100
1110
  });
@@ -1118,15 +1128,14 @@ export class SelectScrollDownButtonState {
1118
1128
  if (!manual) {
1119
1129
  this.scrollButtonState.handleUserScroll();
1120
1130
  }
1121
- if (!this.content.viewportNode)
1131
+ if (!this.root.viewportNode)
1122
1132
  return;
1123
- const maxScroll = this.content.viewportNode.scrollHeight - this.content.viewportNode.clientHeight;
1124
- const paddingTop = Number.parseInt(getComputedStyle(this.content.viewportNode).paddingTop, 10);
1125
- this.canScrollDown =
1126
- Math.ceil(this.content.viewportNode.scrollTop) < maxScroll - paddingTop;
1133
+ const maxScroll = this.root.viewportNode.scrollHeight - this.root.viewportNode.clientHeight;
1134
+ const paddingTop = Number.parseInt(getComputedStyle(this.root.viewportNode).paddingTop, 10);
1135
+ this.canScrollDown = Math.ceil(this.root.viewportNode.scrollTop) < maxScroll - paddingTop;
1127
1136
  };
1128
1137
  handleAutoScroll = () => {
1129
- const viewport = this.content.viewportNode;
1138
+ const viewport = this.root.viewportNode;
1130
1139
  const selectedItem = this.root.highlightedNode;
1131
1140
  if (!viewport || !selectedItem)
1132
1141
  return;
@@ -1150,11 +1159,11 @@ export class SelectScrollUpButtonState {
1150
1159
  this.content = scrollButtonState.content;
1151
1160
  this.root = scrollButtonState.root;
1152
1161
  this.scrollButtonState.onAutoScroll = this.handleAutoScroll;
1153
- watch([() => this.content.viewportNode, () => this.content.isPositioned], () => {
1154
- if (!this.content.viewportNode || !this.content.isPositioned)
1162
+ watch([() => this.root.viewportNode, () => this.content.isPositioned], () => {
1163
+ if (!this.root.viewportNode || !this.content.isPositioned)
1155
1164
  return;
1156
1165
  this.handleScroll(true);
1157
- return on(this.content.viewportNode, "scroll", () => this.handleScroll());
1166
+ return on(this.root.viewportNode, "scroll", () => this.handleScroll());
1158
1167
  });
1159
1168
  }
1160
1169
  /**
@@ -1165,16 +1174,16 @@ export class SelectScrollUpButtonState {
1165
1174
  if (!manual) {
1166
1175
  this.scrollButtonState.handleUserScroll();
1167
1176
  }
1168
- if (!this.content.viewportNode)
1177
+ if (!this.root.viewportNode)
1169
1178
  return;
1170
- const paddingTop = Number.parseInt(getComputedStyle(this.content.viewportNode).paddingTop, 10);
1171
- this.canScrollUp = this.content.viewportNode.scrollTop - paddingTop > 0.1;
1179
+ const paddingTop = Number.parseInt(getComputedStyle(this.root.viewportNode).paddingTop, 10);
1180
+ this.canScrollUp = this.root.viewportNode.scrollTop - paddingTop > 0.1;
1172
1181
  };
1173
1182
  handleAutoScroll = () => {
1174
- if (!this.content.viewportNode || !this.root.highlightedNode)
1183
+ if (!this.root.viewportNode || !this.root.highlightedNode)
1175
1184
  return;
1176
- this.content.viewportNode.scrollTop =
1177
- this.content.viewportNode.scrollTop - this.root.highlightedNode.offsetHeight;
1185
+ this.root.viewportNode.scrollTop =
1186
+ this.root.viewportNode.scrollTop - this.root.highlightedNode.offsetHeight;
1178
1187
  };
1179
1188
  props = $derived.by(() => ({
1180
1189
  ...this.scrollButtonState.props,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.11.7",
3
+ "version": "2.12.0",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",
@@ -41,8 +41,8 @@
41
41
  "@floating-ui/core": "^1.7.1",
42
42
  "@floating-ui/dom": "^1.7.1",
43
43
  "esm-env": "^1.1.2",
44
- "runed": "^0.31.1",
45
- "svelte-toolbelt": "^0.10.4",
44
+ "runed": "^0.35.1",
45
+ "svelte-toolbelt": "^0.10.6",
46
46
  "tabbable": "^6.2.0"
47
47
  },
48
48
  "peerDependencies": {