React Material UI:在blur事件显示Dialog并避免交互时重复触发onBlur
解决方案
问题根源
弹窗显示时,用户点击OK或其他区域会触发TextField的onBlur事件,再次调用validateItem方法导致弹窗重复弹出。本质是Dialog获取焦点时,TextField失去焦点触发了onBlur,而原代码未做判断,只要触发onBlur就执行验证逻辑。
具体修改步骤
1. 补全状态与DOM引用
要实现清空和聚焦功能,需要存储每个TextField的输入值、记录出错字段的位置,同时用useRef保存每个TextField的DOM节点,方便后续聚焦操作:
// 给初始数据添加inputValue字段存储输入值,默认空字符串 const [prompts, setPrompts] = useState<ServerData[]>( initialPrompts.map(item => ({ ...item, inputValue: "", isValidateditem: false })) ); const [errorDialogOpen, setErrorDialogOpen] = useState(false); const [errorErrorMsg, setErrorErrorMsg] = useState(''); const [invalidFieldIndex, setInvalidFieldIndex] = useState<number | null>(null); // 记录出错字段的下标 const [invalidFieldPromptId, setInvalidFieldPromptId] = useState<number | null>(null); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); // 存储每个TextField的DOM引用
2. 拦截重复验证请求
在validateItem开头添加判断,只要弹窗处于打开状态,直接返回不执行验证,避免重复触发弹窗:
async function validateItem(e: any, promptId: number, arrIndex: number) { // 弹窗已打开时,跳过所有验证逻辑 if (errorDialogOpen) return; const inputValue = e.target.value; // 空值可自行决定是否跳过,这里加判断避免无效请求 if (!inputValue.trim()) return; const response = await fetch(`http://localhost:5276/Validateitems?promptId=${promptId}&item=${inputValue}`); if (!response.ok) { const text = await response.text(); setErrorErrorMsg(text); // 记录出错字段的信息,后续关闭弹窗时使用 setInvalidFieldIndex(arrIndex); setInvalidFieldPromptId(promptId); handleDialogOpen(); return; } const data = await response.json(); // 验证通过,更新字段的验证状态和输入值 const updatedPrompts = prompts.map(x => x.promptId === promptId ? { ...x, isValidateditem: true, inputValue } : x ); setPrompts(updatedPrompts); }
3. 弹窗关闭时清空并聚焦目标字段
修改handleDialogClose方法,在弹窗关闭后清空对应TextField的值,并聚焦该字段:
const handleDialogClose = () => { setErrorDialogOpen(false); if (invalidFieldIndex !== null && invalidFieldPromptId !== null) { // 清空出错字段的输入值,重置验证状态 const updatedPrompts = prompts.map((item, idx) => idx === invalidFieldIndex ? { ...item, inputValue: "", isValidateditem: false } : item ); setPrompts(updatedPrompts); // 加延迟确保状态更新后DOM已渲染,再执行聚焦 setTimeout(() => { inputRefs.current[invalidFieldIndex]?.focus(); }, 0); // 重置出错字段的记录 setInvalidFieldIndex(null); setInvalidFieldPromptId(null); } };
4. 将TextField改为受控组件
原TextField是非受控状态,无法直接控制输入值,改为受控组件后可精准管理输入内容:
<TextField key={'tfield'+index} variant="outlined" size="small" fullWidth error={item.isValidateditem === false} value={item.inputValue} // 绑定状态值实现受控 onChange={(e) => { // 输入时更新对应字段的inputValue const updatedPrompts = prompts.map((item, idx) => idx === index ? { ...item, inputValue: e.target.value } : item ); setPrompts(updatedPrompts); }} inputRef={(input) => { // 将每个TextField的DOM节点存入inputRefs数组 inputRefs.current[index] = input; // 保留初始聚焦第一个字段的逻辑 if (index === 0) input?.focus(); }} onBlur={(e) => validateItem(e, item.promptId, index)} />
5. 可选:优化Dialog焦点行为
给Material UI的Dialog添加disableRestoreFocus属性,避免关闭弹窗时自动将焦点切回之前的元素,减少不必要的onBlur触发:
<Dialog open={errorDialogOpen} onClose={handleDialogClose} disableRestoreFocus // 添加此属性优化焦点行为 > <DialogTitle> Error </DialogTitle> <DialogContent> <DialogContentText> {errorErrorMsg} </DialogContentText> </DialogContent> <DialogActions> <Button onClick={handleDialogClose} autoFocus> OK </Button> </DialogActions> </Dialog>
完整修改后的代码
import { useState, useRef } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, TableContainer, Paper, Table, TableBody, TableRow, TableCell, TextField } from '@mui/material'; export const MyComponent = ({initialPrompts}: any) => { const [prompts, setPrompts] = useState<ServerData[]>( initialPrompts.map(item => ({ ...item, inputValue: "", isValidateditem: false })) ); const [errorDialogOpen, setErrorDialogOpen] = useState(false); const [errorErrorMsg, setErrorErrorMsg] = useState(''); const [invalidFieldIndex, setInvalidFieldIndex] = useState<number | null>(null); const [invalidFieldPromptId, setInvalidFieldPromptId] = useState<number | null>(null); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const handleDialogOpen = () => { setErrorDialogOpen(true); }; const handleDialogClose = () => { setErrorDialogOpen(false); if (invalidFieldIndex !== null && invalidFieldPromptId !== null) { const updatedPrompts = prompts.map((item, idx) => idx === invalidFieldIndex ? { ...item, inputValue: "", isValidateditem: false } : item ); setPrompts(updatedPrompts); setTimeout(() => { inputRefs.current[invalidFieldIndex]?.focus(); }, 0); setInvalidFieldIndex(null); setInvalidFieldPromptId(null); } }; async function validateItem(e: any, promptId: number, arrIndex: number) { if (errorDialogOpen) return; const inputValue = e.target.value; if (!inputValue.trim()) return; const response = await fetch(`http://localhost:5276/Validateitems?promptId=${promptId}&item=${inputValue}`); if (!response.ok) { const text = await response.text(); setErrorErrorMsg(text); setInvalidFieldIndex(arrIndex); setInvalidFieldPromptId(promptId); handleDialogOpen(); return; } const data = await response.json(); const updatedPrompts = prompts.map(x => x.promptId === promptId ? { ...x, isValidateditem: true, inputValue } : x ); setPrompts(updatedPrompts); } return ( <div style={{ width: '700px' }}> <Dialog open={errorDialogOpen} onClose={handleDialogClose} disableRestoreFocus > <DialogTitle> Error </DialogTitle> <DialogContent> <DialogContentText> {errorErrorMsg} </DialogContentText> </DialogContent> <DialogActions> <Button onClick={handleDialogClose} autoFocus> OK </Button> </DialogActions> </Dialog> <TableContainer component={Paper}> <Table size="small"> <TableBody>{ prompts.map((item, index) => ( <TableRow key={'trow'+index}> <TableCell>{item.prompt}</TableCell> <TableCell> <TextField key={'tfield'+index} variant="outlined" size="small" fullWidth error={item.isValidateditem === false} value={item.inputValue} onChange={(e) => { const updatedPrompts = prompts.map((item, idx) => idx === index ? { ...item, inputValue: e.target.value } : item ); setPrompts(updatedPrompts); }} inputRef={(input) => { inputRefs.current[index] = input; if (index === 0) input?.focus(); }} onBlur={(e) => validateItem(e, item.promptId, index)} /> </TableCell> </TableRow> )) }</TableBody> </Table> </TableContainer> <br/> <Button variant="contained" size='small'>Done</Button> <Button variant="contained" size='small' style={{float: 'right'}}>Reset</Button> </div> ) }
关键说明
- 受控组件:通过绑定
value和onChange实现对输入值的精准控制,确保清空操作有效。 - 弹窗状态拦截:在验证逻辑开头判断弹窗状态,从根源阻止重复验证和弹窗。
- DOM引用聚焦:使用
useRef存储TextField节点,关闭弹窗后直接聚焦目标字段,无需额外DOM查询。 - 焦点优化:
disableRestoreFocus避免Dialog关闭时自动切换焦点,减少不必要的onBlur触发。
内容的提问来源于stack exchange,提问作者wally2
相关产品推荐
相关产品推荐

