【问题标题】:How to keep focus within modal dialog?如何在模态对话框中保持焦点?
【发布时间】:2017-06-09 07:42:28
【问题描述】:

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

我找到了一些工作示例,如下所示:

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

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

role="dialog"

aria-labelledby="modal-title"

aria-modal="true"

但他们没有解决我的问题。你知道有什么方法可以让我的模态保持焦点并且只有在用户点击取消/确认按钮时才会失去它?

【问题讨论】:

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


【解决方案1】:

目前没有简单的方法来实现这一点。提议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 components 和 shadow DOM 的兴起,这个解决方案可以no longer find all focusable elements

如果您始终控制对话框中的所有元素(并且您没有创建通用对话框库),那么最简单的方法可能是为第一个和最后一个可聚焦元素的 keydown 添加事件侦听器,检查如果使用了 tab 或 shift tab,则聚焦第一个或最后一个元素以捕获焦点。

如果您正在创建一个通用对话框库,我发现唯一可以正常工作的方法是使用惰性 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;
    }
  }
}

【讨论】:

    【解决方案2】:

    使用屏幕阅读器时,不同的“工作示例”无法按预期工作。

    它们不会将屏幕阅读器的视觉焦点困在模态框内。

    为此,您必须:

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

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

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

    【讨论】:

    • @Pants 喜欢一个 aria-hidden 属性、一个 jQuery 调用 ($(".disabled :focusable").attr("tabindex", -1);)、一个 css 类 (pointer-events: none)。
    【解决方案3】:

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

    【讨论】:

    • 这个插件帮助我将注意力集中在我的 bootstrap 4 modal 上。
    【解决方案4】:

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

    1. 关注对话框

    将 -1 的 tabindex 添加到主容器,它是具有 role="dialog" 的 DOM 元素。将焦点设置到容器。

    1. 包装制表键

    我发现没有其他方法可以做到这一点,除了在对话框中获取可选项卡元素并在 keydown 上监听它。当我知道焦点中的元素(document.activeElement)是列表中的最后一个时,我将其换行

    【讨论】:

      【解决方案5】:

      不要使用任何要求您查找“可选项卡”元素的解决方案。相反,在有效的庄园中使用keydownclick 事件或背景

      (Angular1)

      https://stackoverflow.com/a/31292097/1754995 上查看 Asheesh Kumar 的回答,了解与我在下面的内容类似的内容。

      (Angular2-x,好久没做Angular1了)

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

      您需要做的是有一个带有preventDefault() 的通用window.keydown 事件,以在显示背景/模态组件时停止所有选项卡。我建议你把它放在 BackdropComponent 上。然后你需要一个带有stopPropagation()keydown.tab 事件来处理ModalComponent 的制表符。 window.keydownkeydown.tab 都可能在 ModalComponent 中,但 BackdropComponent 中的用途不仅仅是模态框。

      这应该防止单击和切换到 AppComponent 输入,并且仅在显示模式时单击或切换到 ModalComponent 输入[和浏览器的东西]。

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

      背景组件:

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

      【讨论】:

        【解决方案6】:

        这是我的解决方案。它根据需要在模态对话框的第一个/最后一个元素上捕获 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;
        });
        

        【讨论】:

        • 这很好,但可以通过删除内部检查来改进,以查看选项卡式元素是否是模态的子元素,而是通过侦听模态本身的 keydown 事件:$('#modal[role=dialog]').on('keydown', e =&gt; { // dosomething });
        【解决方案7】:

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

        npm i 焦点陷阱

        【讨论】:

        • 对于代码块,使用代码格式化程序而不是标题格式化程序。只是一个提示,因为我不确定这是否是一段代码。
        • @ArdentCoder 我想这是错误的。 i-i 完全不同。这个答案也是多余的,因为this 答案已经注意到了。
        【解决方案8】:

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

        第一步:在对话框组件上添加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 事件)。

        编码愉快.. :)

        【讨论】:

        • 在阅读了各种令人费解的例子之后,这个答案让我印象深刻。谢谢。
        • @ACJ 很高兴听到......干杯:)
        【解决方案9】:

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

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

        【讨论】:

          【解决方案10】:

          我使用了 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();
              }
          
            }
          
          }
          

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

          【讨论】:

            【解决方案11】:

            我已经成功使用了 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 等。

            运行应用程序,选项卡现在将包含在装饰的父元素中。

            【讨论】:

              【解决方案12】:

              对于 jquery 用户:

              1. 将 role="dialog" 分配给您的模态
              2. 在对话框模式中查找第一个和最后一个交互元素。
              3. 检查当前目标是否是其中之一(取决于 shift 键是 按下与否)。
              4. 如果目标元素是第一个或最后一个交互元素之一 对话框,返回 false

              工作代码示例:

              //on keydown inside dialog
              $('.modal[role=dialog]').on('keydown', e => {
                  let target = e.target;
                  let shiftPressed = e.shiftKey;
                  // If TAB is pressed
                  if (e.keyCode === 9) {
              
                      // Find first and last element in the ,modal-dialog parent.
                      // Elements must be interactive i.e. visible, and can be Input/Select/Button/Textarea.
              
                      let first =  $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first();
                      let last =  $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
                          let borderElem = shiftPressed ? first : last //border element on the basis of shift key pressed
                          if ($(borderElem).length) {
                              return !$(target).is($(borderElem)); //if target is border element , return false
                          }
                  }
                  return true;
              });
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2010-10-29
                • 2012-04-12
                • 1970-01-01
                • 1970-01-01
                • 2016-03-06
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多