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 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:

3

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>
0

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>
1

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.

1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Marcus Vance

Marcus Vance

Cybersecurity & Digital Privacy Researcher

Marcus Vance is a cybersecurity auditor and technology writer dedicated to educating the public about online safety, data privacy regulations, enterprise security, and emerging cyber threats.

Share this article
Twitter Facebook Pinterest