React状态更新/渲染后如何设置焦点

时间:2019-05-09 14:25:18

标签: reactjs

在React中使用功能组件和Hooks时,我很难将焦点转移到新添加的元素上。最简单的方法可能是以下组件,

function Todos (props) {
    const addButton = React.useRef(null)
    const [todos, setTodos] = React.useState(Immutable.List([]))

    const addTodo = e => {
      setTodos(todos.push('Todo text...'))

      // AFTER THE TODO IS ADDED HERE IS WHERE I'D LIKE TO
      // THROW THE FOCUS TO THE <LI> CONTAINING THE NEW TODO
      // THIS WAY A KEYBOARD USER CAN CHOOSE WHAT TO DO WITH
      // THE NEWLY ADDED TODO
    }

    const updateTodo = (index, value) => {
      setTodos(todos.set(index, value))
    }

    const removeTodo = index => {
      setTodos(todos.delete(index))
      addButton.current.focus()
    }

    return <div>
      <button ref={addButton} onClick={addTodo}>Add todo</button>
      <ul>
        {todos.map((todo, index) => (
          <li tabIndex="0" aria-label={`Todo ${index+1} of ${todos.size}`}>
            <input type="text" value={todos[index]} onChange={e => updateTodo(index, e.target.value)}/>
            <a onClick={e => removeTodo(index)} href="#">Delete todo</a>
          </li>
        ))}
      </ul>
  </div>
}

ReactDOM.render(React.createElement(Todos, {}), document.getElementById('app'))

FYI,todos.map实际上将呈现一个Todo组件,该组件具有被选择的功能,可以使用键盘上下移动等……这就是为什么我要集中注意{{ 1}},而不是其中的输入(我意识到可以通过<li>属性来完成。

理想情况下,我可以调用autoFocus,然后立即在新的待办事项上调用setTodos,但这是不可能的,因为新的待办事项在DOM中尚不存在,因为呈现器还没有没发生。

我认为我可以通过按状态跟踪焦点来解决此问题,但这需要捕获.focus()onFocus并保持状态变量为最新。这似乎是有风险的,因为焦点可以通过键盘,鼠标,点击,开关,操纵杆等疯狂地移动……窗口可能会失去焦点……

2 个答案:

答案 0 :(得分:1)

使用useEffect订阅todos的更新,并在发生这种情况时将焦点设置。

示例:

useEffect(() => {
 addButton.current.focus()
}, [todos])

更新后的答案:

因此,您在按钮上只有一个裁判。这使您无法访问待办事项以对其进行聚焦,而只是访问addButton。我添加了一个currentTodo引用,默认情况下它将分配给最后一个待办事项。这仅用于具有一个待办事项并集中最近添加的一个待办事项的默认渲染。如果您只想删除输入,则需要找出一种集中输入的方法。

ref={index === todos.length -1 ? currentTodo : null}将引用分配给索引中的最后一项,否则引用为空

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

function Todos(props) {
    const currentTodo = React.useRef(null)
    const addButton = React.useRef(null)
    const [todos, setTodos] = useState([])

    useEffect(() => {
        const newTodos = [...todos];
        newTodos.push('Todo text...');
        setTodos(newTodos);

        // event listener for click
        document.addEventListener('mousedown', handleClick);

        // removal of event listener on unmount
        return () => {
            document.removeEventListener('mousedown', handleClick);
        };
    }, []);


    const handleClick = event => {
        // if there's a currentTodo and a current addButton ref
        if(currentTodo.current && addButton.current){
            // if the event target was the addButton ref (they clicked addTodo)
            if(event.target === addButton.current) {
                // select the last todo (presumably the latest)
                currentTodo.current.querySelector('input').select();
            }
        }
    }

    const addTodo = e => {
        const newTodo = [...todos];
        newTodo.push('New text...');
        setTodos(newTodo);
    }

    // this is for if you wanted to focus the last on every state change
    // useEffect(() => {
    //     // if the currentTodo ref is set
    //     if(currentTodo.current) {
    //         console.log('input', currentTodo.current.querySelector('input'));
    //         currentTodo.current.querySelector('input').select();
    //     }
    // }, [todos])

    const updateTodo = (index, value) => {
        setTodos(todos.set(index, value))
    }

    const removeTodo = index => {
        setTodos(todos.delete(index))
        currentTodo.current.focus()
    }

    return <div>
        <button onClick={addTodo} ref={addButton}>Add todo</button>
        <ul>
            {todos.length > 0 && todos.map((todo, index) => (
                <li tabIndex="0" aria-label={`Todo ${index + 1} of ${todos.length}`} key={index} ref={index === todos.length -1 ? currentTodo : null}>
                    <input type="text" value={todo} onChange={e => updateTodo(index, e.target.value)} />
                    <a onClick={e => removeTodo(index)} href="#">Delete todo</a>
                </li>
            ))}
        </ul>
    </div>
}

ReactDOM.render(React.createElement(Todos, {}), document.getElementById('root'))

答案 1 :(得分:0)

只需将focus()调用包装到setTimeout

setTimeout(() => {
  addButton.current.focus()
})