diff --git a/src/components/Select.tsx b/src/components/Select.tsx index d2beedc..fd2d7b0 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -10,6 +10,7 @@ import { JSX, Match, mergeProps, + onCleanup, onMount, Show, Switch, @@ -40,7 +41,7 @@ const SimpleOptions: Component = (props) => { {(option) => (
= (props) => { {(option) => (
= (props) => { let trigger: HTMLDivElement; let optionFrame: HTMLDivElement; + let hideOptionTimer: number | undefined = undefined; const [selected, setSelected] = createSignal(undefined); const [optionVisible, setOptionVisible] = createSignal(false); @@ -156,6 +158,70 @@ const Select: Component = (props) => { return placeholderContent; } }); + const adjustPosition = () => { + const triggerRect = trigger.getBoundingClientRect(); + const optionsRect = optionFrame.getBoundingClientRect(); + const distanceToBottom = window.innerHeight - triggerRect.bottom; + + if (triggerRect.top < distanceToBottom) { + optionFrame.style.top = `${triggerRect.bottom + 4}px`; + optionFrame.style.height = `${Math.min(distanceToBottom * 0.6, window.innerHeight * 0.4)}px`; + } else { + optionFrame.style.top = `${triggerRect.top - optionsRect.height - 4}px`; + optionFrame.style.height = `${Math.min(distanceToBottom * 0.6, window.innerHeight * 0.4)}px`; + } + optionFrame.style.left = `${triggerRect.left}px`; + optionFrame.style.width = `clamp(${triggerRect.width}px, ${triggerRect.width * 1.5}px, ${ + triggerRect.width * 2.5 + }px)`; + }; + const resizeObserver = new ResizeObserver(adjustPosition); + + createEffect(() => { + if (optionVisible()) { + adjustPosition(); + resizeObserver.observe(trigger); + } + }); + onCleanup(() => { + resizeObserver.unobserve(trigger); + resizeObserver.disconnect(); + clearTimeout(hideOptionTimer); + }); + + const handleMouseEnter = () => { + if (mProps.disabled) { + return; + } + clearTimeout(hideOptionTimer); + }; + const handleMouseLeave = () => { + if (mProps.disabled) { + return; + } + clearTimeout(hideOptionTimer); + hideOptionTimer = setTimeout(() => { + setOptionVisible(false); + }, 500); + }; + const handleActivation = () => { + if (mProps.disabled) { + return; + } + clearTimeout(hideOptionTimer); + setOptionVisible((prev) => !prev); + }; + const handleOptionClick = (value: Option['value']) => { + if (selected() !== value) { + setSelected(value); + mProps.onChange?.(value); + } else { + setSelected(undefined); + mProps.onChange?.(undefined); + } + setOptionVisible(false); + clearTimeout(hideOptionTimer); + }; return (
@@ -170,7 +236,10 @@ const Select: Component = (props) => { 'border-b border-solid pb-[calc(var(--spacing)*1.5-1px)] border-on-surface text-on-surface aria-disabled:border-neutral-disabled aria-disabled:text-neutral-disabled hover:not-aria-disabled:text-primary-hover hover:not-aria-disabled:border-primary-hover', mProps.variant === 'immersive' && 'text-on-surface hover:not-aria-disabled:text-primary-hover aria-disabled:text-neutral-disabled', - )}> + )} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + onClick={handleActivation}>
@@ -186,11 +255,24 @@ const Select: Component = (props) => {
+ class="absolute z-[15] w-auto rounded-sm bg-neutral-variant-active px-1 py-1 elevation-1" + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave}> - }> + + }> - +