web-dev-qa-db-fra.com

Comment garder le focus dans le dialogue modal?

Je développe une application avec Angular et Semantic-UI. L'application doit être accessible, cela signifie qu'elle doit être conforme aux WCAG 2.0. Pour atteindre cet objectif, les modaux doivent garder le focus dans la boîte de dialogue et empêcher les utilisateurs de sortir ou de se déplacer avec des "tabulations" entre les éléments de la page qui se trouve sous le modal.

J'ai trouvé quelques exemples de travail, comme le suivant:

Voici mon essai pour créer un modal accessible avec Semantic-UI: https://plnkr.co/edit/HjhkZg

Comme vous pouvez le voir, j'ai utilisé les attributs suivants:

role="dialog"

aria-labelledby="modal-title"

aria-modal="true"

Mais ils ne résolvent pas mon problème. Connaissez-vous un moyen de faire en sorte que ma conservation modale se concentre et de la perdre uniquement lorsque l'utilisateur clique sur les boutons Annuler/Confirmer?

8
smartmouse

Il n'y a actuellement aucun moyen facile d'y parvenir. attribut inerte a été proposé pour essayer de résoudre ce problème en rendant inaccessible tout élément avec l'attribut et tous ses enfants. Cependant, l'adoption a été lente et ce n'est que récemment atterrir dans Chrome Canary derrière un drapea .

Une autre solution proposée est créer une API native qui garderait une trace de la pile modale , rendant essentiellement tout ce qui n'est pas actuellement le haut de la pile inerte. Je ne suis pas sûr de l'état de la proposition, mais il ne semble pas qu'elle sera mise en œuvre de sitôt.

Alors, où en sommes-nous?

Malheureusement sans une bonne solution. Une solution populaire consiste à créer un sélecteur de requête de tous les éléments focalisables connus puis à piéger le focus sur le modal en ajoutant un événement keydown aux dernier et premier éléments du modal. Cependant, avec l'essor des composants Web et du DOM fantôme, cette solution peut ne trouve plus tous les éléments focalisables .

Si vous contrôlez toujours tous les éléments de la boîte de dialogue (et que vous ne créez pas de bibliothèque de boîte de dialogue générique), alors la façon la plus simple de procéder consiste à ajouter un écouteur d'événements pour le keydown sur les premier et dernier éléments focalisables, vérifiez si l'onglet ou onglet Maj a été utilisé, puis concentrez le premier ou le dernier élément pour intercepter le focus.

Si vous créez une bibliothèque de boîtes de dialogue génériques, la seule chose que j'ai trouvée qui fonctionne raisonnablement bien est d'utiliser le polyfill inerte ou de faire en sorte que tout ce qui est en dehors du modal ait un tabindex=-1.

var nonModalNodes;

function openDialog() {    
  var modalNodes = Array.from( document.querySelectorAll('dialog *') );

  // by only finding elements that do not have tabindex="-1" we ensure we don't
  // corrupt the previous state of the element if a modal was already open
  nonModalNodes = document.querySelectorAll('body *:not(dialog):not([tabindex="-1"])');

  for (var i = 0; i < nonModalNodes.length; i++) {
    var node = nonModalNodes[i];

    if (!modalNodes.includes(node)) {

      // save the previous tabindex state so we can restore it on close
      node._prevTabindex = node.getAttribute('tabindex');
      node.setAttribute('tabindex', -1);

      // tabindex=-1 does not prevent the mouse from focusing the node (which
      // would show a focus outline around the element). prevent this by disabling
      // outline styles while the modal is open
      // @see https://www.sitepoint.com/when-do-elements-take-the-focus/
      node.style.outline = 'none';
    }
  }
}

function closeDialog() {

  // close the modal and restore tabindex
  if (this.type === 'modal') {
    document.body.style.overflow = null;

    // restore or remove tabindex from nodes
    for (var i = 0; i < nonModalNodes.length; i++) {
      var node = nonModalNodes[i];
      if (node._prevTabindex) {
        node.setAttribute('tabindex', node._prevTabindex);
        node._prevTabindex = null;
      }
      else {
        node.removeAttribute('tabindex');
      }
      node.style.outline = null;
    }
  }
}
10
Steven Lambert

Les différents "exemples de travail" ne fonctionnent pas comme prévu avec un lecteur d'écran.

Ils ne captent pas le focus visuel du lecteur d'écran à l'intérieur du modal.

Pour que cela fonctionne, vous devez:

  1. Met le aria-hidden attribut sur tous les autres nœuds
  2. désactiver les éléments focalisables du clavier à l'intérieur de ces arbres (liens utilisant tabindex=-1, contrôles utilisant disabled, ...)

    • Le jQuery :focusable le pseudo-sélecteur peut être utile pour trouver des éléments focalisables.
  3. ajoutez un calque transparent sur la page pour désactiver la sélection de la souris.

    • ou vous pouvez utiliser le css pointer-events: none propriété lorsque le navigateur la gère avec des éléments non SVG, pas dans IE
4
Adam

Ce plugin focus-trap est excellent pour s'assurer que le focus reste piégé à l'intérieur des éléments de dialogue.

3
Daniel Tonon

Il semble que votre problème puisse être divisé en 2 catégories:

  1. focus sur la boîte de dialogue

Ajoutez un tabindex de -1 au conteneur principal qui est l'élément DOM qui a role = "dialog". Réglez le focus sur le conteneur.

  1. encapsulant la touche de tabulation

Je n'ai trouvé aucun autre moyen de le faire, sauf en obtenant les éléments tabables dans la boîte de dialogue et en l'écoutant au clavier. Quand je sais que l'élément en focus (document.activeElement) est le dernier de la liste, je le fais boucler

0
Marwan

N'utilisez aucune solution vous obligeant à rechercher des éléments "tabables". À la place, utilisez keydown et soit click événements ou un fond dans un manoir efficace.

(Angular1)

Voir la réponse d'Asheesh Kumar à https://stackoverflow.com/a/31292097/1754995 pour quelque chose de similaire à ce que je cherche ci-dessous.

(Angular2-x, je n'ai pas fait Angular1 depuis un moment)

Supposons que vous ayez 3 composants: BackdropComponent, ModalComponent (a une entrée) et AppComponent (a une entrée, BackdropComponent et ModalComponent). Vous affichez BackdropComponent et ModalComponent avec le z-index correct, les deux sont actuellement affichés/visibles.

Ce que vous devez faire est d'avoir un événement général window.keydown Avec preventDefault() pour arrêter tous les tabulations lorsque le composant backdrop/modal est affiché. Je vous recommande de mettre cela sur un BackdropComponent. Ensuite, vous avez besoin d'un événement keydown.tab Avec stopPropagation() pour gérer la tabulation du ModalComponent. window.keydown Et keydown.tab Pourraient probablement être dans le ModalComponent mais il y a un but dans un BackdropComponent plus que juste des modaux.

Cela devrait empêcher de cliquer et de tabuler sur l'entrée AppComponent et de ne cliquer ou tabuler sur l'entrée ModalComponent [et les éléments du navigateur] que lorsque le modal est affiché.

Si vous ne souhaitez pas utiliser de toile de fond pour éviter de cliquer, vous pouvez utiliser les événements click de la même manière que les événements keydown décrits ci-dessus.

Composant Backdrop:

@Component({
selector: 'my-backdrop',
Host: {
    'tabindex': '-1',
    '(window:keydown)': 'preventTabbing($event)'
},
...
})
export class BackdropComponent {
    ...
    private preventTabbing(event: KeyboardEvent) {
        if (event.keyCode === 9) { // && backdrop shown?
            event.preventDefault();
        }
    }
    ...
}

Composant modal:

@Component({
selector: 'my-modal',
Host: {
    'tabindex': '-1',
    '(keydown.tab)': 'onTab($event)'
},
...
})
export class ModalComponent {
    ...
    private onTab(event: KeyboardEvent) {
        event.stopPropagation();
    }
    ...
}
0
Kody

Voici ma solution. Il piège Tab ou Shift + Tab si nécessaire sur le premier/dernier élément de la boîte de dialogue modale (dans mon cas, trouvé avec role="dialog"). Les éléments éligibles en cours de vérification sont tous les contrôles d'entrée visibles dont le code HTML peut être input,select,textarea,button.

$(document).on('keydown', function(e) {
    var target = e.target;
    var shiftPressed = e.shiftKey;
    // If TAB key pressed
    if (e.keyCode == 9) {
        // If inside a Modal dialog (determined by attribute role="dialog")
        if ($(target).parents('[role=dialog]').length) {                            
            // Find first or last input element in the dialog parent (depending on whether Shift was pressed). 
            // Input elements must be visible, and can be Input/Select/Button/Textarea.
            var borderElem = shiftPressed ?
                                $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first() 
                             :
                                $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
            if ($(borderElem).length) {
                if ($(target).is($(borderElem))) {
                    return false;
                } else {
                    return true;
                }
            }
        }
    }
    return true;
});
0
gene b.

J'ai utilisé l'une des méthodes suggérées par Steven Lambert, à savoir écouter les événements de keydown et intercepter les touches "tab" et "shift + tab". Voici mon exemple de code (Angular 5):

import { Directive, ElementRef, Attribute, HostListener, OnInit } from '@angular/core';

/**
 * This directive allows to override default tab order for page controls.
 * Particularly useful for working around the modal dialog TAB issue
 * (when tab key allows to move focus outside of dialog).
 *
 * Usage: add "custom-taborder" and "tab-next='next_control'"/"tab-prev='prev_control'" attributes
 * to the first and last controls of the dialog.
 *
 * For example, the first control is <input type="text" name="ctlName">
 * and the last one is <button type="submit" name="btnOk">
 *
 * You should modify the above declarations as follows:
 * <input type="text" name="ctlName" custom-taborder tab-prev="btnOk">
 * <button type="submit" name="btnOk" custom-taborder tab-next="ctlName">
 */

@Directive({
  selector: '[custom-taborder]'
})
export class CustomTabOrderDirective {

  private elem: HTMLInputElement;
  private nextElemName: string;
  private prevElemName: string;
  private nextElem: HTMLElement;
  private prevElem: HTMLElement;

  constructor(
    private elemRef: ElementRef
    , @Attribute('tab-next') public tabNext: string
    , @Attribute('tab-prev') public tabPrev: string
  ) {
    this.elem = this.elemRef.nativeElement;
    this.nextElemName = tabNext;
    this.prevElemName = tabPrev;
  }

  ngOnInit() {
    if (this.nextElemName) {
      var elems = document.getElementsByName(this.nextElemName);
      if (elems && elems.length && elems.length > 0)
        this.nextElem = elems[0];
    }

    if (this.prevElemName) {
      var elems = document.getElementsByName(this.prevElemName);
      if (elems && elems.length && elems.length > 0)
        this.prevElem = elems[0];
    }
  }

  @HostListener('keydown', ['$event'])
  onKeyDown(event: KeyboardEvent) {

    if (event.key !== "Tab")
      return;

    if (!event.shiftKey && this.nextElem) {
      this.nextElem.focus();
      event.preventDefault();
    }

    if (event.shiftKey && this.prevElem) {
      this.prevElem.focus();
      event.preventDefault();
    }

  }

}

Pour utiliser cette directive, importez-la simplement dans votre module et ajoutez-la à la section Déclarations.

0
Denys Avilov