From 81e2a01db25a41d24afa13f84bfe440b7ad1f1a1 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 2 Mar 2023 17:40:15 -0700 Subject: [PATCH 1/8] fix(create-grid): correctly compute stack order for non-positioned stacking contexts --- lib/commons/dom/create-grid.js | 45 ++++++++++++++++++--------- test/commons/dom/get-element-stack.js | 19 +++++++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index f522ddd17e..5485e69f81 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -255,26 +255,41 @@ function getStackingOrder(vNode, parentVNode) { vNode.getComputedStylePropertyValue('position') !== 'static'; const floated = vNode.getComputedStylePropertyValue('float') !== 'none'; - // flex and grid items can use z-index even if position: static - // @see https://www.w3.org/TR/css-flexbox-1/#painting - // @see https://www.w3.org/TR/css-grid-1/#z-order - if ( - !['auto', '0'].includes(zIndex) && - (positioned || isFlexOrGridContainer(parentVNode)) - ) { - // if a positioned element has a z-index > 0, find the first + if (isStackingContext(vNode, parentVNode)) { + // if an element creates a stacking context, find the first // true stack (not a "fake" stack created from positioned or // floated elements without a z-index) and create a new stack at // that point (step #5 and step #8) // @see https://www.w3.org/Style/css2-updates/css2/zindex.html - while (stackingOrder.find(value => value % 1 !== 0)) { - const index = stackingOrder.findIndex(value => value % 1 !== 0); - stackingOrder.splice(index, 1); + const index = stackingOrder.findIndex( + value => (value < 1 && value > 0.2) || value === 0 + ); + if (index !== -1) { + stackingOrder.splice(index, stackingOrder.length - index); + } + + // flex and grid items can use z-index even if position: static + // @see https://www.w3.org/TR/css-flexbox-1/#painting + // @see https://www.w3.org/TR/css-grid-1/#z-order + if ( + !['auto', '0'].includes(zIndex) && + (positioned || isFlexOrGridContainer(parentVNode)) + ) { + stackingOrder.push(parseInt(zIndex)); + } + // since many things can create a new stacking context without position or + // z-index, we need to know the order in the dom to sort them by. Use the + // nodeIndex property to create a number less than the "fake" stacks from + // positioned or floated elements but still larger than 0 + // 10 pad gives us the ability to sort up to 1B nodes (padStart does not + // exist in ie11) + else { + let float = vNode.nodeIndex.toString(); + while (float.length < 10) { + float = '0' + float; + } + stackingOrder.push(parseFloat('0.' + float)); } - stackingOrder[stackingOrder.length - 1] = parseInt(zIndex); - } - if (isStackingContext(vNode, parentVNode)) { - stackingOrder.push(0); } // if a positioned element has z-index: auto or 0 (step #8), or if // a non-positioned floating element (step #5), treat it as its diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 6f99c999d0..26a68e111a 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -399,6 +399,25 @@ describe('dom.getElementStack', () => { assert.deepEqual(stack, ['5', 'target', '4', '3', '2', '1', 'fixture']); }); + it('should correctly position opacity elements and positioned elements', () => { + fixture.innerHTML = ` +
+
+

Hello World

+
+
+
+
+
+
+
+ `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['5', '4', 'target', '2', '1', 'fixture']); + }); + it('should return empty array for hidden elements', () => { fixture.innerHTML = '
' + From 46d38322d86fbb5611cf30a0bf0eb59e7379136e Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Mon, 6 Mar 2023 17:48:18 +0100 Subject: [PATCH 2/8] chore: Refactor createStackingOrder (#3932) * chore: Refactor createStackingOrder * Remove magic numbers --- lib/commons/dom/create-grid.js | 112 ++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 5485e69f81..050adb80db 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -7,6 +7,11 @@ import constants from '../../core/constants'; import cache from '../../core/base/cache'; import assert from '../../core/utils/assert'; +const ROOT_ORDER = 0; +const DEFAULT_ORDER = 0.1; +const FLOAT_ORDER = 0.2; +const POSITION_STATIC_ORDER = 0.3; + /** * Setup the 2d grid and add every element to it, even elements not * included in the flat tree @@ -32,7 +37,7 @@ export default function createGrid( vNode = new VirtualNode(document.documentElement); } - vNode._stackingOrder = [0]; + vNode._stackingOrder = [ROOT_ORDER]; rootGrid ??= new Grid(); addNodeToGrid(rootGrid, vNode); @@ -57,11 +62,11 @@ export default function createGrid( if (vNode && vNode.parent) { parentVNode = vNode.parent; } - // elements with an assigned slot need to be a child of the slot element + // Elements with an assigned slot need to be a child of the slot element else if (node.assignedSlot) { parentVNode = getNodeFromTree(node.assignedSlot); } - // an svg in IE11 does not have a parentElement but instead has a + // An SVG in IE11 does not have a parentElement but instead has a // parentNode. but parentNode could be a shadow root so we need to // verify it's in the tree first else if (node.parentElement) { @@ -74,7 +79,7 @@ export default function createGrid( vNode = new axe.VirtualNode(node, parentVNode); } - vNode._stackingOrder = getStackingOrder(vNode, parentVNode); + vNode._stackingOrder = createStackingOrder(vNode, parentVNode); const scrollRegionParent = findScrollRegionParent(vNode, parentVNode); const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; @@ -248,65 +253,68 @@ function isFlexOrGridContainer(vNode) { * @param {VirtualNode} * @return {Number[]} */ -function getStackingOrder(vNode, parentVNode) { +function createStackingOrder(vNode, parentVNode) { const stackingOrder = parentVNode._stackingOrder.slice(); - const zIndex = vNode.getComputedStylePropertyValue('z-index'); - const positioned = - vNode.getComputedStylePropertyValue('position') !== 'static'; - const floated = vNode.getComputedStylePropertyValue('float') !== 'none'; - - if (isStackingContext(vNode, parentVNode)) { - // if an element creates a stacking context, find the first - // true stack (not a "fake" stack created from positioned or - // floated elements without a z-index) and create a new stack at - // that point (step #5 and step #8) - // @see https://www.w3.org/Style/css2-updates/css2/zindex.html - const index = stackingOrder.findIndex( - value => (value < 1 && value > 0.2) || value === 0 - ); - if (index !== -1) { - stackingOrder.splice(index, stackingOrder.length - index); - } - // flex and grid items can use z-index even if position: static - // @see https://www.w3.org/TR/css-flexbox-1/#painting - // @see https://www.w3.org/TR/css-grid-1/#z-order - if ( - !['auto', '0'].includes(zIndex) && - (positioned || isFlexOrGridContainer(parentVNode)) - ) { - stackingOrder.push(parseInt(zIndex)); - } - // since many things can create a new stacking context without position or - // z-index, we need to know the order in the dom to sort them by. Use the - // nodeIndex property to create a number less than the "fake" stacks from - // positioned or floated elements but still larger than 0 - // 10 pad gives us the ability to sort up to 1B nodes (padStart does not - // exist in ie11) - else { - let float = vNode.nodeIndex.toString(); - while (float.length < 10) { - float = '0' + float; - } - stackingOrder.push(parseFloat('0.' + float)); - } - } // if a positioned element has z-index: auto or 0 (step #8), or if // a non-positioned floating element (step #5), treat it as its // own stacking context // @see https://www.w3.org/Style/css2-updates/css2/zindex.html - else if (positioned) { - // Put positioned elements above floated elements - stackingOrder.push(0.5); - } else if (floated) { - // Put floated elements above z-index: 0 - // (step #5 floating get sorted below step #8 positioned) - stackingOrder.push(0.25); + if (!isStackingContext(vNode, parentVNode)) { + if (vNode.getComputedStylePropertyValue('position') !== 'static') { + // Put positioned elements above floated elements + stackingOrder.push(POSITION_STATIC_ORDER); + } else if (vNode.getComputedStylePropertyValue('float') !== 'none') { + // Put floated elements above z-index: 0 + // (step #5 floating get sorted below step #8 positioned) + stackingOrder.push(FLOAT_ORDER); + } + return stackingOrder; + } + + // if an element creates a stacking context, find the first + // true stack (not a "fake" stack created from positioned or + // floated elements without a z-index) and create a new stack at + // that point (step #5 and step #8) + // @see https://www.w3.org/Style/css2-updates/css2/zindex.html + const index = stackingOrder.findIndex(value => + [ROOT_ORDER, FLOAT_ORDER, POSITION_STATIC_ORDER].includes(value) + ); + if (index !== -1) { + stackingOrder.splice(index, stackingOrder.length - index); + } + + const zIndex = getRealZIndex(vNode, parentVNode); + if (!['auto', '0'].includes(zIndex)) { + stackingOrder.push(parseInt(zIndex)); + return stackingOrder; + } + // since many things can create a new stacking context without position or + // z-index, we need to know the order in the dom to sort them by. Use the + // nodeIndex property to create a number less than the "fake" stacks from + // positioned or floated elements but still larger than 0 + // 10 pad gives us the ability to sort up to 1B nodes (padStart does not + // exist in ie11) + let float = vNode.nodeIndex.toString(); + while (float.length < 10) { + float = '0' + float; } + stackingOrder.push(DEFAULT_ORDER + parseFloat('0.0' + float)); return stackingOrder; } +function getRealZIndex(vNode, parentVNode) { + const position = vNode.getComputedStylePropertyValue('position'); + if (position === 'static' && !isFlexOrGridContainer(parentVNode)) { + // z-index is ignored on position:static, except if on a flex or grid + // @see https://www.w3.org/TR/css-flexbox-1/#painting + // @see https://www.w3.org/TR/css-grid-1/#z-order + return 'auto'; + } + return vNode.getComputedStylePropertyValue('z-index'); +} + /** * Return the parent node that is a scroll region. * @param {VirtualNode} From e269217dffb7de9686e194f3cdb4ce2ef692c032 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 8 Mar 2023 09:18:53 -0700 Subject: [PATCH 3/8] use treewalker nodeIndex --- lib/commons/dom/create-grid.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 050adb80db..6d974aba0d 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -49,6 +49,7 @@ export default function createGrid( // IE11 requires the first 3 parameters // @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker + let nodeIndex = 0; const treeWalker = document.createTreeWalker( root, window.NodeFilter.SHOW_ELEMENT, @@ -79,7 +80,7 @@ export default function createGrid( vNode = new axe.VirtualNode(node, parentVNode); } - vNode._stackingOrder = createStackingOrder(vNode, parentVNode); + vNode._stackingOrder = createStackingOrder(vNode, parentVNode, nodeIndex++); const scrollRegionParent = findScrollRegionParent(vNode, parentVNode); const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; @@ -250,10 +251,12 @@ function isFlexOrGridContainer(vNode) { /** * Determine the stacking order of an element. The stacking order is an array of * zIndex values for each stacking context parent. - * @param {VirtualNode} + * @param {VirtualNode} vNode + * @param {VirtualNode} parentVNode + * @param {Number} nodeIndex * @return {Number[]} */ -function createStackingOrder(vNode, parentVNode) { +function createStackingOrder(vNode, parentVNode, nodeIndex) { const stackingOrder = parentVNode._stackingOrder.slice(); // if a positioned element has z-index: auto or 0 (step #8), or if @@ -295,7 +298,7 @@ function createStackingOrder(vNode, parentVNode) { // positioned or floated elements but still larger than 0 // 10 pad gives us the ability to sort up to 1B nodes (padStart does not // exist in ie11) - let float = vNode.nodeIndex.toString(); + let float = nodeIndex.toString(); while (float.length < 10) { float = '0' + float; } From b8bfb2967a7321478b6079956d87866d85f16a2a Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 8 Mar 2023 09:24:52 -0700 Subject: [PATCH 4/8] no floatpoint precision errors --- lib/commons/dom/create-grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 6d974aba0d..1a899c1080 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -302,7 +302,7 @@ function createStackingOrder(vNode, parentVNode, nodeIndex) { while (float.length < 10) { float = '0' + float; } - stackingOrder.push(DEFAULT_ORDER + parseFloat('0.0' + float)); + stackingOrder.push(parseFloat(`${DEFAULT_ORDER}${float}`)); return stackingOrder; } From f40d21aceadd006451c9d59b980acdd706bf51a6 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 8 Mar 2023 09:27:01 -0700 Subject: [PATCH 5/8] pass nodeIndex to createGrid for shadowDom --- lib/commons/dom/create-grid.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 1a899c1080..858096bc90 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -20,7 +20,8 @@ const POSITION_STATIC_ORDER = 0.3; export default function createGrid( root = document.body, rootGrid, - parentVNode = null + parentVNode = null, + nodeIndex = 0 ) { // Prevent multiple calls per run if (cache.get('gridCreated') && !parentVNode) { @@ -49,7 +50,7 @@ export default function createGrid( // IE11 requires the first 3 parameters // @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker - let nodeIndex = 0; + const treeWalker = document.createTreeWalker( root, window.NodeFilter.SHOW_ELEMENT, @@ -100,7 +101,7 @@ export default function createGrid( // add shadow root elements to the grid if (isShadowRoot(node)) { - createGrid(node.shadowRoot, grid, vNode); + createGrid(node.shadowRoot, grid, vNode, nodeIndex); } node = treeWalker.nextNode(); From b160c3d6415723ed294cbab9e326bfdd022da573 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 8 Mar 2023 09:36:55 -0700 Subject: [PATCH 6/8] do it properly --- lib/commons/dom/create-grid.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 858096bc90..1e23926711 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -11,6 +11,7 @@ const ROOT_ORDER = 0; const DEFAULT_ORDER = 0.1; const FLOAT_ORDER = 0.2; const POSITION_STATIC_ORDER = 0.3; +let nodeIndex = 0; /** * Setup the 2d grid and add every element to it, even elements not @@ -20,8 +21,7 @@ const POSITION_STATIC_ORDER = 0.3; export default function createGrid( root = document.body, rootGrid, - parentVNode = null, - nodeIndex = 0 + parentVNode = null ) { // Prevent multiple calls per run if (cache.get('gridCreated') && !parentVNode) { @@ -38,6 +38,7 @@ export default function createGrid( vNode = new VirtualNode(document.documentElement); } + nodeIndex = 0; vNode._stackingOrder = [ROOT_ORDER]; rootGrid ??= new Grid(); addNodeToGrid(rootGrid, vNode); @@ -101,7 +102,7 @@ export default function createGrid( // add shadow root elements to the grid if (isShadowRoot(node)) { - createGrid(node.shadowRoot, grid, vNode, nodeIndex); + createGrid(node.shadowRoot, grid, vNode); } node = treeWalker.nextNode(); From 0cd2b0d929ad8ad0c198bcb4ea03f0407bb67887 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 8 Mar 2023 09:37:32 -0700 Subject: [PATCH 7/8] remove whitespace --- lib/commons/dom/create-grid.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 1e23926711..d2fed47748 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -51,7 +51,6 @@ export default function createGrid( // IE11 requires the first 3 parameters // @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker - const treeWalker = document.createTreeWalker( root, window.NodeFilter.SHOW_ELEMENT, From 99154562743301de7fb909a14d40e34351a1978c Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 14 Mar 2023 09:32:12 -0600 Subject: [PATCH 8/8] test for outside tree elements --- test/commons/dom/get-element-stack.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 26a68e111a..df466da016 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -418,6 +418,25 @@ describe('dom.getElementStack', () => { assert.deepEqual(stack, ['5', '4', 'target', '2', '1', 'fixture']); }); + it('should correctly order elements outside of the axe tree', () => { + fixture.innerHTML = ` +
+
+

Hello World

+
+
+
+
+
+
+
+ `; + axe.testUtils.flatTreeSetup(fixture.querySelector('#tree')); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['5', '4', 'target', '2', '1', 'fixture']); + }); + it('should return empty array for hidden elements', () => { fixture.innerHTML = '
' +