Créer une Visionneuse Photo au Clavier

JavaScriptBeginner
Pratiquer maintenant

Introduction

Les cartes de véritables photographies de Moonroom ont désormais des légendes lisibles et une mise en page adaptative. La visionneuse détaillée est déconnectée. Votre tâche consiste à ouvrir la grande photographie propre à chaque carte, à permettre aux visiteurs de parcourir la collection et à les ramener exactement à la carte utilisée. Vous allez aussi provoquer un véritable échec dû à une image manquante, puis le résoudre sans lui substituer une autre image.

Ce laboratoire démarre avec sa propre galerie complète et ses photos locales. Il ne dépend pas de fichiers d'un environnement précédent. Vous devez connaître les relations entre cartes et données du premier laboratoire, les fonctions JavaScript, la sélection DOM et les événements. Les nouveaux comportements de recherche dans un tableau, de dialogue, de focus et d'événements d'image sont présentés avant utilisation. L'auteur estime la lecture et la pratique à 60 minutes.

Après avoir ouvert ou actualisé une page du site, attendez la fin de son chargement avant d'activer les commandes des photos. Utilisez WebIDE pour /home/labex/project/moonroom et Web 8080 pour le site. Modifiez viewer.js ; le changement temporaire du chemin large dans photos.js est réservé à l'exercice d'échec explicitement enseigné et doit être annulé avant la vérification. Conservez les autres fichiers, métadonnées et images originales fournis. Les deux enregistrements Earthrise restent distincts, et chaque carte, miniature et légende doit rester utilisable. Vous pouvez adopter un comportement JavaScript équivalent ; conservez les identifiants de bouton fournis, les attributs stables de liaison des photos, les commandes visibles Close, Previous et Next, ainsi que les associations du titre et de la description du dialogue natif. Ces exercices au clavier ne constituent pas une certification complète d'accessibilité.

Ouvrir la Bonne Photo et Revenir à Sa Carte

Dans cette étape, reliez les cartes existantes à un dialogue modal natif, avec un point de retour fiable pour le focus du clavier.

Passez à Web 8080, choisissez le lien du site Study collection et utilisez Tab, Enter et Esc pour ouvrir et fermer une photographie. Choisissez ensuite Current exhibition. Ses trois cartes et légendes sont déjà présentes. Essayez Enter sur une carte : rien ne se passe encore, car votre viewer.js ne contient qu'un commentaire. C'est le comportement incomplet attendu, pas une panne de l'environnement.

Passez à WebIDE, ouvrez le dossier moonroom dans Explorer et lisez index.html. L'élément dialog est un panneau intégré au navigateur. Son aria-labelledby="photo-title" pointe vers le titre visible, et aria-describedby="photo-caption" vers l'explication courante. showModal() l'ouvre comme un dialogue modal : les autres contenus de la page deviennent inactifs pendant son ouverture. Le navigateur gère le comportement modal de base de Tab et la fermeture par Esc ; vous n'avez pas à écrire un mécanisme personnalisé pour retenir le focus. N'ajoutez pas de tabindex au dialogue lui-même. Le bouton Close préparé possède autofocus, de sorte que l'ouverture du panneau donne le focus à un moyen clair de le quitter.

Ouvrez viewer.js. const conserve les références aux éléments DOM. let est utilisé pour les valeurs qui changeront : currentIndex est la position sélectionnée dans le tableau, à partir de zéro, et openingButton mémorise le véritable bouton ayant ouvert le panneau. photos[currentIndex] récupère l'enregistrement complet de cette photographie. showPhoto() met à jour ensemble son titre, sa légende, son texte alternatif et sa véritable grande image.

La galerie possède un seul écouteur de clic. Enter au clavier sur un bouton natif l'active également. event.target est l'élément exact sur lequel le clic a eu lieu, qui peut être l'image ou le titre dans le bouton. closest('button[data-photo-id]') remonte vers le bouton d'ouverture correspondant. if (!button) return ignore les clics qui ne viennent pas d'un tel bouton. C'est la délégation d'événements : un seul écouteur gère tous les boutons de carte existants.

findIndex() recherche dans le tableau et renvoie la position dont l'identifiant correspond au dataset.photoId du bouton. Sa fonction de rappel renvoie true lorsque photo.id === button.dataset.photoId ; === compare sans convertir les types des valeurs. Si aucun enregistrement ne correspond, findIndex() renvoie -1, et la condition de protection interrompt l'opération plutôt que d'ouvrir une mauvaise photo. Nous comparons volontairement les identifiants, pas le titre répété Earthrise.

L'événement close du dialogue s'exécute qu'il ait été fermé par Esc ou par le bouton Close. openingButton.focus() restitue le focus à la carte mémorisée. Cette utilisation de focus() implémente le retour ; votre test manuel doit néanmoins atteindre et activer les cartes au véritable clavier.

Remplacez tout le contenu de viewer.js par le code complet ci-dessous, puis enregistrez avec 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();
});

Passez à Web 8080, choisissez Current exhibition si nécessaire et actualisez la page. Utilisez Tab pour atteindre chaque carte, puis Enter pour l'ouvrir. Le premier Earthrise doit montrer la photographie et la légende d'Apollo 8 ; le dernier, celles d'Apollo 11. Appuyez sur Esc après chaque ouverture et observez le focus sur cette même carte. Rouvrez une photo et utilisez Tab puis Enter sur Close pour tester une seconde fermeture. Previous et Next ne seront connectés qu'à l'étape suivante.

Si aucun panneau ne s'ouvre, revenez à WebIDE et comparez le fichier au bloc complet. Vérifiez qu'il est enregistré sous le nom viewer.js, que la ligne de script demeure dans index.html et que les valeurs de data-photo-id correspondent toujours à photos.js. Enregistrez et actualisez le site après avoir corrigé une faute de frappe. Si la mauvaise scène apparaît, comparez ensemble le champ large, le titre et la légende de l'enregistrement courant ; ne modifiez pas les données source pour dissimuler une erreur de recherche par identifiant. Lancez la vérification lorsque les trois parcours indépendants Enter/Esc fonctionnent.

Parcourir Toute la Collection et Ses Limites

Dans cette étape, mettez à jour la même vue détaillée sans perdre la carte qui l'a ouverte à l'origine.

Revenez à WebIDE et comparez les positions du tableau indexées à partir de zéro dans photos.js : la première vaut 0, la deuxième 1 et la dernière photos.length - 1. length compte les enregistrements. Une position visible doit au contraire commencer à un ; currentIndex + 1 fournit donc un nombre lisible comme 1 / 3.

La propriété disabled prend un booléen. previous.disabled = currentIndex === 0 désactive Previous uniquement sur la première photographie ; la règle correspondante pour Next le désactive uniquement sur la dernière. Ces limites évitent une recherche hors du tableau. Les gestionnaires de clic vérifient aussi les limites avant de changer l'indice. currentIndex -= 1 soustrait un et enregistre le nouvel indice ; += 1 ajoute un. Chaque changement appelle showPhoto() afin que l'image, la description et la position restent synchronisées.

Seule l'ouverture d'une carte modifie openingButton. Naviguer vers une autre photo dans le dialogue ne doit pas le modifier : la fermeture ramène toujours à la carte où la visite a commencé, même si une autre photographie est actuellement affichée.

Remplacez tout le contenu de viewer.js par cette version complète et enregistrez-la. Elle conserve l'ouverture et la fermeture tout en ajoutant la navigation avec 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();
});

Passez à Web 8080 et actualisez Current exhibition. Ouvrez le premier Earthrise au clavier. Previous est désactivé ; Tab ignore ce bouton. Atteignez Next avec Tab et appuyez sur Enter. Vous devez voir l'empreinte au format portrait, son titre, sa légende et 2 / 3. Continuez jusqu'au dernier Earthrise et confirmez que son image et sa légende Apollo 11 apparaissent tandis que Next est désactivé.

Atteignez Previous avec Tab et revenez à travers les deux images. Maintenez Shift en appuyant sur Tab pour reculer parmi les commandes disponibles ; cela déplace le focus du clavier, tandis qu'activer Previous change la photographie. Ce sont deux actions différentes. Appuyez sur Esc et vérifiez que le focus revient à la première carte d'origine. Ouvrez maintenant directement la carte centrale, naviguez ailleurs et fermez : le focus doit revenir à la carte centrale.

La désactivation d'une commande à la première ou à la dernière photo est normale. Une commande qui semble active mais ne fait rien ne l'est pas. Si le titre change mais pas la photo, revenez à WebIDE et vérifiez que chaque déplacement appelle la même fonction showPhoto(). Si Esc revient toujours à la première carte, vérifiez que openingButton est affecté dans l'écouteur d'ouverture de carte et n'est pas écrasé dans les gestionnaires de navigation. Restaurez le code complet si nécessaire, enregistrez et actualisez avant de vérifier.

Signaler une Image Manquante et Permettre la Reprise

Dans cette étape, gérez un véritable échec de chargement d'image tout en conservant la position du visiteur et l'utilisation du reste de la collection.

L'événement load d'une image signifie que son fichier a été chargé avec succès ; error signifie que l'image n'a pas pu être chargée, par exemple à cause d'un chemin absent. Ce sont des événements d'image du navigateur, pas la preuve qu'une réponse HTTP quelconque contient une image valide. Une panne de connexion au serveur peut rendre toute la page indisponible ; ici, la page fonctionne tandis qu'une seule requête d'image échoue.

Dans WebIDE, ouvrez viewer.js. Cette version définit les gestionnaires d'événements avant d'affecter src, pour qu'une image rapide en cache ne termine pas son chargement avant l'existence de ses gestionnaires. Elle masque l'ancienne image pendant le chargement et affiche Loading photo... dans le paragraphe existant avec role="status". En cas de succès, elle montre la véritable image et efface le message. En cas d'échec, elle garde l'image masquée et fournit des indications de reprise lisibles. La règle CSS dialog img[hidden] { display: none; } est nécessaire, car la règle ordinaire des images du dialogue utilise display: block. Sinon, un attribut hidden seul serait supplanté par cette règle CSS.

Le gestionnaire d'erreur ne remplace pas src par une miniature ou une autre photographie. Cela masquerait l'échec et afficherait un contenu ne correspondant pas à l'enregistrement courant. L'utilisateur peut toujours fermer le panneau ou naviguer vers une photographie intacte.

Remplacez tout le contenu de viewer.js par la version finale complète et enregistrez :

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

Provoquez maintenant une erreur de chemin contrôlée dans vos propres fichiers, puis corrigez-la. Ouvrez photos.js dans WebIDE. Dans l'enregistrement dont l'id est footprint, modifiez uniquement la valeur large, de images/footprint.jpg en images/missing-photo.jpg. Ne changez pas sa miniature, son identifiant, sa légende ou son texte alternatif. Enregistrez le fichier.

Passez à Web 8080, actualisez Current exhibition, puis ouvrez A trace of a visitor avec Tab et Enter. Sa miniature fonctionne toujours, mais la requête de grande image échoue désormais. Le texte visible attendu est This photo could not be loaded. Close the viewer and check its large image path. Le panneau ne doit pas afficher Earthrise ou une miniature étirée en remplacement. Le titre et la légende de l'empreinte restent présents, et les commandes Close, Previous et Next restent disponibles.

La grande photographie manquante laisse une erreur lisible et une navigation fonctionnelle.

Le panneau en échec conserve le titre et la légende de l'empreinte, sans image de remplacement. Next reste disponible pour permettre au visiteur de continuer.

Utilisez Tab et Enter sur Next. L'image intacte d'Apollo 11 doit se charger et le message d'erreur doit disparaître. Appuyez sur Esc et vérifiez que le focus revient à la carte de l'empreinte qui a ouvert le panneau. Il s'agit d'une reprise de la consultation dans la collection ; cela n'a pas réparé le mauvais chemin du fichier.

Revenez à WebIDE, rétablissez la valeur exacte de large de l'enregistrement footprint à images/footprint.jpg, puis enregistrez. Passez à Web 8080 et actualisez Current exhibition pour que le navigateur lise les données restaurées. Ouvrez de nouveau l'empreinte. Sa véritable image au format portrait doit maintenant être décodée et apparaître sans message d'erreur. Actualiser le navigateur recharge les données de la page ; cela ne réinitialise pas votre travail enregistré et ne recrée pas l'environnement.

Le chemin restauré charge de nouveau la véritable photographie au format portrait.

Après restauration du chemin et actualisation, l'empreinte au format portrait occupe la visionneuse sans être étirée ; le message d'erreur a disparu.

Enfin, vérifiez à nouveau les trois cartes, les limites dans les deux sens et les deux façons de fermer. Réduisez la largeur du panneau du site et confirmez que la photographie entière et le titre sur plusieurs lignes tiennent dans le dialogue sans défilement horizontal de la page. Toutes les miniatures, métadonnées, images et mentions de crédit originales doivent rester présentes. La vérification finale exige que les données fournies soient restaurées et teste le traitement de l'échec uniquement dans une copie jetable de vos fichiers ; elle ne supprime jamais une image de votre véritable exposition.

Résumé

Vous avez utilisé des identifiants stables pour montrer le contenu réel de chaque photographie dans un dialogue natif. Vous avez ajouté une navigation précédente/suivante avec limites, conservé le point de retour initial pour Esc et Close et traité une véritable requête d'image manquante sans lui substituer une autre photo. Votre galerie garde sa grille, ses légendes et ses autres photographies utilisables tout en signalant honnêtement un échec local.