Crie uma Galeria de Fotografias Reais

HTMLBeginner
Pratique Agora

Introdução

Moonroom é uma pequena exposição de fotografias reais das missões Apollo. A coleção está pronta, mas a exposição atual ainda não tem cartões de fotos. Você criará um cartão com significado, ampliará a galeria a partir dos dados e tornará o conjunto legível em telas estreitas e largas. A Study collection, separada, mostra a experiência de visualização esperada antes de você começar.

Você precisa conhecer estrutura HTML básica, seletores e layout CSS, além de objetos, arrays e eventos DOM em JavaScript. Este laboratório apresenta figure, figcaption, botões nativos, IDs estáveis de fotos, forEach, dataset e as regras responsivas usadas aqui. O visualizador detalhado já está preparado neste primeiro laboratório; no próximo, você construirá seu comportamento. Os 50 minutos são uma estimativa do autor, incluindo leitura e erros comuns de edição.

Após abrir ou atualizar uma página do site, espere o carregamento terminar antes de acionar os controles das fotos. Use WebIDE para editar /home/labex/project/moonroom e Web 8080 para usar o site. Neste laboratório, edite somente index.html, gallery.js e styles.css. Preserve photos.js, viewer.js, images/ e credits.html. Os arquivos de imagem e metadados fornecidos devem continuar associados aos IDs originais. Todos os rótulos do site permanecem em inglês. Estas verificações práticas com o teclado não constituem uma certificação completa de acessibilidade.

Conecte uma Fotografia ao Seu Significado

Nesta etapa, use uma exposição funcional com o teclado e depois crie o primeiro cartão da sua própria exposição.

Mude para Web 8080 e escolha o link da página Study collection. Este é um exemplo preparado separadamente; alterá-lo está fora do seu espaço de trabalho. Pressione Tab até que o primeiro botão de fotografia Earthrise mostre um contorno visível e pressione Enter. A visualização detalhada mostra a Terra acima do horizonte lunar. Pressione Esc: o foco volta ao mesmo cartão. Observe que o outro cartão Earthrise tem uma fotografia e uma legenda diferentes. Um título é um rótulo, não uma identidade única.

Escolha o link da página Current exhibition para voltar à sua própria página. Esse link navega dentro do site; ele não troca a aba do LabEx. Sua página inicialmente tem o cabeçalho e os créditos, mas nenhum cartão. Esse é o estado incompleto que você modificará.

Mude para WebIDE. No Explorer, abra a pasta moonroom dentro de /home/labex/project. Se o Explorer estiver oculto, escolha View → Explorer. Abra photos.js. O array photos contém um objeto por fotografia. Cada objeto reúne estes valores relacionados:

Campo Significado
id Identidade estável, como earthrise-eight; dois títulos podem ser iguais.
thumbnail Uma versão local real, menor, usada no cartão.
large A versão correspondente maior para o visualizador.
alt Texto significativo que descreve a imagem para pessoas que não podem vê-la.
title e caption Nome visível e contexto.

Não edite esse arquivo. No primeiro registro, a miniatura é images/earthrise-eight-thumb.jpg e a imagem maior é images/earthrise-eight.jpg. Um caminho relativo de imagem é resolvido a partir da página HTML, não da pasta atual do editor. Maiúsculas, minúsculas e a extensão .jpg importam.

Um figure agrupa uma imagem com seu figcaption. O botão torna a abertura da fotografia uma ação nativa de teclado: ele pode receber foco com Tab e ser ativado com Enter. Um div com um manipulador de clique do mouse não forneceria esses comportamentos automaticamente. type="button" explicita o comportamento pretendido do botão. O título e a legenda visíveis continuam disponíveis sem passar o ponteiro sobre eles. aria-label dá ao botão seu nome acessível curto, Open Earthrise; alt descreve o conteúdo da imagem. Como duas fotografias compartilham esse nome, aria-describedby também conecta o botão à legenda por meio do id único dela. A legenda fornece contexto que permite distingui-las, como Apollo 8 ou Apollo 11. Um nome acessível identifica um controle; uma descrição associada acrescenta contexto. Isso evita que dois controles de mesmo nome sejam indistinguíveis para quem navega sem ver as fotos. Um marcador de posição ou uma imagem de fundo não forneceria essa descrição da imagem.

O atributo data-photo-card identifica o contêiner completo do cartão; data-photo-id identifica seu botão de abertura. Ambos usam o ID estável do registro. Mantemos esses pequenos atributos de ligação para que dados, cartões e visualizador se refiram à mesma fotografia; eles não são texto visível. Um cartão pode ter elementos envoltórios adicionais, mas seu botão e sua legenda devem pertencer ao contêiner mais próximo com o data-photo-card correspondente.

Abra index.html, selecione todo o texto com Ctrl+A ou Cmd+A e substitua-o pelo HTML completo abaixo. Mantenha as linhas script: defer executa esses scripts após a análise do HTML, na ordem indicada. photos.js fornece os dados, gallery.js construirá a coleção na próxima etapa e o viewer.js preparado controla a visualização detalhada deste laboratório. A marcação dialog é o painel que você acabou de experimentar; seu comportamento será explicado e construído no próximo laboratório.

<!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>

Salve com Ctrl+S ou Cmd+S. Mude para Web 8080, escolha Current exhibition se necessário e atualize essa página do site. Você deve ver um cartão Earthrise, sua imagem real e a legenda de Apollo 8. O layout ainda é simples; você o estilizará depois. Use Tab para chegar ao cartão, Enter para abri-lo e Esc para voltar. A imagem grande deve mostrar a mesma cena com mais detalhes.

Se a imagem estiver quebrada ou a página estiver vazia, volte ao WebIDE. Confirme que o arquivo se chama index.html, está salvo dentro de moonroom e que src, as aspas e as tags de fechamento correspondem ao exemplo. Abra Current exhibition novamente e atualize após salvar. Não redefina o ambiente nem substitua a foto por outro arquivo para esconder um caminho incorreto. Você pode restaurar o arquivo inteiro copiando novamente o HTML completo.

Execute a verificação desta etapa depois que o cartão real estiver visível. Ela verifica sua exposição atual, nunca o exemplo separado.

Renderize Todos os Registros sem Perder a Identidade

Nesta etapa, substitua o cartão único escrito à mão por cartões gerados a partir do array completo.

Permaneça no WebIDE e reabra photos.js para comparar. O primeiro e o terceiro registros têm o nome Earthrise, mas seus IDs, caminhos de imagem e legendas são diferentes. Buscar fotografias pelo título perderia essa distinção. A galeria concluída deve incluir exatamente um botão de abertura visível para cada um dos três IDs fornecidos, com a miniatura, o título, o texto alternativo e a legenda correspondentes. Mantenha a ordem e os dados fornecidos.

Agora abra gallery.js. document.querySelector('#gallery') seleciona o elemento cujo id é gallery. replaceChildren() esvazia esse elemento, removendo o cartão temporário escrito à mão para que ele não apareça duas vezes. photos.forEach(function (photo) { ... }) chama a função interna uma vez para cada registro do array, em ordem; photo é o objeto atual. É a mesma ideia de trabalho repetido de um laço, com cada registro passado para a função.

document.createElement() cria um elemento HTML real na memória. Atribuir textContent insere texto nele, em vez de interpretar o texto como HTML. append() insere os elementos preparados na ordem indicada. O navegador vê as mesmas relações do seu primeiro figure, botão, imagem e legenda.

dataset lê e escreve atributos data-. JavaScript usa camel case: figure.dataset.photoCard escreve data-photo-card e button.dataset.photoId escreve data-photo-id. Ambos recebem photo.id, enquanto o título exibido recebe photo.title. Cada legenda também recebe um id único ao unir caption- com photo.id; o aria-describedby do botão se refere a esse ID da legenda. A legenda visível continua fora do botão, mas está associada a ele. setAttribute() define um atributo como aria-label; o operador + une o texto Open ao título. Não é necessário usar sintaxe de templates de string.

Substitua todo o conteúdo de gallery.js por este código completo e salve:

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);
});

Mude para Web 8080 e atualize Current exhibition. Você deve ver Earthrise, A trace of a visitor e o segundo Earthrise, nessa ordem. Cada legenda continua visível abaixo de seu botão. A pegada é uma fotografia vertical; as imagens Earthrise são quadradas. Em breve, a galeria enquadrará suas miniaturas de maneira uniforme, preservando as diferentes proporções na visualização detalhada.

Use Tab e Enter para abrir cada cartão separadamente. O primeiro Earthrise tem a legenda de Apollo 8; o último, a de Apollo 11. Pressione Esc após cada um e verifique se o foco volta ao cartão usado, inclusive ao último em vez de sempre voltar ao primeiro.

Se restar apenas um cartão, o script pode não estar salvo, ter um erro de sintaxe ou estar na pasta errada. Volte ao WebIDE, compare gallery.js com o bloco completo e confira o }); de fechamento no final de forEach. Não altere photos.js para tornar seus registros parecidos. Salve, atualize o site e compare os cartões novamente antes de executar a verificação.

Adapte a Galeria e Mostre o Foco do Teclado

Nesta etapa, torne a exposição completa legível em uma tela estreita e facilite a localização do cartão que está com foco.

Volte ao WebIDE e abra styles.css. Um layout responsivo se ajusta ao espaço disponível em vez de forçar uma largura fixa de página. Esta folha de estilos usa flexbox, que você conheceu nos fundamentos de CSS. display: flex coloca os cartões em uma linha; flex-wrap: wrap permite uma nova linha; gap deixa espaço entre os cartões. flex: 1 1 260px permite que um figure cresça, encolha e comece com uma largura preferida de 260 pixels. min-width: 0 permite que um conteúdo longo encolha dentro de um cartão em vez de alargar a página.

A imagem do cartão usa width: 100% e aspect-ratio: 4 / 3 para um enquadramento uniforme. object-fit: cover preenche esse quadro de miniatura recortando as bordas exibidas; não muda o arquivo. Em contraste, a imagem detalhada usa object-fit: contain, max-width e max-height para manter a foto inteira visível em suas próprias proporções. vw e vh são centésimos da largura e da altura da área de visualização. min() limita um tamanho, enquanto clamp() permite que o tamanho do título cresça entre valores mínimo e máximo. overflow-wrap: anywhere permite que um título muito longo quebre linha em vez de alargar a página.

:focus-visible seleciona um controle quando o foco do teclado deve ser mostrado. Um contorno de alto contraste e outline-offset tornam o botão atual visível sem deslocar o layout. Não remova o contorno do navegador sem oferecer outro indicador claro. Este projeto aceita um contorno de foco distinto de pelo menos 2 pixels, uma borda visível distinta de pelo menos 2 pixels ou uma sombra visível que apareça quando o controle recebe foco pelo teclado. Você pode mudar as cores ou o layout mantendo esses comportamentos observáveis.

As regras inferiores preservam o visualizador preparado: ::backdrop estiliza o fundo atrás de um diálogo modal, :disabled distingue visualmente um botão de limite e [hidden] deve realmente ocultar uma imagem com falha, embora as imagens normais do visualizador usem display: block. A consulta de mídia de movimento reduzido respeita a preferência de movimento do navegador; esta exposição não tem transições animadas.

Substitua todo o conteúdo de styles.css pela folha de estilos completa a seguir e salve:

* { 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; } }

Mude para Web 8080 e atualize Current exhibition. Em uma área de visualização larga, os cartões compartilham uma linha; em uma estreita, ficam empilhados em uma coluna legível. Redimensione o navegador ou o painel do site para cerca de 390 pixels de largura e depois amplie novamente. Não deve haver rolagem horizontal da página, títulos cortados ou cartão ausente. A fotografia real maior deve manter sua forma vertical ou quadrada quando aberta.

A partir da página, pressione Tab repetidamente. Os links do site e os três botões de fotografia devem mostrar um contorno de foco visível. Enter abre a foto em foco; Esc volta ao cartão. Nenhuma descrição deve exigir passar o mouse por cima. Abra Image credits para confirmar que sua página real lista as fontes das imagens de NASA e escolha Current exhibition para voltar. Esses são links do site, diferentes da troca entre WebIDE e Web 8080.

Se o layout continuar simples, confirme que o nome do arquivo salvo é styles.css e que index.html ainda aponta para ele. Se um cartão estiver mais largo que a página, confira min-width: 0, width: 100% na imagem e as regras de quebra de linha flex. Restaure a folha de estilos completa se necessário; mantenha todas as fotografias em vez de esconder uma difícil. Execute a verificação final após revisar os dois tamanhos de tela e o percurso com teclado.

A galeria Moonroom concluída com três fotografias identificadas de forma independente

Nesta visualização final de tela larga, compare as legendas do primeiro e do último Earthrise: as fotografias são de missões diferentes. A pegada continua sendo um cartão central separado. Os mesmos três cartões se empilham em uma tela estreita.

Resumo

Você conectou uma imagem real à sua descrição, ampliou um cartão semântico para uma coleção completa gerada a partir de dados e manteve duas fotografias de mesmo título distintas por meio de IDs estáveis. Sua galeria responsiva preserva todos os cartões, o conteúdo real das imagens e o foco do teclado visível. No próximo laboratório, você construirá o visualizador detalhado, incluindo limites de navegação e uma falha de imagem recuperável.