What is most simple way to make "roving tabindex" in React? It's basically switch focus and tabindex=0/-1
between child elements. Only a single element have tabindex
of 0
, while other receives -1
. Arrow keys switch tabindex
between child elements, and focus it.
For now, I do a simple children mapping of required type, and set index
prop and get ref
, to use it later. It looks robust, but may be there more simple solution?
My current solution (pseudo-javascript, for idea illustration only):
ElementWithFocusManagement.js
function recursivelyMapElementsOfType(children, isRequiredType, getProps) {
return Children.map(children, function(child) {
if (isValidElement(child) === false) {return child;}
if (isRequiredType(child)) {
return cloneElement(
child,
// Return new props
// {
// index, iterated in getProps closure
// focusRef, saved to `this.focusable` aswell, w/ index above
// }
getProps()
);
}
if (child.props.children) {
return cloneElement(child, {
children: recursivelyMapElementsOfType(child.props.children, isRequiredType, getProps)
});
}
return child;
});
}
export class ElementWithFocusManagement {
constructor(props) {
super(props);
// Map of all refs, that should receive focus
// {
// 0: {current: HTMLElement}
// ...
// }
this.focusable = {};
this.state = {
lastInteractionIndex: 0
};
}
handleKeyDown() {
// Handle arrow keys,
// check that element index in `this.focusable`
// update state if it is
// focus element
}
render() {
return (
<div onKeyDown={this.handleKeyDown}>
<Provider value={{lastInteractionIndex: this.state.lastInteractionIndex}}>
{recursivelyMapElementsOfType(
children,
isRequiredType, // Check for required `displayName` match
getProps(this.focusable) // Get index, and pass ref, that would be saved to `this.focusable[index]`
)}
</Provider>
</div>
);
}
}
with-focus.js
export function withFocus(WrappedComponent) {
function Focus({index, focusRef, ...props}) {
return (
<Consumer>
{({lastInteractionIndex}) => (
<WrappedComponent
{...props}
elementRef={focusRef}
tabIndex={lastInteractionIndex === index ? 0 : -1}
/>
)}
</Consumer>
);
}
// We will match for this name later
Focus.displayName = `WithFocus(${WrappedComponent.name})`;
return Focus;
}
Anything.js
const FooWithFocus = withFocus(Foo);
<ElementWithFocusManagement> // Like toolbar, dropdown menu and etc.
<FooWithFocus>Hi there</FooWithFocus> // Button, menu item and etc.
<AnythingThatPreventSimpleMapping>
<FooWithFocus>How it's going?</FooWithFocus>
</AnythingThatPreventSimpleMapping>
<SomethingWithoutFocus />
</ElementWithFocusManagement>
ElementWithFocusManagement
could be a HOC aswell – Cyrenechildren
gets mapped in component, mapping ofwithFocus
HOCs just get failed. – Cyrene