simplesvelte 2.4.5 → 2.4.7
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 +98 -14
- package/package.json +1 -1
package/dist/Select.svelte
CHANGED
|
@@ -253,6 +253,88 @@
|
|
|
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) openDropdown()
|
|
299
|
+
// Move to next option (or first option if none highlighted)
|
|
300
|
+
const currentPos = optionIndices.indexOf(highlightedIndex)
|
|
301
|
+
const nextPos = currentPos + 1
|
|
302
|
+
if (nextPos < optionIndices.length) {
|
|
303
|
+
highlightedIndex = optionIndices[nextPos]
|
|
304
|
+
scrollToHighlighted(highlightedIndex)
|
|
305
|
+
}
|
|
306
|
+
searchEL?.focus()
|
|
307
|
+
} else if (e.key === 'ArrowUp') {
|
|
308
|
+
e.preventDefault()
|
|
309
|
+
if (!dropdownOpen) openDropdown()
|
|
310
|
+
// Move to previous option (or last option if none highlighted)
|
|
311
|
+
const currentPos = optionIndices.indexOf(highlightedIndex)
|
|
312
|
+
const prevPos = currentPos >= 0 ? currentPos - 1 : optionIndices.length - 1
|
|
313
|
+
if (prevPos >= 0) {
|
|
314
|
+
highlightedIndex = optionIndices[prevPos]
|
|
315
|
+
scrollToHighlighted(highlightedIndex)
|
|
316
|
+
}
|
|
317
|
+
searchEL?.focus()
|
|
318
|
+
} else if (e.key === 'Enter') {
|
|
319
|
+
e.preventDefault()
|
|
320
|
+
if (!dropdownOpen) {
|
|
321
|
+
openDropdown()
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
if (highlightedIndex >= 0) {
|
|
325
|
+
const entry = flatList[highlightedIndex]
|
|
326
|
+
if (entry && entry.type === 'option') {
|
|
327
|
+
toggleItemSelection(entry.item.value)
|
|
328
|
+
if (multiple) searchEL?.focus()
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} else if (e.key === 'Escape') {
|
|
332
|
+
closeDropdown()
|
|
333
|
+
} else if (e.key === 'Tab') {
|
|
334
|
+
// Close dropdown on tab to allow normal tab navigation
|
|
335
|
+
closeDropdown()
|
|
336
|
+
}
|
|
337
|
+
}
|
|
256
338
|
|
|
257
339
|
// Virtual list implementation
|
|
258
340
|
let scrollTop = $state(0)
|
|
@@ -413,11 +495,6 @@
|
|
|
413
495
|
searchEL?.focus()
|
|
414
496
|
// Only clear filter in single-select mode; in multi-select, keep filter for continued searching
|
|
415
497
|
if (!multiple) filterInput = ''
|
|
416
|
-
}}
|
|
417
|
-
onkeydown={(e) => {
|
|
418
|
-
if (e.key === 'Escape') {
|
|
419
|
-
closeDropdown()
|
|
420
|
-
}
|
|
421
498
|
}}>
|
|
422
499
|
{#if multiple}
|
|
423
500
|
<!-- Multi-select display with condensed chips -->
|
|
@@ -425,7 +502,8 @@
|
|
|
425
502
|
{#if selectedItems.length > 0}
|
|
426
503
|
<!-- Show first selected item -->
|
|
427
504
|
<div class="badge badge-neutral bg-base-200 text-base-content gap-1">
|
|
428
|
-
<span class="max-w-
|
|
505
|
+
<span class="max-w-50 truncate">{selectedItems[0].label}</span>
|
|
506
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
429
507
|
<span
|
|
430
508
|
role="button"
|
|
431
509
|
tabindex="-1"
|
|
@@ -447,11 +525,12 @@
|
|
|
447
525
|
{/if}
|
|
448
526
|
<!-- Search input for filtering in multi-select -->
|
|
449
527
|
<input
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
528
|
+
type="text"
|
|
529
|
+
class="h-full min-w-30 flex-1 outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
|
|
530
|
+
bind:this={searchEL}
|
|
531
|
+
bind:value={filterInput}
|
|
532
|
+
onclick={() => openDropdown()}
|
|
533
|
+
onkeydown={handleKeydown}
|
|
455
534
|
placeholder="Search..."
|
|
456
535
|
required={required && (!Array.isArray(normalizedValue) || normalizedValue.length === 0)} />
|
|
457
536
|
</div>
|
|
@@ -467,10 +546,12 @@
|
|
|
467
546
|
filterInput = ''
|
|
468
547
|
openDropdown()
|
|
469
548
|
}}
|
|
549
|
+
onkeydown={handleKeydown}
|
|
470
550
|
{placeholder}
|
|
471
551
|
required={required && !normalizedValue} />
|
|
472
552
|
{/if}
|
|
473
553
|
{#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
|
|
554
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
474
555
|
<span
|
|
475
556
|
role="button"
|
|
476
557
|
tabindex="-1"
|
|
@@ -481,7 +562,7 @@
|
|
|
481
562
|
clearAll()
|
|
482
563
|
}}>
|
|
483
564
|
✕
|
|
484
|
-
|
|
565
|
+
</span>
|
|
485
566
|
{/if}
|
|
486
567
|
</button>
|
|
487
568
|
|
|
@@ -531,7 +612,7 @@
|
|
|
531
612
|
{/if}
|
|
532
613
|
|
|
533
614
|
{#if flatList.length > 0}
|
|
534
|
-
<div class="relative max-h-80 overflow-y-auto pr-2" use:scrollToSelected onscroll={handleScroll}>
|
|
615
|
+
<div class="relative max-h-80 overflow-y-auto pr-2" bind:this={scrollContainerEl} use:scrollToSelected onscroll={handleScroll}>
|
|
535
616
|
<!-- Virtual spacer for items before visible range -->
|
|
536
617
|
{#if visibleItems.startIndex > 0}
|
|
537
618
|
<div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
|
|
@@ -547,14 +628,16 @@
|
|
|
547
628
|
</li>
|
|
548
629
|
{:else}
|
|
549
630
|
{@const item = entry.item}
|
|
631
|
+
{@const flatIdx = visibleItems.startIndex + idx}
|
|
550
632
|
{@const isSelected = multiple
|
|
551
633
|
? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
|
|
552
634
|
: item.value === normalizedValue}
|
|
635
|
+
{@const isHighlighted = flatIdx === highlightedIndex}
|
|
553
636
|
<li style="height: {itemHeight}px;" role="option" aria-selected={isSelected}>
|
|
554
637
|
<button
|
|
555
638
|
class="flex h-full w-full items-center gap-2 {isSelected
|
|
556
639
|
? ' bg-primary text-primary-content hover:bg-primary/70!'
|
|
557
|
-
: ''}"
|
|
640
|
+
: ''} {isHighlighted && !isSelected ? 'bg-base-200' : ''}"
|
|
558
641
|
type="button"
|
|
559
642
|
onclick={(e) => {
|
|
560
643
|
e.stopPropagation()
|
|
@@ -564,6 +647,7 @@
|
|
|
564
647
|
{#if multiple}
|
|
565
648
|
<input
|
|
566
649
|
type="checkbox"
|
|
650
|
+
tabindex="-1"
|
|
567
651
|
class="checkbox checkbox-sm text-primary-content! pointer-events-none"
|
|
568
652
|
checked={isSelected}
|
|
569
653
|
readonly />
|