feat(Select): 优化下拉框位置调整和交互逻辑
- 添加光标悬停样式,增强用户体验 - 实现下拉框位置动态调整,适应不同屏幕尺寸 - 增加鼠标移入移出事件处理,优化下拉框显示逻辑 - 修复下拉框颜色样式,提升视觉一致性
This commit is contained in:
@@ -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>
|
||||
|
Reference in New Issue
Block a user