Using functional components and Hooks in React, I'm having trouble moving focus to newly added elements. The shortest way to see this is probably the following component,
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
realistically would render a Todo
component that has the ability to be selected, move up and down with a keyboard, etc… That is why I'm trying to focus the <li>
and not the input within (which I realize could be done with the autoFocus
attribute.
Ideally, I would be able to call setTodos
and then immediately call .focus()
on the new todo, but that's not possible because the new todo doesn't exist in the DOM yet because the render hasn't happened.
I think I can work around this by tracking focus via state but that would require capturing onFocus
and onBlur
and keeping a state variable up to date. This seems risky because focus can move so wildly with a keyboard, mouse, tap, switch, joystick, etc… The window could lose focus…