Saltar a contenido

WA4 · Ejercicios de movimiento, accesibilidad y rendimiento

Los ejercicios WA4.1 a WA4.4 son de JavaScript sin DOM: escríbelos en un .js/.mjs y ejecútalos con node; tu salida debe ser idéntica al resultado esperado, obtenido ejecutando la solución modelo con Node de verdad. Los WA4.5 y WA4.6 son de HTML/DOM: se hacen en una página y se comprueban en el navegador. Los marcados con ⭐ son más difíciles.

Ejercicio WA4.1

Calcular el contraste. Completa contraste (la función luminancia ya está hecha).

function luminancia(hex) {
  const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
    .map(c => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

// contraste(a, b): la proporción de contraste WCAG entre dos colores #rrggbb, SIEMPRE mayor o igual que 1
// (el color más claro va en el numerador): (Lclaro + 0.05) / (Loscuro + 0.05)
function contraste(a, b) {
  // ...
}

Para comprobarlo:

console.log(contraste("#000000", "#ffffff").toFixed(2));
console.log(contraste("#ffffff", "#000000").toFixed(2));
console.log(contraste("#777777", "#ffffff").toFixed(2));
console.log(contraste("#ffffff", "#2563eb").toFixed(2));

Resultado esperado (ejecutado con Node):

21.00
21.00
4.48
5.17
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

function luminancia(hex) {
  const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
    .map(c => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
// ... (resto de la solución bloqueado)

Ejercicio WA4.2

¿Pasa la prueba? Escribe nivel.

// (usa la función contraste del ejercicio anterior)
// nivel(texto, fondo, grande = false) devuelve "AAA", "AA" o "suspenso":
//   texto normal:  AAA si contraste >= 7,   AA si >= 4.5
//   texto grande:  AAA si contraste >= 4.5, AA si >= 3
function nivel(texto, fondo, grande = false) {
  // ...
}

Para comprobarlo:

console.log(nivel("#000000", "#ffffff"));
console.log(nivel("#767676", "#ffffff"));
console.log(nivel("#777777", "#ffffff"));
console.log(nivel("#777777", "#ffffff", true));
console.log(nivel("#aaaaaa", "#ffffff", true));

Resultado esperado (ejecutado con Node):

AAA
AA
suspenso
AA
suspenso
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

function luminancia(hex) {
  const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
    .map(c => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
// ... (resto de la solución bloqueado)

Ejercicio WA4.3

debounce. Escríbelo desde cero.

// debounce(fn, ms): devuelve una función que, al llamarla, espera `ms` milisegundos SIN nuevas llamadas
// y entonces ejecuta fn UNA vez, con los argumentos de la ÚLTIMA llamada.
function debounce(fn, ms) {
  // ...
}

Para comprobarlo:

const dormir = ms => new Promise(r => setTimeout(r, ms));
const vistos = [];
const guardar = debounce(valor => vistos.push(valor), 60);

for (const v of ["a", "b", "c"]) { guardar(v); await dormir(10); }
await dormir(150);
guardar("d");
await dormir(10);
guardar("e");
await dormir(150);
console.log(vistos);

Resultado esperado (ejecutado con Node):

[ 'c', 'e' ]
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

function debounce(fn, ms) {
  let id;
  return (...args) => {
    clearTimeout(id);
    id = setTimeout(() => fn(...args), ms);
// ... (resto de la solución bloqueado)

Ejercicio WA4.4

throttle. Escríbelo desde cero, con un reloj inyectado para poder probarlo sin esperar.

// throttle(fn, ms, ahora = Date.now): devuelve una función que ejecuta fn como mucho UNA vez cada `ms` ms:
// la primera llamada pasa; las siguientes se ignoran hasta que hayan pasado `ms` desde la última que SÍ pasó.
// Devuelve lo que devuelve fn cuando se ejecuta, y undefined cuando se ignora la llamada.
function throttle(fn, ms, ahora = Date.now) {
  // ...
}

Para comprobarlo:

let reloj = 0;
const f = throttle(x => "pasó " + x, 100, () => reloj);
for (const [t, x] of [[0, "A"], [50, "B"], [99, "C"], [100, "D"], [150, "E"], [199, "F"], [200, "G"]]) {
  reloj = t;
  console.log(t, "→", f(x));
}

Resultado esperado (ejecutado con Node):

0 → pasó A
50 → undefined
99 → undefined
100 → pasó D
150 → undefined
199 → undefined
200 → pasó G
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

function throttle(fn, ms, ahora = Date.now) {
  let ultima = -Infinity;
  return (...args) => {
    const t = ahora();
    if (t - ultima >= ms) {
// ... (resto de la solución bloqueado)

Ejercicio WA4.5

Un menú desplegable accesible. (DOM: se comprueba en el navegador.) Con este HTML, escribe el JavaScript para que:

  • el botón abra y cierre el menú, manteniendo aria-expanded sincronizado con su estado (el menú se oculta con el atributo hidden);
  • Escape cierre el menú y devuelva el foco al botón;
  • un clic fuera del menú lo cierre.

Comprueba todo solo con el teclado (Tab, Intro, Escape).

<button id="boton" aria-expanded="false" aria-controls="menu">Menú</button>
<ul id="menu" hidden>
  <li><a href="#inicio">Inicio</a></li>
  <li><a href="#cursos">Cursos</a></li>
  <li><a href="#contacto">Contacto</a></li>
</ul>
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

<button id="boton" aria-expanded="false" aria-controls="menu">Menú</button>
<ul id="menu" hidden>
  <li><a href="#inicio">Inicio</a></li>
  <li><a href="#cursos">Cursos</a></li>
  <li><a href="#contacto">Contacto</a></li>
</ul>
<!-- ... (resto de la solución bloqueado) -->

Ejercicio WA4.6

⭐ Una imagen responsiva. (HTML: se comprueba en las herramientas del navegador.) Escribe el HTML de una imagen de portada que:

  • ofrezca tres anchos (480w, 960w y 1600w) con srcset, y que ocupe toda la pantalla en móviles y la mitad a partir de 60 rem (sizes),
  • ofrezca el formato WebP con <picture> y JPG como alternativa,
  • reserve su hueco (proporción 16:9, 1600 × 900) para evitar saltos de diseño,
  • tenga un texto alternativo descriptivo,
  • sea la imagen principal de la página: no debe cargarse de forma perezosa y debe tener prioridad alta.

(Los archivos no existen: lo que se comprueba es la estructura. En la pestaña «Red» de las herramientas del navegador verás qué archivos intenta pedir.)

Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

<picture>
  <source type="image/webp"
          srcset="portada-480.webp 480w, portada-960.webp 960w, portada-1600.webp 1600w"
          sizes="(min-width: 60rem) 50vw, 100vw">
  <img src="portada-960.jpg"
       srcset="portada-480.jpg 480w, portada-960.jpg 960w, portada-1600.jpg 1600w"
<!-- ... (resto de la solución bloqueado) -->