Introducción
Moonroom es una pequeña exposición de fotografías reales del programa Apollo. La colección está preparada, pero la exposición actual todavía no tiene tarjetas. Crearás una tarjeta con significado, la ampliarás a partir de los datos y harás que la galería completa se lea bien en pantallas estrechas y anchas. La Study collection, independiente, muestra la experiencia de consulta esperada antes de empezar.
Necesitas conocimientos básicos de estructura HTML, selectores y diseño CSS, y objetos, arrays y eventos DOM de JavaScript. Este laboratorio presenta figure, figcaption, botones nativos, identificadores de foto estables, forEach, dataset y las reglas adaptables que se usan aquí. El visor detallado ya está preparado en este primer laboratorio; en el siguiente construirás su comportamiento. El autor estima 50 minutos, incluida la lectura y los errores de edición habituales.
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 editar /home/labex/project/moonroom y Web 8080 para usar el sitio. En este laboratorio, edita solo index.html, gallery.js y styles.css. Conserva photos.js, viewer.js, images/ y credits.html. Los archivos de imagen y los metadatos proporcionados deben seguir asociados a sus identificadores originales. Todas las etiquetas del sitio siguen en inglés. Estas comprobaciones prácticas con el teclado no constituyen una certificación completa de accesibilidad.
Relaciona una Fotografía con Su Significado
En este paso, usarás una exposición funcional con el teclado y crearás la primera tarjeta de tu propia exposición.
Cambia a Web 8080 y selecciona el enlace de la página Study collection. Es un ejemplo preparado por separado; modificarlo queda fuera de tu espacio de trabajo. Pulsa Tab hasta que el primer botón de fotografía Earthrise muestre un contorno visible y pulsa Enter. La vista detallada muestra la Tierra sobre el horizonte lunar. Pulsa Esc: el foco vuelve a la misma tarjeta. Observa que la otra tarjeta Earthrise tiene una fotografía y una descripción diferentes. Un título es una etiqueta, no una identidad única.
Selecciona el enlace de la página Current exhibition para volver a tu propia página. Este enlace navega dentro del sitio; no cambia de pestaña de LabEx. Tu página tiene inicialmente el encabezado y los créditos, pero ninguna tarjeta. Ese es el estado incompleto que vas a cambiar.
Cambia a WebIDE. En Explorer, abre la carpeta moonroom dentro de /home/labex/project. Si Explorer está oculto, selecciona View → Explorer. Abre photos.js. El array photos contiene un objeto por fotografía. Cada objeto reúne estos valores relacionados:
| Campo | Significado |
|---|---|
id |
Identidad estable, como earthrise-eight; dos títulos pueden ser iguales. |
thumbnail |
Una versión local real de menor tamaño que se usa en la tarjeta. |
large |
La versión correspondiente de mayor tamaño para el visor. |
alt |
Texto significativo que describe la imagen para quienes no pueden verla. |
title y caption |
Nombre visible y contexto. |
No edites este archivo. En el primer registro, la miniatura es images/earthrise-eight-thumb.jpg y la imagen grande es images/earthrise-eight.jpg. Una ruta de imagen relativa se resuelve desde la página HTML, no desde la carpeta actual del editor. Importan las mayúsculas, las minúsculas y la extensión .jpg.
Un figure agrupa una imagen con su figcaption. El botón hace que abrir la fotografía sea una acción nativa de teclado: puede recibir el foco mediante Tab y activarse con Enter. Un div con un manejador de clic del ratón no ofrecería automáticamente esos comportamientos. type="button" hace explícito el comportamiento previsto. El título y la descripción visibles siguen disponibles sin pasar el puntero por encima. aria-label proporciona al botón su nombre accesible breve, Open Earthrise; alt describe el contenido de la imagen. Como dos fotografías comparten ese nombre, aria-describedby también conecta el botón con su descripción a través del id único de esta. La descripción aporta un contexto que permite distinguirlas, como Apollo 8 o Apollo 11. Un nombre accesible identifica un control; una descripción asociada añade contexto. Así se evita que dos controles con el mismo nombre resulten indistinguibles para quien navega sin ver las imágenes. Un marcador de posición o una imagen de fondo no proporcionarían esa descripción de la imagen.
El atributo data-photo-card identifica el contenedor completo de la tarjeta; data-photo-id identifica su botón de apertura. Ambos usan el identificador estable del registro. Conservamos estos pequeños atributos de enlace para que datos, tarjetas y visor se refieran a la misma fotografía; no son texto visible. Una tarjeta puede tener envoltorios adicionales, pero su botón y su descripción deben pertenecer al contenedor más cercano cuyo data-photo-card coincida.
Abre index.html, selecciona todo su texto con Ctrl+A o Cmd+A y sustitúyelo por el HTML completo de abajo. Conserva las líneas script: defer ejecuta esos scripts después de analizar el HTML, en el orden indicado. photos.js proporciona los datos, gallery.js construirá la colección en el paso siguiente y el viewer.js preparado gestiona la vista detallada de este laboratorio. El marcado dialog es el panel que acabas de probar; en el siguiente laboratorio se explicará su comportamiento y lo construirás.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Moonroom — A little farther from home</title>
<link rel="stylesheet" href="styles.css">
<script src="photos.js" defer></script>
<script src="gallery.js" defer></script>
<script src="viewer.js" defer></script>
</head>
<body>
<main>
<header>
<nav aria-label="Exhibitions"><a href="/">Current exhibition</a> · <a href="/example/">Study collection</a></nav>
<p class="eyebrow">Moonroom / Photographic studies</p>
<h1>A little farther from home.</h1>
<p class="intro">Three moments from the Apollo archive. Take a closer look.</p>
</header>
<section id="gallery" class="photo-grid" aria-label="Photographs">
<figure data-photo-card="earthrise-eight">
<button type="button" class="photo-button" data-photo-id="earthrise-eight" aria-label="Open Earthrise" aria-describedby="caption-earthrise-eight">
<img src="images/earthrise-eight-thumb.jpg" alt="Blue and white Earth above the grey lunar horizon">
<span class="photo-name">Earthrise</span>
</button>
<figcaption id="caption-earthrise-eight">Apollo 8 · Earth above the Moon, December 1968.</figcaption>
</figure>
</section>
<footer>Photographs: NASA. An independent educational exhibition. <a href="credits.html">Image credits</a></footer>
</main>
<dialog id="viewer" aria-labelledby="photo-title" aria-describedby="photo-caption">
<button id="close" type="button" autofocus>Close</button>
<h2 id="photo-title"></h2>
<img id="large-photo" alt="">
<p id="photo-error" role="status"></p>
<p id="photo-caption"></p>
<nav aria-label="Photo navigation">
<button id="previous" type="button">Previous</button>
<p id="photo-position"></p>
<button id="next" type="button">Next</button>
</nav>
</dialog>
</body>
</html>
Guarda con Ctrl+S o Cmd+S. Cambia a Web 8080, selecciona Current exhibition si hace falta y actualiza esa página del sitio. Deberías ver una tarjeta Earthrise, su imagen real y la descripción de Apollo 8. El diseño todavía es sencillo; lo estilizarás más adelante. Usa Tab para llegar a la tarjeta, Enter para abrirla y Esc para volver. La imagen grande debe mostrar la misma escena con más detalle.
Si la imagen está rota o la página está vacía, vuelve a WebIDE. Confirma que el archivo se llama index.html, está guardado dentro de moonroom y que src, las comillas y las etiquetas de cierre coinciden con el ejemplo. Vuelve a abrir Current exhibition y actualiza después de guardar. No reinicies el entorno ni sustituyas la imagen por otro archivo para ocultar una ruta incorrecta. Puedes restaurar todo el archivo copiando de nuevo el HTML completo.
Ejecuta la comprobación de este paso cuando la tarjeta real sea visible. Comprueba tu exposición actual, nunca el ejemplo independiente.
Renderiza Cada Registro sin Perder Su Identidad
En este paso, sustituirás la única tarjeta escrita a mano por tarjetas generadas a partir del array completo.
Permanece en WebIDE y vuelve a abrir photos.js para comparar. Sus registros primero y tercero se llaman Earthrise, pero tienen identificadores, rutas de imagen y descripciones diferentes. Buscar fotografías por el título haría perder esa distinción. La galería terminada debe incluir exactamente un botón de apertura visible para cada uno de los tres identificadores proporcionados, con la miniatura, el título, el texto alternativo y la descripción correspondientes. Conserva el orden y los datos proporcionados.
Ahora abre gallery.js. document.querySelector('#gallery') selecciona el elemento cuyo id es gallery. replaceChildren() vacía ese elemento y elimina la tarjeta temporal escrita a mano para que no aparezca dos veces. photos.forEach(function (photo) { ... }) llama a la función incluida una vez por cada registro del array, en orden; photo es el objeto actual. Es la misma idea de trabajo repetido que un bucle, pasando cada registro a la función.
document.createElement() crea un elemento HTML real en memoria. Asignar textContent introduce texto en él, en lugar de interpretar el texto como HTML. append() inserta los elementos preparados en el orden indicado. El navegador ve las mismas relaciones que en tu primer figure, botón, imagen y descripción.
dataset lee y escribe atributos data-. JavaScript usa camel case: figure.dataset.photoCard escribe data-photo-card, y button.dataset.photoId escribe data-photo-id. Ambos reciben photo.id, mientras que el título mostrado recibe photo.title. Cada descripción también recibe un id único al unir caption- con photo.id; el aria-describedby del botón hace referencia a ese identificador de descripción. La descripción visible permanece fuera del botón, pero está asociada a él. setAttribute() establece un atributo como aria-label; el operador + une el texto Open con el título. No hace falta sintaxis de plantillas de cadenas.
Sustituye todo gallery.js por este código completo y guárdalo:
const gallery = document.querySelector('#gallery');
gallery.replaceChildren();
photos.forEach(function (photo) {
const figure = document.createElement('figure');
figure.dataset.photoCard = photo.id;
const button = document.createElement('button');
button.type = 'button';
button.className = 'photo-button';
button.dataset.photoId = photo.id;
button.setAttribute('aria-label', 'Open ' + photo.title);
const image = document.createElement('img');
image.src = photo.thumbnail;
image.alt = photo.alt;
const title = document.createElement('span');
title.className = 'photo-name';
title.textContent = photo.title;
const caption = document.createElement('figcaption');
caption.id = 'caption-' + photo.id;
button.setAttribute('aria-describedby', caption.id);
caption.textContent = photo.caption;
button.append(image, title);
figure.append(button, caption);
gallery.append(figure);
});
Cambia a Web 8080 y actualiza Current exhibition. Deberías ver Earthrise, A trace of a visitor y el segundo Earthrise, en ese orden. Cada descripción sigue visible debajo de su botón. La huella es una fotografía vertical; las imágenes Earthrise son cuadradas. Pronto la galería enmarcará las miniaturas de forma uniforme, conservando las distintas proporciones en la vista detallada.
Usa Tab y Enter para abrir cada tarjeta por separado. El primer Earthrise tiene la descripción de Apollo 8; el último, la de Apollo 11. Pulsa Esc después de cada uno y comprueba que el foco vuelve a la tarjeta que usaste, incluida la última en lugar de volver siempre a la primera.
Si sigue habiendo una sola tarjeta, puede que el script no esté guardado, tenga un error de sintaxis o esté en la carpeta equivocada. Vuelve a WebIDE, compara gallery.js con el bloque completo y comprueba el }); de cierre al final de forEach. No cambies photos.js para que sus registros parezcan iguales. Guarda, actualiza el sitio y repite la comparación de las tarjetas antes de ejecutar la comprobación.
Adapta la Galería y Muestra el Foco del Teclado
En este paso, harás que la exposición completa se lea bien en una pantalla estrecha y que resulte fácil localizar la tarjeta que tiene el foco.
Vuelve a WebIDE y abre styles.css. Un diseño adaptable se ajusta al espacio disponible en lugar de imponer un ancho de página fijo. Esta hoja de estilos utiliza flexbox, que ya has visto en los fundamentos de CSS. display: flex coloca las tarjetas en una fila; flex-wrap: wrap permite pasar a otra fila; gap deja espacio entre tarjetas. flex: 1 1 260px permite que un figure crezca, se reduzca y parta de un ancho preferido de 260 píxeles. min-width: 0 permite que el contenido largo se reduzca dentro de una tarjeta, en lugar de ensanchar la página.
La imagen de la tarjeta usa width: 100% y aspect-ratio: 4 / 3 para tener un marco uniforme. object-fit: cover llena ese marco de miniatura recortando sus bordes en pantalla; no cambia el archivo. En cambio, la imagen detallada usa object-fit: contain, max-width y max-height, para que se vea entera y con sus propias proporciones. vw y vh son centésimas del ancho y del alto de la ventana gráfica. min() limita un tamaño, mientras que clamp() permite que el encabezado crezca entre un mínimo y un máximo. overflow-wrap: anywhere permite dividir un título muy largo en líneas, en lugar de ensanchar la página.
:focus-visible selecciona un control cuando debe mostrarse el foco del teclado. Un contorno de alto contraste y outline-offset hacen visible el botón actual sin desplazar el diseño. No elimines el contorno del navegador sin ofrecer otro indicador claro. Este proyecto acepta un contorno de foco distinguible de al menos 2 píxeles, un borde visible distinguible de al menos 2 píxeles o una sombra visible que aparezca cuando el control recibe el foco mediante teclado. Puedes cambiar los colores o el diseño conservando esos comportamientos observables.
Las reglas inferiores conservan el visor preparado: ::backdrop da estilo al fondo detrás de un diálogo modal, :disabled distingue visualmente un botón de límite y [hidden] debe ocultar realmente una imagen que ha fallado aunque las imágenes normales del visor usen display: block. La consulta de medios de movimiento reducido respeta la preferencia de movimiento del navegador; esta exposición no tiene transiciones animadas.
Sustituye todo styles.css por la siguiente hoja de estilos completa y guárdala:
* { box-sizing: border-box; }
body { margin: 0; background: #f5f1e8; color: #252c28; font: 17px/1.6 system-ui, sans-serif; }
main { max-width: 1160px; margin: auto; padding: 40px 24px; }
header { max-width: 760px; margin-bottom: 40px; }
.eyebrow { color: #6e593f; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; }
h1 { margin: 12px 0; font: clamp(2.5rem, 6vw, 5rem)/1.08 Georgia, serif; }
.intro { max-width: 40rem; color: #5c655e; }
.photo-grid { display: flex; flex-wrap: wrap; gap: 28px; }
figure { flex: 1 1 260px; min-width: 0; margin: 0; }
button { color: inherit; font: inherit; cursor: pointer; }
.photo-button { width: 100%; padding: 0; text-align: left; background: transparent; border: 0; }
.photo-button img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 4px; }
.photo-name { display: block; margin-top: 12px; font: 1.5rem/1.3 Georgia, serif; overflow-wrap: anywhere; }
figcaption { margin-top: 6px; color: #5c655e; font-size: 14px; }
a { color: #814820; text-underline-offset: 4px; }
button:focus-visible, a:focus-visible { outline: 3px solid #af421f; outline-offset: 5px; }
footer { margin-top: 48px; border-top: 1px solid #d1c8b8; padding-top: 18px; color: #5c655e; font-size: 13px; }
dialog { width: min(880px, 94vw); max-height: 94vh; overflow: auto; padding: 24px; border: 0; border-radius: 8px; background: #fffcf6; color: #252c28; }
dialog::backdrop { background: #19251fe6; }
dialog h2 { font: 1.8rem/1.3 Georgia, serif; overflow-wrap: anywhere; }
dialog img { display: block; max-width: 100%; max-height: 52vh; margin: auto; object-fit: contain; }
dialog img[hidden] { display: none; }
dialog button { padding: 8px 14px; border: 1px solid #6c746b; border-radius: 4px; background: #fffcf6; }
dialog nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
dialog button:disabled { opacity: .45; cursor: default; }
#photo-position { white-space: nowrap; }
#photo-error { color: #9b301c; }
#photo-error:empty { display: none; }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } }
Cambia a Web 8080 y actualiza Current exhibition. En una ventana gráfica ancha, las tarjetas comparten una fila; en una estrecha, se apilan en una columna legible. Cambia el tamaño del navegador o del panel del sitio a unos 390 píxeles de ancho y después vuelve a ampliarlo. No debe haber desplazamiento horizontal de la página, títulos recortados ni tarjetas ausentes. La fotografía real de mayor tamaño debe conservar su forma vertical o cuadrada al abrirla.
Desde la página, pulsa Tab repetidamente. Los enlaces del sitio y los tres botones de fotografía deben mostrar un contorno de foco visible. Enter abre la fotografía enfocada; Esc vuelve a ella. Ninguna descripción debe exigir pasar el puntero por encima. Abre Image credits para confirmar que su página real enumera las fuentes de las imágenes de NASA y elige Current exhibition para volver. Son enlaces del sitio, distintos de cambiar entre WebIDE y Web 8080.
Si el diseño sigue siendo sencillo, confirma que el archivo guardado se llama styles.css y que index.html sigue enlazándolo. Si una tarjeta es más ancha que la página, comprueba min-width: 0, width: 100% en su imagen y las reglas de ajuste de línea de flex. Restaura la hoja de estilos completa si hace falta; conserva todas las fotografías en lugar de ocultar una difícil. Ejecuta la comprobación final después de revisar ambos tamaños de pantalla y la navegación con teclado.

En esta vista final de pantalla ancha, compara las descripciones del primer y del último Earthrise: las fotografías proceden de misiones distintas. La huella sigue siendo una tarjeta central independiente. Las mismas tres tarjetas se apilan en una pantalla estrecha.
Resumen
Has relacionado una imagen real con su descripción, ampliado una tarjeta semántica hasta convertirla en una colección completa generada a partir de datos y distinguido dos fotografías con el mismo título mediante identificadores estables. Tu galería adaptable conserva todas las tarjetas, el contenido real de las imágenes y el foco del teclado visible. En el siguiente laboratorio construirás el visor detallado, incluidos los límites de navegación y un fallo de imagen recuperable.
