From 6c81c22c7241d6fdd7bd888e18c76671c9c0287a Mon Sep 17 00:00:00 2001 From: Vixalie Date: Thu, 21 Aug 2025 06:09:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(Select):=20=E4=BC=98=E5=8C=96=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E6=A1=86=E4=BD=8D=E7=BD=AE=E8=B0=83=E6=95=B4=E5=92=8C?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加光标悬停样式,增强用户体验 - 实现下拉框位置动态调整,适应不同屏幕尺寸 - 增加鼠标移入移出事件处理,优化下拉框显示逻辑 - 修复下拉框颜色样式,提升视觉一致性 --- src/components/Select.tsx | 94 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 6 deletions(-) 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}> - }> + + }> - +