@juspay/svelte-ui-components 4.17.0 → 4.18.0
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/Select.svelte +311 -134
- package/dist/Select/properties.d.ts +18 -0
- package/dist-wc/index.js +1558 -1489
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
value = $bindable([]),
|
|
13
13
|
multiple = false,
|
|
14
14
|
searchable = false,
|
|
15
|
+
searchPosition = 'trigger',
|
|
15
16
|
placeholder = '',
|
|
16
17
|
disabled = false,
|
|
17
18
|
error = false,
|
|
@@ -64,6 +65,12 @@
|
|
|
64
65
|
const listboxId = `select-listbox-${instanceId}`;
|
|
65
66
|
const errorMessageId = `${listboxId}-error`;
|
|
66
67
|
|
|
68
|
+
/* `searchable` alone no longer says where the input goes, so every place
|
|
69
|
+
that used to branch on it now asks which of the two it meant. Filtering
|
|
70
|
+
still keys off `searchable`, since that is true in both placements. */
|
|
71
|
+
const searchInTrigger = $derived(searchable && searchPosition !== 'menu');
|
|
72
|
+
const searchInMenu = $derived(searchable && searchPosition === 'menu');
|
|
73
|
+
|
|
67
74
|
const hasErrorMessage = $derived(
|
|
68
75
|
error && typeof errorMessage === 'string' && errorMessage.trim().length > 0
|
|
69
76
|
);
|
|
@@ -222,7 +229,7 @@
|
|
|
222
229
|
onopen?.();
|
|
223
230
|
highlightedIndex = -1;
|
|
224
231
|
query = '';
|
|
225
|
-
if (
|
|
232
|
+
if (searchInTrigger) {
|
|
226
233
|
await tick();
|
|
227
234
|
if (searchInputEl !== null) {
|
|
228
235
|
searchInputEl.focus();
|
|
@@ -246,6 +253,11 @@
|
|
|
246
253
|
} else {
|
|
247
254
|
value = [id];
|
|
248
255
|
close();
|
|
256
|
+
// Focus was in the menu's own search box, which is now gone; without
|
|
257
|
+
// this it falls back to <body> and the keyboard user loses their place.
|
|
258
|
+
if (searchInMenu) {
|
|
259
|
+
triggerEl?.focus();
|
|
260
|
+
}
|
|
249
261
|
}
|
|
250
262
|
onchange?.(value);
|
|
251
263
|
}
|
|
@@ -319,6 +331,28 @@
|
|
|
319
331
|
}
|
|
320
332
|
}
|
|
321
333
|
|
|
334
|
+
const FOCUSABLE =
|
|
335
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
336
|
+
|
|
337
|
+
/* Options carry tabindex="-1", so this is the search box plus whatever the
|
|
338
|
+
consumer pinned via bottomContent -- the panel's real tab stops. */
|
|
339
|
+
function menuFocusables(): HTMLElement[] {
|
|
340
|
+
return dropdownEl === null ? [] : [...dropdownEl.querySelectorAll<HTMLElement>(FOCUSABLE)];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Leaving the menu forwards should continue from the trigger, as if the
|
|
344
|
+
panel had never been in the tab order -- which is literally true for a
|
|
345
|
+
portaled one, whose DOM position is the end of <body>. */
|
|
346
|
+
function focusAfterTrigger(): void {
|
|
347
|
+
if (triggerEl === null || typeof document === 'undefined') {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const tabbable = [...document.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(
|
|
351
|
+
(element) => element.closest('.select-dropdown') === null
|
|
352
|
+
);
|
|
353
|
+
tabbable.at(tabbable.indexOf(triggerEl) + 1)?.focus();
|
|
354
|
+
}
|
|
355
|
+
|
|
322
356
|
function handleKeydown(event: KeyboardEvent): void {
|
|
323
357
|
if (disabled) {
|
|
324
358
|
return;
|
|
@@ -357,7 +391,7 @@
|
|
|
357
391
|
case 'Escape':
|
|
358
392
|
if (open) {
|
|
359
393
|
close();
|
|
360
|
-
if (!
|
|
394
|
+
if (!searchInTrigger && triggerEl !== null) {
|
|
361
395
|
triggerEl.focus();
|
|
362
396
|
}
|
|
363
397
|
}
|
|
@@ -370,14 +404,69 @@
|
|
|
370
404
|
}
|
|
371
405
|
}
|
|
372
406
|
break;
|
|
373
|
-
case 'Tab':
|
|
374
|
-
if (open) {
|
|
407
|
+
case 'Tab': {
|
|
408
|
+
if (!open) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
/* Outside the in-menu variant, Tab always means focus is leaving. */
|
|
412
|
+
if (!searchInMenu) {
|
|
375
413
|
close();
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
const active = document.activeElement;
|
|
417
|
+
if (event.shiftKey) {
|
|
418
|
+
/* Backwards out of the trigger genuinely leaves the widget; every
|
|
419
|
+
other Shift+Tab walks back through the menu and must not close
|
|
420
|
+
it, or the search box could never be left and re-entered. */
|
|
421
|
+
if (active === triggerEl) {
|
|
422
|
+
close();
|
|
423
|
+
} else if (active === searchInputEl) {
|
|
424
|
+
/* The trigger is not the portaled panel's DOM predecessor, so
|
|
425
|
+
native Shift+Tab would land outside the widget entirely. */
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
triggerEl?.focus();
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
if (active === triggerEl) {
|
|
432
|
+
/* A portaled panel is appended to <body>, so it is not the
|
|
433
|
+
trigger's DOM-order successor and native Tab would skip the whole
|
|
434
|
+
menu. Moving focus explicitly makes the in-flow and portaled
|
|
435
|
+
variants answer Tab identically. */
|
|
436
|
+
event.preventDefault();
|
|
437
|
+
searchInputEl?.focus();
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
/* Forward Tab only leaves once there is nothing left to reach: the
|
|
441
|
+
search box is followed by any pinned bottom actions, which were
|
|
442
|
+
unreachable while Tab from the search closed the menu outright. */
|
|
443
|
+
if (active === menuFocusables().at(-1)) {
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
close();
|
|
446
|
+
focusAfterTrigger();
|
|
376
447
|
}
|
|
377
448
|
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* The panel handler exists for the pinned bottom actions, which have no
|
|
454
|
+
handler of their own. The search box already carries one, and letting its
|
|
455
|
+
bubbled event run a second time here moved the highlight twice per key. */
|
|
456
|
+
function handleMenuKeydown(event: KeyboardEvent): void {
|
|
457
|
+
if (event.target !== searchInputEl) {
|
|
458
|
+
handleKeydown(event);
|
|
378
459
|
}
|
|
379
460
|
}
|
|
380
461
|
|
|
462
|
+
/* Attached imperatively: the panel is a plain container, and a keydown
|
|
463
|
+
handler written in the template would demand an ARIA role that the
|
|
464
|
+
wrapper around the search box and the listbox must not claim. */
|
|
465
|
+
const menuKeys = (node: HTMLElement) => {
|
|
466
|
+
node.addEventListener('keydown', handleMenuKeydown);
|
|
467
|
+
return { destroy: () => node.removeEventListener('keydown', handleMenuKeydown) };
|
|
468
|
+
};
|
|
469
|
+
|
|
381
470
|
function handleSearchInput(event: Event): void {
|
|
382
471
|
if (!(event.target instanceof HTMLInputElement)) {
|
|
383
472
|
return;
|
|
@@ -442,7 +531,7 @@
|
|
|
442
531
|
aria-invalid={error ? 'true' : null}
|
|
443
532
|
aria-describedby={hasErrorMessage ? errorMessageId : null}
|
|
444
533
|
{...highlightedOptionId !== null ? { 'aria-activedescendant': highlightedOptionId } : {}}
|
|
445
|
-
tabindex={disabled ? -1 :
|
|
534
|
+
tabindex={disabled ? -1 : searchInTrigger ? -1 : 0}
|
|
446
535
|
>
|
|
447
536
|
{#if typeof leftIcon === 'string' && leftIcon.length > 0}
|
|
448
537
|
<Img
|
|
@@ -457,7 +546,7 @@
|
|
|
457
546
|
{#if multiple}
|
|
458
547
|
{#if typeof triggerSummary === 'function'}
|
|
459
548
|
{@render triggerSummary({ value, items })}
|
|
460
|
-
{#if
|
|
549
|
+
{#if searchInTrigger}
|
|
461
550
|
<input
|
|
462
551
|
class="select-search"
|
|
463
552
|
type="text"
|
|
@@ -485,7 +574,7 @@
|
|
|
485
574
|
{...typeof testId === 'string' ? { testId: `${testId}-pill-${id}` } : {}}
|
|
486
575
|
/>
|
|
487
576
|
{/each}
|
|
488
|
-
{#if
|
|
577
|
+
{#if searchInTrigger}
|
|
489
578
|
<input
|
|
490
579
|
class="select-search"
|
|
491
580
|
type="text"
|
|
@@ -506,7 +595,7 @@
|
|
|
506
595
|
<span class="select-placeholder">{placeholder}</span>
|
|
507
596
|
{/if}
|
|
508
597
|
{/if}
|
|
509
|
-
{:else if
|
|
598
|
+
{:else if searchInTrigger}
|
|
510
599
|
<input
|
|
511
600
|
class="select-search"
|
|
512
601
|
type="text"
|
|
@@ -562,146 +651,199 @@
|
|
|
562
651
|
</div>
|
|
563
652
|
{/if}
|
|
564
653
|
|
|
565
|
-
{#
|
|
566
|
-
|
|
567
|
-
class="select-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
:
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
654
|
+
{#snippet menuOptions()}
|
|
655
|
+
{#if filteredItems.length === 0}
|
|
656
|
+
<div class="select-empty">No results</div>
|
|
657
|
+
{:else}
|
|
658
|
+
{#each optionRows as row, index (row.kind === 'select-all' ? 'select-all' : row.item.id)}
|
|
659
|
+
{#if row.kind === 'select-all'}
|
|
660
|
+
<div
|
|
661
|
+
class="select-option select-all"
|
|
662
|
+
class:multi={multiple}
|
|
663
|
+
class:selected={allFilteredSelected}
|
|
664
|
+
class:highlighted={index === highlightedIndex}
|
|
665
|
+
role="option"
|
|
666
|
+
id={`${listboxId}-option-${index}`}
|
|
667
|
+
aria-selected={allFilteredSelected}
|
|
668
|
+
aria-label={selectAllIndeterminate
|
|
669
|
+
? `${selectAllLabel}, ${selectedFilteredCount} of ${filteredItems.length} selected`
|
|
670
|
+
: selectAllLabel}
|
|
671
|
+
tabindex="-1"
|
|
672
|
+
{...typeof testId === 'string'
|
|
673
|
+
? { 'data-pw': `${testId}-select-all`, testID: `${testId}-select-all` }
|
|
674
|
+
: {}}
|
|
675
|
+
onclick={toggleSelectAll}
|
|
676
|
+
onmouseenter={() => (highlightedIndex = index)}
|
|
677
|
+
>
|
|
678
|
+
{#if typeof optionIndicator === 'function'}
|
|
679
|
+
{@render optionIndicator({
|
|
680
|
+
checked: allFilteredSelected,
|
|
681
|
+
indeterminate: selectAllIndeterminate
|
|
682
|
+
})}
|
|
683
|
+
{:else}
|
|
684
|
+
<span
|
|
685
|
+
class="select-option-indicator"
|
|
686
|
+
class:checked={allFilteredSelected}
|
|
687
|
+
class:indeterminate={selectAllIndeterminate}
|
|
688
|
+
aria-hidden="true"
|
|
689
|
+
data-checked={allFilteredSelected ? 'true' : 'false'}
|
|
690
|
+
data-pw={typeof testId === 'string' ? `${testId}-select-all-indicator` : null}
|
|
691
|
+
testID={typeof testId === 'string' ? `${testId}-select-all-indicator` : null}
|
|
692
|
+
>
|
|
693
|
+
{#if allFilteredSelected}
|
|
694
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
695
|
+
<span class="select-option-check">{@html checkmarkSvg}</span>
|
|
696
|
+
{:else if selectAllIndeterminate}
|
|
697
|
+
<span
|
|
698
|
+
class="select-option-dash"
|
|
699
|
+
data-pw={typeof testId === 'string' ? `${testId}-select-all-dash` : null}
|
|
700
|
+
testID={typeof testId === 'string' ? `${testId}-select-all-dash` : null}
|
|
701
|
+
></span>
|
|
702
|
+
{/if}
|
|
703
|
+
</span>
|
|
704
|
+
{/if}
|
|
705
|
+
{selectAllLabel}
|
|
706
|
+
</div>
|
|
707
|
+
{:else}
|
|
708
|
+
<div
|
|
709
|
+
class="select-option"
|
|
710
|
+
class:multi={multiple}
|
|
711
|
+
class:tickable={showSelectedTick && !multiple}
|
|
712
|
+
class:selected={value.includes(row.item.id)}
|
|
713
|
+
class:highlighted={index === highlightedIndex}
|
|
714
|
+
role="option"
|
|
715
|
+
id={`${listboxId}-option-${index}`}
|
|
716
|
+
aria-selected={value.includes(row.item.id)}
|
|
717
|
+
tabindex="-1"
|
|
718
|
+
{...typeof row.item.testId === 'string'
|
|
719
|
+
? { 'data-pw': row.item.testId, testID: row.item.testId }
|
|
720
|
+
: typeof itemTestId === 'string'
|
|
721
|
+
? {
|
|
722
|
+
'data-pw': `${itemTestId}-${row.item.id}`,
|
|
723
|
+
testID: `${itemTestId}-${row.item.id}`
|
|
724
|
+
}
|
|
725
|
+
: typeof testId === 'string'
|
|
726
|
+
? { 'data-pw': `${testId}-${row.item.id}`, testID: `${testId}-${row.item.id}` }
|
|
727
|
+
: {}}
|
|
728
|
+
onclick={() => selectItem(row.item.id)}
|
|
729
|
+
onmouseenter={() => (highlightedIndex = index)}
|
|
730
|
+
>
|
|
731
|
+
{#if multiple}
|
|
602
732
|
{#if typeof optionIndicator === 'function'}
|
|
603
733
|
{@render optionIndicator({
|
|
604
|
-
checked:
|
|
605
|
-
indeterminate:
|
|
734
|
+
checked: value.includes(row.item.id),
|
|
735
|
+
indeterminate: false
|
|
606
736
|
})}
|
|
607
737
|
{:else}
|
|
608
738
|
<span
|
|
609
739
|
class="select-option-indicator"
|
|
610
|
-
class:checked={
|
|
611
|
-
class:indeterminate={selectAllIndeterminate}
|
|
740
|
+
class:checked={value.includes(row.item.id)}
|
|
612
741
|
aria-hidden="true"
|
|
613
|
-
data-checked={
|
|
614
|
-
data-pw={typeof testId === 'string'
|
|
615
|
-
|
|
742
|
+
data-checked={value.includes(row.item.id) ? 'true' : 'false'}
|
|
743
|
+
data-pw={typeof testId === 'string'
|
|
744
|
+
? `${testId}-option-indicator-${row.item.id}`
|
|
745
|
+
: null}
|
|
746
|
+
testID={typeof testId === 'string'
|
|
747
|
+
? `${testId}-option-indicator-${row.item.id}`
|
|
748
|
+
: null}
|
|
616
749
|
>
|
|
617
|
-
{#if
|
|
750
|
+
{#if value.includes(row.item.id)}
|
|
618
751
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
619
752
|
<span class="select-option-check">{@html checkmarkSvg}</span>
|
|
620
|
-
{:else if selectAllIndeterminate}
|
|
621
|
-
<span
|
|
622
|
-
class="select-option-dash"
|
|
623
|
-
data-pw={typeof testId === 'string' ? `${testId}-select-all-dash` : null}
|
|
624
|
-
testID={typeof testId === 'string' ? `${testId}-select-all-dash` : null}
|
|
625
|
-
></span>
|
|
626
753
|
{/if}
|
|
627
754
|
</span>
|
|
628
755
|
{/if}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
class
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
<span class="select-option-tick" aria-hidden="true">{@html checkmarkSvg}</span>
|
|
694
|
-
{/if}
|
|
695
|
-
</div>
|
|
696
|
-
{/if}
|
|
697
|
-
{/each}
|
|
698
|
-
{/if}
|
|
699
|
-
{#if typeof bottomContent === 'function'}
|
|
700
|
-
<div class="select-bottom-content">
|
|
701
|
-
{@render bottomContent()}
|
|
756
|
+
{/if}
|
|
757
|
+
{#if typeof row.item.icon === 'string' && row.item.icon.length > 0}
|
|
758
|
+
<Img inlineSvg src={row.item.icon} alt="" fallback="" classes="select-option-icon" />
|
|
759
|
+
{/if}
|
|
760
|
+
<span class="select-option-label">{row.item.label}</span>
|
|
761
|
+
{#if showSelectedTick && !multiple && value.includes(row.item.id)}
|
|
762
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
763
|
+
<span class="select-option-tick" aria-hidden="true">{@html checkmarkSvg}</span>
|
|
764
|
+
{/if}
|
|
765
|
+
</div>
|
|
766
|
+
{/if}
|
|
767
|
+
{/each}
|
|
768
|
+
{/if}
|
|
769
|
+
{#if typeof bottomContent === 'function' && !searchInMenu}
|
|
770
|
+
<div class="select-bottom-content">
|
|
771
|
+
{@render bottomContent()}
|
|
772
|
+
</div>
|
|
773
|
+
{/if}
|
|
774
|
+
{/snippet}
|
|
775
|
+
|
|
776
|
+
{#if open && !disabled}
|
|
777
|
+
{#if searchInMenu}
|
|
778
|
+
<!-- The panel itself carries no role here: a listbox may only contain
|
|
779
|
+
options, so the search box has to be a SIBLING of the listbox rather
|
|
780
|
+
than a child of it. `aria-controls` on the trigger still points at
|
|
781
|
+
the inner element that actually holds the options. -->
|
|
782
|
+
<div
|
|
783
|
+
class="select-dropdown"
|
|
784
|
+
class:select-dropdown-right={dropdownAlign === 'right'}
|
|
785
|
+
class:select-dropdown-portal={usePortal}
|
|
786
|
+
bind:this={dropdownEl}
|
|
787
|
+
bind:clientWidth={dropdownWidth}
|
|
788
|
+
bind:clientHeight={dropdownHeight}
|
|
789
|
+
style={portalStyle}
|
|
790
|
+
use:menuKeys
|
|
791
|
+
use:portalToBody
|
|
792
|
+
>
|
|
793
|
+
<input
|
|
794
|
+
class="select-menu-search"
|
|
795
|
+
type="search"
|
|
796
|
+
value={query}
|
|
797
|
+
oninput={handleSearchInput}
|
|
798
|
+
onkeydown={handleKeydown}
|
|
799
|
+
bind:this={searchInputEl}
|
|
800
|
+
placeholder={placeholder.length > 0 ? placeholder : 'Search'}
|
|
801
|
+
aria-label="Search options"
|
|
802
|
+
aria-controls={listboxId}
|
|
803
|
+
autocomplete="off"
|
|
804
|
+
data-pw={typeof testId === 'string' ? `${testId}-menu-search` : null}
|
|
805
|
+
testID={typeof testId === 'string' ? `${testId}-menu-search` : null}
|
|
806
|
+
/>
|
|
807
|
+
<!-- tabindex="-1" because the list scrolls, and a scroll container is
|
|
808
|
+
a tab stop of its own in Chromium -- which swallowed the Tab that
|
|
809
|
+
should reach the pinned bottom actions. The options are navigated
|
|
810
|
+
from the search box via aria-activedescendant, so the list itself
|
|
811
|
+
never needs to hold focus. -->
|
|
812
|
+
<div
|
|
813
|
+
class="select-menu-list"
|
|
814
|
+
role="listbox"
|
|
815
|
+
id={listboxId}
|
|
816
|
+
aria-multiselectable={multiple}
|
|
817
|
+
tabindex="-1"
|
|
818
|
+
>
|
|
819
|
+
{@render menuOptions()}
|
|
702
820
|
</div>
|
|
703
|
-
|
|
704
|
-
|
|
821
|
+
{#if typeof bottomContent === 'function'}
|
|
822
|
+
<!-- Outside the listbox: a listbox may only contain options, so
|
|
823
|
+
pinned actions (clear-all, apply) would be invalid children and
|
|
824
|
+
assistive tech would announce them as options. -->
|
|
825
|
+
<div class="select-bottom-content">
|
|
826
|
+
{@render bottomContent()}
|
|
827
|
+
</div>
|
|
828
|
+
{/if}
|
|
829
|
+
</div>
|
|
830
|
+
{:else}
|
|
831
|
+
<div
|
|
832
|
+
class="select-dropdown"
|
|
833
|
+
class:select-dropdown-right={dropdownAlign === 'right'}
|
|
834
|
+
class:select-dropdown-portal={usePortal}
|
|
835
|
+
bind:this={dropdownEl}
|
|
836
|
+
bind:clientWidth={dropdownWidth}
|
|
837
|
+
bind:clientHeight={dropdownHeight}
|
|
838
|
+
role="listbox"
|
|
839
|
+
id={listboxId}
|
|
840
|
+
aria-multiselectable={multiple}
|
|
841
|
+
style={portalStyle}
|
|
842
|
+
use:portalToBody
|
|
843
|
+
>
|
|
844
|
+
{@render menuOptions()}
|
|
845
|
+
</div>
|
|
846
|
+
{/if}
|
|
705
847
|
{/if}
|
|
706
848
|
</div>
|
|
707
849
|
|
|
@@ -910,6 +1052,41 @@
|
|
|
910
1052
|
z-index: var(--select-dropdown-z-index, 10);
|
|
911
1053
|
}
|
|
912
1054
|
|
|
1055
|
+
/* With the search box inside the panel, the PANEL must stop scrolling and
|
|
1056
|
+
the list scroll instead -- otherwise the search box scrolls away as soon
|
|
1057
|
+
as the user moves down the results, which is the one control they still
|
|
1058
|
+
need. The max-height moves down with the scrolling, so the panel keeps
|
|
1059
|
+
growing to fit the search box plus a list of the same height as before. */
|
|
1060
|
+
.select-dropdown:has(> .select-menu-list) {
|
|
1061
|
+
max-height: none;
|
|
1062
|
+
overflow-y: visible;
|
|
1063
|
+
display: flex;
|
|
1064
|
+
flex-direction: column;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.select-menu-list {
|
|
1068
|
+
max-height: var(--select-dropdown-max-height, 200px);
|
|
1069
|
+
overflow-y: auto;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.select-menu-search {
|
|
1073
|
+
flex: none;
|
|
1074
|
+
margin: var(--select-menu-search-margin, 6px);
|
|
1075
|
+
padding: var(--select-menu-search-padding, 6px 8px);
|
|
1076
|
+
border: var(--select-menu-search-border, 1px solid #cccccc);
|
|
1077
|
+
border-radius: var(--select-menu-search-border-radius, var(--radius, 4px));
|
|
1078
|
+
background: var(--select-menu-search-background, #ffffff);
|
|
1079
|
+
color: var(--select-menu-search-color, inherit);
|
|
1080
|
+
font-family: inherit;
|
|
1081
|
+
font-size: var(--select-menu-search-font-size, inherit);
|
|
1082
|
+
outline: none;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.select-menu-search:focus-visible {
|
|
1086
|
+
border-color: var(--select-menu-search-focus-border-color, #2563eb);
|
|
1087
|
+
box-shadow: var(--select-menu-search-focus-shadow, 0 0 0 2px rgba(37, 99, 235, 0.2));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
913
1090
|
.select-dropdown-right {
|
|
914
1091
|
left: auto;
|
|
915
1092
|
right: var(--select-dropdown-right, 0);
|
|
@@ -20,10 +20,28 @@ export type MandatorySelectProperties = {
|
|
|
20
20
|
items: SelectItem[] | string[];
|
|
21
21
|
};
|
|
22
22
|
export type SelectHierarchy = 'default' | 'ghost';
|
|
23
|
+
/** Placement of the `searchable` filter input. See `searchPosition`. */
|
|
24
|
+
export type SelectSearchPosition = 'trigger' | 'menu';
|
|
23
25
|
export type OptionalSelectProperties = {
|
|
24
26
|
value?: string[];
|
|
25
27
|
multiple?: boolean;
|
|
26
28
|
searchable?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Where the `searchable` filter input lives. `'trigger'` (the default) keeps
|
|
31
|
+
* today's behaviour: the input replaces the trigger's label and is focused
|
|
32
|
+
* automatically on open.
|
|
33
|
+
*
|
|
34
|
+
* `'menu'` puts it inside the open dropdown instead, which is what the
|
|
35
|
+
* design-system keyboard contract specifies — the trigger stays a plain
|
|
36
|
+
* combobox, and the search box is reached by Tab AFTER opening rather than
|
|
37
|
+
* by being focused for you. That difference is the point: with the input in
|
|
38
|
+
* the trigger there is no way to open the menu and keep focus on the
|
|
39
|
+
* combobox, so a keyboard user who wanted to arrow through the options had
|
|
40
|
+
* to type first.
|
|
41
|
+
*
|
|
42
|
+
* No effect unless `searchable` is also set.
|
|
43
|
+
*/
|
|
44
|
+
searchPosition?: SelectSearchPosition;
|
|
27
45
|
placeholder?: string;
|
|
28
46
|
disabled?: boolean;
|
|
29
47
|
/**
|