diff --git a/packages/components/hooks/useKeyboardNavigation.ts b/packages/components/hooks/useKeyboardNavigation.ts new file mode 100644 index 0000000000..064185b9a4 --- /dev/null +++ b/packages/components/hooks/useKeyboardNavigation.ts @@ -0,0 +1,75 @@ +import { useCallback, useEffect, useState } from 'react'; + +interface KeyboardNavigationProps { + options: any[]; + initialIndex: number; + onSelect: (option: any, e: React.KeyboardEvent) => void; +} + +const useKeyboardNavigation = ({ options, initialIndex, onSelect }: KeyboardNavigationProps) => { + const [hoverIndex, setHoverIndex] = useState(initialIndex); + + useEffect(() => { + setHoverIndex(initialIndex); + }, [initialIndex]); + + const findNextEnabledIndex = useCallback( + (startIndex: number, direction: 1 | -1) => { + if (!options || options.length === 0) return -1; + const len = options.length; + let i = startIndex; + for (let step = 0; step < len; step += 1) { + i = direction === 1 ? (i + 1) % len : (i - 1 + len) % len; + const opt = options[i]; + if (!opt?.disabled) return i; + } + return startIndex; + }, + [options], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (options.length === 0) return; + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setHoverIndex((prev) => { + const start = prev < 0 ? -1 : prev; + const next = findNextEnabledIndex(start, 1); + return next === start ? prev : next; + }); + break; + + case 'ArrowUp': + e.preventDefault(); + setHoverIndex((prev) => { + const start = prev < 0 ? 0 : prev; + const next = findNextEnabledIndex(start, -1); + return next === start ? prev : next; + }); + break; + + case 'Enter': + e.preventDefault(); + if (hoverIndex >= 0 && hoverIndex < options.length) { + const current = options[hoverIndex]; + onSelect(current, e); + } + break; + + default: + break; + } + }, + [options, hoverIndex, onSelect, findNextEnabledIndex], + ); + + return { + hoverIndex, + handleKeyDown, + }; +}; + +export default useKeyboardNavigation; diff --git a/packages/components/select/base/PopupContent.tsx b/packages/components/select/base/PopupContent.tsx index eed40ae167..0472b43f64 100644 --- a/packages/components/select/base/PopupContent.tsx +++ b/packages/components/select/base/PopupContent.tsx @@ -46,15 +46,10 @@ interface SelectPopupProps trigger: SelectValueChangeTrigger; }, ) => void; - /** - * 是否展示popup - */ showPopup: boolean; - /** - * 控制popup展示的函数 - */ - setShowPopup: (show: boolean) => void; + hoverIndex: number; children?: React.ReactNode; + setShowPopup: (show: boolean) => void; onCheckAllChange?: (checkAll: boolean, e: React.MouseEvent) => void; getPopupInstance?: () => HTMLDivElement; } @@ -177,10 +172,13 @@ const PopupContent = React.forwardRef((props, const { value: optionValue, label, disabled, children, ...restData } = item as TdOptionProps; // 当 keys 属性配置 content 作为 value 或 label 时,确保 restData 中也包含它, 不参与渲染计算 const { content } = item as TdOptionProps; - const shouldOmitContent = Object.values(keys || {}).includes('content'); + const shouldOmitContent = Object.values(keys || {}).includes('content'); return (