AngularJS Accessible Custom Select Menu
by niceux
HTML
<div ng-app ng-controller="SelectMenuController">
<span id="selectLabel">
What's your favorite 90's music act:
</span>
<div id="selectMenuWrapper">
<button
aria-haspopup="listbox"
aria-labelledby="selectLabel selectMenu"
id="selectMenu"
ng-click="toggleSelectMenu($event)"
style="min-width: {{initButtonWidth}}">
{{ selectMenuLabel }}
</button>
<ul
id="selectMenuList"
tabindex="-1"
role="listbox"
aria-labelledby="selectLabel"
ng-class="{'hidden': showList == false}">
<li
ng-repeat="option in menuOptions"
id="{{option.id}}"
role="option"
ng-click="selectOption(option)"
ng-model="selectedArtist">
{{option.name}}
</li>
</ul>
</div>
</div>
SCSS
* {
box-sizing: border-box;
&:focus {
outline-color: red;
}
}
.annotate {
font-style: italic;
color: #366ed4;
}
.listbox-area {
padding: 20px;
background: #eee;
border: 1px solid #aaa;
font-size: 0;
}
.left-area,
.right-area {
box-sizing: border-box;
display: inline-block;
font-size: 14px;
vertical-align: top;
width: 50%;
}
.left-area {
padding-right: 10px;
}
.right-area {
padding-left: 10px;
}
[role="listbox"] {
padding: 0;
background: white;
border: 1px solid #aaa;
}
[role="option"] {
display: block;
padding: 3px 6px;
position: relative;
line-height: 1.8em;
&:hover {
cursor: pointer;
}
}
[role="option"].focused {
background: #bde4ff;
}
button {
font-size: 16px;
&:hover {
cursor: pointer;
}
}
button[aria-disabled="true"] {
opacity: 0.5;
}
.move-right-btn {
padding-right: 20px;
position: relative;
}
.move-right-btn::after {
content: ' ';
height: 10px;
width: 12px;
background-image: url('../imgs/Arrows-Right-icon.png');
background-position: center right;
position: absolute;
right: 2px;
top: 6px;
}
.move-left-btn {
padding-left: 20px;
position: relative;
}
.move-left-btn::after {
content: ' ';
height: 10px;
width: 12px;
background-image: url('../imgs/Arrows-Left-icon.png');
background-position: center left;
position: absolute;
left: 2px;
top: 6px;
}
#ss_elem_list {
max-height: 18em;
overflow-y: auto;
position: relative;
}
#selectMenu {
border-radius: 0;
font-size: 16px;
text-align: left;
padding: 10px 30px 10px 8px;
position: relative;
}
#selectMenu::after {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid #005eb8;
content: " ";
position: absolute;
right: 8px;
top: 16px;
}
#selectMenu[aria-expanded="true"]::after {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 0;
...
JavaScript
function SelectMenuController($scope) {
$scope.showList = false;
$scope.selectMenuButton = document.getElementById('selectMenu');
$scope.initButtonWidth = $scope.selectMenuButton.offsetWidth + 'px';
$scope.menuList = document.getElementById('selectMenuList');
$scope.selectedArtist = null;
$scope.selectMenuLabel = 'Select an option from the menu';
$scope.menuOptions = [{
name: "Snoop Dogg",
id: "snoopDogg"
},
{
name: "Weird Al Yankovic",
id: "weirdAl"
},
{
name: "Spice Girls",
id: "spiceGirls"
}
];
$scope.toggleSelectMenu = function() {
$scope.showList = !$scope.showList;
let htmlElementToSelect;
switch ($scope.showList) {
case true:
window.setTimeout(function() {
$scope.menuList.focus();
$scope.selectMenuButton.setAttribute('aria-expanded', 'true');
let focusedElement = $scope.menuOptions.find(option => option.focused === true);
if (focusedElement && $scope.showList) {
htmlElementToSelect = document.getElementById(focusedElement.id);
} else if (!focusedElement && $scope.showList) {
$scope.selectedArtist = $scope.menuOptions[0];
htmlElementToSelect = document.getElementById($scope.menuOptions[0].id);
$scope.menuOptions[0].focused = true;
htmlElementToSelect.classList.add('focused');
}
$scope.menuList.setAttribute('aria-activedescendant', $scope.selectedArtist.id);
htmlElementToSelect.setAttribute('aria-selected', true);
}, 0);
break;
default:
window.setTimeout(function() {
$scope.selectMenuButton.focus();
$scope.selectMenuButton.removeAttribute('aria-expanded');
}, 0);
}
}
// Select the menu option on click or arrow keypress
$scope.selectOption = function (selectedOption) {
let currentSelection =...