如何通过键盘+屏幕阅读器访问我的手风琴?

时间:2018-01-08 19:31:17

标签: javascript jquery html accessibility accordion

我一直在尝试通过键盘访问和屏幕阅读器访问来使我目前的可扩展手风琴符合W3C Web内容辅助功能指南的AA级。

我对JavaScript / jQuery并不是很熟悉所以到目前为止我一直在进行大量的猜测和检查。

我完成了以下工作:

  1. 使用Tab键
  2. 的制表符索引顺序
  3. 使用上/下或左/右键盘键来回移动的能力
  4. 使用Enter或空格键扩展/折叠手风琴的能力
  5. 但显然我错过了以下内容:

    1. 无法使用“Shift + Tab”向后导航。
    2. 当焦点移动不正确时无法折叠展开的切换,并且使用shift + tab不会将焦点重新带回展开的切换。
    3. 焦点不会移动到切换下的链接。
    4. 标签分组不存在,屏幕阅读器不读取标签1的3,标签2的3,等等。
    5. 以下是我一直在使用的CodePen:https://codepen.io/kwhytock/pen/Ozzopr 我包含了所有jQuery UI代码,但是以Accordion为中心的代码从第2516行开始。

      $(function() {
        $("#accordion:nth-child(1n)").accordion({
          collapsible: true
        });
        $("#accordion:nth-child(1n)").accordion({
          active: false
        });
      });
      
        var widgetsAccordion = $.widget("ui.accordion", {
          version: "1.12.1",
          options: {
            active: 0,
            animate: {},
            classes: {
              "ui-accordion-header": "ui-corner-top",
              "ui-accordion-header-collapsed": "ui-corner-all",
              "ui-accordion-content": "ui-corner-bottom"
            },
            collapsible: false,
            event: "click",
            header: ".accordionTitle",
            heightStyle: "auto",
      
            // Callbacks
            activate: null,
            beforeActivate: null
          },
      
          hideProps: {
            borderTopWidth: "hide",
            borderBottomWidth: "hide",
            paddingTop: "hide",
            paddingBottom: "hide",
            height: "hide"
          },
      
          showProps: {
            borderTopWidth: "show",
            borderBottomWidth: "show",
            paddingTop: "show",
            paddingBottom: "show",
            height: "show"
          },
      
          _create: function() {
            var options = this.options;
      
            this.prevShow = this.prevHide = $();
            this._addClass("ui-accordion", "ui-widget ui-helper-reset");
            this.element.attr("role", "tablist");
      
            // Don't allow collapsible: false and active: false / null
            if (!options.collapsible && (options.active === false || options.active == null)) {
              options.active = 0;
            }
      
            this._processPanels();
      
            // handle negative values
            if (options.active < 0) {
              options.active += this.headers.length;
            }
            this._refresh();
          },
      
          _getCreateEventData: function() {
            return {
              header: this.active,
              panel: !this.active.length ? $() : this.active.next()
            };
          },
      
          _createIcons: function() {
            var icon, children,
              icons = this.options.icons;
      
            if (icons) {
              icon = $("<span>");
              this._addClass(icon, "ui-accordion-header-icon", "ui-icon " + icons.header);
              icon.prependTo(this.headers);
              children = this.active.children(".ui-accordion-header-icon");
              this._removeClass(children, icons.header)
                ._addClass(children, null, icons.activeHeader)
                ._addClass(this.headers, "ui-accordion-icons");
            }
          },
      
          _destroyIcons: function() {
            this._removeClass(this.headers, "ui-accordion-icons");
            this.headers.children(".ui-accordion-header-icon").remove();
          },
      
          _destroy: function() {
            var contents;
      
            // Clean up main element
            this.element.removeAttr("role");
      
            // Clean up headers
            this.headers
              .removeAttr("role aria-expanded aria-selected aria-controls tabIndex")
              .removeUniqueId();
      
            this._destroyIcons();
      
            // Clean up content panels
            contents = this.headers.next()
              .css("display", "")
              .removeAttr("role aria-hidden aria-labelledby")
              .removeUniqueId();
      
            if (this.options.heightStyle !== "content") {
              contents.css("height", "");
            }
          },
      
          _setOption: function(key, value) {
            if (key === "active") {
      
              // _activate() will handle invalid values and update this.options
              this._activate(value);
              return;
            }
      
            if (key === "event") {
              if (this.options.event) {
                this._off(this.headers, this.options.event);
              }
              this._setupEvents(value);
            }
      
            this._super(key, value);
      
            // Setting collapsible: false while collapsed; open first panel
            if (key === "collapsible" && !value && this.options.active === false) {
              this._activate(0);
            }
      
            if (key === "icons") {
              this._destroyIcons();
              if (value) {
                this._createIcons();
              }
            }
          },
      
          _setOptionDisabled: function(value) {
            this._super(value);
      
            this.element.attr("aria-disabled", value);
      
            // Support: IE8 Only
            // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE
            // so we need to add the disabled class to the headers and panels
            this._toggleClass(null, "ui-state-disabled", !!value);
            this._toggleClass(this.headers.add(this.headers.next()), null, "ui-state-disabled", !!value);
          },
      
          _keydown: function(event) {
            if (event.altKey || event.ctrlKey) {
              return;
            }
      
            var keyCode = $.ui.keyCode,
              length = this.headers.length,
              currentIndex = this.headers.index(event.target),
              toFocus = true;
      
            switch (event.keyCode) {
              case keyCode.RIGHT:
              case keyCode.TAB:
                if (event.shiftKey && event.keyCode == 9) {
                  //shift was down when tab was pressed
                }
                toFocus = this.headers[(currentIndex - 1) % length];
              case keyCode.DOWN:
                toFocus = this.headers[(currentIndex + 1)];
                break;
              case keyCode.LEFT:
              case keyCode.UP:
                toFocus = this.headers[(currentIndex - 1 + length) % length];
                break;
              case keyCode.SPACE:
              case keyCode.ENTER:
                this._eventHandler(event);
                break;
              case keyCode.HOME:
                toFocus = this.headers[0];
                break;
              case keyCode.END:
                toFocus = this.headers[length - 1];
                break;
            }
      
            if (toFocus) {
              $(event.target).attr("tabIndex", -1);
              $(toFocus).attr("tabIndex", 0);
              $(toFocus).trigger("focus");
              event.preventDefault();
            }
          },
      
          _panelKeyDown: function(event) {
            if (event.keyCode === $.ui.keyCode.UP && event.ctrlKey) {
              $(event.currentTarget).prev().trigger("focus");
            }
          },
      
          refresh: function() {
            var options = this.options;
            this._processPanels();
      
            // Was collapsed or no panel
            if ((options.active === false && options.collapsible === true) ||
              !this.headers.length) {
              options.active = false;
              this.active = $();
      
              // active false only when collapsible is true
            } else if (options.active === false) {
              this._activate(0);
      
              // was active, but active panel is gone
            } else if (this.active.length && !$.contains(this.element[0], this.active[0])) {
      
              // all remaining panel are disabled
              if (this.headers.length === this.headers.find(".ui-state-disabled").length) {
                options.active = false;
                this.active = $();
      
                // activate previous panel
              } else {
                this._activate(Math.max(0, options.active - 1));
              }
      
              // was active, active panel still exists
            } else {
      
              // make sure active index is correct
              options.active = this.headers.index(this.active);
            }
      
            this._destroyIcons();
      
            this._refresh();
          },
      
          _processPanels: function() {
            var prevHeaders = this.headers,
              prevPanels = this.panels;
      
            this.headers = this.element.find(this.options.header);
            this._addClass(this.headers, "ui-accordion-header ui-accordion-header-collapsed",
              "ui-state-default");
      
            this.panels = this.headers.next().filter(":not(.ui-accordion-content-active)").hide();
            this._addClass(this.panels, "ui-accordion-content", "ui-helper-reset ui-widget-content");
      
            // Avoid memory leaks (#10056)
            if (prevPanels) {
              this._off(prevHeaders.not(this.headers));
              this._off(prevPanels.not(this.panels));
            }
          },
      
          _refresh: function() {
            var maxHeight,
              options = this.options,
              heightStyle = options.heightStyle,
              parent = this.element.parent();
      
            this.active = this._findActive(options.active);
            this._addClass(this.active, "ui-accordion-header-active", "ui-state-active")
              ._removeClass(this.active, "ui-accordion-header-collapsed");
            this._addClass(this.active.next(), "ui-accordion-content-active");
            this.active.next().show();
      
            this.headers
              .attr("role", "heading")
              .attr("type", "button")
              .each(function() {
                var header = $(this),
                  headerId = header.uniqueId().attr("id"),
                  panel = header.next(),
                  panelId = panel.uniqueId().attr("id");
                header.attr("aria-controls", panelId);
                panel.attr("aria-labelledby", headerId);
              })
              .next()
              .attr("role", "region");
      
            this.headers
              .not(this.active)
              .attr({
                "aria-selected": "false",
                "aria-expanded": "false",
                tabIndex: -1
              })
              .next()
              .attr({
                "aria-hidden": "true"
              })
              .hide();
      
            // Make sure at least one header is in the tab order
            if (!this.active.length) {
              this.headers.eq(0).attr("tabIndex", 0);
            } else {
              this.active.attr({
                  "aria-selected": "true",
                  "aria-expanded": "true",
                  tabIndex: 0
                })
                .next()
                .attr({
                  "aria-hidden": "false"
                });
            }
      
            this._createIcons();
      
            this._setupEvents(options.event);
      
            if (heightStyle === "fill") {
              maxHeight = parent.height();
              this.element.siblings(":visible").each(function() {
                var elem = $(this),
                  position = elem.css("position");
      
                if (position === "absolute" || position === "fixed") {
                  return;
                }
                maxHeight -= elem.outerHeight(true);
              });
      
              this.headers.each(function() {
                maxHeight -= $(this).outerHeight(true);
              });
      
              this.headers.next()
                .each(function() {
                  $(this).height(Math.max(0, maxHeight -
                    $(this).innerHeight() + $(this).height()));
                })
                .css("overflow", "auto");
            } else if (heightStyle === "auto") {
              maxHeight = 0;
              this.headers.next()
                .each(function() {
                  var isVisible = $(this).is(":visible");
                  if (!isVisible) {
                    $(this).show();
                  }
                  maxHeight = Math.max(maxHeight, $(this).css("height", "").height());
                  if (!isVisible) {
                    $(this).hide();
                  }
                })
                .height(maxHeight);
            }
          },
      
          _activate: function(index) {
            var active = this._findActive(index)[0];
      
            // Trying to activate the already active panel
            if (active === this.active[0]) {
              return;
            }
      
            // Trying to collapse, simulate a click on the currently active header
            active = active || this.active[0];
      
            this._eventHandler({
              target: active,
              currentTarget: active,
              preventDefault: $.noop
            });
          },
      
          _findActive: function(selector) {
            return typeof selector === "number" ? this.headers.eq(selector) : $();
          },
      
          _setupEvents: function(event) {
            var events = {
              keydown: "_keydown"
            };
            if (event) {
              $.each(event.split(" "), function(index, eventName) {
                events[eventName] = "_eventHandler";
              });
            }
      
            this._off(this.headers.add(this.headers.next()));
            this._on(this.headers, events);
            this._on(this.headers.next(), {
              keydown: "_panelKeyDown"
            });
            this._hoverable(this.headers);
            this._focusable(this.headers);
          },
      
          _eventHandler: function(event) {
            var activeChildren, clickedChildren,
              options = this.options,
              active = this.active,
              clicked = $(event.currentTarget),
              clickedIsActive = clicked[0] === active[0],
              collapsing = clickedIsActive && options.collapsible,
              toShow = collapsing ? $() : clicked.next(),
              toHide = active.next(),
              eventData = {
                oldHeader: active,
                oldPanel: toHide,
                newHeader: collapsing ? $() : clicked,
                newPanel: toShow
              };
      
            event.preventDefault();
      
            if (
      
              // click on active header, but not collapsible
              (clickedIsActive && !options.collapsible) ||
      
              // allow canceling activation
              (this._trigger("beforeActivate", event, eventData) === false)) {
              return;
            }
      
            options.active = collapsing ? false : this.headers.index(clicked);
      
            // When the call to ._toggle() comes after the class changes
            // it causes a very odd bug in IE 8 (see #6720)
            this.active = clickedIsActive ? $() : clicked;
            this._toggle(eventData);
      
            // Switch classes
            // corner classes on the previously active header stay after the animation
            this._removeClass(active, "ui-accordion-header-active", "ui-state-active");
            if (options.icons) {
              activeChildren = active.children(".ui-accordion-header-icon");
              this._removeClass(activeChildren, null, options.icons.activeHeader)
                ._addClass(activeChildren, null, options.icons.header);
            }
      
            if (!clickedIsActive) {
              this._removeClass(clicked, "ui-accordion-header-collapsed")
                ._addClass(clicked, "ui-accordion-header-active", "ui-state-active");
              if (options.icons) {
                clickedChildren = clicked.children(".ui-accordion-header-icon");
                this._removeClass(clickedChildren, null, options.icons.header)
                  ._addClass(clickedChildren, null, options.icons.activeHeader);
              }
      
              this._addClass(clicked.next(), "ui-accordion-content-active");
            }
          },
      
          _toggle: function(data) {
            var toShow = data.newPanel,
              toHide = this.prevShow.length ? this.prevShow : data.oldPanel;
      
            // Handle activating a panel during the animation for another activation
            this.prevShow.add(this.prevHide).stop(true, true);
            this.prevShow = toShow;
            this.prevHide = toHide;
      
            if (this.options.animate) {
              this._animate(toShow, toHide, data);
            } else {
              toHide.hide();
              toShow.show();
              this._toggleComplete(data);
            }
      
            toHide.attr({
              "aria-hidden": "true"
            });
            toHide.prev().attr({
              "aria-selected": "false",
              "aria-expanded": "false"
            });
      
            // if we're switching panels, remove the old header from the tab order
            // if we're opening from collapsed state, remove the previous header from the tab order
            // if we're collapsing, then keep the collapsing header in the tab order
            if (toShow.length && toHide.length) {
              toHide.prev().attr({
                "tabIndex": -1,
                "aria-expanded": "false"
              });
            } else if (toShow.length) {
              this.headers.filter(function() {
                  return parseInt($(this).attr("tabIndex"), 10) === 0;
                })
                .attr("tabIndex", -1);
            }
      
            toShow
              .attr("aria-hidden", "false")
              .prev()
              .attr({
                "aria-selected": "true",
                "aria-expanded": "true",
                tabIndex: 0
              });
          },
      
          _animate: function(toShow, toHide, data) {
            var total, easing, duration,
              that = this,
              adjust = 0,
              boxSizing = toShow.css("box-sizing"),
              down = toShow.length &&
              (!toHide.length || (toShow.index() < toHide.index())),
              animate = this.options.animate || {},
              options = down && animate.down || animate,
              complete = function() {
                that._toggleComplete(data);
              };
      
            if (typeof options === "number") {
              duration = options;
            }
            if (typeof options === "string") {
              easing = options;
            }
      
            // fall back from options to animation in case of partial down settings
            easing = easing || options.easing || animate.easing;
            duration = duration || options.duration || animate.duration;
      
            if (!toHide.length) {
              return toShow.animate(this.showProps, duration, easing, complete);
            }
            if (!toShow.length) {
              return toHide.animate(this.hideProps, duration, easing, complete);
            }
      
            total = toShow.show().outerHeight();
            toHide.animate(this.hideProps, {
              duration: duration,
              easing: easing,
              step: function(now, fx) {
                fx.now = Math.round(now);
              }
            });
            toShow
              .hide()
              .animate(this.showProps, {
                duration: duration,
                easing: easing,
                complete: complete,
                step: function(now, fx) {
                  fx.now = Math.round(now);
                  if (fx.prop !== "height") {
                    if (boxSizing === "content-box") {
                      adjust += fx.now;
                    }
                  } else if (that.options.heightStyle !== "content") {
                    fx.now = Math.round(total - toHide.outerHeight() - adjust);
                    adjust = 0;
                  }
                }
              });
          },
      
          _toggleComplete: function(data) {
            var toHide = data.oldPanel,
              prev = toHide.prev();
      
            this._removeClass(toHide, "ui-accordion-content-active");
            this._removeClass(prev, "ui-accordion-header-active")
              ._addClass(prev, "ui-accordion-header-collapsed");
      
            // Work around for rendering bug in IE (#5421)
            if (toHide.length) {
              toHide.parent()[0].className = toHide.parent()[0].className;
            }
            this._trigger("activate", null, data);
          }
        });
      
        var safeActiveElement = $.ui.safeActiveElement = function(document) {
          var activeElement;
      
          // Support: IE 9 only
          // IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe>
          try {
            activeElement = document.activeElement;
          } catch (error) {
            activeElement = document.body;
          }
      
          // Support: IE 9 - 11 only
          // IE may return null instead of an element
          // Interestingly, this only seems to occur when NOT in an iframe
          if (!activeElement) {
            activeElement = document.body;
          }
      
          // Support: IE 11 only
          // IE11 returns a seemingly empty object in some cases when accessing
          // document.activeElement from an <iframe>
          if (!activeElement.nodeName) {
            activeElement = document.body;
          }
      
          return activeElement;
        };
      .accordionTitle {
        border: 1px solid #ccc;
        margin: 5px 0 0 0;
        font-weight: 200 !important;
        font-size: 1.15em;
        background-color: #F8F8F8;
        padding: 1em 0.5em;
        text-decoration: none;
        color: #000;
        -webkit-transition: background-color 0.5s ease-in-out;
        transition: background-color 0.5s ease-in-out;
      }
      
      .accordionTitle:before {
        content: "";
        font-size: 1.5em;
        border-left: 6px solid transparent;
        border-right: 6px solid transparent;
        border-top: 6px solid;
        float: left;
        margin: 0.475em;
        margin-right: 0.55em;
        -webkit-transition: -webkit-transform 0.3s ease-in-out;
        transition: -webkit-transform 0.3s ease-in-out;
        transition: transform 0.3s ease-in-out;
        transition: transform 0.3s ease-in-out, -webkit-transform 0.3s ease-in-out;
        -webkit-transform: rotate(-90deg);
        transform: rotate(-90deg);
      }
      
      .accordionTitle[aria-selected="true"]:before {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
      }
      
      .accordionTitle:focus,
      .accordionTitle:hover {
        background-color: #dadada;
      }
      
      .ui-accordion-content {
        height: auto !important;
        overflow: hidden;
        padding: 1.5em 1.5em;
        border: 1px solid #ccc;
      }
      
      [aria-pressed=true],
      [aria-expanded=true] {
        background-color: #f9f9f9;
      }
      <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
      <script src="http://sh101ftp.net/imgload/wordpress/jquery-ui.js"></script>
      <script src="http://sh101ftp.net/imgload/wordpress/NewCustomCodeJS.js"></script>
      <h2 id="question1" class="question"><span class="dropcap dropcap3" style="color: #127eb6;">1</span> <span style="color: #404040;">What might help you make physical activity an ongoing thing?</span></h2>
      <div id="accordion" role="presentation">
      <h3 class="accordionTitle"><strong>A.</strong> Option A</h3>
       <div>
       <p>This plan is practical, social, and could work well for both of you. Some disabilities an</span>d other pre-existing conditions have implications for working out. Your friend knows her own body and can seek medical clearance if needed. This is her call.</p>
       <p><u><a href="http://www.prochange.com/college-health" target="_blank" rel="noopener noreferrer">liveWell program (Pro-Change Behavior Systems, Inc.)</a></u></p>
       </div>
      
       <h3 class="accordionTitle"><strong>B.</strong> Option B</h3>
       <div>
       <p>Self-consciousness can be a barrier to working out, yes. Candy hasn’t said that’s a problem for her, though. Many people with disabilities are marginalized and excluded. We all do better when we’re socially integrated into our communities. For example, people with robust social networks (supportive friends and family) experience lower rates of chronic disease and longer lives, and more job opportunities, according to a 2011 report from the National Research Council.</p>
       <p><u><a href="http://november-project.com/" target="_blank" rel="noopener noreferrer">November Project</a></u></p>
       <p><u><a href="https://www.meetup.com/" target="_blank" rel="noopener noreferrer">Meetup</a></u></p>
       </div>
       <h3 class="accordionTitle"><strong>C.</strong> Option C</h3>
       <div>
       <p>Disability advocates call this “inspiration porn.” It’s condescending. Why should you be amazed that Candy wants to do something with her life?</p>
       </div>
      </div>

1 个答案:

答案 0 :(得分:1)

您正在做太多工作。我说这是基于看到的代码,例如:

<div id="accordion" role="presentation">

默认情况下,<div>没有角色,因此设置role="presentation"是多余的,只会使代码code肿。

此外,由于遍历您的Codepen示例似乎非常混乱(您不能向后翻转),因此您对tabindex的动态使用已关闭。通常,在使用本机HTML元素(例如<button>)时,您不必弄乱tabindex

一旦您开始抛出ARIA属性和tabindex,它就会变得非常混乱。我建议您构建一个简单的示例,以便您可以看到它如何正常工作。从Accordions的WAI-ARIA创作惯例1.1部分开始。它有一个working example

基本上,手风琴由以下组成:

  • 手风琴元素 –外窗格(通常是列表)中包含的一组面板
  • Accordion header –可折叠和可折叠的手风琴面板上标记的区域
  • 手风琴面板 –包含特定于每个标题的内容的区域(容器)

首先尝试这些简单的步骤:

  1. 每个手风琴标题的标题都包含在<button>或带有role="button"的元素中。

  2. 每个手风琴标题按钮都包装在<hX>元素中,其元素具有适合页面信息体系结构的级别。 button元素是标题元素内的唯一元素。

  3. 如果与手风琴标题关联的手风琴面板可见,则标题按钮元素的aria-expanded设置为true。如果面板不可见,则将aria-expanded设置为false。面板本身应适当设置aria-hidden或用CSS("display:none")隐藏

  4. 手风琴标题按钮元素应将aria-controls设置为包含手风琴面板内容的元素的ID。

  5. 手风琴面板有role="region"aria-labelledby,其值表示控制面板显示的按钮。

所以你会有类似的东西:

<div> <!-- accordion container -->
  <h3>
    <button id="first" aria-expanded="false" aria-controls="panel1">first accordion title</button>
  </h3>
  <div id="panel1" role="region" style="display:none;" aria-labelledby= "first">
    <!-- contents of your panel -->
  </div>
  <h3>
    <button id="second" aria-expanded="false" aria-controls="panel2">second title</button>
  </h3>
  <div id="panel2" role="region" style="display:none;" aria-labelledby= "second">
    <!-- contents of your panel -->
  </div>
</div>

选择按钮后,应切换按钮的aria-expanded属性和面板的display:none CSS样式。

这将允许对所有手风琴标题(按钮)进行本机制表,在您的情况下是问题A,B和C。您不必弄混tabindex,因为按钮可以通过默认。您所要做的就是切换按钮的aria-expanded属性并隐藏/取消隐藏面板内容。十分简单。它可以与键盘或屏幕阅读器搭配使用。