fix(a11y): SelectLite 键盘高亮自动滚动 + popover 横向 viewport clamp

pr-agent #419 第二轮 review 指出的两个 focus areas:

1. Keyboard scroll:长 dropdown 超过 280px 时 ArrowUp/Down 切换 highlight
   不会把高亮项 scroll into view,键盘用户看不到当前选中。新增 useEffect
   监听 highlight 改变,找到 data-option-index 对应 DOM 元素后
   scrollIntoView({ block: 'nearest' }),已可见的项不会被强行滚动。

2. Viewport clipping:popover 用 trigger.left 作 anchor,没有横向边界
   clamp。窗口右边的 select 弹出后可能溢出屏幕。positionPopover 改成测
   真实 popover 宽度(首次开打用 trigger 宽度估),然后把 left clamp 到
   [8, viewport - width - 8] 区间。

不动 keyboard 导航 / disabled 处理 / vertical flip 等已 verified 行为。
This commit is contained in:
baiqing committed 2026-05-12 14:28:19 +08:00
1 parent 70917cb027
commit 6c7598a858
1 file changed
+23 -5
@@ -80,12 +80,18 @@ export function SelectLite({
const trigger = triggerRef.current;
if (!trigger) return;
const rect = trigger.getBoundingClientRect();
// 默认在触发器下方;若下方空间放不下 popover(按 maxHeight 280 估),翻转向上以避免被视口裁剪。
const popoverMaxHeight = popoverRef.current?.getBoundingClientRect().height ?? 280;
const popoverRect = popoverRef.current?.getBoundingClientRect();
const popoverHeight = popoverRect?.height ?? 280;
const popoverWidth = popoverRect?.width ?? rect.width;
// 纵向:默认在触发器下方;若下方空间放不下 popover,翻转向上以避免被视口裁剪。
const spaceBelow = window.innerHeight - rect.bottom;
const flipUp = spaceBelow < popoverMaxHeight + 8 && rect.top > popoverMaxHeight + 8;
const top = flipUp ? rect.top - popoverMaxHeight - 4 : rect.bottom + 4;
setAnchor({ left: rect.left, top, width: rect.width });
const flipUp = spaceBelow < popoverHeight + 8 && rect.top > popoverHeight + 8;
const top = flipUp ? rect.top - popoverHeight - 4 : rect.bottom + 4;
// 横向:在窗口右边的 select 可能让 popover 溢出屏幕;clamp 到 [8, viewport - width - 8] 区间。
const minLeft = 8;
const maxLeft = Math.max(minLeft, window.innerWidth - popoverWidth - 8);
const left = Math.min(Math.max(rect.left, minLeft), maxLeft);
setAnchor({ left, top, width: rect.width });
}, []);
useLayoutEffect(() => {
@@ -100,6 +106,17 @@ export function SelectLite({
};
}, [open, positionPopover]);
// 键盘 ArrowUp/Down 改 highlight 后把高亮项 scroll into view —— 长 dropdown 超过
// maxHeight 280 时键盘用户能看到当前高亮。{ block: 'nearest' } 避免把已经可见的项
// 强行滚到顶部,符合 listbox 滚动惯例。
useEffect(() => {
if (!open || highlight < 0) return;
const target = popoverRef.current?.querySelector(
`[data-option-index="${highlight}"]`,
) as HTMLElement | null;
target?.scrollIntoView({ block: 'nearest' });
}, [highlight, open]);
useEffect(() => {
if (!open) return;
const handlePointerDown = (event: MouseEvent) => {
@@ -238,6 +255,7 @@ export function SelectLite({
return (
<div
key={option.value || `__opt_${index}`}
data-option-index={index}
role="option"
aria-selected={isSelected}
aria-disabled={option.disabled}