You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 10:32:03