From 0cd509a2a5d2253faa1b9d083f70981bffc607b0 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Fri, 4 Oct 2019 11:52:50 -0600 Subject: [PATCH 01/14] feat(position): add way to get element stack --- lib/commons/position/index.js | 363 +++++++++++++++++++++ lib/core/base/virtual-node/virtual-node.js | 35 ++ 2 files changed, 398 insertions(+) create mode 100644 lib/commons/position/index.js diff --git a/lib/commons/position/index.js b/lib/commons/position/index.js new file mode 100644 index 0000000000..08b38370fd --- /dev/null +++ b/lib/commons/position/index.js @@ -0,0 +1,363 @@ +// split the page into 100px x 100px cells +const gridSize = 100; // TODO: change to const once removed from getBackgroundColor + +/** + * Determine if node produces a stacking context. + * References: + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context) + * https://github.com/gwwar/z-context/blob/master/devtools/index.js + * @param {VirtualNode} vNode + * @return {Boolean} + */ +function isStackingContext(vNode) { + const node = vNode.actualNode; + + //the root element (HTML) + if ( + !node || + node.nodeName === 'HTML' || + node.nodeName === '#document-fragment' + ) { + return true; + } + + const computedStyle = vNode.computedStyle; + + // position: fixed or sticky + if ( + computedStyle.position === 'fixed' || + computedStyle.position === 'sticky' + ) { + return true; + } + + // positioned (absolutely or relatively) with a z-index value other than "auto", + if (computedStyle.zIndex !== 'auto' && computedStyle.position !== 'static') { + return true; + } + + // elements with an opacity value less than 1. + if (computedStyle.opacity !== '1') { + return true; + } + + // elements with a transform value other than "none" + if (computedStyle.transform !== 'none') { + return true; + } + + // elements with a mix-blend-mode value other than "normal" + if (computedStyle.mixBlendMode !== 'normal') { + return true; + } + + // elements with a filter value other than "none" + if (computedStyle.filter !== 'none') { + return true; + } + + // elements with a perspective value other than "none" + if (computedStyle.perspective !== 'none') { + return true; + } + + // elements with isolation set to "isolate" + if (computedStyle.isolation === 'isolate') { + return true; + } + + // transform or opacity in will-change even if you don't specify values for these attributes directly + if ( + computedStyle.willChange === 'transform' || + computedStyle.willChange === 'opacity' + ) { + return true; + } + + // elements with -webkit-overflow-scrolling set to "touch" + if (computedStyle.webkitOverflowScrolling === 'touch') { + return true; + } + + // a flex item with a z-index value other than "auto", that is the parent element display: flex|inline-flex, + if (computedStyle.zIndex !== 'auto') { + var parentStyle = getComputedStyle(node.parentNode); + if ( + parentStyle.display === 'flex' || + parentStyle.display === 'inline-flex' + ) { + return true; + } + } + + return false; +} + +/** + * Visually sort nodes based on their stack order + * @param {VirtualNode} + * @param {VirtualNode} + */ +function visuallySort(a, b) { + /*eslint no-bitwise: 0 */ + for (let i = 0; i < a._stackingOrder.length; i++) { + if (typeof b._stackingOrder[i] === 'undefined') { + return -1; + } + + if (a._stackingOrder[i] === b._stackingOrder[i]) { + continue; + } + + if (b.stackOrder[i] > a.stackOrder[i]) { + return 1; + } else { + return -1; + } + } + + // same stack, return the later node + // TODO: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index + if (a.actualNode.compareDocumentPosition(b.actualNode) & 4) { + return 1; // a before b + } else { + return -1; // b before a + } +} + +/** + * Determine the stacking order of an element. The stacking order is an array of + * zIndex values for each stacking context parent. + * @param {VirtualNode} + * @return {Number[]} + */ +function getStackingOrder(vNode) { + const style = vNode.computedStyle; + const stackingOrder = vNode.parent + ? vNode.parent._stackingOrder.slice() + : [0]; + + if (style.zIndex !== 'auto') { + stackingOrder[stackingOrder.length - 1] = parseInt(style.zIndex); + } + if (isStackingContext(vNode)) { + stackingOrder.push(0); + } + + return stackingOrder; +} + +/** + * Determine if a node is a scroll region. + * @param {VirtualNode} + * @return {Boolean} + */ +function isScrollRegion(vNode) { + const style = vNode.computedStyle; + + return ( + style.overflowX === 'auto' || + style.overflowX === 'scroll' || + style.overflowY === 'auto' || + style.overflowY === 'scroll' + ); +} + +/** + * Return the parent node that is a scroll region. + * @param {VirtualNode} + * @return {VirtualNode|null} + */ +function findScrollRegionParent(vNode) { + let scrollRegionParent = null; + let vNodeParent = vNode.parent; + let checkedNodes = [vNode]; + + while (vNodeParent) { + if (vNodeParent._scrollRegionParent) { + scrollRegionParent = vNodeParent._scrollRegionParent; + break; + } + + if (isScrollRegion(vNodeParent)) { + scrollRegionParent = vNodeParent; + break; + } + + checkedNodes.push(vNodeParent); + vNodeParent = vNodeParent.parent; + } + + // cache result of parent scroll region so we don't have to look up the entire + // tree again for a child node + checkedNodes.forEach( + vNode => (vNode._scrollRegionParent = scrollRegionParent) + ); + return scrollRegionParent; +} + +/** + * Add a node to every cell of the grid it intersects with. + * @param {Grid} + * @param {VirtualNode} + */ +function addNodeToGrid(grid, vNode) { + // save a reference to where this element is in the grid so we + // can find it even if it's in a subgrid + vNode._grid = grid; + + vNode.clientRects.forEach(rect => { + const startRow = Math.floor(rect.y / gridSize); + const endRow = Math.floor((rect.y + rect.height) / gridSize); + const startCol = Math.floor(rect.x / gridSize); + const endCol = Math.floor((rect.x + rect.width) / gridSize); + + for (let row = startRow; row <= endRow; row++) { + grid.cells[row] = grid.cells[row] || []; + + for (let col = startCol; col <= endCol; col++) { + grid.cells[row][col] = grid.cells[row][col] || []; + + if (!grid.cells[row][col].includes(vNode)) { + grid.cells[row][col].push(vNode); + } + } + } + }); +} + +/** + * + */ +function createGrid() { + const rootGrid = { + container: axe._tree[0], + cells: [] + }; + + axe.utils + .querySelectorAll(axe._tree[0], 'html, body, body *') + .forEach(vNode => { + if (vNode.actualNode.nodeType !== 1) { + return; + } + + vNode._stackingOrder = getStackingOrder(vNode); + + // filter out any elements with 0 width or height + // (we don't do this before so we can calculate stacking context + // of parents with 0 width/height) + const rect = vNode.boundingClientRect; + if (rect.width === 0 || rect.height === 0) { + return; + } + + const scrollRegionParent = findScrollRegionParent(vNode); + const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; + + if (isScrollRegion(vNode)) { + const subGrid = { + container: vNode, + cells: [] + }; + vNode._subGrid = subGrid; + } + + addNodeToGrid(grid, vNode); + }); + + // sort the grid by stacking order + // for (let row = 0; row < grid.cells.length; row++) { + // for (let col = 0; col < grid.cells[row].length; col++) { + // grid.cells[row][col].sort(visuallySort); + // } + // } + + // return grid; +} + +const position = (commons.position = {}); + +position.getElementStack = function(vNode, recurrsed) { + if (!axe._cache.get('grid')) { + createGrid(); + axe._cache.set('grid', true); + } + + const grid = vNode._grid; + const boundingRect = vNode.boundingClientRect; + + // use center point of rect + const x = boundingRect.x + boundingRect.width / 2; + const y = boundingRect.y + boundingRect.height / 2; + + const row = Math.floor(y / gridSize); + const col = Math.floor(x / gridSize); + + let stack = grid.cells[row][col].filter(vNode => { + const rects = vNode.clientRects; + return rects.find(rect => { + return ( + x < rect.x + rect.width && + x > rect.x && + y < rect.y + rect.height && + y > rect.y + ); + }); + }); + + if ( + grid.container && + grid.container.actualNode !== document.documentElement + ) { + stack = position.getElementStack(grid.container, true).concat(stack); + } + + if (!recurrsed) { + stack.sort(visuallySort); + } + + return stack; +}; + +// position.elementFromPoint = function(x, y) { +// if(!axe._cache.get('grid')) { +// axe._cache.set('grid', position.createGrid()); +// } +// const grid = axe._cache.get('grid'); +// const row = Math.floor(y / gridSize); +// const col = Math.floor(x / gridSize); + +// return grid.cells[row][col].find(vNode => { +// const rects = vNode.clientRects; +// return rects.find(rect => { +// return ( +// x < rect.x + rect.width && +// x > rect.x && +// y < rect.y + rect.height && +// y > rect.y +// ); +// }); +// }); +// } + +// position.elementsFromPoint = function(x, y) { +// if(!axe._cache.get('grid')) { +// axe._cache.set('grid', position.createGrid()); +// } +// const grid = axe._cache.get('grid'); +// const row = Math.floor(y / gridSize); +// const col = Math.floor(x / gridSize); + +// return grid.cells[row][col].filter(vNode => { +// const rects = vNode.clientRects; +// return rects.find(rect => { +// return ( +// x < rect.x + rect.width && +// x > rect.x && +// y < rect.y + rect.height && +// y > rect.y +// ); +// }); +// }); +// } diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index f4aeb57f3a..405c49a798 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -82,4 +82,39 @@ class VirtualNode extends axe.AbstractVirtualNode { } return this._cache.tabbableElements; } + + /** + * Return the computed style for this element and cache the result. + * @return {CSSStyleDeclaration} + */ + get computedStyle() { + if (!this._cache.hasOwnProperty('computedStyle')) { + this._cache.computedStyle = window.getComputedStyle(this.actualNode); + } + return this._cache.computedStyle; + } + + /** + * Return the client rects for this element and cache the result. + * @return {DOMRect[]} + */ + get clientRects() { + if (!this._cache.hasOwnProperty('clientRects')) { + this._cache.clientRects = Array.from( + this.actualNode.getClientRects() + ).filter(rect => rect.width > 0); + } + return this._cache.clientRects; + } + + /** + * Return the bounding rect for this element and cache the result. + * @return {DOMRect} + */ + get boundingClientRect() { + if (!this._cache.hasOwnProperty('boundingClientRect')) { + this._cache.boundingClientRect = this.actualNode.getBoundingClientRect(); + } + return this._cache.boundingClientRect; + } } From 57a3513b9c7f9b2803824bd9177dc9441df5db1b Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 7 Oct 2019 16:55:40 -0600 Subject: [PATCH 02/14] finish visual sort, add tests --- .../index.js => dom/get-element-stack.js} | 124 ++++++------- lib/core/base/virtual-node.js | 171 ++++++++++++++++++ test/commons/dom/get-element-stack.js | 131 ++++++++++++++ 3 files changed, 355 insertions(+), 71 deletions(-) rename lib/commons/{position/index.js => dom/get-element-stack.js} (75%) create mode 100644 lib/core/base/virtual-node.js create mode 100644 test/commons/dom/get-element-stack.js diff --git a/lib/commons/position/index.js b/lib/commons/dom/get-element-stack.js similarity index 75% rename from lib/commons/position/index.js rename to lib/commons/dom/get-element-stack.js index 08b38370fd..492425c108 100644 --- a/lib/commons/position/index.js +++ b/lib/commons/dom/get-element-stack.js @@ -1,5 +1,7 @@ +/* global dom */ + // split the page into 100px x 100px cells -const gridSize = 100; // TODO: change to const once removed from getBackgroundColor +const gridSize = 100; /** * Determine if node produces a stacking context. @@ -93,6 +95,27 @@ function isStackingContext(vNode) { return false; } +/** + * Return the index order of how to position this element. return nodes in non-positioned, floating, positioned order + * Reference: + * - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index + * - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float + * @param {CSSStyleDeclaration} + * @return {Number} + */ +function getPositionOrder(style) { + if (style.float !== 'none') { + return 1; + } + + if (style.position === 'static') { + return 0; + } + + // positioned element + return 2; +} + /** * Visually sort nodes based on their stack order * @param {VirtualNode} @@ -109,20 +132,31 @@ function visuallySort(a, b) { continue; } - if (b.stackOrder[i] > a.stackOrder[i]) { + if (b._stackingOrder[i] > a._stackingOrder[i]) { return 1; } else { return -1; } } - // same stack, return the later node - // TODO: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index - if (a.actualNode.compareDocumentPosition(b.actualNode) & 4) { - return 1; // a before b - } else { - return -1; // b before a + // nodes are the same stacking order, compute special cases for when z-index + // is not set + const DOMOrder = + a.actualNode.compareDocumentPosition(b.actualNode) & 4 ? 1 : -1; + const aStyle = a.computedStyle; + const bStyle = b.computedStyle; + const aPosition = getPositionOrder(aStyle); + const bPosition = getPositionOrder(bStyle); + if (aStyle.zIndex === 'auto' && bStyle.zIndex === 'auto') { + if (aPosition === bPosition) { + return DOMOrder; + } + + return bPosition - aPosition; } + + // default to return nodes in DOM order + return DOMOrder; } /** @@ -231,12 +265,13 @@ function addNodeToGrid(grid, vNode) { */ function createGrid() { const rootGrid = { - container: axe._tree[0], + container: null, cells: [] }; axe.utils - .querySelectorAll(axe._tree[0], 'html, body, body *') + .querySelectorAll(axe._tree[0], '*') + .filter(vNode => vNode.actualNode.parentElement !== document.head) .forEach(vNode => { if (vNode.actualNode.nodeType !== 1) { return; @@ -265,23 +300,12 @@ function createGrid() { addNodeToGrid(grid, vNode); }); - - // sort the grid by stacking order - // for (let row = 0; row < grid.cells.length; row++) { - // for (let col = 0; col < grid.cells[row].length; col++) { - // grid.cells[row][col].sort(visuallySort); - // } - // } - - // return grid; } -const position = (commons.position = {}); - -position.getElementStack = function(vNode, recurrsed) { - if (!axe._cache.get('grid')) { +dom.getElementStack = function(vNode, recurrsed) { + if (!axe._cache.get('gridCreated')) { createGrid(); - axe._cache.set('grid', true); + axe._cache.set('gridCreated', true); } const grid = vNode._grid; @@ -306,58 +330,16 @@ position.getElementStack = function(vNode, recurrsed) { }); }); - if ( - grid.container && - grid.container.actualNode !== document.documentElement - ) { - stack = position.getElementStack(grid.container, true).concat(stack); + if (grid.container) { + stack = dom.getElementStack(grid.container, true).concat(stack); } if (!recurrsed) { stack.sort(visuallySort); } + // TODO: should we cache the result of the lookup for each node in the stack + // since it won't change for a parent element? + return stack; }; - -// position.elementFromPoint = function(x, y) { -// if(!axe._cache.get('grid')) { -// axe._cache.set('grid', position.createGrid()); -// } -// const grid = axe._cache.get('grid'); -// const row = Math.floor(y / gridSize); -// const col = Math.floor(x / gridSize); - -// return grid.cells[row][col].find(vNode => { -// const rects = vNode.clientRects; -// return rects.find(rect => { -// return ( -// x < rect.x + rect.width && -// x > rect.x && -// y < rect.y + rect.height && -// y > rect.y -// ); -// }); -// }); -// } - -// position.elementsFromPoint = function(x, y) { -// if(!axe._cache.get('grid')) { -// axe._cache.set('grid', position.createGrid()); -// } -// const grid = axe._cache.get('grid'); -// const row = Math.floor(y / gridSize); -// const col = Math.floor(x / gridSize); - -// return grid.cells[row][col].filter(vNode => { -// const rects = vNode.clientRects; -// return rects.find(rect => { -// return ( -// x < rect.x + rect.width && -// x > rect.x && -// y < rect.y + rect.height && -// y > rect.y -// ); -// }); -// }); -// } diff --git a/lib/core/base/virtual-node.js b/lib/core/base/virtual-node.js new file mode 100644 index 0000000000..a8976d3dce --- /dev/null +++ b/lib/core/base/virtual-node.js @@ -0,0 +1,171 @@ +const whitespaceRegex = /[\t\r\n\f]/g; + +class AbstractVirtualNode { + constructor() { + this.children = []; + this.parent = null; + } + + get props() { + throw new Error( + 'VirtualNode class must have a "props" object consisting ' + + 'of "nodeType" and "nodeName" properties' + ); + } + + hasClass() { + throw new Error('VirtualNode class must have a "hasClass" function'); + } + + attr() { + throw new Error('VirtualNode class must have a "attr" function'); + } + + hasAttr() { + throw new Error('VirtualNode class must have a "hasAttr" function'); + } +} + +// class is unused in the file... +// eslint-disable-next-line no-unused-vars +class VirtualNode extends AbstractVirtualNode { + /** + * Wrap the real node and provide list of the flattened children + * @param {Node} node the node in question + * @param {VirtualNode} parent The parent VirtualNode + * @param {String} shadowId the ID of the shadow DOM to which this node belongs + */ + constructor(node, parent, shadowId) { + super(); + this.shadowId = shadowId; + this.children = []; + this.actualNode = node; + this.parent = parent; + + this._isHidden = null; // will be populated by axe.utils.isHidden + this._cache = {}; + + if (axe._cache.get('nodeMap')) { + axe._cache.get('nodeMap').set(node, this); + } + } + + // abstract Node properties so we can run axe in DOM-less environments. + // add to the prototype so memory is shared across all virtual nodes + get props() { + const { nodeType, nodeName, id, type } = this.actualNode; + + return { + nodeType, + nodeName: nodeName.toLowerCase(), + id, + type + }; + } + + /** + * 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. + */ + hasClass(className) { + // get the value of the class attribute as svgs return a SVGAnimatedString + // if you access the className property + let classAttr = this.attr('class'); + if (!classAttr) { + return false; + } + + let selector = ' ' + className + ' '; + return ( + (' ' + classAttr + ' ').replace(whitespaceRegex, ' ').indexOf(selector) >= + 0 + ); + } + + /** + * Get the value of the given attribute name. + * @param {String} attrName The name of the attribute. + * @return {String|null} The value of the attribute or null if the attribute does not exist + */ + attr(attrName) { + if (typeof this.actualNode.getAttribute !== 'function') { + return null; + } + + return this.actualNode.getAttribute(attrName); + } + + /** + * Determine if the element has the given attribute. + * @param {String} attrName The name of the attribute + * @return {Boolean} True if the element has the attribute, false otherwise. + */ + hasAttr(attrName) { + if (typeof this.actualNode.hasAttribute !== 'function') { + return false; + } + + return this.actualNode.hasAttribute(attrName); + } + + /** + * Determine if the element is focusable and cache the result. + * @return {Boolean} True if the element is focusable, false otherwise. + */ + get isFocusable() { + 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. + * @return {VirtualNode[]} + */ + get tabbableElements() { + if (!this._cache.hasOwnProperty('tabbableElements')) { + this._cache.tabbableElements = axe.commons.dom.getTabbableElements(this); + } + return this._cache.tabbableElements; + } + + /** + * Return the computed style for this element and cache the result. + * @return {CSSStyleDeclaration} + */ + get computedStyle() { + if (!this._cache.hasOwnProperty('computedStyle')) { + this._cache.computedStyle = window.getComputedStyle(this.actualNode); + } + return this._cache.computedStyle; + } + + /** + * Return the client rects for this element and cache the result. + * @return {DOMRect[]} + */ + get clientRects() { + if (!this._cache.hasOwnProperty('clientRects')) { + this._cache.clientRects = Array.from( + this.actualNode.getClientRects() + ).filter(rect => rect.width > 0); + } + return this._cache.clientRects; + } + + /** + * Return the bounding rect for this element and cache the result. + * @return {DOMRect} + */ + get boundingClientRect() { + if (!this._cache.hasOwnProperty('boundingClientRect')) { + this._cache.boundingClientRect = this.actualNode.getBoundingClientRect(); + } + return this._cache.boundingClientRect; + } +} + +axe.AbstractVirtualNode = AbstractVirtualNode; diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js new file mode 100644 index 0000000000..def38a81de --- /dev/null +++ b/test/commons/dom/get-element-stack.js @@ -0,0 +1,131 @@ +describe('dom.getElementStack', function() { + 'use strict'; + + var getElementStack = axe.commons.dom.getElementStack; + var queryFixture = axe.testUtils.queryFixture; + + it('should return stack in DOM order of non-positioned elements', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '2', '1', 'fixture']); + }); + + it('should not return elements outside of the stack', function() { + var vNode = queryFixture( + '
' + + '
' + + 'Foo' + + '

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '2', '1', 'fixture']); + }); + + it('should should handle positioned elements without z-index', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index + var vNode = queryFixture( + '
' + + 'DIV #1
position: absolute;
' + + '
' + + 'DIV #2
position: relative;
' + + '
' + + 'DIV #3
position: relative;
' + + '
' + + 'DIV #4
position: absolute;
' + + '
' + + 'DIV #5
position: static;
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['4', '3', '2', '1', 'target', 'fixture']); + }); + + it('should handle floating and positioned elements without z-index', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float + var vNode = queryFixture( + '
' + + 'DIV #1
position: absolute;
' + + '
' + + 'DIV #2
float: left;
' + + '
' + + 'DIV #3
no positioning
' + + '
' + + 'DIV #4
position: absolute;
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['4', '1', '2', 'target', 'fixture']); + }); + + it('should handle z-index positioned elements in the same stacking context', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_1 + var vNode = queryFixture( + '
' + + '
DIV #1' + + '
position: relative;' + + '
' + + '
DIV #2' + + '
position: absolute;' + + '
z-index: 1;' + + '
' + + '
' + + '
' + + '
' + + '
DIV #3' + + '
position: relative;' + + '
' + + '
DIV #4' + + '
position: absolute;' + + '
z-index: 2;' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['4', '2', '3', 'target', 'fixture']); + }); + + it('should handle z-index positioned elements in different stacking contexts', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_2 + var vNode = queryFixture( + '
' + + '
DIV #1' + + '
position: relative;' + + '
' + + '
DIV #2' + + '
position: absolute;' + + '
z-index: 2;' + + '
' + + '
' + + '
' + + '
' + + '
DIV #3' + + '
position: relative;' + + '
' + + '
DIV #4' + + '
position: absolute;' + + '
z-index: 10;' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['2', '4', '3', 'target', 'fixture']); + }); +}); From a0250c06b6df9fb4af81cdb7e885699075d4cc96 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 9 Oct 2019 11:35:09 -0600 Subject: [PATCH 03/14] rotation logic --- lib/commons/dom/get-element-stack.js | 140 +++++++-- test/commons/dom/get-element-stack.js | 431 +++++++++++++++++++------- 2 files changed, 424 insertions(+), 147 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index 492425c108..49de0fc23b 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -1,12 +1,15 @@ /* global dom */ -// split the page into 100px x 100px cells -const gridSize = 100; +// split the page cells to group elements by the position +const gridSize = 200; // arbitrary size, increase to reduce memory (less cells) use but increase time (more nodes per grid to check collision) + +// https://developer.mozilla.org/en-US/docs/Web/CSS/Replaced_element +const replacedElements = ['iframe', 'video', 'embed', 'img']; /** * Determine if node produces a stacking context. * References: - * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context) + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context * https://github.com/gwwar/z-context/blob/master/devtools/index.js * @param {VirtualNode} vNode * @return {Boolean} @@ -97,9 +100,9 @@ function isStackingContext(vNode) { /** * Return the index order of how to position this element. return nodes in non-positioned, floating, positioned order - * Reference: - * - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index - * - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float + * References: + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float * @param {CSSStyleDeclaration} * @return {Number} */ @@ -122,6 +125,15 @@ function getPositionOrder(style) { * @param {VirtualNode} */ function visuallySort(a, b) { + // html node is always sorted to the end of the stack + // TODO: how do we test this? + if (a.actualNode.nodeName.toLowerCase() === 'html') { + return 1; + } + if (b.actualNode.nodeName.toLowerCase() === 'html') { + return -1; + } + /*eslint no-bitwise: 0 */ for (let i = 0; i < a._stackingOrder.length; i++) { if (typeof b._stackingOrder[i] === 'undefined') { @@ -141,21 +153,21 @@ function visuallySort(a, b) { // nodes are the same stacking order, compute special cases for when z-index // is not set - const DOMOrder = - a.actualNode.compareDocumentPosition(b.actualNode) & 4 ? 1 : -1; - const aStyle = a.computedStyle; - const bStyle = b.computedStyle; - const aPosition = getPositionOrder(aStyle); - const bPosition = getPositionOrder(bStyle); - if (aStyle.zIndex === 'auto' && bStyle.zIndex === 'auto') { - if (aPosition === bPosition) { + const docPosition = a.actualNode.compareDocumentPosition(b.actualNode); + const DOMOrder = docPosition & 4 ? 1 : -1; + const isDescendant = docPosition & 8 || docPosition & 16; + const aPosition = getPositionOrder(a.computedStyle); + const bPosition = getPositionOrder(b.computedStyle); + + if (a.computedStyle.zIndex === 'auto' && b.computedStyle.zIndex === 'auto') { + // a child of a positioned element should also be on top of the parent + if (aPosition === bPosition || isDescendant) { return DOMOrder; } return bPosition - aPosition; } - // default to return nodes in DOM order return DOMOrder; } @@ -242,8 +254,9 @@ function addNodeToGrid(grid, vNode) { vNode.clientRects.forEach(rect => { const startRow = Math.floor(rect.y / gridSize); - const endRow = Math.floor((rect.y + rect.height) / gridSize); const startCol = Math.floor(rect.x / gridSize); + + const endRow = Math.floor((rect.y + rect.height) / gridSize); const endCol = Math.floor((rect.x + rect.width) / gridSize); for (let row = startRow; row <= endRow; row++) { @@ -261,7 +274,7 @@ function addNodeToGrid(grid, vNode) { } /** - * + * Setup the 2d grid and add every element to it. */ function createGrid() { const rootGrid = { @@ -302,7 +315,15 @@ function createGrid() { }); } -dom.getElementStack = function(vNode, recurrsed) { +/** + * Return all elements that are at the center point of the passed in virtual node. + * @method getElementStack + * @memberof axe.commons.dom + * @param {VirtualNode} vNode + * @param {Boolean} recursed If the function has been called recursively + * @return {VirtualNode[]} + */ +dom.getElementStack = function(vNode, recursed) { if (!axe._cache.get('gridCreated')) { createGrid(); axe._cache.set('gridCreated', true); @@ -312,20 +333,84 @@ dom.getElementStack = function(vNode, recurrsed) { const boundingRect = vNode.boundingClientRect; // use center point of rect - const x = boundingRect.x + boundingRect.width / 2; - const y = boundingRect.y + boundingRect.height / 2; - + let x = boundingRect.x + boundingRect.width / 2; + let y = boundingRect.y + boundingRect.height / 2; + + // NOTE: there is a very rare edge case in Chrome vs Firefox that can + // return different results of `document.elementsFromPoint`. If the center + // point of the element is <1px outside of another elements bounding rect, + // Chrome appears to round the number up and return the element while Firefox + // keeps the number as is and won't return the element. In this case, we + // went with pixel perfect collision rather than rounding const row = Math.floor(y / gridSize); const col = Math.floor(x / gridSize); let stack = grid.cells[row][col].filter(vNode => { const rects = vNode.clientRects; + const style = vNode.computedStyle; + const nodeName = vNode.actualNode.nodeName.toLowerCase(); return rects.find(rect => { + let pointX = x; + let pointY = y; + + let rectWidth = rect.width; + let rectHeight = rect.height; + let rectX = rect.x; + let rectY = rect.y; + + // if rect is rotated, rotate point around center of rect and then + // perform collision detection as though the rect was not rotated + if ( + style.transform.startsWith('matrix') && + // only non-replaced inline elements can be rotated + // see https://developer.mozilla.org/en-US/docs/Web/CSS/transform + (replacedElements.includes(nodeName) || + (style.display !== 'inline' && style.display !== 'contents')) + ) { + // rect will no longer be the true width/height of element so we need + // to look at offsetWidth/Height + // @see https://stackoverflow.com/questions/40809153/get-element-dimensions-regardless-of-rotation + rectWidth = vNode.actualNode.offsetWidth; + rectHeight = vNode.actualNode.offsetHeight; + + // calculate non-rotated rect x/y based on the center point of the + // rotated rect + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + rectX = centerX - rectWidth / 2; + rectY = centerY - rectHeight / 2; + + // calculate the angle of rotation from the rotation matrix + // @see https://css-tricks.com/get-value-of-css-rotation-through-javascript/ + let values = style.transform.split('(')[1]; + values = values.split(')')[0]; + values = values.split(','); + const a = values[0]; + const b = values[1]; + const angle = -Math.atan2(b, a); // rotate in the reverse direction + + // rotate point around center of rect and then perform collision + // detection as though the rect was not rotated + // @see https://www.gamefromscratch.com/post/2012/11/24/GameDev-math-recipes-Rotating-one-point-around-another-point.aspx + if (angle !== 0) { + pointX = + Math.cos(angle) * (x - centerX) - + Math.sin(angle) * (y - centerY) + + centerX; + pointY = + Math.sin(angle) * (x - centerX) + + Math.cos(angle) * (y - centerY) + + centerY; + } + } + + // perform an AABB (axis-aligned bounding box) collision check for the + // point inside the rect return ( - x < rect.x + rect.width && - x > rect.x && - y < rect.y + rect.height && - y > rect.y + pointX < rectX + rectWidth && + pointX > rectX && + pointY < rectY + rectHeight && + pointY > rectY ); }); }); @@ -334,12 +419,9 @@ dom.getElementStack = function(vNode, recurrsed) { stack = dom.getElementStack(grid.container, true).concat(stack); } - if (!recurrsed) { + if (!recursed) { stack.sort(visuallySort); } - // TODO: should we cache the result of the lookup for each node in the stack - // since it won't change for a parent element? - return stack; }; diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index def38a81de..2ec79745fd 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -4,128 +4,323 @@ describe('dom.getElementStack', function() { var getElementStack = axe.commons.dom.getElementStack; var queryFixture = axe.testUtils.queryFixture; - it('should return stack in DOM order of non-positioned elements', function() { - var vNode = queryFixture( - '
' + - '
' + - '

Hello World

' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['target', '2', '1', 'fixture']); - }); + describe('stack order', function() { + it('should return stack in DOM order of non-positioned elements', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '2', '1', 'fixture']); + }); - it('should not return elements outside of the stack', function() { - var vNode = queryFixture( - '
' + - '
' + - 'Foo' + - '

Hello World

' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['target', '2', '1', 'fixture']); - }); + it('should not return elements outside of the stack', function() { + var vNode = queryFixture( + '
' + + '
' + + 'Foo' + + '

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '2', '1', 'fixture']); + }); - it('should should handle positioned elements without z-index', function() { - // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index - var vNode = queryFixture( - '
' + - 'DIV #1
position: absolute;
' + - '
' + - 'DIV #2
position: relative;
' + - '
' + - 'DIV #3
position: relative;
' + - '
' + - 'DIV #4
position: absolute;
' + - '
' + - 'DIV #5
position: static;
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['4', '3', '2', '1', 'target', 'fixture']); - }); + it('should should handle positioned elements without z-index', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index + var vNode = queryFixture( + '
' + + 'DIV #1
position:absolute;
' + + '
' + + 'DIV #2
position:relative;
' + + '
' + + 'DIV #3
position:relative;
' + + '
' + + 'DIV #4
position:absolute;
' + + '
' + + 'DIV #5
position:static;
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['4', '3', '2', '1', 'target', 'fixture']); + }); - it('should handle floating and positioned elements without z-index', function() { - // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float - var vNode = queryFixture( - '
' + - 'DIV #1
position: absolute;
' + - '
' + - 'DIV #2
float: left;
' + - '
' + - 'DIV #3
no positioning
' + - '
' + - 'DIV #4
position: absolute;
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['4', '1', '2', 'target', 'fixture']); - }); + it('should handle floating and positioned elements without z-index', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float + var vNode = queryFixture( + '
' + + 'DIV #1
position:absolute;
' + + '
' + + 'DIV #2
float:left;
' + + '
' + + 'DIV #3
no positioning
' + + '
' + + 'DIV #4
position:absolute;
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['4', '1', '2', 'target', 'fixture']); + }); - it('should handle z-index positioned elements in the same stacking context', function() { - // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_1 - var vNode = queryFixture( - '
' + - '
DIV #1' + - '
position: relative;' + - '
' + - '
DIV #2' + - '
position: absolute;' + - '
z-index: 1;' + - '
' + - '
' + - '
' + - '
' + - '
DIV #3' + - '
position: relative;' + - '
' + - '
DIV #4' + - '
position: absolute;' + - '
z-index: 2;' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['4', '2', '3', 'target', 'fixture']); - }); + it('should handle z-index positioned elements in the same stacking context', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_1 + var vNode = queryFixture( + '
' + + '
DIV #1' + + '
position:relative;' + + '
' + + '
DIV #2' + + '
position:absolute;' + + '
z-index:1;' + + '
' + + '
' + + '
' + + '
' + + '
DIV #3' + + '
position:relative;' + + '
' + + '
DIV #4' + + '
position:absolute;' + + '
z-index:2;' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['4', '2', '3', 'target', 'fixture']); + }); + + it('should handle z-index positioned elements in different stacking contexts', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_2 + var vNode = queryFixture( + '
' + + '
DIV #1' + + '
position:relative;' + + '
' + + '
DIV #2' + + '
position:absolute;' + + '
z-index:2;' + + '
' + + '
' + + '
' + + '
' + + '
DIV #3' + + '
position:relative;' + + '
' + + '
DIV #4' + + '
position:absolute;' + + '
z-index:10;' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['2', '4', '3', 'target', 'fixture']); + }); + + it('should handle complex stacking context', function() { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context + var vNode = queryFixture( + '
' + + 'Division Element #1
' + + 'position: relative;
' + + 'z-index: 5;' + + '
' + + '
' + + 'Division Element #2
' + + 'position: relative;
' + + 'z-index: 2;' + + '
' + + '
' + + '
' + + 'Division Element #4
' + + 'position: relative;
' + + 'z-index: 6;' + + '
' + + 'Division Element #3
' + + 'position: absolute;
' + + 'z-index: 4;' + + '
' + + 'Division Element #5
' + + 'position: relative;
' + + 'z-index: 1;' + + '
' + + '' + + '
' + + 'Division Element #6
' + + 'position: absolute;
' + + 'z-index: 3;' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['1', '4', 'target', '5', '3', '2']); + }); + + it('should correctly order children of position elements without z-index', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '1', 'fixture']); + }); + + it('should correctly order children of position elements with z-index', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '1', 'fixture']); + }); - it('should handle z-index positioned elements in different stacking contexts', function() { - // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_2 - var vNode = queryFixture( - '
' + - '
DIV #1' + - '
position: relative;' + - '
' + - '
DIV #2' + - '
position: absolute;' + - '
z-index: 2;' + - '
' + - '
' + - '
' + - '
' + - '
DIV #3' + - '
position: relative;' + - '
' + - '
DIV #4' + - '
position: absolute;' + - '
z-index: 10;' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['2', '4', '3', 'target', 'fixture']); + it('should handle modals on top of the stack', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); + }); + + it('should handle "pointer-events:none"', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); + }); + + it('should return elements left out by document.elementsFromPoint', function() { + var vNode = queryFixture( + '
' + + '
' + + '' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + }); + + it('should not return elements that do not fully cover the target', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Text oh heyyyy and here\'s
a link

' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '1', 'fixture']); + }); + + it('should not return parent elements that do not fully cover the target', function() { + var vNode = queryFixture( + '
' + + '
Text
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target']); + }); + + it('should return elements that partially cover the target', function() { + var vNode = queryFixture( + '
' + + '
' + + '
Text
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '2', '1', 'fixture']); + }); + + it('should handle CSS rotation', function() { + var vNode = queryFixture( + '
' + + '
' + + '
transform:rotate
' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '2', '1', 'fixture']); + }); + + it("should handle CSS rotation when applied to an element that can't be rotated", function() { + var vNode = queryFixture( + '
' + + '
' + + 'transform:rotate
but no rotation
' + + '
' + + '' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + }); + + it('should handle negative z-index', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['1', 'fixture', 'target', '2']); + }); }); + + describe('scroll regions', function() {}); }); From 6a35ff28e4e78d71ab7f9802589981b307f20d60 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 9 Oct 2019 16:31:29 -0600 Subject: [PATCH 04/14] finish tests --- lib/commons/dom/get-element-stack.js | 65 +++++++++++++++++++-------- lib/core/public/run-rules.js | 4 +- test/commons/dom/get-element-stack.js | 60 +++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 24 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index 49de0fc23b..d0603068a4 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -47,22 +47,27 @@ function isStackingContext(vNode) { } // elements with a transform value other than "none" - if (computedStyle.transform !== 'none') { + const transform = + computedStyle.getPropertyValue('-webkit-transform') || + computedStyle.getPropertyValue('-ms-transform') || + computedStyle.getPropertyValue('transform') || + 'none'; + if (transform !== 'none') { return true; } // elements with a mix-blend-mode value other than "normal" - if (computedStyle.mixBlendMode !== 'normal') { + if (computedStyle.mixBlendMode && computedStyle.mixBlendMode !== 'normal') { return true; } // elements with a filter value other than "none" - if (computedStyle.filter !== 'none') { + if (computedStyle.filter && computedStyle.filter !== 'none') { return true; } // elements with a perspective value other than "none" - if (computedStyle.perspective !== 'none') { + if (computedStyle.perspective && computedStyle.perspective !== 'none') { return true; } @@ -86,7 +91,7 @@ function isStackingContext(vNode) { // a flex item with a z-index value other than "auto", that is the parent element display: flex|inline-flex, if (computedStyle.zIndex !== 'auto') { - var parentStyle = getComputedStyle(node.parentNode); + var parentStyle = node.parentNode.computedStyle; if ( parentStyle.display === 'flex' || parentStyle.display === 'inline-flex' @@ -242,6 +247,21 @@ function findScrollRegionParent(vNode) { return scrollRegionParent; } +/** + * Get the DOMRect x or y value. IE11 (and Phantom) does not support x/y + * on DOMRect. + * @param {DOMRect} + * @param {String} pos 'x' or 'y' + * @return {Number} + */ +function getDomPosition(rect, pos) { + if (pos === 'x') { + return 'x' in rect ? rect.x : rect.left; + } + + return 'y' in rect ? rect.y : rect.top; +} + /** * Add a node to every cell of the grid it intersects with. * @param {Grid} @@ -253,11 +273,15 @@ function addNodeToGrid(grid, vNode) { vNode._grid = grid; vNode.clientRects.forEach(rect => { - const startRow = Math.floor(rect.y / gridSize); - const startCol = Math.floor(rect.x / gridSize); + const startRow = Math.floor(getDomPosition(rect, 'y') / gridSize); + const startCol = Math.floor(getDomPosition(rect, 'x') / gridSize); - const endRow = Math.floor((rect.y + rect.height) / gridSize); - const endCol = Math.floor((rect.x + rect.width) / gridSize); + const endRow = Math.floor( + (getDomPosition(rect, 'y') + rect.height) / gridSize + ); + const endCol = Math.floor( + (getDomPosition(rect, 'x') + rect.width) / gridSize + ); for (let row = startRow; row <= endRow; row++) { grid.cells[row] = grid.cells[row] || []; @@ -333,8 +357,8 @@ dom.getElementStack = function(vNode, recursed) { const boundingRect = vNode.boundingClientRect; // use center point of rect - let x = boundingRect.x + boundingRect.width / 2; - let y = boundingRect.y + boundingRect.height / 2; + let x = getDomPosition(boundingRect, 'x') + boundingRect.width / 2; + let y = getDomPosition(boundingRect, 'y') + boundingRect.height / 2; // NOTE: there is a very rare edge case in Chrome vs Firefox that can // return different results of `document.elementsFromPoint`. If the center @@ -344,7 +368,6 @@ dom.getElementStack = function(vNode, recursed) { // went with pixel perfect collision rather than rounding const row = Math.floor(y / gridSize); const col = Math.floor(x / gridSize); - let stack = grid.cells[row][col].filter(vNode => { const rects = vNode.clientRects; const style = vNode.computedStyle; @@ -355,13 +378,19 @@ dom.getElementStack = function(vNode, recursed) { let rectWidth = rect.width; let rectHeight = rect.height; - let rectX = rect.x; - let rectY = rect.y; + let rectX = getDomPosition(rect, 'x'); + let rectY = getDomPosition(rect, 'y'); + + const transform = + style.getPropertyValue('-webkit-transform') || + style.getPropertyValue('-ms-transform') || + style.getPropertyValue('transform') || + 'none'; // if rect is rotated, rotate point around center of rect and then // perform collision detection as though the rect was not rotated if ( - style.transform.startsWith('matrix') && + transform.indexOf('matrix') === 0 && // only non-replaced inline elements can be rotated // see https://developer.mozilla.org/en-US/docs/Web/CSS/transform (replacedElements.includes(nodeName) || @@ -375,14 +404,14 @@ dom.getElementStack = function(vNode, recursed) { // calculate non-rotated rect x/y based on the center point of the // rotated rect - const centerX = rect.x + rect.width / 2; - const centerY = rect.y + rect.height / 2; + const centerX = rectX + rect.width / 2; + const centerY = rectY + rect.height / 2; rectX = centerX - rectWidth / 2; rectY = centerY - rectHeight / 2; // calculate the angle of rotation from the rotation matrix // @see https://css-tricks.com/get-value-of-css-rotation-through-javascript/ - let values = style.transform.split('(')[1]; + let values = transform.split('(')[1]; values = values.split(')')[0]; values = values.split(','); const a = values[0]; diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index 32be4c952b..8d8e712220 100644 --- a/lib/core/public/run-rules.js +++ b/lib/core/public/run-rules.js @@ -3,8 +3,8 @@ // Clean up after resolve / reject function cleanup() { - axe._cache.clear(); - axe._tree = undefined; + // axe._cache.clear(); + // axe._tree = undefined; axe._selectorData = undefined; } diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 2ec79745fd..e1310f8e0d 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -1,4 +1,4 @@ -describe('dom.getElementStack', function() { +describe.only('dom.getElementStack', function() { 'use strict'; var getElementStack = axe.commons.dom.getElementStack; @@ -243,7 +243,7 @@ describe('dom.getElementStack', function() { var vNode = queryFixture( '
' + '
' + - '

Text oh heyyyy and here\'s
a link

' + + '

Text oh heyyyy and here\'s
a link

' + '
' ); var stack = getElementStack(vNode).map(function(vNode) { @@ -281,7 +281,7 @@ describe('dom.getElementStack', function() { var vNode = queryFixture( '
' + '
' + - '
transform:rotate
' + + '
transform:rotate
' + '
' + '
' + '
' @@ -322,5 +322,57 @@ describe('dom.getElementStack', function() { }); }); - describe('scroll regions', function() {}); + describe('scroll regions', function() { + it('should return stack of scroll regions', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + }); + + it('should return stack when scroll region is larger than parent', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + }); + + it('should return stack of recursive scroll regions', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '5', '4', '3', '2', '1', 'fixture']); + }); + }); }); From ebeed1e3bc3e34cae13568ab956b6ebdd40ec8de Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 9 Oct 2019 16:36:14 -0600 Subject: [PATCH 05/14] undo run rules --- lib/core/public/run-rules.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index 8d8e712220..32be4c952b 100644 --- a/lib/core/public/run-rules.js +++ b/lib/core/public/run-rules.js @@ -3,8 +3,8 @@ // Clean up after resolve / reject function cleanup() { - // axe._cache.clear(); - // axe._tree = undefined; + axe._cache.clear(); + axe._tree = undefined; axe._selectorData = undefined; } From fd307adbfbcce6d4155b6b21a1187810cdfa9a18 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 9 Oct 2019 17:03:42 -0600 Subject: [PATCH 06/14] add test for virtual node --- test/core/base/virtual-node/virtual-node.js | 96 +++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/core/base/virtual-node/virtual-node.js b/test/core/base/virtual-node/virtual-node.js index df1e352db1..5bde6401a6 100644 --- a/test/core/base/virtual-node/virtual-node.js +++ b/test/core/base/virtual-node/virtual-node.js @@ -190,5 +190,101 @@ describe('VirtualNode', function() { assert.equal(count, 1); }); }); + + describe('computedStyle', function() { + var computedStyle; + + beforeEach(function() { + computedStyle = window.getComputedStyle; + }); + + afterEach(function() { + window.getComputedStyle = computedStyle; + }); + + it('should call window.getComputedStyle', function() { + var called = false; + window.getComputedStyle = function() { + called = true; + }; + var vNode = new VirtualNode(node); + vNode.computedStyle; + + assert.isTrue(called); + }); + + it('should only call window.getComputedStyle once', function() { + var count = 0; + window.getComputedStyle = function() { + count++; + }; + var vNode = new VirtualNode(node); + vNode.computedStyle; + vNode.computedStyle; + vNode.computedStyle; + assert.equal(count, 1); + }); + }); + + describe('clientRects', function() { + it('should call node.getClientRects', function() { + var called = false; + node.getClientRects = function() { + called = true; + return []; + }; + var vNode = new VirtualNode(node); + vNode.clientRects; + + assert.isTrue(called); + }); + + it('should only call node.getClientRects once', function() { + var count = 0; + node.getClientRects = function() { + count++; + return []; + }; + var vNode = new VirtualNode(node); + vNode.clientRects; + vNode.clientRects; + vNode.clientRects; + assert.equal(count, 1); + }); + + it('should filter out 0 width rects', function() { + node.getClientRects = function() { + return [{ width: 10 }, { width: 0 }, { width: 20 }]; + }; + var vNode = new VirtualNode(node); + + assert.deepEqual(vNode.clientRects, [{ width: 10 }, { width: 20 }]); + }); + }); + + describe('boundingClientRect', function() { + it('should call node.getBoundingClientRect', function() { + var called = false; + node.getBoundingClientRect = function() { + called = true; + }; + var vNode = new VirtualNode(node); + vNode.boundingClientRect; + + assert.isTrue(called); + }); + + it('should only call node.getBoundingClientRect once', function() { + var count = 0; + node.getBoundingClientRect = function() { + count++; + }; + var vNode = new VirtualNode(node); + vNode.boundingClientRect; + vNode.boundingClientRect; + vNode.boundingClientRect; + assert.equal(count, 1); + }); + }); }); }); From 872b8881a5aa2d26a10fde1a6ed2b12f949f003e Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 9 Oct 2019 17:13:14 -0600 Subject: [PATCH 07/14] fix --- test/commons/dom/get-element-stack.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index e1310f8e0d..ccedf4ae4a 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -38,15 +38,15 @@ describe.only('dom.getElementStack', function() { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index var vNode = queryFixture( '
' + - 'DIV #1
position:absolute;
' + + 'DIV #1
position:absolute;' + '
' + - 'DIV #2
position:relative;
' + + 'DIV #2
position:relative;' + '
' + - 'DIV #3
position:relative;
' + + 'DIV #3
position:relative;' + '
' + - 'DIV #4
position:absolute;
' + + 'DIV #4
position:absolute;' + '
' + - 'DIV #5
position:static;
' + 'DIV #5
position:static;' ); var stack = getElementStack(vNode).map(function(vNode) { return vNode.actualNode.id; @@ -58,13 +58,13 @@ describe.only('dom.getElementStack', function() { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float var vNode = queryFixture( '
' + - 'DIV #1
position:absolute;
' + + 'DIV #1
position:absolute;' + '
' + - 'DIV #2
float:left;
' + + 'DIV #2
float:left;' + '
' + - 'DIV #3
no positioning
' + + 'DIV #3
no positioning' + '
' + - 'DIV #4
position:absolute;
' + 'DIV #4
position:absolute;' ); var stack = getElementStack(vNode).map(function(vNode) { return vNode.actualNode.id; From e5465bd8a1283b80a6f79e4ed494da6dd9488995 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 9 Oct 2019 17:18:44 -0600 Subject: [PATCH 08/14] remove --- lib/core/base/virtual-node.js | 171 ---------------------------------- 1 file changed, 171 deletions(-) delete mode 100644 lib/core/base/virtual-node.js diff --git a/lib/core/base/virtual-node.js b/lib/core/base/virtual-node.js deleted file mode 100644 index a8976d3dce..0000000000 --- a/lib/core/base/virtual-node.js +++ /dev/null @@ -1,171 +0,0 @@ -const whitespaceRegex = /[\t\r\n\f]/g; - -class AbstractVirtualNode { - constructor() { - this.children = []; - this.parent = null; - } - - get props() { - throw new Error( - 'VirtualNode class must have a "props" object consisting ' + - 'of "nodeType" and "nodeName" properties' - ); - } - - hasClass() { - throw new Error('VirtualNode class must have a "hasClass" function'); - } - - attr() { - throw new Error('VirtualNode class must have a "attr" function'); - } - - hasAttr() { - throw new Error('VirtualNode class must have a "hasAttr" function'); - } -} - -// class is unused in the file... -// eslint-disable-next-line no-unused-vars -class VirtualNode extends AbstractVirtualNode { - /** - * Wrap the real node and provide list of the flattened children - * @param {Node} node the node in question - * @param {VirtualNode} parent The parent VirtualNode - * @param {String} shadowId the ID of the shadow DOM to which this node belongs - */ - constructor(node, parent, shadowId) { - super(); - this.shadowId = shadowId; - this.children = []; - this.actualNode = node; - this.parent = parent; - - this._isHidden = null; // will be populated by axe.utils.isHidden - this._cache = {}; - - if (axe._cache.get('nodeMap')) { - axe._cache.get('nodeMap').set(node, this); - } - } - - // abstract Node properties so we can run axe in DOM-less environments. - // add to the prototype so memory is shared across all virtual nodes - get props() { - const { nodeType, nodeName, id, type } = this.actualNode; - - return { - nodeType, - nodeName: nodeName.toLowerCase(), - id, - type - }; - } - - /** - * 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. - */ - hasClass(className) { - // get the value of the class attribute as svgs return a SVGAnimatedString - // if you access the className property - let classAttr = this.attr('class'); - if (!classAttr) { - return false; - } - - let selector = ' ' + className + ' '; - return ( - (' ' + classAttr + ' ').replace(whitespaceRegex, ' ').indexOf(selector) >= - 0 - ); - } - - /** - * Get the value of the given attribute name. - * @param {String} attrName The name of the attribute. - * @return {String|null} The value of the attribute or null if the attribute does not exist - */ - attr(attrName) { - if (typeof this.actualNode.getAttribute !== 'function') { - return null; - } - - return this.actualNode.getAttribute(attrName); - } - - /** - * Determine if the element has the given attribute. - * @param {String} attrName The name of the attribute - * @return {Boolean} True if the element has the attribute, false otherwise. - */ - hasAttr(attrName) { - if (typeof this.actualNode.hasAttribute !== 'function') { - return false; - } - - return this.actualNode.hasAttribute(attrName); - } - - /** - * Determine if the element is focusable and cache the result. - * @return {Boolean} True if the element is focusable, false otherwise. - */ - get isFocusable() { - 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. - * @return {VirtualNode[]} - */ - get tabbableElements() { - if (!this._cache.hasOwnProperty('tabbableElements')) { - this._cache.tabbableElements = axe.commons.dom.getTabbableElements(this); - } - return this._cache.tabbableElements; - } - - /** - * Return the computed style for this element and cache the result. - * @return {CSSStyleDeclaration} - */ - get computedStyle() { - if (!this._cache.hasOwnProperty('computedStyle')) { - this._cache.computedStyle = window.getComputedStyle(this.actualNode); - } - return this._cache.computedStyle; - } - - /** - * Return the client rects for this element and cache the result. - * @return {DOMRect[]} - */ - get clientRects() { - if (!this._cache.hasOwnProperty('clientRects')) { - this._cache.clientRects = Array.from( - this.actualNode.getClientRects() - ).filter(rect => rect.width > 0); - } - return this._cache.clientRects; - } - - /** - * Return the bounding rect for this element and cache the result. - * @return {DOMRect} - */ - get boundingClientRect() { - if (!this._cache.hasOwnProperty('boundingClientRect')) { - this._cache.boundingClientRect = this.actualNode.getBoundingClientRect(); - } - return this._cache.boundingClientRect; - } -} - -axe.AbstractVirtualNode = AbstractVirtualNode; From ac8f121b87855fdd1a1ea21d809e81e662130f9c Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 10 Oct 2019 09:40:56 -0600 Subject: [PATCH 09/14] fix for inline elements --- lib/commons/dom/get-element-stack.js | 54 ++++++++++++++++----------- test/commons/dom/get-element-stack.js | 26 +++++++++---- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index d0603068a4..55ae4da02a 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -52,6 +52,7 @@ function isStackingContext(vNode) { computedStyle.getPropertyValue('-ms-transform') || computedStyle.getPropertyValue('transform') || 'none'; + if (transform !== 'none') { return true; } @@ -90,8 +91,8 @@ function isStackingContext(vNode) { } // a flex item with a z-index value other than "auto", that is the parent element display: flex|inline-flex, - if (computedStyle.zIndex !== 'auto') { - var parentStyle = node.parentNode.computedStyle; + if (computedStyle.zIndex !== 'auto' && vNode.parent) { + const parentStyle = vNode.parent.computedStyle; if ( parentStyle.display === 'flex' || parentStyle.display === 'inline-flex' @@ -108,29 +109,43 @@ function isStackingContext(vNode) { * References: * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float + * https://drafts.csswg.org/css2/visuren.html#layers * @param {CSSStyleDeclaration} * @return {Number} */ function getPositionOrder(style) { - if (style.float !== 'none') { - return 1; - } - if (style.position === 'static') { - return 0; + // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. + if (style.display.indexOf('inline') !== -1) { + return 2; + } + + // 4. the non-positioned floats. + if (style.float !== 'none') { + return 1; + } + + // 3. the in-flow, non-inline-level, non-positioned descendants. + if (style.position === 'static') { + return 0; + } } - // positioned element - return 2; + // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. + return 3; } /** * Visually sort nodes based on their stack order + * References: + * https://drafts.csswg.org/css2/visuren.html#layers * @param {VirtualNode} * @param {VirtualNode} */ function visuallySort(a, b) { - // html node is always sorted to the end of the stack + /*eslint no-bitwise: 0 */ + + // 1. The root element forms the root stacking context. // TODO: how do we test this? if (a.actualNode.nodeName.toLowerCase() === 'html') { return 1; @@ -139,7 +154,6 @@ function visuallySort(a, b) { return -1; } - /*eslint no-bitwise: 0 */ for (let i = 0; i < a._stackingOrder.length; i++) { if (typeof b._stackingOrder[i] === 'undefined') { return -1; @@ -149,11 +163,13 @@ function visuallySort(a, b) { continue; } + // 7. the child stacking contexts with positive stack levels (least positive first). if (b._stackingOrder[i] > a._stackingOrder[i]) { return 1; - } else { - return -1; } + + // 2. the child stacking contexts with negative stack levels (most negative first). + return -1; } // nodes are the same stacking order, compute special cases for when z-index @@ -164,16 +180,12 @@ function visuallySort(a, b) { const aPosition = getPositionOrder(a.computedStyle); const bPosition = getPositionOrder(b.computedStyle); - if (a.computedStyle.zIndex === 'auto' && b.computedStyle.zIndex === 'auto') { - // a child of a positioned element should also be on top of the parent - if (aPosition === bPosition || isDescendant) { - return DOMOrder; - } - - return bPosition - aPosition; + // a child of a positioned element should also be on top of the parent + if (aPosition === bPosition || isDescendant) { + return DOMOrder; } - return DOMOrder; + return bPosition - aPosition; } /** diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index ccedf4ae4a..0e232d475f 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -1,9 +1,14 @@ describe.only('dom.getElementStack', function() { 'use strict'; + var fixture = document.getElementById('fixture'); var getElementStack = axe.commons.dom.getElementStack; var queryFixture = axe.testUtils.queryFixture; + afterEach(function() { + fixture.innerHTML = ''; + }); + describe('stack order', function() { it('should return stack in DOM order of non-positioned elements', function() { var vNode = queryFixture( @@ -281,8 +286,8 @@ describe.only('dom.getElementStack', function() { var vNode = queryFixture( '
' + '
' + - '
transform:rotate
' + - '
' + + '
transform:rotate
' + + '
' + '
' + '
' ); @@ -296,15 +301,20 @@ describe.only('dom.getElementStack', function() { var vNode = queryFixture( '
' + '
' + - 'transform:rotate
but no rotation
' + - '
' + + 'transform:rotate' + + '
' + '' + '
' ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + var stack = []; + try { + stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + } catch (e) { + console.log(e); + } + assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); }); it('should handle negative z-index', function() { From f9bf65d0fc27d1544f3ccd7f3bc0d0040569e915 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 10 Oct 2019 11:48:41 -0600 Subject: [PATCH 10/14] use getPropertyValue --- lib/commons/dom/get-element-stack.js | 70 +++++++++++++++++---------- test/commons/dom/get-element-stack.js | 2 +- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index 55ae4da02a..62924ac05d 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -30,19 +30,22 @@ function isStackingContext(vNode) { // position: fixed or sticky if ( - computedStyle.position === 'fixed' || - computedStyle.position === 'sticky' + computedStyle.getPropertyValue('position') === 'fixed' || + computedStyle.getPropertyValue('position') === 'sticky' ) { return true; } // positioned (absolutely or relatively) with a z-index value other than "auto", - if (computedStyle.zIndex !== 'auto' && computedStyle.position !== 'static') { + if ( + computedStyle.zIndex !== 'auto' && + computedStyle.getPropertyValue('position') !== 'static' + ) { return true; } // elements with an opacity value less than 1. - if (computedStyle.opacity !== '1') { + if (computedStyle.getPropertyValue('opacity') !== '1') { return true; } @@ -58,29 +61,38 @@ function isStackingContext(vNode) { } // elements with a mix-blend-mode value other than "normal" - if (computedStyle.mixBlendMode && computedStyle.mixBlendMode !== 'normal') { + if ( + computedStyle.getPropertyValue('mix-blend-mode') && + computedStyle.getPropertyValue('mix-blend-mode') !== 'normal' + ) { return true; } // elements with a filter value other than "none" - if (computedStyle.filter && computedStyle.filter !== 'none') { + if ( + computedStyle.getPropertyValue('filter') && + computedStyle.getPropertyValue('filter') !== 'none' + ) { return true; } // elements with a perspective value other than "none" - if (computedStyle.perspective && computedStyle.perspective !== 'none') { + if ( + computedStyle.getPropertyValue('perspective') && + computedStyle.getPropertyValue('perspective') !== 'none' + ) { return true; } // elements with isolation set to "isolate" - if (computedStyle.isolation === 'isolate') { + if (computedStyle.getPropertyValue('isolation') === 'isolate') { return true; } // transform or opacity in will-change even if you don't specify values for these attributes directly if ( - computedStyle.willChange === 'transform' || - computedStyle.willChange === 'opacity' + computedStyle.getPropertyValue('will-change') === 'transform' || + computedStyle.getPropertyValue('will-change') === 'opacity' ) { return true; } @@ -91,11 +103,11 @@ function isStackingContext(vNode) { } // a flex item with a z-index value other than "auto", that is the parent element display: flex|inline-flex, - if (computedStyle.zIndex !== 'auto' && vNode.parent) { + if (computedStyle.getPropertyValue('z-index') !== 'auto' && vNode.parent) { const parentStyle = vNode.parent.computedStyle; if ( - parentStyle.display === 'flex' || - parentStyle.display === 'inline-flex' + parentStyle.getPropertyValue('display') === 'flex' || + parentStyle.getPropertyValue('display') === 'inline-flex' ) { return true; } @@ -114,19 +126,19 @@ function isStackingContext(vNode) { * @return {Number} */ function getPositionOrder(style) { - if (style.position === 'static') { + if (style.getPropertyValue('position') === 'static') { // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. - if (style.display.indexOf('inline') !== -1) { + if (style.getPropertyValue('display').indexOf('inline') !== -1) { return 2; } // 4. the non-positioned floats. - if (style.float !== 'none') { + if (style.getPropertyValue('float') !== 'none') { return 1; } // 3. the in-flow, non-inline-level, non-positioned descendants. - if (style.position === 'static') { + if (style.getPropertyValue('position') === 'static') { return 0; } } @@ -172,7 +184,7 @@ function visuallySort(a, b) { return -1; } - // nodes are the same stacking order, compute special cases for when z-index + // nodes are the same stacking order, compute special cases for when f // is not set const docPosition = a.actualNode.compareDocumentPosition(b.actualNode); const DOMOrder = docPosition & 4 ? 1 : -1; @@ -200,8 +212,10 @@ function getStackingOrder(vNode) { ? vNode.parent._stackingOrder.slice() : [0]; - if (style.zIndex !== 'auto') { - stackingOrder[stackingOrder.length - 1] = parseInt(style.zIndex); + if (style.getPropertyValue('z-index') !== 'auto') { + stackingOrder[stackingOrder.length - 1] = parseInt( + style.getPropertyValue('z-index') + ); } if (isStackingContext(vNode)) { stackingOrder.push(0); @@ -219,10 +233,10 @@ function isScrollRegion(vNode) { const style = vNode.computedStyle; return ( - style.overflowX === 'auto' || - style.overflowX === 'scroll' || - style.overflowY === 'auto' || - style.overflowY === 'scroll' + style.getPropertyValue('overflow-x') === 'auto' || + style.getPropertyValue('overflow-x') === 'scroll' || + style.getPropertyValue('overflow-y') === 'auto' || + style.getPropertyValue('overflow-y') === 'scroll' ); } @@ -366,6 +380,11 @@ dom.getElementStack = function(vNode, recursed) { } const grid = vNode._grid; + + if (!grid) { + return []; + } + const boundingRect = vNode.boundingClientRect; // use center point of rect @@ -406,7 +425,8 @@ dom.getElementStack = function(vNode, recursed) { // only non-replaced inline elements can be rotated // see https://developer.mozilla.org/en-US/docs/Web/CSS/transform (replacedElements.includes(nodeName) || - (style.display !== 'inline' && style.display !== 'contents')) + (style.getPropertyValue('display') !== 'inline' && + style.getPropertyValue('display') !== 'contents')) ) { // rect will no longer be the true width/height of element so we need // to look at offsetWidth/Height diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 0e232d475f..5d7df21bb3 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -1,4 +1,4 @@ -describe.only('dom.getElementStack', function() { +describe('dom.getElementStack', function() { 'use strict'; var fixture = document.getElementById('fixture'); From 4852ed2a1103aea026a61196669d69ba8ce90be6 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 16 Oct 2019 14:07:43 -0600 Subject: [PATCH 11/14] cache style propertly lookups --- lib/commons/dom/get-element-stack.js | 87 +++++++++++---------- lib/core/base/virtual-node/virtual-node.js | 13 +++ test/core/base/virtual-node/virtual-node.js | 48 ++++++++++++ 3 files changed, 105 insertions(+), 43 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index 62924ac05d..cbd0c631ca 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -30,8 +30,8 @@ function isStackingContext(vNode) { // position: fixed or sticky if ( - computedStyle.getPropertyValue('position') === 'fixed' || - computedStyle.getPropertyValue('position') === 'sticky' + vNode.getComputedStylePropertyValue('position') === 'fixed' || + vNode.getComputedStylePropertyValue('position') === 'sticky' ) { return true; } @@ -39,21 +39,21 @@ function isStackingContext(vNode) { // positioned (absolutely or relatively) with a z-index value other than "auto", if ( computedStyle.zIndex !== 'auto' && - computedStyle.getPropertyValue('position') !== 'static' + vNode.getComputedStylePropertyValue('position') !== 'static' ) { return true; } // elements with an opacity value less than 1. - if (computedStyle.getPropertyValue('opacity') !== '1') { + if (vNode.getComputedStylePropertyValue('opacity') !== '1') { return true; } // elements with a transform value other than "none" const transform = - computedStyle.getPropertyValue('-webkit-transform') || - computedStyle.getPropertyValue('-ms-transform') || - computedStyle.getPropertyValue('transform') || + vNode.getComputedStylePropertyValue('-webkit-transform') || + vNode.getComputedStylePropertyValue('-ms-transform') || + vNode.getComputedStylePropertyValue('transform') || 'none'; if (transform !== 'none') { @@ -62,37 +62,37 @@ function isStackingContext(vNode) { // elements with a mix-blend-mode value other than "normal" if ( - computedStyle.getPropertyValue('mix-blend-mode') && - computedStyle.getPropertyValue('mix-blend-mode') !== 'normal' + vNode.getComputedStylePropertyValue('mix-blend-mode') && + vNode.getComputedStylePropertyValue('mix-blend-mode') !== 'normal' ) { return true; } // elements with a filter value other than "none" if ( - computedStyle.getPropertyValue('filter') && - computedStyle.getPropertyValue('filter') !== 'none' + vNode.getComputedStylePropertyValue('filter') && + vNode.getComputedStylePropertyValue('filter') !== 'none' ) { return true; } // elements with a perspective value other than "none" if ( - computedStyle.getPropertyValue('perspective') && - computedStyle.getPropertyValue('perspective') !== 'none' + vNode.getComputedStylePropertyValue('perspective') && + vNode.getComputedStylePropertyValue('perspective') !== 'none' ) { return true; } // elements with isolation set to "isolate" - if (computedStyle.getPropertyValue('isolation') === 'isolate') { + if (vNode.getComputedStylePropertyValue('isolation') === 'isolate') { return true; } // transform or opacity in will-change even if you don't specify values for these attributes directly if ( - computedStyle.getPropertyValue('will-change') === 'transform' || - computedStyle.getPropertyValue('will-change') === 'opacity' + vNode.getComputedStylePropertyValue('will-change') === 'transform' || + vNode.getComputedStylePropertyValue('will-change') === 'opacity' ) { return true; } @@ -103,11 +103,14 @@ function isStackingContext(vNode) { } // a flex item with a z-index value other than "auto", that is the parent element display: flex|inline-flex, - if (computedStyle.getPropertyValue('z-index') !== 'auto' && vNode.parent) { - const parentStyle = vNode.parent.computedStyle; + if ( + vNode.getComputedStylePropertyValue('z-index') !== 'auto' && + vNode.parent + ) { + const parent = vNode.parent; if ( - parentStyle.getPropertyValue('display') === 'flex' || - parentStyle.getPropertyValue('display') === 'inline-flex' + parent.getComputedStylePropertyValue('display') === 'flex' || + parent.getComputedStylePropertyValue('display') === 'inline-flex' ) { return true; } @@ -122,23 +125,25 @@ function isStackingContext(vNode) { * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float * https://drafts.csswg.org/css2/visuren.html#layers - * @param {CSSStyleDeclaration} + * @param {VirtualNode} vNode * @return {Number} */ -function getPositionOrder(style) { - if (style.getPropertyValue('position') === 'static') { +function getPositionOrder(vNode) { + if (vNode.getComputedStylePropertyValue('position') === 'static') { // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. - if (style.getPropertyValue('display').indexOf('inline') !== -1) { + if ( + vNode.getComputedStylePropertyValue('display').indexOf('inline') !== -1 + ) { return 2; } // 4. the non-positioned floats. - if (style.getPropertyValue('float') !== 'none') { + if (vNode.getComputedStylePropertyValue('float') !== 'none') { return 1; } // 3. the in-flow, non-inline-level, non-positioned descendants. - if (style.getPropertyValue('position') === 'static') { + if (vNode.getComputedStylePropertyValue('position') === 'static') { return 0; } } @@ -189,8 +194,8 @@ function visuallySort(a, b) { const docPosition = a.actualNode.compareDocumentPosition(b.actualNode); const DOMOrder = docPosition & 4 ? 1 : -1; const isDescendant = docPosition & 8 || docPosition & 16; - const aPosition = getPositionOrder(a.computedStyle); - const bPosition = getPositionOrder(b.computedStyle); + const aPosition = getPositionOrder(a); + const bPosition = getPositionOrder(b); // a child of a positioned element should also be on top of the parent if (aPosition === bPosition || isDescendant) { @@ -207,14 +212,13 @@ function visuallySort(a, b) { * @return {Number[]} */ function getStackingOrder(vNode) { - const style = vNode.computedStyle; const stackingOrder = vNode.parent ? vNode.parent._stackingOrder.slice() : [0]; - if (style.getPropertyValue('z-index') !== 'auto') { + if (vNode.getComputedStylePropertyValue('z-index') !== 'auto') { stackingOrder[stackingOrder.length - 1] = parseInt( - style.getPropertyValue('z-index') + vNode.getComputedStylePropertyValue('z-index') ); } if (isStackingContext(vNode)) { @@ -230,13 +234,11 @@ function getStackingOrder(vNode) { * @return {Boolean} */ function isScrollRegion(vNode) { - const style = vNode.computedStyle; - return ( - style.getPropertyValue('overflow-x') === 'auto' || - style.getPropertyValue('overflow-x') === 'scroll' || - style.getPropertyValue('overflow-y') === 'auto' || - style.getPropertyValue('overflow-y') === 'scroll' + vNode.getComputedStylePropertyValue('overflow-x') === 'auto' || + vNode.getComputedStylePropertyValue('overflow-x') === 'scroll' || + vNode.getComputedStylePropertyValue('overflow-y') === 'auto' || + vNode.getComputedStylePropertyValue('overflow-y') === 'scroll' ); } @@ -401,7 +403,6 @@ dom.getElementStack = function(vNode, recursed) { const col = Math.floor(x / gridSize); let stack = grid.cells[row][col].filter(vNode => { const rects = vNode.clientRects; - const style = vNode.computedStyle; const nodeName = vNode.actualNode.nodeName.toLowerCase(); return rects.find(rect => { let pointX = x; @@ -413,9 +414,9 @@ dom.getElementStack = function(vNode, recursed) { let rectY = getDomPosition(rect, 'y'); const transform = - style.getPropertyValue('-webkit-transform') || - style.getPropertyValue('-ms-transform') || - style.getPropertyValue('transform') || + vNode.getComputedStylePropertyValue('-webkit-transform') || + vNode.getComputedStylePropertyValue('-ms-transform') || + vNode.getComputedStylePropertyValue('transform') || 'none'; // if rect is rotated, rotate point around center of rect and then @@ -425,8 +426,8 @@ dom.getElementStack = function(vNode, recursed) { // only non-replaced inline elements can be rotated // see https://developer.mozilla.org/en-US/docs/Web/CSS/transform (replacedElements.includes(nodeName) || - (style.getPropertyValue('display') !== 'inline' && - style.getPropertyValue('display') !== 'contents')) + (vNode.getComputedStylePropertyValue('display') !== 'inline' && + vNode.getComputedStylePropertyValue('display') !== 'contents')) ) { // rect will no longer be the true width/height of element so we need // to look at offsetWidth/Height diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index 405c49a798..7f7d7d150b 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -61,6 +61,19 @@ class VirtualNode extends axe.AbstractVirtualNode { return this.actualNode.hasAttribute(attrName); } + /** + * Return a property of the computed style for this element and cache the result. + * @see https://jsperf.com/get-property-value + * @return {String} + */ + getComputedStylePropertyValue(property) { + const key = 'computedStyle_' + property; + if (!this._cache.hasOwnProperty(key)) { + this._cache[key] = this.computedStyle.getPropertyValue(property); + } + return this._cache[key]; + } + /** * Determine if the element is focusable and cache the result. * @return {Boolean} True if the element is focusable, false otherwise. diff --git a/test/core/base/virtual-node/virtual-node.js b/test/core/base/virtual-node/virtual-node.js index 5bde6401a6..ad491ca77e 100644 --- a/test/core/base/virtual-node/virtual-node.js +++ b/test/core/base/virtual-node/virtual-node.js @@ -226,6 +226,54 @@ describe('VirtualNode', function() { }); }); + describe('getComputedStylePropertyValue', function() { + var computedStyle; + + beforeEach(function() { + computedStyle = window.getComputedStyle; + }); + + afterEach(function() { + window.getComputedStyle = computedStyle; + }); + + it('should call window.getComputedStyle and return the property', function() { + var called = false; + window.getComputedStyle = function() { + called = true; + return { + getPropertyValue: function() { + return 'result'; + } + }; + }; + var vNode = new VirtualNode(node); + var result = vNode.getComputedStylePropertyValue('prop'); + + assert.isTrue(called); + assert.equal(result, 'result'); + }); + + it('should only call window.getComputedStyle and getPropertyValue once', function() { + var computedCount = 0; + var propertyCount = 0; + window.getComputedStyle = function() { + computedCount++; + return { + getPropertyValue: function() { + propertyCount++; + } + }; + }; + var vNode = new VirtualNode(node); + vNode.getComputedStylePropertyValue('prop'); + vNode.getComputedStylePropertyValue('prop'); + vNode.getComputedStylePropertyValue('prop'); + assert.equal(computedCount, 1); + assert.equal(propertyCount, 1); + }); + }); + describe('clientRects', function() { it('should call node.getClientRects', function() { var called = false; From bb1c64a0d5011d0e1bedee051bf1f2cc0290ebc4 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 29 Oct 2019 13:24:07 -0600 Subject: [PATCH 12/14] remove CSS rotation --- lib/commons/dom/get-element-stack.js | 64 ++------------------------- test/commons/dom/get-element-stack.js | 35 --------------- 2 files changed, 3 insertions(+), 96 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index cbd0c631ca..c86cbed1b3 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -3,9 +3,6 @@ // split the page cells to group elements by the position const gridSize = 200; // arbitrary size, increase to reduce memory (less cells) use but increase time (more nodes per grid to check collision) -// https://developer.mozilla.org/en-US/docs/Web/CSS/Replaced_element -const replacedElements = ['iframe', 'video', 'embed', 'img']; - /** * Determine if node produces a stacking context. * References: @@ -338,7 +335,7 @@ function createGrid() { .querySelectorAll(axe._tree[0], '*') .filter(vNode => vNode.actualNode.parentElement !== document.head) .forEach(vNode => { - if (vNode.actualNode.nodeType !== 1) { + if (vNode.actualNode.nodeType !== window.Node.ELEMENT_NODE) { return; } @@ -401,10 +398,8 @@ dom.getElementStack = function(vNode, recursed) { // went with pixel perfect collision rather than rounding const row = Math.floor(y / gridSize); const col = Math.floor(x / gridSize); - let stack = grid.cells[row][col].filter(vNode => { - const rects = vNode.clientRects; - const nodeName = vNode.actualNode.nodeName.toLowerCase(); - return rects.find(rect => { + let stack = grid.cells[row][col].filter(gridCellNode => { + return gridCellNode.clientRects.find(rect => { let pointX = x; let pointY = y; @@ -413,59 +408,6 @@ dom.getElementStack = function(vNode, recursed) { let rectX = getDomPosition(rect, 'x'); let rectY = getDomPosition(rect, 'y'); - const transform = - vNode.getComputedStylePropertyValue('-webkit-transform') || - vNode.getComputedStylePropertyValue('-ms-transform') || - vNode.getComputedStylePropertyValue('transform') || - 'none'; - - // if rect is rotated, rotate point around center of rect and then - // perform collision detection as though the rect was not rotated - if ( - transform.indexOf('matrix') === 0 && - // only non-replaced inline elements can be rotated - // see https://developer.mozilla.org/en-US/docs/Web/CSS/transform - (replacedElements.includes(nodeName) || - (vNode.getComputedStylePropertyValue('display') !== 'inline' && - vNode.getComputedStylePropertyValue('display') !== 'contents')) - ) { - // rect will no longer be the true width/height of element so we need - // to look at offsetWidth/Height - // @see https://stackoverflow.com/questions/40809153/get-element-dimensions-regardless-of-rotation - rectWidth = vNode.actualNode.offsetWidth; - rectHeight = vNode.actualNode.offsetHeight; - - // calculate non-rotated rect x/y based on the center point of the - // rotated rect - const centerX = rectX + rect.width / 2; - const centerY = rectY + rect.height / 2; - rectX = centerX - rectWidth / 2; - rectY = centerY - rectHeight / 2; - - // calculate the angle of rotation from the rotation matrix - // @see https://css-tricks.com/get-value-of-css-rotation-through-javascript/ - let values = transform.split('(')[1]; - values = values.split(')')[0]; - values = values.split(','); - const a = values[0]; - const b = values[1]; - const angle = -Math.atan2(b, a); // rotate in the reverse direction - - // rotate point around center of rect and then perform collision - // detection as though the rect was not rotated - // @see https://www.gamefromscratch.com/post/2012/11/24/GameDev-math-recipes-Rotating-one-point-around-another-point.aspx - if (angle !== 0) { - pointX = - Math.cos(angle) * (x - centerX) - - Math.sin(angle) * (y - centerY) + - centerX; - pointY = - Math.sin(angle) * (x - centerX) + - Math.cos(angle) * (y - centerY) + - centerY; - } - } - // perform an AABB (axis-aligned bounding box) collision check for the // point inside the rect return ( diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 5d7df21bb3..5fc851d42b 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -282,41 +282,6 @@ describe('dom.getElementStack', function() { assert.deepEqual(stack, ['target', '2', '1', 'fixture']); }); - it('should handle CSS rotation', function() { - var vNode = queryFixture( - '
' + - '
' + - '
transform:rotate
' + - '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - assert.deepEqual(stack, ['target', '2', '1', 'fixture']); - }); - - it("should handle CSS rotation when applied to an element that can't be rotated", function() { - var vNode = queryFixture( - '
' + - '
' + - 'transform:rotate' + - '
' + - '
' + - '
' - ); - var stack = []; - try { - stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); - } catch (e) { - console.log(e); - } - assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); - }); - it('should handle negative z-index', function() { var vNode = queryFixture( '
' + From 33502923d8800a21d90e5a6e59fe80eb77bb2c50 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 29 Oct 2019 13:32:39 -0600 Subject: [PATCH 13/14] remove computedStyle getter --- lib/commons/dom/get-element-stack.js | 9 +++--- lib/core/base/virtual-node/virtual-node.js | 19 ++++------- test/core/base/virtual-node/virtual-node.js | 35 --------------------- 3 files changed, 11 insertions(+), 52 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index c86cbed1b3..f86a7519d8 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -23,8 +23,6 @@ function isStackingContext(vNode) { return true; } - const computedStyle = vNode.computedStyle; - // position: fixed or sticky if ( vNode.getComputedStylePropertyValue('position') === 'fixed' || @@ -35,7 +33,7 @@ function isStackingContext(vNode) { // positioned (absolutely or relatively) with a z-index value other than "auto", if ( - computedStyle.zIndex !== 'auto' && + vNode.getComputedStylePropertyValue('z-index') !== 'auto' && vNode.getComputedStylePropertyValue('position') !== 'static' ) { return true; @@ -95,7 +93,10 @@ function isStackingContext(vNode) { } // elements with -webkit-overflow-scrolling set to "touch" - if (computedStyle.webkitOverflowScrolling === 'touch') { + if ( + vNode.getComputedStylePropertyValue('-webkit-overflow-scrolling') === + 'touch' + ) { return true; } diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index 7f7d7d150b..e4b6e47f45 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -62,14 +62,18 @@ class VirtualNode extends axe.AbstractVirtualNode { } /** - * Return a property of the computed style for this element and cache the result. + * Return a property of the computed style for this element and cache the result. This is much faster than called `getPropteryValue` every time. * @see https://jsperf.com/get-property-value * @return {String} */ getComputedStylePropertyValue(property) { const key = 'computedStyle_' + property; if (!this._cache.hasOwnProperty(key)) { - this._cache[key] = this.computedStyle.getPropertyValue(property); + if (!this._cache.hasOwnProperty('computedStyle')) { + this._cache.computedStyle = window.getComputedStyle(this.actualNode); + } + + this._cache[key] = this._cache.computedStyle.getPropertyValue(property); } return this._cache[key]; } @@ -96,17 +100,6 @@ class VirtualNode extends axe.AbstractVirtualNode { return this._cache.tabbableElements; } - /** - * Return the computed style for this element and cache the result. - * @return {CSSStyleDeclaration} - */ - get computedStyle() { - if (!this._cache.hasOwnProperty('computedStyle')) { - this._cache.computedStyle = window.getComputedStyle(this.actualNode); - } - return this._cache.computedStyle; - } - /** * Return the client rects for this element and cache the result. * @return {DOMRect[]} diff --git a/test/core/base/virtual-node/virtual-node.js b/test/core/base/virtual-node/virtual-node.js index ad491ca77e..47c96a78d1 100644 --- a/test/core/base/virtual-node/virtual-node.js +++ b/test/core/base/virtual-node/virtual-node.js @@ -191,41 +191,6 @@ describe('VirtualNode', function() { }); }); - describe('computedStyle', function() { - var computedStyle; - - beforeEach(function() { - computedStyle = window.getComputedStyle; - }); - - afterEach(function() { - window.getComputedStyle = computedStyle; - }); - - it('should call window.getComputedStyle', function() { - var called = false; - window.getComputedStyle = function() { - called = true; - }; - var vNode = new VirtualNode(node); - vNode.computedStyle; - - assert.isTrue(called); - }); - - it('should only call window.getComputedStyle once', function() { - var count = 0; - window.getComputedStyle = function() { - count++; - }; - var vNode = new VirtualNode(node); - vNode.computedStyle; - vNode.computedStyle; - vNode.computedStyle; - assert.equal(count, 1); - }); - }); - describe('getComputedStylePropertyValue', function() { var computedStyle; From 0c688122cb26da7ba48c7e43a4bdde58dae6ce2b Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 14 Nov 2019 16:08:00 -0700 Subject: [PATCH 14/14] update --- lib/commons/dom/get-element-stack.js | 93 ++++++++++++++++++--------- test/commons/dom/get-element-stack.js | 2 +- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index f86a7519d8..2ce44bc9ed 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -79,6 +79,44 @@ function isStackingContext(vNode) { return true; } + // element with a clip-path value other than "none" + if ( + vNode.getComputedStylePropertyValue('clip-path') && + vNode.getComputedStylePropertyValue('clip-path') !== 'none' + ) { + return true; + } + + // element with a mask value other than "none" + const mask = + vNode.getComputedStylePropertyValue('-webkit-mask') || + vNode.getComputedStylePropertyValue('mask') || + 'none'; + + if (mask !== 'none') { + return true; + } + + // element with a mask-image value other than "none" + const maskImage = + vNode.getComputedStylePropertyValue('-webkit-mask-image') || + vNode.getComputedStylePropertyValue('mask-image') || + 'none'; + + if (maskImage !== 'none') { + return true; + } + + // element with a mask-border value other than "none" + const maskBorder = + vNode.getComputedStylePropertyValue('-webkit-mask-border') || + vNode.getComputedStylePropertyValue('mask-border') || + 'none'; + + if (maskBorder !== 'none') { + return true; + } + // elements with isolation set to "isolate" if (vNode.getComputedStylePropertyValue('isolation') === 'isolate') { return true; @@ -100,15 +138,28 @@ function isStackingContext(vNode) { return true; } - // a flex item with a z-index value other than "auto", that is the parent element display: flex|inline-flex, + // element with a contain value of "layout" or "paint" or a composite value + // that includes either of them (i.e. contain: strict, contain: content). + const contain = vNode.getComputedStylePropertyValue('contain'); + if (['layout', 'paint', 'strict', 'content'].includes(contain)) { + return true; + } + + // a flex item or gird item with a z-index value other than "auto", that is the parent element display: flex|inline-flex|grid|inline-grid, if ( vNode.getComputedStylePropertyValue('z-index') !== 'auto' && vNode.parent ) { - const parent = vNode.parent; + const parentDsiplay = vNode.parent.getComputedStylePropertyValue('display'); if ( - parent.getComputedStylePropertyValue('display') === 'flex' || - parent.getComputedStylePropertyValue('display') === 'inline-flex' + [ + 'flex', + 'inline-flex', + 'inline flex', + 'grid', + 'inline-grid', + 'inline grid' + ].includes(parentDsiplay) ) { return true; } @@ -161,7 +212,6 @@ function visuallySort(a, b) { /*eslint no-bitwise: 0 */ // 1. The root element forms the root stacking context. - // TODO: how do we test this? if (a.actualNode.nodeName.toLowerCase() === 'html') { return 1; } @@ -174,21 +224,18 @@ function visuallySort(a, b) { return -1; } - if (a._stackingOrder[i] === b._stackingOrder[i]) { - continue; - } - // 7. the child stacking contexts with positive stack levels (least positive first). if (b._stackingOrder[i] > a._stackingOrder[i]) { return 1; } // 2. the child stacking contexts with negative stack levels (most negative first). - return -1; + if (b._stackingOrder[i] < a._stackingOrder[i]) { + return -1; + } } - // nodes are the same stacking order, compute special cases for when f - // is not set + // nodes are the same stacking order const docPosition = a.actualNode.compareDocumentPosition(b.actualNode); const DOMOrder = docPosition & 4 ? 1 : -1; const isDescendant = docPosition & 8 || docPosition & 16; @@ -226,20 +273,6 @@ function getStackingOrder(vNode) { return stackingOrder; } -/** - * Determine if a node is a scroll region. - * @param {VirtualNode} - * @return {Boolean} - */ -function isScrollRegion(vNode) { - return ( - vNode.getComputedStylePropertyValue('overflow-x') === 'auto' || - vNode.getComputedStylePropertyValue('overflow-x') === 'scroll' || - vNode.getComputedStylePropertyValue('overflow-y') === 'auto' || - vNode.getComputedStylePropertyValue('overflow-y') === 'scroll' - ); -} - /** * Return the parent node that is a scroll region. * @param {VirtualNode} @@ -256,7 +289,7 @@ function findScrollRegionParent(vNode) { break; } - if (isScrollRegion(vNodeParent)) { + if (axe.utils.getScroll(vNodeParent.actualNode)) { scrollRegionParent = vNodeParent; break; } @@ -353,7 +386,7 @@ function createGrid() { const scrollRegionParent = findScrollRegionParent(vNode); const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; - if (isScrollRegion(vNode)) { + if (axe.utils.getScroll(vNode.actualNode)) { const subGrid = { container: vNode, cells: [] @@ -370,10 +403,10 @@ function createGrid() { * @method getElementStack * @memberof axe.commons.dom * @param {VirtualNode} vNode - * @param {Boolean} recursed If the function has been called recursively + * @param {Boolean} [recursed] If the function has been called recursively * @return {VirtualNode[]} */ -dom.getElementStack = function(vNode, recursed) { +dom.getElementStack = function(vNode, recursed = false) { if (!axe._cache.get('gridCreated')) { createGrid(); axe._cache.set('gridCreated', true); diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 5fc851d42b..52adc87cce 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -248,7 +248,7 @@ describe('dom.getElementStack', function() { var vNode = queryFixture( '
' + '
' + - '

Text oh heyyyy and here\'s
a link

' + + '

Text oh heyyyy and here\'s
a link

' + '
' ); var stack = getElementStack(vNode).map(function(vNode) {