├── 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 |
5 |
6 |
7 |
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 | One
4 |
5 |
6 | One One
7 |
8 |
9 | One Two
10 |
11 |
12 | One Three
13 |
14 |
15 |
16 |
17 | Two
18 |
19 |
20 | Three
21 |
22 |
23 | Three One
24 |
25 |
26 | Three Two
27 |
28 |
29 | Three Three
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 |
55 |
56 |
57 |
58 |
71 |
81 |
119 |
120 |
121 |
159 |
160 |
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 |
94 |
95 |
96 |
97 |
110 |
120 |
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 |
162 |
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 |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/polymer/aria-menuitem.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
307 |
--------------------------------------------------------------------------------
/native/aria-menuitem.html:
--------------------------------------------------------------------------------
1 |
47 |
383 |
--------------------------------------------------------------------------------