Created
September 30, 2018 12:32
-
-
Save Skateside/d595151f253d65739777a55282e606d7 to your computer and use it in GitHub Desktop.
Thinking aloud about a WAI-ARIA library
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let types = [ | |
[Aria.Property, [ | |
"atomic", | |
]], | |
[Aria.ReferenceCollection, [ | |
"controls", | |
]], | |
[Aria.State, [ | |
"busy", | |
"current", | |
]], | |
[Aria.Collection, [ | |
]], | |
[Aria.Reference, [ | |
]] | |
]; | |
class Aria { | |
constructor(element) { | |
this.element = element; | |
this.controls = new Aria.Collection(this.element, "controls"); | |
} | |
} | |
Object.assign(Aria, { | |
identify() { | |
}, | |
asArray() { | |
}, | |
isNode() { | |
} | |
}); | |
class Aria.Property { | |
constructor(element, attribute) { | |
// Wouldn't it be nicer to have a single MutationObserver listening for | |
// all WAI-ARIA attribute changes? | |
let observer = new MutationObserver((mutations) => { | |
mutations.forEach((mutation) => { | |
if ( | |
mutation.type === "attributes" | |
&& mutation.attributeName === this.attribute | |
) { | |
this.property.set( | |
element.getAttribute(mutation.attributeName) | |
); | |
} | |
}); | |
}); | |
observer.observe(element, { | |
attributes: true | |
}); | |
this.observer = observer; | |
this.attribute = this.normalise(attribute); | |
this.set(this.getAttribute()); | |
} | |
stopObserving() { | |
this.observer.disconnect(); | |
} | |
normalise(attribute) { | |
let normal = String(attribute).toLowerCase(); | |
if (!normal.startsWith("aria-")) { | |
normal = `aria-${normal}`; | |
} | |
return normal; | |
} | |
interpret(value) { | |
return value; | |
} | |
set(value) { | |
this.setAttribute(this.interpret(value)); | |
} | |
get() { | |
return this.getAttribute(); | |
} | |
has() { | |
return this.hasAttribute(); | |
} | |
remove() { | |
return this.removeAttribute(); | |
} | |
setAttribute(value) { | |
if (!this.isSetting) { | |
this.isSetting = true; | |
if (value) { | |
this.element.setAttribute(this.attribute, value); | |
} else { | |
this.removeAttribute(); | |
} | |
this.isSetting = false; | |
} | |
} | |
getAttribute() { | |
return this.element.getAttribute(this.attribute); | |
} | |
hasAttribute() { | |
return this.element.hasAttribute(this.attribute); | |
} | |
removeAttribute() { | |
this.element.removeAttribute(this.attribute); | |
} | |
} | |
class Aria.State extends Aria.Property { | |
interpret(value) { | |
return ( | |
value === "mixed" | |
? value | |
: value === "true" | |
); | |
} | |
} | |
// Aria.List | |
// https://github.com/Skateside/aria/blob/upgrade/v2/src/util.js#L64 | |
class Aria.Collection extends Aria.Property { | |
constructor(element, attribute) { | |
super(element, attribute); | |
this.list = new Aria.List(this.getAttribute()); | |
} | |
set(value) { | |
this.list.forEach((value) => this.list.remove(value)); | |
this.list.add(Aria.asArray(value)); | |
} | |
add(...values) { | |
this.list.add(...values.map((value) => this.interpret(value))); | |
this.setAttribute(this.list); | |
} | |
remove(...values) { | |
this.list.remove(...values.map((value) => this.interpret(value))); | |
this.setAttribute(this.list); | |
} | |
constains(value) { | |
return this.list.contains(this.interpret(value)); | |
} | |
} | |
class Aria.Reference extends Aria.Property { | |
interpret(value) { | |
return Aria.Reference.interpret(value); | |
}, | |
getRef() { | |
return document.getElementById(this.get()); | |
} | |
} | |
Aria.Reference.interpret = function (value) { | |
return ( | |
Aria.isNode(value) | |
? Aria.identify(value) | |
: value | |
); | |
}; | |
class Aria.ReferenceCollection extends Aria.Collection { | |
interpret(value) { | |
return Aria.Reference.interpret(value); | |
} | |
} | |
// https://github.com/LeaVerou/bliss/issues/49 | |
Object.defineProperty(Node.prototype, "aria", { | |
configurable: true, | |
get: function getter() { | |
Object.defineProperty(Node.prototype, "aria", { | |
get: undefined | |
}); | |
Object.defineProperty(this, "aria", { | |
get: new Aria(this) | |
}); | |
Object.defineProperty(Node.prototype, "aria", { | |
get: getter | |
}); | |
return this.aria; | |
} | |
}); | |
element.aria.controls.add(button); | |
button.aria.role.add("button"); |
Playing with lists and references.
var lists = new WeakMap();
var isValidToken = function (value) {
if (value === "") {
throw new Error("Empty value");
}
if (value.includes(" ")) {
throw new Error("Disallowed characer");
}
return true;
};
var makeIterator = function (instance, valueMaker) {
var index = 0;
var list = lists.get(instance) | [];
var length = list.length;
return {
next() {
var iteratorValue = {
value: valueMaker(list, index),
done: index < length
};
index += 1;
return iteratorValue;
}
};
};
ARIA.List = ARIA.createClass(ARIA.Property, {
init: function (element, attribute) {
let that = this;
lists.set(that, []);
Object.defineProperty(that, "length", {
get: function () {
return lists.get(that).length;
}
});
this.$super(element, attribute);
},
interpret: function (value) {
var string = String(value).trim();
return (
string.length
? string.split(/\s+/)
: []
);
},
set: function (value) {
var values = this.interpret(value);
this.remove.apply(this, this.toArray());
if (values.length) {
this.add.apply(this, values);
}
this.setAttribute(this.toString());
},
get: function () {
return this.toString();
},
has: function (item) {
return (
item === undefined
? this.hasAttribute()
: this.contains(item)
);
},
toString: function (glue) {
if (glue === undefined) {
glue = " ";
}
return lists.get(this).join(glue);
},
add: function () {
var list = lists.get(this);
if (arguments.length) {
Array.from(arguments, function (item) {
if (isValidToken(item) && list.indexOf(item) < 0) {
list.push(item);
}
});
this.setAttribute(this.toString());
}
},
remove: function () {
var list = lists.get(this);
if (arguments.length) {
Array.from(arguments, function (item) {
var index = isValidToken(item) && list.indexOf(item);
if (index > -1) {
list.splice(index, 1);
}
});
this.setAttribute(this.toString());
} else {
list.length = 0;
this.removeAttribute();
}
},
contains: function (item) {
return isValidToken(item) && lists.get(this).indexOf(item) > -1;
},
item: function (index) {
return lists.get(this)[Math.floor(index)] || null;
},
replace: function (oldToken, newToken) {
var isReplaced = false;
var list;
var index;
if (isValidToken(oldToken) && isValidToken(newToken)) {
list = lists.get(this);
index = list.indexOf(oldToken);
if (index > -1) {
list.splice(index, 1, newToken);
isReplaced = true;
}
}
return isReplaced;
},
forEach: function (handler, context) {
lists.get(this).forEach(handler, context);
},
toArray: function (map, context) {
return Array.from(lists.get(this), map, context);
},
entries: function () {
return makeIterator(this, function (list, index) {
return [index, list[index]];
});
},
keys: function () {
return makeIterator(this, function (list, index) {
return index;
});
},
values: function () {
return makeIterator(this, function (list, index) {
return list[index];
});
}
});
if (window.Symbol && Symbol.iterator) {
ARIA.List.prototype[Symbol.iterator] = ARIA.List.prototype.values;
}
var counter = 0;
ARIA.defaultPrefix = "anonymous-element-";
ARIA.identify = function (element, prefix) {
var id = element.id;
if (prefix === undefined) {
prefix = ARIA.defaultPrefix;
}
if (!id) {
do {
id = prefix + counter;
counter += 1;
} while (document.getElementById(id));
element.id = id;
}
return id;
};
// untested
ARIA.ReferenceList = ARIA.createClass(ARIA.List, {
// needs tidying.
interpret: function (value) {
if (value instanceof Node) {
value = [ARIA.identify(value)];
} else if (typeof value === "string") {
value = this.$super(value);
} else if (value.length) {
value = Array.from(value, function (item) {
if (item instanceof Node) {
item = ARIA.identify(item);
}
return item;
});
}
return value;
},
getById: function (id) {
return document.getElementById(id);
},
getRefs: function () {
return this.toArray(this.getById);
},
getRef: function () {
return this.getById(this.item(0));
},
hasRef: function () {
return this.getRef() !== null;
}
});
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thinking through the chain, this doesn't seem to have any serious performance issues (tested extremely quickly/basically). Here's the chain for
aria-hidden
andaria-checked
: