feat(Select): 优化下拉框位置调整和交互逻辑

- 添加光标悬停样式,增强用户体验
- 实现下拉框位置动态调整,适应不同屏幕尺寸
- 增加鼠标移入移出事件处理,优化下拉框显示逻辑
- 修复下拉框颜色样式,提升视觉一致性
This commit is contained in:
Vixalie
2025-08-21 06:09:44 +08:00
parent e5e01610f5
commit 6c81c22c72

View File

@@ -10,6 +10,7 @@ import {
JSX,
Match,
mergeProps,
onCleanup,
onMount,
Show,
Switch,
@@ -40,7 +41,7 @@ const SimpleOptions: Component<SimpleOptionsProps> = (props) => {
{(option) => (
<div
class={cx(
'rounded-sm px-2 py-1 transition-colors duration-100',
'rounded-sm px-2 py-1 transition-colors duration-100 cursor-pointer',
option().value === props.active && 'bg-primary-surface text-on-primary-surface',
option().value === props.active
? 'hover:bg-primary-surface-hover hover:text-on-primary-surface'
@@ -79,7 +80,7 @@ const GroupedOptions: Component<GroupedOptionsProps> = (props) => {
{(option) => (
<div
class={cx(
'rounded-sm px-2 py-1 transition-colors duration-100',
'rounded-sm px-2 py-1 transition-colors duration-100 cursor-pointer',
option().value === props.active &&
'bg-primary-surface text-on-primary-surface',
option().value === props.active
@@ -123,6 +124,7 @@ const Select: Component<SelectProps> = (props) => {
let trigger: HTMLDivElement;
let optionFrame: HTMLDivElement;
let hideOptionTimer: number | undefined = undefined;
const [selected, setSelected] = createSignal<Option['value'] | undefined>(undefined);
const [optionVisible, setOptionVisible] = createSignal(false);
@@ -156,6 +158,70 @@ const Select: Component<SelectProps> = (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 (
<div class="relative inline-block">
@@ -170,7 +236,10 @@ const Select: Component<SelectProps> = (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}>
<div class="flex-1 flex flex-row items-center truncate">
<Dynamic component={displayLabel} />
</div>
@@ -186,11 +255,24 @@ const Select: Component<SelectProps> = (props) => {
<Portal>
<div
ref={optionFrame}
class="absolute z-[15] w-auto rounded-sm bg-surface-active px-1 py-1 elevation-1">
class="absolute z-[15] w-auto rounded-sm bg-neutral-variant-active px-1 py-1 elevation-1"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<ScrollArea enableY fullSize>
<Switch fallback={<GroupedOptions options={mProps.options} />}>
<Switch
fallback={
<GroupedOptions
options={mProps.options}
active={selected()}
onClick={handleOptionClick}
/>
}>
<Match when={Array.isArray(mProps.options)}>
<SimpleOptions options={mProps.options} />
<SimpleOptions
options={mProps.options}
active={selected()}
onClick={handleOptionClick}
/>
</Match>
</Switch>
</ScrollArea>