@data-slot/combobox 0.2.30
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 +207 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @data-slot/combobox
|
|
2
|
+
|
|
3
|
+
A headless, accessible combobox component with autocomplete/typeahead filtering.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @data-slot/combobox
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### HTML Structure
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<div data-slot="combobox" data-placeholder="Search fruits...">
|
|
17
|
+
<input data-slot="combobox-input" />
|
|
18
|
+
<button data-slot="combobox-trigger">▼</button>
|
|
19
|
+
<div data-slot="combobox-content" hidden>
|
|
20
|
+
<div data-slot="combobox-list">
|
|
21
|
+
<div data-slot="combobox-empty">No results found</div>
|
|
22
|
+
<div data-slot="combobox-group">
|
|
23
|
+
<div data-slot="combobox-label">Fruits</div>
|
|
24
|
+
<div data-slot="combobox-item" data-value="apple">Apple</div>
|
|
25
|
+
<div data-slot="combobox-item" data-value="banana">Banana</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div data-slot="combobox-separator"></div>
|
|
28
|
+
<div data-slot="combobox-item" data-value="other">Other</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### JavaScript
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
import { create, createCombobox } from '@data-slot/combobox';
|
|
38
|
+
|
|
39
|
+
// Auto-discover and bind all comboboxes
|
|
40
|
+
const controllers = create();
|
|
41
|
+
|
|
42
|
+
// Or bind a specific element
|
|
43
|
+
const root = document.querySelector('[data-slot="combobox"]');
|
|
44
|
+
const controller = createCombobox(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
|
+
controller.clear();
|
|
54
|
+
console.log(controller.value); // 'banana'
|
|
55
|
+
|
|
56
|
+
// Cleanup
|
|
57
|
+
controller.destroy();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Slots
|
|
61
|
+
|
|
62
|
+
| Slot | Description |
|
|
63
|
+
|------|-------------|
|
|
64
|
+
| `combobox` | Root container |
|
|
65
|
+
| `combobox-input` | Text input for filtering |
|
|
66
|
+
| `combobox-trigger` | Optional button that toggles the popup |
|
|
67
|
+
| `combobox-content` | Popup container |
|
|
68
|
+
| `combobox-list` | Scrollable list wrapper |
|
|
69
|
+
| `combobox-item` | Individual selectable option |
|
|
70
|
+
| `combobox-group` | Groups related items |
|
|
71
|
+
| `combobox-label` | Group label (inside a `combobox-group`) |
|
|
72
|
+
| `combobox-separator` | Visual divider between items/groups |
|
|
73
|
+
| `combobox-empty` | Message shown when no items match filter |
|
|
74
|
+
|
|
75
|
+
### Native Label Support
|
|
76
|
+
|
|
77
|
+
Use a standard HTML `<label for="...">` element to label the combobox. The `for` attribute should match the `id` on the input. Clicking the label focuses the input, and `aria-labelledby` is set automatically.
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<label for="fruit-input">Choose a fruit</label>
|
|
81
|
+
<div data-slot="combobox">
|
|
82
|
+
<input data-slot="combobox-input" id="fruit-input" />
|
|
83
|
+
<div data-slot="combobox-content" hidden>
|
|
84
|
+
<div data-slot="combobox-list">
|
|
85
|
+
<div data-slot="combobox-item" data-value="apple">Apple</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Options
|
|
92
|
+
|
|
93
|
+
Options can be passed via JavaScript or data attributes (JS takes precedence).
|
|
94
|
+
|
|
95
|
+
| Option | Data Attribute | Type | Default | Description |
|
|
96
|
+
|--------|---------------|------|---------|-------------|
|
|
97
|
+
| `defaultValue` | `data-default-value` | `string` | `null` | Initial selected value |
|
|
98
|
+
| `placeholder` | `data-placeholder` | `string` | `""` | Input placeholder text |
|
|
99
|
+
| `disabled` | `data-disabled` | `boolean` | `false` | Disable interaction |
|
|
100
|
+
| `required` | `data-required` | `boolean` | `false` | Form validation required |
|
|
101
|
+
| `name` | `data-name` | `string` | - | Form field name (creates hidden input) |
|
|
102
|
+
| `openOnFocus` | `data-open-on-focus` | `boolean` | `true` | Open popup when input is focused |
|
|
103
|
+
| `autoHighlight` | `data-auto-highlight` | `boolean` | `true` | Auto-highlight first visible item when filtering |
|
|
104
|
+
| `filter` | - | `function` | substring | Custom filter function |
|
|
105
|
+
| `side` | `data-side` | `"top" \| "bottom"` | `"bottom"` | Popup placement |
|
|
106
|
+
| `align` | `data-align` | `"start" \| "center" \| "end"` | `"start"` | Popup alignment |
|
|
107
|
+
| `sideOffset` | `data-side-offset` | `number` | `4` | Distance from input (px) |
|
|
108
|
+
| `alignOffset` | `data-align-offset` | `number` | `0` | Offset from alignment edge (px) |
|
|
109
|
+
| `avoidCollisions` | `data-avoid-collisions` | `boolean` | `true` | Adjust to stay in viewport |
|
|
110
|
+
| `collisionPadding` | `data-collision-padding` | `number` | `8` | Viewport edge padding (px) |
|
|
111
|
+
|
|
112
|
+
### Callbacks
|
|
113
|
+
|
|
114
|
+
| Callback | Type | Description |
|
|
115
|
+
|----------|------|-------------|
|
|
116
|
+
| `onValueChange` | `(value: string \| null) => void` | Called when selection changes |
|
|
117
|
+
| `onOpenChange` | `(open: boolean) => void` | Called when popup opens/closes |
|
|
118
|
+
| `onInputValueChange` | `(inputValue: string) => void` | Called when user types in the input |
|
|
119
|
+
|
|
120
|
+
## Controller API
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
interface ComboboxController {
|
|
124
|
+
readonly value: string | null; // Current selected value
|
|
125
|
+
readonly inputValue: string; // Current input text
|
|
126
|
+
readonly isOpen: boolean; // Current open state
|
|
127
|
+
select(value: string): void; // Select a value
|
|
128
|
+
clear(): void; // Clear selection
|
|
129
|
+
open(): void; // Open the popup
|
|
130
|
+
close(): void; // Close the popup
|
|
131
|
+
destroy(): void; // Cleanup
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Events
|
|
136
|
+
|
|
137
|
+
### Outbound Events (component emits)
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
root.addEventListener('combobox:change', (e) => {
|
|
141
|
+
console.log('Value changed:', e.detail.value);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
root.addEventListener('combobox:open-change', (e) => {
|
|
145
|
+
console.log('Open state:', e.detail.open);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
root.addEventListener('combobox:input-change', (e) => {
|
|
149
|
+
console.log('Input changed:', e.detail.inputValue);
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Inbound Events (component listens)
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// Set value
|
|
157
|
+
root.dispatchEvent(new CustomEvent('combobox:set', {
|
|
158
|
+
detail: { value: 'apple' }
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
// Set open state
|
|
162
|
+
root.dispatchEvent(new CustomEvent('combobox:set', {
|
|
163
|
+
detail: { open: true }
|
|
164
|
+
}));
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Keyboard Navigation
|
|
168
|
+
|
|
169
|
+
| Key | Action |
|
|
170
|
+
|-----|--------|
|
|
171
|
+
| `ArrowDown` | Open popup (when closed); move to next visible item |
|
|
172
|
+
| `ArrowUp` | Open popup (when closed); move to previous visible item |
|
|
173
|
+
| `Home` | Move to first visible item |
|
|
174
|
+
| `End` | Move to last visible item |
|
|
175
|
+
| `Enter` | Select highlighted item |
|
|
176
|
+
| `Escape` | Close popup, restore input to committed value |
|
|
177
|
+
| `Tab` | Close popup, restore input, allow normal tab flow |
|
|
178
|
+
|
|
179
|
+
## Accessibility
|
|
180
|
+
|
|
181
|
+
- Input: `role="combobox"`, `aria-expanded`, `aria-controls`, `aria-activedescendant`, `aria-autocomplete="list"`
|
|
182
|
+
- List: `role="listbox"`, `aria-labelledby`
|
|
183
|
+
- Item: `role="option"`, `aria-selected`, `aria-disabled`
|
|
184
|
+
- Group: `role="group"`, `aria-labelledby`
|
|
185
|
+
- Disabled items are skipped during keyboard navigation
|
|
186
|
+
|
|
187
|
+
## Form Integration
|
|
188
|
+
|
|
189
|
+
When `name` is provided, a hidden input is automatically created for form submission:
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<form>
|
|
193
|
+
<div data-slot="combobox" data-name="fruit">
|
|
194
|
+
<input data-slot="combobox-input" />
|
|
195
|
+
<div data-slot="combobox-content" hidden>
|
|
196
|
+
<div data-slot="combobox-list">
|
|
197
|
+
<div data-slot="combobox-item" data-value="apple">Apple</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
<button type="submit">Submit</button>
|
|
202
|
+
</form>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`@data-slot/core`)),l=[`top`,`bottom`],u=[`start`,`center`,`end`];function d(e,t={}){let n=(0,c.getPart)(e,`combobox-input`),r=(0,c.getPart)(e,`combobox-content`),i=(0,c.getPart)(e,`combobox-list`)??(0,c.getPart)(r??e,`combobox-list`),a=(0,c.getPart)(e,`combobox-trigger`),o=(0,c.getPart)(i??r??e,`combobox-empty`);if(!n||!r)throw Error(`Combobox requires combobox-input and combobox-content slots`);let s=t.defaultValue??(0,c.getDataString)(e,`defaultValue`)??null,d=t.defaultOpen??(0,c.getDataBool)(e,`defaultOpen`)??!1,p=t.placeholder??(0,c.getDataString)(e,`placeholder`)??``,m=t.disabled??(0,c.getDataBool)(e,`disabled`)??!1,h=t.required??(0,c.getDataBool)(e,`required`)??!1,g=t.name??(0,c.getDataString)(e,`name`)??null,_=t.openOnFocus??(0,c.getDataBool)(e,`openOnFocus`)??!0,v=t.autoHighlight??(0,c.getDataBool)(e,`autoHighlight`)??!0,ee=t.filter??null,te=t.onValueChange,y=t.onOpenChange,ne=t.onInputValueChange,re=t.side??(0,c.getDataEnum)(r,`side`,l)??(0,c.getDataEnum)(e,`side`,l)??`bottom`,ie=t.align??(0,c.getDataEnum)(r,`align`,u)??(0,c.getDataEnum)(e,`align`,u)??`start`,b=t.sideOffset??(0,c.getDataNumber)(r,`sideOffset`)??(0,c.getDataNumber)(e,`sideOffset`)??4,ae=t.alignOffset??(0,c.getDataNumber)(r,`alignOffset`)??(0,c.getDataNumber)(e,`alignOffset`)??0,oe=t.avoidCollisions??(0,c.getDataBool)(r,`avoidCollisions`)??(0,c.getDataBool)(e,`avoidCollisions`)??!0,x=t.collisionPadding??(0,c.getDataNumber)(r,`collisionPadding`)??(0,c.getDataNumber)(e,`collisionPadding`)??8,S=!1,C=s,w=-1,T=!1,E=[],D=[],O=[],k=[],A=new Map,j=null,M=(0,c.createPortalLifecycle)({content:r,root:e}),N=e=>e.hasAttribute(`disabled`)||e.hasAttribute(`data-disabled`)||e.getAttribute(`aria-disabled`)===`true`,P=e=>{if(e.dataset.label)return e.dataset.label;let t=``;for(let n of e.childNodes)n.nodeType===Node.TEXT_NODE&&(t+=n.textContent);let n=t.trim();return n||(e.textContent?.trim()??``)},F=e=>e.hasAttribute(`data-value`)?e.getAttribute(`data-value`):void 0,I=e=>{if(e===null)return``;let t=i??r,n=(0,c.getParts)(t,`combobox-item`),a=n.find(t=>F(t)===e);return a?P(a):``},se=(0,c.ensureId)(n,`combobox-input`),L=i??r,ce=(0,c.ensureId)(L,`combobox-list`);n.setAttribute(`role`,`combobox`),n.setAttribute(`aria-autocomplete`,`list`),n.setAttribute(`autocomplete`,`off`),n.setAttribute(`aria-controls`,ce),i?i.setAttribute(`role`,`listbox`):r.setAttribute(`role`,`listbox`),a&&(a.hasAttribute(`type`)||a.setAttribute(`type`,`button`),a.tabIndex=-1,a.setAttribute(`aria-label`,`Toggle`));let R=document.querySelector(`label[for="${CSS.escape(se)}"]`);if(R){let e=(0,c.ensureId)(R,`combobox-label`),t=n.getAttribute(`aria-labelledby`);n.setAttribute(`aria-labelledby`,t?`${t} ${e}`:e),L.setAttribute(`aria-labelledby`,e)}m&&(n.setAttribute(`aria-disabled`,`true`),n.disabled=!0,a&&(a.setAttribute(`aria-disabled`,`true`),a.setAttribute(`data-disabled`,``))),h&&(n.setAttribute(`aria-required`,`true`),n.required=!0);let z=()=>{h&&n.setCustomValidity(C===null?`Please select a value`:``)};p&&(n.placeholder=p),g&&(n.name&&n.removeAttribute(`name`),j=document.createElement(`input`),j.type=`hidden`,j.name=g,j.value=C??``,e.appendChild(j));let B=(e,t,n)=>n.toLowerCase().includes(e.toLowerCase()),V=ee??B,H=()=>{let e=i??r;D=(0,c.getParts)(e,`combobox-item`);for(let e of D){e.setAttribute(`role`,`option`),(0,c.ensureId)(e,`combobox-item`),N(e)?e.setAttribute(`aria-disabled`,`true`):e.removeAttribute(`aria-disabled`);let t=F(e);t===C?((0,c.setAria)(e,`selected`,!0),e.setAttribute(`data-selected`,``)):((0,c.setAria)(e,`selected`,!1),e.removeAttribute(`data-selected`))}let t=(0,c.getParts)(e,`combobox-group`);for(let e of t){e.setAttribute(`role`,`group`);let t=(0,c.getPart)(e,`combobox-label`);if(t){let n=(0,c.ensureId)(t,`combobox-label`);e.setAttribute(`aria-labelledby`,n)}}U()},U=()=>{O=D.filter(e=>!e.hidden),k=O.filter(e=>!N(e)),A=new Map(k.map((e,t)=>[e,t]))},W=(e,t)=>{let n=t===`previous`?e.previousElementSibling:e.nextElementSibling;for(;n;){if(n instanceof HTMLElement&&!n.hidden&&n.dataset.slot!==`combobox-separator`)return!0;n=t===`previous`?n.previousElementSibling:n.nextElementSibling}return!1},G=e=>{let t=i??r,n=e.trim(),a=0;for(let e of D){let t=F(e)??``,r=P(e),i=n===``||V(n,t,r);e.hidden=!i,i&&a++}let s=(0,c.getParts)(t,`combobox-group`);for(let e of s){let t=(0,c.getParts)(e,`combobox-item`),n=t.some(e=>!e.hidden);e.hidden=!n}let l=(0,c.getParts)(t,`combobox-separator`);for(let e of l)e.hidden=!W(e,`previous`)||!W(e,`next`);o&&(o.hidden=a>0),a===0?r.setAttribute(`data-empty`,``):r.removeAttribute(`data-empty`),U()},K=()=>{let t=e.ownerDocument.defaultView??window,n=e.getBoundingClientRect();r.style.minWidth=`${n.width}px`;let i=r.getBoundingClientRect(),a=(0,c.computeFloatingPosition)({anchorRect:n,contentRect:i,side:re,align:ie,sideOffset:b,alignOffset:ae,avoidCollisions:oe,collisionPadding:x,allowedSides:l});r.style.position=`absolute`,r.style.top=`${a.y+t.scrollY}px`,r.style.left=`${a.x+t.scrollX}px`,r.style.margin=`0`,r.setAttribute(`data-side`,a.side),r.setAttribute(`data-align`,a.align)},q=(0,c.createPositionSync)({observedElements:[e,r],isActive:()=>S,ancestorScroll:!1,onUpdate:K,ignoreScrollTarget:e=>e instanceof Node&&r.contains(e)}),le=e=>i&&i.contains(e)&&i.scrollHeight>i.clientHeight?i:r,J=e=>{for(let t=0;t<k.length;t++){let r=k[t];t===e?(r.setAttribute(`data-highlighted`,``),n.setAttribute(`aria-activedescendant`,r.id),(0,c.ensureItemVisibleInContainer)(r,le(r))):r.removeAttribute(`data-highlighted`)}w=e},Y=()=>{for(let e of D)e.removeAttribute(`data-highlighted`);w=-1,n.removeAttribute(`aria-activedescendant`)},X=t=>{e.setAttribute(`data-state`,t),r.setAttribute(`data-state`,t),a&&a.setAttribute(`data-state`,t)},Z=(t,i=!1)=>{if(S!==t&&!(m&&t)){if(t){S=!0,(0,c.setAria)(n,`expanded`,!0),M.mount(),r.hidden=!1,X(`open`),H(),T=!1,G(n.value);let e=k.findIndex(e=>F(e)===C);e>=0?J(e):v&&k.length>0?J(0):Y(),q.start(),K(),q.update(),requestAnimationFrame(()=>{S&&q.update()})}else{S=!1,(0,c.setAria)(n,`expanded`,!1),M.restore(),r.hidden=!0,X(`closed`),Y(),T=!1,q.stop();let e=I(C);n.value=e}(0,c.emit)(e,`combobox:open-change`,{open:S}),y?.(S)}},Q=(t,a=!1)=>{if(C===t&&!a)return;let o=C;C=t,z(),j&&(j.value=t??``),t===null?e.removeAttribute(`data-value`):e.setAttribute(`data-value`,t);let s=i??r,l=D.length>0?D:(0,c.getParts)(s,`combobox-item`);for(let e of l){let n=F(e);n===t?((0,c.setAria)(e,`selected`,!0),e.setAttribute(`data-selected`,``)):((0,c.setAria)(e,`selected`,!1),e.removeAttribute(`data-selected`))}n.value=I(t),!a&&o!==t&&((0,c.emit)(e,`combobox:change`,{value:t}),te?.(t))},$=e=>{if(N(e))return;let t=F(e);t!==void 0&&(Q(t),Z(!1))},ue=e=>{if(!m)switch(e.key){case`ArrowDown`:{if(e.preventDefault(),!S){Z(!0);return}T=!0;let t=k.length;if(t===0)return;J(w===-1?0:(w+1)%t);break}case`ArrowUp`:{if(e.preventDefault(),!S){Z(!0);return}T=!0;let t=k.length;if(t===0)return;J(w===-1?t-1:(w-1+t)%t);break}case`Home`:if(!S)return;e.preventDefault(),T=!0,k.length>0&&J(0);break;case`End`:if(!S)return;e.preventDefault(),T=!0,k.length>0&&J(k.length-1);break;case`Enter`:if(!S)return;e.preventDefault(),w>=0&&w<k.length&&$(k[w]);break;case`Escape`:S?(e.preventDefault(),Z(!1)):C!==null&&(e.preventDefault(),Q(null));break;case`Tab`:S&&Z(!1,!0);break}},de=()=>{let t=n.value;(0,c.emit)(e,`combobox:input-change`,{inputValue:t}),ne?.(t),S?(G(t),v&&k.length>0?J(0):Y(),q.update()):Z(!0)},fe=()=>{m||(n.select(),_&&!S&&Z(!0))};(0,c.setAria)(n,`expanded`,!1),r.hidden=!0,X(`closed`),Q(C,!0),E.push((0,c.on)(n,`input`,de),(0,c.on)(n,`keydown`,ue),(0,c.on)(n,`focus`,fe)),a&&E.push((0,c.on)(a,`click`,()=>{m||(S?Z(!1):(Z(!0),n.focus()))})),E.push((0,c.on)(r,`click`,e=>{let t=e.target.closest?.(`[data-slot="combobox-item"]`);t&&!t.hidden&&$(t)}),(0,c.on)(r,`pointermove`,e=>{let t=e.target.closest?.(`[data-slot="combobox-item"]`);if(!(T&&(T=!1,t&&A.get(t)===w)))if(t&&!N(t)&&!t.hidden){let e=A.get(t);e!==void 0&&e!==w&&J(e)}else Y()}),(0,c.on)(r,`pointerleave`,()=>{T||Y()}),(0,c.on)(r,`mousedown`,e=>{e.preventDefault()})),E.push((0,c.createDismissLayer)({root:e,isOpen:()=>S,onDismiss:()=>Z(!1),closeOnClickOutside:!0,closeOnEscape:!1})),E.push((0,c.on)(e,`combobox:set`,e=>{let t=e.detail;t?.value!==void 0&&Q(t.value),t?.open!==void 0&&Z(t.open),t?.inputValue!==void 0&&(n.value=t.inputValue)}));let pe={get value(){return C},get inputValue(){return n.value},get isOpen(){return S},select:e=>Q(e),clear:()=>Q(null),open:()=>Z(!0),close:()=>Z(!1),destroy:()=>{q.stop(),M.cleanup(),E.forEach(e=>e()),E.length=0,j&&j.parentNode&&j.parentNode.removeChild(j),f.delete(e)}};return d&&Z(!0),pe}const f=new WeakSet;function p(e=document){let t=[];for(let n of(0,c.getRoots)(e,`combobox`)){if(f.has(n))continue;f.add(n),t.push(d(n))}return t}exports.create=p,exports.createCombobox=d;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/** Side of the input to place the content */
|
|
3
|
+
type Side = "top" | "bottom";
|
|
4
|
+
/** Alignment of the content relative to the input */
|
|
5
|
+
type Align = "start" | "center" | "end";
|
|
6
|
+
interface ComboboxOptions {
|
|
7
|
+
/** Initial selected value */
|
|
8
|
+
defaultValue?: string;
|
|
9
|
+
/** Callback when value changes */
|
|
10
|
+
onValueChange?: (value: string | null) => void;
|
|
11
|
+
/** Initial open state */
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
/** Callback when open state changes */
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
/** Callback when user types in the input (not on programmatic syncs) */
|
|
16
|
+
onInputValueChange?: (inputValue: string) => void;
|
|
17
|
+
/** Placeholder text for the input */
|
|
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
|
+
/** Open popup when input receives focus @default true */
|
|
26
|
+
openOnFocus?: boolean;
|
|
27
|
+
/** Auto-highlight first visible item when filtering @default true */
|
|
28
|
+
autoHighlight?: boolean;
|
|
29
|
+
/** Custom filter function. Return true to show item. */
|
|
30
|
+
filter?: (inputValue: string, itemValue: string, itemLabel: string) => boolean;
|
|
31
|
+
/** @default "bottom" */
|
|
32
|
+
side?: Side;
|
|
33
|
+
/** @default "start" */
|
|
34
|
+
align?: Align;
|
|
35
|
+
/** @default 4 */
|
|
36
|
+
sideOffset?: number;
|
|
37
|
+
/** @default 0 */
|
|
38
|
+
alignOffset?: number;
|
|
39
|
+
/** @default true */
|
|
40
|
+
avoidCollisions?: boolean;
|
|
41
|
+
/** @default 8 */
|
|
42
|
+
collisionPadding?: number;
|
|
43
|
+
}
|
|
44
|
+
interface ComboboxController {
|
|
45
|
+
/** Current selected value */
|
|
46
|
+
readonly value: string | null;
|
|
47
|
+
/** Current input text */
|
|
48
|
+
readonly inputValue: string;
|
|
49
|
+
/** Current open state */
|
|
50
|
+
readonly isOpen: boolean;
|
|
51
|
+
/** Select a value programmatically */
|
|
52
|
+
select(value: string): void;
|
|
53
|
+
/** Clear selected value */
|
|
54
|
+
clear(): void;
|
|
55
|
+
/** Open the popup */
|
|
56
|
+
open(): void;
|
|
57
|
+
/** Close the popup */
|
|
58
|
+
close(): void;
|
|
59
|
+
/** Cleanup all event listeners */
|
|
60
|
+
destroy(): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a combobox controller for a root element.
|
|
64
|
+
*
|
|
65
|
+
* ## Events
|
|
66
|
+
* - **Outbound** `combobox:change` (on root): Fires when value changes.
|
|
67
|
+
* `event.detail: { value: string | null }`
|
|
68
|
+
* - **Outbound** `combobox:open-change` (on root): Fires when popup opens/closes.
|
|
69
|
+
* `event.detail: { open: boolean }`
|
|
70
|
+
* - **Outbound** `combobox:input-change` (on root): Fires when user types.
|
|
71
|
+
* `event.detail: { inputValue: string }`
|
|
72
|
+
* - **Inbound** `combobox:set` (on root): Set value, open state, or input value.
|
|
73
|
+
* `event.detail: { value?: string | null, open?: boolean, inputValue?: string }`
|
|
74
|
+
*/
|
|
75
|
+
declare function createCombobox(root: Element, options?: ComboboxOptions): ComboboxController;
|
|
76
|
+
/**
|
|
77
|
+
* Find and bind all combobox components in a scope
|
|
78
|
+
* Returns array of controllers for programmatic access
|
|
79
|
+
*/
|
|
80
|
+
declare function create(scope?: ParentNode): ComboboxController[];
|
|
81
|
+
//#endregion
|
|
82
|
+
export { Align, ComboboxController, ComboboxOptions, Side, create, createCombobox };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/** Side of the input to place the content */
|
|
3
|
+
type Side = "top" | "bottom";
|
|
4
|
+
/** Alignment of the content relative to the input */
|
|
5
|
+
type Align = "start" | "center" | "end";
|
|
6
|
+
interface ComboboxOptions {
|
|
7
|
+
/** Initial selected value */
|
|
8
|
+
defaultValue?: string;
|
|
9
|
+
/** Callback when value changes */
|
|
10
|
+
onValueChange?: (value: string | null) => void;
|
|
11
|
+
/** Initial open state */
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
/** Callback when open state changes */
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
/** Callback when user types in the input (not on programmatic syncs) */
|
|
16
|
+
onInputValueChange?: (inputValue: string) => void;
|
|
17
|
+
/** Placeholder text for the input */
|
|
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
|
+
/** Open popup when input receives focus @default true */
|
|
26
|
+
openOnFocus?: boolean;
|
|
27
|
+
/** Auto-highlight first visible item when filtering @default true */
|
|
28
|
+
autoHighlight?: boolean;
|
|
29
|
+
/** Custom filter function. Return true to show item. */
|
|
30
|
+
filter?: (inputValue: string, itemValue: string, itemLabel: string) => boolean;
|
|
31
|
+
/** @default "bottom" */
|
|
32
|
+
side?: Side;
|
|
33
|
+
/** @default "start" */
|
|
34
|
+
align?: Align;
|
|
35
|
+
/** @default 4 */
|
|
36
|
+
sideOffset?: number;
|
|
37
|
+
/** @default 0 */
|
|
38
|
+
alignOffset?: number;
|
|
39
|
+
/** @default true */
|
|
40
|
+
avoidCollisions?: boolean;
|
|
41
|
+
/** @default 8 */
|
|
42
|
+
collisionPadding?: number;
|
|
43
|
+
}
|
|
44
|
+
interface ComboboxController {
|
|
45
|
+
/** Current selected value */
|
|
46
|
+
readonly value: string | null;
|
|
47
|
+
/** Current input text */
|
|
48
|
+
readonly inputValue: string;
|
|
49
|
+
/** Current open state */
|
|
50
|
+
readonly isOpen: boolean;
|
|
51
|
+
/** Select a value programmatically */
|
|
52
|
+
select(value: string): void;
|
|
53
|
+
/** Clear selected value */
|
|
54
|
+
clear(): void;
|
|
55
|
+
/** Open the popup */
|
|
56
|
+
open(): void;
|
|
57
|
+
/** Close the popup */
|
|
58
|
+
close(): void;
|
|
59
|
+
/** Cleanup all event listeners */
|
|
60
|
+
destroy(): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a combobox controller for a root element.
|
|
64
|
+
*
|
|
65
|
+
* ## Events
|
|
66
|
+
* - **Outbound** `combobox:change` (on root): Fires when value changes.
|
|
67
|
+
* `event.detail: { value: string | null }`
|
|
68
|
+
* - **Outbound** `combobox:open-change` (on root): Fires when popup opens/closes.
|
|
69
|
+
* `event.detail: { open: boolean }`
|
|
70
|
+
* - **Outbound** `combobox:input-change` (on root): Fires when user types.
|
|
71
|
+
* `event.detail: { inputValue: string }`
|
|
72
|
+
* - **Inbound** `combobox:set` (on root): Set value, open state, or input value.
|
|
73
|
+
* `event.detail: { value?: string | null, open?: boolean, inputValue?: string }`
|
|
74
|
+
*/
|
|
75
|
+
declare function createCombobox(root: Element, options?: ComboboxOptions): ComboboxController;
|
|
76
|
+
/**
|
|
77
|
+
* Find and bind all combobox components in a scope
|
|
78
|
+
* Returns array of controllers for programmatic access
|
|
79
|
+
*/
|
|
80
|
+
declare function create(scope?: ParentNode): ComboboxController[];
|
|
81
|
+
//#endregion
|
|
82
|
+
export { Align, ComboboxController, ComboboxOptions, Side, create, createCombobox };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{computeFloatingPosition as e,createDismissLayer as t,createPortalLifecycle as n,createPositionSync as r,emit as i,ensureId as a,ensureItemVisibleInContainer as ee,getDataBool as o,getDataEnum as s,getDataNumber as c,getDataString as l,getPart as u,getParts as d,getRoots as f,on as p,setAria as m}from"@data-slot/core";const h=[`top`,`bottom`],g=[`start`,`center`,`end`];function _(f,_={}){let y=u(f,`combobox-input`),b=u(f,`combobox-content`),x=u(f,`combobox-list`)??u(b??f,`combobox-list`),S=u(f,`combobox-trigger`),C=u(x??b??f,`combobox-empty`);if(!y||!b)throw Error(`Combobox requires combobox-input and combobox-content slots`);let te=_.defaultValue??l(f,`defaultValue`)??null,ne=_.defaultOpen??o(f,`defaultOpen`)??!1,w=_.placeholder??l(f,`placeholder`)??``,T=_.disabled??o(f,`disabled`)??!1,E=_.required??o(f,`required`)??!1,re=_.name??l(f,`name`)??null,ie=_.openOnFocus??o(f,`openOnFocus`)??!0,ae=_.autoHighlight??o(f,`autoHighlight`)??!0,oe=_.filter??null,se=_.onValueChange,ce=_.onOpenChange,le=_.onInputValueChange,ue=_.side??s(b,`side`,h)??s(f,`side`,h)??`bottom`,de=_.align??s(b,`align`,g)??s(f,`align`,g)??`start`,fe=_.sideOffset??c(b,`sideOffset`)??c(f,`sideOffset`)??4,pe=_.alignOffset??c(b,`alignOffset`)??c(f,`alignOffset`)??0,me=_.avoidCollisions??o(b,`avoidCollisions`)??o(f,`avoidCollisions`)??!0,he=_.collisionPadding??c(b,`collisionPadding`)??c(f,`collisionPadding`)??8,D=!1,O=te,k=-1,A=!1,j=[],M=[],N=[],P=[],F=new Map,I=null,L=n({content:b,root:f}),R=e=>e.hasAttribute(`disabled`)||e.hasAttribute(`data-disabled`)||e.getAttribute(`aria-disabled`)===`true`,z=e=>{if(e.dataset.label)return e.dataset.label;let t=``;for(let n of e.childNodes)n.nodeType===Node.TEXT_NODE&&(t+=n.textContent);let n=t.trim();return n||(e.textContent?.trim()??``)},B=e=>e.hasAttribute(`data-value`)?e.getAttribute(`data-value`):void 0,V=e=>{if(e===null)return``;let t=x??b,n=d(t,`combobox-item`),r=n.find(t=>B(t)===e);return r?z(r):``},ge=a(y,`combobox-input`),H=x??b,_e=a(H,`combobox-list`);y.setAttribute(`role`,`combobox`),y.setAttribute(`aria-autocomplete`,`list`),y.setAttribute(`autocomplete`,`off`),y.setAttribute(`aria-controls`,_e),x?x.setAttribute(`role`,`listbox`):b.setAttribute(`role`,`listbox`),S&&(S.hasAttribute(`type`)||S.setAttribute(`type`,`button`),S.tabIndex=-1,S.setAttribute(`aria-label`,`Toggle`));let U=document.querySelector(`label[for="${CSS.escape(ge)}"]`);if(U){let e=a(U,`combobox-label`),t=y.getAttribute(`aria-labelledby`);y.setAttribute(`aria-labelledby`,t?`${t} ${e}`:e),H.setAttribute(`aria-labelledby`,e)}T&&(y.setAttribute(`aria-disabled`,`true`),y.disabled=!0,S&&(S.setAttribute(`aria-disabled`,`true`),S.setAttribute(`data-disabled`,``))),E&&(y.setAttribute(`aria-required`,`true`),y.required=!0);let ve=()=>{E&&y.setCustomValidity(O===null?`Please select a value`:``)};w&&(y.placeholder=w),re&&(y.name&&y.removeAttribute(`name`),I=document.createElement(`input`),I.type=`hidden`,I.name=re,I.value=O??``,f.appendChild(I));let ye=(e,t,n)=>n.toLowerCase().includes(e.toLowerCase()),be=oe??ye,xe=()=>{let e=x??b;M=d(e,`combobox-item`);for(let e of M){e.setAttribute(`role`,`option`),a(e,`combobox-item`),R(e)?e.setAttribute(`aria-disabled`,`true`):e.removeAttribute(`aria-disabled`);let t=B(e);t===O?(m(e,`selected`,!0),e.setAttribute(`data-selected`,``)):(m(e,`selected`,!1),e.removeAttribute(`data-selected`))}let t=d(e,`combobox-group`);for(let e of t){e.setAttribute(`role`,`group`);let t=u(e,`combobox-label`);if(t){let n=a(t,`combobox-label`);e.setAttribute(`aria-labelledby`,n)}}W()},W=()=>{N=M.filter(e=>!e.hidden),P=N.filter(e=>!R(e)),F=new Map(P.map((e,t)=>[e,t]))},G=(e,t)=>{let n=t===`previous`?e.previousElementSibling:e.nextElementSibling;for(;n;){if(n instanceof HTMLElement&&!n.hidden&&n.dataset.slot!==`combobox-separator`)return!0;n=t===`previous`?n.previousElementSibling:n.nextElementSibling}return!1},K=e=>{let t=x??b,n=e.trim(),r=0;for(let e of M){let t=B(e)??``,i=z(e),a=n===``||be(n,t,i);e.hidden=!a,a&&r++}let i=d(t,`combobox-group`);for(let e of i){let t=d(e,`combobox-item`),n=t.some(e=>!e.hidden);e.hidden=!n}let a=d(t,`combobox-separator`);for(let e of a)e.hidden=!G(e,`previous`)||!G(e,`next`);C&&(C.hidden=r>0),r===0?b.setAttribute(`data-empty`,``):b.removeAttribute(`data-empty`),W()},Se=()=>{let t=f.ownerDocument.defaultView??window,n=f.getBoundingClientRect();b.style.minWidth=`${n.width}px`;let r=b.getBoundingClientRect(),i=e({anchorRect:n,contentRect:r,side:ue,align:de,sideOffset:fe,alignOffset:pe,avoidCollisions:me,collisionPadding:he,allowedSides:h});b.style.position=`absolute`,b.style.top=`${i.y+t.scrollY}px`,b.style.left=`${i.x+t.scrollX}px`,b.style.margin=`0`,b.setAttribute(`data-side`,i.side),b.setAttribute(`data-align`,i.align)},q=r({observedElements:[f,b],isActive:()=>D,ancestorScroll:!1,onUpdate:Se,ignoreScrollTarget:e=>e instanceof Node&&b.contains(e)}),Ce=e=>x&&x.contains(e)&&x.scrollHeight>x.clientHeight?x:b,J=e=>{for(let t=0;t<P.length;t++){let n=P[t];t===e?(n.setAttribute(`data-highlighted`,``),y.setAttribute(`aria-activedescendant`,n.id),ee(n,Ce(n))):n.removeAttribute(`data-highlighted`)}k=e},Y=()=>{for(let e of M)e.removeAttribute(`data-highlighted`);k=-1,y.removeAttribute(`aria-activedescendant`)},X=e=>{f.setAttribute(`data-state`,e),b.setAttribute(`data-state`,e),S&&S.setAttribute(`data-state`,e)},Z=(e,t=!1)=>{if(D!==e&&!(T&&e)){if(e){D=!0,m(y,`expanded`,!0),L.mount(),b.hidden=!1,X(`open`),xe(),A=!1,K(y.value);let e=P.findIndex(e=>B(e)===O);e>=0?J(e):ae&&P.length>0?J(0):Y(),q.start(),Se(),q.update(),requestAnimationFrame(()=>{D&&q.update()})}else{D=!1,m(y,`expanded`,!1),L.restore(),b.hidden=!0,X(`closed`),Y(),A=!1,q.stop();let e=V(O);y.value=e}i(f,`combobox:open-change`,{open:D}),ce?.(D)}},Q=(e,t=!1)=>{if(O===e&&!t)return;let n=O;O=e,ve(),I&&(I.value=e??``),e===null?f.removeAttribute(`data-value`):f.setAttribute(`data-value`,e);let r=x??b,a=M.length>0?M:d(r,`combobox-item`);for(let t of a){let n=B(t);n===e?(m(t,`selected`,!0),t.setAttribute(`data-selected`,``)):(m(t,`selected`,!1),t.removeAttribute(`data-selected`))}y.value=V(e),!t&&n!==e&&(i(f,`combobox:change`,{value:e}),se?.(e))},$=e=>{if(R(e))return;let t=B(e);t!==void 0&&(Q(t),Z(!1))},we=e=>{if(!T)switch(e.key){case`ArrowDown`:{if(e.preventDefault(),!D){Z(!0);return}A=!0;let t=P.length;if(t===0)return;J(k===-1?0:(k+1)%t);break}case`ArrowUp`:{if(e.preventDefault(),!D){Z(!0);return}A=!0;let t=P.length;if(t===0)return;J(k===-1?t-1:(k-1+t)%t);break}case`Home`:if(!D)return;e.preventDefault(),A=!0,P.length>0&&J(0);break;case`End`:if(!D)return;e.preventDefault(),A=!0,P.length>0&&J(P.length-1);break;case`Enter`:if(!D)return;e.preventDefault(),k>=0&&k<P.length&&$(P[k]);break;case`Escape`:D?(e.preventDefault(),Z(!1)):O!==null&&(e.preventDefault(),Q(null));break;case`Tab`:D&&Z(!1,!0);break}},Te=()=>{let e=y.value;i(f,`combobox:input-change`,{inputValue:e}),le?.(e),D?(K(e),ae&&P.length>0?J(0):Y(),q.update()):Z(!0)},Ee=()=>{T||(y.select(),ie&&!D&&Z(!0))};m(y,`expanded`,!1),b.hidden=!0,X(`closed`),Q(O,!0),j.push(p(y,`input`,Te),p(y,`keydown`,we),p(y,`focus`,Ee)),S&&j.push(p(S,`click`,()=>{T||(D?Z(!1):(Z(!0),y.focus()))})),j.push(p(b,`click`,e=>{let t=e.target.closest?.(`[data-slot="combobox-item"]`);t&&!t.hidden&&$(t)}),p(b,`pointermove`,e=>{let t=e.target.closest?.(`[data-slot="combobox-item"]`);if(!(A&&(A=!1,t&&F.get(t)===k)))if(t&&!R(t)&&!t.hidden){let e=F.get(t);e!==void 0&&e!==k&&J(e)}else Y()}),p(b,`pointerleave`,()=>{A||Y()}),p(b,`mousedown`,e=>{e.preventDefault()})),j.push(t({root:f,isOpen:()=>D,onDismiss:()=>Z(!1),closeOnClickOutside:!0,closeOnEscape:!1})),j.push(p(f,`combobox:set`,e=>{let t=e.detail;t?.value!==void 0&&Q(t.value),t?.open!==void 0&&Z(t.open),t?.inputValue!==void 0&&(y.value=t.inputValue)}));let De={get value(){return O},get inputValue(){return y.value},get isOpen(){return D},select:e=>Q(e),clear:()=>Q(null),open:()=>Z(!0),close:()=>Z(!1),destroy:()=>{q.stop(),L.cleanup(),j.forEach(e=>e()),j.length=0,I&&I.parentNode&&I.parentNode.removeChild(I),v.delete(f)}};return ne&&Z(!0),De}const v=new WeakSet;function y(e=document){let t=[];for(let n of f(e,`combobox`)){if(v.has(n))continue;v.add(n),t.push(_(n))}return t}export{y as create,_ as createCombobox};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-slot/combobox",
|
|
3
|
+
"version": "0.2.30",
|
|
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
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/bejamas/data-slot",
|
|
30
|
+
"directory": "packages/combobox"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"headless",
|
|
34
|
+
"ui",
|
|
35
|
+
"combobox",
|
|
36
|
+
"autocomplete",
|
|
37
|
+
"typeahead",
|
|
38
|
+
"vanilla",
|
|
39
|
+
"data-slot"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@data-slot/core": "workspace:*"
|
|
44
|
+
}
|
|
45
|
+
}
|