|
74 | 74 | let show = false; |
75 | 75 | let triggerElement: HTMLElement | null = null; |
76 | 76 | let contentElement: HTMLElement | null = null; |
| 77 | + let panelElement: HTMLElement | null = null; |
77 | 78 | let dropdownPosition = { top: 0, left: 0, maxHeight: undefined as number | undefined }; |
78 | 79 | let positionFrame: number | undefined; |
79 | 80 | let settleTimers: number[] = []; |
|
87 | 88 | }; |
88 | 89 | }; |
89 | 90 |
|
| 91 | + const measureContent = () => { |
| 92 | + if (!contentElement) return { width: 0, height: 0 }; |
| 93 | +
|
| 94 | + const previousMaxHeight = panelElement?.style.maxHeight; |
| 95 | + if (panelElement) panelElement.style.maxHeight = ''; |
| 96 | + const rect = contentElement.getBoundingClientRect(); |
| 97 | + if (panelElement && previousMaxHeight !== undefined) { |
| 98 | + panelElement.style.maxHeight = previousMaxHeight; |
| 99 | + } |
| 100 | +
|
| 101 | + return { width: rect.width, height: rect.height }; |
| 102 | + }; |
| 103 | +
|
90 | 104 | const visualViewportRect = () => { |
91 | 105 | const viewport = window.visualViewport; |
92 | 106 | return { |
|
100 | 114 | const updatePosition = () => { |
101 | 115 | if (!show || !triggerElement) return; |
102 | 116 | const rect = triggerElement.getBoundingClientRect(); |
103 | | - const contentRect = contentElement?.getBoundingClientRect(); |
104 | | - const contentWidth = contentRect?.width ?? 0; |
105 | | - const contentHeight = contentRect?.height ?? 0; |
| 117 | + const { width: contentWidth, height: contentHeight } = measureContent(); |
106 | 118 | const viewport = visualViewportRect(); |
107 | 119 | const viewportRight = viewport.left + viewport.width; |
108 | 120 | const viewportBottom = viewport.top + viewport.height; |
|
155 | 167 | } |
156 | 168 | }; |
157 | 169 |
|
| 170 | + const handleScroll = (event: Event) => { |
| 171 | + if (event.target instanceof Node && contentElement?.contains(event.target)) return; |
| 172 | + schedulePositionUpdate(); |
| 173 | + }; |
| 174 | +
|
158 | 175 | const toggleOpen = async () => { |
159 | 176 | show = !show; |
160 | 177 | if (show) { |
|
550 | 567 | tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b)); |
551 | 568 | } |
552 | 569 |
|
553 | | - window.addEventListener('scroll', schedulePositionUpdate, true); |
| 570 | + window.addEventListener('scroll', handleScroll, true); |
554 | 571 | window.visualViewport?.addEventListener('resize', scheduleSettledPositionUpdates); |
555 | 572 | window.visualViewport?.addEventListener('scroll', schedulePositionUpdate); |
556 | 573 |
|
557 | 574 | return () => { |
558 | 575 | if (positionFrame != null) cancelAnimationFrame(positionFrame); |
559 | 576 | for (const timer of settleTimers) window.clearTimeout(timer); |
560 | | - window.removeEventListener('scroll', schedulePositionUpdate, true); |
| 577 | + window.removeEventListener('scroll', handleScroll, true); |
561 | 578 | window.visualViewport?.removeEventListener('resize', scheduleSettledPositionUpdates); |
562 | 579 | window.visualViewport?.removeEventListener('scroll', schedulePositionUpdate); |
563 | 580 | }; |
|
642 | 659 |
|
643 | 660 | let listScrollTop = 0; |
644 | 661 | let listContainer; |
645 | | - $: listViewportHeight = Math.min(288, Math.max(96, (dropdownPosition.maxHeight ?? 352) - 64)); |
| 662 | + let listViewportHeight = 288; |
| 663 | +
|
| 664 | + const trackListViewport = (node: HTMLElement) => { |
| 665 | + const updateHeight = () => { |
| 666 | + listViewportHeight = node.clientHeight || 288; |
| 667 | + }; |
| 668 | +
|
| 669 | + updateHeight(); |
| 670 | +
|
| 671 | + if (!('ResizeObserver' in window)) { |
| 672 | + return { destroy() {} }; |
| 673 | + } |
| 674 | +
|
| 675 | + const observer = new ResizeObserver(updateHeight); |
| 676 | + observer.observe(node); |
| 677 | +
|
| 678 | + return { |
| 679 | + destroy() { |
| 680 | + observer.disconnect(); |
| 681 | + } |
| 682 | + }; |
| 683 | + }; |
646 | 684 |
|
647 | 685 | $: visibleStart = Math.max(0, Math.floor(listScrollTop / ITEM_HEIGHT) - OVERSCAN); |
648 | 686 | $: visibleEnd = Math.min( |
|
707 | 745 | <div |
708 | 746 | use:portal |
709 | 747 | bind:this={contentElement} |
710 | | - style="position: fixed; z-index: 9999; top: {dropdownPosition.top}px; left: {dropdownPosition.left}px; {dropdownPosition.maxHeight |
711 | | - ? `max-height: ${dropdownPosition.maxHeight}px;` |
712 | | - : ''}" |
| 748 | + style="position: fixed; z-index: 9999; top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;" |
713 | 749 | > |
714 | 750 | <div |
715 | | - class="z-40 {className} max-w-[calc(100vw-1rem)] justify-start rounded-xl border border-gray-100 bg-white p-0.5 shadow-lg outline-hidden dark:border-gray-800 dark:bg-gray-850 dark:text-white" |
| 751 | + bind:this={panelElement} |
| 752 | + class="z-40 {className} max-w-[calc(100vw-1rem)] justify-start rounded-xl border border-gray-100 bg-white p-0.5 shadow-lg outline-hidden dark:border-gray-800 dark:bg-gray-850 dark:text-white flex flex-col overflow-hidden" |
| 753 | + style={dropdownPosition.maxHeight ? `max-height: ${dropdownPosition.maxHeight}px;` : ''} |
716 | 754 | transition:flyAndScale |
717 | 755 | > |
718 | 756 | <slot> |
719 | 757 | {#if searchEnabled} |
720 | | - <div class="my-0.5 flex h-[1.6875rem] items-center gap-2"> |
| 758 | + <div class="my-0.5 flex h-[1.6875rem] shrink-0 items-center gap-2"> |
721 | 759 | <Search className="ml-2 size-3.5 shrink-0" strokeWidth="2" /> |
722 | 760 |
|
723 | 761 | <input |
|
788 | 826 | </div> |
789 | 827 | {/if} |
790 | 828 |
|
791 | | - <div class="group relative"> |
| 829 | + <div class="group relative flex min-h-0 flex-1 flex-col"> |
792 | 830 | {#if filteredItems.length === 0} |
793 | 831 | {#if items.length === 0 && $user?.role === 'admin'} |
794 | 832 | <div class="flex flex-col items-start justify-center py-6 px-4 text-start"> |
|
818 | 856 | {:else} |
819 | 857 | <!-- svelte-ignore a11y-no-static-element-interactions --> |
820 | 858 | <div |
821 | | - class="overflow-y-auto" |
822 | | - style="max-height: {listViewportHeight}px;" |
| 859 | + class="min-h-0 flex-1 overflow-y-auto" |
| 860 | + style="max-height: 288px;" |
823 | 861 | role="listbox" |
824 | 862 | aria-label={$i18n.t('Available models')} |
825 | 863 | bind:this={listContainer} |
| 864 | + use:trackListViewport |
826 | 865 | on:scroll={() => { |
827 | 866 | listScrollTop = listContainer.scrollTop; |
828 | 867 | }} |
|
936 | 975 | </div> |
937 | 976 |
|
938 | 977 | {#if showSetDefault} |
939 | | - <div class="flex items-center justify-end px-2 py-1 leading-none"> |
| 978 | + <div class="flex shrink-0 items-center justify-end px-2 py-1 leading-none"> |
940 | 979 | <button |
941 | 980 | type="button" |
942 | 981 | class="text-[0.65rem] font-normal leading-none text-gray-500 underline-offset-2 transition-colors duration-100 hover:text-gray-700 hover:underline dark:text-gray-500 dark:hover:text-gray-300" |
|
946 | 985 | </button> |
947 | 986 | </div> |
948 | 987 | {:else} |
949 | | - <div class="pb-1"></div> |
| 988 | + <div class="shrink-0 pb-1"></div> |
950 | 989 | {/if} |
951 | 990 |
|
952 | 991 | <div class="hidden w-[42rem]" /> |
|
0 commit comments