Accessibility - React Ensure Click Events Have Key Events
I Want to Be Sure That All My onClick Event Are Next to a onKeyDown Event. I Will Use Eslint-Plugin-Jsx-A11Y to Ensure This. but in Code, It Is a Way to Do...
I want to be sure that all my onClick event are next to a onKeyDown event.
I will use eslint-plugin-jsx-a11y to ensure this. But in code, It is a way to do this generic. I mean, it will be annoying to do all the time:
if(event.keyCode === 13){
...
}
I would like to have a way to tell an element that in onKeyDown in case that the user use the execute the function in the onClick. Or a similar solution like
In angular for instance, I have this clear. Let's go for a directive to do this automatically. But in React I don't know which is the best approach.
eslint rule:
5 Answers
Another approach without decorators could be to use object spread to add the necessary props to an element:
function buttonize(handlerFn) {
return {
role: 'button',
onClick: handlerFn,
onKeyDown: event => {
// insert your preferred method for detecting the keypress
if (event.keycode === 13) handlerFn(event);
}
}
}
and use like so:
<div
className="element-with-very-good-excuse-to-dont-be-a-button"
{...buttonize(this.myAction)}
>click me</div>
Finally I only see 2 solutions:
1 I can create a component that encapsulate all those actions... But this is the work of a button. And I want not to open a new way to create buttons in my project, this is just for exceptions and this can create a vicious behave inside the project. And for that we have another component... the button :)
2 create a decorator for the actions.
export function a11yButtonActionHandler(target, key, descriptor) {
const fn = descriptor.value;
if (typeof fn !== 'function') {
throw new Error(`@a11yButtonActionHandler decorator can only be applied to methods not: ${typeof fn}`);
}
descriptor.value = function actionHandler(event) {
if (!event || event.type === 'click' ||
(['keydown', 'keypress'].includes(event.type) && ['Enter', ' '].includes(event.key))
) {
fn.call(this, event);
}
};
return descriptor;
}
and use the decorator.
@a11yButtonActionHandler
myAction(event) {
...
}
<div className="element-with-very-good-excuse-to-dont-be-a-button"
role="button"
tabIndex="0"
onKeyDown={ this.myAction }
onClick={ this.myAction }>
Great solutions here already, but I wanted an option where I didn't have to use decorators or prop-spreading. So I rejigged some of the existing answers into this:
function keyDownA11y(handler) {
return function onKeyDown(event) {
if (
['keydown', 'keypress'].includes(event.type)
&& ['Enter', ' '].includes(event.key)
) {
handler();
}
}
}
usage:
<div
onClick={myHandler}
onKeyDown={keyDownA11y(myHandler)}
role="button"
>
Hurray!
</div>
EDIT: Alternatively, you could use this to make an A11yButton component and just import that. This way, you could get the benefits of cragb's excellent buttonizer solution:
import React from 'react';
export default function A11yButton({
elementType,
onClick,
...props
}) {
return React.createElement(elementType, {
...props,
onClick,
onKeyDown: keyDownA11y(onClick),
role: 'button',
// Add other props that might be necessary, like "tabIndex: 0,"
});
}
Usage:
<A11yButton
elementType="div"
onClick={myHandler}
>
Hurray!
</A11yButton>
To simplify Raúl Martíns answer and don't use decorators which are still just a proposal you can also create a simple function out of it:
function isButtonAction(event) {
return (
event.type === 'click' ||
(['keydown', 'keypress'].includes(event.type) &&
['Enter', ' '].includes(event.key))
);
}
then simply check it in your action handler:
function myAction(event) {
if (isButtonAction(event)) {
...
}
}
I suggest you use a state to handle it. For example:
First, initial a state in your component and set the event listener:
getInitialState(){
window.addEventListener("keypress", this.onKeypresshandler);// if you are not rendering on server side, you cannot bind the listener to the element you want, which doesn't exist yet.
return{
isPressed: false
}
}
Then, change the state when the key keypress event is called:
onKeypresskhandler(e){
if(e.keyCode === 13){// if the key is enter
this.setState({isPressed:true})
}
}
finally, check the state before executing your function
YourFunction(){
if(this.state.isPressed){// check if the key is pressed
... // the code you want to execute
}
}
It is the simplest way I know. I hope it helps.