Skip to content

Instantly share code, notes, and snippets.

@Skateside
Created September 30, 2018 12:32
Show Gist options
  • Save Skateside/d595151f253d65739777a55282e606d7 to your computer and use it in GitHub Desktop.
Save Skateside/d595151f253d65739777a55282e606d7 to your computer and use it in GitHub Desktop.
Thinking aloud about a WAI-ARIA library
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");
@james-jlo-long
Copy link

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