ReactJS+Fluent UI表格行焦点管理:如何检测离开TableRow?
解决方案:实现Save As行的焦点与外部点击管理
你的核心问题是TableRow的onBlur会在子元素(输入框、按钮)切换焦点时误触发,导致行提前消失。下面是针对所有需求的分步实现方案:
1. 修复onBlur误触发问题:用容器Ref判断焦点范围
首先给Save As行添加一个Ref,通过判断焦点是否仍在该行内部,避免子元素切换焦点时触发取消逻辑:
步骤1:创建容器Ref
在组件顶层定义Ref(不能放在getNewNameRow函数内):
const nameRowRef = useRef(null);
步骤2:修改TableRow的onBlur事件
替换原有的onBlur={btnSaveAsCancelClick},添加焦点范围判断:
<TableRow ref={nameRowRef} className={styles.tableNameRow} width="100%" onBlur={(e) => { // 只有当焦点完全离开当前行的所有元素时,才执行取消逻辑 if (!nameRowRef.current?.contains(e.relatedTarget)) { btnSaveAsCancelClick(); } }} >
2. 实现外部点击取消逻辑
除了焦点移出,点击行外区域也要触发取消。通过全局点击监听实现:
在组件顶层添加useEffect:
useEffect(() => { if (!showNewName) return; const handleClickOutside = (e) => { if (nameRowRef.current && !nameRowRef.current.contains(e.target)) { btnSaveAsCancelClick(); } }; // 添加全局点击监听 document.addEventListener('mousedown', handleClickOutside); // 组件卸载或隐藏行时移除监听 return () => document.removeEventListener('mousedown', handleClickOutside); }, [showNewName]);
3. 实现Tab键焦点控制(二选一)
针对需求4,有两种实现方式,按需选择:
方式A:Tab移出时直接隐藏行
给输入框和Cancel按钮添加键盘事件监听:
// 给输入框添加Shift+Tab监听(移出到行外时触发取消) <Input // 其他属性... onKeyDown={(e) => { if (e.key === 'Tab' && e.shiftKey) { e.preventDefault(); btnSaveAsCancelClick(); } }} /> // 给Cancel按钮添加Tab监听(移出到行外时触发取消) <Button // 其他属性... onKeyDown={(e) => { if (e.key === 'Tab' && !e.shiftKey) { e.preventDefault(); btnSaveAsCancelClick(); } }} />
方式B:锁定焦点在输入框、OK、Cancel之间循环
如果希望用户必须点击OK/Cancel才能退出,可让焦点在三个元素内循环:
步骤1:给三个元素添加Ref
const inputRef = useRef(null); const okBtnRef = useRef(null); const cancelBtnRef = useRef(null);
步骤2:添加键盘事件监听
在组件顶层添加useEffect:
useEffect(() => { if (!showNewName) return; // 输入框按Shift+Tab时,焦点跳转到Cancel按钮 const handleInputKeyDown = (e) => { if (e.key === 'Tab' && e.shiftKey) { e.preventDefault(); cancelBtnRef.current?.focus(); } }; // Cancel按钮按Tab时,焦点跳转到输入框 const handleCancelKeyDown = (e) => { if (e.key === 'Tab' && !e.shiftKey) { e.preventDefault(); inputRef.current?.focus(); } }; inputRef.current?.addEventListener('keydown', handleInputKeyDown); cancelBtnRef.current?.addEventListener('keydown', handleCancelKeyDown); return () => { inputRef.current?.removeEventListener('keydown', handleInputKeyDown); cancelBtnRef.current?.removeEventListener('keydown', handleCancelKeyDown); }; }, [showNewName]);
步骤3:给元素绑定Ref
<Input ref={inputRef} autoFocus {...其他属性} /> <Button ref={okBtnRef} {...其他属性} /> <Button ref={cancelBtnRef} {...其他属性} />
4. 完整代码整合示例
将上述逻辑整合到你的组件中,最终的核心代码片段如下:
function YourFormComponent() { const [showNewName, setShowNewName] = useState(false); const [enableSaveAsOK, setEnableSaveAsOK] = useState(false); const nameRowRef = useRef(null); const inputRef = useRef(null); const cancelBtnRef = useRef(null); const styles = useStyles(); // 外部点击监听 useEffect(() => { if (!showNewName) return; const handleClickOutside = (e) => { if (nameRowRef.current && !nameRowRef.current.contains(e.target)) { btnSaveAsCancelClick(); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [showNewName]); // 焦点循环逻辑(可选) useEffect(() => { if (!showNewName) return; const handleInputKeyDown = (e) => { if (e.key === 'Tab' && e.shiftKey) { e.preventDefault(); cancelBtnRef.current?.focus(); } }; const handleCancelKeyDown = (e) => { if (e.key === 'Tab' && !e.shiftKey) { e.preventDefault(); inputRef.current?.focus(); } }; inputRef.current?.addEventListener('keydown', handleInputKeyDown); cancelBtnRef.current?.addEventListener('keydown', handleCancelKeyDown); return () => { inputRef.current?.removeEventListener('keydown', handleInputKeyDown); cancelBtnRef.current?.removeEventListener('keydown', handleCancelKeyDown); }; }, [showNewName]); const btnSaveAsOKClick = () => { // 执行你的保存逻辑 setShowNewName(false); }; const btnSaveAsCancelClick = () => { // 执行你的取消逻辑(比如清空输入值) setShowNewName(false); }; const checkNewName = (e) => { setEnableSaveAsOK(e.target.value.trim() !== ''); }; const getNewNameRow = () => { const inputId = useId("newName"); return ( showNewName && ( <TableRow ref={nameRowRef} className={styles.tableNameRow} width="100%" onBlur={(e) => { if (!nameRowRef.current?.contains(e.relatedTarget)) { btnSaveAsCancelClick(); } }} > <TableCell className={styles.tableNoResize}> <label id={inputId}>New name:</label> </TableCell> <TableCell> <Input ref={inputRef} id={inputId} className={styles.input} aria-labelledby={inputId} onChange={checkNewName} autoFocus /> </TableCell> <TableCell className={styles.tableNoResize}> <Button appearance="primary" size="small" icon={<Checkmark20Filled />} onClick={btnSaveAsOKClick} disabled={!enableSaveAsOK} ></Button> <Button ref={cancelBtnRef} appearance="secondary" size="small" icon={<Dismiss20Filled />} onClick={btnSaveAsCancelClick} ></Button> </TableCell> </TableRow> ) ); }; // 其他表单渲染逻辑... }
内容的提问来源于stack exchange,提问作者S Bullen
相关产品推荐
相关产品推荐

