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