12 de julio de 2022

Enfocado: enfoque/desenfoque

Un elemento se enfoca cuando el usuario hace click sobre él o al pulsar Tab en el teclado. Existen también un atributo autofocus de HTML que enfoca un elemento por defecto cuando una página carga, y otros medios de conseguir el enfoque.

Enfocarse sobre un elemento generalmente significa: “prepárate para aceptar estos datos”, por lo que es el momento en el cual podemos correr el código para inicializar la funcionalidad requerida.

El momento de desenfoque (“blur”) puede ser incluso más importante. Ocurre cuando un usuario clica en otro punto o presiona Tab para ir al siguiente campo de un formulario. También hay otras maneras.

Perder el foco o desenfocarse generalmente significa: “los datos ya han sido introducidos”, entonces podemos correr el código para comprobarlo, o para guardarlo en el servidor, etc.

Existen importantes peculiaridades al trabajar con eventos de enfoque. Haremos lo posible para abarcarlas a continuación.

Eventos focus/blur

El evento focus es llamado al enfocar, y el blur cuando el elemento pierde el foco.

Utilicémoslos para la validación de un campo de entrada.

En el ejemplo a continuación:

  • El manejador blur comprueba si se ha introducido un correo, y en caso contrario muestra un error.
  • El manejador focus esconde el mensaje de error (en blur se volverá a comprobar):
<style>
  .invalid { border-color: red; }
  #error { color: red }
</style>

Su correo por favor: <input type="email" id="input">

<div id="error"></div>

<script>
input.onblur = function() {
  if (!input.value.includes('@')) { // not email
    input.classList.add('invalid');
    error.innerHTML = 'Por favor introduzca un correo válido.'
  }
};

input.onfocus = function() {
  if (this.classList.contains('invalid')) {
    // quitar la  indicación "error", porque el usuario quiere reintroducir algo
    this.classList.remove('invalid');
    error.innerHTML = "";
  }
};
</script>

El HTML actual nos permite efectuar diversas validaciones utilizando atributos de entrada: required, pattern, etc. Y muchas veces son todo lo que necesitamos. JavaScript puede ser utilizado cuando queremos más flexibilidad. También podríamos enviar automáticamente el valor modificado al servidor si es correcto.

Métodos focus/blur

Los métodos elem.focus() y elem.blur() ponen/quitan el foco sobre el elemento.

Por ejemplo, impidamos al visitante que deje la entrada si el valor es inválido:

<style>
  .error {
    background: red;
  }
</style>

Su correo por favor: <input type="email" id="input">
<input type="text" style="width:220px" placeholder="hacer que el correo sea inválido y tratar de enfocar aquí">

<script>
  input.onblur = function() {
    if (!this.value.includes('@')) { // no es un correo
      // mostrar error
      this.classList.add("error");
      // ...y volver a enfocar
      input.focus();
    } else {
      this.classList.remove("error");
    }
  };
</script>

Funciona en todos los navegadores excepto Firefox (bug).

Si introducimos algo en la entrada y luego intentamos pulsar Tab o hacer click fuera del <input>, entonces onblur lo vuelve a enfocar.

Por favor tened en cuenta que no podemos “prevenir perder el foco” llamando a event.preventDefault() en onblur, porque onblur funciona después de que el elemento perdió el foco.

Aunque en la práctica uno debería pensarlo bien antes de implementar algo como esto, porque generalmente debemos mostrar errores al usuario, pero no evitar que siga adelante al llenar nuestro formulario. Podría querer llenar otros campos primero.

Pérdida de foco iniciada por JavaScript

Una pérdida de foco puede ocurrir por diversas razones.

Una de ellas ocurre cuando el visitante clica en algún otro lado. Pero el propio JavaScript podría causarlo, por ejemplo:

  • Un alert traslada el foco hacia sí mismo, lo que causa la pérdida de foco sobre el elemento (evento blur). Y cuando el alert es cerrado, el foco vuelve (evento focus).
  • Si un elemento es eliminado del DOM, también causa pérdida de foco. Si es reinsertado el foco no vuelve.

Estas situaciones a veces causan que los manejadores focus/blur no funcionen adecuadamente y se activen cuando no son necesarios.

Es recomendable tener cuidado al utilizar estos eventos. Si queremos monitorear pérdidas de foco iniciadas por el usuario deberíamos evitar causarlas nosotros mismos.

Permitir enfocado sobre cualquier elemento: tabindex

Por defecto, muchos elementos no permiten enfoque.

La lista varía un poco entre navegadores, pero una cosa es siempre cierta: focus/blur está garantizado para elementos con los que el visitante puede interactuar: <button>, <input>, <select>, <a>, etc.

En cambio, elementos que existen para formatear algo, tales como <div>, <span>, <table>, por defecto no son posibles de enfocar. El método elem.focus() no funciona en ellos, y los eventos focus/blur no son desencadenados.

Esto puede ser modificado usando el atributo HTML tabindex.

Cualquier elemento se vuelve enfocable si contiene tabindex. El valor del atributo es el número de orden del elemento cuando Tab (o algo similar) es utilizado para cambiar entre ellos.

Es decir: si tenemos dos elementos donde el primero contiene tabindex="1" y el segundo contiene tabindex="2", al presionar Tab estando situado sobre el primer elemento se traslada el foco al segundo.

El orden de cambio es el siguiente: los elementos con tabindex de valor “1” y mayores tienen prioridad (en el orden tabindex) y después los elementos sin tabindex (por ejemplo un estándar).

Elementos sin el tabindex correspondiente van cambiando en el orden del código fuente del documento (el orden por defecto).

Existen dos valores especiales:

  • tabindex="0" incluye al elemento entre los que carecen de tabindex. Esto es, cuando cambiamos entre elementos, elementos con tabindex="0" van después de elementos con tabindex ≥ "1".

Habitualmente se utiliza para hacer que un elemento sea enfocable y a la vez mantener intacto el orden de cambio por defecto. Para hacer que un elemento sea parte del formulario a la par con .

  • tabindex="-1" permite enfocar un elemento solamente a través de código. Tab ignora estos elementos, pero el método elem.focus() funciona.

Por ejemplo, he aquí una lista. Clique sobre el primer ítem y pulse Tab:

Clique sobre el primer ítem y pulse `key:Tab`. Fíjese en el orden. Note que subsiguientes `key:Tab` pueden desplazar el foco fuera del iframe en el ejemplo.
<ul>
  <li tabindex="1">Uno</li>
  <li tabindex="0">Cero</li>
  <li tabindex="2">Dos</li>
  <li tabindex="-1">Menos uno</li>
</ul>

<style>
  li { cursor: pointer; }
  :focus { outline: 1px dashed green; }
</style>

El orden es el siguiente: 1 - 2 - 0. Normalmente, <li> no admite enfocado, pero tabindex lo habilita, junto con eventos y estilado con :focus.

La propiedad elem.tabIndex también funciona

Podemos añadir tabindex desde JavaScript utilizando la propiedad elem.tabIndex. Se consigue el mismo resultado.

Delegación: focusin/focusout

Los eventos focus y blur no se propagan.

Por ejemplo, no podemos añadir onfocus en

para resaltarlo, así:

<!-- enfocando en el formulario -- añadir la clase -->
<form onfocus="this.className='focused'">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

El ejemplo anterior no funciona porque cuando el usuario enfoca sobre un el evento ´focus´ se dispara solamente sobre esa entrada y no se propaga, por lo que form.onfocus nunca se dispara.

Existen dos soluciones.

Primera: hay una peculiar característica histórica: focus/blur no se propagan hacia arriba, pero lo hacen hacia abajo en la fase de captura.

Esto funcionará:

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  // pon el manejador en fase de captura (último argumento "verdadero")
  form.addEventListener("focus", () => form.classList.add('focused'), true);
  form.addEventListener("blur", () => form.classList.remove('focused'), true);
</script>

Segunda: existen los eventos focusin y focusout, exactamente iguales a focus/blur, pero se propagan.

Hay que tener en cuenta que han de asignarse utilizando elem.addEventListener, no on<event>.

La otra opción que funciona:

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  form.addEventListener("focusin", () => form.classList.add('focused'));
  form.addEventListener("focusout", () => form.classList.remove('focused'));
</script>

Resumen

Los eventos focus y blur hacen que un elemento se enfoque/pierda el foco.

Se caracterizan por lo siguiente:

  • No se propagan. En su lugar se puede capturar el estado o usar focusin/focusout.
  • La mayoría de los elementos no permiten enfoque por defecto. Utiliza tabindex para hacer cualquier elemento enfocable.

El elemento que en el momento tiene el foco está disponible como document.activeElement.

Tareas

importancia: 5

Crea un <div> que se vuelva <textarea> cuando es clicado.

El textarea permite editar HTML en <div>.

Cuando el usuario presiona Enter o se pierde el foco, el <textarea> se vuelve <div> de nuevo, y su contenido se vuelve el HTML del <div>.

Demo en nueva ventana

Abrir un entorno controlado para la tarea.

importancia: 5

Haz las celdas de la tabla editables al clicarlas.

  • Al clicar, la celda se vuelve “editable” (aparece un textarea dentro), y podemos cambiar el HTML. No debe haber cambios de tamaño, la geometría debe conservarse.
  • Bajo la celda aparecen los botones OK y CANCEL para terminar/cancelar la edición.
  • Solo una celda a la vez puede ser editable. Mientras un <td> esté en “modo de edición”, los clics en otras celdas son ignorados.
  • La tabla puede tener varias celdas. Usa delegación de eventos.

El demo:

Abrir un entorno controlado para la tarea.

  1. Al clicar (onclick) se reemplaza el innerHTML de la celda por un <textarea> com los mismos tamaños y sin bordes. Se puede usar JavaScript o CSS para establecer el tamaño correcto.
  2. Establece textarea.value a td.innerHTML.
  3. Pone el foco en textarea.
  4. Muestra los botones OK/CANCEL bajo la celda, y maneja los clics en ellos.

Abrir la solución en un entorno controlado.

importancia: 4

Enfoca el ratón. Luego usa las flechas del teclado para moverlo:

Demo en nueva ventana

P.S. No pongas manejadores de eventos en ningún lado excepto el elemento #mouse.

P.P.S. No modifiques HTML/CSS, el proceso debe ser genérico y trabajar con cualquier elemento.

Abrir un entorno controlado para la tarea.

Podemos usar mouse.onclick para manejar el clic y hacer el ratón “movible” con position:fixed, y luego mouse.onkeydown para manejar las flechas del teclado.

La única trampa es que keydown solo se dispara en elementos con foco. Así que necesitamos agregar tabindex al elemento. Como un requisito es no cambiar el HTML, podemos usar la propiedad mouse.tabIndex para eso.

P.S. También podemos reemplazar mouse.onclick con mouse.onfocus.

Abrir la solución en un entorno controlado.

Mapa del Tutorial