Angular Bootstrap 键盘可访问下拉子菜单的实现?
Angular Bootstrap implementation for keyboard accessible dropdown submenu?
我的目标是制作一个具有正常键盘和屏幕阅读器行为的子菜单,使用:
- AngularJS
- jQuery
- Bootstrap 3
- 来自 'css - Bootstrap 3 dropdown sub menu missing'
的 CSS 子菜单实现
这是一个起点,展示了子菜单(不)如何在没有任何自定义的情况下与键盘一起工作:
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.selectItem = function (item) {
alert(item);
};
}
/*
* No submenu in Bootstrap 3 - need to put CSS in manually.
* All CSS from
*/
.dropdown-submenu {
position: relative;
}
.dropdown-submenu>.dropdown-menu {
top: 0;
left: 100%;
margin-top: -6px;
margin-left: -1px;
-webkit-border-radius: 0 6px 6px 6px;
-moz-border-radius: 0 6px 6px 6px;
border-radius: 0 6px 6px 6px;
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
.dropdown-submenu > a:after {
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width:5px 0 5px 5px;
border-left-color:#cccccc;
margin-top:5px;
margin-right:-10px;
}
.dropdown-submenu:hover>a:after {
border-left-color: #ffffff;
}
.dropdown-submenu.pull-left {
float: none;
}
.dropdown-submenu.pull-left>.dropdown-menu {
left: -100%;
margin-left: 10px;
-webkit-border-radius: 6px 0 6px 6px;
-moz-border-radius: 6px 0 6px 6px;
border-radius: 6px 0 6px 6px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller="MyCtrl">
<div class="dropdown">
<a class="dropdown-toggle btn btn-primary" id="DropdownButton" data-toggle="dropdown" role="button" href="#">DROPDOWN</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="DropdownButton">
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('1')">option 1</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('2')">option 2</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('3')">option 3</a>
</li>
<li role="presentation" class="dropdown dropdown-submenu" custom-submenu>
<a class="dropdown-toggle" id="SubmenuButton" role="menuitem" tabindex="0" href="#" aria-haspopup="true">option 4 (has children)</a>
<ul class="dropdown-menu" role="menu" aria-hidden="true" aria-labelledby="SubmenuButton">
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('submenu item 1')">
child option 1
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('submenu item 2')">
child option 2
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('submenu item 3')">
child option 3
</a>
</li>
</ul>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('item 5')">option 5</a>
</li>
</ul>
</div>
</div>
Here's a JSFiddle of the above。我会 post 我的解决方案在答案中,但我认为它并不完美;我对反馈或替代方案感兴趣。
Here's a JSFiddle of my solution(出于某种原因,它在这里不能用作片段?)。这是一个名为 custom-submenu
的自定义指令,我将其附加到子菜单的最外层(li
)。
行为:
- 向上和向下箭头导航顶级菜单
- Space 打开子菜单,然后你可以向下箭头进入它
- 在子菜单外向上或向下箭头会关闭子菜单并返回其父菜单
- Enter 选择一个项目
就屏幕阅读器而言,这在 FF 和 IE 中的 NVDA 中运行良好,但 ChromeVox 不喜欢它。
指令代码如下:
myApp.directive("customSubmenu", ['$timeout', function ($timeout) {
return {
link: function ($scope, element, attrs) {
var toggleButton = $(element).find('.dropdown-toggle');
var submenu = $(element).find('.dropdown-menu');
/*
* handle keydown on the submenu itself - if we arrow up from the first element or down from the last,
* close submenu
*/
submenu.keydown(function (event) {
if (!(event.keyCode === 38 || event.keyCode === 40)) return;
var links = $(element).find('li:not(.divider):not(.disabled) a');
if (event.keyCode === 38 && event.target == links[0]) {
// first submenu item - up arrow - close submenu, focus toggle button, stop propagation
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
toggleButton.focus();
event.stopPropagation();
event.preventDefault();
} else if (event.keyCode === 40 && event.target == links[links.length - 1]) {
// last submenu item - down arrow - close submenu, focus toggle button, stop propagation
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
toggleButton.focus();
event.stopPropagation();
event.preventDefault();
}
});
/*
* handle keydown on toggle button - space toggles submenu visibility, arrows navigate outer menu
*/
toggleButton.keydown(function (event) {
if (event.keyCode === 32) { // space bar - open/close submenu
if ($(element).hasClass('open')) {
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
} else {
$(element).addClass('open');
submenu.attr('aria-hidden', false);
}
event.stopPropagation();
event.preventDefault();
} else if (event.keyCode === 40) { // down arrow
if (!$(element).hasClass('open')) {
// even though the submenu isn't open, the bootstrap dropdown directive will try to focus the
// hidden submenu items, so intercept the keydown and focus the next outer menu item instead
var nextSibling = $(element).nextAll('li:not(.divider):not(.disabled):visible');
if (nextSibling && nextSibling[0]) {
var nextSiblingLink = $(nextSibling[0]).find('a');
if (nextSiblingLink && nextSiblingLink[0]) {
// focus next menu item
$(nextSiblingLink[0]).focus();
// while we're at it, let's attach a handler to that next link, telling it to focus this
// one when the up arrow is pressed (instead of trying to go into the hidden submenu items)
// (TODO: is this going to chain a bunch of these handlers?)
$(nextSiblingLink[0]).keydown(function (e) {
if (e.keyCode === 38) { // up
toggleButton.focus();
e.stopPropagation();
e.preventDefault();
};
});
}
}
event.stopPropagation();
event.preventDefault();
}
}
});
/*
* handle click on toggle button - open or close submenu
*/
toggleButton.click(function (event) {
if ($(element).hasClass('open')) {
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
} else {
$(element).addClass('open');
submenu.attr('aria-hidden', false);
}
event.stopPropagation();
event.preventDefault();
});
}
}
}]);
如有任何建议,我将不胜感激!
我的目标是制作一个具有正常键盘和屏幕阅读器行为的子菜单,使用:
- AngularJS
- jQuery
- Bootstrap 3
- 来自 'css - Bootstrap 3 dropdown sub menu missing' 的 CSS 子菜单实现
这是一个起点,展示了子菜单(不)如何在没有任何自定义的情况下与键盘一起工作:
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.selectItem = function (item) {
alert(item);
};
}
/*
* No submenu in Bootstrap 3 - need to put CSS in manually.
* All CSS from
*/
.dropdown-submenu {
position: relative;
}
.dropdown-submenu>.dropdown-menu {
top: 0;
left: 100%;
margin-top: -6px;
margin-left: -1px;
-webkit-border-radius: 0 6px 6px 6px;
-moz-border-radius: 0 6px 6px 6px;
border-radius: 0 6px 6px 6px;
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
.dropdown-submenu > a:after {
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width:5px 0 5px 5px;
border-left-color:#cccccc;
margin-top:5px;
margin-right:-10px;
}
.dropdown-submenu:hover>a:after {
border-left-color: #ffffff;
}
.dropdown-submenu.pull-left {
float: none;
}
.dropdown-submenu.pull-left>.dropdown-menu {
left: -100%;
margin-left: 10px;
-webkit-border-radius: 6px 0 6px 6px;
-moz-border-radius: 6px 0 6px 6px;
border-radius: 6px 0 6px 6px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller="MyCtrl">
<div class="dropdown">
<a class="dropdown-toggle btn btn-primary" id="DropdownButton" data-toggle="dropdown" role="button" href="#">DROPDOWN</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="DropdownButton">
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('1')">option 1</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('2')">option 2</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('3')">option 3</a>
</li>
<li role="presentation" class="dropdown dropdown-submenu" custom-submenu>
<a class="dropdown-toggle" id="SubmenuButton" role="menuitem" tabindex="0" href="#" aria-haspopup="true">option 4 (has children)</a>
<ul class="dropdown-menu" role="menu" aria-hidden="true" aria-labelledby="SubmenuButton">
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('submenu item 1')">
child option 1
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('submenu item 2')">
child option 2
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('submenu item 3')">
child option 3
</a>
</li>
</ul>
</li>
<li role="presentation">
<a role="menuitem" tabindex="0" href="#" ng-click="selectItem('item 5')">option 5</a>
</li>
</ul>
</div>
</div>
Here's a JSFiddle of the above。我会 post 我的解决方案在答案中,但我认为它并不完美;我对反馈或替代方案感兴趣。
Here's a JSFiddle of my solution(出于某种原因,它在这里不能用作片段?)。这是一个名为 custom-submenu
的自定义指令,我将其附加到子菜单的最外层(li
)。
行为:
- 向上和向下箭头导航顶级菜单
- Space 打开子菜单,然后你可以向下箭头进入它
- 在子菜单外向上或向下箭头会关闭子菜单并返回其父菜单
- Enter 选择一个项目
就屏幕阅读器而言,这在 FF 和 IE 中的 NVDA 中运行良好,但 ChromeVox 不喜欢它。
指令代码如下:
myApp.directive("customSubmenu", ['$timeout', function ($timeout) {
return {
link: function ($scope, element, attrs) {
var toggleButton = $(element).find('.dropdown-toggle');
var submenu = $(element).find('.dropdown-menu');
/*
* handle keydown on the submenu itself - if we arrow up from the first element or down from the last,
* close submenu
*/
submenu.keydown(function (event) {
if (!(event.keyCode === 38 || event.keyCode === 40)) return;
var links = $(element).find('li:not(.divider):not(.disabled) a');
if (event.keyCode === 38 && event.target == links[0]) {
// first submenu item - up arrow - close submenu, focus toggle button, stop propagation
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
toggleButton.focus();
event.stopPropagation();
event.preventDefault();
} else if (event.keyCode === 40 && event.target == links[links.length - 1]) {
// last submenu item - down arrow - close submenu, focus toggle button, stop propagation
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
toggleButton.focus();
event.stopPropagation();
event.preventDefault();
}
});
/*
* handle keydown on toggle button - space toggles submenu visibility, arrows navigate outer menu
*/
toggleButton.keydown(function (event) {
if (event.keyCode === 32) { // space bar - open/close submenu
if ($(element).hasClass('open')) {
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
} else {
$(element).addClass('open');
submenu.attr('aria-hidden', false);
}
event.stopPropagation();
event.preventDefault();
} else if (event.keyCode === 40) { // down arrow
if (!$(element).hasClass('open')) {
// even though the submenu isn't open, the bootstrap dropdown directive will try to focus the
// hidden submenu items, so intercept the keydown and focus the next outer menu item instead
var nextSibling = $(element).nextAll('li:not(.divider):not(.disabled):visible');
if (nextSibling && nextSibling[0]) {
var nextSiblingLink = $(nextSibling[0]).find('a');
if (nextSiblingLink && nextSiblingLink[0]) {
// focus next menu item
$(nextSiblingLink[0]).focus();
// while we're at it, let's attach a handler to that next link, telling it to focus this
// one when the up arrow is pressed (instead of trying to go into the hidden submenu items)
// (TODO: is this going to chain a bunch of these handlers?)
$(nextSiblingLink[0]).keydown(function (e) {
if (e.keyCode === 38) { // up
toggleButton.focus();
e.stopPropagation();
e.preventDefault();
};
});
}
}
event.stopPropagation();
event.preventDefault();
}
}
});
/*
* handle click on toggle button - open or close submenu
*/
toggleButton.click(function (event) {
if ($(element).hasClass('open')) {
$(element).removeClass('open');
submenu.attr('aria-hidden', true);
} else {
$(element).addClass('open');
submenu.attr('aria-hidden', false);
}
event.stopPropagation();
event.preventDefault();
});
}
}
}]);
如有任何建议,我将不胜感激!