如何将焦点保持在模态对话框中?

时间:2017-06-09 07:42:28

标签: modal-dialog accessibility semantic-ui wai-aria wcag2.0

我正在使用AngularSemantic-UI开发应用。应用程序应该是可访问的,这意味着它应该符合WCAG 2.0。 为了达到这个目的,模态应该在对话框中保持焦点,并防止用户外出或移动"标签"位于模态下的页面元素之间。

我找到了一些有用的例子,如下所示:

我尝试使用Semantic-UI创建一个可访问的模式:https://plnkr.co/edit/HjhkZg

如您所见,我使用了以下属性:

role="dialog"

aria-labelledby="modal-title"

aria-modal="true"

但他们没有解决我的问题。你有没有办法让我的模态保持专注,只有当用户点击取消/确认按钮时才会失去它?

11 个答案:

答案 0 :(得分:7)

目前还没有简单的方法来实现这一目标。提出inert attribute试图通过使任何具有该属性的元素及其所有子元素都不可访问来解决此问题。但是,采用速度很慢,而且最近才采用land in Chrome Canary behind a flag

另一个提议的解决方案是making a native API that would keep track of the modal stack,基本上使得当前不是堆栈顶部的所有内容都是惰性的。我不确定提案的状态,但看起来不会很快实施。

那么离开我们的地方呢?

遗憾的是没有一个好的解决方案。一种流行的解决方案是create a query selector of all known focusable elements,然后通过向模态中的最后一个元素和第一个元素添加一个keydown事件来将焦点捕获到模态。但是,随着Web组件和影子DOM的兴起,此解决方案可以no longer find all focusable elements

如果你总是控制对话框中的所有元素(并且你没有创建一个通用的对话框库),那么最简单的方法就是在第一个和最后一个可聚焦元素上为keydown添加一个事件监听器,检查如果使用了制表符或移位制表符,则将第一个或最后一个元素聚焦到陷阱焦点。

如果你正在创建一个通用的对话库,我发现的唯一合理的工作就是使用惰性polyfill或使模态之外的所有东西都有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;
    }
  }
}

答案 1 :(得分:4)

不同的工作实例&#34;使用屏幕阅读器无法正常工作。

它们不会将屏幕阅读器视觉焦点捕获到模态中。

为此,您必须:

  1. 在任何其他节点上设置aria-hidden属性
  2. 禁用这些树中的键盘焦点元素(使用tabindex=-1的链接,使用disabled的控件,...)

  3. 在页面上添加透明图层以禁用鼠标选择。

    • 或者当浏览器使用非SVG元素处理它时,您可以使用css pointer-events: none属性,而不是在IE中

答案 2 :(得分:2)

This focus-trap plugin非常擅长确保焦点停留在对话元素内部。

答案 3 :(得分:1)

我们可以使用焦点陷阱npm软件包。

npm我聚焦陷阱

答案 4 :(得分:1)

这可能对在 Angular 中寻找解决方案的人有所帮助。

第 1 步:在对话框组件上添加 keydown 事件

  @HostListener('document:keydown', ['$event'])
  handleTabKeyWInModel(event: any) {
       this.sharedService.handleTabKeyWInModel(event, '#modal_id', this.elementRef.nativeElement, 'input,button,select,textarea,a,[tabindex]:not([tabindex="-1"])');
  }

这将过滤在模态对话框中预设的元素。

第 2 步:在共享服务中为处理焦点添加通用方法(或者您也可以将其添加到您的组件中) >

handleTabKeyWInModel(e, modelId: string, nativeElement, tagsList: string) {
        if (e.keyCode === 9) {
            const focusable = nativeElement.querySelector(modelId).querySelectorAll(tagsList);
            if (focusable.length) {
               const first = focusable[0];
               const last = focusable[focusable.length - 1];
               const shift = e.shiftKey;
               if (shift) {
                  if (e.target === first) { // shift-tab pressed on first input in dialog
                     last.focus();
                     e.preventDefault();
                  }
                } else {
                    if (e.target === last) { // tab pressed on last input in dialog
                        first.focus();
                        e.preventDefault();
                    }
                }
            }
        }
    }

现在此方法将采用模态对话框原生元素并开始对每个tab键进行评估。最后,我们将过滤第一个和最后一个事件,以便我们可以关注适当的元素(在最后一个元素选项卡点击后的第一个和第一个元素上的最后一个 shift+tab 事件)。

快乐编码.. :)

答案 5 :(得分:1)

"focus" 事件可以在捕获阶段被拦截,因此您可以在 document.body 级别监听它们,在它们到达目标元素之前压制它们,并将焦点重定向回模态对话框中的控件。这个例子假设一个带有输入元素的模态对话框被显示并分配给变量 currDialog:

document.body.addEventListener("focus", (event) => {
    if (currDialog && !currDialog.contains(event.target)) {
        event.preventDefault();
        event.stopPropagation();
        currDialog.querySelector("input").focus();
    }
}, {capture: true});

您可能还希望在占据全屏的固定位置、清晰(或低不透明度)背景元素中包含这样一个对话框,以便捕获和抑制鼠标/指针事件,从而避免浏览器反馈(悬停等)发生,这可能会给用户留下背景处于活动状态的印象。

答案 6 :(得分:0)

请勿使用任何需要您查找“可标记”元素的解决方案。相反,在有效庄园中使用keydownclick个事件或背景

(Angular1)

请参阅Asheesh Kumar在https://stackoverflow.com/a/31292097/1754995的答案,其中的内容与我下面的内容类似。

(Angular2-x,我有一段时间没有完成Angular1)

假设您有3个组件:BackdropComponent,ModalComponent(具有输入)和AppComponent(具有输入,BackdropComponent和ModalComponent)。使用正确的z-index显示BackdropComponent和ModalComponent,两者当前都显示/可见。

当您显示背景/模态组件时,您需要执行window.keydown一般事件preventDefault()来停止所有标签。我建议你把它放在BackdropComponent上。然后,您需要keydown.tab事件stopPropagation()来处理ModalComponent的标签。 window.keydownkeydown.tab都可能在ModalComponent中,但BackdropComponent的目的不仅仅是模态。

这样可以防止点击和跳转到AppComponent输入,只有在显示模态时才点击或选项卡到ModalComponent输入[和浏览器内容]。

如果您不想使用背景来阻止点击,则可以使用类似于上述click事件的keydown事件。

背景组件:

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

模态组件:

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

答案 7 :(得分:0)

我使用了Steven Lambert建议的方法之一,即监听keydown事件并拦截“tab”和“shift + tab”键。这是我的示例代码(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();
    }

  }

}

要使用此指令,只需将其导入模块并添加到声明部分。

答案 8 :(得分:0)

听起来您的问题可分为两类:

  1. 专注于对话框
  2. 在主容器中添加-1的tabindex,主容器是具有role =“dialog”的DOM元素。将焦点设置为容器。

    1. 包装标签键
    2. 除了通过获取对话框中的tabbable元素并在keydown上监听它之外,我没有找到其他方法。当我知道焦点中的元素(document.activeElement)是列表中的最后一个元素时,我将其包装

答案 9 :(得分:0)

这是我的解决方案。它会根据需要在模式对话框的第一个/最后一个元素上捕获Tab或Shift + Tab(在我的情况下为role="dialog")。被检查的合格元素是所有可见的输入控件,它们的HTML可能是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;
});

答案 10 :(得分:0)

我已经成功使用了Angular Material的A11yModule。

使用您喜欢的软件包管理器将它们安装到Angular应用程序中。

**"@angular/material": "^10.1.2"**

**"@angular/cdk": "^10.1.2"**

在导入Angular Material模块的Angular模块中,添加以下内容:

**import {A11yModule} from '@angular/cdk/a11y';**

在组件HTML中,将 cdkTrapFocus 指令应用于任何父元素,例如div,form等。

运行应用程序,现在将在装饰后的父元素中包含制表符。