simplesvelte 2.4.4 → 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 +105 -32
- 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)
|
|
@@ -405,6 +488,7 @@
|
|
|
405
488
|
aria-expanded={dropdownOpen}
|
|
406
489
|
aria-haspopup="listbox"
|
|
407
490
|
aria-controls={popoverId}
|
|
491
|
+
tabindex="-1"
|
|
408
492
|
class="select relative h-max min-h-10 w-full min-w-12 cursor-pointer bg-none! pr-1 text-left"
|
|
409
493
|
style="anchor-name: {anchorName}"
|
|
410
494
|
title={tooltipText}
|
|
@@ -412,11 +496,6 @@
|
|
|
412
496
|
searchEL?.focus()
|
|
413
497
|
// Only clear filter in single-select mode; in multi-select, keep filter for continued searching
|
|
414
498
|
if (!multiple) filterInput = ''
|
|
415
|
-
}}
|
|
416
|
-
onkeydown={(e) => {
|
|
417
|
-
if (e.key === 'Escape') {
|
|
418
|
-
closeDropdown()
|
|
419
|
-
}
|
|
420
499
|
}}>
|
|
421
500
|
{#if multiple}
|
|
422
501
|
<!-- Multi-select display with condensed chips -->
|
|
@@ -424,21 +503,15 @@
|
|
|
424
503
|
{#if selectedItems.length > 0}
|
|
425
504
|
<!-- Show first selected item -->
|
|
426
505
|
<div class="badge badge-neutral bg-base-200 text-base-content gap-1">
|
|
427
|
-
<span class="max-w-
|
|
506
|
+
<span class="max-w-50 truncate">{selectedItems[0].label}</span>
|
|
507
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
428
508
|
<span
|
|
429
509
|
role="button"
|
|
430
|
-
tabindex="
|
|
510
|
+
tabindex="-1"
|
|
431
511
|
class="btn btn-xs btn-circle btn-ghost hover:bg-base-300"
|
|
432
512
|
onclick={(e) => {
|
|
433
513
|
e.stopPropagation()
|
|
434
514
|
removeSelectedItem(selectedItems[0].value)
|
|
435
|
-
}}
|
|
436
|
-
onkeydown={(e) => {
|
|
437
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
438
|
-
e.preventDefault()
|
|
439
|
-
e.stopPropagation()
|
|
440
|
-
removeSelectedItem(selectedItems[0].value)
|
|
441
|
-
}
|
|
442
515
|
}}>
|
|
443
516
|
✕
|
|
444
517
|
</span>
|
|
@@ -453,11 +526,12 @@
|
|
|
453
526
|
{/if}
|
|
454
527
|
<!-- Search input for filtering in multi-select -->
|
|
455
528
|
<input
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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}
|
|
461
535
|
placeholder="Search..."
|
|
462
536
|
required={required && (!Array.isArray(normalizedValue) || normalizedValue.length === 0)} />
|
|
463
537
|
</div>
|
|
@@ -473,28 +547,23 @@
|
|
|
473
547
|
filterInput = ''
|
|
474
548
|
openDropdown()
|
|
475
549
|
}}
|
|
550
|
+
onkeydown={handleKeydown}
|
|
476
551
|
{placeholder}
|
|
477
552
|
required={required && !normalizedValue} />
|
|
478
553
|
{/if}
|
|
479
554
|
{#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
|
|
555
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
480
556
|
<span
|
|
481
557
|
role="button"
|
|
482
|
-
tabindex="
|
|
558
|
+
tabindex="-1"
|
|
483
559
|
class="btn btn-sm btn-circle btn-ghost bg-base-100 absolute top-1 right-1"
|
|
484
560
|
onclick={(e) => {
|
|
485
561
|
e.preventDefault()
|
|
486
562
|
e.stopPropagation()
|
|
487
563
|
clearAll()
|
|
488
|
-
}}
|
|
489
|
-
onkeydown={(e) => {
|
|
490
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
491
|
-
e.preventDefault()
|
|
492
|
-
e.stopPropagation()
|
|
493
|
-
clearAll()
|
|
494
|
-
}
|
|
495
564
|
}}>
|
|
496
565
|
✕
|
|
497
|
-
|
|
566
|
+
</span>
|
|
498
567
|
{/if}
|
|
499
568
|
</button>
|
|
500
569
|
|
|
@@ -504,8 +573,9 @@
|
|
|
504
573
|
id={popoverId}
|
|
505
574
|
popover
|
|
506
575
|
role="listbox"
|
|
507
|
-
|
|
508
|
-
|
|
576
|
+
inert={!dropdownOpen}
|
|
577
|
+
class="dropdown menu bg-base-100 rounded-box z-50 flex flex-col flex-nowrap gap-1 p-2 shadow outline m-0"
|
|
578
|
+
style="position-anchor: {anchorName}; position: fixed; top: anchor(bottom); left: anchor(left); width: anchor-size(width); margin-block: 0.5rem; position-try-fallbacks: flip-block;"
|
|
509
579
|
ontoggle={handlePopoverToggle}>
|
|
510
580
|
{#if multiple && filteredItems.length > 1}
|
|
511
581
|
<!-- Select All / Clear All options for multi-select -->
|
|
@@ -543,7 +613,7 @@
|
|
|
543
613
|
{/if}
|
|
544
614
|
|
|
545
615
|
{#if flatList.length > 0}
|
|
546
|
-
<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}>
|
|
547
617
|
<!-- Virtual spacer for items before visible range -->
|
|
548
618
|
{#if visibleItems.startIndex > 0}
|
|
549
619
|
<div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
|
|
@@ -559,14 +629,16 @@
|
|
|
559
629
|
</li>
|
|
560
630
|
{:else}
|
|
561
631
|
{@const item = entry.item}
|
|
632
|
+
{@const flatIdx = visibleItems.startIndex + idx}
|
|
562
633
|
{@const isSelected = multiple
|
|
563
634
|
? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
|
|
564
635
|
: item.value === normalizedValue}
|
|
636
|
+
{@const isHighlighted = flatIdx === highlightedIndex}
|
|
565
637
|
<li style="height: {itemHeight}px;" role="option" aria-selected={isSelected}>
|
|
566
638
|
<button
|
|
567
639
|
class="flex h-full w-full items-center gap-2 {isSelected
|
|
568
640
|
? ' bg-primary text-primary-content hover:bg-primary/70!'
|
|
569
|
-
: ''}"
|
|
641
|
+
: ''} {isHighlighted && !isSelected ? 'bg-base-200' : ''}"
|
|
570
642
|
type="button"
|
|
571
643
|
onclick={(e) => {
|
|
572
644
|
e.stopPropagation()
|
|
@@ -576,6 +648,7 @@
|
|
|
576
648
|
{#if multiple}
|
|
577
649
|
<input
|
|
578
650
|
type="checkbox"
|
|
651
|
+
tabindex="-1"
|
|
579
652
|
class="checkbox checkbox-sm text-primary-content! pointer-events-none"
|
|
580
653
|
checked={isSelected}
|
|
581
654
|
readonly />
|