simplesvelte 2.4.5 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Select.svelte +99 -14
- package/package.json +1 -1
package/dist/Select.svelte
CHANGED
|
@@ -253,6 +253,89 @@
|
|
|
253
253
|
|
|
254
254
|
let searchEL: HTMLInputElement | undefined = $state(undefined)
|
|
255
255
|
let popoverEl: HTMLElement | undefined = $state(undefined)
|
|
256
|
+
let scrollContainerEl: HTMLDivElement | undefined = $state(undefined)
|
|
257
|
+
|
|
258
|
+
// Keyboard navigation
|
|
259
|
+
let highlightedIndex = $state(-1)
|
|
260
|
+
|
|
261
|
+
// Get only the option entries from flatList (skip headers)
|
|
262
|
+
let optionIndices = $derived.by(() => {
|
|
263
|
+
const indices: number[] = []
|
|
264
|
+
for (let i = 0; i < flatList.length; i++) {
|
|
265
|
+
if (flatList[i].type === 'option') indices.push(i)
|
|
266
|
+
}
|
|
267
|
+
return indices
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// Reset highlight when filter changes or dropdown closes
|
|
271
|
+
$effect(() => {
|
|
272
|
+
// Track filterInput to reset highlight when user types
|
|
273
|
+
void filterInput
|
|
274
|
+
if (!dropdownOpen) {
|
|
275
|
+
highlightedIndex = -1
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Scroll the virtual list to keep highlighted item visible
|
|
280
|
+
function scrollToHighlighted(index: number) {
|
|
281
|
+
if (!scrollContainerEl || index < 0) return
|
|
282
|
+
const targetTop = index * itemHeight
|
|
283
|
+
const targetBottom = targetTop + itemHeight
|
|
284
|
+
const viewTop = scrollContainerEl.scrollTop
|
|
285
|
+
const viewBottom = viewTop + containerHeight
|
|
286
|
+
|
|
287
|
+
if (targetTop < viewTop) {
|
|
288
|
+
scrollContainerEl.scrollTop = targetTop
|
|
289
|
+
} else if (targetBottom > viewBottom) {
|
|
290
|
+
scrollContainerEl.scrollTop = targetBottom - containerHeight
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Keyboard handler for arrow navigation
|
|
295
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
296
|
+
if (e.key === 'ArrowDown') {
|
|
297
|
+
e.preventDefault()
|
|
298
|
+
if (!dropdownOpen) {
|
|
299
|
+
openDropdown()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
// Move to next option
|
|
303
|
+
const currentPos = optionIndices.indexOf(highlightedIndex)
|
|
304
|
+
const nextPos = currentPos + 1
|
|
305
|
+
if (nextPos < optionIndices.length) {
|
|
306
|
+
highlightedIndex = optionIndices[nextPos]
|
|
307
|
+
scrollToHighlighted(highlightedIndex)
|
|
308
|
+
}
|
|
309
|
+
} else if (e.key === 'ArrowUp') {
|
|
310
|
+
e.preventDefault()
|
|
311
|
+
if (!dropdownOpen) {
|
|
312
|
+
openDropdown()
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
// Move to previous option
|
|
316
|
+
const currentPos = optionIndices.indexOf(highlightedIndex)
|
|
317
|
+
const prevPos = currentPos - 1
|
|
318
|
+
if (prevPos >= 0) {
|
|
319
|
+
highlightedIndex = optionIndices[prevPos]
|
|
320
|
+
scrollToHighlighted(highlightedIndex)
|
|
321
|
+
}
|
|
322
|
+
} else if (e.key === 'Enter') {
|
|
323
|
+
e.preventDefault()
|
|
324
|
+
if (!dropdownOpen) {
|
|
325
|
+
openDropdown()
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
if (highlightedIndex >= 0) {
|
|
329
|
+
const entry = flatList[highlightedIndex]
|
|
330
|
+
if (entry && entry.type === 'option') {
|
|
331
|
+
toggleItemSelection(entry.item.value)
|
|
332
|
+
if (multiple) searchEL?.focus()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} else if (e.key === 'Escape') {
|
|
336
|
+
closeDropdown()
|
|
337
|
+
}
|
|
338
|
+
}
|
|
256
339
|
|
|
257
340
|
// Virtual list implementation
|
|
258
341
|
let scrollTop = $state(0)
|
|
@@ -413,11 +496,6 @@
|
|
|
413
496
|
searchEL?.focus()
|
|
414
497
|
// Only clear filter in single-select mode; in multi-select, keep filter for continued searching
|
|
415
498
|
if (!multiple) filterInput = ''
|
|
416
|
-
}}
|
|
417
|
-
onkeydown={(e) => {
|
|
418
|
-
if (e.key === 'Escape') {
|
|
419
|
-
closeDropdown()
|
|
420
|
-
}
|
|
421
499
|
}}>
|
|
422
500
|
{#if multiple}
|
|
423
501
|
<!-- Multi-select display with condensed chips -->
|
|
@@ -425,7 +503,8 @@
|
|
|
425
503
|
{#if selectedItems.length > 0}
|
|
426
504
|
<!-- Show first selected item -->
|
|
427
505
|
<div class="badge badge-neutral bg-base-200 text-base-content gap-1">
|
|
428
|
-
<span class="max-w-
|
|
506
|
+
<span class="max-w-50 truncate">{selectedItems[0].label}</span>
|
|
507
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
429
508
|
<span
|
|
430
509
|
role="button"
|
|
431
510
|
tabindex="-1"
|
|
@@ -447,11 +526,12 @@
|
|
|
447
526
|
{/if}
|
|
448
527
|
<!-- Search input for filtering in multi-select -->
|
|
449
528
|
<input
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
529
|
+
type="text"
|
|
530
|
+
class="h-full min-w-30 flex-1 outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
|
|
531
|
+
bind:this={searchEL}
|
|
532
|
+
bind:value={filterInput}
|
|
533
|
+
onclick={() => openDropdown()}
|
|
534
|
+
onkeydown={handleKeydown}
|
|
455
535
|
placeholder="Search..."
|
|
456
536
|
required={required && (!Array.isArray(normalizedValue) || normalizedValue.length === 0)} />
|
|
457
537
|
</div>
|
|
@@ -467,10 +547,12 @@
|
|
|
467
547
|
filterInput = ''
|
|
468
548
|
openDropdown()
|
|
469
549
|
}}
|
|
550
|
+
onkeydown={handleKeydown}
|
|
470
551
|
{placeholder}
|
|
471
552
|
required={required && !normalizedValue} />
|
|
472
553
|
{/if}
|
|
473
554
|
{#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
|
|
555
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
474
556
|
<span
|
|
475
557
|
role="button"
|
|
476
558
|
tabindex="-1"
|
|
@@ -481,7 +563,7 @@
|
|
|
481
563
|
clearAll()
|
|
482
564
|
}}>
|
|
483
565
|
✕
|
|
484
|
-
|
|
566
|
+
</span>
|
|
485
567
|
{/if}
|
|
486
568
|
</button>
|
|
487
569
|
|
|
@@ -531,7 +613,7 @@
|
|
|
531
613
|
{/if}
|
|
532
614
|
|
|
533
615
|
{#if flatList.length > 0}
|
|
534
|
-
<div class="relative max-h-80 overflow-y-auto pr-2" use:scrollToSelected onscroll={handleScroll}>
|
|
616
|
+
<div class="relative max-h-80 overflow-y-auto pr-2" bind:this={scrollContainerEl} use:scrollToSelected onscroll={handleScroll}>
|
|
535
617
|
<!-- Virtual spacer for items before visible range -->
|
|
536
618
|
{#if visibleItems.startIndex > 0}
|
|
537
619
|
<div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
|
|
@@ -547,14 +629,16 @@
|
|
|
547
629
|
</li>
|
|
548
630
|
{:else}
|
|
549
631
|
{@const item = entry.item}
|
|
632
|
+
{@const flatIdx = visibleItems.startIndex + idx}
|
|
550
633
|
{@const isSelected = multiple
|
|
551
634
|
? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
|
|
552
635
|
: item.value === normalizedValue}
|
|
636
|
+
{@const isHighlighted = flatIdx === highlightedIndex}
|
|
553
637
|
<li style="height: {itemHeight}px;" role="option" aria-selected={isSelected}>
|
|
554
638
|
<button
|
|
555
639
|
class="flex h-full w-full items-center gap-2 {isSelected
|
|
556
640
|
? ' bg-primary text-primary-content hover:bg-primary/70!'
|
|
557
|
-
: ''}"
|
|
641
|
+
: ''} {isHighlighted && !isSelected ? 'bg-base-200' : ''}"
|
|
558
642
|
type="button"
|
|
559
643
|
onclick={(e) => {
|
|
560
644
|
e.stopPropagation()
|
|
@@ -564,6 +648,7 @@
|
|
|
564
648
|
{#if multiple}
|
|
565
649
|
<input
|
|
566
650
|
type="checkbox"
|
|
651
|
+
tabindex="-1"
|
|
567
652
|
class="checkbox checkbox-sm text-primary-content! pointer-events-none"
|
|
568
653
|
checked={isSelected}
|
|
569
654
|
readonly />
|