├── angular2 ├── myapp.css ├── README.md ├── package.json ├── index.html ├── myapp.es6.js ├── aria-combined.css ├── myapp.html ├── gulpfile.js ├── ariamenubar.es6.js ├── ariamenuitem.es6.js └── ariamenu.es6.js ├── polymer ├── aria-menubar.css ├── README.md ├── bower.json ├── aria-menuitem.css ├── aria-menubar.html ├── aria-menu.html ├── index.html └── aria-menuitem.html ├── native ├── README.md ├── aria-menubar.html ├── web_components.html ├── aria-menu.html └── aria-menuitem.html ├── README.md ├── .gitignore ├── package.json └── LICENSE /angular2/myapp.css: -------------------------------------------------------------------------------- 1 | .content { 2 | padding-top:10em; 3 | padding-bottom:10em; 4 | padding-left:40%; 5 | padding-right:40%; 6 | width:20%; 7 | } 8 | -------------------------------------------------------------------------------- /polymer/aria-menubar.css: -------------------------------------------------------------------------------- 1 | :host { 2 | box-sizing: border-box; 3 | } 4 | :host(:before), :host(:after) { 5 | box-sizing: inherit; 6 | } 7 | :host { 8 | padding: 0; 9 | margin: 0; 10 | width:100%; 11 | } 12 | :host(:after) { 13 | content: ""; 14 | display:table; 15 | clear:both; 16 | } 17 | -------------------------------------------------------------------------------- /native/README.md: -------------------------------------------------------------------------------- 1 | # Accessible Native Web Components 2 | 3 | To get started make sure you have a web server 4 | 5 | ``` 6 | npm install -g serve 7 | ``` 8 | 9 | Start a web server in this directory 10 | 11 | ``` 12 | serve -p 8000 13 | ``` 14 | 15 | Browse to [http://localhost:8000](http://localhost:8000) with Chrome (or any Web browser that supports the Web components standard) 16 | -------------------------------------------------------------------------------- /polymer/README.md: -------------------------------------------------------------------------------- 1 | # Accessible Polymer Examples 2 | 3 | To get started run bower in this directory 4 | 5 | ``` 6 | bower install 7 | ``` 8 | 9 | Make sure you have a web server 10 | 11 | ``` 12 | npm install -g serve 13 | ``` 14 | 15 | Start a web server in this directory 16 | 17 | ``` 18 | serve -p 8000 19 | ``` 20 | 21 | Browse to [http://localhost:8000](http://localhost:8000) 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Axponents 2 | This repository consosts of examples of Accessible Web Components implemented on different frameworks 3 | 4 | The [native](native) directory contains native web components 5 | 6 | The [polymer](polymer) directory contains portable web components using the Polymer framework 7 | 8 | The [angular2](angular2) directory contains Angular 2 examples 9 | 10 | ## Getting started 11 | 12 | Look at the README in each of the directories for information on how to get started. 13 | -------------------------------------------------------------------------------- /angular2/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | For all of these instructions to work, make sure your current directory is the angular2 directory. 4 | 5 | Clone the Angular2 quickstart repository: 6 | 7 | ``` 8 | git clone https://github.com/angular/quickstart.git 9 | ``` 10 | 11 | Make sure you have a web server: 12 | 13 | ``` 14 | npm install -g serve 15 | ``` 16 | 17 | Start a web server: 18 | 19 | ``` 20 | serve -p 8000 21 | ``` 22 | 23 | Browse to [http://localhost:8000](http://localhost:8000) 24 | -------------------------------------------------------------------------------- /angular2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngAxponents", 3 | "version": "1.0.0", 4 | "description": "Example Angular 2 Component", 5 | "main": "myapp.es6.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "gulp": "^3.8.11", 13 | "gulp-copy": "0.0.2", 14 | "gulp-sourcemaps": "^1.5.2", 15 | "gulp-traceur": "^0.17.1", 16 | "gulp-util": "^3.0.4", 17 | "through2": "^0.6.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /polymer/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Axponents-polymer", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/dylanb/Axponents", 5 | "description": "Accessible Polymer Components", 6 | "moduleType": [ 7 | "globals" 8 | ], 9 | "keywords": [ 10 | "Accessibility", 11 | "Polymer", 12 | "a11y" 13 | ], 14 | "authors": [ 15 | "Dylan Barrell" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ], 25 | "dependencies": { 26 | "polymer": "Polymer/polymer#~0.5.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /angular2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ARIA Menu 5 | 6 | 7 | 8 | 9 | 10 | before 11 | 12 | after 13 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /polymer/aria-menuitem.css: -------------------------------------------------------------------------------- 1 | :host; { 2 | box-sizing: border-box; 3 | } 4 | :host(:before), :host(:after) { 5 | box-sizing: inherit; 6 | } 7 | :host { 8 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | line-height:3.236em; 10 | float: left; 11 | width:100%; 12 | background: #999999; 13 | text-align: center; 14 | color:#fff; 15 | display:inline-block; 16 | } 17 | span { 18 | display: none; 19 | width: 100%; 20 | } 21 | :host.open { 22 | padding-bottom:0; 23 | } 24 | :host(:focus) { 25 | outline: none; 26 | border: 2px dotted black; 27 | } 28 | :host(:hover), 29 | :host(:focus) { 30 | background: #CCCCCC; 31 | color: #000000; 32 | } 33 | span.open { 34 | display:block; 35 | position:absolute; 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | angular2/quickstart 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Axponents", 3 | "version": "1.0.0", 4 | "description": "Accessible Web Components", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dylanb/Axponents.git" 12 | }, 13 | "keywords": [ 14 | "Accessibility", 15 | "Web", 16 | "Components", 17 | "Polymer", 18 | "Angularjs", 19 | "2", 20 | "Angular2", 21 | "Angularjs2", 22 | "JavaScript", 23 | "a11y" 24 | ], 25 | "author": "Dylan Barrell", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/dylanb/Axponents/issues" 29 | }, 30 | "homepage": "https://github.com/dylanb/Axponents" 31 | } 32 | -------------------------------------------------------------------------------- /angular2/myapp.es6.js: -------------------------------------------------------------------------------- 1 | import {bootstrap, ElementRef, Directive} from 'angular2/angular2'; 2 | import {ComponentAnnotation as Component, ViewAnnotation as View} from "angular2/angular2"; 3 | import {AriaMenubar} from 'ariamenubar'; 4 | import {AriaMenu} from 'ariamenu'; 5 | import {AriaMenuitem} from 'ariamenuitem'; 6 | 7 | // console.log(Directive) 8 | 9 | @Component({ 10 | selector:'my-app' 11 | }) 12 | @View({ 13 | templateUrl: 'myapp.html', 14 | directives:[AriaMenubar, AriaMenuitem, AriaMenu] 15 | }) 16 | class MyApp { 17 | elem:ElementRef; 18 | string:menuValue; 19 | constructor(el: ElementRef) { 20 | this.elem = el; 21 | this.menuValue = 'nothing'; 22 | } 23 | handleMenuChange() { 24 | this.menuValue = this.elem.domElement.querySelector('aria-menubar').getAttribute('value'); 25 | console.log("application menu changed", this.menuValue); 26 | } 27 | } 28 | 29 | bootstrap(MyApp); 30 | 31 | -------------------------------------------------------------------------------- /polymer/aria-menubar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dylan Barrell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /angular2/aria-combined.css: -------------------------------------------------------------------------------- 1 | /* 2 | * aria-menuitem styles 3 | */ 4 | aria-menuitem { 5 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 6 | line-height:3.236em; 7 | float: left; 8 | width:100%; 9 | background: #999999; 10 | text-align: center; 11 | color:#fff; 12 | display:inline-block; 13 | } 14 | aria-menuitem[aria-expanded=true] { 15 | padding-bottom:0; 16 | } 17 | aria-menuitem:focus { 18 | outline: none; 19 | } 20 | 21 | aria-menuitem:hover, 22 | aria-menuitem:focus { 23 | background: #CCCCCC; 24 | color: #000000; 25 | } 26 | 27 | aria-menu:hover aria-menuitem:focus { 28 | background: #999999; 29 | color:#fff; 30 | } 31 | 32 | aria-menu:hover aria-menuitem:hover { 33 | background: #CCCCCC; 34 | color: #000000; 35 | } 36 | 37 | /* 38 | * aria-menu styles 39 | */ 40 | aria-menu { 41 | padding: 0; 42 | margin: 0; 43 | float:left; 44 | width:100%; 45 | display: none; 46 | width: 100%; 47 | } 48 | 49 | aria-menu.open { 50 | display:block; 51 | position:absolute; 52 | } 53 | 54 | /* 55 | * aria-menubar styles 56 | */ 57 | aria-menubar { 58 | padding: 0; 59 | margin: 0; 60 | width:100%; 61 | display:block; 62 | } 63 | aria-menubar:after { 64 | content: ""; 65 | display:table; 66 | clear:both; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /angular2/myapp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | {{menuValue}} 36 |
37 | -------------------------------------------------------------------------------- /angular2/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gulpTraceur = require('gulp-traceur'); 3 | var through2 = require('through2'); 4 | var gutil = require('gulp-util'); 5 | var sourcemaps = require('gulp-sourcemaps'); 6 | var copy = require('gulp-copy'); 7 | 8 | var log = function () { 9 | return through2.obj( 10 | function (file, encoding, cb) { 11 | console.log(file.path); 12 | // for (var att in file) { 13 | // console.log('attribute: ', att); 14 | // } 15 | this.push(file); 16 | cb(); 17 | }, function (cb) { 18 | cb(); 19 | }); 20 | }; 21 | 22 | gulp.task('compile',function() { 23 | return gulp.src("./*.es6.js") 24 | .pipe(sourcemaps.init()) 25 | .pipe(gulpTraceur({ 26 | sourceMaps: true, 27 | outputLanguage: 'es5', 28 | annotations: true, // parse annotations 29 | types: true, // parse types 30 | script: false, // parse as a module 31 | memberVariables: true, // parse class fields 32 | modules: 'instantiate' 33 | })) 34 | .pipe(sourcemaps.write('.')) 35 | .pipe(gulp.dest('dist')); 36 | }); 37 | 38 | gulp.task('copy', function () { 39 | return gulp.src(['*.html', '*.css']) 40 | .pipe(copy('dist')); 41 | }); 42 | 43 | gulp.task('watch', function () { 44 | gulp.watch(['*.html', '*.css', './*.es6.js'], ['compile', 'copy']); 45 | }); 46 | 47 | 48 | gulp.task('default', ['compile', 'copy', 'watch']); 49 | -------------------------------------------------------------------------------- /native/aria-menubar.html: -------------------------------------------------------------------------------- 1 | 24 | 63 | -------------------------------------------------------------------------------- /angular2/ariamenubar.es6.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'angular2/angular2'; 2 | import {ComponentAnnotation as Component, ViewAnnotation as View} from "angular2/angular2"; 3 | import {AriaMenuitem} from 'ariamenuitem'; 4 | 5 | var KEY_LEFT = 37; 6 | var KEY_UP = 38; 7 | var KEY_RIGHT = 39; 8 | var KEY_DOWN = 40; 9 | var KEY_ENTER = 13; 10 | 11 | @Component({ 12 | selector: 'aria-menubar', 13 | events: ['change'], 14 | hostListeners: { 15 | '^keydown': 'onKeydown($event)', 16 | '^click': 'onClick($event)' 17 | }, 18 | hostProperties: { 19 | 'role': 'attr.role', 20 | 'value': 'attr.value' 21 | } 22 | }) 23 | @View({ 24 | template: '' 25 | }) 26 | export class AriaMenubar { 27 | children: Array; 28 | change:EventEmitter; 29 | 30 | _value: string; 31 | 32 | role: string; 33 | 34 | constructor() { 35 | var link; 36 | this.change = new EventEmitter(); 37 | this.children = []; 38 | this.role = 'menubar'; 39 | } 40 | /* 41 | * API 42 | */ 43 | registerChild(child) { 44 | this.children.push(child); 45 | var numChildren = this.children.length; 46 | // set the width of the children to responsively fill the 47 | // whole menu bar 48 | this.children.forEach(function (child) { 49 | child.width = 99/numChildren + '%' 50 | }); 51 | return (numChildren === 1); 52 | } 53 | setSelected(child) { 54 | this.children.forEach(function (ch) { 55 | if (ch !== child) { 56 | ch.selected = false; 57 | } 58 | }); 59 | } 60 | /* 61 | * Event handlers 62 | */ 63 | onClick(e) { 64 | this.openOrSelect(e); 65 | } 66 | openOrSelect (e) { 67 | this.children.forEach(function (child) { 68 | if (child.isMyDomOrLabel(e.target) && !child.selected) { 69 | child.selected = true; 70 | } else if (child.isMyDomOrLabel(e.target)) { 71 | child.selected = false; 72 | } else if (child.hasMenu) { 73 | child.selected = false; 74 | } 75 | }); 76 | e.preventDefault(); 77 | e.stopPropagation(); 78 | } 79 | onKeydown(e) { 80 | var which = e.which || e.keyCode; 81 | var handled = false; 82 | var keysWeHandle = [KEY_LEFT,KEY_RIGHT,KEY_UP,KEY_DOWN,KEY_ENTER]; 83 | 84 | if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) { 85 | return; 86 | } 87 | if (keysWeHandle.indexOf(which) !== -1) { 88 | switch(which) { 89 | case KEY_LEFT: 90 | this._focusPrev(e); 91 | handled = true; 92 | break; 93 | case KEY_RIGHT: 94 | this._focusNext(e); 95 | handled = true; 96 | break; 97 | case KEY_DOWN: 98 | case KEY_ENTER: 99 | this.openOrSelect(e); 100 | handled = true; 101 | break; 102 | } 103 | if (handled) { 104 | e.preventDefault(); 105 | e.stopPropagation(); 106 | } 107 | } 108 | } 109 | /* 110 | * Getters and setters 111 | */ 112 | /* 113 | * value property 114 | */ 115 | get value() { 116 | return this._value; 117 | } 118 | set value(value) { 119 | this._value = value; 120 | this.change.next(null); 121 | } 122 | /* 123 | * Helpers 124 | */ 125 | _focusPrev(e) { 126 | var index; 127 | var children = this.children.filter(function(child) { 128 | return child.visible; 129 | }); 130 | children.forEach(function (child, ind) { 131 | if (child.isMyDomOrLabel(e.target)) { 132 | index = ind; 133 | } 134 | }); 135 | children[index].removeFocus(); 136 | if (index > 0) { 137 | index -= 1; 138 | } else { 139 | index = children.length - 1; 140 | } 141 | children[index].takeFocus(); 142 | } 143 | _focusNext(e) { 144 | var index; 145 | var children = this.children.filter(function(child) { 146 | return child.visible; 147 | }); 148 | children.forEach(function (child, ind) { 149 | if (child.isMyDomOrLabel(e.target)) { 150 | index = ind; 151 | } 152 | }); 153 | children[index].removeFocus(); 154 | if (index < children.length - 1) { 155 | index += 1; 156 | } else { 157 | index = 0; 158 | } 159 | children[index].takeFocus(); 160 | } 161 | // these functions are required by the menuitem 162 | blur() {} 163 | focus() {} 164 | } 165 | -------------------------------------------------------------------------------- /polymer/aria-menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 192 | -------------------------------------------------------------------------------- /native/web_components.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 49 | 50 | 51 | 52 | 53 | 54 |
before
55 |
56 | 57 |
58 | 71 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
between
120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 |
after
161 | 170 | 171 | -------------------------------------------------------------------------------- /angular2/ariamenuitem.es6.js: -------------------------------------------------------------------------------- 1 | import {ElementRef} from 'angular2/angular2'; 2 | import {Parent} from 'angular2/src/core/annotations_impl/visibility'; 3 | import {Optional} from 'angular2/src/di/annotations_impl'; 4 | import {Attribute} from 'angular2/src/core/annotations_impl/di'; 5 | import {ComponentAnnotation as Component, ViewAnnotation as View} from "angular2/angular2"; 6 | import {AriaMenu} from 'ariamenu'; 7 | import {AriaMenubar} from 'ariamenubar'; 8 | 9 | // Annotation section 10 | @Component({ 11 | selector: 'aria-menuitem', 12 | hostListeners: { 13 | '^blur': 'handleBlur($event)', 14 | '^focus': 'handleFocus($event)' 15 | }, 16 | hostProperties: { 17 | 'role': 'attr.role', 18 | 'value': 'attr.value', 19 | 'tabindex': 'tabindex', 20 | 'expanded': 'attr.aria-expanded', 21 | 'haspopup': 'attr.aria-haspopup', 22 | 'styleWidth': 'style.width' 23 | } 24 | }) 25 | @View({ 26 | template: '' 27 | }) 28 | export class AriaMenuitem { 29 | parent:any; 30 | 31 | domElement:any; 32 | 33 | menu:any; 34 | 35 | _hasMenu:Boolean; 36 | 37 | _selected:Boolean; 38 | 39 | _value: string; 40 | 41 | role: string; 42 | 43 | expanded: boolean; 44 | 45 | haspopup: boolean; 46 | 47 | tabindex: number; 48 | 49 | styleWidth: string; 50 | 51 | constructor(el: ElementRef, 52 | @Attribute('value') value:string, 53 | @Optional() @Parent() parentMenu: AriaMenu, 54 | @Optional() @Parent() parentMenubar: AriaMenubar) { 55 | this.domElement = el.domElement; 56 | this.parent = (parentMenu !== null) ? parentMenu : parentMenubar; 57 | 58 | this.role = 'menuitem'; 59 | 60 | this._hasMenu = false; 61 | this._selected = false; 62 | this._value = value; 63 | 64 | if (this.parent.registerChild(this)) { 65 | this.tabindex = 0; 66 | } else { 67 | this.tabindex = -1; 68 | } 69 | 70 | } 71 | /* 72 | * Event handlers 73 | */ 74 | handleBlur(e) { 75 | this.parent.blur(); 76 | } 77 | handleFocus(e) { 78 | this.parent.focus(); 79 | } 80 | /* 81 | * API 82 | */ 83 | registerChild(child) { 84 | this.menu = child; 85 | this.hasMenu = true; 86 | } 87 | isMyDomElement(domElement:any) { 88 | var retVal = (this.domElement === domElement); 89 | return retVal; 90 | } 91 | isMyDomOrLabel(domElement:any) { 92 | var retVal = ((this.domElement === domElement) || 93 | (this.domElement.querySelector('label') === domElement)); 94 | return retVal; 95 | } 96 | close(dontFocus) { 97 | this.menu.removeFocus(); 98 | 99 | this.tabindex = 0; 100 | this.expanded = false; 101 | if (!dontFocus) { 102 | this.domElement.focus(); 103 | } else { 104 | this._selected = false; 105 | } 106 | } 107 | open() { 108 | function getElementCoordinates(node) { 109 | var coords = { 110 | "top": 0, "right": 0, "bottom": 0, "left": 0, "width": 0, "height": 0 111 | }, 112 | xOffset, yOffset, rect; 113 | 114 | if (node) { 115 | xOffset = window.scrollX; 116 | yOffset = window.scrollY; 117 | rect = node.getBoundingClientRect(); 118 | 119 | coords = { 120 | "top": rect.top + yOffset, 121 | "right": rect.right + xOffset, 122 | "bottom": rect.bottom + yOffset, 123 | "left": rect.left + xOffset, 124 | "width": rect.right - rect.left, 125 | "height": rect.bottom - rect.top 126 | }; 127 | } 128 | return coords; 129 | } 130 | this.tabindex = -1; 131 | this.expanded = true; 132 | 133 | var coords = getElementCoordinates(this.domElement); 134 | this.menu.takeFocus(coords.width, coords.left, coords.top, coords.height); 135 | } 136 | closeMenu() { 137 | this.close(); 138 | } 139 | takeFocus() { 140 | this.tabindex = 0; 141 | this.domElement.focus(); 142 | } 143 | removeFocus() { 144 | this.tabindex = -1; 145 | } 146 | selectAndClose() { 147 | this.selected = true; // this will close the menu 148 | this.parent.setSelected(this); // reset all peer elements to not selected 149 | } 150 | /* 151 | * Getters and Setters 152 | */ 153 | /* 154 | * selected property 155 | */ 156 | set selected(value) { 157 | if (value === true) { 158 | if (this.hasMenu) { 159 | if (!this.menu.visible) { 160 | this.open(); 161 | this._selected = true; 162 | this.value = ''; 163 | } else { 164 | this.close(); 165 | this._selected = false; 166 | } 167 | } else { 168 | this.tabindex = 0; 169 | this.domElement.focus(); 170 | this._selected = true; 171 | this.parent.value = this.value; 172 | this.parent.setSelected(this); 173 | } 174 | } else { 175 | if (this.hasMenu && this.menu.visible) { 176 | this.close(); 177 | } else { 178 | this.tabindex = -1; 179 | } 180 | this._selected = false; 181 | } 182 | } 183 | get selected() { 184 | return this._selected; 185 | } 186 | /* 187 | * value property 188 | */ 189 | set value(value) { 190 | this._value = value; 191 | if (value !== '') { 192 | this.parent.value = value; //cascade 193 | } 194 | } 195 | get value() { 196 | return this._value; 197 | } 198 | /* 199 | * width property 200 | */ 201 | set width(value) { 202 | this.styleWidth = value; 203 | } 204 | /* 205 | * hasMenu property 206 | */ 207 | get hasMenu() { 208 | return this._hasMenu; 209 | } 210 | set hasMenu(value) { 211 | if (value === true) { 212 | this.expanded = false; 213 | } 214 | this.haspopup = value; 215 | this._hasMenu = value; 216 | } 217 | /* 218 | * visible property 219 | */ 220 | get visible() { 221 | return !(!this.domElement.offsetWidth || !this.domElement.offsetHeight) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /angular2/ariamenu.es6.js: -------------------------------------------------------------------------------- 1 | import {ElementRef, EventEmitter} from 'angular2/angular2'; 2 | import {Parent} from 'angular2/src/core/annotations_impl/visibility'; 3 | import {Optional} from 'angular2/src/di/annotations_impl'; 4 | import {ComponentAnnotation as Component, ViewAnnotation as View} from "angular2/angular2"; 5 | import {AriaMenuitem} from 'ariamenuitem'; 6 | 7 | var KEY_LEFT = 37; 8 | var KEY_UP = 38; 9 | var KEY_RIGHT = 39; 10 | var KEY_DOWN = 40; 11 | var KEY_ENTER = 13; 12 | var KEY_ESC = 27; 13 | var blurTimer; 14 | 15 | @Component({ 16 | selector: 'aria-menu', 17 | events: ['change'], 18 | hostListeners: { 19 | '^click': 'handleClick($event)', 20 | '^keydown': 'handleKeyDown($event)' 21 | }, 22 | hostProperties: { 23 | 'role': 'attr.role', 24 | 'value': 'attr.value', 25 | 'styleLeft': 'style.left', 26 | 'styleTop': 'style.top', 27 | 'styleWidth': 'style.width', 28 | 'open': 'class.open' 29 | } 30 | }) 31 | @View({ 32 | template: '' 33 | }) 34 | export class AriaMenu { 35 | children: Array; 36 | 37 | parent:any; 38 | 39 | domElement:any; 40 | 41 | blurTimer:any; 42 | 43 | change:EventEmitter; 44 | 45 | role: string; 46 | 47 | _value: string; 48 | 49 | styleWidth: string; 50 | styleTop: string; 51 | styleLeft: string; 52 | 53 | constructor(el: ElementRef, 54 | @Optional() @Parent() parentMenuitem: AriaMenuitem) { 55 | 56 | // remember our DOM element 57 | this.domElement = el.domElement; 58 | 59 | this.change = new EventEmitter(); 60 | this.parent = parentMenuitem; 61 | if (this.parent) { 62 | this.parent.registerChild(this); 63 | } 64 | this.children = []; 65 | 66 | this.role = 'menu'; 67 | } 68 | /* 69 | * Our API 70 | */ 71 | registerChild(child) { 72 | this.children.push(child); 73 | return (this.parent && this.children.length === 1); 74 | } 75 | takeFocus(width, left, top, height) { 76 | var that = this; 77 | this.value = ''; 78 | this.open = true; 79 | this.styleWidth = width + 'px'; 80 | this.styleLeft = left + 'px'; 81 | this.styleTop = top + height + 'px'; 82 | this.children.forEach(function (child) { 83 | child.removeFocus(); 84 | }); 85 | setTimeout(function () { 86 | // required because the class changes are async 87 | // see https://github.com/angular/angular/issues/2090 88 | that.children[0].takeFocus(); 89 | }, 0); 90 | } 91 | removeFocus() { 92 | this.open = false; 93 | } 94 | setSelected(child) { 95 | this.children.forEach(function (ch) { 96 | if (ch !== child) { 97 | ch.selected = false; 98 | } 99 | }); 100 | if (this.parent !== null) { 101 | this.parent.selectAndClose(); 102 | } 103 | } 104 | close(dontFocus) { 105 | this.parent.close(dontFocus); 106 | } 107 | /* 108 | * Helper functions 109 | */ 110 | _focusNext(e) { 111 | var index; 112 | var children = this.children.filter(function(child) { 113 | return child.visible; 114 | }); 115 | children.forEach(function (child, ind) { 116 | if (child.isMyDomOrLabel(e.target)) { 117 | index = ind; 118 | } 119 | }); 120 | children[index].removeFocus(); 121 | if (index < children.length - 1) { 122 | index += 1; 123 | } else { 124 | index = 0; 125 | } 126 | children[index].takeFocus(); 127 | } 128 | _focusPrev(e) { 129 | var index; 130 | var children = this.children.filter(function(child) { 131 | return child.visible; 132 | }); 133 | children.forEach(function (child, ind) { 134 | if (child.isMyDomOrLabel(e.target)) { 135 | index = ind; 136 | } 137 | }); 138 | children[index].removeFocus(); 139 | if (index > 0) { 140 | index -= 1; 141 | } else { 142 | index = children.length - 1; 143 | } 144 | children[index].takeFocus(); 145 | } 146 | /* 147 | * event handlers 148 | */ 149 | handleClick(e) { 150 | var children = this.children.filter(function (child) { 151 | return child.visible; 152 | }); 153 | children.forEach(function (child) { 154 | if (child.isMyDomOrLabel(e.target)) { 155 | child.selected = true; 156 | child.takeFocus(); 157 | } else { 158 | child.selected = false; 159 | child.removeFocus(); 160 | } 161 | }); 162 | e.stopPropagation(); // stop clicks from going outside 163 | e.preventDefault(); 164 | } 165 | handleKeyDown(e) { 166 | var which = e.which || e.keyCode; 167 | var handled = false; 168 | var keysWeHandle = [KEY_RIGHT,KEY_LEFT,KEY_ESC,KEY_UP,KEY_DOWN,KEY_ENTER]; 169 | 170 | if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) { 171 | return; 172 | } 173 | if (keysWeHandle.indexOf(which) !== -1) { 174 | switch(which) { 175 | case KEY_UP: 176 | this._focusPrev(e); 177 | handled = true; 178 | break; 179 | case KEY_RIGHT: 180 | // TODO: handle sub-sub-menu 181 | handled = true; 182 | break; 183 | case KEY_LEFT: 184 | case KEY_ESC: 185 | this.close(); 186 | handled = true; 187 | break; 188 | case KEY_DOWN: 189 | this._focusNext(e); 190 | handled = true; 191 | break; 192 | case KEY_ENTER: 193 | this.handleClick(e); 194 | handled = true; 195 | break; 196 | } 197 | if (handled) { 198 | e.preventDefault(); 199 | e.stopPropagation(); 200 | } 201 | } 202 | } 203 | blur() { 204 | var that = this; 205 | // TODO: commented out until this bug gets fixed https://github.com/angular/angular/issues/1050 206 | if (!this.blurTimer) { 207 | this.blurTimer = setTimeout(function () { 208 | that.close(true); 209 | this.blurTimer = undefined; 210 | }, 500); 211 | } 212 | } 213 | focus() { 214 | if (this.blurTimer) { 215 | clearTimeout(this.blurTimer); 216 | this.blurTimer = undefined; 217 | } 218 | } 219 | /* 220 | * Property getters and setters 221 | */ 222 | /* 223 | * value property 224 | */ 225 | set value(value) { 226 | this._value = value; 227 | if (value !== '') { 228 | if (this.parent === null) { 229 | // If the menu is standalone 230 | this.change.next(null); 231 | } else { 232 | // cascade the value 233 | this.parent.value = value; 234 | } 235 | } 236 | } 237 | get value() { 238 | return this._value; 239 | } 240 | /* 241 | * visible property 242 | */ 243 | get visible() { 244 | return !(!this.domElement.offsetWidth || !this.domElement.offsetHeight) 245 | } 246 | } 247 | 248 | -------------------------------------------------------------------------------- /native/aria-menu.html: -------------------------------------------------------------------------------- 1 | 11 | 28 | 224 | -------------------------------------------------------------------------------- /polymer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Examples of Accessible Polymer ARIA Menus 9 | 10 | 87 | 88 | 89 |

Examples of Accessible Polymer ARIA Menus

90 |

91 | This page contains examples of an accessible Polymer ARIA menu component. The first one is a popup menu activated by the "popup" button. The popup example also shows how to custom-style a web component within an emulated and native web components environment. The links between the examples allow for easy testing of focus management. 92 |

93 |
before
94 |
95 | 96 |
97 | 110 | 120 |
between
121 |

122 | This next example shows a two-level menu with a menu bar. It is using the default styling of the component and is using the full screen width. 123 |

124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 |
between
163 |

164 | This final example shows the same two-level menu. It is using custom styling of and is showing responsive sizing of the component (note the sub-menu does not respond to screen size changes dynamically, but only on page load). 165 |

166 |
167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 |
206 |
after
207 | 208 | -------------------------------------------------------------------------------- /polymer/aria-menuitem.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 307 | -------------------------------------------------------------------------------- /native/aria-menuitem.html: -------------------------------------------------------------------------------- 1 | 47 | 383 | --------------------------------------------------------------------------------