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 =...