Introducción
Las tarjetas de fotografías reales de Moonroom ya tienen descripciones legibles y un diseño adaptable. El visor detallado está desconectado. Tu tarea es hacer que cada tarjeta abra su propia fotografía de mayor tamaño, permitir que los visitantes recorran la colección y devolverlos exactamente a la tarjeta que usaron. También provocarás y resolverás un error real de imagen ausente, sin sustituirla por otra.
Este laboratorio empieza con su propia galería completa y fotografías locales. No depende de archivos de un entorno anterior. Necesitas las relaciones entre tarjetas y datos del primer laboratorio, funciones de JavaScript, selección DOM y eventos. Antes de usarlos, se presentan la nueva búsqueda en arrays y los comportamientos de diálogo, foco y eventos de imagen. El autor estima 60 minutos de lectura y práctica.
Después de abrir o actualizar una página del sitio, deja que termine de cargar antes de activar sus controles de fotografías. Usa WebIDE para /home/labex/project/moonroom y Web 8080 para el sitio. Edita viewer.js; el cambio temporal de la ruta large en photos.js solo se permite para el ejercicio de fallo enseñado explícitamente y debe restaurarse antes de comprobar. Conserva los demás archivos, metadatos e imágenes originales proporcionados. Los dos registros Earthrise siguen siendo independientes, y todas las tarjetas, miniaturas y descripciones deben seguir siendo utilizables. Puedes usar un comportamiento JavaScript equivalente; conserva los identificadores de botón proporcionados, los atributos de asociación estable de fotos, los controles visibles Close, Previous y Next, y las asociaciones de título y descripción del diálogo nativo. Estos ejercicios con teclado no son una certificación completa de accesibilidad.
Abre la Foto Correcta y Vuelve a Su Tarjeta
En este paso, conectarás las tarjetas existentes con un diálogo modal nativo, incluido un punto de retorno fiable para el foco del teclado.
Cambia a Web 8080, selecciona el enlace del sitio Study collection y usa Tab, Enter y Esc para abrir y cerrar una fotografía. Después selecciona Current exhibition. Sus tres tarjetas y descripciones ya están presentes. Prueba Enter en una tarjeta: todavía no tiene efecto porque tu viewer.js solo contiene un comentario. Ese es el comportamiento incompleto esperado, no un fallo del entorno.
Cambia a WebIDE, abre la carpeta moonroom en Explorer y lee index.html. El elemento dialog es un panel integrado del navegador. Su aria-labelledby="photo-title" apunta al encabezado visible, y aria-describedby="photo-caption" apunta a la explicación actual. showModal() lo abre como modal: el resto del contenido de la página queda inactivo mientras está abierto. El navegador gestiona el comportamiento básico de Tab en el modal y el cierre con Esc; no necesitas escribir un mecanismo personalizado para mantener el foco dentro. No añadas tabindex al propio diálogo. El botón Close preparado tiene autofocus, por lo que al abrir el panel el foco se sitúa en una forma clara de salir.
Abre viewer.js. const guarda referencias a elementos DOM. Se usa let para los valores que cambiaremos: currentIndex es la posición seleccionada del array, empezando por cero, y openingButton recuerda el botón real que abrió el panel. photos[currentIndex] obtiene el registro completo de esa fotografía. showPhoto() actualiza conjuntamente su título, descripción, texto alternativo e imagen grande real.
La galería tiene un único listener de clic. Enter en un botón nativo también lo activa. event.target es el elemento exacto en el que se hizo clic, que puede ser la imagen o el título dentro del botón. closest('button[data-photo-id]') busca hacia fuera el botón de apertura correspondiente. if (!button) return ignora los clics que no proceden de uno. Esto es delegación de eventos: un único listener gestiona todos los botones de tarjeta existentes.
findIndex() busca en el array y devuelve la posición cuyo identificador coincide con el dataset.photoId del botón. Su callback devuelve true cuando photo.id === button.dataset.photoId; === compara sin convertir los tipos de los valores. Si no coincide ningún registro, findIndex() devuelve -1, así que la condición de protección detiene la operación en lugar de abrir una fotografía incorrecta. Comparamos deliberadamente identificadores, no el título repetido Earthrise.
El evento close del diálogo se ejecuta tanto si se cierra con Esc como con el botón Close. openingButton.focus() devuelve el foco a la tarjeta recordada. Este uso de focus() implementa el retorno; aun así, tu prueba manual debe llegar a las tarjetas y activarlas con el teclado real.
Sustituye todo viewer.js por el código completo de abajo y guarda con Ctrl+S o Cmd+S:
const viewer = document.querySelector('#viewer');
const largePhoto = document.querySelector('#large-photo');
const photoTitle = document.querySelector('#photo-title');
const photoCaption = document.querySelector('#photo-caption');
let currentIndex = 0;
let openingButton;
function showPhoto() {
const photo = photos[currentIndex];
photoTitle.textContent = photo.title;
photoCaption.textContent = photo.caption;
largePhoto.alt = photo.alt;
largePhoto.src = photo.large;
}
document.querySelector('#gallery').addEventListener('click', function (event) {
const button = event.target.closest('button[data-photo-id]');
if (!button) return;
currentIndex = photos.findIndex(function (photo) {
return photo.id === button.dataset.photoId;
});
if (currentIndex === -1) return;
openingButton = button;
showPhoto();
viewer.showModal();
});
document.querySelector('#close').addEventListener('click', function () {
viewer.close();
});
viewer.addEventListener('close', function () {
openingButton.focus();
});
Cambia a Web 8080, selecciona Current exhibition si hace falta y actualiza la página. Usa Tab para llegar a cada tarjeta y Enter para abrirla. El primer Earthrise debe mostrar la fotografía y la descripción de Apollo 8; el último, las de Apollo 11. Pulsa Esc después de cada uno y observa el foco en esa misma tarjeta. Vuelve a abrir una foto y usa Tab y luego Enter en Close como segunda forma de cerrar. Previous y Next no se conectarán hasta el siguiente paso.
Si no se abre ningún panel, vuelve a WebIDE y compara el archivo con el bloque completo. Comprueba que esté guardado como viewer.js, que la línea del script permanezca en index.html y que los valores data-photo-id sigan coincidiendo con photos.js. Guarda y actualiza el sitio después de corregir un error de escritura. Si aparece la escena equivocada, compara conjuntamente large, el título y la descripción del registro actual; no cambies los datos de origen para disimular un error de búsqueda por identificador. Ejecuta la comprobación cuando funcionen las tres rutas independientes de Enter/Esc.
Recorre Toda la Colección y Sus Límites
En este paso, actualizarás la misma vista detallada sin perder la tarjeta original que la abrió.
Vuelve a WebIDE y compara las posiciones del array, que empiezan en cero, en photos.js: la primera es 0, la segunda es 1 y la última es photos.length - 1. length cuenta los registros. Una posición visible debe contar desde uno, así que currentIndex + 1 da un número fácil de leer, como 1 / 3.
La propiedad disabled recibe un booleano. previous.disabled = currentIndex === 0 desactiva Previous solo en la primera fotografía; la regla correspondiente de Next lo desactiva solo en la última. Estos límites evitan buscar fuera del array. Los manejadores de clic también comprueban los límites antes de cambiar el índice. currentIndex -= 1 resta uno y guarda el índice nuevo; += 1 suma uno. Cada cambio llama a showPhoto() para que imagen, descripción y posición permanezcan sincronizadas.
Solo abrir una tarjeta cambia openingButton. Navegar a otra fotografía dentro del diálogo no debe cambiarlo: al cerrar, se vuelve a la tarjeta donde empezó la visita, aunque se esté mostrando una fotografía distinta.
Sustituye todo viewer.js por esta versión completa y guárdala. Conserva la apertura y el cierre, y añade la navegación con límites:
const viewer = document.querySelector('#viewer');
const largePhoto = document.querySelector('#large-photo');
const photoTitle = document.querySelector('#photo-title');
const photoCaption = document.querySelector('#photo-caption');
const previous = document.querySelector('#previous');
const next = document.querySelector('#next');
let currentIndex = 0;
let openingButton;
function showPhoto() {
const photo = photos[currentIndex];
photoTitle.textContent = photo.title;
photoCaption.textContent = photo.caption;
largePhoto.alt = photo.alt;
largePhoto.src = photo.large;
previous.disabled = currentIndex === 0;
next.disabled = currentIndex === photos.length - 1;
document.querySelector('#photo-position').textContent = (currentIndex + 1) + ' / ' + photos.length;
}
document.querySelector('#gallery').addEventListener('click', function (event) {
const button = event.target.closest('button[data-photo-id]');
if (!button) return;
currentIndex = photos.findIndex(function (photo) {
return photo.id === button.dataset.photoId;
});
if (currentIndex === -1) return;
openingButton = button;
showPhoto();
viewer.showModal();
});
previous.addEventListener('click', function () {
if (currentIndex > 0) {
currentIndex -= 1;
showPhoto();
}
});
next.addEventListener('click', function () {
if (currentIndex < photos.length - 1) {
currentIndex += 1;
showPhoto();
}
});
document.querySelector('#close').addEventListener('click', function () {
viewer.close();
});
viewer.addEventListener('close', function () {
openingButton.focus();
});
Cambia a Web 8080 y actualiza Current exhibition. Usa el teclado para abrir el primer Earthrise. Previous está desactivado; Tab omite ese botón. Llega a Next con Tab y pulsa Enter. Deberías ver la huella vertical, su título y descripción, y 2 / 3. Continúa hasta el último Earthrise y confirma que aparecen la imagen y la descripción de Apollo 11 mientras Next está desactivado.
Llega a Previous con Tab y vuelve pasando por ambas imágenes. Mantén pulsada Shift al pulsar Tab cuando necesites retroceder entre los controles disponibles; esto mueve el foco del teclado, mientras que activar Previous cambia la fotografía. Son acciones distintas. Pulsa Esc y confirma que el foco vuelve a la primera tarjeta original. Ahora abre directamente la tarjeta central, navega a otra fotografía y cierra: el foco debe volver a la tarjeta central.
Es normal que un control de límite esté desactivado en la primera o la última fotografía. Un control que parece activo pero no hace nada no es normal. Si cambia un título pero la fotografía no, vuelve a WebIDE y asegúrate de que cada movimiento llama a la misma función showPhoto(). Si Esc siempre vuelve a la primera tarjeta, confirma que openingButton se asigna dentro del listener de apertura de tarjeta y no se sobrescribe en los manejadores de navegación. Restaura el código completo si hace falta, guarda y actualiza antes de comprobar.
Muestra el Fallo de una Imagen y Permite Recuperarse
En este paso, gestionarás un fallo real de carga de imagen conservando la posición del visitante y la utilidad del resto de la colección.
El evento load de una imagen significa que su archivo se ha cargado correctamente; error significa que no se ha podido cargar, por ejemplo, porque falta su ruta. Son eventos de imagen del navegador, no una prueba de que cualquier respuesta HTTP contenga una imagen válida. Un fallo de conexión al servidor puede hacer que toda la página deje de estar disponible; aquí la página funciona y falla la petición de una sola imagen.
En WebIDE, abre viewer.js. Esta versión establece los manejadores de eventos antes de asignar src, de modo que una imagen rápida en caché no termine antes de que existan sus manejadores. Oculta la imagen anterior mientras carga y muestra Loading photo... en el párrafo existente con role="status". Si tiene éxito, muestra la imagen real y borra el mensaje. Si falla, mantiene la imagen oculta y ofrece indicaciones legibles para recuperarse. La regla CSS dialog img[hidden] { display: none; } es necesaria porque la regla normal de imagen del diálogo usa display: block. De otro modo, esa regla CSS anularía un atributo hidden por sí solo.
El manejador de error no cambia src a una miniatura ni a otra fotografía. Eso ocultaría el fallo y mostraría contenido que no corresponde al registro actual. El usuario puede cerrar el panel o navegar a una fotografía intacta.
Sustituye todo viewer.js por la versión final completa y guárdala:
const viewer = document.querySelector('#viewer');
const largePhoto = document.querySelector('#large-photo');
const photoTitle = document.querySelector('#photo-title');
const photoCaption = document.querySelector('#photo-caption');
const photoError = document.querySelector('#photo-error');
const previous = document.querySelector('#previous');
const next = document.querySelector('#next');
let currentIndex = 0;
let openingButton;
function showPhoto() {
const photo = photos[currentIndex];
photoTitle.textContent = photo.title;
photoCaption.textContent = photo.caption;
largePhoto.alt = photo.alt;
photoError.textContent = 'Loading photo...';
largePhoto.hidden = true;
largePhoto.onload = function () {
largePhoto.hidden = false;
photoError.textContent = '';
};
largePhoto.onerror = function () {
largePhoto.hidden = true;
photoError.textContent = 'This photo could not be loaded. Close the viewer and check its large image path.';
};
largePhoto.src = photo.large;
previous.disabled = currentIndex === 0;
next.disabled = currentIndex === photos.length - 1;
document.querySelector('#photo-position').textContent = (currentIndex + 1) + ' / ' + photos.length;
}
document.querySelector('#gallery').addEventListener('click', function (event) {
const button = event.target.closest('button[data-photo-id]');
if (!button) return;
currentIndex = photos.findIndex(function (photo) {
return photo.id === button.dataset.photoId;
});
if (currentIndex === -1) return;
openingButton = button;
showPhoto();
viewer.showModal();
});
previous.addEventListener('click', function () {
if (currentIndex > 0) {
currentIndex -= 1;
showPhoto();
}
});
next.addEventListener('click', function () {
if (currentIndex < photos.length - 1) {
currentIndex += 1;
showPhoto();
}
});
document.querySelector('#close').addEventListener('click', function () {
viewer.close();
});
viewer.addEventListener('close', function () {
openingButton.focus();
});
Ahora comete un error de ruta controlado en tus propios archivos y recupérate de él. Abre photos.js en WebIDE. En el registro cuyo id es footprint, cambia únicamente su valor large de images/footprint.jpg a images/missing-photo.jpg. No cambies su miniatura, identificador, descripción ni texto alternativo. Guarda el archivo.
Cambia a Web 8080, actualiza Current exhibition y usa Tab y Enter para abrir A trace of a visitor. Su miniatura sigue funcionando, pero ahora falla la petición de la imagen grande. Debe aparecer el texto visible This photo could not be loaded. Close the viewer and check its large image path. El panel no debe mostrar Earthrise ni una miniatura estirada como sustituto. Se conservan el título y la descripción de la huella, y los controles Close, Previous y Next siguen disponibles.

El panel con el fallo conserva el título y la descripción de la huella, sin ninguna imagen de sustitución. Next sigue disponible para que el visitante pueda continuar.
Usa Tab y Enter en Next. La imagen intacta de Apollo 11 debe cargarse y el mensaje de error debe borrarse. Pulsa Esc y comprueba que el foco vuelve a la tarjeta de la huella que abrió el panel. Esto recupera la navegación dentro de la colección; no ha reparado la ruta incorrecta del archivo.
Vuelve a WebIDE, restaura el valor exacto de large del registro footprint a images/footprint.jpg y guarda. Cambia a Web 8080 y actualiza Current exhibition para que el navegador lea los datos restaurados. Abre de nuevo la huella. Su imagen vertical real debe decodificarse y aparecer sin el mensaje de error. Actualizar el navegador vuelve a cargar los datos de la página; no reinicia el trabajo guardado ni recrea el entorno.

Después de restaurar la ruta y actualizar, la huella vertical ocupa el visor sin estirarse; el mensaje de error ha desaparecido.
Por último, comprueba de nuevo las tres tarjetas, los límites en ambos sentidos y las dos formas de cerrar. Estrecha el panel del sitio y confirma que la fotografía completa y el encabezado dividido en líneas caben dentro del diálogo sin desplazamiento horizontal de la página. Deben conservarse todas las miniaturas, metadatos, imágenes y créditos originales. La comprobación final exige restaurar los datos proporcionados y prueba el manejo de fallos solo en una copia desechable de tus archivos; nunca elimina una imagen de tu exposición real.
Resumen
Has usado identificadores estables para mostrar el contenido real de cada fotografía en un diálogo nativo. Has añadido navegación anterior/siguiente con límites, conservado el punto de retorno original para Esc y Close y gestionado una petición real de imagen ausente sin sustituirla por otra. Tu galería mantiene utilizables sus tarjetas, descripciones y demás fotografías mientras informa de un fallo local de forma honesta.
