Skip to content

Commit

Permalink
Fixes: 1411 - [Accessibility] Add keyboard navigation to side nav (#1459
Browse files Browse the repository at this point in the history
)

* Fixes: 1411 - [Accessibility] Add keyboard navigation to side nav

* Changing to use regular event handling
  • Loading branch information
Elliott Hamai authored Jun 13, 2017
1 parent 902d062 commit 92dab86
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
[class.has-value]="!!value && warning"
[(ngModel)]="value"
placeholder="{{placeholder}}"
(keyup)="onKeyUp($event)" />
(keyup)="onKeyUp($event)"
#searchTextBox/>
<span class="right">
<i *ngIf="value" class="fa fa-times" (click)="onClearClick($event)"></i>
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Output, Input } from '@angular/core';
import { Component, Output, Input, ViewChild } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Component({
Expand All @@ -13,7 +13,10 @@ export class SearchBoxComponent {
@Output() onInputChange = new Subject<string>();
@Output() onClear = new Subject<void>();

constructor() { }
@ViewChild('searchTextBox') searchTextBox;


constructor() {}

onKeyUp(event: any) {
if (event.keyCode === 27) { // ESC
Expand All @@ -28,4 +31,8 @@ export class SearchBoxComponent {
this.onClear.next(null);
}

focus(){
this.searchTextBox.nativeElement.focus();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,12 @@ export class Order {
'ExternalFileTrigger-',
'ExternalTable-'
]
}

export class KeyCodes{
public static readonly enter = 13;
public static readonly arrowLeft = 37;
public static readonly arrowUp = 38;
public static readonly arrowRight = 39;
public static readonly arrowDown = 40;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
[value]="searchTerm"
[placeholder]="'search' | translate"
[warning]="true">
</search-box>


</search-box>

<div id="sidenav-subs">
<multi-drop-down
Expand All @@ -21,7 +19,12 @@
</div>

<!--<div *ngIf="selectedSubscriptions.length > 0">-->
<div>
<div tabindex="0"
#treeViewContainer
id="tree-view-container"
(focus)="onFocus($event)"
(blur)="onBlur($event)"
(keydown)="onKeyDown($event)">
<tree-view *ngIf="rootNode" [node]="rootNode" [levelInput]="0"></tree-view>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@
}
}



#sidenav-subs{
margin-top: 5px;
}

:host /deep/ multi-drop-down .multi-drop-down-container{
width: $sidenav-width;
}

#tree-view-container{
&:focus{
outline: none;
}
}
108 changes: 100 additions & 8 deletions AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Component, OnInit, EventEmitter, OnDestroy, Output, Input } from '@angular/core';
import { SearchBoxComponent } from './../search-box/search-box.component';
import { TreeNodeIterator } from './../tree-view/tree-node-iterator';
import { Component, OnInit, EventEmitter, OnDestroy, Output, Input, ViewChild, AfterViewInit } from '@angular/core';
import {Http} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
Expand All @@ -13,7 +15,7 @@ import { FunctionApp } from './../shared/function-app';
import { PortalResources } from './../shared/models/portal-resources';
import { AuthzService } from './../shared/services/authz.service';
import { LanguageService } from './../shared/services/language.service';
import { Arm } from './../shared/models/constants';
import { Arm, KeyCodes } from './../shared/models/constants';
import { SiteDescriptor, Descriptor } from './../shared/resourceDescriptors';
import { PortalService } from './../shared/services/portal.service';
import { ArmArrayResult } from './../shared/models/arm/arm-obj';
Expand Down Expand Up @@ -44,8 +46,10 @@ import {SlotsService} from './../shared/services/slots.service';
styleUrls: ['./side-nav.component.scss'],
inputs: ['tryFunctionAppInput']
})
export class SideNavComponent{
export class SideNavComponent implements AfterViewInit {
@Output() treeViewInfoEvent: EventEmitter<TreeViewInfo>;
@ViewChild('treeViewContainer') treeViewContainer;
@ViewChild(SearchBoxComponent) searchBox : SearchBoxComponent;

public rootNode : TreeNode;
public subscriptionOptions: DropDownElement<Subscription>[] = [];
Expand All @@ -57,10 +61,13 @@ export class SideNavComponent{

public searchTerm = "";
public hasValue = false;
public tryFunctionApp : FunctionApp;

public selectedNode : TreeNode;
public selectedDashboardType : DashboardType;
public firstLevelOrDescendentIsSelected : boolean;

private _focusedNode : TreeNode; // For keyboard navigation
private _iterator : TreeNodeIterator;

private _savedSubsKey = "/subscriptions/selectedIds";
private _subscriptionsStream = new ReplaySubject<Subscription[]>(1);
Expand All @@ -69,8 +76,6 @@ export class SideNavComponent{
private _initialized = false;

private _tryFunctionAppStream = new Subject<FunctionApp>();
// public tryFunctions = false;
public tryFunctionApp : FunctionApp;

set tryFunctionAppInput(functionApp : FunctionApp){
if(functionApp){
Expand Down Expand Up @@ -108,16 +113,20 @@ export class SideNavComponent{
// this.resourceId = !!this.resourceId ? this.resourceId : info.resourceId;
this.initialResourceId = info.resourceId;

this.rootNode = new TreeNode(this, null, null);

let appsNode = new AppsNode(
this,
this.rootNode,
this._subscriptionsStream,
this._searchTermStream,
this.resourceId);

this.rootNode = new TreeNode(this, null, null);
this.rootNode.children = [appsNode];
this.rootNode.isExpanded = true;

appsNode.parent = this.rootNode;

// Need to allow the appsNode to wire up its subscriptions
setTimeout(() =>{
appsNode.select();
Expand Down Expand Up @@ -168,7 +177,7 @@ export class SideNavComponent{
let appNode = new AppNode(
this,
this.tryFunctionApp.site,
null,
this.rootNode,
[],
false);

Expand All @@ -180,6 +189,87 @@ export class SideNavComponent{
})
}

ngAfterViewInit(){
// Search box is not available for Try Functions
if(this.searchBox){
this.searchBox.focus();
}
}

onFocus(event : FocusEvent){
if(!this._focusedNode){
this._focusedNode = this.rootNode.children[0];
this._iterator = new TreeNodeIterator(this._focusedNode);
}

this._focusedNode.isFocused = true;
}

onBlur(event : FocusEvent){
if(this._focusedNode){

// Keep the focused node around in case user navigates back to it
this._focusedNode.isFocused = false;
}
}

onKeyDown(event : KeyboardEvent){
if(event.keyCode === KeyCodes.arrowDown){
this._moveDown();
}
else if(event.keyCode === KeyCodes.arrowUp){
this._moveUp();
}
else if(event.keyCode === KeyCodes.enter){
this._focusedNode.select();
}
else if(event.keyCode === KeyCodes.arrowRight){
if(this._focusedNode.showExpandIcon && !this._focusedNode.isExpanded){
this._focusedNode.toggle(event);
}
else{
this._moveDown();
}
}
else if(event.keyCode === KeyCodes.arrowLeft){
if(this._focusedNode.showExpandIcon && this._focusedNode.isExpanded){
this._focusedNode.toggle(event);
}
else{
this._moveUp();
}
}
}

private _moveDown(){
let nextNode = this._iterator.next();
if(nextNode){
this._focusedNode.isFocused = false;
this._focusedNode = nextNode;
}

this._focusedNode.isFocused = true;
}

private _moveUp(){
let prevNode = this._iterator.previous();
if(prevNode){
this._focusedNode.isFocused = false;
this._focusedNode = prevNode;
}

this._focusedNode.isFocused = true;
}

private _changeFocus(node : TreeNode){
if(this._focusedNode){
this._focusedNode.isFocused = false;
node.isFocused = true;
this._iterator = new TreeNodeIterator(node);
this._focusedNode = node;
}
}

updateView(newSelectedNode : TreeNode, newDashboardType : DashboardType, force? : boolean) : Observable<boolean>{
if(this.selectedNode){

Expand Down Expand Up @@ -213,6 +303,8 @@ export class SideNavComponent{
this.treeViewInfoEvent.emit(viewInfo);
this._updateTitle(newSelectedNode);
this.portalService.closeBlades();

this._changeFocus(newSelectedNode);
return newSelectedNode.handleSelection();
}

Expand Down
3 changes: 2 additions & 1 deletion AzureFunctions.AngularClient/src/app/tree-view/apps-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ export class AppsNode extends TreeNode implements MutableCollection, Disposable,

constructor(
sideNav: SideNavComponent,
rootNode: TreeNode,
private _subscriptionsStream : Subject<Subscription[]>,
private _searchTermStream : Subject<string>,
private _initialResourceId : string) { // Should only be used for when the iframe is open on a specific app

super(sideNav, null, null);
super(sideNav, null, rootNode);

this.newDashboardType = sideNav.configService.isStandalone() ? DashboardType.createApp : null;
this.inSelectedTree = !!this.newDashboardType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { TreeNode } from './tree-node';
export class TreeNodeIterator{
constructor(private _curNode : TreeNode){
}

public next() : TreeNode{

// If node has any immediate children
if(this._curNode.children.length > 0 && this._curNode.isExpanded){
this._curNode = this._curNode.children[0];
}
else{
let curIndex = this._curNode.parent.children.indexOf(this._curNode);

// If node has a lower sibling
if(curIndex < this._curNode.parent.children.length - 1){
this._curNode = this._curNode.parent.children[curIndex + 1];
}
else if(this._curNode.parent.parent){
let nextAncestor = this._findNextAncestor(this._curNode);
if(nextAncestor){
this._curNode = nextAncestor;
}
else{
// You're at the end, but don't set node to null because
// a user may expand the current node, which will allow you
// to continue iterating if called again later.
return null;
}
}
else{
return null;
}
}

return this._curNode;
}

public previous() : TreeNode{
let curIndex = this._curNode.parent.children.indexOf(this._curNode);

// If node has higher sibling
if(curIndex > 0){
let prevSibling = this._curNode.parent.children[curIndex - 1];
this._curNode = this._findLastDescendant(prevSibling);
}
else if(this._curNode.parent.parent){

// Check to make sure we don't set curNode to a parent if it's
// the root node which has no UI
this._curNode = this._curNode.parent;
}

else{
return null;
}

return this._curNode;
}

private _findNextAncestor(curNode : TreeNode) : TreeNode{

if(curNode.parent.parent){
let parentIndex = curNode.parent.parent.children.indexOf(curNode.parent);
if(parentIndex < curNode.parent.parent.children.length - 1){
return curNode.parent.parent.children[parentIndex + 1];
}
else{
return this._findNextAncestor(curNode.parent);
}
}

return null;
}

private _findLastDescendant(node : TreeNode){
if(node.isExpanded && node.children.length > 0){
return this._findLastDescendant(node.children[node.children.length-1]);
}
else{
return node;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class TreeNode implements Disposable, Removable, CanBlockNavChange, Custo
public supportsScope = false;
public disabled = false;
public inSelectedTree = false;
public isFocused = false;

constructor(
public sideNav : SideNavComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[class.clickable]="!node.disabled"
[class.selected]="node.sideNav && node.sideNav.resourceId === node.resourceId"
[class.try-root-node]="level === 0 && showTryView"
[class.focused]="node.isFocused"
(mouseenter)="node.showMenu = true"
(mouseleave)="node.showMenu = false"
(click)="node.select()">
Expand Down
Loading

0 comments on commit 92dab86

Please sign in to comment.