Skip to content

Commit

Permalink
menu,menu-item: Improve a11y
Browse files Browse the repository at this point in the history
  • Loading branch information
hcodes committed Feb 26, 2015
1 parent 2f9ca2f commit a9a76af
Show file tree
Hide file tree
Showing 22 changed files with 152 additions and 22 deletions.
13 changes: 12 additions & 1 deletion common.blocks/menu-item/menu-item.bemhtml
Expand Up @@ -2,11 +2,22 @@ block('menu-item')(
js()(function() {
return { val : this.ctx.val };
}),
attrs()({ role : 'menuitem' }),
attrs()(function() {
var attrs = {
role : 'menuitem'
},
mods = this.mods;

mods.disabled && (attrs['aria-disabled'] = true);
mods.checked && (attrs['aria-checked'] = true);

return this.extend(applyNext(), attrs);
}),
match(this._menuMods).def()(function() {
var mods = this.mods;
mods.theme = mods.theme || this._menuMods.theme;
mods.disabled = mods.disabled || this._menuMods.disabled;

applyNext();
})
);
10 changes: 8 additions & 2 deletions common.blocks/menu-item/menu-item.bh.js
@@ -1,14 +1,20 @@
module.exports = function(bh) {
bh.match('menu-item', function(ctx, json) {
var menuMods = ctx.tParam('menuMods');
var menuMods = ctx.tParam('menuMods'),
attrs = {
role : 'menuitem'
};

menuMods && ctx.mods({
theme : menuMods.theme,
disabled : menuMods.disabled
});

ctx.mod('disabled') && (attrs['aria-disabled'] = true);
ctx.mod('checked') && (attrs['aria-checked'] = true);

ctx
.js({ val : json.val })
.attr('role', 'menuitem');
.attrs(attrs);
});
};
12 changes: 12 additions & 0 deletions common.blocks/menu-item/menu-item.js
Expand Up @@ -31,6 +31,18 @@ provide(BEMDOM.decl(this.name, /** @lends menu-item.prototype */{
'true' : function() {
this.__base.apply(this, arguments);
this.delMod('hovered');
this.domElem.attr('aria-disabled', 'true');
},
'' : function() {
this.__base.apply(this, arguments);
this.domElem.removeAttr('aria-disabled');
}
},

'checked' : {
'*' : function(modName, modValue) {
this.__base.apply(this, arguments);
this.domElem.attr('aria-checked', !!modValue);
}
}
},
Expand Down
29 changes: 27 additions & 2 deletions common.blocks/menu-item/menu-item.spec.js
@@ -1,7 +1,9 @@
modules.define(
'spec',
['menu-item', 'i-bem__dom', 'jquery', 'sinon', 'BEMHTML'],
function(provide, MenuItem, BEMDOM, $, sinon, BEMHTML) {
['menu-item', 'i-bem__dom', 'jquery', 'sinon', 'chai', 'BEMHTML'],
function(provide, MenuItem, BEMDOM, $, sinon, chai, BEMHTML) {

var expect = chai.expect;

describe('menu-item', function() {
var menuItem;
Expand Down Expand Up @@ -37,6 +39,29 @@ describe('menu-item', function() {
});
});

describe('disabled', function() {
it('should set "aria-disabled" attribute if disabled', function() {
var domItem = menuItem.domElem;
expect(domItem.attr('aria-disabled')).to.be.undefined;

menuItem.setMod('disabled', true);
domItem.attr('aria-disabled').should.be.equal('true');

menuItem.delMod('disabled');
expect(domItem.attr('aria-disabled')).to.be.undefined;
});
});

describe('checked', function() {
it('should set "aria-checked" attribute if checked', function() {
menuItem.setMod('checked', true);
menuItem.domElem.attr('aria-checked').should.be.equal('true');

menuItem.delMod('checked');
menuItem.domElem.attr('aria-checked').should.be.equal('false');
});
});

describe('events', function() {
it('emit event on pointer click if it is not disabled', function() {
var spy = sinon.spy();
Expand Down
@@ -0,0 +1,6 @@
({
block : 'menu-item',
mods : { theme : 'islands' },
val : 1,
content : 'content'
})
@@ -0,0 +1 @@
<div class="menu-item menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem">content</div>
@@ -0,0 +1,6 @@
({
block : 'menu-item',
mods : { theme : 'islands', disabled : true, checked : true },
val : 1,
content : 'content'
})
@@ -0,0 +1 @@
<div class="menu-item menu-item_checked menu-item_disabled menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem" aria-disabled="true" aria-checked="true">content</div>
20 changes: 16 additions & 4 deletions common.blocks/menu/menu.bemhtml
Expand Up @@ -13,7 +13,7 @@ block('menu')(
ctx.val.indexOf(val) > -1 :
ctx.val === val);
},
iterateItems = function(content) {
iterateItems = (function(content) {
var i = 0, itemOrGroup;
while(itemOrGroup = content[i++]) {
if(itemOrGroup.block === 'menu-item') {
Expand All @@ -26,7 +26,7 @@ block('menu')(
iterateItems(itemOrGroup.content);
}
}
};
}).bind(this);

if(!this.isArray(ctx.content)) throw Error('menu: content must be an array of the menu items');

Expand All @@ -44,8 +44,20 @@ block('menu')(
});
}),
attrs()(function() {
var attrs = { role : 'menu' };
this.mods.disabled || (attrs.tabindex = 0);
var ctx = this.ctx,
attrs = {
role : 'menu',
id : ctx.id,
'aria-label' : ctx.ariaLabel,
'aria-labelledby' : ctx.ariaLabelledBy
};

if(this.mods.disabled) {
attrs['aria-disabled'] = true;
} else {
attrs.tabindex = ctx.tabIndex || 0;
}

return attrs;
}),
js()(true),
Expand Down
15 changes: 13 additions & 2 deletions common.blocks/menu/menu.bh.js
Expand Up @@ -11,8 +11,19 @@ module.exports = function(bh) {
.tParam('menuMods', menuMods)
.mix({ elem : 'control' });

var attrs = { role : 'menu' };
ctx.mod('disabled') || (attrs.tabindex = 0);
var attrs = {
role : 'menu',
id : json.id,
'aria-label' : json.ariaLabel,
'aria-labelledby' : json.ariaLabelledBy
};

if(ctx.mod('disabled')) {
attrs['aria-disabled'] = true;
} else {
attrs.tabindex = json.tabIndex || 0;
}

ctx.attrs(attrs);

var firstItem,
Expand Down
14 changes: 12 additions & 2 deletions common.blocks/menu/menu.js
Expand Up @@ -4,8 +4,8 @@

modules.define(
'menu',
['i-bem__dom', 'control', 'keyboard__codes', 'menu-item'],
function(provide, BEMDOM, Control, keyCodes) {
['i-bem__dom', 'control', 'keyboard__codes', 'identify', 'menu-item'],
function(provide, BEMDOM, Control, keyCodes, identify) {

/** @const Number */
var TIMEOUT_KEYBOARD_SEARCH = 1500;
Expand All @@ -31,6 +31,8 @@ provide(BEMDOM.decl({ block : this.name, baseBlock : Control }, /** @lends menu.
time : 0
};

this._activedescendant = identify();

this.hasMod('focused') && this.bindToDoc('keydown', this._onKeyDown);
}
},
Expand Down Expand Up @@ -134,8 +136,16 @@ provide(BEMDOM.decl({ block : this.name, baseBlock : Control }, /** @lends menu.
_onItemHover : function(item) {
if(item.hasMod('hovered')) {
this._hoveredItem && this._hoveredItem.delMod('hovered');

var id = this._activedescendant;
item.domElem.attr('id', id);
this.domElem.attr('aria-activedescendant', id);

this._scrollToItem(this._hoveredItem = item);
} else if(this._hoveredItem === item) {
item.domElem.removeAttr('id');
this.domElem.removeAttr('aria-activedescendant');

this._hoveredItem = null;
}
},
Expand Down
11 changes: 10 additions & 1 deletion common.blocks/menu/menu.spec.js
Expand Up @@ -3,7 +3,8 @@ modules.define(
['menu', 'i-bem__dom', 'jquery', 'sinon', 'chai', 'keyboard__codes', 'BEMHTML'],
function(provide, Menu, BEMDOM, $, sinon, chai, keyCodes, BEMHTML) {

var expect = chai.expect;
var expect = chai.expect,
should = chai.should();

describe('menu', function() {
var menu, menuItems, menu2, menuItems2;
Expand Down Expand Up @@ -176,6 +177,14 @@ describe('menu', function() {
menuItems2[0].hasMod('hovered').should.be.true;
});

it('should change "aria-activedescendant" attribute if hovered item', function() {
var item = menuItems[1];
item.setMod('hovered');

var id = item.domElem.attr('id');
should.exist(id);
menu.domElem.attr('aria-activedescendant').should.be.equal(id);
});
});

describe('events', function() {
Expand Down
2 changes: 1 addition & 1 deletion common.blocks/menu/menu.tmpl-specs/10-default.html
@@ -1 +1 @@
<div class="menu menu_theme_islands menu_disabled menu__control i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu"><div class="menu-item menu-item_theme_islands menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem">item 1</div><div class="menu-item menu-item_theme_islands menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem">item 2</div></div>
<div class="menu menu_theme_islands menu_disabled menu__control i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu" aria-disabled="true"><div class="menu-item menu-item_theme_islands menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem" aria-disabled="true">item 1</div><div class="menu-item menu-item_theme_islands menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem" aria-disabled="true">item 2</div></div>
2 changes: 1 addition & 1 deletion common.blocks/menu/menu.tmpl-specs/20-theme-disabled.html
@@ -1 +1 @@
<div class="menu menu_focused menu__control i-bem" data-bem="{&quot;menu&quot;:{&quot;live&quot;:false}}" role="menu" tabindex="0"><div class="menu__group" role="group" aria-label="Group 1"><div class="menu__group-title" role="presentation">Group 1</div><div class="menu-item i-bem" data-bem="{&quot;menu-item&quot;:{}}" role="menuitem">item 1</div><div class="menu-item menu-item_type_link menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem"><a class="link link_disabled link__control i-bem" data-bem="{&quot;link&quot;:{&quot;url&quot;:&quot;#&quot;}}">Google</a></div></div></div>
<div class="menu menu_focused menu__control i-bem" data-bem="{&quot;menu&quot;:{&quot;live&quot;:false}}" role="menu" tabindex="0"><div class="menu__group" role="group" aria-label="Group 1"><div class="menu__group-title" role="presentation">Group 1</div><div class="menu-item i-bem" data-bem="{&quot;menu-item&quot;:{}}" role="menuitem">item 1</div><div class="menu-item menu-item_type_link menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem" aria-disabled="true"><a class="link link_disabled link__control i-bem" data-bem="{&quot;link&quot;:{&quot;url&quot;:&quot;#&quot;}}">Google</a></div></div></div>
2 changes: 1 addition & 1 deletion common.blocks/menu/menu.tmpl-specs/30-mode-radio-val.html
@@ -1,4 +1,4 @@
<div class="menu menu_mode_radio menu__control i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu" tabindex="0">
<div class="menu-item menu-item_checked i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem">item 1</div>
<div class="menu-item menu-item_checked i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem" aria-checked="true">item 1</div>
<div class="menu-item i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem">item 2</div>
</div>
19 changes: 19 additions & 0 deletions common.blocks/menu/menu.tmpl-specs/40-a11y.bemjson.js
@@ -0,0 +1,19 @@
({
block : 'menu',
mods : { theme : 'islands', disabled : true },
id : 'id',
ariaLabel : 'label',
ariaLabelledBy : 'id0',
content : [
{
block : 'menu-item',
val : 1,
content : 'item 1'
},
{
block : 'menu-item',
val : 2,
content : 'item 2'
}
]
})
1 change: 1 addition & 0 deletions common.blocks/menu/menu.tmpl-specs/40-a11y.html
@@ -0,0 +1 @@
<div class="menu menu_theme_islands menu_disabled menu__control i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu" aria-label="label" aria-labelledby="id0" aria-disabled="true" id=""><div class="menu-item menu-item_theme_islands menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem" aria-disabled="true">item 1</div><div class="menu-item menu-item_theme_islands menu-item_disabled i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem" aria-disabled="true">item 2</div></div>
@@ -1 +1 @@
<div class="select select_mode_radio select_theme_islands select_size_l select_focused i-bem" data-bem="{&quot;select&quot;:{&quot;name&quot;:&quot;select&quot;,&quot;optionsMaxHeight&quot;:100,&quot;live&quot;:false}}"><input class="select__control" type="hidden" name="select" value="2"/><button class="button button_size_l button_theme_islands button_focused button__control select__button i-bem" data-bem="{&quot;button&quot;:{&quot;live&quot;:false}}" role="button" type="button" id="1"><span class="button__text">second</span><i aria-hidden="true" class="icon select__tick"></i></button><div class="popup popup_theme_islands popup_autoclosable popup_target_anchor i-bem" data-bem="{&quot;popup&quot;:{&quot;directions&quot;:[&quot;bottom-left&quot;,&quot;bottom-right&quot;,&quot;top-left&quot;,&quot;top-right&quot;]}}"><div class="menu menu_size_l menu_theme_islands menu_mode_radio menu__control select__menu i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu"><div class="menu-item menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem">first</div><div class="menu-item menu-item_checked menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;text&quot;:&quot;second&quot;,&quot;val&quot;:2}}" role="menuitem"><i aria-hidden="true" class="icon icon_social_vk"></i>second</div></div></div></div>
<div class="select select_mode_radio select_theme_islands select_size_l select_focused i-bem" data-bem="{&quot;select&quot;:{&quot;name&quot;:&quot;select&quot;,&quot;optionsMaxHeight&quot;:100,&quot;live&quot;:false}}"><input class="select__control" type="hidden" name="select" value="2"/><button class="button button_size_l button_theme_islands button_focused button__control select__button i-bem" data-bem="{&quot;button&quot;:{&quot;live&quot;:false}}" role="button" type="button" id="1"><span class="button__text">second</span><i aria-hidden="true" class="icon select__tick"></i></button><div class="popup popup_theme_islands popup_autoclosable popup_target_anchor i-bem" data-bem="{&quot;popup&quot;:{&quot;directions&quot;:[&quot;bottom-left&quot;,&quot;bottom-right&quot;,&quot;top-left&quot;,&quot;top-right&quot;]}}"><div class="menu menu_size_l menu_theme_islands menu_mode_radio menu__control select__menu i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu"><div class="menu-item menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem">first</div><div class="menu-item menu-item_checked menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;text&quot;:&quot;second&quot;,&quot;val&quot;:2}}" role="menuitem" aria-checked="true"><i aria-hidden="true" class="icon icon_social_vk"></i>second</div></div></div></div>
@@ -1 +1 @@
<div class="select select_mode_radio select_theme_islands select_disabled i-bem" data-bem="{&quot;select&quot;:{}}"><input class="select__control" type="hidden" value="1" disabled="disabled"/><button class="button button_theme_islands button_disabled button__control select__button i-bem" data-bem="{&quot;button&quot;:{}}" role="button" type="button" disabled="disabled"><span class="button__text">first</span><i aria-hidden="true" class="icon select__tick"></i></button><div class="popup popup_theme_islands popup_autoclosable popup_target_anchor i-bem" data-bem="{&quot;popup&quot;:{&quot;directions&quot;:[&quot;bottom-left&quot;,&quot;bottom-right&quot;,&quot;top-left&quot;,&quot;top-right&quot;]}}"><div class="menu menu_theme_islands menu_disabled menu_mode_radio menu__control select__menu i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu"><div class="menu-item menu-item_checked menu-item_disabled menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem">first</div><div class="menu-item menu-item_disabled menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem">second</div></div></div></div>
<div class="select select_mode_radio select_theme_islands select_disabled i-bem" data-bem="{&quot;select&quot;:{}}"><input class="select__control" type="hidden" value="1" disabled="disabled"/><button class="button button_theme_islands button_disabled button__control select__button i-bem" data-bem="{&quot;button&quot;:{}}" role="button" type="button" disabled="disabled"><span class="button__text">first</span><i aria-hidden="true" class="icon select__tick"></i></button><div class="popup popup_theme_islands popup_autoclosable popup_target_anchor i-bem" data-bem="{&quot;popup&quot;:{&quot;directions&quot;:[&quot;bottom-left&quot;,&quot;bottom-right&quot;,&quot;top-left&quot;,&quot;top-right&quot;]}}"><div class="menu menu_theme_islands menu_disabled menu_mode_radio menu__control select__menu i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu" aria-disabled="true"><div class="menu-item menu-item_checked menu-item_disabled menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:1}}" role="menuitem" aria-checked="true" aria-disabled="true">first</div><div class="menu-item menu-item_disabled menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem" aria-disabled="true">second</div></div></div></div>
@@ -1 +1 @@
<div class="select select_mode_check select_theme_islands i-bem" data-bem="{&quot;select&quot;:{&quot;name&quot;:&quot;select&quot;,&quot;text&quot;:&quot;select-check&quot;}}"><input class="select__control" type="hidden" name="select" value="1"/><input class="select__control" type="hidden" name="select" value="2"/><button class="button button_theme_islands button_checked button__control select__button i-bem" data-bem="{&quot;button&quot;:{}}" role="button" type="button"><span class="button__text">checkedText, second</span><i aria-hidden="true" class="icon select__tick"></i></button><div class="popup popup_theme_islands popup_autoclosable popup_target_anchor i-bem" data-bem="{&quot;popup&quot;:{&quot;directions&quot;:[&quot;bottom-left&quot;,&quot;bottom-right&quot;,&quot;top-left&quot;,&quot;top-right&quot;]}}"><div class="menu menu_theme_islands menu_mode_check menu__control select__menu i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu"><div class="menu-item menu-item_checked menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;checkedText&quot;:&quot;checkedText&quot;,&quot;val&quot;:1}}" role="menuitem">first</div><div class="menu-item menu-item_checked menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem">second</div></div></div></div>
<div class="select select_mode_check select_theme_islands i-bem" data-bem="{&quot;select&quot;:{&quot;name&quot;:&quot;select&quot;,&quot;text&quot;:&quot;select-check&quot;}}"><input class="select__control" type="hidden" name="select" value="1"/><input class="select__control" type="hidden" name="select" value="2"/><button class="button button_theme_islands button_checked button__control select__button i-bem" data-bem="{&quot;button&quot;:{}}" role="button" type="button"><span class="button__text">checkedText, second</span><i aria-hidden="true" class="icon select__tick"></i></button><div class="popup popup_theme_islands popup_autoclosable popup_target_anchor i-bem" data-bem="{&quot;popup&quot;:{&quot;directions&quot;:[&quot;bottom-left&quot;,&quot;bottom-right&quot;,&quot;top-left&quot;,&quot;top-right&quot;]}}"><div class="menu menu_theme_islands menu_mode_check menu__control select__menu i-bem" data-bem="{&quot;menu&quot;:{}}" role="menu"><div class="menu-item menu-item_checked menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;checkedText&quot;:&quot;checkedText&quot;,&quot;val&quot;:1}}" role="menuitem" aria-checked="true">first</div><div class="menu-item menu-item_checked menu-item_theme_islands i-bem" data-bem="{&quot;menu-item&quot;:{&quot;val&quot;:2}}" role="menuitem" aria-checked="true">second</div></div></div></div>

0 comments on commit a9a76af

Please sign in to comment.