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.
Files changed (2) hide show
  1. package/dist/Select.svelte +99 -14
  2. package/package.json +1 -1
@@ -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-[200px] truncate">{selectedItems[0].label}</span>
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
- type="text"
451
- class="h-full min-w-[120px] flex-1 outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
452
- bind:this={searchEL}
453
- bind:value={filterInput}
454
- onclick={() => openDropdown()}
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
- </span>
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 />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplesvelte",
3
- "version": "2.4.5",
3
+ "version": "2.4.6",
4
4
  "scripts": {
5
5
  "dev": "bun vite dev",
6
6
  "build": "bun vite build && bun run prepack",