From 8db2c2492d55a903b7903ed71f8b792e58dc2e8c Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Wed, 15 Mar 2023 08:49:28 -0600 Subject: [PATCH] fix(create-grid): correctly compute stack order for non-positioned stacking contexts (#3930) * fix(create-grid): correctly compute stack order for non-positioned stacking contexts * chore: Refactor createStackingOrder (#3932) * chore: Refactor createStackingOrder * Remove magic numbers * use treewalker nodeIndex * no floatpoint precision errors * pass nodeIndex to createGrid for shadowDom * do it properly * remove whitespace * test for outside tree elements --------- Co-authored-by: Wilco Fiers --- lib/commons/dom/create-grid.js | 103 ++++++++++++++++---------- test/commons/dom/get-element-stack.js | 38 ++++++++++ 2 files changed, 103 insertions(+), 38 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index f522ddd17e..d2fed47748 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -7,6 +7,12 @@ 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; +let nodeIndex = 0; + /** * Setup the 2d grid and add every element to it, even elements not * included in the flat tree @@ -32,7 +38,8 @@ export default function createGrid( vNode = new VirtualNode(document.documentElement); } - vNode._stackingOrder = [0]; + nodeIndex = 0; + vNode._stackingOrder = [ROOT_ORDER]; rootGrid ??= new Grid(); addNodeToGrid(rootGrid, vNode); @@ -57,11 +64,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 +81,7 @@ export default function createGrid( vNode = new axe.VirtualNode(node, parentVNode); } - vNode._stackingOrder = getStackingOrder(vNode, parentVNode); + vNode._stackingOrder = createStackingOrder(vNode, parentVNode, nodeIndex++); const scrollRegionParent = findScrollRegionParent(vNode, parentVNode); const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; @@ -245,53 +252,73 @@ 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 getStackingOrder(vNode, parentVNode) { +function createStackingOrder(vNode, parentVNode, nodeIndex) { const stackingOrder = parentVNode._stackingOrder.slice(); - const zIndex = vNode.getComputedStylePropertyValue('z-index'); - const positioned = - 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 - // 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); - } - 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 // 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 = nodeIndex.toString(); + while (float.length < 10) { + float = '0' + float; + } + stackingOrder.push(parseFloat(`${DEFAULT_ORDER}${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} diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 6f99c999d0..df466da016 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -399,6 +399,44 @@ 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 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 = '
' +