|
101 | 101 |
|
102 | 102 | // Generate unique ID for popover |
103 | 103 | const popoverId = `select-popover-${Math.random().toString(36).slice(2, 9)}` |
| 104 | + const anchorName = `--anchor-${popoverId}` |
104 | 105 |
|
105 | 106 | // Ensure value is properly typed for single/multiple mode |
106 | 107 | // Normalize on access rather than maintaining separate state |
|
258 | 259 | }) |
259 | 260 |
|
260 | 261 | let searchEL: HTMLInputElement | undefined = $state(undefined) |
261 | | - let triggerEl: HTMLButtonElement | undefined = $state(undefined) |
262 | 262 | let popoverEl: HTMLElement | undefined = $state(undefined) |
263 | 263 | let scrollContainerEl: HTMLDivElement | undefined = $state(undefined) |
264 | 264 |
|
|
347 | 347 | // Virtual list implementation |
348 | 348 | let scrollTop = $state(0) |
349 | 349 | const itemHeight = 40 // Approximate height of each item in pixels |
350 | | - const maxContainerHeight = 320 // max-h-80 = 320px |
351 | | - const minContainerHeight = 120 |
352 | | - let listViewportHeight = $state(maxContainerHeight) |
| 350 | + const listViewportHeight = 320 // max-h-80 = 320px |
353 | 351 | let visibleCount = $derived(Math.ceil(listViewportHeight / itemHeight) + 2) // Add buffer items |
354 | | - let dropdownPanelStyle = $state('') |
355 | | - const dropdownGap = 8 |
356 | | -
|
357 | | - function updateDropdownPosition() { |
358 | | - if (!triggerEl || !popoverEl) return |
359 | | -
|
360 | | - const rect = triggerEl.getBoundingClientRect() |
361 | | - const viewportHeight = window.innerHeight |
362 | | - const viewportWidth = window.innerWidth |
363 | | - const viewportPadding = 8 |
364 | | -
|
365 | | - const spaceBelow = viewportHeight - rect.bottom - dropdownGap - viewportPadding |
366 | | - const spaceAbove = rect.top - dropdownGap - viewportPadding |
367 | | - const openUpward = spaceBelow < minContainerHeight && spaceAbove > spaceBelow |
368 | | -
|
369 | | - const availableHeight = Math.max(minContainerHeight, openUpward ? spaceAbove : spaceBelow) |
370 | | - listViewportHeight = Math.min(maxContainerHeight, availableHeight) |
371 | | -
|
372 | | - const maxLeft = Math.max(viewportPadding, viewportWidth - rect.width - viewportPadding) |
373 | | - const left = Math.min(Math.max(viewportPadding, rect.left), maxLeft) |
374 | | -
|
375 | | - const minWidthStyle = dropdownMinWidth ? `min-width: ${dropdownMinWidth};` : '' |
376 | | - if (openUpward) { |
377 | | - const bottom = Math.max(viewportPadding, viewportHeight - rect.top + dropdownGap) |
378 | | - dropdownPanelStyle = `position: fixed; left: ${left}px; bottom: ${bottom}px; width: ${rect.width}px; max-height: ${availableHeight}px; ${minWidthStyle}` |
379 | | - } else { |
380 | | - const top = Math.max(viewportPadding, rect.bottom + dropdownGap) |
381 | | - dropdownPanelStyle = `position: fixed; left: ${left}px; top: ${top}px; width: ${rect.width}px; max-height: ${availableHeight}px; ${minWidthStyle}` |
382 | | - } |
383 | | - } |
384 | | -
|
385 | | - $effect(() => { |
386 | | - if (!dropdownOpen) return |
387 | | -
|
388 | | - updateDropdownPosition() |
389 | | - const reposition = () => updateDropdownPosition() |
390 | | -
|
391 | | - window.addEventListener('resize', reposition) |
392 | | - window.addEventListener('scroll', reposition, true) |
393 | | -
|
394 | | - return () => { |
395 | | - window.removeEventListener('resize', reposition) |
396 | | - window.removeEventListener('scroll', reposition, true) |
397 | | - } |
398 | | - }) |
399 | 352 |
|
400 | 353 | // Calculate visible items based on scroll position (for flatList) |
401 | 354 | let visibleItems = $derived.by(() => { |
|
529 | 482 | function handlePopoverToggle(e: ToggleEvent) { |
530 | 483 | dropdownOpen = e.newState === 'open' |
531 | 484 | if (dropdownOpen) { |
532 | | - requestAnimationFrame(() => { |
533 | | - updateDropdownPosition() |
534 | | - if (scrollContainerEl) scrollTop = scrollContainerEl.scrollTop |
535 | | - }) |
| 485 | + if (scrollContainerEl) scrollTop = scrollContainerEl.scrollTop |
536 | 486 | searchEL?.focus() |
537 | 487 | } else { |
538 | 488 | hasScrolledOnOpen = false |
|
555 | 505 | {#if !disabled} |
556 | 506 | <!-- Trigger button with popover target and anchor positioning --> |
557 | 507 | <button |
558 | | - bind:this={triggerEl} |
559 | 508 | type="button" |
560 | 509 | popovertarget={popoverId} |
561 | 510 | popovertargetaction="show" |
|
565 | 514 | aria-controls={popoverId} |
566 | 515 | tabindex="-1" |
567 | 516 | class="select relative h-max min-h-10 w-full min-w-12 cursor-pointer bg-none! pr-1 text-left" |
| 517 | + style="anchor-name: {anchorName}" |
568 | 518 | title={tooltipText} |
569 | 519 | onclick={() => { |
570 | 520 | searchEL?.focus() |
|
659 | 609 | role="listbox" |
660 | 610 | inert={!dropdownOpen} |
661 | 611 | class="menu bg-base-100 rounded-box z-50 m-0 flex flex-col flex-nowrap gap-1 overflow-hidden p-2 shadow outline" |
662 | | - style={dropdownPanelStyle} |
| 612 | + style="position: fixed; position-anchor: {anchorName}; top: anchor(bottom); left: anchor(left); width: anchor-size(width); max-width: calc(100vw - 1rem); margin-top: 0.5rem; margin-bottom: 0;{dropdownMinWidth ? ` min-width: ${dropdownMinWidth};` : ''} position-try-fallbacks: --select-above, --select-below-right, --select-above-right;" |
663 | 613 | ontoggle={handlePopoverToggle}> |
664 | 614 | {#if multiple && filteredItems.length > 1} |
665 | 615 | <!-- Select All / Clear All options for multi-select --> |
|
778 | 728 | </Label> |
779 | 729 |
|
780 | 730 | <style> |
| 731 | + @position-try --select-above { |
| 732 | + top: unset; |
| 733 | + bottom: anchor(top); |
| 734 | + left: anchor(left); |
| 735 | + right: unset; |
| 736 | + margin-top: 0; |
| 737 | + margin-bottom: 0.5rem; |
| 738 | + } |
| 739 | +
|
| 740 | + @position-try --select-below-right { |
| 741 | + top: anchor(bottom); |
| 742 | + bottom: unset; |
| 743 | + left: unset; |
| 744 | + right: anchor(right); |
| 745 | + margin-top: 0.5rem; |
| 746 | + margin-bottom: 0; |
| 747 | + } |
| 748 | +
|
| 749 | + @position-try --select-above-right { |
| 750 | + top: unset; |
| 751 | + bottom: anchor(top); |
| 752 | + left: unset; |
| 753 | + right: anchor(right); |
| 754 | + margin-top: 0; |
| 755 | + margin-bottom: 0.5rem; |
| 756 | + } |
| 757 | +
|
781 | 758 | /* The `menu` DaisyUI class applies display:flex as an author style, which has higher |
782 | 759 | cascade priority than the UA stylesheet's display:none for closed [popover] elements. |
783 | 760 | This rule restores the correct hide behavior without relying on UA defaults. */ |
|
0 commit comments