bard-tag_field 0.5.1 → 0.5.3
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.
- checksums.yaml +4 -4
- data/CLAUDE.md +9 -8
- data/Rakefile +4 -1
- data/app/assets/javascripts/input-tag.js +16 -30
- data/input-tag/.gitignore +6 -2
- data/input-tag/CLAUDE.md +87 -0
- data/input-tag/LICENSE +21 -0
- data/input-tag/README.md +135 -0
- data/input-tag/TESTING.md +99 -0
- data/input-tag/bun.lock +821 -0
- data/input-tag/index.html +331 -0
- data/input-tag/package.json +52 -8
- data/input-tag/rollup.config.js +4 -4
- data/input-tag/src/input-tag.js +849 -0
- data/input-tag/src/taggle.js +546 -0
- data/input-tag/test/api-methods.test.js +684 -0
- data/input-tag/test/autocomplete.test.js +615 -0
- data/input-tag/test/basic-functionality.test.js +567 -0
- data/input-tag/test/dom-mutation.test.js +466 -0
- data/input-tag/test/edge-cases.test.js +524 -0
- data/input-tag/test/events.test.js +425 -0
- data/input-tag/test/form-integration.test.js +447 -0
- data/input-tag/test/input-tag.test.js +90 -0
- data/input-tag/test/lib/fail-only.mjs +24 -0
- data/input-tag/test/lib/test-utils.js +187 -0
- data/input-tag/test/nested-datalist.test.js +328 -0
- data/input-tag/test/value-label-separation.test.js +357 -0
- data/input-tag/web-test-runner.config.mjs +20 -0
- data/lib/bard/tag_field/version.rb +1 -1
- metadata +23 -4
- data/input-tag/bun.lockb +0 -0
- data/input-tag/index.js +0 -1
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { expect } from '@esm-bundle/chai'
|
|
2
|
+
import '../src/input-tag.js'
|
|
3
|
+
import {
|
|
4
|
+
setupGlobalTestHooks,
|
|
5
|
+
setupInputTag,
|
|
6
|
+
waitForElement,
|
|
7
|
+
waitForBasicInitialization,
|
|
8
|
+
waitForUpdate,
|
|
9
|
+
simulateInput,
|
|
10
|
+
getTagElements,
|
|
11
|
+
getTagValues
|
|
12
|
+
} from './lib/test-utils.js'
|
|
13
|
+
|
|
14
|
+
describe('Autocomplete', () => {
|
|
15
|
+
setupGlobalTestHooks()
|
|
16
|
+
|
|
17
|
+
describe('Datalist Integration', () => {
|
|
18
|
+
it('should read options from associated datalist', async () => {
|
|
19
|
+
document.body.innerHTML = `
|
|
20
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
21
|
+
<datalist id="suggestions">
|
|
22
|
+
<option value="react">React</option>
|
|
23
|
+
<option value="vue">Vue</option>
|
|
24
|
+
<option value="angular">Angular</option>
|
|
25
|
+
<option value="svelte">Svelte</option>
|
|
26
|
+
</datalist>
|
|
27
|
+
`
|
|
28
|
+
const inputTag = document.querySelector('input-tag')
|
|
29
|
+
|
|
30
|
+
await waitForBasicInitialization(inputTag)
|
|
31
|
+
|
|
32
|
+
expect(inputTag.options).to.deep.equal(['react', 'vue', 'angular', 'svelte'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should return empty array when no datalist is associated', async () => {
|
|
36
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
37
|
+
|
|
38
|
+
expect(inputTag.options).to.deep.equal([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should return empty array when datalist does not exist', async () => {
|
|
42
|
+
const inputTag = await setupInputTag('<input-tag name="tags" list="nonexistent" multiple></input-tag>')
|
|
43
|
+
|
|
44
|
+
expect(inputTag.options).to.deep.equal([])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should handle empty datalist', async () => {
|
|
48
|
+
document.body.innerHTML = `
|
|
49
|
+
<input-tag name="tags" list="empty" multiple></input-tag>
|
|
50
|
+
<datalist id="empty"></datalist>
|
|
51
|
+
`
|
|
52
|
+
const inputTag = document.querySelector('input-tag')
|
|
53
|
+
|
|
54
|
+
await waitForBasicInitialization(inputTag)
|
|
55
|
+
|
|
56
|
+
expect(inputTag.options).to.deep.equal([])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should update options when datalist changes', async () => {
|
|
60
|
+
document.body.innerHTML = `
|
|
61
|
+
<input-tag name="frameworks" list="dynamic" multiple></input-tag>
|
|
62
|
+
<datalist id="dynamic">
|
|
63
|
+
<option value="initial">Initial</option>
|
|
64
|
+
</datalist>
|
|
65
|
+
`
|
|
66
|
+
const inputTag = document.querySelector('input-tag')
|
|
67
|
+
const datalist = document.querySelector('#dynamic')
|
|
68
|
+
|
|
69
|
+
await waitForBasicInitialization(inputTag)
|
|
70
|
+
|
|
71
|
+
expect(inputTag.options).to.deep.equal(['initial'])
|
|
72
|
+
|
|
73
|
+
// Add option to datalist
|
|
74
|
+
const newOption = document.createElement('option')
|
|
75
|
+
newOption.value = 'added'
|
|
76
|
+
datalist.appendChild(newOption)
|
|
77
|
+
|
|
78
|
+
expect(inputTag.options).to.deep.equal(['initial', 'added'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should handle datalist with option text different from value', async () => {
|
|
82
|
+
document.body.innerHTML = `
|
|
83
|
+
<input-tag name="languages" list="lang-list" multiple></input-tag>
|
|
84
|
+
<datalist id="lang-list">
|
|
85
|
+
<option value="js">JavaScript</option>
|
|
86
|
+
<option value="ts">TypeScript</option>
|
|
87
|
+
<option value="py">Python</option>
|
|
88
|
+
</datalist>
|
|
89
|
+
`
|
|
90
|
+
const inputTag = document.querySelector('input-tag')
|
|
91
|
+
|
|
92
|
+
await waitForBasicInitialization(inputTag)
|
|
93
|
+
|
|
94
|
+
expect(inputTag.options).to.deep.equal(['js', 'ts', 'py'])
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('Autocomplete Suggestions Filtering', () => {
|
|
99
|
+
async function setupAutocompleteTest() {
|
|
100
|
+
document.body.innerHTML = `
|
|
101
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
102
|
+
<datalist id="suggestions">
|
|
103
|
+
<option value="react">React</option>
|
|
104
|
+
<option value="vue">Vue</option>
|
|
105
|
+
<option value="angular">Angular</option>
|
|
106
|
+
<option value="svelte">Svelte</option>
|
|
107
|
+
<option value="backbone">Backbone</option>
|
|
108
|
+
</datalist>
|
|
109
|
+
`
|
|
110
|
+
const inputTag = document.querySelector('input-tag')
|
|
111
|
+
await waitForBasicInitialization(inputTag)
|
|
112
|
+
return inputTag
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
it('should filter suggestions based on input text', async () => {
|
|
116
|
+
const inputTag = await setupAutocompleteTest()
|
|
117
|
+
const input = inputTag._taggleInputTarget
|
|
118
|
+
|
|
119
|
+
// Test 're' shows only 'React' (label)
|
|
120
|
+
await simulateInput(input, 're')
|
|
121
|
+
expect(inputTag._autocompleteSuggestions).to.deep.equal(['React'])
|
|
122
|
+
|
|
123
|
+
// Test 'e' shows multiple matches (labels)
|
|
124
|
+
await simulateInput(input, 'e')
|
|
125
|
+
expect(inputTag._autocompleteSuggestions).to.include.members(['React', 'Vue', 'Svelte', 'Backbone'])
|
|
126
|
+
|
|
127
|
+
// Test 'ang' shows only 'Angular' (label)
|
|
128
|
+
await simulateInput(input, 'ang')
|
|
129
|
+
expect(inputTag._autocompleteSuggestions).to.deep.equal(['Angular'])
|
|
130
|
+
|
|
131
|
+
// Test 'xyz' shows no matches
|
|
132
|
+
await simulateInput(input, 'xyz')
|
|
133
|
+
expect(inputTag._autocompleteSuggestions).to.have.length(0)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should handle case-insensitive filtering', async () => {
|
|
137
|
+
const inputTag = await setupAutocompleteTest()
|
|
138
|
+
const input = inputTag._taggleInputTarget
|
|
139
|
+
|
|
140
|
+
// Test uppercase 'REACT' shows 'React' (label)
|
|
141
|
+
await simulateInput(input, 'REACT')
|
|
142
|
+
expect(inputTag._autocompleteSuggestions).to.deep.equal(['React'])
|
|
143
|
+
|
|
144
|
+
// Test mixed case 'VuE' shows 'Vue' (label)
|
|
145
|
+
await simulateInput(input, 'VuE')
|
|
146
|
+
expect(inputTag._autocompleteSuggestions).to.deep.equal(['Vue'])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should filter with partial matches', async () => {
|
|
150
|
+
const inputTag = await setupAutocompleteTest()
|
|
151
|
+
const input = inputTag._taggleInputTarget
|
|
152
|
+
|
|
153
|
+
// Test 'a' shows tags containing 'a' (labels)
|
|
154
|
+
await simulateInput(input, 'a')
|
|
155
|
+
expect(inputTag._autocompleteSuggestions).to.include.members(['React', 'Angular', 'Backbone'])
|
|
156
|
+
|
|
157
|
+
// Test 'ck' shows only 'Backbone' (label)
|
|
158
|
+
await simulateInput(input, 'ck')
|
|
159
|
+
expect(inputTag._autocompleteSuggestions).to.deep.equal(['Backbone'])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should exclude already-entered tags from autocomplete suggestions', async () => {
|
|
163
|
+
const inputTag = await setupAutocompleteTest()
|
|
164
|
+
const input = inputTag._taggleInputTarget
|
|
165
|
+
|
|
166
|
+
// Add some existing tags
|
|
167
|
+
inputTag.add('react')
|
|
168
|
+
inputTag.add('vue')
|
|
169
|
+
await waitForUpdate()
|
|
170
|
+
|
|
171
|
+
expect(getTagValues(inputTag)).to.deep.equal(['react', 'vue'])
|
|
172
|
+
|
|
173
|
+
// Type 'e' which should trigger autocomplete
|
|
174
|
+
await simulateInput(input, 'e')
|
|
175
|
+
|
|
176
|
+
// Should show 'Svelte' and 'Backbone' (labels) but NOT 'React' or 'Vue'
|
|
177
|
+
expect(inputTag._autocompleteSuggestions).to.include('Svelte')
|
|
178
|
+
expect(inputTag._autocompleteSuggestions).to.include('Backbone')
|
|
179
|
+
expect(inputTag._autocompleteSuggestions).to.not.include('React')
|
|
180
|
+
expect(inputTag._autocompleteSuggestions).to.not.include('Vue')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('Autocomplete Selection Behavior', () => {
|
|
185
|
+
it('should add selected suggestion as tag', async () => {
|
|
186
|
+
document.body.innerHTML = `
|
|
187
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
188
|
+
<datalist id="suggestions">
|
|
189
|
+
<option value="react">React</option>
|
|
190
|
+
<option value="vue">Vue</option>
|
|
191
|
+
</datalist>
|
|
192
|
+
`
|
|
193
|
+
const inputTag = document.querySelector('input-tag')
|
|
194
|
+
|
|
195
|
+
await waitForElement(inputTag, '_taggle')
|
|
196
|
+
|
|
197
|
+
// Simulate selecting a suggestion (direct taggle.add simulates the autocomplete selection)
|
|
198
|
+
inputTag.add('react')
|
|
199
|
+
await waitForUpdate()
|
|
200
|
+
|
|
201
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
202
|
+
expect(getTagValues(inputTag)).to.deep.equal(['react'])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should clear input after selection', async () => {
|
|
206
|
+
document.body.innerHTML = `
|
|
207
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
208
|
+
<datalist id="suggestions">
|
|
209
|
+
<option value="react">React</option>
|
|
210
|
+
</datalist>
|
|
211
|
+
`
|
|
212
|
+
const inputTag = document.querySelector('input-tag')
|
|
213
|
+
|
|
214
|
+
await waitForElement(inputTag, '_taggle')
|
|
215
|
+
|
|
216
|
+
const input = inputTag._taggleInputTarget
|
|
217
|
+
await simulateInput(input, 'react')
|
|
218
|
+
|
|
219
|
+
// Simulate autocomplete selection
|
|
220
|
+
inputTag.add('react')
|
|
221
|
+
await waitForUpdate()
|
|
222
|
+
|
|
223
|
+
expect(input.value).to.equal('')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should handle multiple autocomplete selections', async () => {
|
|
227
|
+
document.body.innerHTML = `
|
|
228
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
229
|
+
<datalist id="suggestions">
|
|
230
|
+
<option value="react">React</option>
|
|
231
|
+
<option value="vue">Vue</option>
|
|
232
|
+
<option value="angular">Angular</option>
|
|
233
|
+
</datalist>
|
|
234
|
+
`
|
|
235
|
+
const inputTag = document.querySelector('input-tag')
|
|
236
|
+
|
|
237
|
+
await waitForElement(inputTag, '_taggle')
|
|
238
|
+
|
|
239
|
+
// Simulate multiple selections
|
|
240
|
+
inputTag.add('react')
|
|
241
|
+
await waitForUpdate()
|
|
242
|
+
inputTag.add('vue')
|
|
243
|
+
await waitForUpdate()
|
|
244
|
+
inputTag.add('angular')
|
|
245
|
+
await waitForUpdate()
|
|
246
|
+
|
|
247
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
248
|
+
expect(getTagValues(inputTag)).to.deep.equal(['react', 'vue', 'angular'])
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('Multiple Datalist Scenarios', () => {
|
|
253
|
+
it('should handle switching datalist association', async () => {
|
|
254
|
+
document.body.innerHTML = `
|
|
255
|
+
<input-tag name="items" list="list1" multiple></input-tag>
|
|
256
|
+
<datalist id="list1">
|
|
257
|
+
<option value="item1">Item 1</option>
|
|
258
|
+
<option value="item2">Item 2</option>
|
|
259
|
+
</datalist>
|
|
260
|
+
<datalist id="list2">
|
|
261
|
+
<option value="item3">Item 3</option>
|
|
262
|
+
<option value="item4">Item 4</option>
|
|
263
|
+
</datalist>
|
|
264
|
+
`
|
|
265
|
+
const inputTag = document.querySelector('input-tag')
|
|
266
|
+
|
|
267
|
+
await waitForBasicInitialization(inputTag)
|
|
268
|
+
|
|
269
|
+
expect(inputTag.options).to.deep.equal(['item1', 'item2'])
|
|
270
|
+
|
|
271
|
+
// Switch to different datalist
|
|
272
|
+
inputTag.setAttribute('list', 'list2')
|
|
273
|
+
|
|
274
|
+
expect(inputTag.options).to.deep.equal(['item3', 'item4'])
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should handle removing datalist association', async () => {
|
|
278
|
+
document.body.innerHTML = `
|
|
279
|
+
<input-tag name="items" list="suggestions" multiple></input-tag>
|
|
280
|
+
<datalist id="suggestions">
|
|
281
|
+
<option value="item1">Item 1</option>
|
|
282
|
+
</datalist>
|
|
283
|
+
`
|
|
284
|
+
const inputTag = document.querySelector('input-tag')
|
|
285
|
+
|
|
286
|
+
await waitForBasicInitialization(inputTag)
|
|
287
|
+
|
|
288
|
+
expect(inputTag.options).to.deep.equal(['item1'])
|
|
289
|
+
|
|
290
|
+
// Remove list attribute
|
|
291
|
+
inputTag.removeAttribute('list')
|
|
292
|
+
|
|
293
|
+
expect(inputTag.options).to.deep.equal([])
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should handle multiple input-tags sharing same datalist', async () => {
|
|
297
|
+
document.body.innerHTML = `
|
|
298
|
+
<input-tag name="tags1" list="shared" multiple></input-tag>
|
|
299
|
+
<input-tag name="tags2" list="shared" multiple></input-tag>
|
|
300
|
+
<datalist id="shared">
|
|
301
|
+
<option value="shared1">Shared 1</option>
|
|
302
|
+
<option value="shared2">Shared 2</option>
|
|
303
|
+
</datalist>
|
|
304
|
+
`
|
|
305
|
+
const inputTag1 = document.querySelector('input-tag[name="tags1"]')
|
|
306
|
+
const inputTag2 = document.querySelector('input-tag[name="tags2"]')
|
|
307
|
+
|
|
308
|
+
await waitForElement(inputTag1, '_taggle')
|
|
309
|
+
await waitForElement(inputTag2, '_taggle')
|
|
310
|
+
|
|
311
|
+
expect(inputTag1.options).to.deep.equal(['shared1', 'shared2'])
|
|
312
|
+
expect(inputTag2.options).to.deep.equal(['shared1', 'shared2'])
|
|
313
|
+
|
|
314
|
+
// They should work independently
|
|
315
|
+
inputTag1.add('shared1')
|
|
316
|
+
await waitForUpdate()
|
|
317
|
+
|
|
318
|
+
expect(getTagValues(inputTag1)).to.deep.equal(['shared1'])
|
|
319
|
+
expect(getTagValues(inputTag2)).to.deep.equal([])
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('Autocomplete UI Interaction', () => {
|
|
324
|
+
it('should have autocomplete container in DOM', async () => {
|
|
325
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
326
|
+
|
|
327
|
+
expect(inputTag.autocompleteContainerTarget).to.not.be.null
|
|
328
|
+
expect(inputTag.autocompleteContainerTarget.tagName.toLowerCase()).to.equal('ul')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('should position autocomplete correctly when input-tag wraps to multiple lines', async () => {
|
|
332
|
+
document.body.innerHTML = `
|
|
333
|
+
<div style="width: 200px;">
|
|
334
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
335
|
+
<datalist id="suggestions">
|
|
336
|
+
<option value="react">React</option>
|
|
337
|
+
<option value="vue">Vue</option>
|
|
338
|
+
<option value="angular">Angular</option>
|
|
339
|
+
</datalist>
|
|
340
|
+
</div>
|
|
341
|
+
`
|
|
342
|
+
const inputTag = document.querySelector('input-tag')
|
|
343
|
+
await waitForBasicInitialization(inputTag)
|
|
344
|
+
const input = inputTag._taggleInputTarget
|
|
345
|
+
|
|
346
|
+
// Add enough tags to force wrapping
|
|
347
|
+
inputTag.add('very-long-tag-name-that-will-wrap')
|
|
348
|
+
inputTag.add('another-very-long-tag-name')
|
|
349
|
+
inputTag.add('yet-another-long-tag')
|
|
350
|
+
await waitForUpdate()
|
|
351
|
+
|
|
352
|
+
// Get the container height after tags are added
|
|
353
|
+
const containerHeight = inputTag.containerTarget.getBoundingClientRect().height
|
|
354
|
+
|
|
355
|
+
// Trigger autocomplete
|
|
356
|
+
await simulateInput(input, 'r')
|
|
357
|
+
|
|
358
|
+
// Check that autocomplete container is positioned correctly
|
|
359
|
+
const autocompleteContainer = inputTag.autocompleteContainerTarget
|
|
360
|
+
const computedStyle = window.getComputedStyle(autocompleteContainer)
|
|
361
|
+
|
|
362
|
+
expect(computedStyle.position).to.equal('absolute')
|
|
363
|
+
expect(computedStyle.top).to.equal(`${containerHeight}px`)
|
|
364
|
+
expect(computedStyle.zIndex).to.equal('1000')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('should dynamically reposition autocomplete when tags are added while open', async () => {
|
|
368
|
+
document.body.innerHTML = `
|
|
369
|
+
<div style="width: 200px;">
|
|
370
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
371
|
+
<datalist id="suggestions">
|
|
372
|
+
<option value="react">React</option>
|
|
373
|
+
<option value="vue">Vue</option>
|
|
374
|
+
<option value="angular">Angular</option>
|
|
375
|
+
</datalist>
|
|
376
|
+
</div>
|
|
377
|
+
`
|
|
378
|
+
const inputTag = document.querySelector('input-tag')
|
|
379
|
+
await waitForBasicInitialization(inputTag)
|
|
380
|
+
const input = inputTag._taggleInputTarget
|
|
381
|
+
|
|
382
|
+
// Get initial container height
|
|
383
|
+
const initialHeight = inputTag.containerTarget.getBoundingClientRect().height
|
|
384
|
+
|
|
385
|
+
// Open autocomplete
|
|
386
|
+
await simulateInput(input, 'r')
|
|
387
|
+
|
|
388
|
+
// Get initial autocomplete position
|
|
389
|
+
const autocompleteContainer = inputTag.autocompleteContainerTarget
|
|
390
|
+
let computedStyle = window.getComputedStyle(autocompleteContainer)
|
|
391
|
+
expect(computedStyle.top).to.equal(`${initialHeight}px`)
|
|
392
|
+
|
|
393
|
+
// Add a tag while autocomplete is open
|
|
394
|
+
inputTag.add('very-long-tag-name-that-will-cause-wrapping')
|
|
395
|
+
await waitForUpdate()
|
|
396
|
+
|
|
397
|
+
// Wait for position update
|
|
398
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
399
|
+
|
|
400
|
+
// Get new container height and verify autocomplete repositioned
|
|
401
|
+
const newHeight = inputTag.containerTarget.getBoundingClientRect().height
|
|
402
|
+
computedStyle = window.getComputedStyle(autocompleteContainer)
|
|
403
|
+
|
|
404
|
+
expect(newHeight).to.be.greaterThan(initialHeight)
|
|
405
|
+
expect(computedStyle.top).to.equal(`${newHeight}px`)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
xit('should position autocomplete container after button', async () => {
|
|
409
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
410
|
+
|
|
411
|
+
const button = inputTag.buttonTarget
|
|
412
|
+
const autocompleteContainer = inputTag.autocompleteContainerTarget
|
|
413
|
+
|
|
414
|
+
expect(button.nextElementSibling).to.equal(autocompleteContainer)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('should have correct CSS classes on autocomplete elements', async () => {
|
|
418
|
+
document.body.innerHTML = `
|
|
419
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
420
|
+
<datalist id="suggestions">
|
|
421
|
+
<option value="react">React</option>
|
|
422
|
+
</datalist>
|
|
423
|
+
`
|
|
424
|
+
const inputTag = document.querySelector('input-tag')
|
|
425
|
+
|
|
426
|
+
await waitForElement(inputTag, '_taggle')
|
|
427
|
+
|
|
428
|
+
const container = inputTag.autocompleteContainerTarget
|
|
429
|
+
|
|
430
|
+
// Check if the autocomplete setup has the right className
|
|
431
|
+
// (This is set in the autocomplete configuration)
|
|
432
|
+
expect(container.tagName.toLowerCase()).to.equal('ul')
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
describe('Missing Datalist Handling', () => {
|
|
437
|
+
it('should gracefully handle missing datalist element', async () => {
|
|
438
|
+
const inputTag = await setupInputTag('<input-tag name="tags" list="missing" multiple></input-tag>')
|
|
439
|
+
|
|
440
|
+
expect(inputTag.options).to.deep.equal([])
|
|
441
|
+
|
|
442
|
+
// Should still work for adding tags manually
|
|
443
|
+
inputTag.add('manual-tag')
|
|
444
|
+
await waitForUpdate()
|
|
445
|
+
|
|
446
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
447
|
+
expect(getTagValues(inputTag)).to.deep.equal(['manual-tag'])
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('should handle datalist that gets removed from DOM', async () => {
|
|
451
|
+
document.body.innerHTML = `
|
|
452
|
+
<input-tag name="tags" list="temporary" multiple></input-tag>
|
|
453
|
+
<datalist id="temporary">
|
|
454
|
+
<option value="temp">Temporary</option>
|
|
455
|
+
</datalist>
|
|
456
|
+
`
|
|
457
|
+
const inputTag = document.querySelector('input-tag')
|
|
458
|
+
const datalist = document.querySelector('#temporary')
|
|
459
|
+
|
|
460
|
+
await waitForBasicInitialization(inputTag)
|
|
461
|
+
|
|
462
|
+
expect(inputTag.options).to.deep.equal(['temp'])
|
|
463
|
+
|
|
464
|
+
// Remove datalist from DOM
|
|
465
|
+
datalist.remove()
|
|
466
|
+
|
|
467
|
+
expect(inputTag.options).to.deep.equal([])
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('should handle datalist that gets added to DOM after initialization', async () => {
|
|
471
|
+
const inputTag = await setupInputTag('<input-tag name="tags" list="future" multiple></input-tag>')
|
|
472
|
+
|
|
473
|
+
expect(inputTag.options).to.deep.equal([])
|
|
474
|
+
|
|
475
|
+
// Add datalist to DOM
|
|
476
|
+
const datalist = document.createElement('datalist')
|
|
477
|
+
datalist.id = 'future'
|
|
478
|
+
const option = document.createElement('option')
|
|
479
|
+
option.value = 'future-option'
|
|
480
|
+
datalist.appendChild(option)
|
|
481
|
+
document.body.appendChild(datalist)
|
|
482
|
+
|
|
483
|
+
expect(inputTag.options).to.deep.equal(['future-option'])
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
describe('Autocomplete with Pre-existing Tags', () => {
|
|
488
|
+
it('should work with pre-existing tags and autocomplete', async () => {
|
|
489
|
+
document.body.innerHTML = `
|
|
490
|
+
<input-tag name="frameworks" list="suggestions" multiple>
|
|
491
|
+
<tag-option value="react">React</tag-option>
|
|
492
|
+
</input-tag>
|
|
493
|
+
<datalist id="suggestions">
|
|
494
|
+
<option value="react">React</option>
|
|
495
|
+
<option value="vue">Vue</option>
|
|
496
|
+
<option value="angular">Angular</option>
|
|
497
|
+
</datalist>
|
|
498
|
+
`
|
|
499
|
+
const inputTag = document.querySelector('input-tag')
|
|
500
|
+
|
|
501
|
+
await waitForElement(inputTag, '_taggle')
|
|
502
|
+
|
|
503
|
+
expect(getTagValues(inputTag)).to.deep.equal(['react'])
|
|
504
|
+
expect(inputTag.options).to.deep.equal(['react', 'vue', 'angular'])
|
|
505
|
+
|
|
506
|
+
// Should be able to add more from autocomplete
|
|
507
|
+
inputTag.add('vue')
|
|
508
|
+
await waitForUpdate()
|
|
509
|
+
|
|
510
|
+
expect(getTagValues(inputTag)).to.deep.equal(['react', 'vue'])
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('should identify existing options correctly for isNew flag', async () => {
|
|
514
|
+
document.body.innerHTML = `
|
|
515
|
+
<input-tag name="frameworks" list="suggestions" multiple>
|
|
516
|
+
<tag-option value="existing">Existing</tag-option>
|
|
517
|
+
</input-tag>
|
|
518
|
+
<datalist id="suggestions">
|
|
519
|
+
<option value="existing">Existing</option>
|
|
520
|
+
<option value="from-list">From List</option>
|
|
521
|
+
</datalist>
|
|
522
|
+
`
|
|
523
|
+
const inputTag = document.querySelector('input-tag')
|
|
524
|
+
|
|
525
|
+
await waitForElement(inputTag, '_taggle')
|
|
526
|
+
|
|
527
|
+
let updateEvent = null
|
|
528
|
+
inputTag.addEventListener('update', (e) => {
|
|
529
|
+
updateEvent = e
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
// Add from datalist - should not be new
|
|
533
|
+
inputTag.add('from-list')
|
|
534
|
+
await waitForUpdate()
|
|
535
|
+
|
|
536
|
+
expect(updateEvent.detail.isNew).to.be.false
|
|
537
|
+
|
|
538
|
+
// Add custom tag - should be new
|
|
539
|
+
inputTag.add('custom-tag')
|
|
540
|
+
await waitForUpdate()
|
|
541
|
+
|
|
542
|
+
expect(updateEvent.detail.isNew).to.be.true
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
describe('Autocomplete in Single Mode', () => {
|
|
547
|
+
// Helper to simulate autocomplete selection behavior
|
|
548
|
+
function simulateAutocompleteSelection(inputTag, value, label) {
|
|
549
|
+
// Autocomplete onSelect directly creates and appends tag-option element
|
|
550
|
+
const tagOption = document.createElement('tag-option')
|
|
551
|
+
tagOption.setAttribute('value', value)
|
|
552
|
+
tagOption.textContent = label
|
|
553
|
+
inputTag.appendChild(tagOption)
|
|
554
|
+
inputTag._taggleInputTarget.value = ''
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
it('should hide input after selecting first autocomplete option in single mode', async () => {
|
|
558
|
+
document.body.innerHTML = `
|
|
559
|
+
<input-tag name="status" list="suggestions"></input-tag>
|
|
560
|
+
<datalist id="suggestions">
|
|
561
|
+
<option value="option1">Option 1</option>
|
|
562
|
+
<option value="option2">Option 2</option>
|
|
563
|
+
<option value="option3">Option 3</option>
|
|
564
|
+
</datalist>
|
|
565
|
+
`
|
|
566
|
+
const inputTag = document.querySelector('input-tag')
|
|
567
|
+
await waitForBasicInitialization(inputTag)
|
|
568
|
+
|
|
569
|
+
// Initially, input should be visible (no tags)
|
|
570
|
+
const input = inputTag._taggleInputTarget
|
|
571
|
+
const button = inputTag.buttonTarget
|
|
572
|
+
|
|
573
|
+
expect(input.style.display).to.not.equal('none')
|
|
574
|
+
expect(button.style.display).to.not.equal('none')
|
|
575
|
+
|
|
576
|
+
// Select first option from autocomplete (simulating actual autocomplete behavior)
|
|
577
|
+
simulateAutocompleteSelection(inputTag, 'option1', 'Option 1')
|
|
578
|
+
await waitForUpdate()
|
|
579
|
+
|
|
580
|
+
// Input should now be hidden (single mode with one tag)
|
|
581
|
+
expect(input.style.display).to.equal('none')
|
|
582
|
+
expect(button.style.display).to.equal('none')
|
|
583
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
584
|
+
expect(getTagValues(inputTag)).to.deep.equal(['option1'])
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('should prevent adding multiple tags via autocomplete in single mode', async () => {
|
|
588
|
+
document.body.innerHTML = `
|
|
589
|
+
<input-tag name="status" list="suggestions"></input-tag>
|
|
590
|
+
<datalist id="suggestions">
|
|
591
|
+
<option value="option1">Option 1</option>
|
|
592
|
+
<option value="option2">Option 2</option>
|
|
593
|
+
<option value="option3">Option 3</option>
|
|
594
|
+
</datalist>
|
|
595
|
+
`
|
|
596
|
+
const inputTag = document.querySelector('input-tag')
|
|
597
|
+
await waitForBasicInitialization(inputTag)
|
|
598
|
+
|
|
599
|
+
// Select first option (simulating actual autocomplete behavior)
|
|
600
|
+
simulateAutocompleteSelection(inputTag, 'option1', 'Option 1')
|
|
601
|
+
await waitForUpdate()
|
|
602
|
+
|
|
603
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
604
|
+
expect(getTagValues(inputTag)).to.deep.equal(['option1'])
|
|
605
|
+
|
|
606
|
+
// Try to select second option - should fail in single mode
|
|
607
|
+
simulateAutocompleteSelection(inputTag, 'option2', 'Option 2')
|
|
608
|
+
await waitForUpdate()
|
|
609
|
+
|
|
610
|
+
// Should still only have one tag (the first one)
|
|
611
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
612
|
+
expect(getTagValues(inputTag)).to.deep.equal(['option1'])
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
})
|