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.
Files changed (2) hide show
  1. package/dist/Select.svelte +105 -32
  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)
@@ -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-[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 -->
428
508
  <span
429
509
  role="button"
430
- tabindex="0"
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
- type="text"
457
- class="h-full min-w-[120px] flex-1 outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
458
- bind:this={searchEL}
459
- bind:value={filterInput}
460
- 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}
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="0"
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
- </span>
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
- class="dropdown menu bg-base-100 rounded-box z-50 mt-2 flex flex-col flex-nowrap gap-1 p-2 shadow outline m-0 {!dropdownOpen ? 'pointer-events-none' : ''}"
508
- style="position-anchor: {anchorName}; position: fixed; top: anchor(bottom); left: anchor(left); width: anchor-size(width)"
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 />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplesvelte",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "scripts": {
5
5
  "dev": "bun vite dev",
6
6
  "build": "bun vite build && bun run prepack",