@data-slot/select 0.2.14
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/README.md +198 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +105 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @data-slot/select
|
|
2
|
+
|
|
3
|
+
A headless, accessible select component for choosing a single value from a dropdown list.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @data-slot/select
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### HTML Structure
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<div data-slot="select" data-placeholder="Choose a fruit...">
|
|
17
|
+
<button data-slot="select-trigger">
|
|
18
|
+
<span data-slot="select-value"></span>
|
|
19
|
+
<!-- Add your own chevron icon -->
|
|
20
|
+
</button>
|
|
21
|
+
<div data-slot="select-content" hidden>
|
|
22
|
+
<div data-slot="select-group">
|
|
23
|
+
<div data-slot="select-label">Fruits</div>
|
|
24
|
+
<div data-slot="select-item" data-value="apple">Apple</div>
|
|
25
|
+
<div data-slot="select-item" data-value="banana">Banana</div>
|
|
26
|
+
<div data-slot="select-item" data-value="orange">Orange</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div data-slot="select-separator"></div>
|
|
29
|
+
<div data-slot="select-item" data-value="other">Other</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### JavaScript
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
import { create, createSelect } from '@data-slot/select';
|
|
38
|
+
|
|
39
|
+
// Auto-discover and bind all selects
|
|
40
|
+
const controllers = create();
|
|
41
|
+
|
|
42
|
+
// Or bind a specific element
|
|
43
|
+
const root = document.querySelector('[data-slot="select"]');
|
|
44
|
+
const controller = createSelect(root, {
|
|
45
|
+
defaultValue: 'apple',
|
|
46
|
+
onValueChange: (value) => console.log('Selected:', value),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Programmatic control
|
|
50
|
+
controller.open();
|
|
51
|
+
controller.close();
|
|
52
|
+
controller.select('banana');
|
|
53
|
+
console.log(controller.value); // 'banana'
|
|
54
|
+
|
|
55
|
+
// Cleanup
|
|
56
|
+
controller.destroy();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Slots
|
|
60
|
+
|
|
61
|
+
| Slot | Description |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `select` | Root container |
|
|
64
|
+
| `select-trigger` | Button that opens the popup |
|
|
65
|
+
| `select-value` | Displays selected value (inside trigger) |
|
|
66
|
+
| `select-content` | Popup container for options |
|
|
67
|
+
| `select-item` | Individual selectable option |
|
|
68
|
+
| `select-group` | Groups related items |
|
|
69
|
+
| `select-label` | Label for a group |
|
|
70
|
+
| `select-separator` | Visual divider between items/groups |
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
Options can be passed via JavaScript or data attributes (JS takes precedence).
|
|
75
|
+
|
|
76
|
+
| Option | Data Attribute | Type | Default | Description |
|
|
77
|
+
|--------|---------------|------|---------|-------------|
|
|
78
|
+
| `defaultValue` | `data-default-value` | `string` | `null` | Initial selected value |
|
|
79
|
+
| `placeholder` | `data-placeholder` | `string` | `""` | Text when no value selected |
|
|
80
|
+
| `disabled` | `data-disabled` | `boolean` | `false` | Disable interaction |
|
|
81
|
+
| `required` | `data-required` | `boolean` | `false` | Form validation required |
|
|
82
|
+
| `name` | `data-name` | `string` | - | Form field name (creates hidden input) |
|
|
83
|
+
| `position` | `data-position` | `"item-aligned" \| "popper"` | `"item-aligned"` | Positioning mode (see below) |
|
|
84
|
+
| `avoidCollisions` | `data-avoid-collisions` | `boolean` | `true` | Adjust to stay in viewport |
|
|
85
|
+
| `collisionPadding` | `data-collision-padding` | `number` | `8` | Viewport edge padding (px) |
|
|
86
|
+
|
|
87
|
+
### Positioning Modes
|
|
88
|
+
|
|
89
|
+
**`item-aligned` (default)**: The popup positions itself so the selected item aligns with the trigger, similar to native `<select>` elements. The popup width matches the trigger width.
|
|
90
|
+
|
|
91
|
+
**`popper`**: The popup appears below or above the trigger like a dropdown menu. Additional options apply:
|
|
92
|
+
|
|
93
|
+
| Option | Data Attribute | Type | Default | Description |
|
|
94
|
+
|--------|---------------|------|---------|-------------|
|
|
95
|
+
| `side` | `data-side` | `"top" \| "bottom"` | `"bottom"` | Popup placement |
|
|
96
|
+
| `align` | `data-align` | `"start" \| "center" \| "end"` | `"start"` | Popup alignment |
|
|
97
|
+
| `sideOffset` | `data-side-offset` | `number` | `4` | Distance from trigger (px) |
|
|
98
|
+
| `alignOffset` | `data-align-offset` | `number` | `0` | Offset from alignment edge (px) |
|
|
99
|
+
|
|
100
|
+
### Callbacks
|
|
101
|
+
|
|
102
|
+
| Callback | Type | Description |
|
|
103
|
+
|----------|------|-------------|
|
|
104
|
+
| `onValueChange` | `(value: string \| null) => void` | Called when selection changes |
|
|
105
|
+
| `onOpenChange` | `(open: boolean) => void` | Called when popup opens/closes |
|
|
106
|
+
|
|
107
|
+
## Controller API
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface SelectController {
|
|
111
|
+
readonly value: string | null; // Current selected value
|
|
112
|
+
readonly isOpen: boolean; // Current open state
|
|
113
|
+
select(value: string): void; // Select a value
|
|
114
|
+
open(): void; // Open the popup
|
|
115
|
+
close(): void; // Close the popup
|
|
116
|
+
destroy(): void; // Cleanup
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Events
|
|
121
|
+
|
|
122
|
+
### Outbound Events (component emits)
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
root.addEventListener('select:change', (e) => {
|
|
126
|
+
console.log('Value changed:', e.detail.value);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
root.addEventListener('select:open-change', (e) => {
|
|
130
|
+
console.log('Open state:', e.detail.open);
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Inbound Events (component listens)
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
// Set value
|
|
138
|
+
root.dispatchEvent(new CustomEvent('select:set', {
|
|
139
|
+
detail: { value: 'apple' }
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
// Set open state
|
|
143
|
+
root.dispatchEvent(new CustomEvent('select:set', {
|
|
144
|
+
detail: { open: true }
|
|
145
|
+
}));
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Data Attributes (State)
|
|
149
|
+
|
|
150
|
+
The component sets these attributes to reflect state:
|
|
151
|
+
|
|
152
|
+
| Attribute | Element | Values | Description |
|
|
153
|
+
|-----------|---------|--------|-------------|
|
|
154
|
+
| `data-state` | root, trigger, content | `"open" \| "closed"` | Open state |
|
|
155
|
+
| `data-value` | root | `string` | Current selected value |
|
|
156
|
+
| `data-selected` | item | (presence) | Selected item |
|
|
157
|
+
| `data-highlighted` | item | (presence) | Keyboard-focused item |
|
|
158
|
+
| `data-placeholder` | trigger | (presence) | When showing placeholder |
|
|
159
|
+
| `data-label` | item | `string` | Display text for trigger (optional, falls back to textContent) |
|
|
160
|
+
|
|
161
|
+
## Keyboard Navigation
|
|
162
|
+
|
|
163
|
+
| Key | Action |
|
|
164
|
+
|-----|--------|
|
|
165
|
+
| `Enter`, `Space`, `ArrowDown`, `ArrowUp` | Open popup (when trigger focused) |
|
|
166
|
+
| `ArrowDown` | Move to next item |
|
|
167
|
+
| `ArrowUp` | Move to previous item |
|
|
168
|
+
| `Home` | Move to first item |
|
|
169
|
+
| `End` | Move to last item |
|
|
170
|
+
| `Enter`, `Space` | Select highlighted item |
|
|
171
|
+
| `Escape` | Close popup |
|
|
172
|
+
| `Tab` | Close popup and move focus |
|
|
173
|
+
| Type characters | Jump to matching item |
|
|
174
|
+
|
|
175
|
+
## Accessibility
|
|
176
|
+
|
|
177
|
+
- Trigger: `role="combobox"`, `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`
|
|
178
|
+
- Content: `role="listbox"`, `aria-labelledby`
|
|
179
|
+
- Item: `role="option"`, `aria-selected`, `aria-disabled`
|
|
180
|
+
- Group: `role="group"`, `aria-labelledby`
|
|
181
|
+
- Disabled items are skipped during keyboard navigation
|
|
182
|
+
|
|
183
|
+
## Form Integration
|
|
184
|
+
|
|
185
|
+
When `name` is provided, a hidden input is automatically created for form submission:
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<form>
|
|
189
|
+
<div data-slot="select" data-name="fruit">
|
|
190
|
+
<!-- ... -->
|
|
191
|
+
</div>
|
|
192
|
+
<button type="submit">Submit</button>
|
|
193
|
+
</form>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=(e,t)=>e.querySelector(`[data-slot="${t}"]`),t=(e,t)=>[...e.querySelectorAll(`[data-slot="${t}"]`)],n=(e,t)=>[...e.querySelectorAll(`[data-slot="${t}"]`)],r=new WeakMap;function i(e,t,n){if(typeof process<`u`&&process.env?.NODE_ENV===`production`)return;let i=r.get(e);i||(i=new Set,r.set(e,i)),!i.has(t)&&(i.add(t),console.warn(`[@data-slot] ${n}`))}function a(e){let t=`data-${e.replace(/([A-Z])/g,`-$1`).toLowerCase()}`,n=`data-${e}`;return t===n?[t]:[t,n]}function o(e,t){for(let n of a(t))if(e.hasAttribute(n))return e.getAttribute(n);return null}function s(e,t){return a(t).some(t=>e.hasAttribute(t))}const c=new Set([``,`true`,`1`,`yes`]),l=new Set([`false`,`0`,`no`]);function u(e,t){if(!s(e,t))return;let n=o(e,t);if(n===null)return;let r=n.toLowerCase();if(c.has(r))return!0;if(l.has(r))return!1;i(e,t,`Invalid boolean value "${n}" for data-${t}. Expected: true/false/1/0/yes/no or empty.`)}function d(e,t){let n=o(e,t);if(n===null||n===``)return;let r=Number(n);if(Number.isNaN(r)||!Number.isFinite(r)){i(e,t,`Invalid number value "${n}" for data-${t}.`);return}return r}function f(e,t){if(s(e,t))return o(e,t)??void 0}function p(e,t,n){let r=o(e,t);if(r!==null){if(n.includes(r))return r;i(e,t,`Invalid value "${r}" for data-${t}. Expected one of: ${n.join(`, `)}.`)}}let m=0;const h=(e,t)=>e.id||=`${t}-${++m}`,g=(e,t,n)=>{n===null?e.removeAttribute(`aria-${t}`):e.setAttribute(`aria-${t}`,String(n))};function _(e,t,n,r){return e.addEventListener(t,n,r),()=>e.removeEventListener(t,n,r)}const v=(e,t,n)=>e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:n})),y=[`top`,`bottom`],b=[`start`,`center`,`end`],ee=[`item-aligned`,`popper`],te={top:`bottom`,bottom:`top`};function x(n,r={}){let i=e(n,`select-trigger`),a=e(n,`select-content`),o=e(n,`select-value`);if(!i||!a)throw Error(`Select requires trigger and content slots`);let s=r.defaultValue??f(n,`defaultValue`)??null,c=r.defaultOpen??u(n,`defaultOpen`)??!1,l=r.placeholder??f(n,`placeholder`)??``,m=r.disabled??u(n,`disabled`)??!1,x=r.required??u(n,`required`)??!1,C=r.name??f(n,`name`)??null,ne=r.onValueChange,re=r.onOpenChange,w=r.position??p(n,`position`,ee)??`item-aligned`,ie=r.side??p(a,`side`,y)??p(n,`side`,y)??`bottom`,T=r.align??p(a,`align`,b)??p(n,`align`,b)??`start`,E=r.sideOffset??d(a,`sideOffset`)??d(n,`sideOffset`)??4,D=r.alignOffset??d(a,`alignOffset`)??d(n,`alignOffset`)??0,O=r.avoidCollisions??u(a,`avoidCollisions`)??u(n,`avoidCollisions`)??!0,k=r.collisionPadding??d(a,`collisionPadding`)??d(n,`collisionPadding`)??8,A=!1,j=s,M=null,N=-1,P=``,F=null,I=!1,L=[],R=[],z=[],B=new Map,V=null,H=[],U=null,W=e=>e.hasAttribute(`disabled`)||e.hasAttribute(`data-disabled`)||e.getAttribute(`aria-disabled`)===`true`,ae=h(i,`select-trigger`),oe=h(a,`select-content`);i.setAttribute(`role`,`combobox`),i.setAttribute(`aria-haspopup`,`listbox`),i.setAttribute(`aria-controls`,oe),i.hasAttribute(`type`)||i.setAttribute(`type`,`button`),a.setAttribute(`role`,`listbox`),a.setAttribute(`aria-labelledby`,ae),a.tabIndex=-1,m&&(i.setAttribute(`aria-disabled`,`true`),i.setAttribute(`data-disabled`,``)),x&&i.setAttribute(`aria-required`,`true`),C&&(U=document.createElement(`input`),U.type=`hidden`,U.name=C,U.value=j??``,n.appendChild(U));let G=()=>{R=t(a,`select-item`);for(let e of R){e.setAttribute(`role`,`option`),e.hasAttribute(`data-disabled`)||e.hasAttribute(`disabled`)?e.setAttribute(`aria-disabled`,`true`):e.removeAttribute(`aria-disabled`),e.tabIndex=-1;let t=e.dataset.value;t===j?(g(e,`selected`,!0),e.setAttribute(`data-selected`,``)):(g(e,`selected`,!1),e.removeAttribute(`data-selected`))}z=R.filter(e=>!W(e)),B=new Map(z.map((e,t)=>[e,t]));let n=t(a,`select-group`);for(let t of n){t.setAttribute(`role`,`group`);let n=e(t,`select-label`);if(n){let e=h(n,`select-label`);t.setAttribute(`aria-labelledby`,e)}}},K=(e,t,n,r)=>{let i=0,a=0;return a=e===`top`?n.top-r.height-E:n.bottom+E,i=t===`start`?n.left+D:t===`center`?n.left+n.width/2-r.width/2+D:n.right-r.width-D,{x:i,y:a}},se=(e,t)=>{let n=R.find(e=>e.dataset.value===j),r=e.left,i,a=0;if(n){let r=n.getBoundingClientRect();a=r.top-t.top,i=e.top+e.height/2-r.height/2-a}else i=e.top;return{x:r,y:i,itemOffsetTop:a}},q=()=>{let e=i.getBoundingClientRect(),t=window.innerWidth,n=window.innerHeight;a.style.minWidth=`${e.width}px`;let r=a.getBoundingClientRect(),o,s=`bottom`;if(w===`item-aligned`){let i=se(e,r);o={x:i.x,y:i.y},O&&(o.y<k?o.y=k:o.y+r.height>n-k&&(o.y=n-r.height-k),o.x<k?o.x=k:o.x+r.width>t-k&&(o.x=t-r.width-k)),s=o.y<e.top?`top`:`bottom`}else if(s=ie,o=K(s,T,e,r),O){let i=(e,t)=>e===`top`?t.y<k:t.y+r.height>n-k;if(i(s,o)){let t=te[s],n=K(t,T,e,r);i(t,n)||(s=t,o=n)}o.x<k?o.x=k:o.x+r.width>t-k&&(o.x=t-r.width-k),o.y<k?o.y=k:o.y+r.height>n-k&&(o.y=n-r.height-k)}a.style.position=`fixed`,a.style.top=`${o.y}px`,a.style.left=`${o.x}px`,a.style.margin=`0`,a.setAttribute(`data-side`,s),a.setAttribute(`data-align`,w===`item-aligned`?`center`:T)},ce=()=>{V===null&&(V=requestAnimationFrame(()=>{V=null,A&&q()}))},J=()=>{V!==null&&(cancelAnimationFrame(V),V=null),H.forEach(e=>e()),H.length=0},le=()=>{if(H.length>0)return;let e=()=>ce();window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),H.push(()=>window.removeEventListener(`resize`,e),()=>window.removeEventListener(`scroll`,e,!0));let t=new ResizeObserver(e);t.observe(i),t.observe(a),H.push(()=>t.disconnect())},Y=(e,t=!0)=>{for(let n=0;n<z.length;n++){let r=z[n];n===e?(r.setAttribute(`data-highlighted`,``),t&&r.focus()):r.removeAttribute(`data-highlighted`)}N=e},X=()=>{for(let e of R)e.removeAttribute(`data-highlighted`);N=-1},Z=e=>{n.setAttribute(`data-state`,e),i.setAttribute(`data-state`,e),a.setAttribute(`data-state`,e)},ue=()=>{if(o)if(j===null)o.textContent=l,i.setAttribute(`data-placeholder`,``);else{let e=R.find(e=>e.dataset.value===j),t=e?.dataset.label??e?.textContent?.trim()??j;o.textContent=t,i.removeAttribute(`data-placeholder`)}},Q=(e,t=!1)=>{if(A!==e&&!(m&&e)){if(e){M=document.activeElement,A=!0,g(i,`expanded`,!0),a.hidden=!1,Z(`open`),G(),I=!1;let e=z.findIndex(e=>e.dataset.value===j);e>=0?Y(e,!1):X(),le(),q(),a.focus()}else A=!1,g(i,`expanded`,!1),a.hidden=!0,Z(`closed`),X(),P=``,I=!1,J(),t?M=null:requestAnimationFrame(()=>{M&&document.contains(M)?M.focus():i&&document.contains(i)&&i.focus(),M=null});v(n,`select:open-change`,{open:A}),re?.(A)}},$=(e,t=!1)=>{if(j===e&&!t)return;let r=j;j=e,U&&(U.value=e??``),e===null?n.removeAttribute(`data-value`):n.setAttribute(`data-value`,e);for(let t of R){let n=t.dataset.value;n===e?(g(t,`selected`,!0),t.setAttribute(`data-selected`,``)):(g(t,`selected`,!1),t.removeAttribute(`data-selected`))}ue(),!t&&r!==e&&(v(n,`select:change`,{value:e}),ne?.(e))},de=e=>{if(W(e))return;let t=e.dataset.value;t!==void 0&&($(t),Q(!1))},fe=e=>{let t=z.length;if(t!==0)switch(e.key){case`ArrowDown`:e.preventDefault(),I=!0,Y(N===-1?0:(N+1)%t);break;case`ArrowUp`:e.preventDefault(),I=!0,Y(N===-1?t-1:(N-1+t)%t);break;case`Home`:e.preventDefault(),I=!0,Y(0);break;case`End`:e.preventDefault(),I=!0,Y(t-1);break;case`Enter`:case` `:e.preventDefault(),N>=0&&de(z[N]);break;case`Tab`:Q(!1,!0);break;case`Escape`:e.preventDefault(),Q(!1);break;default:e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&(e.preventDefault(),pe(e.key.toLowerCase()))}},pe=e=>{F&&clearTimeout(F),F=setTimeout(()=>{P=``},500),P+=e;let t=z.findIndex(e=>(e.textContent?.trim().toLowerCase()||``).startsWith(P));if(t===-1&&P.length===1){let n=N+1;for(let r=0;r<z.length;r++){let i=(n+r)%z.length;if((z[i].textContent?.trim().toLowerCase()||``).startsWith(e)){t=i;break}}}t!==-1&&(I=!0,Y(t))},me=e=>{if(!m)switch(e.key){case`Enter`:case` `:case`ArrowDown`:case`ArrowUp`:e.preventDefault(),Q(!0);break}};g(i,`expanded`,!1),a.hidden=!0,Z(`closed`),G(),$(j,!0),L.push(_(i,`click`,()=>{m||Q(!A)}),_(i,`keydown`,me)),L.push(_(a,`keydown`,fe),_(a,`click`,e=>{let t=e.target.closest?.(`[data-slot="select-item"]`);t&&de(t)}),_(a,`pointermove`,e=>{let t=e.target.closest?.(`[data-slot="select-item"]`);if(!(I&&(I=!1,t&&B.get(t)===N))&&t&&!W(t)){let e=B.get(t);e!==void 0&&e!==N&&Y(e,!1)}}),_(a,`pointerleave`,()=>{I||X()})),L.push(_(document,`pointerdown`,e=>{let t=e.target;A&&!n.contains(t)&&!a.contains(t)&&Q(!1)})),L.push(_(n,`select:set`,e=>{let t=e.detail;t?.value!==void 0&&$(t.value),t?.open!==void 0&&Q(t.open)}));let he={get value(){return j},get isOpen(){return A},select:e=>$(e),open:()=>Q(!0),close:()=>Q(!1),destroy:()=>{F&&clearTimeout(F),J(),L.forEach(e=>e()),L.length=0,U&&U.parentNode&&U.parentNode.removeChild(U),S.delete(n)}};return c&&Q(!0),he}const S=new WeakSet;function C(e=document){let t=[];for(let r of n(e,`select`)){if(S.has(r))continue;S.add(r),t.push(x(r))}return t}exports.create=C,exports.createSelect=x;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/** Side of the trigger to place the content */
|
|
3
|
+
type Side = "top" | "bottom";
|
|
4
|
+
/** Alignment of the content relative to the trigger */
|
|
5
|
+
type Align = "start" | "center" | "end";
|
|
6
|
+
/** Positioning mode for the content */
|
|
7
|
+
type Position = "item-aligned" | "popper";
|
|
8
|
+
interface SelectOptions {
|
|
9
|
+
/** Initial selected value */
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
/** Callback when value changes */
|
|
12
|
+
onValueChange?: (value: string | null) => void;
|
|
13
|
+
/** Initial open state */
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
/** Callback when open state changes */
|
|
16
|
+
onOpenChange?: (open: boolean) => void;
|
|
17
|
+
/** Placeholder text when no value selected */
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Disable interaction */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** Form validation required */
|
|
22
|
+
required?: boolean;
|
|
23
|
+
/** Form field name (auto-creates hidden input) */
|
|
24
|
+
name?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Positioning mode for the content.
|
|
27
|
+
* - "item-aligned": Positions content so selected item aligns with trigger (like native select)
|
|
28
|
+
* - "popper": Positions content below/above trigger like a dropdown
|
|
29
|
+
* @default "item-aligned"
|
|
30
|
+
*/
|
|
31
|
+
position?: Position;
|
|
32
|
+
/**
|
|
33
|
+
* The preferred side of the trigger to render against.
|
|
34
|
+
* Will be reversed when collisions occur and `avoidCollisions` is enabled.
|
|
35
|
+
* @default "bottom"
|
|
36
|
+
*/
|
|
37
|
+
side?: Side;
|
|
38
|
+
/**
|
|
39
|
+
* The preferred alignment against the trigger.
|
|
40
|
+
* May change when collisions occur.
|
|
41
|
+
* @default "start"
|
|
42
|
+
*/
|
|
43
|
+
align?: Align;
|
|
44
|
+
/**
|
|
45
|
+
* The distance in pixels from the trigger.
|
|
46
|
+
* @default 4
|
|
47
|
+
*/
|
|
48
|
+
sideOffset?: number;
|
|
49
|
+
/**
|
|
50
|
+
* An offset in pixels from the "start" or "end" alignment options.
|
|
51
|
+
* @default 0
|
|
52
|
+
*/
|
|
53
|
+
alignOffset?: number;
|
|
54
|
+
/**
|
|
55
|
+
* When true, overrides side/align preferences to prevent collisions with viewport edges.
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
avoidCollisions?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The padding between the content and the viewport edges when avoiding collisions.
|
|
61
|
+
* @default 8
|
|
62
|
+
*/
|
|
63
|
+
collisionPadding?: number;
|
|
64
|
+
}
|
|
65
|
+
interface SelectController {
|
|
66
|
+
/** Current selected value */
|
|
67
|
+
readonly value: string | null;
|
|
68
|
+
/** Current open state */
|
|
69
|
+
readonly isOpen: boolean;
|
|
70
|
+
/** Select a value programmatically */
|
|
71
|
+
select(value: string): void;
|
|
72
|
+
/** Open the popup */
|
|
73
|
+
open(): void;
|
|
74
|
+
/** Close the popup */
|
|
75
|
+
close(): void;
|
|
76
|
+
/** Cleanup all event listeners */
|
|
77
|
+
destroy(): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a select controller for a root element.
|
|
81
|
+
*
|
|
82
|
+
* Supports Radix-compatible positioning props for precise placement:
|
|
83
|
+
* - `side`: "top" | "bottom" (default: "bottom")
|
|
84
|
+
* - `align`: "start" | "center" | "end" (default: "start")
|
|
85
|
+
* - `sideOffset`: distance from trigger in px (default: 4)
|
|
86
|
+
* - `alignOffset`: offset from alignment edge in px (default: 0)
|
|
87
|
+
* - `avoidCollisions`: flip/shift to stay in viewport (default: true)
|
|
88
|
+
* - `collisionPadding`: viewport edge padding in px (default: 8)
|
|
89
|
+
*
|
|
90
|
+
* ## Events
|
|
91
|
+
* - **Outbound** `select:change` (on root): Fires when value changes.
|
|
92
|
+
* `event.detail: { value: string | null }`
|
|
93
|
+
* - **Outbound** `select:open-change` (on root): Fires when popup opens/closes.
|
|
94
|
+
* `event.detail: { open: boolean }`
|
|
95
|
+
* - **Inbound** `select:set` (on root): Set value or open state.
|
|
96
|
+
* `event.detail: { value: string } | { open: boolean }`
|
|
97
|
+
*/
|
|
98
|
+
declare function createSelect(root: Element, options?: SelectOptions): SelectController;
|
|
99
|
+
/**
|
|
100
|
+
* Find and bind all select components in a scope
|
|
101
|
+
* Returns array of controllers for programmatic access
|
|
102
|
+
*/
|
|
103
|
+
declare function create(scope?: ParentNode): SelectController[];
|
|
104
|
+
//#endregion
|
|
105
|
+
export { Align, Position, SelectController, SelectOptions, Side, create, createSelect };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/** Side of the trigger to place the content */
|
|
3
|
+
type Side = "top" | "bottom";
|
|
4
|
+
/** Alignment of the content relative to the trigger */
|
|
5
|
+
type Align = "start" | "center" | "end";
|
|
6
|
+
/** Positioning mode for the content */
|
|
7
|
+
type Position = "item-aligned" | "popper";
|
|
8
|
+
interface SelectOptions {
|
|
9
|
+
/** Initial selected value */
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
/** Callback when value changes */
|
|
12
|
+
onValueChange?: (value: string | null) => void;
|
|
13
|
+
/** Initial open state */
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
/** Callback when open state changes */
|
|
16
|
+
onOpenChange?: (open: boolean) => void;
|
|
17
|
+
/** Placeholder text when no value selected */
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Disable interaction */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** Form validation required */
|
|
22
|
+
required?: boolean;
|
|
23
|
+
/** Form field name (auto-creates hidden input) */
|
|
24
|
+
name?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Positioning mode for the content.
|
|
27
|
+
* - "item-aligned": Positions content so selected item aligns with trigger (like native select)
|
|
28
|
+
* - "popper": Positions content below/above trigger like a dropdown
|
|
29
|
+
* @default "item-aligned"
|
|
30
|
+
*/
|
|
31
|
+
position?: Position;
|
|
32
|
+
/**
|
|
33
|
+
* The preferred side of the trigger to render against.
|
|
34
|
+
* Will be reversed when collisions occur and `avoidCollisions` is enabled.
|
|
35
|
+
* @default "bottom"
|
|
36
|
+
*/
|
|
37
|
+
side?: Side;
|
|
38
|
+
/**
|
|
39
|
+
* The preferred alignment against the trigger.
|
|
40
|
+
* May change when collisions occur.
|
|
41
|
+
* @default "start"
|
|
42
|
+
*/
|
|
43
|
+
align?: Align;
|
|
44
|
+
/**
|
|
45
|
+
* The distance in pixels from the trigger.
|
|
46
|
+
* @default 4
|
|
47
|
+
*/
|
|
48
|
+
sideOffset?: number;
|
|
49
|
+
/**
|
|
50
|
+
* An offset in pixels from the "start" or "end" alignment options.
|
|
51
|
+
* @default 0
|
|
52
|
+
*/
|
|
53
|
+
alignOffset?: number;
|
|
54
|
+
/**
|
|
55
|
+
* When true, overrides side/align preferences to prevent collisions with viewport edges.
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
avoidCollisions?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The padding between the content and the viewport edges when avoiding collisions.
|
|
61
|
+
* @default 8
|
|
62
|
+
*/
|
|
63
|
+
collisionPadding?: number;
|
|
64
|
+
}
|
|
65
|
+
interface SelectController {
|
|
66
|
+
/** Current selected value */
|
|
67
|
+
readonly value: string | null;
|
|
68
|
+
/** Current open state */
|
|
69
|
+
readonly isOpen: boolean;
|
|
70
|
+
/** Select a value programmatically */
|
|
71
|
+
select(value: string): void;
|
|
72
|
+
/** Open the popup */
|
|
73
|
+
open(): void;
|
|
74
|
+
/** Close the popup */
|
|
75
|
+
close(): void;
|
|
76
|
+
/** Cleanup all event listeners */
|
|
77
|
+
destroy(): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a select controller for a root element.
|
|
81
|
+
*
|
|
82
|
+
* Supports Radix-compatible positioning props for precise placement:
|
|
83
|
+
* - `side`: "top" | "bottom" (default: "bottom")
|
|
84
|
+
* - `align`: "start" | "center" | "end" (default: "start")
|
|
85
|
+
* - `sideOffset`: distance from trigger in px (default: 4)
|
|
86
|
+
* - `alignOffset`: offset from alignment edge in px (default: 0)
|
|
87
|
+
* - `avoidCollisions`: flip/shift to stay in viewport (default: true)
|
|
88
|
+
* - `collisionPadding`: viewport edge padding in px (default: 8)
|
|
89
|
+
*
|
|
90
|
+
* ## Events
|
|
91
|
+
* - **Outbound** `select:change` (on root): Fires when value changes.
|
|
92
|
+
* `event.detail: { value: string | null }`
|
|
93
|
+
* - **Outbound** `select:open-change` (on root): Fires when popup opens/closes.
|
|
94
|
+
* `event.detail: { open: boolean }`
|
|
95
|
+
* - **Inbound** `select:set` (on root): Set value or open state.
|
|
96
|
+
* `event.detail: { value: string } | { open: boolean }`
|
|
97
|
+
*/
|
|
98
|
+
declare function createSelect(root: Element, options?: SelectOptions): SelectController;
|
|
99
|
+
/**
|
|
100
|
+
* Find and bind all select components in a scope
|
|
101
|
+
* Returns array of controllers for programmatic access
|
|
102
|
+
*/
|
|
103
|
+
declare function create(scope?: ParentNode): SelectController[];
|
|
104
|
+
//#endregion
|
|
105
|
+
export { Align, Position, SelectController, SelectOptions, Side, create, createSelect };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=(e,t)=>e.querySelector(`[data-slot="${t}"]`),t=(e,t)=>[...e.querySelectorAll(`[data-slot="${t}"]`)],n=(e,t)=>[...e.querySelectorAll(`[data-slot="${t}"]`)],r=new WeakMap;function i(e,t,n){if(typeof process<`u`&&process.env?.NODE_ENV===`production`)return;let i=r.get(e);i||(i=new Set,r.set(e,i)),!i.has(t)&&(i.add(t),console.warn(`[@data-slot] ${n}`))}function a(e){let t=`data-${e.replace(/([A-Z])/g,`-$1`).toLowerCase()}`,n=`data-${e}`;return t===n?[t]:[t,n]}function o(e,t){for(let n of a(t))if(e.hasAttribute(n))return e.getAttribute(n);return null}function s(e,t){return a(t).some(t=>e.hasAttribute(t))}const c=new Set([``,`true`,`1`,`yes`]),l=new Set([`false`,`0`,`no`]);function u(e,t){if(!s(e,t))return;let n=o(e,t);if(n===null)return;let r=n.toLowerCase();if(c.has(r))return!0;if(l.has(r))return!1;i(e,t,`Invalid boolean value "${n}" for data-${t}. Expected: true/false/1/0/yes/no or empty.`)}function d(e,t){let n=o(e,t);if(n===null||n===``)return;let r=Number(n);if(Number.isNaN(r)||!Number.isFinite(r)){i(e,t,`Invalid number value "${n}" for data-${t}.`);return}return r}function f(e,t){if(s(e,t))return o(e,t)??void 0}function p(e,t,n){let r=o(e,t);if(r!==null){if(n.includes(r))return r;i(e,t,`Invalid value "${r}" for data-${t}. Expected one of: ${n.join(`, `)}.`)}}let m=0;const h=(e,t)=>e.id||=`${t}-${++m}`,g=(e,t,n)=>{n===null?e.removeAttribute(`aria-${t}`):e.setAttribute(`aria-${t}`,String(n))};function _(e,t,n,r){return e.addEventListener(t,n,r),()=>e.removeEventListener(t,n,r)}const v=(e,t,n)=>e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:n})),y=[`top`,`bottom`],b=[`start`,`center`,`end`],ee=[`item-aligned`,`popper`],te={top:`bottom`,bottom:`top`};function x(n,r={}){let i=e(n,`select-trigger`),a=e(n,`select-content`),o=e(n,`select-value`);if(!i||!a)throw Error(`Select requires trigger and content slots`);let s=r.defaultValue??f(n,`defaultValue`)??null,c=r.defaultOpen??u(n,`defaultOpen`)??!1,l=r.placeholder??f(n,`placeholder`)??``,m=r.disabled??u(n,`disabled`)??!1,x=r.required??u(n,`required`)??!1,C=r.name??f(n,`name`)??null,ne=r.onValueChange,re=r.onOpenChange,w=r.position??p(n,`position`,ee)??`item-aligned`,ie=r.side??p(a,`side`,y)??p(n,`side`,y)??`bottom`,T=r.align??p(a,`align`,b)??p(n,`align`,b)??`start`,E=r.sideOffset??d(a,`sideOffset`)??d(n,`sideOffset`)??4,D=r.alignOffset??d(a,`alignOffset`)??d(n,`alignOffset`)??0,O=r.avoidCollisions??u(a,`avoidCollisions`)??u(n,`avoidCollisions`)??!0,k=r.collisionPadding??d(a,`collisionPadding`)??d(n,`collisionPadding`)??8,A=!1,j=s,M=null,N=-1,P=``,F=null,I=!1,L=[],R=[],z=[],B=new Map,V=null,H=[],U=null,W=e=>e.hasAttribute(`disabled`)||e.hasAttribute(`data-disabled`)||e.getAttribute(`aria-disabled`)===`true`,ae=h(i,`select-trigger`),oe=h(a,`select-content`);i.setAttribute(`role`,`combobox`),i.setAttribute(`aria-haspopup`,`listbox`),i.setAttribute(`aria-controls`,oe),i.hasAttribute(`type`)||i.setAttribute(`type`,`button`),a.setAttribute(`role`,`listbox`),a.setAttribute(`aria-labelledby`,ae),a.tabIndex=-1,m&&(i.setAttribute(`aria-disabled`,`true`),i.setAttribute(`data-disabled`,``)),x&&i.setAttribute(`aria-required`,`true`),C&&(U=document.createElement(`input`),U.type=`hidden`,U.name=C,U.value=j??``,n.appendChild(U));let G=()=>{R=t(a,`select-item`);for(let e of R){e.setAttribute(`role`,`option`),e.hasAttribute(`data-disabled`)||e.hasAttribute(`disabled`)?e.setAttribute(`aria-disabled`,`true`):e.removeAttribute(`aria-disabled`),e.tabIndex=-1;let t=e.dataset.value;t===j?(g(e,`selected`,!0),e.setAttribute(`data-selected`,``)):(g(e,`selected`,!1),e.removeAttribute(`data-selected`))}z=R.filter(e=>!W(e)),B=new Map(z.map((e,t)=>[e,t]));let n=t(a,`select-group`);for(let t of n){t.setAttribute(`role`,`group`);let n=e(t,`select-label`);if(n){let e=h(n,`select-label`);t.setAttribute(`aria-labelledby`,e)}}},K=(e,t,n,r)=>{let i=0,a=0;return a=e===`top`?n.top-r.height-E:n.bottom+E,i=t===`start`?n.left+D:t===`center`?n.left+n.width/2-r.width/2+D:n.right-r.width-D,{x:i,y:a}},se=(e,t)=>{let n=R.find(e=>e.dataset.value===j),r=e.left,i,a=0;if(n){let r=n.getBoundingClientRect();a=r.top-t.top,i=e.top+e.height/2-r.height/2-a}else i=e.top;return{x:r,y:i,itemOffsetTop:a}},q=()=>{let e=i.getBoundingClientRect(),t=window.innerWidth,n=window.innerHeight;a.style.minWidth=`${e.width}px`;let r=a.getBoundingClientRect(),o,s=`bottom`;if(w===`item-aligned`){let i=se(e,r);o={x:i.x,y:i.y},O&&(o.y<k?o.y=k:o.y+r.height>n-k&&(o.y=n-r.height-k),o.x<k?o.x=k:o.x+r.width>t-k&&(o.x=t-r.width-k)),s=o.y<e.top?`top`:`bottom`}else if(s=ie,o=K(s,T,e,r),O){let i=(e,t)=>e===`top`?t.y<k:t.y+r.height>n-k;if(i(s,o)){let t=te[s],n=K(t,T,e,r);i(t,n)||(s=t,o=n)}o.x<k?o.x=k:o.x+r.width>t-k&&(o.x=t-r.width-k),o.y<k?o.y=k:o.y+r.height>n-k&&(o.y=n-r.height-k)}a.style.position=`fixed`,a.style.top=`${o.y}px`,a.style.left=`${o.x}px`,a.style.margin=`0`,a.setAttribute(`data-side`,s),a.setAttribute(`data-align`,w===`item-aligned`?`center`:T)},ce=()=>{V===null&&(V=requestAnimationFrame(()=>{V=null,A&&q()}))},J=()=>{V!==null&&(cancelAnimationFrame(V),V=null),H.forEach(e=>e()),H.length=0},le=()=>{if(H.length>0)return;let e=()=>ce();window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),H.push(()=>window.removeEventListener(`resize`,e),()=>window.removeEventListener(`scroll`,e,!0));let t=new ResizeObserver(e);t.observe(i),t.observe(a),H.push(()=>t.disconnect())},Y=(e,t=!0)=>{for(let n=0;n<z.length;n++){let r=z[n];n===e?(r.setAttribute(`data-highlighted`,``),t&&r.focus()):r.removeAttribute(`data-highlighted`)}N=e},X=()=>{for(let e of R)e.removeAttribute(`data-highlighted`);N=-1},Z=e=>{n.setAttribute(`data-state`,e),i.setAttribute(`data-state`,e),a.setAttribute(`data-state`,e)},ue=()=>{if(o)if(j===null)o.textContent=l,i.setAttribute(`data-placeholder`,``);else{let e=R.find(e=>e.dataset.value===j),t=e?.dataset.label??e?.textContent?.trim()??j;o.textContent=t,i.removeAttribute(`data-placeholder`)}},Q=(e,t=!1)=>{if(A!==e&&!(m&&e)){if(e){M=document.activeElement,A=!0,g(i,`expanded`,!0),a.hidden=!1,Z(`open`),G(),I=!1;let e=z.findIndex(e=>e.dataset.value===j);e>=0?Y(e,!1):X(),le(),q(),a.focus()}else A=!1,g(i,`expanded`,!1),a.hidden=!0,Z(`closed`),X(),P=``,I=!1,J(),t?M=null:requestAnimationFrame(()=>{M&&document.contains(M)?M.focus():i&&document.contains(i)&&i.focus(),M=null});v(n,`select:open-change`,{open:A}),re?.(A)}},$=(e,t=!1)=>{if(j===e&&!t)return;let r=j;j=e,U&&(U.value=e??``),e===null?n.removeAttribute(`data-value`):n.setAttribute(`data-value`,e);for(let t of R){let n=t.dataset.value;n===e?(g(t,`selected`,!0),t.setAttribute(`data-selected`,``)):(g(t,`selected`,!1),t.removeAttribute(`data-selected`))}ue(),!t&&r!==e&&(v(n,`select:change`,{value:e}),ne?.(e))},de=e=>{if(W(e))return;let t=e.dataset.value;t!==void 0&&($(t),Q(!1))},fe=e=>{let t=z.length;if(t!==0)switch(e.key){case`ArrowDown`:e.preventDefault(),I=!0,Y(N===-1?0:(N+1)%t);break;case`ArrowUp`:e.preventDefault(),I=!0,Y(N===-1?t-1:(N-1+t)%t);break;case`Home`:e.preventDefault(),I=!0,Y(0);break;case`End`:e.preventDefault(),I=!0,Y(t-1);break;case`Enter`:case` `:e.preventDefault(),N>=0&&de(z[N]);break;case`Tab`:Q(!1,!0);break;case`Escape`:e.preventDefault(),Q(!1);break;default:e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&(e.preventDefault(),pe(e.key.toLowerCase()))}},pe=e=>{F&&clearTimeout(F),F=setTimeout(()=>{P=``},500),P+=e;let t=z.findIndex(e=>(e.textContent?.trim().toLowerCase()||``).startsWith(P));if(t===-1&&P.length===1){let n=N+1;for(let r=0;r<z.length;r++){let i=(n+r)%z.length;if((z[i].textContent?.trim().toLowerCase()||``).startsWith(e)){t=i;break}}}t!==-1&&(I=!0,Y(t))},me=e=>{if(!m)switch(e.key){case`Enter`:case` `:case`ArrowDown`:case`ArrowUp`:e.preventDefault(),Q(!0);break}};g(i,`expanded`,!1),a.hidden=!0,Z(`closed`),G(),$(j,!0),L.push(_(i,`click`,()=>{m||Q(!A)}),_(i,`keydown`,me)),L.push(_(a,`keydown`,fe),_(a,`click`,e=>{let t=e.target.closest?.(`[data-slot="select-item"]`);t&&de(t)}),_(a,`pointermove`,e=>{let t=e.target.closest?.(`[data-slot="select-item"]`);if(!(I&&(I=!1,t&&B.get(t)===N))&&t&&!W(t)){let e=B.get(t);e!==void 0&&e!==N&&Y(e,!1)}}),_(a,`pointerleave`,()=>{I||X()})),L.push(_(document,`pointerdown`,e=>{let t=e.target;A&&!n.contains(t)&&!a.contains(t)&&Q(!1)})),L.push(_(n,`select:set`,e=>{let t=e.detail;t?.value!==void 0&&$(t.value),t?.open!==void 0&&Q(t.open)}));let he={get value(){return j},get isOpen(){return A},select:e=>$(e),open:()=>Q(!0),close:()=>Q(!1),destroy:()=>{F&&clearTimeout(F),J(),L.forEach(e=>e()),L.length=0,U&&U.parentNode&&U.parentNode.removeChild(U),S.delete(n)}};return c&&Q(!0),he}const S=new WeakSet;function C(e=document){let t=[];for(let r of n(e,`select`)){if(S.has(r))continue;S.add(r),t.push(x(r))}return t}export{C as create,x as createSelect};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-slot/select",
|
|
3
|
+
"version": "0.2.14",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsdown"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@data-slot/core": "workspace:*"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/bejamas/data-slot",
|
|
33
|
+
"directory": "packages/select"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"headless",
|
|
37
|
+
"ui",
|
|
38
|
+
"select",
|
|
39
|
+
"dropdown",
|
|
40
|
+
"combobox",
|
|
41
|
+
"vanilla",
|
|
42
|
+
"data-slot"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|