Le site de mon émission CPU me demande parfois bien du temps pour résoudre des problèmes. Des problèmes d'interactions, des problèmes d'aspects ou d'accessibilité. Et pas uniquement parce que je fais ça sur mon temps de loisir, mais aussi parce qu'il vient après l'objectif de publication hebdo de mon émission, laquelle émission demande énormément de temps de préparation. Alors certes, je pourrais d'autant plus déléguer que je publie le source du thème Dotclear modifié, mais je fais aussi mes sites web pour acquérir de l'expérience, voire pour expérimenter.

Le problème dont je vais vous parler aujourd'hui, j'étais persuadé que j'allais le résoudre avec des déclarations HTML et du stylage CSS. Cela me semblait évident.

Et en fait, non.

Vous êtes ici

Dans le site de CPU, comme pour la plupart des sites à contenus, comporte 3 grandes régions fonctionnelles :

  • Il y a un layout de présentation générale, qui se veut minimaliste ;
  • une partie centrale de contenu, qui est l'intérêt de votre passage sur mon site, normalement ;) ;
  • et une partie secondaire de navigation censée permettre au visiteur de farfouiller dans le site si icelui l'intéresse (on peut toujours rêver).
Les 3 zones, dans une résolution confortable de bureau.

Cette partie secondaire se présente en sidebar si la largeur du navigateur le permet, sinon, elle est tout en bas de page, parce que je suis un fanatique du liquid responsive web-design. Peut-être que cette partie de navigation secondaire est trop présente et qu'il faille alléger son contenu, mais le débat n'est pas là.

La partie layout comporte un entête du site, un bandeau horizontal dont la hauteur est limité au logo de l'émission, et qui reste fixe en scrollant (merci display : sticky). Et comporte aussi une ligne de navigation rapide en 5 segments qui chapeaute le contenu et la navigation secondaire, laquelle disparait normalement en défilant.

Le header, la navigation rapide, et un exemple de contenu précédé de sa navigation contextuelle

La problématique

Ce qui m'intéresse, c'est mon lien Chercher, qui est un lien interne à la page et doit renvoyer sur le champ de recherche texte, lequel est situé dans la navigation secondaire. Et ce que je veux, c'est qu'en cliquant dessus, on voit le champ de recherche et qu'il aie le focus, pour qu'on n'aie plus qu'à rentrer des mots clés.

Je veux ça : Le champ de recherche visible, avec le focus.

Seulement, quand on cible un élément avec une ancre dans un lien, suivre celui-ci est censé faire ne sorte que l'élément entre dans la partie visible du viewport, et autant que possible, les navigateurs le mettront en haut à gauche. Et c'est pas le cas à cause de notre entête sticky qui cache le champ.

Du coup, on ne voit pas le champ texte. C'est ballot.

Note pour les experts : Cela aurait mérité de regarder comment se comporte la méthode Element.ScrollIntoView() et la toute nouvelle API IntersectionObserver.

Le laboratoire de Dexter

Donc, je me suis dit, tentons en appliquant une marge sur l'élément, pour qu'il sorte de sous le header.

#q { […] margin-top : 100px;; }

Sauf que : 1, c'est moche, le bouton OK se déforme, et 2, c'est inutile car le navigateur place l'élément sur le coin haut-gauche de son bord. excluant la marge.

du coup, le navigateur fera en sorte que le border soit collé en haut à gauche, mettant margin hors limites.
Donc, je me suis dit, appliquons les translations… pour voir…
#q { […] transform : translateY(200px); }

Ha ben oui, j'ai vu : ça fait effectivement le job, mais il faut alors appliquer tout un bazar pour décaler l'ensemble des autres éléments. On commence à entrer dans des crâderies qui feraient dégainer l'Inspecteur Dirty Hacky. Et il aurait raison tellement que ça pue.
Et puis, même si j'y serais arrivé, il faudrait toute la patience d'un vrai spécialiste CSS pour tout me remettre d'équerre. Donc, FBI : Fausse Bonne Idée.

Il existe une autre méthode, celle d'utiliser le pseudo-element :before, et du coup de lui appliquer une translation. Alors oui, cela marche, mais là aussi, j'ai un problème : le code n'a rien d'élégant à mes yeux.

Il y a un ensemble de propriétés CSS, scroll-snap points, qui semblaient intéressante, mais absolument pas pour ce que je voulais. L'idée est plus de faire une sorte de pagination et toucher le nirvâna du smooth-scrolling natif, ou, comme dans la démo du lien, faire des diapositives pour une présentation.
Note pour les experts, encore : le standard est encore en état de brouillon mais sa couverture commence à être correcte pour envisager de remplacer Reveal.JS EDIT : Cela a été fait par l'immense Nicolas Hoffmann. Je suis confus.

Le truc

Le truc n'est pas de cibler l'ancre de l'<input>, mais de son propre <label>, lequel a un padding-top confortable. Parce que vous avez été propre, et que vous avez positionné l'attribut for= pour lier le label à son champ, ce qui permet qu'en cliquant sur ce label, le champ prenne le focus clavier.

<form […]>
  <label for="q" id="search">Chercher avec un mot</label>
  <fieldset>
    <input type="search" id="q" name="q" />
    <button type="submit">ok</button>
  </fieldset>
</form>

Le problème, c'est qu'aucun navigateur ne songe à passer le focus au champ <input> quand vous ciblez son <label>, ce qui est un comble en manière d'accessibilité.

En fait, j'ai même remarqué que Firefox ne donnait même pas le focus quand on ciblait directement <input>, et j'en suis resté stupéfait J'ai ouvert un rapport bugzilla où vous trouverez un test-case.

Du coup, j'ai dû écrire un petit snippet javascript de ce qui pourrait bien être un polyfill pour une accessibilité évidente et mobile first.



function focus_on_label_moves_to_input() {
    let focused_on = document.getElementById(location.hash.substr(1));
    if  (focused_on === null) {
        return;
    }
    let focus_for = focused_on.getAttribute('for');
    if (focus_for === null) {
        return;
    }
    let new_target = document.getElementById(focus_for);
    if (new_target === null) {
        return;
    }
    new_target.focus();
}

window.addEventListener('hashchange', focus_on_label_moves_to_input);

Sous le capot

L'événement hashchange a lieu si vous changez la partie après le # dans l'adresse d'une page, donc sur une modification du fragment identifier. J'ai déjà fait joujou avec pour cibler un temps précis

Pour l'événement lancé, inutile de se référer à un objet event, la propriété location.hash vous donnera la partie hash (ou fragment) de votre URL, auquel on soustrait le premier caractère, forcément un # avec un .substr(1).

On va chercher l'élément cible du label, on voit s'il a un for="" avec la méthode .getAttribute('for').

À chaque étape où on frise l'incertitude, on vérifie qu'on a bien un objet et pas rien (null), sinon on return proprement, pour laisser la fonction suivante gérer l'événement. Je vous déconseille fortement de tout cacher derrière un try { } catch (e) { } : une telle structure doit rester exceptionnelle, sinon vous aurez vite d'immonde plats de spaghettis.

À la fin, on crée un événement focus en invoquant simplement la méthode .focus() de l'élément DOM du champ de recherche, parce que construire un trigger d'un focus, j'avais grave la flemme : il était 3h30 passées.

Moralité : ça fait le taf et le code n'est pas affreux.

C'était tout ce que je voulais. C'était si compliqué que ça ?

Bonus : le code est suffisament générique pour servir pour tout champ de formulaire : login, entrée de commentaire, formulaire d'adresse ou fishing de carte-bleue.