Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(qsa, flatten-tree): abstract Node and Element apis in virtual tree #1562

Merged
merged 11 commits into from
May 24, 2019
2 changes: 1 addition & 1 deletion lib/checks/keyboard/focusable-disabled.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const elementsThatCanBeDisabled = [
'TEXTAREA'
];

const tabbableElements = virtualNode.tabbableElements;
const tabbableElements = virtualNode.tabbableElements();

if (!tabbableElements || !tabbableElements.length) {
return true;
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/keyboard/focusable-not-tabbable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const elementsThatCanBeDisabled = [
'TEXTAREA'
];

const tabbableElements = virtualNode.tabbableElements;
const tabbableElements = virtualNode.tabbableElements();

if (!tabbableElements || !tabbableElements.length) {
return true;
Expand Down
2 changes: 1 addition & 1 deletion lib/commons/dom/get-tabbable-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dom.getTabbableElements = function getTabbableElements(virtualNode) {
const nodeAndDescendents = axe.utils.querySelectorAll(virtualNode, '*');

const tabbableElements = nodeAndDescendents.filter(vNode => {
const isFocusable = vNode.isFocusable;
const isFocusable = vNode.isFocusable();
let tabIndex = vNode.actualNode.getAttribute('tabindex');
tabIndex =
tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null;
Expand Down
71 changes: 71 additions & 0 deletions lib/core/base/virtual-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Wrap the real node and provide list of the flattened children
*
* @param node {Node} - the node in question
* @param shadowId {String} - the ID of the shadow DOM to which this node belongs
*/
function VirtualNode(node, shadowId) {
this.shadowId = shadowId;
this.children = [];
this.actualNode = node;

this._isHidden = null; // will be populated by axe.utils.isHidden
this._cache = {};

// abstract Node and Element APIs so we can run axe in DOM-less
// environments. these are static properties in the assumption
// that axe does not change any of them while it runs.
this.elementNodeType = node.nodeType;
this.elementNodeName = node.nodeName.toLowerCase();
this.elementId = node.id;

if (axe._cache.get('nodeMap')) {
axe._cache.get('nodeMap').set(node, this);
}
}

/**
* Determine if the actualNode has the given class name.
* @see https://j11y.io/jquery/#v=2.0.3&fn=jQuery.fn.hasClass
* @param {String} className - The class to check for.
* @return {Boolean} True if the actualNode has the given class, false otherwise.
*/
VirtualNode.prototype.hasClass = function(className) {
if (typeof this.actualNode.className !== 'undefined') {
let selector = ' ' + className + ' ';
return (' ' + this.actualNode.className + ' ').indexOf(selector) >= 0;
}
};

/**
* Get the value of the given attribute name.
* @param {String} attrName - The name of the attribute.
* @returns {String} The value of the attribute
*/
VirtualNode.prototype.attr = function(attrName) {
if (typeof this.actualNode.getAttribute !== 'undefined') {
return this.actualNode.getAttribute(attrName);
}
};

/**
* Determine if the element is focusable and cache the result.
* @return {Boolean} True if the element is focusable, false otherwise.
*/
VirtualNode.prototype.isFocusable = function() {
if (!this._cache.hasOwnProperty('isFocusable')) {
this._cache.isFocusable = axe.commons.dom.isFocusable(this.actualNode);
}
return this._cache.isFocusable;
};

/**
* Return the list of tabbable elements for this element and cache the result.
* @returns {VirtualNode[]}
*/
VirtualNode.prototype.tabbableElements = function() {
if (!this._cache.hasOwnProperty('tabbableElements')) {
this._cache.tabbableElements = axe.commons.dom.getTabbableElements(this);
}
return this._cache.tabbableElements;
};
42 changes: 5 additions & 37 deletions lib/core/utils/flattened-tree.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global VirtualNode */
/*eslint no-use-before-define: 0*/
var axe = axe || { utils: {} };

Expand All @@ -19,39 +20,6 @@ var axe = axe || { utils: {} };
* the spec for this)
*/

/**
* Wrap the real node and provide list of the flattened children
*
* @param node {Node} - the node in question
* @param shadowId {String} - the ID of the shadow DOM to which this node belongs
* @return {Object} - the wrapped node
*/
function virtualDOMfromNode(node, shadowId) {
const vNodeCache = {};
const vNode = {
shadowId: shadowId,
children: [],
actualNode: node,
_isHidden: null, // will be populated by axe.utils.isHidden
get isFocusable() {
if (!vNodeCache.hasOwnProperty('_isFocusable')) {
vNodeCache._isFocusable = axe.commons.dom.isFocusable(node);
}
return vNodeCache._isFocusable;
},
get tabbableElements() {
if (!vNodeCache.hasOwnProperty('_tabbableElements')) {
vNodeCache._tabbableElements = axe.commons.dom.getTabbableElements(
this
);
}
return vNodeCache._tabbableElements;
}
};
axe._cache.get('nodeMap').set(node, vNode);
return vNode;
}

/**
* find all the fallback content for a <slot> and return these as an array
* this array will also include any #text nodes
Expand Down Expand Up @@ -98,7 +66,7 @@ function flattenTree(node, shadowId) {
if (axe.utils.isShadowRoot(node)) {
// generate an ID for this shadow root and overwrite the current
// closure shadowId with this value so that it cascades down the tree
retVal = virtualDOMfromNode(node, shadowId);
retVal = new VirtualNode(node, shadowId);
shadowId =
'a' +
Math.random()
Expand All @@ -125,21 +93,21 @@ function flattenTree(node, shadowId) {
if (false && styl.display !== 'contents') {
// intentionally commented out
// has a box
retVal = virtualDOMfromNode(node, shadowId);
retVal = new VirtualNode(node, shadowId);
retVal.children = realArray.reduce(reduceShadowDOM, []);
return [retVal];
} else {
return realArray.reduce(reduceShadowDOM, []);
}
} else {
if (node.nodeType === 1) {
retVal = virtualDOMfromNode(node, shadowId);
retVal = new VirtualNode(node, shadowId);
realArray = Array.from(node.childNodes);
retVal.children = realArray.reduce(reduceShadowDOM, []);
return [retVal];
} else if (node.nodeType === 3) {
// text
return [virtualDOMfromNode(node)];
return [new VirtualNode(node)];
}
return undefined;
}
Expand Down
23 changes: 9 additions & 14 deletions lib/core/utils/qsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,27 @@ var matchExpressions = function() {};

function matchesTag(node, exp) {
return (
node.nodeType === 1 &&
(exp.tag === '*' || node.nodeName.toLowerCase() === exp.tag)
node.elementNodeType === 1 &&
(exp.tag === '*' || node.elementNodeName === exp.tag)
);
}

function matchesClasses(node, exp) {
return (
!exp.classes ||
exp.classes.reduce((result, cl) => {
return result && (node.className && node.className.match(cl.regexp));
}, true)
);
return !exp.classes || exp.classes.every(cl => node.hasClass(cl.value));
}

function matchesAttributes(node, exp) {
return (
!exp.attributes ||
exp.attributes.reduce((result, att) => {
var nodeAtt = node.getAttribute(att.key);
var nodeAtt = node.attr(att.key);
return result && nodeAtt !== null && (!att.value || att.test(nodeAtt));
}, true)
);
}

function matchesId(node, exp) {
return !exp.id || node.id === exp.id;
return !exp.id || node.elementId === exp.id;
}

function matchesPseudos(target, exp) {
Expand Down Expand Up @@ -207,10 +202,10 @@ function createLocalVariables(nodes, anyLevel, thisLevel, parentShadowId) {

function matchesSelector(node, exp) {
return (
matchesTag(node.actualNode, exp[0]) &&
matchesClasses(node.actualNode, exp[0]) &&
matchesAttributes(node.actualNode, exp[0]) &&
matchesId(node.actualNode, exp[0]) &&
matchesTag(node, exp[0]) &&
matchesClasses(node, exp[0]) &&
matchesAttributes(node, exp[0]) &&
matchesId(node, exp[0]) &&
matchesPseudos(node, exp[0])
);
}
Expand Down
Loading