Crie um Visualizador de Fotos com Teclado

JavaScriptBeginner
Pratique Agora

Introdução

Os cartões de fotografias reais de Moonroom já têm legendas legíveis e um layout responsivo. O visualizador detalhado está desconectado. Sua tarefa é fazer cada cartão abrir sua própria fotografia maior, permitir que os visitantes percorram a coleção e retornem exatamente ao cartão que usaram. Você também criará e resolverá um erro real de imagem ausente sem substituí-la por outra.

Este laboratório começa com sua própria galeria completa e fotos locais. Ele não depende de arquivos de um ambiente anterior. Você precisa das relações entre cartões e dados do primeiro laboratório, funções JavaScript, seleção DOM e eventos. Os novos comportamentos de busca em array, diálogo, foco e eventos de imagem são apresentados antes do uso. O autor estima 60 minutos de leitura e prática.

Após abrir ou atualizar uma página do site, espere o carregamento terminar antes de acionar os controles das fotos. Use WebIDE para /home/labex/project/moonroom e Web 8080 para o site. Edite viewer.js; a mudança temporária do caminho large em photos.js serve apenas para o exercício de falha ensinado explicitamente e deve ser restaurada antes da verificação. Preserve os demais arquivos, metadados e imagens originais fornecidos. Os dois registros Earthrise permanecem separados, e todos os cartões, miniaturas e legendas devem continuar utilizáveis. Você pode usar um comportamento JavaScript equivalente; mantenha os IDs de botão fornecidos, os atributos estáveis de ligação das fotos, os controles visíveis Close, Previous e Next e as associações de título e descrição do diálogo nativo. Estes exercícios com teclado não são uma certificação completa de acessibilidade.

Abra a Foto Correta e Volte ao Seu Cartão

Nesta etapa, conecte os cartões existentes a um diálogo modal nativo, incluindo um ponto de retorno confiável para o foco do teclado.

Mude para Web 8080, escolha o link do site Study collection e use Tab, Enter e Esc para abrir e fechar uma fotografia. Depois escolha Current exhibition. Seus três cartões e legendas já estão presentes. Experimente Enter em um cartão: ainda não tem efeito porque seu viewer.js contém apenas um comentário. Esse é o comportamento incompleto esperado, não uma falha do ambiente.

Mude para WebIDE, abra a pasta moonroom no Explorer e leia index.html. O elemento dialog é um painel integrado do navegador. Seu aria-labelledby="photo-title" aponta para o título visível, e aria-describedby="photo-caption" aponta para a explicação atual. showModal() o abre como modal: o restante do conteúdo da página fica inativo enquanto ele está aberto. O navegador trata o comportamento modal básico de Tab e o fechamento com Esc; você não precisa escrever um mecanismo próprio para manter o foco dentro. Não adicione tabindex ao próprio diálogo. O botão Close preparado tem autofocus, então abrir o painel dá foco a uma forma clara de sair.

Abra viewer.js. const guarda referências aos elementos DOM. let é usado para valores que vamos alterar: currentIndex é a posição selecionada do array, começando em zero, e openingButton lembra o botão real que abriu o painel. photos[currentIndex] obtém o registro completo dessa fotografia. showPhoto() atualiza em conjunto seu título, legenda, texto alternativo e imagem grande real.

A galeria tem um único listener de clique. Enter no teclado em um botão nativo também o ativa. event.target é o elemento exato clicado, que pode ser a imagem ou o título dentro de um botão. closest('button[data-photo-id]') procura para fora até o botão de abertura correspondente. if (!button) return ignora cliques que não vieram de um deles. Isso é delegação de eventos: um único listener trata todos os botões de cartões existentes.

findIndex() busca no array e retorna a posição cujo ID corresponde ao dataset.photoId do botão. Seu callback retorna true quando photo.id === button.dataset.photoId; === compara sem converter os tipos dos valores. Se nenhum registro corresponder, findIndex() retorna -1, então a condição de proteção interrompe a operação em vez de abrir a foto errada. Comparamos IDs deliberadamente, não o título repetido Earthrise.

O evento close do diálogo é executado tanto ao fechar com Esc quanto com o botão Close. openingButton.focus() restaura o foco no cartão lembrado. Esse uso de focus() implementa o comportamento de retorno; seu teste manual ainda precisa alcançar e ativar os cartões com o teclado real.

Substitua todo o conteúdo de viewer.js pelo código completo abaixo e salve com Ctrl+S ou 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();
});

Mude para Web 8080, escolha Current exhibition se necessário e atualize a página. Use Tab para chegar a cada cartão e Enter para abri-lo. O primeiro Earthrise deve mostrar a fotografia e a legenda de Apollo 8; o último, as de Apollo 11. Pressione Esc após cada um e observe o foco naquele mesmo cartão. Reabra uma foto e use Tab e depois Enter em Close como uma segunda forma de fechar. Previous e Next só serão conectados na próxima etapa.

Se nenhum painel abrir, volte ao WebIDE e compare o arquivo com o bloco completo. Confira se está salvo como viewer.js, se a linha do script continua em index.html e se os valores data-photo-id ainda correspondem a photos.js. Salve e atualize o site após corrigir um erro de digitação. Se aparecer a cena errada, compare em conjunto large, título e legenda do registro atual; não mude os dados de origem para disfarçar um erro de busca por ID. Execute a verificação quando os três percursos independentes de Enter/Esc funcionarem.

Nesta etapa, atualize a mesma visualização detalhada sem perder o cartão original que a abriu.

Volte ao WebIDE e compare as posições do array, que começam em zero, em photos.js: a primeira é 0, a segunda é 1 e a última é photos.length - 1. length conta os registros. Uma posição visível deve contar a partir de um, então currentIndex + 1 fornece um número legível, como 1 / 3.

A propriedade disabled recebe um booleano. previous.disabled = currentIndex === 0 desativa Previous somente na primeira fotografia; a regra correspondente de Next o desativa somente na última. Esses limites evitam buscar fora do array. Os manipuladores de clique também verificam os limites antes de mudar o índice. currentIndex -= 1 subtrai um e salva o novo índice; += 1 soma um. Cada mudança chama showPhoto() para manter imagem, descrição e posição sincronizadas.

Somente abrir um cartão muda openingButton. Navegar para outra fotografia dentro do diálogo não deve alterá-lo: fechar ainda retorna ao cartão onde a visita começou, mesmo que outra fotografia esteja sendo exibida.

Substitua todo o conteúdo de viewer.js por esta versão completa e salve. Ela preserva abertura e fechamento enquanto adiciona a navegação com limites:

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

Mude para Web 8080 e atualize Current exhibition. Use o teclado para abrir o primeiro Earthrise. Previous está desativado; Tab pula esse botão. Vá até Next com Tab e pressione Enter. Você deve ver a pegada vertical, seu título e legenda e 2 / 3. Continue até o último Earthrise e confirme que sua imagem e legenda de Apollo 11 aparecem enquanto Next está desativado.

Vá até Previous com Tab e volte passando pelas duas imagens. Mantenha Shift pressionado ao usar Tab quando precisar voltar entre os controles disponíveis; isso move o foco do teclado, enquanto ativar Previous muda a fotografia. São ações diferentes. Pressione Esc e confirme que o foco retorna ao primeiro cartão original. Agora abra diretamente o cartão central, navegue para outra foto e feche: o foco deve voltar ao cartão central.

É esperado que um controle de limite esteja desativado na primeira ou na última foto. Um controle que parece ativo, mas não faz nada, não é esperado. Se um título mudar e a foto não, volte ao WebIDE e certifique-se de que cada movimento chama a mesma função showPhoto(). Se Esc sempre voltar ao primeiro cartão, confirme que openingButton é atribuído dentro do listener de abertura do cartão e não é sobrescrito nos manipuladores de navegação. Restaure o código completo se necessário, salve e atualize antes de verificar.

Informe a Imagem Ausente e Permita a Recuperação

Nesta etapa, trate uma falha real de carregamento de imagem mantendo a posição do visitante e a utilidade do restante da coleção.

O evento load de uma imagem significa que seu arquivo carregou com sucesso; error significa que não foi possível carregá-la, por exemplo, por um caminho ausente. São eventos de imagem do navegador, não uma prova de que qualquer resposta HTTP contenha uma imagem válida. Uma falha de conexão com o servidor pode tornar toda a página indisponível; aqui, a página funciona enquanto uma solicitação de imagem falha.

No WebIDE, abra viewer.js. Esta versão define os manipuladores de eventos antes de atribuir src, para que uma imagem rápida em cache não termine antes de seus manipuladores existirem. Ela oculta a foto anterior durante o carregamento e mostra Loading photo... no parágrafo existente com role="status". Em caso de sucesso, mostra a imagem real e limpa a mensagem. Em caso de erro, mantém a imagem oculta e oferece orientações legíveis de recuperação. A regra CSS dialog img[hidden] { display: none; } é necessária porque a regra comum da imagem do diálogo usa display: block. Caso contrário, essa regra CSS substituiria o efeito de um atributo hidden sozinho.

O manipulador de erro não muda src para uma miniatura nem para outra fotografia. Isso esconderia a falha e mostraria conteúdo que não corresponde ao registro atual. O usuário ainda pode fechar o painel ou navegar para uma fotografia intacta.

Substitua todo o conteúdo de viewer.js pela versão final completa e salve:

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

Agora provoque um erro de caminho controlado nos seus próprios arquivos e recupere-se dele. Abra photos.js no WebIDE. No registro cujo id é footprint, mude somente o valor large de images/footprint.jpg para images/missing-photo.jpg. Não mude a miniatura, o ID, a legenda nem o texto alternativo. Salve o arquivo.

Mude para Web 8080, atualize Current exhibition e use Tab e Enter para abrir A trace of a visitor. Sua miniatura ainda funciona, mas a solicitação da imagem maior agora falha. Espere o texto visível This photo could not be loaded. Close the viewer and check its large image path. O painel não deve mostrar Earthrise nem uma miniatura esticada como substituição. O título e a legenda da pegada permanecem, e os controles Close, Previous e Next continuam disponíveis.

A fotografia maior ausente deixa um erro legível e a navegação funcionando.

O painel com a falha conserva o título e a legenda da pegada, sem imagem substituta. Next continua disponível para que o visitante possa prosseguir.

Use Tab e Enter em Next. A imagem intacta de Apollo 11 deve carregar e a mensagem de erro deve desaparecer. Pressione Esc e verifique se o foco volta ao cartão da pegada que abriu o painel originalmente. Isso recupera a navegação dentro da coleção; não reparou o caminho incorreto do arquivo.

Volte ao WebIDE, restaure o valor exato de large do registro footprint para images/footprint.jpg e salve. Mude para Web 8080 e atualize Current exhibition para que o navegador leia os dados restaurados. Abra a pegada novamente. Sua imagem vertical real deve agora ser decodificada e aparecer sem a mensagem de erro. Atualizar o navegador recarrega os dados da página; não redefine seu trabalho salvo nem recria o ambiente.

O caminho restaurado volta a carregar a fotografia vertical real.

Após restaurar o caminho e atualizar, a pegada vertical ocupa o visualizador sem ser esticada; a mensagem de erro desapareceu.

Por fim, verifique novamente os três cartões, os limites nos dois sentidos e as duas formas de fechar. Estreite o painel do site e confirme que a fotografia inteira e o título quebrado em linhas cabem no diálogo sem rolagem horizontal da página. Todas as miniaturas, metadados, imagens e créditos originais devem permanecer. A verificação final exige restaurar os dados fornecidos e testa o tratamento de falha apenas em uma cópia descartável de seus arquivos; ela nunca apaga uma imagem da sua exposição real.

Resumo

Você usou IDs estáveis para mostrar o conteúdo real de cada fotografia em um diálogo nativo. Adicionou navegação anterior/próxima com limites, preservou o ponto de retorno original para Esc e Close e tratou uma solicitação real de imagem ausente sem substituí-la por outra foto. Sua galeria mantém cartões, legendas e demais fotografias utilizáveis enquanto informa uma falha local de forma honesta.