Escuche la pulsación de tecla para el documento en reactjs

82

Quiero enlazar para cerrar la escapeventana emergente de arranque de reacción activa al presionar. Aquí está el código

_handleEscKey:function(event){
         console.log(event);
        if(event.keyCode == 27){
          this.state.activePopover.hide();
        }
   },

  componentWillMount:function(){
     BannerDataStore.addChangeListener(this._onchange);
     document.addEventListener("click", this._handleDocumentClick, false);
     document.addEventListener("keyPress", this._handleEscKey, false);
   },


   componentWillUnmount: function() {
     BannerDataStore.removeChangeListener(this._onchange);
      document.removeEventListener("click", this._handleDocumentClick, false);
      document.removeEventListener("keyPress", this._handleEscKey, false);
   },

Pero no se registra nada en la consola cuando presiono cualquier tecla. Intenté escuchar eso en la ventana también y con diferentes casos. 'Keypress', 'keyup', etc. pero parece que estoy haciendo algo mal.

siempre aprender
fuente
Por lo que vale, he publicado una biblioteca keydown para React que está destinada a hacer todo esto mucho más fácil: github.com/jedverity/react-keydown
glortho

Respuestas:

61

Deberías usar keydowny no keypress.

La pulsación de teclas (obsoleta) se usa generalmente solo para teclas que producen una salida de caracteres según los documentos

Pulsación de tecla (obsoleta)

El evento de pulsación de tecla se activa cuando se pulsa una tecla y esa tecla normalmente produce un valor de carácter

Keydown

El evento keydown se activa cuando se presiona una tecla.

Dhiraj
fuente
1
la pulsación de tecla ha quedado obsoleta.
TimeParadox
49

Solo tuve un problema similar con esto yo mismo. Usaré su código para ilustrar una solución.

// for other devs who might not know keyCodes
var ESCAPE_KEY = 27;

_handleKeyDown = (event) => {
    switch( event.keyCode ) {
        case ESCAPE_KEY:
            this.state.activePopover.hide();
            break;
        default: 
            break;
    }
},

// componentWillMount deprecated in React 16.3
componentDidMount(){
    BannerDataStore.addChangeListener(this._onchange);
    document.addEventListener("click", this._handleDocumentClick, false);
    document.addEventListener("keydown", this._handleKeyDown);
},


componentWillUnmount() {
    BannerDataStore.removeChangeListener(this._onchange);
    document.removeEventListener("click", this._handleDocumentClick, false);
    document.removeEventListener("keydown", this._handleKeyDown);
},

Dado que está utilizando la forma createClass de hacer las cosas, no necesita vincularse a ciertos métodos como thisestá implícito en cada método definido.

Hay un jsfiddle en funcionamiento, usando el método createClass de creación del componente React aquí.

Chris Sullivan
fuente
9
Esto no eliminará correctamente el detector de eventos debido a que el enlace proporciona una nueva instancia cada vez. Asegúrese de almacenar en caché los resultados que enlazan los retornos para agregarlos y eliminarlos correctamente del documento
Steven10172
@ Steven10172 Buen punto, dado que el constructor no está realmente definido en el método React.createClass, siempre puedes enlazar en getInitialState ().
Chris Sullivan
En relación con los comentarios anteriores, este es un buen ejemplo de dónde enlazar y usar los oyentes de eventos stackoverflow.com/questions/32553158/…
Craig Myles
1
Tenga en cuenta que componentWillMountha quedado obsoleto a partir de React 16.3. En mi opinión, debería registrar los oyentes de eventos en componentDidMount.
Igor Akkerman
20

Si puede usar React Hooks, un buen enfoque es hacerlo useEffect, por lo que el detector de eventos se suscribirá solo una vez y se cancelará correctamente cuando se desmonte el componente.

El siguiente ejemplo se extrajo de https://usehooks.com/useEventListener/ :

// Hook
function useEventListener(eventName, handler, element = window){
  // Create a ref that stores handler
  const savedHandler = useRef();

  // Update ref.current value if handler changes.
  // This allows our effect below to always get latest handler ...
  // ... without us needing to pass it in effect deps array ...
  // ... and potentially cause effect to re-run every render.
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(
    () => {
      // Make sure element supports addEventListener
      // On 
      const isSupported = element && element.addEventListener;
      if (!isSupported) return;

      // Create event listener that calls handler function stored in ref
      const eventListener = event => savedHandler.current(event);

      // Add event listener
      element.addEventListener(eventName, eventListener);

      // Remove event listener on cleanup
      return () => {
        element.removeEventListener(eventName, eventListener);
      };
    },
    [eventName, element] // Re-run if eventName or element changes
  );
};

También puede instalarlo desde npm, por ejemplo, npm i @use-it/event-listener- vea el proyecto aquí - https://github.com/donavon/use-event-listener .

Luego, para usarlo en su componente solo tiene que llamarlo dentro de su componente funcional pasando el nombre del evento y el controlador. Por ejemplo, si lo desea console.logcada vez que se presiona la tecla Escape:

import useEventListener from '@use-it/event-listener'

const ESCAPE_KEYS = ['27', 'Escape'];

const App = () => {
  function handler({ key }) {
    if (ESCAPE_KEYS.includes(String(key))) {
      console.log('Escape key pressed!');
    }
  }

  useEventListener('keydown', handler);

  return <span>hello world</span>;
}
Mate Paiva
fuente
si la aplicación no es un componente funcional, no se puede usar
ashubuntu
Gracias por publicar esto, me ayudó a solucionar una pérdida masiva de memoria en mis controladores de teclado globales. FWIW, el efecto "guardar oyentes en un ref" es realmente clave: ¡no pase sus controladores de eventos en la useEffectmatriz de dependencia que los agrega document.body.onKeyDown!
aendrew
@aendrew: ¿Cuál es la diferencia entre guardar el controlador en una referencia y simplemente declarar una función?
thelonglqd
@thelonglqd Creo que porque, de lo contrario, se agregan como controladores de eventos varias veces; sin embargo, no me cites en eso, ¡eso fue hace más de medio año y mi memoria está confusa!
aendrew
2

Una versión de la respuesta de Jt oso que es más relevante para esta pregunta. Creo que esto es mucho más simple que las otras respuestas que usan bibliotecas externas o ganchos de API para vincular / desvincular al oyente.

var KEY_ESCAPE = 27;
...
    function handleKeyDown(event) {
        if (event.keyCode === KEY_ESCAPE) {
            /* do your action here */
        }
    }
...
    <div onKeyDown={handleKeyDown}>
...
Tongfa
fuente
3
El elemento debe enfocarse primero. Si desea tener un detector de eventos global, es posible que no se active porque inicialmente el elemento del cuerpo está enfocado.
n1ru4l
1

Tenía los mismos requisitos para un div que se podía tabular.

El siguiente código para mí estaba dentro de una llamada a items.map ((item) => ...

  <div
    tabindex="0"
    onClick={()=> update(item.id)}
    onKeyDown={()=> update(item.id)}
   >
      {renderItem(item)}
  </div>

¡Esto funcionó para mí!

Jt oso
fuente
1

Quería tener oyentes de eventos globales y tuve un comportamiento extraño debido al uso de React Portals. El evento aún se desencadenó en el elemento del documento a pesar de que se canceló en un componente modal del portal dentro del documento.

Me moví hacia el uso solo de detectores de eventos en un objeto raíz que envuelve todo el árbol de componentes. El problema aquí era que inicialmente se enfoca el cuerpo y no el elemento raíz, por lo que los eventos se dispararían primero una vez que enfoca un elemento dentro del árbol.

La solución que elegí fue agregar un tabindex y enfocarlo automáticamente con un gancho de efecto.

import React from "react";

export default GlobalEventContainer = ({ children, ...props }) => {
  const rootRef = React.useRef(null);
  useEffect(() => {
    if (document.activeElement === document.body && rootContainer.current) 
      rootContainer.current.focus();
    }
  });

  return <div {...props} tabIndex="0" ref={rootRef}>{children}</div>
};
n1ru4l
fuente