Skip to content

Commit

Permalink
fix(create-grid): correctly compute stack order for non-positioned st…
Browse files Browse the repository at this point in the history
…acking 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 <[email protected]>
  • Loading branch information
straker and WilcoFiers authored Mar 15, 2023
1 parent 235e632 commit 8db2c24
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 38 deletions.
103 changes: 65 additions & 38 deletions lib/commons/dom/create-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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}
Expand Down
38 changes: 38 additions & 0 deletions test/commons/dom/get-element-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div id="1" style="opacity: 0.9;">
<div id="2" style="position: relative; z-index: 2">
<h1 id="target">Hello World</h1>
</div>
</div>
<div id="3" style="opacity: 0.8;">
<div id="4" style="position: absolute; top: 20px; z-index: -1;">
<div id="5" style="height: 40px; width: 100vw; background: red"></div>
</div>
</div>
`;
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 = `
<div id="1" style="opacity: 0.9;">
<div id="2" style="position: relative; z-index: 2">
<h1 id="target">Hello World</h1>
</div>
</div>
<div id="tree" style="opacity: 0.8;">
<div id="4" style="position: absolute; top: 20px; z-index: -1;">
<div id="5" style="height: 40px; width: 100vw; background: red"></div>
</div>
</div>
`;
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 =
'<main id="1">' +
Expand Down

0 comments on commit 8db2c24

Please sign in to comment.