Skip to content

Instantly share code, notes, and snippets.

@beatak
Created January 22, 2016 21:18
Show Gist options
  • Save beatak/4b8ed159c2c95f77f789 to your computer and use it in GitHub Desktop.
Save beatak/4b8ed159c2c95f77f789 to your computer and use it in GitHub Desktop.
(function ($) {
/**
* utility plugin for working on aria-attributes.
* the way it's returning the value is same as .attr(), meaning
* getter only works for the first matched but setter will set
* all matched elements.
* aria attributes
*
* TODO
* - massage get/set val types, according to
* https://www.w3.org/TR/wai-aria/states_and_properties
* - validation for Token characters?
*/
/**
* ARIA value types:
* https://www.w3.org/TR/wai-aria/states_and_properties#propcharacteristic_value
* After all, representation is always string, but when you get or set, you wanna have it as the
*/
var ARIA_BOOLEAN = 'Boolean';
var ARIA_TRISTATE = 'String'; // this is to be equivalent to the visual queue of HTML checkbox
var ARIA_BOOLEAN_UNDEFINED = 'UndefinableBoolean'; // this is the more-computational tri-state, where undefined is usually the default, and means something
var ARIA_ID_REFERENCE = 'String';
var ARIA_ID_REFERENCE_LIST = 'Array';
var ARIA_INTEGER = 'Number';
var ARIA_NUMBER = 'Number';
var ARIA_STRING = 'String';
var ARIA_TOKEN = 'String';
var ARIA_TOKEN_LIST = 'Array';
/**
* Keys of ARIA_PROP are generated by the following code:
* https://www.w3.org/TR/wai-aria/states_and_properties
*
* [].map.apply(
* document.getElementById('index_state_prop')
* .querySelectorAll('dt'),
* [function (elm) {
* return elm.textContent.trim().substring(5).split(' ')[0];
* }]
* ).sort();
*/
var ARIA_PROP = {
'activedescendant': ARIA_ID_REFERENCE,
'atomic': ARIA_BOOLEAN,
'autocomplete': ARIA_TOKEN,
'busy': ARIA_BOOLEAN,
'checked': ARIA_TRISTATE,
'controls': ARIA_ID_REFERENCE_LIST,
'describedby': ARIA_ID_REFERENCE_LIST,
'disabled': ARIA_BOOLEAN,
'dropeffect': ARIA_TOKEN_LIST,
'expanded': ARIA_BOOLEAN_UNDEFINED,
'flowto': ARIA_ID_REFERENCE_LIST,
'grabbed': ARIA_BOOLEAN_UNDEFINED,
'haspopup': ARIA_BOOLEAN,
'hidden': ARIA_BOOLEAN,
'invalid': ARIA_TOKEN,
'label': ARIA_STRING,
'labelledby': ARIA_ID_REFERENCE_LIST,
'level': ARIA_INTEGER,
'live': ARIA_TOKEN,
'multiline': ARIA_BOOLEAN,
'multiselectable': ARIA_BOOLEAN,
'orientation': ARIA_TOKEN,
'owns': ARIA_ID_REFERENCE_LIST,
'posinset': ARIA_INTEGER,
'pressed': ARIA_TRISTATE,
'readonly': ARIA_BOOLEAN,
'relevant': ARIA_TOKEN_LIST,
'required': ARIA_BOOLEAN,
'selected': ARIA_BOOLEAN_UNDEFINED,
'setsize': ARIA_INTEGER,
'sort': ARIA_TOKEN,
'valuemax': ARIA_NUMBER,
'valuemin': ARIA_NUMBER,
'valuenow': ARIA_NUMBER,
'valuetext': ARIA_STRING
};
var validateKey = function (_k) {
var key = _k.toLowerCase();
if (ARIA_PROP[key] === undefined) {
throw new Error('Not legit key: ' + _k);
}
return key;
};
var checkTrue = function (str) {
return str.toLowerCase() === 'true' ? true : false;
};
// FIXME this is not that great
var normalizeVal = function (val, valtype) {
var result;
console.log(valtype + ': ' + val);
if (valtype === 'UndefinableBoolean') {
result = (val.toLowerCase() === 'undefined' ? undefined : checkTrue(val));
}
else if (valtype === 'Boolean') {
console.log('bool: ' + val);
result = checkTrue(val);
}
else if (valtype === 'Number') {
result = parseFloat(val);
}
else if(valtype === 'Array') {
result = val.split(' ');
}
else {
result = val;
}
console.log( ' ==> ' + result);
return result;
};
var ariaGetter = function (elm, _k) {
// FIXME massage value
var key = validateKey(_k);
var val = normalizeVal($(elm).attr('aria-' + key), ARIA_PROP[key]);
return val;
};
var ariaSetter = function (elm, _k, val) {
// FIXME massage value here
var key = validateKey(_k);
$(elm).attr('aria-' + key, '' + val);
return elm;
};
$.fn.aria = function (key, val) {
var result,
is_getting = (arguments.length === 1);
if (is_getting) {
result = ariaGetter(this[0], key);
}
else {
result = [];
$.each(this, function (i, elm) {
result.push(ariaSetter(elm, key, val));
});
result = $(result);
}
return result;
};
})(jQuery);
// ================================================================
// ================================================================
$(function () {
var $mainButton = $('#main-button-link');
var $popupContainer = $('#popup-container');
var $subnavContainer = $('#subnav-region-list2 [data-foo="menu"]');
var $thisButton = $('#this-button');
var $subnavTogglers = $('#subnav-region-list1 a');
var togglePopup = function () {
var expanded = $mainButton.aria('expanded');
$popupContainer.toggleClass('hide');
if (expanded) {
// closing
$mainButton.aria('expanded', false);
$popupContainer.removeAttr('tabindex');
}
else {
// opening
$mainButton.aria('expanded', true);
$popupContainer.attr('tabindex', '-1').focus();
}
};
var displaySubmenu = function (elm) {
var $elm = $(elm);
var target_id = $elm.aria('controls');
console.log('display submenu: ' + target_id);
$subnavTogglers
.aria('selected', false);
$elm.aria('selected', true);
$subnavContainer
.addClass('hide')
.removeAttr('tabindex');
$('#sub-nav-shop-menuitem-orders').aria('flowto', target_id);
$('#' + target_id)
.removeClass('hide')
.attr('tabindex', '-1')
.focus();
// .find('a:first').focus();
};
$subnavTogglers.on(
'click',
function (ev) {
ev.stopPropagation();
ev.preventDefault();
displaySubmenu(this);
}
);
$popupContainer.on('click', function () {
togglePopup();
});
$mainButton.on('click', function (ev) {
ev.preventDefault();
togglePopup();
});
$thisButton.on(
'click',
function (ev) {
ev.preventDefault();
ev.stopPropagation();
var msg = 'this button is useless.';
if (window.speechSynthesis) {
window.speechSynthesis.speak(new SpeechSynthesisUtterance(msg));
}
else {
console.log(msg);
}
}
);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment