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, JSX,
Match, Match,
mergeProps, mergeProps,
onCleanup,
onMount, onMount,
Show, Show,
Switch, Switch,
@@ -40,7 +41,7 @@ const SimpleOptions: Component<SimpleOptionsProps> = (props) => {
{(option) => ( {(option) => (
<div <div
class={cx( 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 && 'bg-primary-surface text-on-primary-surface',
option().value === props.active option().value === props.active
? 'hover:bg-primary-surface-hover hover:text-on-primary-surface' ? 'hover:bg-primary-surface-hover hover:text-on-primary-surface'
@@ -79,7 +80,7 @@ const GroupedOptions: Component<GroupedOptionsProps> = (props) => {
{(option) => ( {(option) => (
<div <div
class={cx( 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 && option().value === props.active &&
'bg-primary-surface text-on-primary-surface', 'bg-primary-surface text-on-primary-surface',
option().value === props.active option().value === props.active
@@ -123,6 +124,7 @@ const Select: Component<SelectProps> = (props) => {
let trigger: HTMLDivElement; let trigger: HTMLDivElement;
let optionFrame: HTMLDivElement; let optionFrame: HTMLDivElement;
let hideOptionTimer: number | undefined = undefined;
const [selected, setSelected] = createSignal<Option['value'] | undefined>(undefined); const [selected, setSelected] = createSignal<Option['value'] | undefined>(undefined);
const [optionVisible, setOptionVisible] = createSignal(false); const [optionVisible, setOptionVisible] = createSignal(false);
@@ -156,6 +158,70 @@ const Select: Component<SelectProps> = (props) => {
return placeholderContent; 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 ( return (
<div class="relative inline-block"> <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', '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' && mProps.variant === 'immersive' &&
'text-on-surface hover:not-aria-disabled:text-primary-hover aria-disabled:text-neutral-disabled', '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"> <div class="flex-1 flex flex-row items-center truncate">
<Dynamic component={displayLabel} /> <Dynamic component={displayLabel} />
</div> </div>
@@ -186,11 +255,24 @@ const Select: Component<SelectProps> = (props) => {
<Portal> <Portal>
<div <div
ref={optionFrame} 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> <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)}> <Match when={Array.isArray(mProps.options)}>
<SimpleOptions options={mProps.options} /> <SimpleOptions
options={mProps.options}
active={selected()}
onClick={handleOptionClick}
/>
</Match> </Match>
</Switch> </Switch>
</ScrollArea> </ScrollArea>