bits-ui 1.0.0-next.17 → 1.0.0-next.19

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.
Files changed (97) hide show
  1. package/dist/bits/combobox/components/combobox-input.svelte +2 -2
  2. package/dist/bits/combobox/components/combobox-trigger.svelte +2 -2
  3. package/dist/bits/combobox/components/combobox.svelte +9 -7
  4. package/dist/bits/combobox/exports.d.ts +8 -8
  5. package/dist/bits/combobox/exports.js +8 -8
  6. package/dist/bits/combobox/types.d.ts +1 -1
  7. package/dist/bits/dialog/components/dialog-content.svelte +1 -1
  8. package/dist/bits/index.d.ts +0 -1
  9. package/dist/bits/index.js +0 -1
  10. package/dist/bits/menu/menu.svelte.js +2 -2
  11. package/dist/bits/menubar/menubar.svelte.js +1 -1
  12. package/dist/bits/{listbox/components/listbox-content-static.svelte → select/components/select-content-static.svelte} +6 -6
  13. package/dist/bits/{listbox/components/listbox-group-heading.svelte.d.ts → select/components/select-content-static.svelte.d.ts} +4 -4
  14. package/dist/bits/select/components/select-content.svelte +56 -35
  15. package/dist/bits/select/components/select-group-heading.svelte +3 -3
  16. package/dist/bits/select/components/select-group.svelte +4 -3
  17. package/dist/bits/{listbox/components/listbox-hidden-input.svelte → select/components/select-hidden-input.svelte} +4 -4
  18. package/dist/bits/{listbox/components/listbox-hidden-input.svelte.d.ts → select/components/select-hidden-input.svelte.d.ts} +4 -4
  19. package/dist/bits/select/components/select-item.svelte +14 -9
  20. package/dist/bits/select/components/select-scroll-down-button.svelte +20 -9
  21. package/dist/bits/select/components/select-scroll-down-button.svelte.d.ts +2 -2
  22. package/dist/bits/select/components/select-scroll-up-button.svelte +21 -10
  23. package/dist/bits/select/components/select-trigger.svelte +5 -7
  24. package/dist/bits/select/components/select-viewport.svelte +10 -0
  25. package/dist/bits/select/components/select.svelte +54 -56
  26. package/dist/bits/select/exports.d.ts +4 -7
  27. package/dist/bits/select/exports.js +3 -6
  28. package/dist/bits/select/select.svelte.d.ts +272 -258
  29. package/dist/bits/select/select.svelte.js +764 -831
  30. package/dist/bits/select/types.d.ts +128 -79
  31. package/dist/bits/slider/components/slider.svelte +2 -2
  32. package/dist/bits/slider/slider.svelte.d.ts +2 -2
  33. package/dist/bits/slider/slider.svelte.js +4 -4
  34. package/dist/bits/slider/types.d.ts +3 -2
  35. package/dist/bits/utilities/popper-layer/popper-layer.svelte +1 -3
  36. package/dist/internal/arrays.d.ts +23 -0
  37. package/dist/internal/arrays.js +35 -0
  38. package/dist/internal/date-time/calendar-helpers.svelte.js +6 -1
  39. package/dist/internal/use-body-scroll-lock.svelte.js +53 -42
  40. package/dist/internal/use-data-typeahead.svelte.d.ts +12 -0
  41. package/dist/internal/use-data-typeahead.svelte.js +30 -0
  42. package/dist/internal/use-dom-typeahead.svelte.d.ts +11 -0
  43. package/dist/internal/use-dom-typeahead.svelte.js +30 -0
  44. package/dist/types.d.ts +1 -1
  45. package/package.json +2 -2
  46. package/dist/bits/listbox/components/listbox-content-static.svelte.d.ts +0 -19
  47. package/dist/bits/listbox/components/listbox-content.svelte +0 -76
  48. package/dist/bits/listbox/components/listbox-content.svelte.d.ts +0 -19
  49. package/dist/bits/listbox/components/listbox-group-heading.svelte +0 -32
  50. package/dist/bits/listbox/components/listbox-group.svelte +0 -32
  51. package/dist/bits/listbox/components/listbox-group.svelte.d.ts +0 -19
  52. package/dist/bits/listbox/components/listbox-item.svelte +0 -43
  53. package/dist/bits/listbox/components/listbox-item.svelte.d.ts +0 -19
  54. package/dist/bits/listbox/components/listbox-scroll-down-button.svelte +0 -39
  55. package/dist/bits/listbox/components/listbox-scroll-down-button.svelte.d.ts +0 -19
  56. package/dist/bits/listbox/components/listbox-scroll-up-button.svelte +0 -39
  57. package/dist/bits/listbox/components/listbox-scroll-up-button.svelte.d.ts +0 -19
  58. package/dist/bits/listbox/components/listbox-trigger.svelte +0 -35
  59. package/dist/bits/listbox/components/listbox-trigger.svelte.d.ts +0 -19
  60. package/dist/bits/listbox/components/listbox-viewport.svelte +0 -54
  61. package/dist/bits/listbox/components/listbox-viewport.svelte.d.ts +0 -19
  62. package/dist/bits/listbox/components/listbox.svelte +0 -81
  63. package/dist/bits/listbox/components/listbox.svelte.d.ts +0 -18
  64. package/dist/bits/listbox/exports.d.ts +0 -12
  65. package/dist/bits/listbox/exports.js +0 -11
  66. package/dist/bits/listbox/index.d.ts +0 -1
  67. package/dist/bits/listbox/index.js +0 -1
  68. package/dist/bits/listbox/listbox.svelte.d.ts +0 -399
  69. package/dist/bits/listbox/listbox.svelte.js +0 -973
  70. package/dist/bits/listbox/types.d.ts +0 -177
  71. package/dist/bits/listbox/types.js +0 -1
  72. package/dist/bits/select/components/select-arrow.svelte +0 -21
  73. package/dist/bits/select/components/select-arrow.svelte.d.ts +0 -18
  74. package/dist/bits/select/components/select-content-floating.svelte +0 -73
  75. package/dist/bits/select/components/select-content-floating.svelte.d.ts +0 -33
  76. package/dist/bits/select/components/select-content-impl.svelte +0 -90
  77. package/dist/bits/select/components/select-content-impl.svelte.d.ts +0 -90
  78. package/dist/bits/select/components/select-content-item-aligned.svelte +0 -37
  79. package/dist/bits/select/components/select-content-item-aligned.svelte.d.ts +0 -29
  80. package/dist/bits/select/components/select-icon.svelte +0 -36
  81. package/dist/bits/select/components/select-icon.svelte.d.ts +0 -19
  82. package/dist/bits/select/components/select-item-text.svelte +0 -39
  83. package/dist/bits/select/components/select-item-text.svelte.d.ts +0 -19
  84. package/dist/bits/select/components/select-native.svelte +0 -36
  85. package/dist/bits/select/components/select-native.svelte.d.ts +0 -30
  86. package/dist/bits/select/components/select-provider.svelte +0 -10
  87. package/dist/bits/select/components/select-provider.svelte.d.ts +0 -23
  88. package/dist/bits/select/components/select-scroll-down-button-mounted.svelte +0 -33
  89. package/dist/bits/select/components/select-scroll-down-button-mounted.svelte.d.ts +0 -29
  90. package/dist/bits/select/components/select-scroll-up-button-mounted.svelte +0 -33
  91. package/dist/bits/select/components/select-scroll-up-button-mounted.svelte.d.ts +0 -29
  92. package/dist/bits/select/components/select-separator.svelte +0 -32
  93. package/dist/bits/select/components/select-separator.svelte.d.ts +0 -19
  94. package/dist/bits/select/components/select-value.svelte +0 -35
  95. package/dist/bits/select/components/select-value.svelte.d.ts +0 -19
  96. package/dist/internal/use-typeahead.svelte.d.ts +0 -34
  97. package/dist/internal/use-typeahead.svelte.js +0 -64
@@ -1,973 +0,0 @@
1
- import { Previous } from "runed";
2
- import { untrack } from "svelte";
3
- import { afterTick, srOnlyStyles, styleToString, useRefById } from "svelte-toolbelt";
4
- import { backward, forward, next, prev } from "../../internal/arrays.js";
5
- import { getAriaExpanded, getAriaHidden, getDataDisabled, getDataOpenClosed, getDisabled, getRequired, } from "../../internal/attrs.js";
6
- import { createContext } from "../../internal/create-context.js";
7
- import { kbd } from "../../internal/kbd.js";
8
- import { noop } from "../../internal/noop.js";
9
- import { addEventListener } from "../../internal/events.js";
10
- import { useTypeahead } from "../../internal/use-typeahead.svelte.js";
11
- // prettier-ignore
12
- export const INTERACTION_KEYS = [kbd.ARROW_LEFT, kbd.ESCAPE, kbd.ARROW_RIGHT, kbd.SHIFT, kbd.CAPS_LOCK, kbd.CONTROL, kbd.ALT, kbd.META, kbd.ENTER, kbd.F1, kbd.F2, kbd.F3, kbd.F4, kbd.F5, kbd.F6, kbd.F7, kbd.F8, kbd.F9, kbd.F10, kbd.F11, kbd.F12];
13
- export const FIRST_KEYS = [kbd.ARROW_DOWN, kbd.PAGE_UP, kbd.HOME];
14
- export const LAST_KEYS = [kbd.ARROW_UP, kbd.PAGE_DOWN, kbd.END];
15
- export const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
16
- export const SELECTION_KEYS = [kbd.ENTER, kbd.SPACE];
17
- class ListboxBaseRootState {
18
- disabled;
19
- required;
20
- name;
21
- loop;
22
- open;
23
- scrollAlignment;
24
- touchedInput = $state(false);
25
- inputValue = $state("");
26
- inputNode = $state(null);
27
- contentNode = $state(null);
28
- triggerNode = $state(null);
29
- highlightedNode = $state(null);
30
- highlightedValue = $derived.by(() => {
31
- if (!this.highlightedNode)
32
- return null;
33
- return this.highlightedNode.getAttribute("data-value");
34
- });
35
- highlightedId = $derived.by(() => {
36
- if (!this.highlightedNode)
37
- return undefined;
38
- return this.highlightedNode.id;
39
- });
40
- highlightedLabel = $derived.by(() => {
41
- if (!this.highlightedNode)
42
- return null;
43
- return this.highlightedNode.getAttribute("data-label");
44
- });
45
- isUsingKeyboard = $state(false);
46
- isCombobox = $state(false);
47
- bitsAttrs;
48
- constructor(props) {
49
- this.disabled = props.disabled;
50
- this.required = props.required;
51
- this.name = props.name;
52
- this.loop = props.loop;
53
- this.open = props.open;
54
- this.scrollAlignment = props.scrollAlignment;
55
- this.isCombobox = props.isCombobox;
56
- this.bitsAttrs = getListboxBitsAttrs(this);
57
- $effect.pre(() => {
58
- if (!this.open.current) {
59
- this.setHighlightedNode(null);
60
- }
61
- });
62
- }
63
- setHighlightedNode = (node) => {
64
- this.highlightedNode = node;
65
- if (node) {
66
- if (this.isUsingKeyboard) {
67
- node.scrollIntoView({ block: "nearest" });
68
- }
69
- }
70
- };
71
- getCandidateNodes = () => {
72
- const node = this.contentNode;
73
- if (!node)
74
- return [];
75
- const nodes = Array.from(node.querySelectorAll(`[${this.bitsAttrs.item}]:not([data-disabled])`));
76
- return nodes;
77
- };
78
- setHighlightedToFirstCandidate = () => {
79
- this.setHighlightedNode(null);
80
- const candidateNodes = this.getCandidateNodes();
81
- if (!candidateNodes.length)
82
- return;
83
- this.setHighlightedNode(candidateNodes[0]);
84
- };
85
- getNodeByValue = (value) => {
86
- const candidateNodes = this.getCandidateNodes();
87
- return candidateNodes.find((node) => node.dataset.value === value) ?? null;
88
- };
89
- setOpen = (open) => {
90
- this.open.current = open;
91
- };
92
- toggleOpen = () => {
93
- this.open.current = !this.open.current;
94
- };
95
- openMenu = () => {
96
- this.setOpen(true);
97
- };
98
- closeMenu = () => {
99
- this.setHighlightedNode(null);
100
- this.setOpen(false);
101
- };
102
- toggleMenu = () => {
103
- this.toggleOpen();
104
- };
105
- }
106
- class ListboxSingleRootState extends ListboxBaseRootState {
107
- value;
108
- isMulti = false;
109
- hasValue = $derived.by(() => this.value.current !== "");
110
- constructor(props) {
111
- super(props);
112
- this.value = props.value;
113
- $effect(() => {
114
- if (!this.open.current && this.highlightedNode) {
115
- this.setHighlightedNode(null);
116
- }
117
- });
118
- $effect(() => {
119
- if (!this.open.current)
120
- return;
121
- afterTick(() => {
122
- this.#setInitialHighlightedNode();
123
- });
124
- });
125
- }
126
- includesItem = (itemValue) => {
127
- return this.value.current === itemValue;
128
- };
129
- toggleItem = (itemValue, itemLabel = itemValue) => {
130
- this.value.current = this.includesItem(itemValue) ? "" : itemValue;
131
- this.inputValue = itemLabel;
132
- };
133
- #setInitialHighlightedNode = () => {
134
- if (this.highlightedNode)
135
- return;
136
- if (this.value.current !== "") {
137
- const node = this.getNodeByValue(this.value.current);
138
- if (node) {
139
- this.setHighlightedNode(node);
140
- return;
141
- }
142
- }
143
- // if no value is set, we want to highlight the first item
144
- const firstCandidate = this.getCandidateNodes()[0];
145
- if (!firstCandidate)
146
- return;
147
- this.setHighlightedNode(firstCandidate);
148
- };
149
- }
150
- class ListboxMultipleRootState extends ListboxBaseRootState {
151
- value;
152
- isMulti = true;
153
- hasValue = $derived.by(() => this.value.current.length > 0);
154
- constructor(props) {
155
- super(props);
156
- this.value = props.value;
157
- $effect(() => {
158
- if (!this.open.current)
159
- return;
160
- afterTick(() => {
161
- if (!this.highlightedNode) {
162
- this.#setInitialHighlightedNode();
163
- }
164
- });
165
- });
166
- }
167
- includesItem = (itemValue) => {
168
- return this.value.current.includes(itemValue);
169
- };
170
- toggleItem = (itemValue, itemLabel = itemValue) => {
171
- if (this.includesItem(itemValue)) {
172
- this.value.current = this.value.current.filter((v) => v !== itemValue);
173
- }
174
- else {
175
- this.value.current = [...this.value.current, itemValue];
176
- }
177
- this.inputValue = itemLabel;
178
- };
179
- #setInitialHighlightedNode = () => {
180
- if (this.highlightedNode)
181
- return;
182
- if (this.value.current.length && this.value.current[0] !== "") {
183
- const node = this.getNodeByValue(this.value.current[0]);
184
- if (node) {
185
- this.setHighlightedNode(node);
186
- return;
187
- }
188
- }
189
- // if no value is set, we want to highlight the first item
190
- const firstCandidate = this.getCandidateNodes()[0];
191
- if (!firstCandidate)
192
- return;
193
- this.setHighlightedNode(firstCandidate);
194
- };
195
- }
196
- class ListboxInputState {
197
- #id;
198
- #ref;
199
- root;
200
- constructor(props, root) {
201
- this.root = root;
202
- this.#id = props.id;
203
- this.#ref = props.ref;
204
- useRefById({
205
- id: this.#id,
206
- ref: this.#ref,
207
- onRefChange: (node) => {
208
- this.root.inputNode = node;
209
- },
210
- });
211
- }
212
- #onkeydown = async (e) => {
213
- this.root.isUsingKeyboard = true;
214
- if (e.key === kbd.ESCAPE)
215
- return;
216
- const open = this.root.open.current;
217
- const inputValue = this.root.inputValue;
218
- // prevent arrow up/down from moving the position of the cursor in the input
219
- if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN)
220
- e.preventDefault();
221
- if (!open) {
222
- if (INTERACTION_KEYS.includes(e.key))
223
- return;
224
- if (e.key === kbd.TAB)
225
- return;
226
- if (e.key === kbd.BACKSPACE && inputValue === "")
227
- return;
228
- this.root.openMenu();
229
- // we need to wait for a tick after the menu opens to ensure the highlighted nodes are
230
- // set correctly.
231
- afterTick(() => {
232
- if (this.root.hasValue)
233
- return;
234
- const candidateNodes = this.root.getCandidateNodes();
235
- if (!candidateNodes.length)
236
- return;
237
- if (e.key === kbd.ARROW_DOWN) {
238
- const firstCandidate = candidateNodes[0];
239
- this.root.setHighlightedNode(firstCandidate);
240
- }
241
- else if (e.key === kbd.ARROW_UP) {
242
- const lastCandidate = candidateNodes[candidateNodes.length - 1];
243
- this.root.setHighlightedNode(lastCandidate);
244
- }
245
- });
246
- return;
247
- }
248
- if (e.key === kbd.TAB) {
249
- this.root.closeMenu();
250
- return;
251
- }
252
- if (e.key === kbd.ENTER && !e.isComposing) {
253
- e.preventDefault();
254
- const highlightedValue = this.root.highlightedValue;
255
- if (highlightedValue) {
256
- this.root.toggleItem(highlightedValue, this.root.highlightedLabel ?? undefined);
257
- }
258
- if (!this.root.isMulti) {
259
- this.root.closeMenu();
260
- }
261
- }
262
- if (e.key === kbd.ARROW_UP && e.altKey) {
263
- this.root.closeMenu();
264
- }
265
- if (FIRST_LAST_KEYS.includes(e.key)) {
266
- e.preventDefault();
267
- const candidateNodes = this.root.getCandidateNodes();
268
- const currHighlightedNode = this.root.highlightedNode;
269
- const currIndex = currHighlightedNode
270
- ? candidateNodes.indexOf(currHighlightedNode)
271
- : -1;
272
- const loop = this.root.loop.current;
273
- let nextItem;
274
- if (e.key === kbd.ARROW_DOWN) {
275
- nextItem = next(candidateNodes, currIndex, loop);
276
- }
277
- else if (e.key === kbd.ARROW_UP) {
278
- nextItem = prev(candidateNodes, currIndex, loop);
279
- }
280
- else if (e.key === kbd.PAGE_DOWN) {
281
- nextItem = forward(candidateNodes, currIndex, 10, loop);
282
- }
283
- else if (e.key === kbd.PAGE_UP) {
284
- nextItem = backward(candidateNodes, currIndex, 10, loop);
285
- }
286
- else if (e.key === kbd.HOME) {
287
- nextItem = candidateNodes[0];
288
- }
289
- else if (e.key === kbd.END) {
290
- nextItem = candidateNodes[candidateNodes.length - 1];
291
- }
292
- if (!nextItem)
293
- return;
294
- this.root.setHighlightedNode(nextItem);
295
- return;
296
- }
297
- if (INTERACTION_KEYS.includes(e.key))
298
- return;
299
- if (!this.root.highlightedNode) {
300
- this.root.setHighlightedToFirstCandidate();
301
- }
302
- // this.root.setHighlightedToFirstCandidate();
303
- };
304
- #oninput = (e) => {
305
- this.root.inputValue = e.currentTarget.value;
306
- this.root.setHighlightedToFirstCandidate();
307
- };
308
- props = $derived.by(() => ({
309
- id: this.#id.current,
310
- role: "combobox",
311
- disabled: this.root.disabled.current ? true : undefined,
312
- "aria-activedescendant": this.root.highlightedId,
313
- "aria-autocomplete": "list",
314
- "aria-expanded": getAriaExpanded(this.root.open.current),
315
- "data-state": getDataOpenClosed(this.root.open.current),
316
- "data-disabled": getDataDisabled(this.root.disabled.current),
317
- onkeydown: this.#onkeydown,
318
- oninput: this.#oninput,
319
- [this.root.bitsAttrs.input]: "",
320
- }));
321
- }
322
- class ListboxComboTriggerState {
323
- #id;
324
- #ref;
325
- root;
326
- constructor(props, root) {
327
- this.root = root;
328
- this.#id = props.id;
329
- this.#ref = props.ref;
330
- useRefById({
331
- id: this.#id,
332
- ref: this.#ref,
333
- });
334
- }
335
- #onkeydown = (e) => {
336
- if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
337
- e.preventDefault();
338
- if (document.activeElement !== this.root.inputNode) {
339
- this.root.inputNode?.focus();
340
- }
341
- this.root.toggleMenu();
342
- }
343
- };
344
- /**
345
- * `pointerdown` fires before the `focus` event, so we can prevent the default
346
- * behavior of focusing the button and keep focus on the input.
347
- */
348
- #onpointerdown = (e) => {
349
- if (this.root.disabled.current)
350
- return;
351
- e.preventDefault();
352
- if (document.activeElement !== this.root.inputNode) {
353
- this.root.inputNode?.focus();
354
- }
355
- this.root.toggleMenu();
356
- };
357
- props = $derived.by(() => ({
358
- id: this.#id.current,
359
- disabled: this.root.disabled.current ? true : undefined,
360
- "aria-haspopup": "listbox",
361
- "data-state": getDataOpenClosed(this.root.open.current),
362
- "data-disabled": getDataDisabled(this.root.disabled.current),
363
- [this.root.bitsAttrs.trigger]: "",
364
- onpointerdown: this.#onpointerdown,
365
- onkeydown: this.#onkeydown,
366
- }));
367
- }
368
- class ListboxTriggerState {
369
- #id;
370
- #ref;
371
- root;
372
- #typeahead;
373
- constructor(props, root) {
374
- this.root = root;
375
- this.#id = props.id;
376
- this.#ref = props.ref;
377
- useRefById({
378
- id: this.#id,
379
- ref: this.#ref,
380
- onRefChange: (node) => {
381
- this.root.triggerNode = node;
382
- },
383
- });
384
- this.#typeahead = useTypeahead({
385
- getCurrentItem: () => this.root.highlightedNode,
386
- onMatch: (node) => {
387
- this.root.setHighlightedNode(node);
388
- },
389
- });
390
- }
391
- #onkeydown = (e) => {
392
- this.root.isUsingKeyboard = true;
393
- if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN)
394
- e.preventDefault();
395
- if (!this.root.open.current) {
396
- if (e.key === kbd.ENTER)
397
- return;
398
- if (e.key === kbd.SPACE || e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_UP) {
399
- e.preventDefault();
400
- this.root.openMenu();
401
- }
402
- // we need to wait for a tick after the menu opens to ensure
403
- // the highlighted nodes are set correctly
404
- afterTick(() => {
405
- if (this.root.hasValue)
406
- return;
407
- const candidateNodes = this.root.getCandidateNodes();
408
- if (!candidateNodes.length)
409
- return;
410
- if (e.key === kbd.ARROW_DOWN) {
411
- const firstCandidate = candidateNodes[0];
412
- this.root.setHighlightedNode(firstCandidate);
413
- }
414
- else if (e.key === kbd.ARROW_UP) {
415
- const lastCandidate = candidateNodes[candidateNodes.length - 1];
416
- this.root.setHighlightedNode(lastCandidate);
417
- }
418
- });
419
- return;
420
- }
421
- if (e.key === kbd.TAB) {
422
- this.root.closeMenu();
423
- return;
424
- }
425
- if ((e.key === kbd.ENTER || e.key === kbd.SPACE) && !e.isComposing) {
426
- e.preventDefault();
427
- const highlightedValue = this.root.highlightedValue;
428
- if (highlightedValue) {
429
- this.root.toggleItem(highlightedValue, this.root.highlightedLabel ?? undefined);
430
- }
431
- if (!this.root.isMulti) {
432
- this.root.closeMenu();
433
- }
434
- }
435
- if (e.key === kbd.ARROW_UP && e.altKey) {
436
- this.root.closeMenu();
437
- }
438
- if (FIRST_LAST_KEYS.includes(e.key)) {
439
- e.preventDefault();
440
- const candidateNodes = this.root.getCandidateNodes();
441
- const currHighlightedNode = this.root.highlightedNode;
442
- const currIndex = currHighlightedNode
443
- ? candidateNodes.indexOf(currHighlightedNode)
444
- : -1;
445
- const loop = this.root.loop.current;
446
- let nextItem;
447
- if (e.key === kbd.ARROW_DOWN) {
448
- nextItem = next(candidateNodes, currIndex, loop);
449
- }
450
- else if (e.key === kbd.ARROW_UP) {
451
- nextItem = prev(candidateNodes, currIndex, loop);
452
- }
453
- else if (e.key === kbd.PAGE_DOWN) {
454
- nextItem = forward(candidateNodes, currIndex, 10, loop);
455
- }
456
- else if (e.key === kbd.PAGE_UP) {
457
- nextItem = backward(candidateNodes, currIndex, 10, loop);
458
- }
459
- else if (e.key === kbd.HOME) {
460
- nextItem = candidateNodes[0];
461
- }
462
- else if (e.key === kbd.END) {
463
- nextItem = candidateNodes[candidateNodes.length - 1];
464
- }
465
- if (!nextItem)
466
- return;
467
- this.root.setHighlightedNode(nextItem);
468
- return;
469
- }
470
- const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
471
- const isCharacterKey = e.key.length === 1;
472
- // prevent space from being considered with typeahead
473
- if (e.code === "Space")
474
- return;
475
- const candidateNodes = this.root.getCandidateNodes();
476
- if (e.key === kbd.TAB)
477
- return;
478
- if (!isModifierKey && isCharacterKey) {
479
- this.#typeahead.handleTypeaheadSearch(e.key, candidateNodes);
480
- return;
481
- }
482
- if (!this.root.highlightedNode) {
483
- this.root.setHighlightedToFirstCandidate();
484
- }
485
- };
486
- /**
487
- * `pointerdown` fires before the `focus` event, so we can prevent the default
488
- * behavior of focusing the button and keep focus on the input.
489
- */
490
- #onpointerdown = () => {
491
- if (this.root.disabled.current)
492
- return;
493
- this.root.toggleMenu();
494
- };
495
- props = $derived.by(() => ({
496
- id: this.#id.current,
497
- disabled: this.root.disabled.current ? true : undefined,
498
- "aria-haspopup": "listbox",
499
- "data-state": getDataOpenClosed(this.root.open.current),
500
- "data-disabled": getDataDisabled(this.root.disabled.current),
501
- [this.root.bitsAttrs.trigger]: "",
502
- onpointerdown: this.#onpointerdown,
503
- onkeydown: this.#onkeydown,
504
- // onclick: this.#onclick,
505
- }));
506
- }
507
- class ListboxContentState {
508
- #id;
509
- #ref;
510
- viewportNode = $state(null);
511
- root;
512
- isPositioned = $state(false);
513
- constructor(props, root) {
514
- this.root = root;
515
- this.#id = props.id;
516
- this.#ref = props.ref;
517
- useRefById({
518
- id: this.#id,
519
- ref: this.#ref,
520
- onRefChange: (node) => {
521
- this.root.contentNode = node;
522
- },
523
- deps: () => this.root.open.current,
524
- });
525
- $effect(() => {
526
- return () => {
527
- this.root.contentNode = null;
528
- };
529
- });
530
- $effect(() => {
531
- if (this.root.open.current === false) {
532
- this.isPositioned = false;
533
- }
534
- });
535
- }
536
- #onpointermove = () => {
537
- this.root.isUsingKeyboard = false;
538
- };
539
- #styles = $derived.by(() => {
540
- if (this.root.isCombobox) {
541
- return {
542
- "--bits-combobox-content-transform-origin": "var(--bits-floating-transform-origin)",
543
- "--bits-combobox-content-available-width": "var(--bits-floating-available-width)",
544
- "--bits-combobox-content-available-height": "var(--bits-floating-available-height)",
545
- "--bits-combobox-anchor-width": "var(--bits-floating-anchor-width)",
546
- "--bits-combobox-anchor-height": "var(--bits-floating-anchor-height)",
547
- };
548
- }
549
- else {
550
- return {
551
- "--bits-listbox-content-transform-origin": "var(--bits-floating-transform-origin)",
552
- "--bits-listbox-content-available-width": "var(--bits-floating-available-width)",
553
- "--bits-listbox-content-available-height": "var(--bits-floating-available-height)",
554
- "--bits-listbox-anchor-width": "var(--bits-floating-anchor-width)",
555
- "--bits-listbox-anchor-height": "var(--bits-floating-anchor-height)",
556
- };
557
- }
558
- });
559
- handleInteractOutside = (e) => {
560
- if (e.target === this.root.triggerNode || e.target === this.root.inputNode) {
561
- e.preventDefault();
562
- }
563
- };
564
- snippetProps = $derived.by(() => ({ open: this.root.open.current }));
565
- props = $derived.by(() => ({
566
- id: this.#id.current,
567
- role: "listbox",
568
- "data-state": getDataOpenClosed(this.root.open.current),
569
- [this.root.bitsAttrs.content]: "",
570
- style: {
571
- display: "flex",
572
- flexDirection: "column",
573
- outline: "none",
574
- boxSizing: "border-box",
575
- ...this.#styles,
576
- },
577
- onpointermove: this.#onpointermove,
578
- }));
579
- }
580
- class ListboxItemState {
581
- #id;
582
- #ref;
583
- root;
584
- value;
585
- label;
586
- onHighlight;
587
- onUnhighlight;
588
- disabled;
589
- isSelected = $derived.by(() => this.root.includesItem(this.value.current));
590
- isHighlighted = $derived.by(() => this.root.highlightedValue === this.value.current);
591
- prevHighlighted = new Previous(() => this.isHighlighted);
592
- constructor(props, root) {
593
- this.root = root;
594
- this.value = props.value;
595
- this.disabled = props.disabled;
596
- this.label = props.label;
597
- this.onHighlight = props.onHighlight;
598
- this.onUnhighlight = props.onUnhighlight;
599
- this.#id = props.id;
600
- this.#ref = props.ref;
601
- $effect(() => {
602
- if (this.isHighlighted) {
603
- this.onHighlight.current();
604
- }
605
- else if (this.prevHighlighted.current) {
606
- this.onUnhighlight.current();
607
- }
608
- });
609
- useRefById({
610
- id: this.#id,
611
- ref: this.#ref,
612
- });
613
- }
614
- snippetProps = $derived.by(() => ({
615
- selected: this.isSelected,
616
- highlighted: this.isHighlighted,
617
- }));
618
- #onpointerdown = (e) => {
619
- // prevent focus from leaving the combobox
620
- e.preventDefault();
621
- };
622
- /**
623
- * Using `pointerup` instead of `click` allows power users to pointerdown
624
- * the trigger, then release pointerup on an item to select it vs having to do
625
- * multiple clicks.
626
- */
627
- #onpointerup = (e) => {
628
- // prevent any default behavior
629
- e.preventDefault();
630
- if (this.disabled.current)
631
- return;
632
- const isCurrentSelectedValue = this.value.current === this.root.value.current;
633
- this.root.toggleItem(this.value.current, this.label.current);
634
- if (!this.root.isMulti && !isCurrentSelectedValue) {
635
- this.root.closeMenu();
636
- }
637
- };
638
- #onpointermove = (_) => {
639
- if (this.root.highlightedNode !== this.#ref.current) {
640
- this.root.setHighlightedNode(this.#ref.current);
641
- }
642
- };
643
- props = $derived.by(() => ({
644
- id: this.#id.current,
645
- "aria-selected": this.root.includesItem(this.value.current) ? "true" : undefined,
646
- "data-value": this.value.current,
647
- "data-disabled": getDataDisabled(this.disabled.current),
648
- "data-highlighted": this.root.highlightedValue === this.value.current ? "" : undefined,
649
- "data-selected": this.root.includesItem(this.value.current) ? "" : undefined,
650
- "data-label": this.label.current,
651
- [this.root.bitsAttrs.item]: "",
652
- onpointermove: this.#onpointermove,
653
- onpointerdown: this.#onpointerdown,
654
- onpointerup: this.#onpointerup,
655
- }));
656
- }
657
- class ListboxGroupState {
658
- #id;
659
- #ref;
660
- root;
661
- labelNode = $state(null);
662
- constructor(props, root) {
663
- this.#id = props.id;
664
- this.#ref = props.ref;
665
- this.root = root;
666
- useRefById({
667
- id: this.#id,
668
- ref: this.#ref,
669
- });
670
- }
671
- props = $derived.by(() => ({
672
- id: this.#id.current,
673
- role: "group",
674
- [this.root.bitsAttrs.group]: "",
675
- "aria-labelledby": this.labelNode?.id ?? undefined,
676
- }));
677
- }
678
- class ListboxGroupHeadingState {
679
- #id;
680
- #ref;
681
- group;
682
- constructor(props, group) {
683
- this.#id = props.id;
684
- this.#ref = props.ref;
685
- this.group = group;
686
- useRefById({
687
- id: this.#id,
688
- ref: this.#ref,
689
- onRefChange: (node) => {
690
- group.labelNode = node;
691
- },
692
- });
693
- }
694
- props = $derived.by(() => ({
695
- id: this.#id.current,
696
- [this.group.root.bitsAttrs["group-label"]]: "",
697
- }));
698
- }
699
- class ListboxHiddenInputState {
700
- #value;
701
- root;
702
- shouldRender = $derived.by(() => this.root.name.current !== "");
703
- constructor(props, root) {
704
- this.root = root;
705
- this.#value = props.value;
706
- }
707
- props = $derived.by(() => ({
708
- disabled: getDisabled(this.root.disabled.current),
709
- required: getRequired(this.root.required.current),
710
- name: this.root.name.current,
711
- value: this.#value.current,
712
- "aria-hidden": getAriaHidden(true),
713
- style: styleToString(srOnlyStyles),
714
- }));
715
- }
716
- class ListboxViewportState {
717
- #id;
718
- #ref;
719
- root;
720
- content;
721
- constructor(props, content) {
722
- this.#id = props.id;
723
- this.#ref = props.ref;
724
- this.content = content;
725
- this.root = content.root;
726
- useRefById({
727
- id: this.#id,
728
- ref: this.#ref,
729
- onRefChange: (node) => {
730
- this.content.viewportNode = node;
731
- },
732
- deps: () => this.root.open.current,
733
- });
734
- }
735
- props = $derived.by(() => ({
736
- id: this.#id.current,
737
- role: "presentation",
738
- [this.root.bitsAttrs.viewport]: "",
739
- style: {
740
- // we use position: 'relative' here on the `viewport` so that when we call
741
- // `selectedItem.offsetTop` in calculations, the offset is relative to the viewport
742
- // (independent of the scrollUpButton).
743
- position: "relative",
744
- flex: 1,
745
- overflow: "auto",
746
- },
747
- }));
748
- }
749
- class ListboxScrollButtonImplState {
750
- id;
751
- ref;
752
- content;
753
- root;
754
- autoScrollTimer = $state(null);
755
- onAutoScroll = noop;
756
- mounted;
757
- constructor(props, content) {
758
- this.ref = props.ref;
759
- this.id = props.id;
760
- this.mounted = props.mounted;
761
- this.content = content;
762
- this.root = content.root;
763
- useRefById({
764
- id: this.id,
765
- ref: this.ref,
766
- deps: () => this.mounted.current,
767
- });
768
- $effect(() => {
769
- if (!this.mounted.current)
770
- return;
771
- const activeItem = untrack(() => this.root.highlightedNode);
772
- activeItem?.scrollIntoView({ block: "nearest" });
773
- });
774
- }
775
- clearAutoScrollTimer = () => {
776
- if (this.autoScrollTimer === null)
777
- return;
778
- window.clearInterval(this.autoScrollTimer);
779
- this.autoScrollTimer = null;
780
- };
781
- #onpointerdown = () => {
782
- if (this.autoScrollTimer !== null)
783
- return;
784
- this.autoScrollTimer = window.setInterval(() => {
785
- this.onAutoScroll();
786
- }, 50);
787
- };
788
- #onpointermove = () => {
789
- if (this.autoScrollTimer !== null)
790
- return;
791
- this.autoScrollTimer = window.setInterval(() => {
792
- this.onAutoScroll();
793
- }, 50);
794
- };
795
- #onpointerleave = () => {
796
- this.clearAutoScrollTimer();
797
- };
798
- props = $derived.by(() => ({
799
- id: this.id.current,
800
- "aria-hidden": getAriaHidden(true),
801
- style: {
802
- flexShrink: 0,
803
- },
804
- onpointerdown: this.#onpointerdown,
805
- onpointermove: this.#onpointermove,
806
- onpointerleave: this.#onpointerleave,
807
- }));
808
- }
809
- class ListboxScrollDownButtonState {
810
- state;
811
- content;
812
- root;
813
- canScrollDown = $state(false);
814
- constructor(state) {
815
- this.state = state;
816
- this.content = state.content;
817
- this.root = state.root;
818
- this.state.onAutoScroll = this.handleAutoScroll;
819
- $effect(() => {
820
- const viewport = this.content.viewportNode;
821
- const isPositioned = this.content.isPositioned;
822
- if (!viewport || !isPositioned)
823
- return;
824
- let cleanup = noop;
825
- untrack(() => {
826
- const handleScroll = () => {
827
- afterTick(() => {
828
- const maxScroll = viewport.scrollHeight - viewport.clientHeight;
829
- const paddingTop = Number.parseInt(getComputedStyle(viewport).paddingTop, 10);
830
- this.canScrollDown = Math.ceil(viewport.scrollTop) < maxScroll - paddingTop;
831
- });
832
- };
833
- handleScroll();
834
- cleanup = addEventListener(viewport, "scroll", handleScroll);
835
- });
836
- return cleanup;
837
- });
838
- $effect(() => {
839
- if (this.state.mounted.current)
840
- return;
841
- this.state.clearAutoScrollTimer();
842
- });
843
- }
844
- handleAutoScroll = () => {
845
- afterTick(() => {
846
- const viewport = this.content.viewportNode;
847
- const selectedItem = this.root.highlightedNode;
848
- if (!viewport || !selectedItem)
849
- return;
850
- viewport.scrollTop = viewport.scrollTop + selectedItem.offsetHeight;
851
- });
852
- };
853
- props = $derived.by(() => ({ ...this.state.props, [this.root.bitsAttrs["scroll-down-button"]]: "" }));
854
- }
855
- class ListboxScrollUpButtonState {
856
- state;
857
- content;
858
- root;
859
- canScrollUp = $state(false);
860
- constructor(state) {
861
- this.state = state;
862
- this.content = state.content;
863
- this.root = state.root;
864
- this.state.onAutoScroll = this.handleAutoScroll;
865
- $effect(() => {
866
- const viewport = this.content.viewportNode;
867
- const isPositioned = this.content.isPositioned;
868
- if (!viewport || !isPositioned)
869
- return;
870
- let cleanup = noop;
871
- untrack(() => {
872
- const handleScroll = () => {
873
- const paddingTop = Number.parseInt(getComputedStyle(viewport).paddingTop, 10);
874
- this.canScrollUp = viewport.scrollTop - paddingTop > 0;
875
- };
876
- handleScroll();
877
- cleanup = addEventListener(viewport, "scroll", handleScroll);
878
- });
879
- return cleanup;
880
- });
881
- $effect(() => {
882
- if (this.state.mounted.current)
883
- return;
884
- this.state.clearAutoScrollTimer();
885
- });
886
- }
887
- handleAutoScroll = () => {
888
- afterTick(() => {
889
- const viewport = this.content.viewportNode;
890
- const selectedItem = this.root.highlightedNode;
891
- if (!viewport || !selectedItem)
892
- return;
893
- viewport.scrollTop = viewport.scrollTop - selectedItem.offsetHeight;
894
- });
895
- };
896
- props = $derived.by(() => ({ ...this.state.props, [this.root.bitsAttrs["scroll-up-button"]]: "" }));
897
- }
898
- const [setListboxRootContext, getListboxRootContext] = createContext([
899
- "Listbox.Root",
900
- "Combobox.Root",
901
- ]);
902
- const [setListboxGroupContext, getListboxGroupContext] = createContext([
903
- "Listbox.Group",
904
- "Combobox.Group",
905
- ]);
906
- const [setListboxContentContext, getListboxContentContext] = createContext([
907
- "Listbox.Content",
908
- "Combobox.Content",
909
- ]);
910
- export function useListboxRoot(props) {
911
- const { type, ...rest } = props;
912
- const rootState = type === "single"
913
- ? new ListboxSingleRootState(rest)
914
- : new ListboxMultipleRootState(rest);
915
- return setListboxRootContext(rootState);
916
- }
917
- export function useListboxInput(props) {
918
- return new ListboxInputState(props, getListboxRootContext());
919
- }
920
- export function useListboxContent(props) {
921
- return setListboxContentContext(new ListboxContentState(props, getListboxRootContext()));
922
- }
923
- export function useListboxTrigger(props) {
924
- return new ListboxTriggerState(props, getListboxRootContext());
925
- }
926
- export function useListboxComboTrigger(props) {
927
- return new ListboxComboTriggerState(props, getListboxRootContext());
928
- }
929
- export function useListboxItem(props) {
930
- return new ListboxItemState(props, getListboxRootContext());
931
- }
932
- export function useListboxViewport(props) {
933
- return new ListboxViewportState(props, getListboxContentContext());
934
- }
935
- export function useListboxScrollUpButton(props) {
936
- return new ListboxScrollUpButtonState(new ListboxScrollButtonImplState(props, getListboxContentContext()));
937
- }
938
- export function useListboxScrollDownButton(props) {
939
- return new ListboxScrollDownButtonState(new ListboxScrollButtonImplState(props, getListboxContentContext()));
940
- }
941
- export function useListboxGroup(props) {
942
- return setListboxGroupContext(new ListboxGroupState(props, getListboxRootContext()));
943
- }
944
- export function useListboxGroupHeading(props) {
945
- return new ListboxGroupHeadingState(props, getListboxGroupContext());
946
- }
947
- export function useListboxHiddenInput(props) {
948
- return new ListboxHiddenInputState(props, getListboxRootContext());
949
- }
950
- ////////////////////////////////////
951
- // Helpers
952
- ////////////////////////////////////
953
- const listboxParts = [
954
- "trigger",
955
- "content",
956
- "item",
957
- "viewport",
958
- "scroll-up-button",
959
- "scroll-down-button",
960
- "group",
961
- "group-label",
962
- "separator",
963
- "arrow",
964
- "input",
965
- ];
966
- export function getListboxBitsAttrs(root) {
967
- const isCombobox = root.isCombobox;
968
- const attrObj = {};
969
- for (const part of listboxParts) {
970
- attrObj[part] = isCombobox ? `data-combobox-${part}` : `data-listbox-${part}`;
971
- }
972
- return attrObj;
973
- }