gpt4 book ai didi

javascript - 如何让键盘+屏幕阅读器访问我的 Accordion ?

转载 作者:行者123 更新时间:2023-11-30 20:51:15 25 4
gpt4 key购买 nike

我一直在努力使我当前的可扩展 Accordion 能够通过键盘访问和屏幕阅读器访问来满足 W3C Web 内容可访问性指南的 AA 级。

我对 JavaScript/jQuery 不是很熟悉,所以到目前为止我一直在猜测和检查。

我完成了以下任务:

  1. 使用 Tab 键的制表索引顺序
  2. 能够使用向上/向下或向左/向右键盘键来回移动
  3. 使用 Enter 或空格键展开/折叠 Accordion 的能力

但显然我缺少以下内容:

  1. 无法使用“Shift+Tab”向后导航。
  2. 无法折叠展开的切换开关,因为焦点移动不正确,并且使用 shift+tab 不会将焦点带回展开的切换开关。
  3. 焦点不会移动到开关下方的链接。
  4. 不存在标签分组,屏幕阅读器不阅读标签 1(共 3 个)、标签 2(共 3 个)等。

这是我一直在使用的 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>

最佳答案

你做的工作太多了。我这样说是基于看到如下代码:

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

A <div> ,默认情况下没有 Angular 色,因此设置 role="presentation"是多余的,只会使您的代码膨胀。

此外,由于在您的 codepen 示例中使用 Tab 键似乎很困惑(您不能向后使用 Tab 键),因此您动态使用 tabindex已关闭。通常,当使用原生 HTML 元素时,例如 <button> ,你不必弄乱 tabindex .

一旦你开始投入 ARIA 属性和 tabindex ,它开始变得非常困惑。我建议构建一个简单的示例,以便您了解它是如何正常工作的。从 Accordions 上的 WAI-ARIA 创作实践 1.1 部分开始.它有一个 working example .

基本上, Accordion 包括:

  • Accordion 元素 – 包含在外面板(通常是列表)中的面板集合
  • Accordion 标题 – 可展开和折叠的 Accordion 面板的标记区域
  • Accordion 面板 – 包含特定于每个 header 的内容的区域(容器)

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

  1. 每个 Accordion 标题的标题包含在 <button> 中或带有 role="button" 的元素.

  2. 每个 Accordion 标题按钮都包裹在 <hX> 中具有适合页面信息架构的级别的元素。按钮元素是标题元素内的唯一元素。

  3. 如果与 Accordion 标题关联的 Accordion 面板可见,则标题按钮元素具有 aria-expanded设置为真。如果面板不可见,aria-expanded设置为假。面板本身应该有 aria-hidden使用 CSS ( "display:none") 适当设置或隐藏

  4. Accordion 标题按钮元素应该有 aria-controls设置为包含 Accordion 面板内容的元素的 ID。

  5. Accordion 面板有 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 样式。

这将允许对所有 Accordion 标题(按钮)进行本地制表,在您的情况下是问题 A、B 和 C。您不必弄乱 tabindex。因为按钮默认是可聚焦的。您所要做的就是切换按钮的 aria-expanded。属性并隐藏/取消隐藏面板内容。十分简单。它适用于键盘或屏幕阅读器。

关于javascript - 如何让键盘+屏幕阅读器访问我的 Accordion ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48156930/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com