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.
- package/dist/bits/combobox/components/combobox-input.svelte +2 -2
- package/dist/bits/combobox/components/combobox-trigger.svelte +2 -2
- package/dist/bits/combobox/components/combobox.svelte +9 -7
- package/dist/bits/combobox/exports.d.ts +8 -8
- package/dist/bits/combobox/exports.js +8 -8
- package/dist/bits/combobox/types.d.ts +1 -1
- package/dist/bits/dialog/components/dialog-content.svelte +1 -1
- package/dist/bits/index.d.ts +0 -1
- package/dist/bits/index.js +0 -1
- package/dist/bits/menu/menu.svelte.js +2 -2
- package/dist/bits/menubar/menubar.svelte.js +1 -1
- package/dist/bits/{listbox/components/listbox-content-static.svelte → select/components/select-content-static.svelte} +6 -6
- package/dist/bits/{listbox/components/listbox-group-heading.svelte.d.ts → select/components/select-content-static.svelte.d.ts} +4 -4
- package/dist/bits/select/components/select-content.svelte +56 -35
- package/dist/bits/select/components/select-group-heading.svelte +3 -3
- package/dist/bits/select/components/select-group.svelte +4 -3
- package/dist/bits/{listbox/components/listbox-hidden-input.svelte → select/components/select-hidden-input.svelte} +4 -4
- package/dist/bits/{listbox/components/listbox-hidden-input.svelte.d.ts → select/components/select-hidden-input.svelte.d.ts} +4 -4
- package/dist/bits/select/components/select-item.svelte +14 -9
- package/dist/bits/select/components/select-scroll-down-button.svelte +20 -9
- package/dist/bits/select/components/select-scroll-down-button.svelte.d.ts +2 -2
- package/dist/bits/select/components/select-scroll-up-button.svelte +21 -10
- package/dist/bits/select/components/select-trigger.svelte +5 -7
- package/dist/bits/select/components/select-viewport.svelte +10 -0
- package/dist/bits/select/components/select.svelte +54 -56
- package/dist/bits/select/exports.d.ts +4 -7
- package/dist/bits/select/exports.js +3 -6
- package/dist/bits/select/select.svelte.d.ts +272 -258
- package/dist/bits/select/select.svelte.js +764 -831
- package/dist/bits/select/types.d.ts +128 -79
- package/dist/bits/slider/components/slider.svelte +2 -2
- package/dist/bits/slider/slider.svelte.d.ts +2 -2
- package/dist/bits/slider/slider.svelte.js +4 -4
- package/dist/bits/slider/types.d.ts +3 -2
- package/dist/bits/utilities/popper-layer/popper-layer.svelte +1 -3
- package/dist/internal/arrays.d.ts +23 -0
- package/dist/internal/arrays.js +35 -0
- package/dist/internal/date-time/calendar-helpers.svelte.js +6 -1
- package/dist/internal/use-body-scroll-lock.svelte.js +53 -42
- package/dist/internal/use-data-typeahead.svelte.d.ts +12 -0
- package/dist/internal/use-data-typeahead.svelte.js +30 -0
- package/dist/internal/use-dom-typeahead.svelte.d.ts +11 -0
- package/dist/internal/use-dom-typeahead.svelte.js +30 -0
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
- package/dist/bits/listbox/components/listbox-content-static.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox-content.svelte +0 -76
- package/dist/bits/listbox/components/listbox-content.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox-group-heading.svelte +0 -32
- package/dist/bits/listbox/components/listbox-group.svelte +0 -32
- package/dist/bits/listbox/components/listbox-group.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox-item.svelte +0 -43
- package/dist/bits/listbox/components/listbox-item.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox-scroll-down-button.svelte +0 -39
- package/dist/bits/listbox/components/listbox-scroll-down-button.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox-scroll-up-button.svelte +0 -39
- package/dist/bits/listbox/components/listbox-scroll-up-button.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox-trigger.svelte +0 -35
- package/dist/bits/listbox/components/listbox-trigger.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox-viewport.svelte +0 -54
- package/dist/bits/listbox/components/listbox-viewport.svelte.d.ts +0 -19
- package/dist/bits/listbox/components/listbox.svelte +0 -81
- package/dist/bits/listbox/components/listbox.svelte.d.ts +0 -18
- package/dist/bits/listbox/exports.d.ts +0 -12
- package/dist/bits/listbox/exports.js +0 -11
- package/dist/bits/listbox/index.d.ts +0 -1
- package/dist/bits/listbox/index.js +0 -1
- package/dist/bits/listbox/listbox.svelte.d.ts +0 -399
- package/dist/bits/listbox/listbox.svelte.js +0 -973
- package/dist/bits/listbox/types.d.ts +0 -177
- package/dist/bits/listbox/types.js +0 -1
- package/dist/bits/select/components/select-arrow.svelte +0 -21
- package/dist/bits/select/components/select-arrow.svelte.d.ts +0 -18
- package/dist/bits/select/components/select-content-floating.svelte +0 -73
- package/dist/bits/select/components/select-content-floating.svelte.d.ts +0 -33
- package/dist/bits/select/components/select-content-impl.svelte +0 -90
- package/dist/bits/select/components/select-content-impl.svelte.d.ts +0 -90
- package/dist/bits/select/components/select-content-item-aligned.svelte +0 -37
- package/dist/bits/select/components/select-content-item-aligned.svelte.d.ts +0 -29
- package/dist/bits/select/components/select-icon.svelte +0 -36
- package/dist/bits/select/components/select-icon.svelte.d.ts +0 -19
- package/dist/bits/select/components/select-item-text.svelte +0 -39
- package/dist/bits/select/components/select-item-text.svelte.d.ts +0 -19
- package/dist/bits/select/components/select-native.svelte +0 -36
- package/dist/bits/select/components/select-native.svelte.d.ts +0 -30
- package/dist/bits/select/components/select-provider.svelte +0 -10
- package/dist/bits/select/components/select-provider.svelte.d.ts +0 -23
- package/dist/bits/select/components/select-scroll-down-button-mounted.svelte +0 -33
- package/dist/bits/select/components/select-scroll-down-button-mounted.svelte.d.ts +0 -29
- package/dist/bits/select/components/select-scroll-up-button-mounted.svelte +0 -33
- package/dist/bits/select/components/select-scroll-up-button-mounted.svelte.d.ts +0 -29
- package/dist/bits/select/components/select-separator.svelte +0 -32
- package/dist/bits/select/components/select-separator.svelte.d.ts +0 -19
- package/dist/bits/select/components/select-value.svelte +0 -35
- package/dist/bits/select/components/select-value.svelte.d.ts +0 -19
- package/dist/internal/use-typeahead.svelte.d.ts +0 -34
- package/dist/internal/use-typeahead.svelte.js +0 -64
|
@@ -1,138 +1,545 @@
|
|
|
1
|
-
|
|
2
|
-
* This logic is adapted from Radix UI Select component.
|
|
3
|
-
* https://github.com/radix-ui/primitives/blob/main/packages/react/select/src/Select.tsx
|
|
4
|
-
* Credit to the Radix UI team for the original implementation.
|
|
5
|
-
*/
|
|
6
|
-
import { afterSleep, afterTick, box, useRefById, } from "svelte-toolbelt";
|
|
7
|
-
import { SvelteMap } from "svelte/reactivity";
|
|
1
|
+
import { Previous } from "runed";
|
|
8
2
|
import { untrack } from "svelte";
|
|
9
|
-
import {
|
|
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";
|
|
10
6
|
import { createContext } from "../../internal/create-context.js";
|
|
11
|
-
import { useFormControl } from "../../internal/use-form-control.svelte.js";
|
|
12
|
-
import { useTypeahead } from "../../internal/use-typeahead.svelte.js";
|
|
13
|
-
import { getAriaDisabled, getAriaExpanded, getAriaHidden, getAriaRequired, getAriaSelected, getDataChecked, getDataDisabled, getDataOpenClosed, } from "../../internal/attrs.js";
|
|
14
7
|
import { kbd } from "../../internal/kbd.js";
|
|
15
|
-
import { clamp } from "../../internal/clamp.js";
|
|
16
8
|
import { noop } from "../../internal/noop.js";
|
|
17
9
|
import { addEventListener } from "../../internal/events.js";
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
import { useDOMTypeahead } from "../../internal/use-dom-typeahead.svelte.js";
|
|
11
|
+
import { useDataTypeahead } from "../../internal/use-data-typeahead.svelte.js";
|
|
12
|
+
// prettier-ignore
|
|
13
|
+
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];
|
|
14
|
+
export const FIRST_KEYS = [kbd.ARROW_DOWN, kbd.PAGE_UP, kbd.HOME];
|
|
15
|
+
export const LAST_KEYS = [kbd.ARROW_UP, kbd.PAGE_DOWN, kbd.END];
|
|
16
|
+
export const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
|
|
17
|
+
export const SELECTION_KEYS = [kbd.ENTER, kbd.SPACE];
|
|
20
18
|
export const CONTENT_MARGIN = 10;
|
|
21
|
-
|
|
22
|
-
const CONTENT_ATTR = "data-select-content";
|
|
23
|
-
const ITEM_ATTR = "data-select-item";
|
|
24
|
-
const VIEWPORT_ATTR = "data-select-viewport";
|
|
25
|
-
const VALUE_ATTR = "data-select-value";
|
|
26
|
-
const ITEM_TEXT_ATTR = "data-select-item-text";
|
|
27
|
-
const CONTENT_WRAPPER_ATTR = "data-select-content-wrapper";
|
|
28
|
-
const SCROLL_UP_BUTTON_ATTR = "data-select-scroll-up-button";
|
|
29
|
-
const SCROLL_DOWN_BUTTON_ATTR = "data-select-scroll-down-button";
|
|
30
|
-
const GROUP_ATTR = "data-select-group";
|
|
31
|
-
const GROUP_LABEL_ATTR = "data-select-group-label";
|
|
32
|
-
const SEPARATOR_ATTR = "data-select-separator";
|
|
33
|
-
const ARROW_ATTR = "data-select-arrow";
|
|
34
|
-
const ICON_ATTR = "data-select-icon";
|
|
35
|
-
export const [setSelectRootContext, getSelectRootContext] = createContext("Select.Root");
|
|
36
|
-
export const [setSelectTriggerContext] = createContext("Select.Trigger");
|
|
37
|
-
export const [setSelectContentContext, getSelectContentContext] = createContext("Select.Content");
|
|
38
|
-
export const [setSelectItemContext, getSelectItemContext] = createContext("Select.Item");
|
|
39
|
-
export const [setSelectContentItemAlignedContext, getSelectContentItemAlignedContext] = createContext("Select.ContentItemAligned");
|
|
40
|
-
const [setSelectGroupContext, getSelectGroupContext] = createContext("Select.Group");
|
|
41
|
-
export class SelectRootState {
|
|
42
|
-
open;
|
|
43
|
-
value;
|
|
44
|
-
dir;
|
|
19
|
+
class SelectBaseRootState {
|
|
45
20
|
disabled;
|
|
46
21
|
required;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
22
|
+
name;
|
|
23
|
+
loop;
|
|
24
|
+
open;
|
|
25
|
+
scrollAlignment;
|
|
26
|
+
items;
|
|
27
|
+
touchedInput = $state(false);
|
|
28
|
+
inputValue = $state("");
|
|
29
|
+
inputNode = $state(null);
|
|
51
30
|
contentNode = $state(null);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
triggerNode = $state(null);
|
|
32
|
+
valueId = $state("");
|
|
33
|
+
highlightedNode = $state(null);
|
|
34
|
+
highlightedValue = $derived.by(() => {
|
|
35
|
+
if (!this.highlightedNode)
|
|
36
|
+
return null;
|
|
37
|
+
return this.highlightedNode.getAttribute("data-value");
|
|
38
|
+
});
|
|
39
|
+
highlightedId = $derived.by(() => {
|
|
40
|
+
if (!this.highlightedNode)
|
|
41
|
+
return undefined;
|
|
42
|
+
return this.highlightedNode.id;
|
|
62
43
|
});
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
highlightedLabel = $derived.by(() => {
|
|
45
|
+
if (!this.highlightedNode)
|
|
46
|
+
return null;
|
|
47
|
+
return this.highlightedNode.getAttribute("data-label");
|
|
48
|
+
});
|
|
49
|
+
isUsingKeyboard = $state(false);
|
|
50
|
+
isCombobox = $state(false);
|
|
51
|
+
bitsAttrs;
|
|
52
|
+
triggerPointerDownPos = $state.raw({ x: 0, y: 0 });
|
|
65
53
|
constructor(props) {
|
|
66
|
-
this.open = props.open;
|
|
67
|
-
this.value = props.value;
|
|
68
|
-
this.dir = props.dir;
|
|
69
54
|
this.disabled = props.disabled;
|
|
70
55
|
this.required = props.required;
|
|
56
|
+
this.name = props.name;
|
|
57
|
+
this.loop = props.loop;
|
|
58
|
+
this.open = props.open;
|
|
59
|
+
this.scrollAlignment = props.scrollAlignment;
|
|
60
|
+
this.isCombobox = props.isCombobox;
|
|
61
|
+
this.items = props.items;
|
|
62
|
+
this.bitsAttrs = getSelectBitsAttrs(this);
|
|
63
|
+
$effect.pre(() => {
|
|
64
|
+
if (!this.open.current) {
|
|
65
|
+
this.setHighlightedNode(null);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
71
68
|
}
|
|
72
|
-
|
|
73
|
-
this.
|
|
74
|
-
|
|
69
|
+
setHighlightedNode = (node) => {
|
|
70
|
+
this.highlightedNode = node;
|
|
71
|
+
if (node) {
|
|
72
|
+
if (this.isUsingKeyboard) {
|
|
73
|
+
node.scrollIntoView({ block: "nearest" });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
75
76
|
};
|
|
76
|
-
|
|
77
|
-
const node = this.
|
|
77
|
+
getCandidateNodes = () => {
|
|
78
|
+
const node = this.contentNode;
|
|
78
79
|
if (!node)
|
|
80
|
+
return [];
|
|
81
|
+
const nodes = Array.from(node.querySelectorAll(`[${this.bitsAttrs.item}]:not([data-disabled])`));
|
|
82
|
+
return nodes;
|
|
83
|
+
};
|
|
84
|
+
setHighlightedToFirstCandidate = () => {
|
|
85
|
+
this.setHighlightedNode(null);
|
|
86
|
+
const candidateNodes = this.getCandidateNodes();
|
|
87
|
+
if (!candidateNodes.length)
|
|
79
88
|
return;
|
|
80
|
-
|
|
81
|
-
afterSleep(10, () => {
|
|
82
|
-
node.focus({ preventScroll });
|
|
83
|
-
});
|
|
89
|
+
this.setHighlightedNode(candidateNodes[0]);
|
|
84
90
|
};
|
|
85
|
-
|
|
86
|
-
this
|
|
91
|
+
getNodeByValue = (value) => {
|
|
92
|
+
const candidateNodes = this.getCandidateNodes();
|
|
93
|
+
return candidateNodes.find((node) => node.dataset.value === value) ?? null;
|
|
87
94
|
};
|
|
88
|
-
|
|
89
|
-
this
|
|
95
|
+
setOpen = (open) => {
|
|
96
|
+
this.open.current = open;
|
|
90
97
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!node)
|
|
94
|
-
return [];
|
|
95
|
-
return Array.from(node.querySelectorAll(`[${ITEM_ATTR}]:not([data-disabled])`));
|
|
98
|
+
toggleOpen = () => {
|
|
99
|
+
this.open.current = !this.open.current;
|
|
96
100
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
handleOpen = () => {
|
|
102
|
+
this.setOpen(true);
|
|
103
|
+
};
|
|
104
|
+
handleClose = () => {
|
|
105
|
+
this.setHighlightedNode(null);
|
|
106
|
+
this.setOpen(false);
|
|
107
|
+
};
|
|
108
|
+
toggleMenu = () => {
|
|
109
|
+
this.toggleOpen();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
class SelectSingleRootState extends SelectBaseRootState {
|
|
113
|
+
value;
|
|
114
|
+
isMulti = false;
|
|
115
|
+
hasValue = $derived.by(() => this.value.current !== "");
|
|
116
|
+
currentLabel = $derived.by(() => {
|
|
117
|
+
if (!this.items.current.length)
|
|
118
|
+
return "";
|
|
119
|
+
const match = this.items.current.find((item) => item.value === this.value.current)?.label;
|
|
120
|
+
return match ?? "";
|
|
121
|
+
});
|
|
122
|
+
candidateLabels = $derived.by(() => {
|
|
123
|
+
if (!this.items.current.length)
|
|
100
124
|
return [];
|
|
101
|
-
|
|
125
|
+
const filteredItems = this.items.current.filter((item) => !item.disabled);
|
|
126
|
+
return filteredItems.map((item) => item.label);
|
|
127
|
+
});
|
|
128
|
+
dataTypeaheadEnabled = $derived.by(() => {
|
|
129
|
+
if (this.isMulti)
|
|
130
|
+
return false;
|
|
131
|
+
if (this.items.current.length === 0)
|
|
132
|
+
return false;
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
constructor(props) {
|
|
136
|
+
super(props);
|
|
137
|
+
this.value = props.value;
|
|
138
|
+
$effect(() => {
|
|
139
|
+
if (!this.open.current && this.highlightedNode) {
|
|
140
|
+
this.setHighlightedNode(null);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
$effect(() => {
|
|
144
|
+
if (!this.open.current)
|
|
145
|
+
return;
|
|
146
|
+
afterTick(() => {
|
|
147
|
+
this.#setInitialHighlightedNode();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
includesItem = (itemValue) => {
|
|
152
|
+
return this.value.current === itemValue;
|
|
153
|
+
};
|
|
154
|
+
toggleItem = (itemValue, itemLabel = itemValue) => {
|
|
155
|
+
this.value.current = this.includesItem(itemValue) ? "" : itemValue;
|
|
156
|
+
this.inputValue = itemLabel;
|
|
157
|
+
};
|
|
158
|
+
#setInitialHighlightedNode = () => {
|
|
159
|
+
if (this.highlightedNode)
|
|
160
|
+
return;
|
|
161
|
+
if (this.value.current !== "") {
|
|
162
|
+
const node = this.getNodeByValue(this.value.current);
|
|
163
|
+
if (node) {
|
|
164
|
+
this.setHighlightedNode(node);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// if no value is set, we want to highlight the first item
|
|
169
|
+
const firstCandidate = this.getCandidateNodes()[0];
|
|
170
|
+
if (!firstCandidate)
|
|
171
|
+
return;
|
|
172
|
+
this.setHighlightedNode(firstCandidate);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
class SelectMultipleRootState extends SelectBaseRootState {
|
|
176
|
+
value;
|
|
177
|
+
isMulti = true;
|
|
178
|
+
hasValue = $derived.by(() => this.value.current.length > 0);
|
|
179
|
+
constructor(props) {
|
|
180
|
+
super(props);
|
|
181
|
+
this.value = props.value;
|
|
182
|
+
$effect(() => {
|
|
183
|
+
if (!this.open.current)
|
|
184
|
+
return;
|
|
185
|
+
afterTick(() => {
|
|
186
|
+
if (!this.highlightedNode) {
|
|
187
|
+
this.#setInitialHighlightedNode();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
includesItem = (itemValue) => {
|
|
193
|
+
return this.value.current.includes(itemValue);
|
|
194
|
+
};
|
|
195
|
+
toggleItem = (itemValue, itemLabel = itemValue) => {
|
|
196
|
+
if (this.includesItem(itemValue)) {
|
|
197
|
+
this.value.current = this.value.current.filter((v) => v !== itemValue);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.value.current = [...this.value.current, itemValue];
|
|
201
|
+
}
|
|
202
|
+
this.inputValue = itemLabel;
|
|
203
|
+
};
|
|
204
|
+
#setInitialHighlightedNode = () => {
|
|
205
|
+
if (this.highlightedNode)
|
|
206
|
+
return;
|
|
207
|
+
if (this.value.current.length && this.value.current[0] !== "") {
|
|
208
|
+
const node = this.getNodeByValue(this.value.current[0]);
|
|
209
|
+
if (node) {
|
|
210
|
+
this.setHighlightedNode(node);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// if no value is set, we want to highlight the first item
|
|
215
|
+
const firstCandidate = this.getCandidateNodes()[0];
|
|
216
|
+
if (!firstCandidate)
|
|
217
|
+
return;
|
|
218
|
+
this.setHighlightedNode(firstCandidate);
|
|
102
219
|
};
|
|
103
220
|
}
|
|
221
|
+
class SelectInputState {
|
|
222
|
+
#id;
|
|
223
|
+
#ref;
|
|
224
|
+
root;
|
|
225
|
+
constructor(props, root) {
|
|
226
|
+
this.root = root;
|
|
227
|
+
this.#id = props.id;
|
|
228
|
+
this.#ref = props.ref;
|
|
229
|
+
useRefById({
|
|
230
|
+
id: this.#id,
|
|
231
|
+
ref: this.#ref,
|
|
232
|
+
onRefChange: (node) => {
|
|
233
|
+
this.root.inputNode = node;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
#onkeydown = async (e) => {
|
|
238
|
+
this.root.isUsingKeyboard = true;
|
|
239
|
+
if (e.key === kbd.ESCAPE)
|
|
240
|
+
return;
|
|
241
|
+
const open = this.root.open.current;
|
|
242
|
+
const inputValue = this.root.inputValue;
|
|
243
|
+
// prevent arrow up/down from moving the position of the cursor in the input
|
|
244
|
+
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN)
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
if (!open) {
|
|
247
|
+
if (INTERACTION_KEYS.includes(e.key))
|
|
248
|
+
return;
|
|
249
|
+
if (e.key === kbd.TAB)
|
|
250
|
+
return;
|
|
251
|
+
if (e.key === kbd.BACKSPACE && inputValue === "")
|
|
252
|
+
return;
|
|
253
|
+
this.root.handleOpen();
|
|
254
|
+
// we need to wait for a tick after the menu opens to ensure the highlighted nodes are
|
|
255
|
+
// set correctly.
|
|
256
|
+
afterTick(() => {
|
|
257
|
+
if (this.root.hasValue)
|
|
258
|
+
return;
|
|
259
|
+
const candidateNodes = this.root.getCandidateNodes();
|
|
260
|
+
if (!candidateNodes.length)
|
|
261
|
+
return;
|
|
262
|
+
if (e.key === kbd.ARROW_DOWN) {
|
|
263
|
+
const firstCandidate = candidateNodes[0];
|
|
264
|
+
this.root.setHighlightedNode(firstCandidate);
|
|
265
|
+
}
|
|
266
|
+
else if (e.key === kbd.ARROW_UP) {
|
|
267
|
+
const lastCandidate = candidateNodes[candidateNodes.length - 1];
|
|
268
|
+
this.root.setHighlightedNode(lastCandidate);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (e.key === kbd.TAB) {
|
|
274
|
+
this.root.handleClose();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (e.key === kbd.ENTER && !e.isComposing) {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
const highlightedValue = this.root.highlightedValue;
|
|
280
|
+
if (highlightedValue) {
|
|
281
|
+
this.root.toggleItem(highlightedValue, this.root.highlightedLabel ?? undefined);
|
|
282
|
+
}
|
|
283
|
+
if (!this.root.isMulti) {
|
|
284
|
+
this.root.handleClose();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (e.key === kbd.ARROW_UP && e.altKey) {
|
|
288
|
+
this.root.handleClose();
|
|
289
|
+
}
|
|
290
|
+
if (FIRST_LAST_KEYS.includes(e.key)) {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
const candidateNodes = this.root.getCandidateNodes();
|
|
293
|
+
const currHighlightedNode = this.root.highlightedNode;
|
|
294
|
+
const currIndex = currHighlightedNode
|
|
295
|
+
? candidateNodes.indexOf(currHighlightedNode)
|
|
296
|
+
: -1;
|
|
297
|
+
const loop = this.root.loop.current;
|
|
298
|
+
let nextItem;
|
|
299
|
+
if (e.key === kbd.ARROW_DOWN) {
|
|
300
|
+
nextItem = next(candidateNodes, currIndex, loop);
|
|
301
|
+
}
|
|
302
|
+
else if (e.key === kbd.ARROW_UP) {
|
|
303
|
+
nextItem = prev(candidateNodes, currIndex, loop);
|
|
304
|
+
}
|
|
305
|
+
else if (e.key === kbd.PAGE_DOWN) {
|
|
306
|
+
nextItem = forward(candidateNodes, currIndex, 10, loop);
|
|
307
|
+
}
|
|
308
|
+
else if (e.key === kbd.PAGE_UP) {
|
|
309
|
+
nextItem = backward(candidateNodes, currIndex, 10, loop);
|
|
310
|
+
}
|
|
311
|
+
else if (e.key === kbd.HOME) {
|
|
312
|
+
nextItem = candidateNodes[0];
|
|
313
|
+
}
|
|
314
|
+
else if (e.key === kbd.END) {
|
|
315
|
+
nextItem = candidateNodes[candidateNodes.length - 1];
|
|
316
|
+
}
|
|
317
|
+
if (!nextItem)
|
|
318
|
+
return;
|
|
319
|
+
this.root.setHighlightedNode(nextItem);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (INTERACTION_KEYS.includes(e.key))
|
|
323
|
+
return;
|
|
324
|
+
if (!this.root.highlightedNode) {
|
|
325
|
+
this.root.setHighlightedToFirstCandidate();
|
|
326
|
+
}
|
|
327
|
+
// this.root.setHighlightedToFirstCandidate();
|
|
328
|
+
};
|
|
329
|
+
#oninput = (e) => {
|
|
330
|
+
this.root.inputValue = e.currentTarget.value;
|
|
331
|
+
this.root.setHighlightedToFirstCandidate();
|
|
332
|
+
};
|
|
333
|
+
props = $derived.by(() => ({
|
|
334
|
+
id: this.#id.current,
|
|
335
|
+
role: "combobox",
|
|
336
|
+
disabled: this.root.disabled.current ? true : undefined,
|
|
337
|
+
"aria-activedescendant": this.root.highlightedId,
|
|
338
|
+
"aria-autocomplete": "list",
|
|
339
|
+
"aria-expanded": getAriaExpanded(this.root.open.current),
|
|
340
|
+
"data-state": getDataOpenClosed(this.root.open.current),
|
|
341
|
+
"data-disabled": getDataDisabled(this.root.disabled.current),
|
|
342
|
+
onkeydown: this.#onkeydown,
|
|
343
|
+
oninput: this.#oninput,
|
|
344
|
+
[this.root.bitsAttrs.input]: "",
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
class SelectComboTriggerState {
|
|
348
|
+
#id;
|
|
349
|
+
#ref;
|
|
350
|
+
root;
|
|
351
|
+
constructor(props, root) {
|
|
352
|
+
this.root = root;
|
|
353
|
+
this.#id = props.id;
|
|
354
|
+
this.#ref = props.ref;
|
|
355
|
+
useRefById({
|
|
356
|
+
id: this.#id,
|
|
357
|
+
ref: this.#ref,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
#onkeydown = (e) => {
|
|
361
|
+
if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
if (document.activeElement !== this.root.inputNode) {
|
|
364
|
+
this.root.inputNode?.focus();
|
|
365
|
+
}
|
|
366
|
+
this.root.toggleMenu();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* `pointerdown` fires before the `focus` event, so we can prevent the default
|
|
371
|
+
* behavior of focusing the button and keep focus on the input.
|
|
372
|
+
*/
|
|
373
|
+
#onpointerdown = (e) => {
|
|
374
|
+
if (this.root.disabled.current)
|
|
375
|
+
return;
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
if (document.activeElement !== this.root.inputNode) {
|
|
378
|
+
this.root.inputNode?.focus();
|
|
379
|
+
}
|
|
380
|
+
this.root.toggleMenu();
|
|
381
|
+
};
|
|
382
|
+
props = $derived.by(() => ({
|
|
383
|
+
id: this.#id.current,
|
|
384
|
+
disabled: this.root.disabled.current ? true : undefined,
|
|
385
|
+
"aria-haspopup": "listbox",
|
|
386
|
+
"data-state": getDataOpenClosed(this.root.open.current),
|
|
387
|
+
"data-disabled": getDataDisabled(this.root.disabled.current),
|
|
388
|
+
[this.root.bitsAttrs.trigger]: "",
|
|
389
|
+
onpointerdown: this.#onpointerdown,
|
|
390
|
+
onkeydown: this.#onkeydown,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
104
393
|
class SelectTriggerState {
|
|
105
|
-
#root;
|
|
106
394
|
#id;
|
|
107
395
|
#ref;
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
return this.#root.disabled.current || this.#disabled.current;
|
|
112
|
-
});
|
|
396
|
+
root;
|
|
397
|
+
#domTypeahead;
|
|
398
|
+
#dataTypeahead;
|
|
113
399
|
constructor(props, root) {
|
|
400
|
+
this.root = root;
|
|
114
401
|
this.#id = props.id;
|
|
115
402
|
this.#ref = props.ref;
|
|
116
|
-
this.#root = root;
|
|
117
|
-
this.#disabled = props.disabled;
|
|
118
403
|
useRefById({
|
|
119
404
|
id: this.#id,
|
|
120
405
|
ref: this.#ref,
|
|
121
406
|
onRefChange: (node) => {
|
|
122
|
-
this
|
|
407
|
+
this.root.triggerNode = node;
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
this.#domTypeahead = useDOMTypeahead({
|
|
411
|
+
getCurrentItem: () => this.root.highlightedNode,
|
|
412
|
+
onMatch: (node) => {
|
|
413
|
+
this.root.setHighlightedNode(node);
|
|
123
414
|
},
|
|
124
415
|
});
|
|
125
|
-
this.#
|
|
416
|
+
this.#dataTypeahead = useDataTypeahead({
|
|
417
|
+
getCurrentItem: () => {
|
|
418
|
+
if (this.root.isMulti)
|
|
419
|
+
return "";
|
|
420
|
+
return this.root.currentLabel;
|
|
421
|
+
},
|
|
422
|
+
onMatch: (label) => {
|
|
423
|
+
if (this.root.isMulti)
|
|
424
|
+
return;
|
|
425
|
+
if (!this.root.items.current)
|
|
426
|
+
return;
|
|
427
|
+
const matchedItem = this.root.items.current.find((item) => item.label === label);
|
|
428
|
+
if (!matchedItem)
|
|
429
|
+
return;
|
|
430
|
+
this.root.value.current = matchedItem.value;
|
|
431
|
+
},
|
|
432
|
+
enabled: !this.root.isMulti && this.root.dataTypeaheadEnabled,
|
|
433
|
+
});
|
|
126
434
|
}
|
|
127
|
-
#
|
|
128
|
-
|
|
435
|
+
#onkeydown = (e) => {
|
|
436
|
+
this.root.isUsingKeyboard = true;
|
|
437
|
+
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN)
|
|
438
|
+
e.preventDefault();
|
|
439
|
+
if (!this.root.open.current) {
|
|
440
|
+
if (e.key === kbd.ENTER) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
else if (e.key === kbd.SPACE || e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_UP) {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
this.root.handleOpen();
|
|
446
|
+
}
|
|
447
|
+
else if (!this.root.isMulti && this.root.dataTypeaheadEnabled) {
|
|
448
|
+
this.#dataTypeahead.handleTypeaheadSearch(e.key, this.root.candidateLabels);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
// we need to wait for a tick after the menu opens to ensure
|
|
452
|
+
// the highlighted nodes are set correctly
|
|
453
|
+
afterTick(() => {
|
|
454
|
+
if (this.root.hasValue)
|
|
455
|
+
return;
|
|
456
|
+
const candidateNodes = this.root.getCandidateNodes();
|
|
457
|
+
if (!candidateNodes.length)
|
|
458
|
+
return;
|
|
459
|
+
if (e.key === kbd.ARROW_DOWN) {
|
|
460
|
+
const firstCandidate = candidateNodes[0];
|
|
461
|
+
this.root.setHighlightedNode(firstCandidate);
|
|
462
|
+
}
|
|
463
|
+
else if (e.key === kbd.ARROW_UP) {
|
|
464
|
+
const lastCandidate = candidateNodes[candidateNodes.length - 1];
|
|
465
|
+
this.root.setHighlightedNode(lastCandidate);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (e.key === kbd.TAB) {
|
|
471
|
+
this.root.handleClose();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if ((e.key === kbd.ENTER || e.key === kbd.SPACE) && !e.isComposing) {
|
|
475
|
+
e.preventDefault();
|
|
476
|
+
const highlightedValue = this.root.highlightedValue;
|
|
477
|
+
if (highlightedValue) {
|
|
478
|
+
this.root.toggleItem(highlightedValue, this.root.highlightedLabel ?? undefined);
|
|
479
|
+
}
|
|
480
|
+
if (!this.root.isMulti) {
|
|
481
|
+
this.root.handleClose();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (e.key === kbd.ARROW_UP && e.altKey) {
|
|
485
|
+
this.root.handleClose();
|
|
486
|
+
}
|
|
487
|
+
if (FIRST_LAST_KEYS.includes(e.key)) {
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
const candidateNodes = this.root.getCandidateNodes();
|
|
490
|
+
const currHighlightedNode = this.root.highlightedNode;
|
|
491
|
+
const currIndex = currHighlightedNode
|
|
492
|
+
? candidateNodes.indexOf(currHighlightedNode)
|
|
493
|
+
: -1;
|
|
494
|
+
const loop = this.root.loop.current;
|
|
495
|
+
let nextItem;
|
|
496
|
+
if (e.key === kbd.ARROW_DOWN) {
|
|
497
|
+
nextItem = next(candidateNodes, currIndex, loop);
|
|
498
|
+
}
|
|
499
|
+
else if (e.key === kbd.ARROW_UP) {
|
|
500
|
+
nextItem = prev(candidateNodes, currIndex, loop);
|
|
501
|
+
}
|
|
502
|
+
else if (e.key === kbd.PAGE_DOWN) {
|
|
503
|
+
nextItem = forward(candidateNodes, currIndex, 10, loop);
|
|
504
|
+
}
|
|
505
|
+
else if (e.key === kbd.PAGE_UP) {
|
|
506
|
+
nextItem = backward(candidateNodes, currIndex, 10, loop);
|
|
507
|
+
}
|
|
508
|
+
else if (e.key === kbd.HOME) {
|
|
509
|
+
nextItem = candidateNodes[0];
|
|
510
|
+
}
|
|
511
|
+
else if (e.key === kbd.END) {
|
|
512
|
+
nextItem = candidateNodes[candidateNodes.length - 1];
|
|
513
|
+
}
|
|
514
|
+
if (!nextItem)
|
|
515
|
+
return;
|
|
516
|
+
this.root.setHighlightedNode(nextItem);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
|
|
520
|
+
const isCharacterKey = e.key.length === 1;
|
|
521
|
+
// prevent space from being considered with typeahead
|
|
522
|
+
if (e.code === "Space")
|
|
523
|
+
return;
|
|
524
|
+
const candidateNodes = this.root.getCandidateNodes();
|
|
525
|
+
if (e.key === kbd.TAB)
|
|
129
526
|
return;
|
|
130
|
-
|
|
131
|
-
|
|
527
|
+
if (!isModifierKey && isCharacterKey) {
|
|
528
|
+
this.#domTypeahead.handleTypeaheadSearch(e.key, candidateNodes);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!this.root.highlightedNode) {
|
|
532
|
+
this.root.setHighlightedToFirstCandidate();
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
#handleOpen = () => {
|
|
536
|
+
this.root.open.current = true;
|
|
537
|
+
this.#dataTypeahead.resetTypeahead();
|
|
538
|
+
this.#domTypeahead.resetTypeahead();
|
|
132
539
|
};
|
|
133
540
|
#handlePointerOpen = (e) => {
|
|
134
541
|
this.#handleOpen();
|
|
135
|
-
this
|
|
542
|
+
this.root.triggerPointerDownPos = {
|
|
136
543
|
x: Math.round(e.pageX),
|
|
137
544
|
y: Math.round(e.pageY),
|
|
138
545
|
};
|
|
@@ -146,9 +553,14 @@ class SelectTriggerState {
|
|
|
146
553
|
const currTarget = e.currentTarget;
|
|
147
554
|
currTarget.focus();
|
|
148
555
|
};
|
|
556
|
+
/**
|
|
557
|
+
* `pointerdown` fires before the `focus` event, so we can prevent the default
|
|
558
|
+
* behavior of focusing the button and keep focus on the input.
|
|
559
|
+
*/
|
|
149
560
|
#onpointerdown = (e) => {
|
|
150
|
-
|
|
151
|
-
|
|
561
|
+
if (this.root.disabled.current)
|
|
562
|
+
return;
|
|
563
|
+
// prevent opening on touch down which can be triggered when scrolling on touch devices
|
|
152
564
|
if (e.pointerType === "touch")
|
|
153
565
|
return e.preventDefault();
|
|
154
566
|
// prevent implicit pointer capture
|
|
@@ -159,12 +571,12 @@ class SelectTriggerState {
|
|
|
159
571
|
// only call the handle if it's a left click, since pointerdown is triggered
|
|
160
572
|
// by right clicks as well, but not when ctrl is pressed
|
|
161
573
|
if (e.button === 0 && e.ctrlKey === false) {
|
|
162
|
-
if (this
|
|
574
|
+
if (this.root.open.current === false) {
|
|
163
575
|
this.#handlePointerOpen(e);
|
|
164
576
|
e.preventDefault();
|
|
165
577
|
}
|
|
166
578
|
else {
|
|
167
|
-
this
|
|
579
|
+
this.root.handleClose();
|
|
168
580
|
}
|
|
169
581
|
}
|
|
170
582
|
};
|
|
@@ -174,661 +586,269 @@ class SelectTriggerState {
|
|
|
174
586
|
this.#handlePointerOpen(e);
|
|
175
587
|
}
|
|
176
588
|
};
|
|
177
|
-
#onkeydown = (e) => {
|
|
178
|
-
const isTypingAhead = this.#typeahead.search.current !== "";
|
|
179
|
-
const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
|
|
180
|
-
if (!isModifierKey && e.key.length === 1) {
|
|
181
|
-
if (isTypingAhead && e.key === " ")
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const newItem = this.#typeahead.handleTypeaheadSearch(e.key, this.#root.getTriggerTypeaheadCandidateNodes());
|
|
185
|
-
if (newItem && newItem.dataset.value) {
|
|
186
|
-
this.#root.value.current = newItem.dataset.value;
|
|
187
|
-
}
|
|
188
|
-
if (OPEN_KEYS.includes(e.key)) {
|
|
189
|
-
this.#handleOpen();
|
|
190
|
-
e.preventDefault();
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
589
|
props = $derived.by(() => ({
|
|
194
590
|
id: this.#id.current,
|
|
195
|
-
disabled: this
|
|
196
|
-
|
|
197
|
-
type: "button",
|
|
198
|
-
"aria-controls": this.#root.contentId,
|
|
199
|
-
"aria-expanded": getAriaExpanded(this.#root.open.current),
|
|
200
|
-
"aria-required": getAriaRequired(this.#root.required.current),
|
|
201
|
-
"aria-autocomplete": "none",
|
|
202
|
-
dir: this.#root.dir.current,
|
|
203
|
-
"data-state": getDataOpenClosed(this.#root.open.current),
|
|
204
|
-
"data-disabled": getDataDisabled(this.#isDisabled),
|
|
205
|
-
"data-placeholder": shouldShowPlaceholder(this.#root.value.current)
|
|
206
|
-
? ""
|
|
207
|
-
: undefined,
|
|
208
|
-
[TRIGGER_ATTR]: "",
|
|
209
|
-
onclick: this.#onclick,
|
|
210
|
-
onpointerdown: this.#onpointerdown,
|
|
211
|
-
onpointerup: this.#onpointerup,
|
|
212
|
-
onkeydown: this.#onkeydown,
|
|
213
|
-
}));
|
|
214
|
-
}
|
|
215
|
-
class SelectValueState {
|
|
216
|
-
root;
|
|
217
|
-
showPlaceholder = $derived.by(() => shouldShowPlaceholder(this.root.value.current));
|
|
218
|
-
ref = box(null);
|
|
219
|
-
constructor(root) {
|
|
220
|
-
this.root = root;
|
|
221
|
-
useRefById({
|
|
222
|
-
id: this.root.valueId,
|
|
223
|
-
ref: this.ref,
|
|
224
|
-
onRefChange: (node) => {
|
|
225
|
-
this.root.valueNode = node;
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
props = $derived.by(() => ({
|
|
230
|
-
id: this.root.valueId.current,
|
|
591
|
+
disabled: this.root.disabled.current ? true : undefined,
|
|
592
|
+
"aria-haspopup": "listbox",
|
|
231
593
|
"data-state": getDataOpenClosed(this.root.open.current),
|
|
232
594
|
"data-disabled": getDataDisabled(this.root.disabled.current),
|
|
233
|
-
[
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
595
|
+
[this.root.bitsAttrs.trigger]: "",
|
|
596
|
+
onpointerdown: this.#onpointerdown,
|
|
597
|
+
onkeydown: this.#onkeydown,
|
|
598
|
+
onclick: this.#onclick,
|
|
599
|
+
onpointerup: this.#onpointerup,
|
|
600
|
+
// onclick: this.#onclick,
|
|
237
601
|
}));
|
|
238
602
|
}
|
|
239
|
-
class
|
|
240
|
-
root;
|
|
241
|
-
constructor(root) {
|
|
242
|
-
this.root = root;
|
|
243
|
-
$effect(() => {
|
|
244
|
-
this.root.contentFragment = new DocumentFragment();
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
export class SelectContentState {
|
|
603
|
+
class SelectContentState {
|
|
249
604
|
id;
|
|
250
605
|
ref;
|
|
251
|
-
root;
|
|
252
606
|
viewportNode = $state(null);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
selectedItemText = box(null);
|
|
256
|
-
position;
|
|
257
|
-
isPositioned = box(false);
|
|
258
|
-
firstValidItemFound = box(false);
|
|
259
|
-
typeahead;
|
|
260
|
-
alignedPositionState = null;
|
|
607
|
+
root;
|
|
608
|
+
isPositioned = $state(false);
|
|
261
609
|
constructor(props, root) {
|
|
262
|
-
this.
|
|
610
|
+
this.root = root;
|
|
263
611
|
this.id = props.id;
|
|
264
612
|
this.ref = props.ref;
|
|
265
|
-
this.root = root;
|
|
266
|
-
this.typeahead = useTypeahead();
|
|
267
613
|
useRefById({
|
|
268
614
|
id: this.id,
|
|
269
615
|
ref: this.ref,
|
|
270
|
-
deps: () => this.root.open.current,
|
|
271
616
|
onRefChange: (node) => {
|
|
272
617
|
this.root.contentNode = node;
|
|
273
|
-
this.root.contentId = node?.id;
|
|
274
618
|
},
|
|
619
|
+
deps: () => this.root.open.current,
|
|
275
620
|
});
|
|
276
621
|
$effect(() => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
afterTick(() => {
|
|
281
|
-
const node = document.getElementById(this.id.current);
|
|
282
|
-
if (!node)
|
|
283
|
-
return;
|
|
284
|
-
let pointerMoveDelta = { x: 0, y: 0 };
|
|
285
|
-
const handlePointerMove = (e) => {
|
|
286
|
-
pointerMoveDelta = {
|
|
287
|
-
x: Math.abs(Math.round(e.pageX) -
|
|
288
|
-
(this.root.triggerPointerDownPos.current?.x ?? 0)),
|
|
289
|
-
y: Math.abs(Math.round(e.pageY) -
|
|
290
|
-
(this.root.triggerPointerDownPos.current?.y ?? 0)),
|
|
291
|
-
};
|
|
292
|
-
};
|
|
293
|
-
const handlePointerUp = (e) => {
|
|
294
|
-
if (e.pointerType === "touch")
|
|
295
|
-
return;
|
|
296
|
-
if (pointerMoveDelta.x <= 10 && pointerMoveDelta.y <= 10) {
|
|
297
|
-
e.preventDefault();
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
if (!this.root.contentNode?.contains(e.target)) {
|
|
301
|
-
this.root.handleClose();
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
document.removeEventListener("pointermove", handlePointerMove);
|
|
305
|
-
this.root.triggerPointerDownPos.current = null;
|
|
306
|
-
};
|
|
307
|
-
if (this.root.triggerPointerDownPos.current !== null) {
|
|
308
|
-
const pointerMove = addEventListener(document, "pointermove", handlePointerMove);
|
|
309
|
-
const pointerUp = addEventListener(document, "pointerup", handlePointerUp, {
|
|
310
|
-
capture: true,
|
|
311
|
-
once: true,
|
|
312
|
-
});
|
|
313
|
-
for (const cleanupFn of cleanup)
|
|
314
|
-
cleanupFn();
|
|
315
|
-
cleanup = [pointerMove, pointerUp];
|
|
316
|
-
}
|
|
317
|
-
return () => {
|
|
318
|
-
for (const cleanupFn of cleanup)
|
|
319
|
-
cleanupFn();
|
|
320
|
-
};
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
$effect(() => {
|
|
325
|
-
if (this.isPositioned.current) {
|
|
326
|
-
this.focusSelectedItem();
|
|
327
|
-
}
|
|
622
|
+
return () => {
|
|
623
|
+
this.root.contentNode = null;
|
|
624
|
+
};
|
|
328
625
|
});
|
|
329
626
|
$effect(() => {
|
|
330
627
|
if (this.root.open.current === false) {
|
|
331
|
-
this.isPositioned
|
|
628
|
+
this.isPositioned = false;
|
|
332
629
|
}
|
|
333
630
|
});
|
|
334
631
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const [lastItem] = restItems.slice(-1);
|
|
338
|
-
const PREV_FOCUSED_ELEMENT = document.activeElement;
|
|
339
|
-
for (const candidate of candidates) {
|
|
340
|
-
if (candidate === PREV_FOCUSED_ELEMENT)
|
|
341
|
-
return;
|
|
342
|
-
candidate?.scrollIntoView({ block: "nearest" });
|
|
343
|
-
// viewport might have padding so scroll to the edge when focusing first/last
|
|
344
|
-
const viewport = this.viewportNode;
|
|
345
|
-
if (candidate === firstItem && viewport) {
|
|
346
|
-
viewport.scrollTop = 0;
|
|
347
|
-
}
|
|
348
|
-
if (candidate === lastItem && viewport) {
|
|
349
|
-
viewport.scrollTop = viewport.scrollHeight;
|
|
350
|
-
}
|
|
351
|
-
candidate?.focus();
|
|
352
|
-
if (document.activeElement !== PREV_FOCUSED_ELEMENT)
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
onItemLeave = () => {
|
|
357
|
-
this.root.contentNode?.focus();
|
|
632
|
+
#onpointermove = () => {
|
|
633
|
+
this.root.isUsingKeyboard = false;
|
|
358
634
|
};
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const selectedItemNode = candidates.find((node) => node?.dataset.value === this.root.value.current) ?? null;
|
|
362
|
-
const first = candidates[0] ?? null;
|
|
363
|
-
if (selectedItemNode) {
|
|
364
|
-
const selectedItemTextNode = selectedItemNode.querySelector(`[${ITEM_TEXT_ATTR}]`);
|
|
635
|
+
#styles = $derived.by(() => {
|
|
636
|
+
if (this.root.isCombobox) {
|
|
365
637
|
return {
|
|
366
|
-
|
|
367
|
-
|
|
638
|
+
"--bits-combobox-content-transform-origin": "var(--bits-floating-transform-origin)",
|
|
639
|
+
"--bits-combobox-content-available-width": "var(--bits-floating-available-width)",
|
|
640
|
+
"--bits-combobox-content-available-height": "var(--bits-floating-available-height)",
|
|
641
|
+
"--bits-combobox-anchor-width": "var(--bits-floating-anchor-width)",
|
|
642
|
+
"--bits-combobox-anchor-height": "var(--bits-floating-anchor-height)",
|
|
368
643
|
};
|
|
369
644
|
}
|
|
370
645
|
else {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return {
|
|
380
|
-
selectedItemNode: null,
|
|
381
|
-
selectedItemTextNode: null,
|
|
382
|
-
};
|
|
383
|
-
};
|
|
384
|
-
focusSelectedItem = () => {
|
|
385
|
-
afterTick(() => {
|
|
386
|
-
const candidates = this.root.getCandidateNodes();
|
|
387
|
-
const selected = candidates.find((node) => node?.dataset.value === this.root.value.current) ?? null;
|
|
388
|
-
const first = candidates[0] ?? null;
|
|
389
|
-
this.focusFirst([selected, first]);
|
|
390
|
-
});
|
|
391
|
-
};
|
|
392
|
-
itemRegister = (value, disabled) => {
|
|
393
|
-
const isFirstValidItem = !this.firstValidItemFound.current && !disabled;
|
|
394
|
-
const isSelectedItem = this.root.value.current !== undefined && this.root.value.current === value;
|
|
395
|
-
if (isSelectedItem || isFirstValidItem) {
|
|
396
|
-
if (isFirstValidItem) {
|
|
397
|
-
this.firstValidItemFound.current = true;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
itemTextRegister = (node, value, disabled) => {
|
|
402
|
-
const isFirstValidItem = !this.firstValidItemFound.current && !disabled;
|
|
403
|
-
const isSelectedItem = this.root.value.current !== undefined && this.root.value.current === value;
|
|
404
|
-
if (isSelectedItem || isFirstValidItem) {
|
|
405
|
-
this.selectedItemText.current = node;
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
#onkeydown = (e) => {
|
|
409
|
-
const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
|
|
410
|
-
if (e.key === "Tab")
|
|
411
|
-
e.preventDefault();
|
|
412
|
-
if (!isModifierKey && e.key.length === 1) {
|
|
413
|
-
this.typeahead.handleTypeaheadSearch(e.key, this.root.getCandidateNodes());
|
|
646
|
+
return {
|
|
647
|
+
"--bits-select-content-transform-origin": "var(--bits-floating-transform-origin)",
|
|
648
|
+
"--bits-select-content-available-width": "var(--bits-floating-available-width)",
|
|
649
|
+
"--bits-select-content-available-height": "var(--bits-floating-available-height)",
|
|
650
|
+
"--bits-select-anchor-width": "var(--bits-floating-anchor-width)",
|
|
651
|
+
"--bits-select-anchor-height": "var(--bits-floating-anchor-height)",
|
|
652
|
+
};
|
|
414
653
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
candidateNodes = candidateNodes.slice().reverse();
|
|
419
|
-
}
|
|
420
|
-
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN) {
|
|
421
|
-
const currElement = e.target;
|
|
422
|
-
const currIndex = candidateNodes.indexOf(currElement);
|
|
423
|
-
candidateNodes = candidateNodes.slice(currIndex + 1);
|
|
424
|
-
}
|
|
425
|
-
setTimeout(() => this.focusFirst(candidateNodes));
|
|
654
|
+
});
|
|
655
|
+
handleInteractOutside = (e) => {
|
|
656
|
+
if (e.target === this.root.triggerNode || e.target === this.root.inputNode) {
|
|
426
657
|
e.preventDefault();
|
|
427
658
|
}
|
|
428
659
|
};
|
|
429
|
-
|
|
430
|
-
e.preventDefault();
|
|
431
|
-
};
|
|
660
|
+
snippetProps = $derived.by(() => ({ open: this.root.open.current }));
|
|
432
661
|
props = $derived.by(() => ({
|
|
433
662
|
id: this.id.current,
|
|
434
663
|
role: "listbox",
|
|
435
664
|
"data-state": getDataOpenClosed(this.root.open.current),
|
|
665
|
+
[this.root.bitsAttrs.content]: "",
|
|
436
666
|
style: {
|
|
437
667
|
display: "flex",
|
|
438
668
|
flexDirection: "column",
|
|
439
669
|
outline: "none",
|
|
670
|
+
boxSizing: "border-box",
|
|
671
|
+
...this.#styles,
|
|
440
672
|
},
|
|
441
|
-
|
|
442
|
-
onkeydown: this.#onkeydown,
|
|
443
|
-
tabIndex: -1,
|
|
444
|
-
[CONTENT_ATTR]: "",
|
|
673
|
+
onpointermove: this.#onpointermove,
|
|
445
674
|
}));
|
|
446
675
|
}
|
|
447
676
|
class SelectItemState {
|
|
448
677
|
#id;
|
|
449
678
|
#ref;
|
|
450
679
|
root;
|
|
451
|
-
content;
|
|
452
|
-
textId = box(undefined);
|
|
453
680
|
value;
|
|
681
|
+
label;
|
|
682
|
+
onHighlight;
|
|
683
|
+
onUnhighlight;
|
|
454
684
|
disabled;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
this.#id = props.id;
|
|
462
|
-
this.#ref = props.ref;
|
|
463
|
-
this.root = content.root;
|
|
464
|
-
this.content = content;
|
|
685
|
+
isSelected = $derived.by(() => this.root.includesItem(this.value.current));
|
|
686
|
+
isHighlighted = $derived.by(() => this.root.highlightedValue === this.value.current);
|
|
687
|
+
prevHighlighted = new Previous(() => this.isHighlighted);
|
|
688
|
+
textId = $state("");
|
|
689
|
+
constructor(props, root) {
|
|
690
|
+
this.root = root;
|
|
465
691
|
this.value = props.value;
|
|
466
692
|
this.disabled = props.disabled;
|
|
467
|
-
this.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
693
|
+
this.label = props.label;
|
|
694
|
+
this.onHighlight = props.onHighlight;
|
|
695
|
+
this.onUnhighlight = props.onUnhighlight;
|
|
696
|
+
this.#id = props.id;
|
|
697
|
+
this.#ref = props.ref;
|
|
472
698
|
$effect(() => {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
this.root.value.current = this.value.current;
|
|
490
|
-
this.root.handleClose();
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
#onpointermove = async (e) => {
|
|
494
|
-
if (e.defaultPrevented)
|
|
495
|
-
return;
|
|
496
|
-
if (this.disabled.current) {
|
|
497
|
-
this.content.onItemLeave();
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
e.currentTarget.focus({ preventScroll: true });
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
#onpointerleave = async (e) => {
|
|
504
|
-
if (e.defaultPrevented)
|
|
505
|
-
return;
|
|
506
|
-
if (e.currentTarget === document.activeElement) {
|
|
507
|
-
this.content.onItemLeave();
|
|
508
|
-
}
|
|
509
|
-
};
|
|
699
|
+
if (this.isHighlighted) {
|
|
700
|
+
this.onHighlight.current();
|
|
701
|
+
}
|
|
702
|
+
else if (this.prevHighlighted.current) {
|
|
703
|
+
this.onUnhighlight.current();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
useRefById({
|
|
707
|
+
id: this.#id,
|
|
708
|
+
ref: this.#ref,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
snippetProps = $derived.by(() => ({
|
|
712
|
+
selected: this.isSelected,
|
|
713
|
+
highlighted: this.isHighlighted,
|
|
714
|
+
}));
|
|
510
715
|
#onpointerdown = (e) => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
#onpointerup = async (e) => {
|
|
514
|
-
await this.handleSelect(e);
|
|
716
|
+
// prevent focus from leaving the combobox
|
|
717
|
+
e.preventDefault();
|
|
515
718
|
};
|
|
516
|
-
|
|
719
|
+
/**
|
|
720
|
+
* Using `pointerup` instead of `click` allows power users to pointerdown
|
|
721
|
+
* the trigger, then release pointerup on an item to select it vs having to do
|
|
722
|
+
* multiple clicks.
|
|
723
|
+
*/
|
|
724
|
+
#onpointerup = (e) => {
|
|
517
725
|
if (e.defaultPrevented)
|
|
518
726
|
return;
|
|
519
|
-
|
|
520
|
-
|
|
727
|
+
// prevent any default behavior
|
|
728
|
+
e.preventDefault();
|
|
729
|
+
if (this.disabled.current)
|
|
521
730
|
return;
|
|
522
|
-
|
|
523
|
-
|
|
731
|
+
const isCurrentSelectedValue = this.value.current === this.root.value.current;
|
|
732
|
+
this.root.toggleItem(this.value.current, this.label.current);
|
|
733
|
+
if (!this.root.isMulti && !isCurrentSelectedValue) {
|
|
734
|
+
this.root.handleClose();
|
|
524
735
|
}
|
|
525
|
-
// prevent page scroll on space
|
|
526
|
-
if (e.key === kbd.SPACE)
|
|
527
|
-
e.preventDefault();
|
|
528
736
|
};
|
|
529
|
-
#
|
|
530
|
-
this.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
this.isFocused.current = false;
|
|
737
|
+
#onpointermove = (_) => {
|
|
738
|
+
if (this.root.highlightedNode !== this.#ref.current) {
|
|
739
|
+
this.root.setHighlightedNode(this.#ref.current);
|
|
740
|
+
}
|
|
534
741
|
};
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
e.stopPropagation();
|
|
742
|
+
setTextId = (id) => {
|
|
743
|
+
this.textId = id;
|
|
538
744
|
};
|
|
539
745
|
props = $derived.by(() => ({
|
|
540
746
|
id: this.#id.current,
|
|
541
|
-
|
|
542
|
-
"aria-labelledby": this.textId.current ?? undefined,
|
|
543
|
-
"data-highlighted": this.isFocused.current ? "" : undefined,
|
|
544
|
-
"aria-selected": getAriaSelected(this.isSelected),
|
|
545
|
-
"data-state": getDataChecked(this.isSelected),
|
|
546
|
-
"aria-disabled": getAriaDisabled(this.disabled.current),
|
|
547
|
-
"data-disabled": getDataDisabled(this.disabled.current),
|
|
548
|
-
"data-selected": this.isSelected ? "" : undefined,
|
|
747
|
+
"aria-selected": this.root.includesItem(this.value.current) ? "true" : undefined,
|
|
549
748
|
"data-value": this.value.current,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
749
|
+
"data-disabled": getDataDisabled(this.disabled.current),
|
|
750
|
+
"data-highlighted": this.root.highlightedValue === this.value.current ? "" : undefined,
|
|
751
|
+
"data-selected": this.root.includesItem(this.value.current) ? "" : undefined,
|
|
752
|
+
"data-label": this.label.current,
|
|
753
|
+
[this.root.bitsAttrs.item]: "",
|
|
554
754
|
onpointermove: this.#onpointermove,
|
|
555
|
-
onpointerleave: this.#onpointerleave,
|
|
556
755
|
onpointerdown: this.#onpointerdown,
|
|
557
756
|
onpointerup: this.#onpointerup,
|
|
558
|
-
onkeydown: this.#onkeydown,
|
|
559
|
-
onblur: this.#onblur,
|
|
560
|
-
ontouchend: this.#ontouchend,
|
|
561
757
|
}));
|
|
562
758
|
}
|
|
563
|
-
class
|
|
564
|
-
item;
|
|
759
|
+
class SelectGroupState {
|
|
565
760
|
#id;
|
|
566
761
|
#ref;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
value: this.item.value.current,
|
|
571
|
-
disabled: this.item.disabled.current,
|
|
572
|
-
innerHTML: this.node.current?.textContent,
|
|
573
|
-
}));
|
|
574
|
-
constructor(props, item) {
|
|
762
|
+
root;
|
|
763
|
+
labelNode = $state(null);
|
|
764
|
+
constructor(props, root) {
|
|
575
765
|
this.#id = props.id;
|
|
576
766
|
this.#ref = props.ref;
|
|
577
|
-
this.
|
|
578
|
-
this.item.setTextId(this.#id.current);
|
|
767
|
+
this.root = root;
|
|
579
768
|
useRefById({
|
|
580
769
|
id: this.#id,
|
|
581
770
|
ref: this.#ref,
|
|
582
771
|
});
|
|
583
|
-
$effect(() => {
|
|
584
|
-
this.item.setTextId(this.#id.current);
|
|
585
|
-
});
|
|
586
|
-
$effect(() => {
|
|
587
|
-
untrack(() => {
|
|
588
|
-
const textNode = this.item.root.contentFragment?.getElementById(this.#id.current);
|
|
589
|
-
if (!textNode)
|
|
590
|
-
return;
|
|
591
|
-
this.item.onItemTextChange(textNode);
|
|
592
|
-
this.item.content.itemTextRegister(textNode, this.item.value.current, this.item.disabled.current);
|
|
593
|
-
this.item.root.onNativeOptionAdd(box.with(() => ({
|
|
594
|
-
key: this.item.value.current,
|
|
595
|
-
value: this.item.value.current,
|
|
596
|
-
disabled: this.item.disabled.current,
|
|
597
|
-
innerHTML: textNode?.textContent,
|
|
598
|
-
})));
|
|
599
|
-
});
|
|
600
|
-
});
|
|
601
|
-
$effect(() => {
|
|
602
|
-
return () => {
|
|
603
|
-
this.item.root.onNativeOptionRemove(this.nativeOption);
|
|
604
|
-
};
|
|
605
|
-
});
|
|
606
772
|
}
|
|
607
773
|
props = $derived.by(() => ({
|
|
608
774
|
id: this.#id.current,
|
|
609
|
-
|
|
775
|
+
role: "group",
|
|
776
|
+
[this.root.bitsAttrs.group]: "",
|
|
777
|
+
"aria-labelledby": this.labelNode?.id ?? undefined,
|
|
610
778
|
}));
|
|
611
779
|
}
|
|
612
|
-
class
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
this.position();
|
|
627
|
-
const contentNode = document.getElementById(this.content.id.current);
|
|
628
|
-
if (contentNode) {
|
|
629
|
-
this.contentZIndex = window.getComputedStyle(contentNode).zIndex;
|
|
630
|
-
}
|
|
631
|
-
});
|
|
780
|
+
class SelectGroupHeadingState {
|
|
781
|
+
#id;
|
|
782
|
+
#ref;
|
|
783
|
+
group;
|
|
784
|
+
constructor(props, group) {
|
|
785
|
+
this.#id = props.id;
|
|
786
|
+
this.#ref = props.ref;
|
|
787
|
+
this.group = group;
|
|
788
|
+
useRefById({
|
|
789
|
+
id: this.#id,
|
|
790
|
+
ref: this.#ref,
|
|
791
|
+
onRefChange: (node) => {
|
|
792
|
+
group.labelNode = node;
|
|
793
|
+
},
|
|
632
794
|
});
|
|
633
795
|
}
|
|
634
|
-
position = () => {
|
|
635
|
-
afterTick(() => {
|
|
636
|
-
const { selectedItemNode, selectedItemTextNode } = this.content.getSelectedItem();
|
|
637
|
-
const contentNode = this.root.contentNode;
|
|
638
|
-
const contentWrapperNode = document.getElementById(this.contentWrapperId);
|
|
639
|
-
const viewportNode = this.content.viewportNode;
|
|
640
|
-
const triggerNode = this.root.triggerNode;
|
|
641
|
-
const valueNode = document.getElementById(this.root.valueId.current);
|
|
642
|
-
if (!contentNode ||
|
|
643
|
-
!contentWrapperNode ||
|
|
644
|
-
!viewportNode ||
|
|
645
|
-
!selectedItemNode ||
|
|
646
|
-
!selectedItemTextNode ||
|
|
647
|
-
!triggerNode ||
|
|
648
|
-
!valueNode) {
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
const triggerRect = triggerNode.getBoundingClientRect();
|
|
652
|
-
// horizontal positioning
|
|
653
|
-
const contentRect = contentNode.getBoundingClientRect();
|
|
654
|
-
const valueRect = valueNode.getBoundingClientRect();
|
|
655
|
-
const itemTextRect = selectedItemTextNode.getBoundingClientRect();
|
|
656
|
-
if (this.root.dir.current === "rtl") {
|
|
657
|
-
const itemTextOffset = itemTextRect.left - contentRect.left;
|
|
658
|
-
const left = valueRect.left - itemTextOffset;
|
|
659
|
-
const leftDelta = triggerRect.left - left;
|
|
660
|
-
const minContentWidth = triggerRect.width + leftDelta;
|
|
661
|
-
const contentWidth = Math.max(minContentWidth, contentRect.width);
|
|
662
|
-
const rightEdge = window.innerWidth - CONTENT_MARGIN;
|
|
663
|
-
const clampedLeft = clamp(left, CONTENT_MARGIN, rightEdge - contentWidth);
|
|
664
|
-
contentWrapperNode.style.minWidth = `${minContentWidth}px`;
|
|
665
|
-
contentWrapperNode.style.left = `${clampedLeft}px`;
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
const itemTextOffset = contentRect.right - itemTextRect.right;
|
|
669
|
-
const right = window.innerWidth - valueRect.right - itemTextOffset;
|
|
670
|
-
const rightDelta = window.innerWidth - triggerRect.right - right;
|
|
671
|
-
const minContentWidth = triggerRect.width + rightDelta;
|
|
672
|
-
const contentWidth = Math.max(minContentWidth, contentRect.width);
|
|
673
|
-
const leftEdge = window.innerWidth - CONTENT_MARGIN;
|
|
674
|
-
const clampedRight = clamp(right, CONTENT_MARGIN, leftEdge - contentWidth);
|
|
675
|
-
contentWrapperNode.style.minWidth = `${minContentWidth}px`;
|
|
676
|
-
contentWrapperNode.style.right = `${clampedRight}px`;
|
|
677
|
-
}
|
|
678
|
-
// vertical positioning
|
|
679
|
-
const items = this.root.getCandidateNodes();
|
|
680
|
-
const availableHeight = window.innerHeight - CONTENT_MARGIN * 2;
|
|
681
|
-
const itemsHeight = viewportNode.scrollHeight;
|
|
682
|
-
const contentStyles = window.getComputedStyle(contentNode);
|
|
683
|
-
const contentBorderTopWidth = Number.parseInt(contentStyles.borderTopWidth, 10);
|
|
684
|
-
const contentPaddingTop = Number.parseInt(contentStyles.paddingTop, 10);
|
|
685
|
-
const contentBorderBottomWidth = Number.parseInt(contentStyles.borderBottomWidth, 10);
|
|
686
|
-
const contentPaddingBottom = Number.parseInt(contentStyles.paddingBottom, 10);
|
|
687
|
-
const fullContentHeight = contentBorderTopWidth +
|
|
688
|
-
contentPaddingTop +
|
|
689
|
-
itemsHeight +
|
|
690
|
-
contentPaddingBottom +
|
|
691
|
-
contentBorderBottomWidth;
|
|
692
|
-
const minContentHeight = Math.min(selectedItemNode.offsetHeight * 5, fullContentHeight);
|
|
693
|
-
const viewportStyles = window.getComputedStyle(viewportNode);
|
|
694
|
-
const viewportPaddingTop = Number.parseInt(viewportStyles.paddingTop, 10);
|
|
695
|
-
const viewportPaddingBottom = Number.parseInt(viewportStyles.paddingBottom, 10);
|
|
696
|
-
const topEdgeToTriggerMiddle = triggerRect.top + triggerRect.height / 2 - CONTENT_MARGIN;
|
|
697
|
-
const triggerMiddleToBottomEdge = availableHeight - topEdgeToTriggerMiddle;
|
|
698
|
-
const selectedItemHalfHeight = selectedItemNode.offsetHeight / 2;
|
|
699
|
-
const itemOffsetMiddle = selectedItemNode.offsetTop + selectedItemHalfHeight;
|
|
700
|
-
const contentTopToItemMiddle = contentBorderTopWidth + contentPaddingTop + itemOffsetMiddle;
|
|
701
|
-
const itemMiddleToContentBottom = fullContentHeight - contentTopToItemMiddle;
|
|
702
|
-
const willAlignWithoutTopOverflow = contentTopToItemMiddle <= topEdgeToTriggerMiddle;
|
|
703
|
-
if (willAlignWithoutTopOverflow) {
|
|
704
|
-
const isLastItem = selectedItemNode === items[items.length - 1];
|
|
705
|
-
contentWrapperNode.style.bottom = `${0}px`;
|
|
706
|
-
const viewportOffsetBottom = contentNode.clientHeight - viewportNode.offsetTop - viewportNode.offsetHeight;
|
|
707
|
-
const clampedTriggerMiddleToBottomEdge = Math.max(triggerMiddleToBottomEdge, selectedItemHalfHeight +
|
|
708
|
-
// viewport might have padding bottom, include it to avoid a scrollable viewport
|
|
709
|
-
(isLastItem ? viewportPaddingBottom : 0) +
|
|
710
|
-
viewportOffsetBottom +
|
|
711
|
-
contentBorderBottomWidth);
|
|
712
|
-
const height = contentTopToItemMiddle + clampedTriggerMiddleToBottomEdge;
|
|
713
|
-
contentWrapperNode.style.height = `${height}px`;
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
const isFirstItem = selectedItemNode === items[0];
|
|
717
|
-
contentWrapperNode.style.top = `${0}px`;
|
|
718
|
-
const clampedTopEdgeToTriggerMiddle = Math.max(topEdgeToTriggerMiddle, contentBorderTopWidth +
|
|
719
|
-
viewportNode.offsetTop +
|
|
720
|
-
// viewport might have padding top, include it to avoid a scrollable viewport
|
|
721
|
-
(isFirstItem ? viewportPaddingTop : 0) +
|
|
722
|
-
selectedItemHalfHeight);
|
|
723
|
-
const height = clampedTopEdgeToTriggerMiddle + itemMiddleToContentBottom;
|
|
724
|
-
contentWrapperNode.style.height = `${height}px`;
|
|
725
|
-
viewportNode.scrollTop =
|
|
726
|
-
contentTopToItemMiddle - topEdgeToTriggerMiddle + viewportNode.offsetTop;
|
|
727
|
-
}
|
|
728
|
-
contentWrapperNode.style.margin = `${CONTENT_MARGIN}px 0`;
|
|
729
|
-
contentWrapperNode.style.minHeight = `${minContentHeight}px`;
|
|
730
|
-
contentWrapperNode.style.maxHeight = `${availableHeight}px`;
|
|
731
|
-
this.onPlaced.current();
|
|
732
|
-
});
|
|
733
|
-
requestAnimationFrame(() => (this.shouldExpandOnScroll = true));
|
|
734
|
-
};
|
|
735
|
-
handleScrollButtonChange = (id) => {
|
|
736
|
-
afterTick(() => {
|
|
737
|
-
const node = document.getElementById(id);
|
|
738
|
-
if (!node)
|
|
739
|
-
return;
|
|
740
|
-
if (!this.shouldReposition)
|
|
741
|
-
return;
|
|
742
|
-
this.position();
|
|
743
|
-
this.content.focusSelectedItem();
|
|
744
|
-
this.shouldReposition = false;
|
|
745
|
-
});
|
|
746
|
-
};
|
|
747
|
-
wrapperProps = $derived.by(() => ({
|
|
748
|
-
id: this.contentWrapperId,
|
|
749
|
-
style: {
|
|
750
|
-
display: "flex",
|
|
751
|
-
flexDirection: "column",
|
|
752
|
-
position: "fixed",
|
|
753
|
-
zIndex: this.contentZIndex,
|
|
754
|
-
},
|
|
755
|
-
[CONTENT_WRAPPER_ATTR]: "",
|
|
756
|
-
}));
|
|
757
796
|
props = $derived.by(() => ({
|
|
758
|
-
id: this
|
|
759
|
-
|
|
760
|
-
boxSizing: "border-box",
|
|
761
|
-
maxHeight: "100%",
|
|
762
|
-
},
|
|
797
|
+
id: this.#id.current,
|
|
798
|
+
[this.group.root.bitsAttrs["group-label"]]: "",
|
|
763
799
|
}));
|
|
764
800
|
}
|
|
765
|
-
class
|
|
801
|
+
class SelectHiddenInputState {
|
|
802
|
+
#value;
|
|
766
803
|
root;
|
|
767
|
-
|
|
768
|
-
constructor(
|
|
769
|
-
this.root =
|
|
770
|
-
this
|
|
804
|
+
shouldRender = $derived.by(() => this.root.name.current !== "");
|
|
805
|
+
constructor(props, root) {
|
|
806
|
+
this.root = root;
|
|
807
|
+
this.#value = props.value;
|
|
771
808
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
},
|
|
809
|
+
#onfocus = (e) => {
|
|
810
|
+
e.preventDefault();
|
|
811
|
+
if (!this.root.isCombobox) {
|
|
812
|
+
this.root.triggerNode?.focus();
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
this.root.inputNode?.focus();
|
|
816
|
+
}
|
|
781
817
|
};
|
|
818
|
+
props = $derived.by(() => ({
|
|
819
|
+
disabled: getDisabled(this.root.disabled.current),
|
|
820
|
+
required: getRequired(this.root.required.current),
|
|
821
|
+
name: this.root.name.current,
|
|
822
|
+
value: this.#value.current,
|
|
823
|
+
style: styleToString(srOnlyStyles),
|
|
824
|
+
tabindex: -1,
|
|
825
|
+
onfocus: this.#onfocus,
|
|
826
|
+
}));
|
|
782
827
|
}
|
|
783
828
|
class SelectViewportState {
|
|
784
|
-
id;
|
|
785
|
-
ref;
|
|
829
|
+
#id;
|
|
830
|
+
#ref;
|
|
831
|
+
root;
|
|
786
832
|
content;
|
|
787
833
|
prevScrollTop = $state(0);
|
|
788
834
|
constructor(props, content) {
|
|
789
|
-
this
|
|
835
|
+
this.#id = props.id;
|
|
836
|
+
this.#ref = props.ref;
|
|
790
837
|
this.content = content;
|
|
791
|
-
this.
|
|
838
|
+
this.root = content.root;
|
|
792
839
|
useRefById({
|
|
793
|
-
id: this
|
|
794
|
-
ref: this
|
|
840
|
+
id: this.#id,
|
|
841
|
+
ref: this.#ref,
|
|
795
842
|
onRefChange: (node) => {
|
|
796
843
|
this.content.viewportNode = node;
|
|
797
844
|
},
|
|
798
|
-
deps: () => this.
|
|
845
|
+
deps: () => this.root.open.current,
|
|
799
846
|
});
|
|
800
847
|
}
|
|
801
|
-
#onscroll = (e) => {
|
|
802
|
-
afterTick(() => {
|
|
803
|
-
const viewport = e.currentTarget;
|
|
804
|
-
const shouldExpandOnScroll = this.content.alignedPositionState?.shouldExpandOnScroll ?? undefined;
|
|
805
|
-
const contentWrapper = document.getElementById(this.content.alignedPositionState?.contentWrapperId ?? "");
|
|
806
|
-
if (shouldExpandOnScroll && contentWrapper) {
|
|
807
|
-
const scrolledBy = Math.abs(this.prevScrollTop - viewport.scrollTop);
|
|
808
|
-
if (scrolledBy > 0) {
|
|
809
|
-
const availableHeight = window.innerHeight - CONTENT_MARGIN * 2;
|
|
810
|
-
const cssMinHeight = Number.parseFloat(contentWrapper.style.minHeight);
|
|
811
|
-
const cssHeight = Number.parseFloat(contentWrapper.style.height);
|
|
812
|
-
const prevHeight = Math.max(cssMinHeight, cssHeight);
|
|
813
|
-
if (prevHeight < availableHeight) {
|
|
814
|
-
const nextHeight = prevHeight + scrolledBy;
|
|
815
|
-
const clampedNextHeight = Math.min(availableHeight, nextHeight);
|
|
816
|
-
const heightDiff = nextHeight - clampedNextHeight;
|
|
817
|
-
contentWrapper.style.height = `${clampedNextHeight}px`;
|
|
818
|
-
if (contentWrapper.style.bottom === "0px") {
|
|
819
|
-
viewport.scrollTop = heightDiff > 0 ? heightDiff : 0;
|
|
820
|
-
contentWrapper.style.justifyContent = "flex-end";
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
this.prevScrollTop = viewport.scrollTop;
|
|
826
|
-
});
|
|
827
|
-
};
|
|
828
848
|
props = $derived.by(() => ({
|
|
829
|
-
id: this
|
|
849
|
+
id: this.#id.current,
|
|
830
850
|
role: "presentation",
|
|
831
|
-
[
|
|
851
|
+
[this.root.bitsAttrs.viewport]: "",
|
|
832
852
|
style: {
|
|
833
853
|
// we use position: 'relative' here on the `viewport` so that when we call
|
|
834
854
|
// `selectedItem.offsetTop` in calculations, the offset is relative to the viewport
|
|
@@ -837,47 +857,39 @@ class SelectViewportState {
|
|
|
837
857
|
flex: 1,
|
|
838
858
|
overflow: "auto",
|
|
839
859
|
},
|
|
840
|
-
onscroll: this.#onscroll,
|
|
841
860
|
}));
|
|
842
861
|
}
|
|
843
862
|
class SelectScrollButtonImplState {
|
|
844
863
|
id;
|
|
845
864
|
ref;
|
|
846
865
|
content;
|
|
847
|
-
|
|
866
|
+
root;
|
|
848
867
|
autoScrollTimer = $state(null);
|
|
849
868
|
onAutoScroll = noop;
|
|
850
869
|
mounted;
|
|
851
870
|
constructor(props, content) {
|
|
852
|
-
this.content = content;
|
|
853
871
|
this.ref = props.ref;
|
|
854
|
-
this.alignedPositionState = content.alignedPositionState;
|
|
855
872
|
this.id = props.id;
|
|
856
873
|
this.mounted = props.mounted;
|
|
874
|
+
this.content = content;
|
|
875
|
+
this.root = content.root;
|
|
857
876
|
useRefById({
|
|
858
877
|
id: this.id,
|
|
859
878
|
ref: this.ref,
|
|
860
879
|
deps: () => this.mounted.current,
|
|
861
880
|
});
|
|
862
881
|
$effect(() => {
|
|
863
|
-
if (this.mounted.current)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
activeItem?.scrollIntoView({ block: "nearest" });
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
$effect(() => {
|
|
871
|
-
return () => {
|
|
872
|
-
this.clearAutoScrollTimer();
|
|
873
|
-
};
|
|
882
|
+
if (!this.mounted.current)
|
|
883
|
+
return;
|
|
884
|
+
const activeItem = untrack(() => this.root.highlightedNode);
|
|
885
|
+
activeItem?.scrollIntoView({ block: "nearest" });
|
|
874
886
|
});
|
|
875
887
|
}
|
|
876
888
|
clearAutoScrollTimer = () => {
|
|
877
|
-
if (this.autoScrollTimer
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
889
|
+
if (this.autoScrollTimer === null)
|
|
890
|
+
return;
|
|
891
|
+
window.clearInterval(this.autoScrollTimer);
|
|
892
|
+
this.autoScrollTimer = null;
|
|
881
893
|
};
|
|
882
894
|
#onpointerdown = () => {
|
|
883
895
|
if (this.autoScrollTimer !== null)
|
|
@@ -887,7 +899,6 @@ class SelectScrollButtonImplState {
|
|
|
887
899
|
}, 50);
|
|
888
900
|
};
|
|
889
901
|
#onpointermove = () => {
|
|
890
|
-
this.content.onItemLeave?.();
|
|
891
902
|
if (this.autoScrollTimer !== null)
|
|
892
903
|
return;
|
|
893
904
|
this.autoScrollTimer = window.setInterval(() => {
|
|
@@ -899,7 +910,7 @@ class SelectScrollButtonImplState {
|
|
|
899
910
|
};
|
|
900
911
|
props = $derived.by(() => ({
|
|
901
912
|
id: this.id.current,
|
|
902
|
-
"aria-hidden":
|
|
913
|
+
"aria-hidden": getAriaHidden(true),
|
|
903
914
|
style: {
|
|
904
915
|
flexShrink: 0,
|
|
905
916
|
},
|
|
@@ -911,246 +922,168 @@ class SelectScrollButtonImplState {
|
|
|
911
922
|
class SelectScrollDownButtonState {
|
|
912
923
|
state;
|
|
913
924
|
content;
|
|
925
|
+
root;
|
|
914
926
|
canScrollDown = $state(false);
|
|
915
927
|
constructor(state) {
|
|
916
928
|
this.state = state;
|
|
917
929
|
this.content = state.content;
|
|
930
|
+
this.root = state.root;
|
|
918
931
|
this.state.onAutoScroll = this.handleAutoScroll;
|
|
919
932
|
$effect(() => {
|
|
920
933
|
const viewport = this.content.viewportNode;
|
|
921
|
-
const isPositioned = this.content.isPositioned
|
|
934
|
+
const isPositioned = this.content.isPositioned;
|
|
922
935
|
if (!viewport || !isPositioned)
|
|
923
936
|
return;
|
|
924
937
|
let cleanup = noop;
|
|
925
938
|
untrack(() => {
|
|
926
939
|
const handleScroll = () => {
|
|
927
|
-
|
|
928
|
-
|
|
940
|
+
afterTick(() => {
|
|
941
|
+
const maxScroll = viewport.scrollHeight - viewport.clientHeight;
|
|
942
|
+
const paddingTop = Number.parseInt(getComputedStyle(viewport).paddingTop, 10);
|
|
943
|
+
this.canScrollDown = Math.ceil(viewport.scrollTop) < maxScroll - paddingTop;
|
|
944
|
+
});
|
|
929
945
|
};
|
|
930
946
|
handleScroll();
|
|
931
947
|
cleanup = addEventListener(viewport, "scroll", handleScroll);
|
|
932
948
|
});
|
|
933
|
-
return
|
|
934
|
-
cleanup();
|
|
935
|
-
};
|
|
949
|
+
return cleanup;
|
|
936
950
|
});
|
|
937
951
|
$effect(() => {
|
|
938
|
-
if (this.state.mounted.current)
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
});
|
|
942
|
-
$effect(() => {
|
|
943
|
-
if (!this.state.mounted.current) {
|
|
944
|
-
this.state.clearAutoScrollTimer();
|
|
945
|
-
}
|
|
952
|
+
if (this.state.mounted.current)
|
|
953
|
+
return;
|
|
954
|
+
this.state.clearAutoScrollTimer();
|
|
946
955
|
});
|
|
947
956
|
}
|
|
948
957
|
handleAutoScroll = () => {
|
|
949
958
|
afterTick(() => {
|
|
950
959
|
const viewport = this.content.viewportNode;
|
|
951
|
-
const selectedItem = this.
|
|
952
|
-
if (!viewport || !selectedItem)
|
|
960
|
+
const selectedItem = this.root.highlightedNode;
|
|
961
|
+
if (!viewport || !selectedItem)
|
|
953
962
|
return;
|
|
954
|
-
}
|
|
955
963
|
viewport.scrollTop = viewport.scrollTop + selectedItem.offsetHeight;
|
|
956
964
|
});
|
|
957
965
|
};
|
|
958
|
-
props = $derived.by(() => ({ ...this.state.props, [
|
|
966
|
+
props = $derived.by(() => ({ ...this.state.props, [this.root.bitsAttrs["scroll-down-button"]]: "" }));
|
|
959
967
|
}
|
|
960
968
|
class SelectScrollUpButtonState {
|
|
961
969
|
state;
|
|
962
970
|
content;
|
|
971
|
+
root;
|
|
963
972
|
canScrollUp = $state(false);
|
|
964
973
|
constructor(state) {
|
|
965
974
|
this.state = state;
|
|
966
975
|
this.content = state.content;
|
|
976
|
+
this.root = state.root;
|
|
967
977
|
this.state.onAutoScroll = this.handleAutoScroll;
|
|
968
978
|
$effect(() => {
|
|
969
|
-
let cleanup = noop;
|
|
970
|
-
cleanup();
|
|
971
979
|
const viewport = this.content.viewportNode;
|
|
972
|
-
const isPositioned = this.content.isPositioned
|
|
980
|
+
const isPositioned = this.content.isPositioned;
|
|
973
981
|
if (!viewport || !isPositioned)
|
|
974
982
|
return;
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (this.state.mounted.current) {
|
|
986
|
-
this.state.alignedPositionState?.handleScrollButtonChange(this.state.id.current);
|
|
987
|
-
}
|
|
983
|
+
let cleanup = noop;
|
|
984
|
+
untrack(() => {
|
|
985
|
+
const handleScroll = () => {
|
|
986
|
+
const paddingTop = Number.parseInt(getComputedStyle(viewport).paddingTop, 10);
|
|
987
|
+
this.canScrollUp = viewport.scrollTop - paddingTop > 0;
|
|
988
|
+
};
|
|
989
|
+
handleScroll();
|
|
990
|
+
cleanup = addEventListener(viewport, "scroll", handleScroll);
|
|
991
|
+
});
|
|
992
|
+
return cleanup;
|
|
988
993
|
});
|
|
989
994
|
$effect(() => {
|
|
990
|
-
if (
|
|
991
|
-
|
|
992
|
-
|
|
995
|
+
if (this.state.mounted.current)
|
|
996
|
+
return;
|
|
997
|
+
this.state.clearAutoScrollTimer();
|
|
993
998
|
});
|
|
994
999
|
}
|
|
995
1000
|
handleAutoScroll = () => {
|
|
996
1001
|
afterTick(() => {
|
|
997
1002
|
const viewport = this.content.viewportNode;
|
|
998
|
-
const selectedItem = this.
|
|
1003
|
+
const selectedItem = this.root.highlightedNode;
|
|
999
1004
|
if (!viewport || !selectedItem)
|
|
1000
1005
|
return;
|
|
1001
1006
|
viewport.scrollTop = viewport.scrollTop - selectedItem.offsetHeight;
|
|
1002
1007
|
});
|
|
1003
1008
|
};
|
|
1004
|
-
props = $derived.by(() => ({ ...this.state.props, [
|
|
1005
|
-
}
|
|
1006
|
-
class SelectGroupState {
|
|
1007
|
-
#id;
|
|
1008
|
-
#ref;
|
|
1009
|
-
labelNode = $state(null);
|
|
1010
|
-
constructor(props) {
|
|
1011
|
-
this.#id = props.id;
|
|
1012
|
-
this.#ref = props.ref;
|
|
1013
|
-
useRefById({
|
|
1014
|
-
id: this.#id,
|
|
1015
|
-
ref: this.#ref,
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
props = $derived.by(() => ({
|
|
1019
|
-
id: this.#id.current,
|
|
1020
|
-
role: "group",
|
|
1021
|
-
"aria-labelledby": this.labelNode?.id ?? undefined,
|
|
1022
|
-
[GROUP_ATTR]: "",
|
|
1023
|
-
}));
|
|
1024
|
-
}
|
|
1025
|
-
class SelectGroupHeadingState {
|
|
1026
|
-
#id;
|
|
1027
|
-
#ref;
|
|
1028
|
-
group;
|
|
1029
|
-
constructor(props, group) {
|
|
1030
|
-
this.#ref = props.ref;
|
|
1031
|
-
this.#id = props.id;
|
|
1032
|
-
this.group = group;
|
|
1033
|
-
useRefById({
|
|
1034
|
-
id: this.#id,
|
|
1035
|
-
ref: this.#ref,
|
|
1036
|
-
onRefChange: (node) => {
|
|
1037
|
-
this.group.labelNode = node;
|
|
1038
|
-
},
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
props = $derived.by(() => ({
|
|
1042
|
-
id: this.#id.current,
|
|
1043
|
-
[GROUP_LABEL_ATTR]: "",
|
|
1044
|
-
}));
|
|
1045
|
-
}
|
|
1046
|
-
class SelectSeparatorState {
|
|
1047
|
-
#id;
|
|
1048
|
-
#ref;
|
|
1049
|
-
constructor(props) {
|
|
1050
|
-
this.#id = props.id;
|
|
1051
|
-
this.#ref = props.ref;
|
|
1052
|
-
useRefById({
|
|
1053
|
-
id: this.#id,
|
|
1054
|
-
ref: this.#ref,
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
props = $derived.by(() => ({
|
|
1058
|
-
id: this.#id.current,
|
|
1059
|
-
[SEPARATOR_ATTR]: "",
|
|
1060
|
-
"aria-hidden": getAriaHidden(true),
|
|
1061
|
-
}));
|
|
1062
|
-
}
|
|
1063
|
-
class SelectArrowState {
|
|
1064
|
-
#id;
|
|
1065
|
-
#ref;
|
|
1066
|
-
constructor(props) {
|
|
1067
|
-
this.#id = props.id;
|
|
1068
|
-
this.#ref = props.ref;
|
|
1069
|
-
useRefById({
|
|
1070
|
-
id: this.#id,
|
|
1071
|
-
ref: this.#ref,
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
props = $derived.by(() => ({
|
|
1075
|
-
id: this.#id.current,
|
|
1076
|
-
[ARROW_ATTR]: "",
|
|
1077
|
-
"aria-hidden": getAriaHidden(true),
|
|
1078
|
-
}));
|
|
1079
|
-
}
|
|
1080
|
-
class SelectIconState {
|
|
1081
|
-
#id;
|
|
1082
|
-
#ref;
|
|
1083
|
-
constructor(props) {
|
|
1084
|
-
this.#id = props.id;
|
|
1085
|
-
this.#ref = props.ref;
|
|
1086
|
-
useRefById({
|
|
1087
|
-
id: this.#id,
|
|
1088
|
-
ref: this.#ref,
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
props = $derived.by(() => ({
|
|
1092
|
-
id: this.#id.current,
|
|
1093
|
-
[ICON_ATTR]: "",
|
|
1094
|
-
"aria-hidden": getAriaHidden(true),
|
|
1095
|
-
}));
|
|
1009
|
+
props = $derived.by(() => ({ ...this.state.props, [this.root.bitsAttrs["scroll-up-button"]]: "" }));
|
|
1096
1010
|
}
|
|
1011
|
+
const [setSelectRootContext, getSelectRootContext] = createContext([
|
|
1012
|
+
"Select.Root",
|
|
1013
|
+
"Combobox.Root",
|
|
1014
|
+
]);
|
|
1015
|
+
const [setSelectGroupContext, getSelectGroupContext] = createContext([
|
|
1016
|
+
"Select.Group",
|
|
1017
|
+
"Combobox.Group",
|
|
1018
|
+
]);
|
|
1019
|
+
const [setSelectContentContext, getSelectContentContext] = createContext([
|
|
1020
|
+
"Select.Content",
|
|
1021
|
+
"Combobox.Content",
|
|
1022
|
+
]);
|
|
1097
1023
|
export function useSelectRoot(props) {
|
|
1098
|
-
|
|
1024
|
+
const { type, ...rest } = props;
|
|
1025
|
+
const rootState = type === "single"
|
|
1026
|
+
? new SelectSingleRootState(rest)
|
|
1027
|
+
: new SelectMultipleRootState(rest);
|
|
1028
|
+
return setSelectRootContext(rootState);
|
|
1099
1029
|
}
|
|
1100
|
-
export function
|
|
1101
|
-
return new
|
|
1030
|
+
export function useSelectInput(props) {
|
|
1031
|
+
return new SelectInputState(props, getSelectRootContext());
|
|
1102
1032
|
}
|
|
1103
1033
|
export function useSelectContent(props) {
|
|
1104
1034
|
return setSelectContentContext(new SelectContentState(props, getSelectRootContext()));
|
|
1105
1035
|
}
|
|
1106
|
-
export function useSelectItemAlignedPosition(props) {
|
|
1107
|
-
const contentContext = getSelectContentContext();
|
|
1108
|
-
const alignedPositionState = new SelectItemAlignedPositionState(props, contentContext);
|
|
1109
|
-
contentContext.alignedPositionState = alignedPositionState;
|
|
1110
|
-
return setSelectContentItemAlignedContext(alignedPositionState);
|
|
1111
|
-
}
|
|
1112
|
-
export function useSelectFloatingPosition() {
|
|
1113
|
-
return new SelectFloatingPositionState(getSelectContentContext());
|
|
1114
|
-
}
|
|
1115
1036
|
export function useSelectTrigger(props) {
|
|
1116
1037
|
return new SelectTriggerState(props, getSelectRootContext());
|
|
1117
1038
|
}
|
|
1118
|
-
export function
|
|
1119
|
-
return new
|
|
1039
|
+
export function useSelectComboTrigger(props) {
|
|
1040
|
+
return new SelectComboTriggerState(props, getSelectRootContext());
|
|
1120
1041
|
}
|
|
1121
1042
|
export function useSelectItem(props) {
|
|
1122
|
-
return
|
|
1123
|
-
}
|
|
1124
|
-
export function useSelectItemText(props) {
|
|
1125
|
-
return new SelectItemTextState(props, getSelectItemContext());
|
|
1043
|
+
return new SelectItemState(props, getSelectRootContext());
|
|
1126
1044
|
}
|
|
1127
1045
|
export function useSelectViewport(props) {
|
|
1128
1046
|
return new SelectViewportState(props, getSelectContentContext());
|
|
1129
1047
|
}
|
|
1130
1048
|
export function useSelectScrollUpButton(props) {
|
|
1131
|
-
|
|
1132
|
-
return new SelectScrollUpButtonState(state);
|
|
1049
|
+
return new SelectScrollUpButtonState(new SelectScrollButtonImplState(props, getSelectContentContext()));
|
|
1133
1050
|
}
|
|
1134
1051
|
export function useSelectScrollDownButton(props) {
|
|
1135
|
-
|
|
1136
|
-
return new SelectScrollDownButtonState(state);
|
|
1052
|
+
return new SelectScrollDownButtonState(new SelectScrollButtonImplState(props, getSelectContentContext()));
|
|
1137
1053
|
}
|
|
1138
1054
|
export function useSelectGroup(props) {
|
|
1139
|
-
return setSelectGroupContext(new SelectGroupState(props));
|
|
1055
|
+
return setSelectGroupContext(new SelectGroupState(props, getSelectRootContext()));
|
|
1140
1056
|
}
|
|
1141
1057
|
export function useSelectGroupHeading(props) {
|
|
1142
1058
|
return new SelectGroupHeadingState(props, getSelectGroupContext());
|
|
1143
1059
|
}
|
|
1144
|
-
export function
|
|
1145
|
-
return new
|
|
1146
|
-
}
|
|
1147
|
-
export function useSelectSeparator(props) {
|
|
1148
|
-
return new SelectSeparatorState(props);
|
|
1060
|
+
export function useSelectHiddenInput(props) {
|
|
1061
|
+
return new SelectHiddenInputState(props, getSelectRootContext());
|
|
1149
1062
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1063
|
+
////////////////////////////////////
|
|
1064
|
+
// Helpers
|
|
1065
|
+
////////////////////////////////////
|
|
1066
|
+
const selectParts = [
|
|
1067
|
+
"trigger",
|
|
1068
|
+
"content",
|
|
1069
|
+
"item",
|
|
1070
|
+
"viewport",
|
|
1071
|
+
"scroll-up-button",
|
|
1072
|
+
"scroll-down-button",
|
|
1073
|
+
"group",
|
|
1074
|
+
"group-label",
|
|
1075
|
+
"separator",
|
|
1076
|
+
"arrow",
|
|
1077
|
+
"input",
|
|
1078
|
+
"content-wrapper",
|
|
1079
|
+
"item-text",
|
|
1080
|
+
"value",
|
|
1081
|
+
];
|
|
1082
|
+
export function getSelectBitsAttrs(root) {
|
|
1083
|
+
const isCombobox = root.isCombobox;
|
|
1084
|
+
const attrObj = {};
|
|
1085
|
+
for (const part of selectParts) {
|
|
1086
|
+
attrObj[part] = isCombobox ? `data-combobox-${part}` : `data-select-${part}`;
|
|
1087
|
+
}
|
|
1088
|
+
return attrObj;
|
|
1156
1089
|
}
|