Interactive Role Needs Focus
ID |
javascript.jsx_interactive_role_needs_focus |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
accessibility, jsx, reliability |
Description
Reports a JSX element that declares an interactive ARIA role and handles activation, yet cannot
receive keyboard focus, because it is neither natively focusable nor given a tabIndex.
Rationale
An interactive role plus a handler is a promise that the element behaves like a control, and the first thing a control has to do is take focus. Without it the element is announced as a button to a screen reader user who can never reach it: the Tab key skips it, so does the screen reader’s own control navigation, and the only way to activate it is a pointer. This is the most common way a custom control ends up mouse-only, and it is invisible in testing because the mouse path works perfectly.
<div role="button" onClick={save}>Save</div> // FLAW - mouse only
<li role="option" onClick={pick}>One</li> // FLAW - same
<a role="link" onKeyDown={go}>Home</a> // FLAW - no href, so no focus
<div role="button" onClick={save} tabIndex={0}>Save</div> // OK
<button role="button" onClick={save}>Save</button> // OK - focusable already
<span role="checkbox" aria-checked="false" /> // OK - no handler yet
<div role="button" onMouseEnter={preview}>Save</div> // OK - hover is not activation
<ul role="listbox">...</ul> // OK - a composite container
<div role="button" onClick={save} {...rest} /> // OK - attributes may be spread in
The handler is required, and that is a deliberate narrowing. A role on its own is a claim; a role
with a handler is a claim the code has started keeping. An element with an interactive role but
neither handler nor tabIndex is far more often decoration, a placeholder or half-built markup than
a broken control, so it is left alone. The handlers that count are the activation ones — onClick,
onKeyDown, onKeyUp, onKeyPress, onMouseDown and onMouseUp; a hover or focus handler
observes the pointer rather than acting on it.
A natively focusable element needs nothing further. Everything else has to opt in with a tabIndex,
and its presence is enough: 0, -1 or computed, the author has thought about focus. The roles
reported are the ones whose own element is the focus target; a composite widget role such as
listbox, menu or tablist manages focus for its children rather than taking it, so a container
declaring one is correct as written.
Remediation
Add tabIndex={0} so the element joins the tab order at its DOM position, and handle Enter and
Space (or the arrow keys, for a widget item) alongside the click. Where the element is one item of a
composite widget, follow that widget’s focus pattern instead: one tab stop on the container, arrow
keys between items. Where the markup is a plain control, a button or an a with an href brings
focus, key handling and the right announcement for free.