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.
Files changed (2) hide show
  1. package/dist/Select.svelte +98 -14
  2. package/package.json +1 -1
@@ -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-[200px] truncate">{selectedItems[0].label}</span>
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
- 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()}
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
- </span>
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 />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplesvelte",
3
- "version": "2.4.5",
3
+ "version": "2.4.7",
4
4
  "scripts": {
5
5
  "dev": "bun vite dev",
6
6
  "build": "bun vite build && bun run prepack",