diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 465acdc0f2..14eac3f9d4 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -7,10 +7,10 @@ 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; +const ROOT_LEVEL = 0; +const DEFAULT_LEVEL = 0.1; +const FLOAT_LEVEL = 0.2; +const POSITION_LEVEL = 0.3; let nodeIndex = 0; /** @@ -39,7 +39,9 @@ export default function createGrid( } nodeIndex = 0; - vNode._stackingOrder = [createContext(ROOT_ORDER, null)]; + vNode._stackingOrder = [ + createStackingContext(ROOT_LEVEL, nodeIndex++, null) + ]; rootGrid ??= new Grid(); addNodeToGrid(rootGrid, vNode); @@ -251,72 +253,94 @@ function isFlexOrGridContainer(vNode) { /** * Determine the stacking order of an element. The stacking order is an array of - * zIndex values for each stacking context parent. + * stacking contexts in ancestor order. * @param {VirtualNode} vNode * @param {VirtualNode} parentVNode - * @param {Number} nodeIndex + * @param {Number} treeOrder * @return {Number[]} */ -function createStackingOrder(vNode, parentVNode, nodeIndex) { +function createStackingOrder(vNode, parentVNode, treeOrder) { const stackingOrder = parentVNode._stackingOrder.slice(); - // 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 - if (!isStackingContext(vNode, parentVNode)) { - if (vNode.getComputedStylePropertyValue('position') !== 'static') { - // Put positioned elements above floated elements - stackingOrder.push(createContext(POSITION_STATIC_ORDER, vNode)); - } else if (vNode.getComputedStylePropertyValue('float') !== 'none') { - // Put floated elements above z-index: 0 - // (step #5 floating get sorted below step #8 positioned) - stackingOrder.push(createContext(FLOAT_ORDER, vNode)); - } - 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); + if (isStackingContext(vNode, parentVNode)) { + const index = stackingOrder.findIndex(({ stackLevel }) => + [ROOT_LEVEL, FLOAT_LEVEL, POSITION_LEVEL].includes(stackLevel) + ); + if (index !== -1) { + stackingOrder.splice(index, stackingOrder.length - index); + } } - const zIndex = getRealZIndex(vNode, parentVNode); - if (!['auto', '0'].includes(zIndex)) { - stackingOrder.push(createContext(parseInt(zIndex), vNode)); - 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( - createContext(parseFloat(`${DEFAULT_ORDER}${float}`), vNode) - ); - + const stackLevel = getStackLevel(vNode, parentVNode); + if (stackLevel !== null) { + stackingOrder.push(createStackingContext(stackLevel, treeOrder, vNode)); + } return stackingOrder; } -function createContext(value, vNode) { +/** + * Create a stacking context, keeping track of the stack level, tree order, and virtual + * node container. + * @see https://www.w3.org/Style/css2-updates/css2/zindex.html + * @see https://www.w3.org/Style/css2-updates/css2/visuren.html#layers + * @param {Number} stackLevel - The stack level of the stacking context + * @param {Number} treeOrder - The elements depth-first traversal order + * @param {VirtualNode} vNode - The virtual node that is the container for the stacking context + */ +function createStackingContext(stackLevel, treeOrder, vNode) { return { - value, + stackLevel, + treeOrder, vNode }; } +/** + * Calculate the level of the stacking context. + * @param {VirtualNode} vNode - The virtual node container of the stacking context + * @param {VirtualNode} parentVNode - The parent virtual node of the vNode + * @return {Number|null} + */ +function getStackLevel(vNode, parentVNode) { + const zIndex = getRealZIndex(vNode, parentVNode); + if (!['auto', '0'].includes(zIndex)) { + return parseInt(zIndex); + } + + // 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 + + // Put positioned elements above floated elements + if (vNode.getComputedStylePropertyValue('position') !== 'static') { + return POSITION_LEVEL; + } + + // Put floated elements above z-index: 0 + // (step #5 floating get sorted below step #8 positioned) + if (vNode.getComputedStylePropertyValue('float') !== 'none') { + return FLOAT_LEVEL; + } + + if (isStackingContext(vNode, parentVNode)) { + return DEFAULT_LEVEL; + } + + return null; +} + +/** + * Calculate the z-index value of a node taking into account when doesn't apply. + * @param {VirtualNode} vNode - The virtual node to get z-index of + * @param {VirtualNode} parentVNode - The parent virtual node of the vNode + * @return {Number|'auto'} + */ function getRealZIndex(vNode, parentVNode) { const position = vNode.getComputedStylePropertyValue('position'); if (position === 'static' && !isFlexOrGridContainer(parentVNode)) { diff --git a/lib/commons/dom/visually-sort.js b/lib/commons/dom/visually-sort.js index 1ceaf8245e..ee5a0c905f 100644 --- a/lib/commons/dom/visually-sort.js +++ b/lib/commons/dom/visually-sort.js @@ -1,8 +1,10 @@ import createGrid from './create-grid'; + /** * Visually sort nodes based on their stack order * References: * https://www.w3.org/Style/css2-updates/css2/zindex.html + * https://www.w3.org/Style/css2-updates/css2/visuren.html#layers * @param {VirtualNode} * @param {VirtualNode} */ @@ -19,14 +21,19 @@ export default function visuallySort(a, b) { } // 7. the child stacking contexts with positive stack levels (least positive first). - if (b._stackingOrder[i].value > a._stackingOrder[i].value) { + if (b._stackingOrder[i].stackLevel > a._stackingOrder[i].stackLevel) { return 1; } // 2. the child stacking contexts with negative stack levels (most negative first). - if (b._stackingOrder[i].value < a._stackingOrder[i].value) { + if (b._stackingOrder[i].stackLevel < a._stackingOrder[i].stackLevel) { return -1; } + + // stacks share the same stack level so compare document order + if (b._stackingOrder[i].treeOrder !== a._stackingOrder[i].treeOrder) { + return b._stackingOrder[i].treeOrder - a._stackingOrder[i].treeOrder; + } } // nodes are the same stacking order diff --git a/test/commons/dom/create-grid.js b/test/commons/dom/create-grid.js index 5b344d3a55..8068acd276 100644 --- a/test/commons/dom/create-grid.js +++ b/test/commons/dom/create-grid.js @@ -1,13 +1,14 @@ // Additional tests for createGrid are part of createRectStack tests, // which is what createGrid was originally part of -describe('create-grid', function () { - var fixture; - var createGrid = axe.commons.dom.createGrid; - var fixtureSetup = axe.testUtils.fixtureSetup; - var gridSize = axe.constants.gridSize; +describe('create-grid', () => { + let fixture; + const createGrid = axe.commons.dom.createGrid; + const fixtureSetup = axe.testUtils.fixtureSetup; + const queryFixture = axe.testUtils.queryFixture; + const gridSize = axe.constants.gridSize; function findPositions(grid, vNode) { - var positions = []; + const positions = []; grid.loopGridPosition(grid.boundaries, (cell, position) => { if (cell.includes(vNode)) { positions.push(position); @@ -16,12 +17,12 @@ describe('create-grid', function () { return positions; } - it('returns the grid size', function () { + it('returns the grid size', () => { axe.setup(); assert.equal(createGrid(), axe.constants.gridSize); }); - it('sets ._grid to nodes in the grid', function () { + it('sets ._grid to nodes in the grid', () => { fixture = fixtureSetup('Hello world'); assert.isUndefined(fixture._grid); assert.isUndefined(fixture.children[0]._grid); @@ -31,21 +32,21 @@ describe('create-grid', function () { assert.equal(fixture._grid, fixture.children[0]._grid); }); - it('adds elements to the correct cell in the grid', function () { + it('adds elements to the correct cell in the grid', () => { fixture = fixtureSetup('Hello world'); createGrid(); - var positions = findPositions(fixture._grid, fixture.children[0]); + const positions = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(positions, [{ col: 0, row: 0 }]); }); - it('adds large elements to multiple cell', function () { + it('adds large elements to multiple cell', () => { fixture = fixtureSetup( '' + 'Hello world' ); createGrid(); - var positions = findPositions(fixture._grid, fixture.children[0]); + const positions = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(positions, [ { col: 0, row: 0 }, { col: 1, row: 0 }, @@ -54,38 +55,94 @@ describe('create-grid', function () { ]); }); - describe('hidden elements', function () { - beforeEach(function () { + describe('stackingOrder', () => { + it('adds stacking context information', () => { + fixture = fixtureSetup('Hello world'); + createGrid(); + const vNode = fixture.children[0]; + assert.lengthOf(vNode._stackingOrder, 1); + // purposefully do not test stackLevel and treeOrder values as they are + // implementation details that can easily change + assert.hasAllKeys(vNode._stackingOrder[0], [ + 'stackLevel', + 'treeOrder', + 'vNode' + ]); + assert.typeOf(vNode._stackingOrder[0].stackLevel, 'number'); + assert.typeOf(vNode._stackingOrder[0].treeOrder, 'number'); + assert.isNull(vNode._stackingOrder[0].vNode); // root stack + }); + + // when to use z-index values can be tested though + it('sets the stack level equal to the z-index of positioned elements', () => { + const vNode = queryFixture( + '