繁体   English   中英

React:使用 useRef 数组处理焦点

[英]React: Handling focus using array of useRef

我已经创建了一个基本的 Todo 应用程序,但很难在其中实现适当的焦点。 以下是对焦要求:

  1. 单击“添加”时,焦点应位于新创建的输入字段上,以便用户可以立即开始输入。

  2. 在删除项目“按钮 X”时,焦点将移至替换已删除行的行中的输入字段,如果没有剩余字段,则焦点将移至新的最后一行,如果最后一行被删除,则焦点将移至新的最后一行。

  3. 向上移动项目时,焦点应放在新移动的字段上,所有相关按钮应与元素一起移动。 如果一个字段已经在列表的顶部,则不应发生重新排序,但仍应将焦点转移到最顶部的字段。

  4. 向下移动项目时,此处应适用相同的原则(应将焦点放在向下移动的字段上)。 如果它是最后一个字段,请关注它。

这是我的实现。

应用程序.js:

import React, { useState, useRef } from "https://cdn.skypack.dev/react@17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17.0.1";

const App = () => {
    const [myRows, setMyRows] = useState([]);
    const focusInput = useRef([]);

    const onAddRow = () => {
        setMyRows((prevRows) => {
            return [
                ...prevRows,
                { id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
            ];
        });
        focusInput.current[myRows.length - 1].focus();
    };

    const onMoveUp = (index) => (event) => {
        const currentState = [...myRows];
        if (index !== 0) {
            const prevObject = currentState[index - 1];
            const nextObject = currentState[index];
            currentState[index - 1] = nextObject;
            currentState[index] = prevObject;
            setMyRows(currentState);
        }
    };

    const onMoveDown = (index) => (event) => {
        const currentState = [...myRows];
        if (index !== myRows.length - 1) {
            const currObject = currentState[index];
            const nextObject = currentState[index + 1];
            currentState[index] = nextObject;
            currentState[index + 1] = currObject;
            setMyRows(currentState);
        }
    };

    const onDelete = (index) => (event) => {
        const currentState = [...myRows];
        currentState.splice(index, 1);
        setMyRows(currentState);
    };

    const onTextUpdate = (id) => (event) => {
        setMyRows((prevState) => {
            const data = [...prevState];
            data[id] = {
                ...data[id],
                text: event.target.value,
            };
            return data;
        });
    };
return (
        <div className="container">
            <button onClick={onAddRow}>Add</button>
            <br />
            {myRows?.map((row, index) => {
                return (
                    <div key={row.id}>
                        <input
                            ref={(el) => (focusInput.current[index] = el)}
                            onChange={onTextUpdate(index)}
                            value={row.text}
                            type="text"></input>
                        <button onClick={onMoveUp(index)}>{row.up}</button>
                        <button onClick={onMoveDown(index)}>{row.down}</button>
                        <button onClick={onDelete(index)}>{row.delete}</button>
                    </div>
                );
            })}
        </div>
    );
}

ReactDOM.render(<App />,
document.getElementById("root"))

在我的实现中,当我单击“添加”时,焦点会以意想不到的方式发生变化(第一次单击不做任何事情,随后的单击会移动焦点但有滞后,即焦点应该在下一个项目上,但它在前一个项目上。

如果有人可以帮助我,我将不胜感激。 谢谢!

您观察到的奇怪行为是由于状态更新是异步的这一事实引起的。 当您在onAddRow中执行以下操作时:

focusInput.current[myRows.length - 1].focus()

那么myRows.length - 1仍然是前一组行的最后一个索引,对应于您实际看到的倒数第二行。 这准确地解释了您所描述的行为 - 如果您所做的只是添加行,那么新的焦点总是在它应该在的“后面”。

鉴于该描述,您可能认为只需将上述语句中的myRows.length - 1替换为myRows.length即可解决此问题。 但这并不是那么简单。 这样做的效果会更差,因为在这段代码运行的时候,当点击 Add 按钮时, focusInput还没有调整到新的长度,实际上新行甚至也没有在 DOM 中呈现然而。 这一切都发生在稍晚一点(尽管在人眼看来是瞬时的),在 React 意识到状态发生了变化并完成了它的事情之后。

鉴于您正在以多种不同方式操作焦点,如您的要求中所述,我相信解决此问题的最简单方法是使您想要关注自己的状态的索引。 这使得以任何你想要的方式管理焦点变得非常容易,只需调用适当的状态更新函数。

这是在下面的代码中实现的,我通过在您的 Codepen 链接上对其进行测试来实现。 我试图在 Stack Overflow 上把它变成一个片段,但由于某种原因,它无法在没有错误的情况下运行,尽管包括 React 并启用 Babel 来转换 JSX - 但是如果你将以下内容粘贴到你的 JS 中Codepen,我想你会发现它的工作令你满意。 (或者,如果我误解了一些要求,希望它至少能让你比以前更接近。)

我将解释关键部分,而不是让您自己研究代码,它们是:

  • 我刚刚提到的那个新状态变量的引入,我称之为focusIndex
  • 如前所述,每当添加、删除或移动行时,都会使用适当的值调用setFocusIndex (我一直在尝试遵循您的要求,这对我来说似乎效果很好,但正如我所说,我可能误解了。)
  • 关键是useEffect ,它在focusIndex更新时运行,并在 DOM 中进行实际聚焦。 如果没有这个,当然,焦点永远不会在调用setFocusIndex时更新,但是有了它,调用该函数将“总是”产生预期的效果。
  • 最后一个微妙之处是,我上面所说的“总是”并不完全正确。 useEffect仅在focusIndex实际更改时运行,但在移动行时,在某些情况下它设置为与以前相同的值,但您仍想移动焦点。 我发现在单击输入外部时会发生这种情况,然后将第一个字段向上或最后一个向下移动 - 当我们希望第一个/最后一个输入被聚焦时,什么也没发生。 发生这种情况是因为focusIndex被设置为它已经拥有的值,所以useEffect没有运行,但我们仍然希望它运行以设置焦点。 我想出的解决方案是在每个输入中添加一个onBlur处理程序,以确保焦点索引设置为某个“不可能”的值(我选择了 -1,但nullundefined之类的值也可以正常工作)丢失了 - 这可能看起来是人为的,但实际上更好地代表了这样一个事实,即当焦点没有输入时,你不希望有一个“合理的”焦点索引,否则 React 状态表示其中一个输入是焦点,而没有是。 请注意,我还将 -1 用于初始状态,原因大致相同 - 如果它从 0 开始,那么添加第一行不会导致焦点改变。

我希望这会有所帮助,并且我的解释足够清楚——如果你对任何事情感到困惑,或者注意到这个实现有什么问题(我承认我还没有完全测试它的破坏性),请告诉我!

import React, { useState, useRef, useEffect } from "https://cdn.skypack.dev/react@17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17.0.1";

const App = () => {
    const [myRows, setMyRows] = useState([]);
    const focusInput = useRef([]);
    const [focusIndex, setFocusIndex] = useState(-1);

    const onAddRow = () => {
        setMyRows((prevRows) => {
            return [
                ...prevRows,
                { id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
            ];
        });
        setFocusIndex(myRows.length);
    };
  
    useEffect(() => {
        console.log(`focusing index ${focusIndex} in`, focusInput.current);
        focusInput.current[focusIndex]?.focus();
    }, [focusIndex]);

    const onMoveUp = (index) => (event) => {
        const currentState = [...myRows];
        if (index !== 0) {
            const prevObject = currentState[index - 1];
            const nextObject = currentState[index];
            currentState[index - 1] = nextObject;
            currentState[index] = prevObject;
            setMyRows(currentState);
            setFocusIndex(index - 1);
        } else {
            setFocusIndex(0);
        }
    };

    const onMoveDown = (index) => (event) => {
        const currentState = [...myRows];
        if (index !== myRows.length - 1) {
            const currObject = currentState[index];
            const nextObject = currentState[index + 1];
            currentState[index] = nextObject;
            currentState[index + 1] = currObject;
            setMyRows(currentState);
            setFocusIndex(index + 1);
        } else {
            setFocusIndex(myRows.length - 1);
        }
    };

    const onDelete = (index) => (event) => {
        const currentState = [...myRows];
        currentState.splice(index, 1);
        setMyRows(currentState);
        const newFocusIndex = index < currentState.length
            ? index
            : currentState.length - 1;
        setFocusIndex(newFocusIndex);
    };

    const onTextUpdate = (id) => (event) => {
        setMyRows((prevState) => {
            const data = [...prevState];
            data[id] = {
                ...data[id],
                text: event.target.value,
            };
            return data;
        });
    };

    return (
        <div className="container">
            <button onClick={onAddRow}>Add</button>
            <br />
            {myRows?.map((row, index) => {
                return (
                    <div key={row.id}>
                        <input
                            ref={(el) => (focusInput.current[index] = el)}
                            onChange={onTextUpdate(index)}
                            onBlur={() => setFocusIndex(-1)}
                            value={row.text}
                            type="text"></input>
                        <button onClick={onMoveUp(index)}>{row.up}</button>
                        <button onClick={onMoveDown(index)}>{row.down}</button>
                        <button onClick={onDelete(index)}>{row.delete}</button>
                    </div>
                );
            })}
        </div>
    );
}

ReactDOM.render(<App />,
document.getElementById("root"))

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM