Dealing with focus and blur in a composite widget in React
I ran into a situation a while back while build a table grid component. I need to know when focus left the containing grid component. But the grid itself never receives focus. Its cells are focused. In other words, given a cell focused inside the containing grid, when the next focused element on the page was NOT inside the grid.
Let’s just consider page traversal with the tab key for now. When the tab key is pressed, the following events are fired: keydown, blur, focus, keyup. The keydown and blur events target the element being tabbed from; the focus and keyup event events target the element being tabbed to. I put together a silly little pen to illustrate this behavior:
When a user is tabbing around cells within a grid, the tabbed from element and the tabbed to element will be contained in the grid. React provides us with a helpful bit of polyfilled behavior here. Ordinarily, the focus and blur events in the DOM dot not bubble. React will bubble these events through its event model. So we can listen for focus and blur events at the root of a component and react to these events fired on its child nodes.
The gist illustrates a React Component that returns a div element with onFocus and onBlur listeners. The component has one state property: isManagingFocus. What the component does when it is managing focus is not relevant here. What the gist intends to demonstrate is how we can toggle the value of the boolean state property isManagingFocus based on blur and focus events that the children of the grid component will fire.
The component should:
- Toggle isManagingFocus to true when an element inside the grid gains focus
- Maintain isManagingFocus as true as long as an element inside the grid has focus
- Toggle isManagingFocus to false when no element inside the grid has focus.
This behavior is achieved by waiting a “tick” on a blur event before toggling the isManagingFocus state to false. By a tick, I mean the next processing cycle in the main thread. We an wait a tick by using setTimeout to delay the state setting. The blur and focus events will happen in the same tick (under normal circumstances), allowing the component to cancel its reaction to the blur event if a focus event occurs in the next moment and clears the timeout. If no focus event from an element within the grid occurs (if the user has traversed out of the grid component), then the blur event will be processed in the next tick and the grid component will toggle isManagingFocus to false.
Any time we use setTimeout/clearTimeout to manage order of operations, it feels icky. I freely admit this. It seems like a hack and admittedly this approach is that. But the DOM gives us scant tools to respond to focus and blur events. We’re often left with timing hacks and interpreting secondary effects to understand where focus is on the page and where it’s going to next. If anyone’s come up with a better approach in React to track focus within and across components, I’d love to read about it in the comments!