首页 > 其他分享 >处理 Input 失焦交互的另一种方案

处理 Input 失焦交互的另一种方案

时间:2022-10-24 13:56:42浏览次数:67  
标签:状态 const shouldReset 重置 value 编辑 失焦 Input 交互

一、需求描述

某个字段通常状态为查看状态,可以通过按钮(或点击字段内容)切换为编辑状态

在编辑状态下,点击当前内容之外的地方则取消编辑,回到查看状态

在编辑状态下,可以点击保存按钮提交数据,并回到查看状态

其实这种交互也做过不少,但这次的需求多了一个二次确认的气泡弹窗


如果没有按钮,仅仅是查看状态/编辑状态的切换(如下图)

这种情况就非常简单,直接通过 clickblur 事件切换状态即可

而加了额外的按钮,甚至加了弹窗气泡,就没办法直接使用 blur 事件

常见的做法是监听 body 的 click 事件,根据触发事件的 DOM 判断是否需要切换状态

document.body.addEventListener('click', handler)

但如果页面上有很多字段都有这样的需求,每一个字段都需要注册一个 body.click 事件处理函数,这似乎不够优雅

于是我转换思路,力求把状态控制在组件内部,let's coding....

 

二、组件设计

先梳理一下整个组件的状态:

1. 有两种模式:“编辑模式”、“查看模式”

2. 有三种操作:“开始编辑(查看→编辑)”、“重置(编辑→查看 并恢复初始值)”、“提交(编辑→查看 并保存当前值)”

那么整个组件的基本结构就出来了

type InputProps = {
  value?: string;
  onSubmit?: (v: string) => void;
};

const Input: React.FC<InputProps> = ({ value, onSubmit }) => {
  const inputRef = useRef<InputRef>(null);
  const [readonly, setReadonly] = useState<boolean>(true);
  const [currentValue, setCurrentValue] = useState<InputProps['value']>(value);

  // 切换为编辑模式
  const toggleEdit = () => {};

  // 重置
  const handleReset = () => {};

  // 提交
  const handleSubmit = () => {};

  useEffect(() => {
    setCurrentValue(value);
  }, [value]);

  return (
    <div className="desc-item">
      {readonly ? (
        <>
          {/* 查看模式 */}
        </>
      ) : (
        <>
          {/* 编辑模式 */}
        </>
      )}
    </div>
  );
};

export default Input;

而在编辑模式下,输入框 Input 旁边还有一个保存按钮,并需要通过弹窗二次确认

所以编辑模式下的组件结构如下:

<>
  {/* 编辑模式 */}
  <AntInput
    className="desc-item-input"
    ref={inputRef}
    defaultValue={value}
    onBlur={handleReset}
  />
  <AntPopconfirm
    title="是否继续操作?"
    okText="确认"
    cancelText="取消"
    onConfirm={handleSubmit}
  >
    <button className="desc-item-button">保存</button>
  </AntPopconfirm>
</>

在这里就会有问题:“重置”操作是通过 onBlur 触发的,而每次点击“保存”按钮的时候必然会触发输入框的 blur 事件,更不用说 AntPopconfirm 里的“取消”或“确认”了

沿着这个业务场景思考,我需要解决的核心问题其实是:打开/关闭 AntPopconfirm 时不触发 onBlur

想到这一层就比较清晰了,除了 body.click 之外,还可以通过加锁来阻止状态切换

 

三、状态锁

使用 useRef 新增一个变量 shouldReset,用来控制是否执行重置操作

const shouldReset = useRef<boolean>();

// 重置
const handleReset = useCallback(() => {
  // 重置之前校验 shouldReset 状态, 防止“保存”等功能按钮触发 blur 事件
  if (!shouldReset?.current) return;
  setCurrentValue(value);
  setReadonly(true);
}, [value]);

然后在 AntPopconfirm 组件的 onVisibleChange 事件回调中锁定状态

// 二次确认的气泡显示/隐藏时的回调
const handleVisibleChange = useCallback(visible => {
  shouldReset.current = false;
}, []);

<AntPopconfirm onVisibleChange={handleVisibleChange} > <button className="desc-item-button">保存</button> </AntPopconfirm>

但如果希望 shouldReset 这个状态锁生效,必须保证 handleVisibleChange 先于 handleReset 触发

最好的解决方案是使用异步任务 setTimeout

// 重置
const handleReset = useCallback(() => {
  // 保证重置功能的正常逻辑
  shouldReset.current = true;
  setTimeout(() => {
    // 重置之前校验 shouldReset 状态, 防止“保存”等功能按钮触发 blur 事件
    if (!shouldReset?.current) return;
    setCurrentValue(value);
    setReadonly(true);
  }, 160);
}, [value]);

// 二次确认的气泡显示/隐藏时的回调
const handleVisibleChange = useCallback(visible => {
  setTimeout(() => {
    shouldReset.current = false;
  });
}, [])

上面给 handleReset 的 setTimeout 加了 160ms 的延时,这样能保证它晚于 handleVisibleChange 执行,并且在交互上不会有明显的卡顿

这样的结果就是:如果在触发了 handleReset 之后的 160ms 毫秒内,有其他函数将 shouldReset 改为 false,则不会执行重置操作

 

四、优化细节

完成了状态锁之后,整个交互的核心逻辑就完成了,但还有一个瑕疵:

触发 onVisibleChange 之后,输入框会失焦,如果不能重新聚焦,则无法再次触发 onBlur,也就无法重置

所以如果失焦后还要继续编辑,也就是二次确认的“取消”操作时,需要让输入框重新聚焦

// 二次确认的气泡显示/隐藏时的回调
const handleVisibleChange = useCallback(visible => {
  // 这里的 visible 是目标状态,不是当前状态
  setTimeout(() => {
    !visible && inputRef?.current?.focus();
    shouldReset.current = false;
  });
}, []);

另外,为了保证交互体验,最好是给查看→编辑的操作也做上自动聚焦

// 切换为编辑视图
const toggleEdit = useCallback(() => {
  setReadonly(false);
  // 自动聚焦
  setTimeout(() => {
    inputRef.current?.focus();
  });
}, []);

 

以上就是通过状态锁来处理 Input 失焦交互的方案

虽然只贴出来 Input 组件的代码,但思路是通用的,对于 Select、DatePicker 或者其他自定义输入控件,也可以用这样的方案处理

 

标签:状态,const,shouldReset,重置,value,编辑,失焦,Input,交互
From: https://www.cnblogs.com/wisewrong/p/16807348.html

相关文章