Evénément change des éléments select

Analyse et tentative de d'harmonisation du comportement d'un élément select et de l'émission de l'événement change lors de la navigation au clavier sous différents navigateurs web. Où l'on voit que l'action la plus généralement adoptée n'est pas forcément la plus pertinente, surtout quand elle ne suis pas les recommendations W3C...

Problème

Prenons l'exemple d'un élément select simple qui a le focus et sur lequel on écoute l'événement change.

<select id="default">
    <option value="item1">aaa 0</option>
    <option value="item2">bbb 1</option>
    <option value="item3">ccc 2</option>
    <option value="item4">ddd 3</option>
    <option value="item5">eee 4</option>
</select>
Code HTML de l'élément select.
document.getElementById('default').addEventListener('change', function (e) {
    console.log('change action !');
}, false);
Ecoute de l'événement change.
  • Avec Internet Explorer, Chrome et Opera (12.12), si on navigue dans la liste d'options avec les flèches directionnelles (haut, bas, gauche, droite), l'écouteur d'événement change sera appelé à chaque fois.
  • Avec Firefox, l'écouteur n'est pas appelé lors de la navigation au clavier, uniquement si on appuie sur la touche « ENTER ».

Un comportement radicalement différent qui devient vite problématique dans le premier cas si l'on effectue des traitements non négligeables avec retour au serveur lors de l'émission de l'événement. Cette différence est d'ailleurs signalée dans un rapport de bug Firefox, qui semble cependant marqué comme « wontfix » (ne sera pas corrigé). Il nous faut donc essayer d'harmoniser ces comportements et, pour comprendre ce que doit signifier l'événement change, nous pouvons nous retourner vers les recommendations W3C :

The onchange event occurs when a control loses the input focus and its value has been modified since gaining focus. This attribute applies to the following elements: INPUT, SELECT, and TEXTAREA.

[ref-html4-change]

L'événement change devrait donc partir lorsque la valeur de l'élément a été modifié ET qu'il perd le focus. Lorsque l'on navigue au clavier, on change bien la valeur de l'élément mais on ne perd absolument pas le focus.

On doit aussi considéré le fait que, pour un utilisateur, cette navigation clavier permet de parcourir les différentes options, pas forcément de les sélectionner (et donc de déclencher les traitements associés) ; la sélection devrait être marquée par une autre action, volontaire, en l'occurence la touche « ENTER » sous Firefox. C'est donc le comportement par défaut de Firefox qu'il serait plus judicieux de retenir.

Solution

Voici une première solution à laquelle je suis parvenu au boulot :

<select id="test">
    <option value="item1">aaa 0</option>
    <option value="item2">bbb 1</option>
    <option value="item3">ccc 2</option>
    <option value="item4">ddd 3</option>
    <option value="item5">eee 4</option>
</select>
Code HTML de l'élément select.
onSelectChange(document.getElementById('test'), function (e) {
    console.log('change action !');
});
Ecoute normalisée de l'événement change.
/**
 *  Méthode permettant d'harmoniser l'écoute de l'événement change sur les
 *  élément select entre les différents navigateurs web.
 *
 *  La navigation par clavier sur les options d'un élément select n'émettra pas
 *  d'événement change ; seul une action spécifique, la touche ENTER, entrainera
 *  l'émission de cet événement.
 *
 *  @param {HTMLSelectElement} select Elément qu'il faut écouter.
 *  @param {Function} listener Ecouteur de l'événement change
 */
var onSelectChange = function (select, listener)
{
        
    var 
    /**
     *  Flag indiquant une action clavier sur le select.
     *  @type Boolean
     */
    keyAction = false,
    
    /**
     *  Dernier événement change émis.
     *  @type Event
     */
    lastChangeEvent = null,
        
    /**
     *  Ecouteur d'événement change du select. Il ne déclenche l'écouteur
     *  utilisateur que s'il n'a pas été provoqué par une sélection au
     *  clavier.
     *
     *  @param {Event} e DOM Event
     */
    onChange = function (e) {
        if (keyAction) {
            keyAction = false;
            lastChangeEvent = e;
        } else {
            // Si fireEvent à lancer l'événement, il n'y pas de e.
            listener(e || lastChangeEvent);
            lastChangeEvent = null;
        }
    },
        
    /**
     *  Ecouteur d'événement keydown du select. Si la touche ENTER est tapé,
     *  déclenche l'émission de l'événement change.
     *
     *  @param {Event} e DOM Event
     */
    onKeyDown = function (e) {
        var src = e.target,
            keyCode = e.keyCode || e.switch;
        keyAction = true;
        if (keyCode === 13) {
            // Stopper le comportement par défaut sous Opera
            // sinon le change part 2 fois.
            e.stopPropagation();
            e.preventDefault();
            keyAction = false;
            if (lastChangeEvent) {
                if (src.onchange) {
                    src.onchange(lastChangeEvent);
                } else {
                    src.dispatchEvent(lastChangeEvent);
                }
                lastChangeEvent = null;
            }
        }
    },
    
    /**
     *  Ecouteur d'événement blur du select, utile s'il y  a eu une sélection
     *  par clavier non suivie de la touche ENTER. Il force alors l'émission
     *  de l'événement change.
     *
     *  @param {Event} e DOM Event
     */
    onBlur = function (e) {
        var src = e.target;
        if (lastChangeEvent) {
            keyAction = false;
            if (src.onchange) {
                src.onchange(lastChangeEvent);
            } else {
                src.dispatchEvent(lastChangeEvent);
            }
            lastChangeEvent = null;
        }
    };

    // Attache des écouteurs d'événements.
    // Attache particulière sous IE qui ne comprend pas le dispatchEvent.
    if (select.fireEvent) {
        select.onchange = onChange;
    } else {
        select.addEventListener('change', onChange, false);
    }
    select.addEventListener('keydown', onKeyDown, false);
    select.addEventListener('blur', onBlur, false);
};
Méthode d'harmonisation de l'écoute de l'événement change.

Démonstration

Une note finale pour rebondir sur les événements de ces dernières semaines avec l'abandon du moteur de rendu presto par Opera au profit de Webkit : dans le monde de bisounours souhaité par certains, où seul Webkit (soit - au final - Apple et Google) ferait sa loi, nous pourrions avoir un seul comportement de l'émission des événements change d'un élément select, un comportement qui ne serait pas forcément celui souhaité, comme expliqué plus haut, et qui ne faciliterait pas non plus a priori le travail des développeurs...

Ressources et références

RAGGETT, Dave ; LE, Hors ; JACOBS, Ian. Spécification HTML 4.01. W3C, . 18.2.3 Intrinsic events

HICKSON, Ian ; BERJON, Robin ; FAULKNER, Steve, et al.. HTML 5. W3C, . 4.10.7.5 Common event behaviors

HICKSON, Ian ; BERJON, Robin ; FAULKNER, Steve, et al.. HTML 5. W3C, . 4.10.9 The select element