From 0c51b58a73ea38b1459d36ea65f8c1353aacdf47 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 8 Nov 2022 16:31:28 -0700 Subject: [PATCH 01/14] upload work --- lib/commons/dom/create-grid.js | 56 +++++++++++++++++++++---------- lib/commons/dom/get-rect-stack.js | 24 ++++++------- test/commons/dom/create-grid.js | 50 ++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 35 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index de6144bd1f..174cb77dd6 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -325,34 +325,56 @@ function findScrollRegionParent(vNode, parentVNode) { function addNodeToGrid(grid, vNode) { const gridSize = constants.gridSize; vNode.clientRects.forEach(rect => { - if (rect.right <= 0 || rect.bottom <= 0) { - return; - } // 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; const x = rect.left; const y = rect.top; - // "| 0" is a faster way to do Math.floor - // @see https://jsperf.com/math-floor-vs-math-round-vs-parseint/152 - const startRow = (y / gridSize) | 0; - const startCol = (x / gridSize) | 0; - const endRow = ((y + rect.height) / gridSize) | 0; - const endCol = ((x + rect.width) / gridSize) | 0; + const startRow = Math.floor(y / gridSize); + const startCol = Math.floor(x / gridSize); + + // an element that ends at a grid line should + // not be added to the next grid cell + const endRow = Math.floor((y + rect.height - 1) / gridSize); + const endCol = Math.floor((x + rect.width - 1) / gridSize); grid.numCols = Math.max(grid.numCols ?? 0, endCol); - for (let row = startRow; row <= endRow; row++) { - grid.cells[row] = grid.cells[row] || []; + addToGrid(grid, { startRow, endRow, startCol, endCol }, vNode); + }); +} - for (let col = startCol; col <= endCol; col++) { - grid.cells[row][col] = grid.cells[row][col] || []; +function addToGrid(grid, { startRow, endRow, startCol, endCol }, vNode) { + grid.cells._negativeIndex ??= 0; + loopGridCells(grid.cells, startRow, endRow, (_, row) => { + grid.cells[row] = grid.cells[row] || []; + grid.cells[row]._negativeIndex ??= 0; - if (!grid.cells[row][col].includes(vNode)) { - grid.cells[row][col].push(vNode); - } + loopGridCells(grid.cells[row], startCol, endCol, (_, col) => { + grid.cells[row][col] = grid.cells[row][col] || []; + + if (!grid.cells[row][col].includes(vNode)) { + grid.cells[row][col].push(vNode); } - } + }); }); } + +// handle negative row/col values +function loopGridCells(cells, start, end, callback) { + if (start < cells._negativeIndex) { + let num = cells._negativeIndex - start; + cells._negativeIndex = start; + while (num--) { + cells.splice(0, 0, []); + } + } + + start -= cells._negativeIndex; + end -= cells._negativeIndex; + + for (let index = start; index <= end; index++) { + callback(cells[index], index, index + cells._negativeIndex); + } +} diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index 427da62366..f22462a3ea 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -6,8 +6,8 @@ export function getRectStack(grid, rect, recursed = false) { // use center point of rect const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; - const floorX = floor(x); - const floorY = floor(y); + const floorX = Math.floor(x); + const floorY = Math.floor(y); // NOTE: there is a very rare edge case in Chrome vs Firefox that can // return different results of `document.elementsFromPoint`. If the center @@ -15,8 +15,8 @@ export function getRectStack(grid, rect, recursed = false) { // 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 = floor(y / constants.gridSize); - const col = floor(x / constants.gridSize); + const row = Math.floor(y / constants.gridSize); + const col = Math.floor(x / constants.gridSize); // we're making an assumption that there cannot be an element in the // grid which escapes the grid bounds. For example, if the grid is 4x4 there @@ -29,8 +29,9 @@ export function getRectStack(grid, rect, recursed = false) { // it is acceptable if a row has empty cells due to client rects not filling // the entire bounding rect of an element // @see https://github.com/dequelabs/axe-core/issues/3166 + const rowCells = grid.cells[row - grid.cells._negativeIndex]; let stack = - grid.cells[row][col]?.filter(gridCellNode => { + rowCells[col - rowCells._negativeIndex]?.filter(gridCellNode => { return gridCellNode.clientRects.find(clientRect => { const rectX = clientRect.left; const rectY = clientRect.top; @@ -40,10 +41,10 @@ export function getRectStack(grid, rect, recursed = false) { // account for differences in how browsers handle floating point // precision of bounding rects return ( - floorX < floor(rectX + clientRect.width) && - floorX >= floor(rectX) && - floorY < floor(rectY + clientRect.height) && - floorY >= floor(rectY) + floorX < Math.floor(rectX + clientRect.width) && + floorX >= Math.floor(rectX) && + floorY < Math.floor(rectY + clientRect.height) && + floorY >= Math.floor(rectY) ); }); }) ?? []; @@ -69,8 +70,3 @@ export function getRectStack(grid, rect, recursed = false) { return stack; } - -// equivalent to Math.floor(float) but is slightly faster -function floor(float) { - return float | 0; -} diff --git a/test/commons/dom/create-grid.js b/test/commons/dom/create-grid.js index 66ee4d73b4..300cd4c479 100644 --- a/test/commons/dom/create-grid.js +++ b/test/commons/dom/create-grid.js @@ -4,13 +4,18 @@ describe('create-grid', function () { var fixture; var createGrid = axe.commons.dom.createGrid; var fixtureSetup = axe.testUtils.fixtureSetup; + var gridSize = axe.constants.gridSize; function findPositions(grid, vNode) { var positions = []; + grid.cells.forEach(function (rowCells, rowIndex) { rowCells.forEach(function (cells, colIndex) { if (cells.includes(vNode)) { - positions.push({ x: rowIndex, y: colIndex }); + positions.push({ + x: colIndex + rowCells._negativeIndex, + y: rowIndex + grid.cells._negativeIndex + }); } }); }); @@ -49,8 +54,8 @@ describe('create-grid', function () { var positions = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(positions, [ { x: 0, y: 0 }, - { x: 0, y: 1 }, { x: 1, y: 0 }, + { x: 0, y: 1 }, { x: 1, y: 1 } ]); }); @@ -87,7 +92,44 @@ describe('create-grid', function () { ); createGrid(); var position = findPositions(fixture._grid, fixture.children[0]); - assert.deepEqual(position, [{ x: 0, y: 0 }]); + assert.deepEqual(position, [ + { x: 0, y: -1 }, + { x: 0, y: 0 } + ]); + }); + }); + + describe('when scrolled', () => { + before(() => { + document.body.setAttribute('style', 'margin: 0'); + }); + + after(() => { + document.body.removeAttribute('style'); + }); + + it('adds elements scrolled out of view', function () { + const gridScroll = 2; + fixture = + fixtureSetup(`
+
T1
+
T2
+
T3
+
T4
+
T5
+
`); + const scroller = fixture.children[0]; + scroller.actualNode.scroll(0, gridSize * gridScroll); + const childElms = scroller.children.filter( + ({ props }) => props.nodeName === 'div' + ); + + createGrid(); + childElms.forEach((child, index) => { + assert.isDefined(child._grid, `Expect child ${index} to be defined`); + var position = findPositions(child._grid, child); + assert.deepEqual(position, [{ x: 0, y: index - gridScroll }]); + }); }); }); @@ -141,7 +183,7 @@ describe('create-grid', function () { var position = findPositions(vOverflow._subGrid, vSpan); assert.deepEqual(position, [ { x: 0, y: 0 }, - { x: 1, y: 0 } + { x: 0, y: 1 } ]); }); }); From f4476cc512d941e0dfeda81cdf52c66791276dfa Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Wed, 9 Nov 2022 23:17:30 +0100 Subject: [PATCH 02/14] Clean up grid code --- lib/commons/dom/create-grid.js | 82 ++++++++++++++++------------- lib/commons/dom/find-nearby-elms.js | 34 +++--------- lib/commons/dom/get-rect-stack.js | 61 +++++++-------------- 3 files changed, 73 insertions(+), 104 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 174cb77dd6..623786ff81 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -323,58 +323,66 @@ function findScrollRegionParent(vNode, parentVNode) { * @param {VirtualNode} */ function addNodeToGrid(grid, vNode) { - const gridSize = constants.gridSize; vNode.clientRects.forEach(rect => { // 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; - const x = rect.left; - const y = rect.top; + const rectPosition = getGridPositionOfRect(rect); + grid.numCols = Math.max(grid.numCols ?? 0, rectPosition.endCol); - const startRow = Math.floor(y / gridSize); - const startCol = Math.floor(x / gridSize); - - // an element that ends at a grid line should - // not be added to the next grid cell - const endRow = Math.floor((y + rect.height - 1) / gridSize); - const endCol = Math.floor((x + rect.width - 1) / gridSize); - - grid.numCols = Math.max(grid.numCols ?? 0, endCol); - - addToGrid(grid, { startRow, endRow, startCol, endCol }, vNode); + loopGridPosition(grid, rectPosition, gridCell => { + if (!gridCell.includes(vNode)) { + gridCell.push(vNode); + } + }); }); } -function addToGrid(grid, { startRow, endRow, startCol, endCol }, vNode) { - grid.cells._negativeIndex ??= 0; - loopGridCells(grid.cells, startRow, endRow, (_, row) => { - grid.cells[row] = grid.cells[row] || []; - grid.cells[row]._negativeIndex ??= 0; - - loopGridCells(grid.cells[row], startCol, endCol, (_, col) => { - grid.cells[row][col] = grid.cells[row][col] || []; - - if (!grid.cells[row][col].includes(vNode)) { - grid.cells[row][col].push(vNode); - } - }); +export function loopGridPosition(grid, gridPosition, callback) { + const { startRow, endRow, startCol, endCol } = gridPosition; + loopNegativeIndexMatrix(grid.cells, startRow, endRow, row => { + loopNegativeIndexMatrix(row, startCol, endCol, callback); }); } // handle negative row/col values -function loopGridCells(cells, start, end, callback) { - if (start < cells._negativeIndex) { - let num = cells._negativeIndex - start; - cells._negativeIndex = start; - while (num--) { - cells.splice(0, 0, []); +export function loopNegativeIndexMatrix(matrix, start, end, callback) { + matrix._negativeIndex ??= 0; + // Shift the array when start is negative + if (start < matrix._negativeIndex) { + for (let i = 0; i < matrix._negativeIndex - start; i++) { + matrix.splice(0, 0, []); } + matrix._negativeIndex = start; } - start -= cells._negativeIndex; - end -= cells._negativeIndex; + const startOffset = start - matrix._negativeIndex; + const endOffset = end - matrix._negativeIndex; + for (let index = startOffset; index <= endOffset; index++) { + matrix[index] ??= []; + callback(matrix[index], index + matrix._negativeIndex, index, matrix); + } +} + +export function getGridPositionOfRect( + { top, left, bottom, right }, + margin = 0 +) { + const { gridSize } = constants; + return { + startRow: Math.floor((top - margin) / gridSize), + startCol: Math.floor((left - margin) / gridSize), + endRow: Math.floor((bottom + margin - 1) / gridSize), + endCol: Math.floor((right + margin - 1) / gridSize) + }; +} - for (let index = start; index <= end; index++) { - callback(cells[index], index, index + cells._negativeIndex); +export function getGridCellFromPoint({ cells, numCols }, { x, y }) { + const rowIndex = Math.floor(y / constants.gridSize); + const colIndex = Math.floor(x / constants.gridSize); + if (rowIndex >= cells.length || colIndex >= numCols) { + throw new Error('Element midpoint exceeds the grid bounds'); } + const row = cells[rowIndex - cells._negativeIndex] ?? []; + return row[colIndex - row._negativeIndex] ?? null; } diff --git a/lib/commons/dom/find-nearby-elms.js b/lib/commons/dom/find-nearby-elms.js index fc77a315de..0fae01f6f6 100644 --- a/lib/commons/dom/find-nearby-elms.js +++ b/lib/commons/dom/find-nearby-elms.js @@ -1,25 +1,20 @@ -import createGrid from './create-grid'; +import createGrid, { + getGridPositionOfRect, + loopGridPosition +} from './create-grid'; import { memoize } from '../../core/utils'; export default function findNearbyElms(vNode, margin = 0) { - /*eslint no-bitwise: 0*/ - const gridSize = createGrid(); - const selfIsFixed = hasFixedPosition(vNode); + createGrid(); // Ensure grid exists if (!vNode._grid?.cells?.length) { return []; // Elements not in the grid don't have ._grid } - const rect = vNode.boundingClientRect; - const gridCells = vNode._grid.cells; - const boundaries = { - topRow: ((rect.top - margin) / gridSize) | 0, - bottomRow: ((rect.bottom + margin) / gridSize) | 0, - leftCol: ((rect.left - margin) / gridSize) | 0, - rightCol: ((rect.right + margin) / gridSize) | 0 - }; + const selfIsFixed = hasFixedPosition(vNode); + const gridPosition = getGridPositionOfRect(rect, margin); const neighbors = []; - loopGridCells(gridCells, boundaries, vNeighbor => { + loopGridPosition(vNode._grid, gridPosition, vNeighbor => { if ( vNeighbor && vNeighbor !== vNode && @@ -33,19 +28,6 @@ export default function findNearbyElms(vNode, margin = 0) { return neighbors; } -function loopGridCells(gridCells, boundaries, cb) { - const { topRow, bottomRow, leftCol, rightCol } = boundaries; - for (let row = topRow; row <= bottomRow; row++) { - for (let col = leftCol; col <= rightCol; col++) { - // Don't loop on elements outside the grid - const length = gridCells[row]?.[col]?.length ?? -1; - for (let i = 0; i < length; i++) { - cb(gridCells[row][col][i]); - } - } - } -} - const hasFixedPosition = memoize(vNode => { if (!vNode) { return false; diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index f22462a3ea..e6ad3e940e 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -1,53 +1,32 @@ /* eslint no-bitwise: 0 */ import visuallySort from './visually-sort'; -import constants from '../../core/constants'; +import { getGridCellFromPoint } from './create-grid'; export function getRectStack(grid, rect, recursed = false) { // use center point of rect const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; + const gridCell = getGridCellFromPoint(grid, { x, y }) || []; + const floorX = Math.floor(x); const floorY = Math.floor(y); - - // 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 / constants.gridSize); - const col = Math.floor(x / constants.gridSize); - - // we're making an assumption that there cannot be an element in the - // grid which escapes the grid bounds. For example, if the grid is 4x4 there - // can't be an element whose midpoint is at column 5. If this happens this - // means there's an error in our grid logic that needs to be fixed - if (row > grid.cells.length || col > grid.numCols) { - throw new Error('Element midpoint exceeds the grid bounds'); - } - - // it is acceptable if a row has empty cells due to client rects not filling - // the entire bounding rect of an element - // @see https://github.com/dequelabs/axe-core/issues/3166 - const rowCells = grid.cells[row - grid.cells._negativeIndex]; - let stack = - rowCells[col - rowCells._negativeIndex]?.filter(gridCellNode => { - return gridCellNode.clientRects.find(clientRect => { - const rectX = clientRect.left; - const rectY = clientRect.top; - - // perform an AABB (axis-aligned bounding box) collision check for the - // point inside the rect - // account for differences in how browsers handle floating point - // precision of bounding rects - return ( - floorX < Math.floor(rectX + clientRect.width) && - floorX >= Math.floor(rectX) && - floorY < Math.floor(rectY + clientRect.height) && - floorY >= Math.floor(rectY) - ); - }); - }) ?? []; + let stack = gridCell.filter(gridCellNode => { + return gridCellNode.clientRects.find(clientRect => { + const rectX = clientRect.left; + const rectY = clientRect.top; + + // perform an AABB (axis-aligned bounding box) collision check for the + // point inside the rect + // account for differences in how browsers handle floating point + // precision of bounding rects + return ( + floorX < Math.floor(rectX + clientRect.width) && + floorX >= Math.floor(rectX) && + floorY < Math.floor(rectY + clientRect.height) && + floorY >= Math.floor(rectY) + ); + }); + }); const gridContainer = grid.container; if (gridContainer) { From 25c01469065616e2021f1babb778c1bc7101ce2a Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 10 Nov 2022 14:54:36 +0100 Subject: [PATCH 03/14] Clean up grid code --- lib/commons/dom/create-grid.js | 50 ++++++++++++++--------- lib/commons/dom/find-nearby-elms.js | 18 ++++---- lib/commons/dom/get-rect-stack.js | 14 +++---- lib/commons/dom/get-text-element-stack.js | 50 +++++++++-------------- lib/commons/math/get-bounding-rect.js | 7 ++++ lib/commons/math/get-rect-center.js | 8 ++++ lib/commons/math/index.js | 3 ++ lib/commons/math/is-point-in-rect.js | 3 ++ test/commons/dom/find-nearby-elms.js | 4 +- test/commons/math/get-bounding-rect.js | 10 +++++ test/commons/math/get-rect-center.js | 9 ++++ test/commons/math/is-point-in-rect.js | 27 ++++++++++++ 12 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 lib/commons/math/get-bounding-rect.js create mode 100644 lib/commons/math/get-rect-center.js create mode 100644 lib/commons/math/is-point-in-rect.js create mode 100644 test/commons/math/get-bounding-rect.js create mode 100644 test/commons/math/get-rect-center.js create mode 100644 test/commons/math/is-point-in-rect.js diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 623786ff81..b8683994de 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -1,5 +1,6 @@ -/* eslint no-bitwise: 0 */ import isVisibleOnScreen from './is-visible-on-screen'; +import { getBoundingRect } from '../math/get-bounding-rect'; +import { isPointInRect } from '../math/is-point-in-rect'; import VirtualNode from '../../core/base/virtual-node/virtual-node'; import { getNodeFromTree, getScroll, isShadowRoot } from '../../core/utils'; import constants from '../../core/constants'; @@ -327,10 +328,12 @@ 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; - const rectPosition = getGridPositionOfRect(rect); - grid.numCols = Math.max(grid.numCols ?? 0, rectPosition.endCol); + const gridRect = getGridPositionOfRect(rect); + grid.boundaries = grid.boundaries + ? getBoundingRect(grid.boundaries, gridRect) + : gridRect; - loopGridPosition(grid, rectPosition, gridCell => { + loopGridPosition(grid, gridRect, gridCell => { if (!gridCell.includes(vNode)) { gridCell.push(vNode); } @@ -338,10 +341,10 @@ function addNodeToGrid(grid, vNode) { }); } -export function loopGridPosition(grid, gridPosition, callback) { - const { startRow, endRow, startCol, endCol } = gridPosition; - loopNegativeIndexMatrix(grid.cells, startRow, endRow, row => { - loopNegativeIndexMatrix(row, startCol, endCol, callback); +export function loopGridPosition(grid, gridRect, callback) { + const { left, right, top, bottom } = gridRect; + loopNegativeIndexMatrix(grid.cells, top, bottom, gridRow => { + loopNegativeIndexMatrix(gridRow, left, right, callback); }); } @@ -365,24 +368,31 @@ export function loopNegativeIndexMatrix(matrix, start, end, callback) { } export function getGridPositionOfRect( - { top, left, bottom, right }, + { top, right, bottom, left }, margin = 0 ) { - const { gridSize } = constants; - return { - startRow: Math.floor((top - margin) / gridSize), - startCol: Math.floor((left - margin) / gridSize), - endRow: Math.floor((bottom + margin - 1) / gridSize), - endCol: Math.floor((right + margin - 1) / gridSize) - }; + top = toGridIndex(top - margin); + right = toGridIndex(right + margin - 1); + bottom = toGridIndex(bottom + margin - 1); + left = toGridIndex(left - margin); + return new window.DOMRect(left, top, right - left, bottom - top); } -export function getGridCellFromPoint({ cells, numCols }, { x, y }) { - const rowIndex = Math.floor(y / constants.gridSize); - const colIndex = Math.floor(x / constants.gridSize); - if (rowIndex >= cells.length || colIndex >= numCols) { +export function getGridCellFromPoint({ cells, boundaries }, { x, y }, debug) { + const rowIndex = toGridIndex(y); + const colIndex = toGridIndex(x); + const { top, right, bottom, left } = boundaries; + + if (debug) { + console.log({ x, y }, { rowIndex, colIndex }, { top, right, bottom, left }); + } + if (!isPointInRect({ y: rowIndex, x: colIndex }, boundaries)) { throw new Error('Element midpoint exceeds the grid bounds'); } const row = cells[rowIndex - cells._negativeIndex] ?? []; return row[colIndex - row._negativeIndex] ?? null; } + +function toGridIndex(num) { + return Math.floor(num / constants.gridSize); +} diff --git a/lib/commons/dom/find-nearby-elms.js b/lib/commons/dom/find-nearby-elms.js index 0fae01f6f6..c57b998bd2 100644 --- a/lib/commons/dom/find-nearby-elms.js +++ b/lib/commons/dom/find-nearby-elms.js @@ -14,14 +14,16 @@ export default function findNearbyElms(vNode, margin = 0) { const gridPosition = getGridPositionOfRect(rect, margin); const neighbors = []; - loopGridPosition(vNode._grid, gridPosition, vNeighbor => { - if ( - vNeighbor && - vNeighbor !== vNode && - !neighbors.includes(vNeighbor) && - selfIsFixed === hasFixedPosition(vNeighbor) - ) { - neighbors.push(vNeighbor); + loopGridPosition(vNode._grid, gridPosition, vNeighbors => { + for (const vNeighbor of vNeighbors) { + if ( + vNeighbor && + vNeighbor !== vNode && + !neighbors.includes(vNeighbor) && + selfIsFixed === hasFixedPosition(vNeighbor) + ) { + neighbors.push(vNeighbor); + } } }); diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index e6ad3e940e..94c9ccf52d 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -1,17 +1,15 @@ -/* eslint no-bitwise: 0 */ import visuallySort from './visually-sort'; import { getGridCellFromPoint } from './create-grid'; +import { getRectCenter } from '../math'; export function getRectStack(grid, rect, recursed = false) { - // use center point of rect - const x = rect.left + rect.width / 2; - const y = rect.top + rect.height / 2; - const gridCell = getGridCellFromPoint(grid, { x, y }) || []; + const center = getRectCenter(rect); + const gridCell = getGridCellFromPoint(grid, center) || []; - const floorX = Math.floor(x); - const floorY = Math.floor(y); + const floorX = Math.floor(center.x); + const floorY = Math.floor(center.y); let stack = gridCell.filter(gridCellNode => { - return gridCellNode.clientRects.find(clientRect => { + return gridCellNode.clientRects.some(clientRect => { const rectX = clientRect.left; const rectY = clientRect.top; diff --git a/lib/commons/dom/get-text-element-stack.js b/lib/commons/dom/get-text-element-stack.js index 641e8d15fc..0c694404fb 100644 --- a/lib/commons/dom/get-text-element-stack.js +++ b/lib/commons/dom/get-text-element-stack.js @@ -3,6 +3,7 @@ import { getRectStack } from './get-rect-stack'; import createGrid from './create-grid'; import sanitize from '../text/sanitize'; import { getNodeFromTree } from '../../core/utils'; +import { isPointInRect, getRectCenter } from '../math'; /** * Return all elements that are at the center of each text client rect of the passed in node. @@ -33,37 +34,29 @@ function getTextElementStack(node) { if (elm.nodeType === 3 && sanitize(elm.textContent) !== '') { const range = document.createRange(); range.selectNodeContents(elm); - const rects = range.getClientRects(); - - // if any text rect is larger than the bounds of the parent, - // or goes outside of the bounds of the parent, we need to use - // the parent rect so we stay within the bounds of the element. - // - // since we use the midpoint of the element when determining - // the rect stack we will also use the midpoint of the text rect - // to determine out of bounds. - // - // @see https://github.com/dequelabs/axe-core/issues/2178 - // @see https://github.com/dequelabs/axe-core/issues/2483 - // @see https://github.com/dequelabs/axe-core/issues/2681 - const outsideRectBounds = Array.from(rects).some(rect => { - const horizontalMidpoint = rect.left + rect.width / 2; - const verticalMidpoint = rect.top + rect.height / 2; - - return ( - horizontalMidpoint < nodeRect.left || - horizontalMidpoint > nodeRect.right || - verticalMidpoint < nodeRect.top || - verticalMidpoint > nodeRect.bottom - ); + const rects = Array.from(range.getClientRects()); + /** + * if any text rect is larger than the bounds of the parent, + * or goes outside of the bounds of the parent, we need to use + * the parent rect so we stay within the bounds of the element. + * + * since we use the midpoint of the element when determining + * the rect stack we will also use the midpoint of the text rect + * to determine out of bounds. + * + * @see https://github.com/dequelabs/axe-core/issues/2178 + * @see https://github.com/dequelabs/axe-core/issues/2483 + * @see https://github.com/dequelabs/axe-core/issues/2681 + */ + const outsideRectBounds = rects.some(rect => { + const centerPoint = getRectCenter(rect); + return !isPointInRect(centerPoint, nodeRect); }); if (outsideRectBounds) { return; } - for (let i = 0; i < rects.length; i++) { - const rect = rects[i]; - + for (const rect of rects) { // filter out 0 width and height rects (newline characters) // ie11 has newline characters return 0.00998, so we'll say if the // line is < 1 it shouldn't be counted @@ -77,10 +70,7 @@ function getTextElementStack(node) { if (!clientRects.length) { return [getElementStack(node)]; } - - return clientRects.map(rect => { - return getRectStack(grid, rect); - }); + return clientRects.map(rect => getRectStack(grid, rect)); } export default getTextElementStack; diff --git a/lib/commons/math/get-bounding-rect.js b/lib/commons/math/get-bounding-rect.js new file mode 100644 index 0000000000..f934e2e328 --- /dev/null +++ b/lib/commons/math/get-bounding-rect.js @@ -0,0 +1,7 @@ +export function getBoundingRect(rectA, rectB) { + const top = Math.min(rectA.top, rectB.top); + const right = Math.max(rectA.right, rectB.right); + const bottom = Math.max(rectA.bottom, rectB.bottom); + const left = Math.min(rectA.left, rectB.left); + return new window.DOMRect(left, top, right - left, bottom - top); +} diff --git a/lib/commons/math/get-rect-center.js b/lib/commons/math/get-rect-center.js new file mode 100644 index 0000000000..eda74cdf02 --- /dev/null +++ b/lib/commons/math/get-rect-center.js @@ -0,0 +1,8 @@ +/** + * Return the center point of a rect + * @param DOMRect + * @returns DOMPoint + */ +export function getRectCenter({ left, top, width, height }) { + return new window.DOMPoint(left + width / 2, top + height / 2); +} diff --git a/lib/commons/math/index.js b/lib/commons/math/index.js index 91b9993ac1..b9727bca6b 100644 --- a/lib/commons/math/index.js +++ b/lib/commons/math/index.js @@ -1,4 +1,7 @@ +export { getBoundingRect } from './get-bounding-rect'; export { default as getOffset } from './get-offset'; +export { getRectCenter } from './get-rect-center'; export { default as hasVisualOverlap } from './has-visual-overlap'; +export { isPointInRect } from './is-point-in-rect'; export { default as rectsOverlap } from './rects-overlap'; export { default as splitRects } from './split-rects'; diff --git a/lib/commons/math/is-point-in-rect.js b/lib/commons/math/is-point-in-rect.js new file mode 100644 index 0000000000..003ab14a13 --- /dev/null +++ b/lib/commons/math/is-point-in-rect.js @@ -0,0 +1,3 @@ +export function isPointInRect({ x, y }, { top, right, bottom, left }) { + return y >= top && x <= right && y <= bottom && x >= left; +} diff --git a/test/commons/dom/find-nearby-elms.js b/test/commons/dom/find-nearby-elms.js index ceef24d37c..2bc46b92f9 100644 --- a/test/commons/dom/find-nearby-elms.js +++ b/test/commons/dom/find-nearby-elms.js @@ -43,8 +43,8 @@ describe('findNearbyElms', () => { describe('on the edge', () => { beforeEach(() => { fixture = fixtureSetup( - '
0
' + - '
1
' + + '
0
' + + '
1
' + '
2
' ); }); diff --git a/test/commons/math/get-bounding-rect.js b/test/commons/math/get-bounding-rect.js new file mode 100644 index 0000000000..b527397267 --- /dev/null +++ b/test/commons/math/get-bounding-rect.js @@ -0,0 +1,10 @@ +describe('getBoundingRect', () => { + const getBoundingRect = axe.commons.math.getBoundingRect; + + it('returns a rect that bounds both rects', () => { + const rectA = new DOMRect(10, 10, 5, 5); + const rectB = new DOMRect(25, 25, 5, 5); + const rectC = new DOMRect(10, 10, 20, 20); + assert.deepEqual(getBoundingRect(rectA, rectB), rectC); + }); +}); diff --git a/test/commons/math/get-rect-center.js b/test/commons/math/get-rect-center.js new file mode 100644 index 0000000000..2bb4bb5a58 --- /dev/null +++ b/test/commons/math/get-rect-center.js @@ -0,0 +1,9 @@ +describe('getRectCenter', () => { + const getRectCenter = axe.commons.math.getRectCenter; + + it('returns the center point of a rect', () => { + const rect = new DOMRect(10, 20, 10, 20); + const center = new DOMPoint(15, 30); + assert.deepEqual(getRectCenter(rect), center); + }); +}); diff --git a/test/commons/math/is-point-in-rect.js b/test/commons/math/is-point-in-rect.js new file mode 100644 index 0000000000..fc23a89b1e --- /dev/null +++ b/test/commons/math/is-point-in-rect.js @@ -0,0 +1,27 @@ +describe('isPointInRect', () => { + const isPointInRect = axe.commons.math.isPointInRect; + + it('returns true when the point is inside the rect', () => { + const rect = new DOMRect(10, 20, 10, 20); + const point = new DOMPoint(15, 30); + assert.isTrue(isPointInRect(point, rect)); + }); + + it('returns true when the point is on the edge', () => { + const rect = new DOMRect(10, 20, 10, 20); + assert.isTrue(isPointInRect(new DOMPoint(10, 20), rect)); + assert.isTrue(isPointInRect(new DOMPoint(20, 40), rect)); + }); + + it('returns false when the point is vertically outside the rect', () => { + const rect = new DOMRect(10, 20, 10, 20); + const point = new DOMPoint(15, 50); + assert.isFalse(isPointInRect(point, rect)); + }); + + it('returns false when the point is vertically outside the rect', () => { + const rect = new DOMRect(10, 20, 10, 20); + const point = new DOMPoint(25, 30); + assert.isFalse(isPointInRect(point, rect)); + }); +}); From f9438ec4488e8bf8581d4f202f5f226e63f323ac Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 10 Nov 2022 17:57:07 +0100 Subject: [PATCH 04/14] Add grid class --- lib/commons/dom/create-grid.js | 100 +++++++++++++--------------- lib/commons/dom/find-nearby-elms.js | 10 ++- lib/commons/dom/get-rect-stack.js | 3 +- 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index b8683994de..1277c5af10 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -13,10 +13,7 @@ import cache from '../../core/base/cache'; */ export default function createGrid( root = document.body, - rootGrid = { - container: null, - cells: [] - }, + rootGrid, parentVNode = null ) { // Prevent multiple calls per run @@ -35,13 +32,11 @@ export default function createGrid( } vNode._stackingOrder = [0]; + rootGrid ??= new Grid(); addNodeToGrid(rootGrid, vNode); if (getScroll(vNode.actualNode)) { - const subGrid = { - container: vNode, - cells: [] - }; + const subGrid = new Grid(vNode); vNode._subGrid = subGrid; } } @@ -77,10 +72,7 @@ export default function createGrid( const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; if (getScroll(vNode.actualNode)) { - const subGrid = { - container: vNode, - cells: [] - }; + const subGrid = new Grid(vNode); vNode._subGrid = subGrid; } @@ -328,12 +320,8 @@ 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; - const gridRect = getGridPositionOfRect(rect); - grid.boundaries = grid.boundaries - ? getBoundingRect(grid.boundaries, gridRect) - : gridRect; - - loopGridPosition(grid, gridRect, gridCell => { + const gridRect = grid.getPositionOfRect(rect); + grid.loopGridPosition(gridRect, gridCell => { if (!gridCell.includes(vNode)) { gridCell.push(vNode); } @@ -341,15 +329,49 @@ function addNodeToGrid(grid, vNode) { }); } -export function loopGridPosition(grid, gridRect, callback) { - const { left, right, top, bottom } = gridRect; - loopNegativeIndexMatrix(grid.cells, top, bottom, gridRow => { - loopNegativeIndexMatrix(gridRow, left, right, callback); - }); +class Grid { + constructor(container = null) { + this.container = container; + this.cells = []; + } + + toGridIndex(num) { + return Math.floor(num / constants.gridSize); + } + + getCellFromPoint({ x, y }) { + const rowIndex = this.toGridIndex(y); + const colIndex = this.toGridIndex(x); + if (!isPointInRect({ y: rowIndex, x: colIndex }, this.boundaries)) { + throw new Error('Element midpoint exceeds the grid bounds'); + } + const row = this.cells[rowIndex - this.cells._negativeIndex] ?? []; + return row[colIndex - row._negativeIndex] ?? null; + } + + loopGridPosition(gridRect, callback) { + const { left, right, top, bottom } = gridRect; + if (this.boundaries) { + gridRect = getBoundingRect(this.boundaries, gridRect); + } + this.boundaries = gridRect; + + loopNegativeIndexMatrix(this.cells, top, bottom, gridRow => { + loopNegativeIndexMatrix(gridRow, left, right, callback); + }); + } + + getPositionOfRect({ top, right, bottom, left }, margin = 0) { + top = this.toGridIndex(top - margin); + right = this.toGridIndex(right + margin - 1); + bottom = this.toGridIndex(bottom + margin - 1); + left = this.toGridIndex(left - margin); + return new window.DOMRect(left, top, right - left, bottom - top); + } } // handle negative row/col values -export function loopNegativeIndexMatrix(matrix, start, end, callback) { +function loopNegativeIndexMatrix(matrix, start, end, callback) { matrix._negativeIndex ??= 0; // Shift the array when start is negative if (start < matrix._negativeIndex) { @@ -366,33 +388,3 @@ export function loopNegativeIndexMatrix(matrix, start, end, callback) { callback(matrix[index], index + matrix._negativeIndex, index, matrix); } } - -export function getGridPositionOfRect( - { top, right, bottom, left }, - margin = 0 -) { - top = toGridIndex(top - margin); - right = toGridIndex(right + margin - 1); - bottom = toGridIndex(bottom + margin - 1); - left = toGridIndex(left - margin); - return new window.DOMRect(left, top, right - left, bottom - top); -} - -export function getGridCellFromPoint({ cells, boundaries }, { x, y }, debug) { - const rowIndex = toGridIndex(y); - const colIndex = toGridIndex(x); - const { top, right, bottom, left } = boundaries; - - if (debug) { - console.log({ x, y }, { rowIndex, colIndex }, { top, right, bottom, left }); - } - if (!isPointInRect({ y: rowIndex, x: colIndex }, boundaries)) { - throw new Error('Element midpoint exceeds the grid bounds'); - } - const row = cells[rowIndex - cells._negativeIndex] ?? []; - return row[colIndex - row._negativeIndex] ?? null; -} - -function toGridIndex(num) { - return Math.floor(num / constants.gridSize); -} diff --git a/lib/commons/dom/find-nearby-elms.js b/lib/commons/dom/find-nearby-elms.js index c57b998bd2..73cfae50c8 100644 --- a/lib/commons/dom/find-nearby-elms.js +++ b/lib/commons/dom/find-nearby-elms.js @@ -1,7 +1,4 @@ -import createGrid, { - getGridPositionOfRect, - loopGridPosition -} from './create-grid'; +import createGrid from './create-grid'; import { memoize } from '../../core/utils'; export default function findNearbyElms(vNode, margin = 0) { @@ -10,11 +7,12 @@ export default function findNearbyElms(vNode, margin = 0) { return []; // Elements not in the grid don't have ._grid } const rect = vNode.boundingClientRect; + const grid = vNode._grid; const selfIsFixed = hasFixedPosition(vNode); - const gridPosition = getGridPositionOfRect(rect, margin); + const gridPosition = grid.getPositionOfRect(rect, margin); const neighbors = []; - loopGridPosition(vNode._grid, gridPosition, vNeighbors => { + grid.loopGridPosition(gridPosition, vNeighbors => { for (const vNeighbor of vNeighbors) { if ( vNeighbor && diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index 94c9ccf52d..4c51bf3b38 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -1,10 +1,9 @@ import visuallySort from './visually-sort'; -import { getGridCellFromPoint } from './create-grid'; import { getRectCenter } from '../math'; export function getRectStack(grid, rect, recursed = false) { const center = getRectCenter(rect); - const gridCell = getGridCellFromPoint(grid, center) || []; + const gridCell = grid.getCellFromPoint(center) || []; const floorX = Math.floor(center.x); const floorY = Math.floor(center.y); From ec9970261b84fee7d4c35a81a22e9c459a5c2453 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 10 Nov 2022 18:33:08 +0100 Subject: [PATCH 05/14] Add doc blocks --- lib/commons/dom/create-grid.js | 41 +++++++++++++++++++++------ lib/commons/dom/find-nearby-elms.js | 2 +- lib/commons/math/get-bounding-rect.js | 8 ++++++ lib/commons/math/get-rect-center.js | 6 ++-- lib/commons/math/is-point-in-rect.js | 8 ++++++ lib/commons/math/rects-overlap.js | 4 +-- lib/commons/math/split-rects.js | 6 ++-- test/commons/dom/create-grid.js | 34 +++++++++------------- 8 files changed, 72 insertions(+), 37 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 1277c5af10..efd1f15240 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -320,7 +320,7 @@ 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; - const gridRect = grid.getPositionOfRect(rect); + const gridRect = grid.getGridPositionOfRect(rect); grid.loopGridPosition(gridRect, gridCell => { if (!gridCell.includes(vNode)) { gridCell.push(vNode); @@ -335,10 +335,20 @@ class Grid { this.cells = []; } + /** + * Convert x or y coordinate from rect, to a position in the grid + * @param {number} + * @returns {number} + */ toGridIndex(num) { return Math.floor(num / constants.gridSize); } + /** + * Return an an array of nodes available at a particular grid coordinate + * @param {DOMPoint} gridPosition + * @returns {Array|null} + */ getCellFromPoint({ x, y }) { const rowIndex = this.toGridIndex(y); const colIndex = this.toGridIndex(x); @@ -349,19 +359,32 @@ class Grid { return row[colIndex - row._negativeIndex] ?? null; } - loopGridPosition(gridRect, callback) { - const { left, right, top, bottom } = gridRect; + /** + * Loop over all cells within the gridPosition rect + * @param {DOMRect} gridPosition + * @param {(vNode:AbstractVirtualNode, place: { row: number, col: number}): any} callback + */ + loopGridPosition(gridPosition, callback) { + const { left, right, top, bottom } = gridPosition; if (this.boundaries) { - gridRect = getBoundingRect(this.boundaries, gridRect); + gridPosition = getBoundingRect(this.boundaries, gridPosition); } - this.boundaries = gridRect; + this.boundaries = gridPosition; - loopNegativeIndexMatrix(this.cells, top, bottom, gridRow => { - loopNegativeIndexMatrix(gridRow, left, right, callback); + loopNegativeIndexMatrix(this.cells, top, bottom, (gridRow, row) => { + loopNegativeIndexMatrix(gridRow, left, right, (gridCell, col) => { + callback(gridCell, { row, col }); + }); }); } - getPositionOfRect({ top, right, bottom, left }, margin = 0) { + /** + * Scale the rect to the position within the grid + * @param {DOMRect} param0 + * @param {number} margin Offset outside the rect, default 0 + * @returns + */ + getGridPositionOfRect({ top, right, bottom, left }, margin = 0) { top = this.toGridIndex(top - margin); right = this.toGridIndex(right + margin - 1); bottom = this.toGridIndex(bottom + margin - 1); @@ -385,6 +408,6 @@ function loopNegativeIndexMatrix(matrix, start, end, callback) { const endOffset = end - matrix._negativeIndex; for (let index = startOffset; index <= endOffset; index++) { matrix[index] ??= []; - callback(matrix[index], index + matrix._negativeIndex, index, matrix); + callback(matrix[index], index + matrix._negativeIndex); } } diff --git a/lib/commons/dom/find-nearby-elms.js b/lib/commons/dom/find-nearby-elms.js index 73cfae50c8..5af87fb527 100644 --- a/lib/commons/dom/find-nearby-elms.js +++ b/lib/commons/dom/find-nearby-elms.js @@ -9,7 +9,7 @@ export default function findNearbyElms(vNode, margin = 0) { const rect = vNode.boundingClientRect; const grid = vNode._grid; const selfIsFixed = hasFixedPosition(vNode); - const gridPosition = grid.getPositionOfRect(rect, margin); + const gridPosition = grid.getGridPositionOfRect(rect, margin); const neighbors = []; grid.loopGridPosition(gridPosition, vNeighbors => { diff --git a/lib/commons/math/get-bounding-rect.js b/lib/commons/math/get-bounding-rect.js index f934e2e328..d19dc70420 100644 --- a/lib/commons/math/get-bounding-rect.js +++ b/lib/commons/math/get-bounding-rect.js @@ -1,3 +1,11 @@ +/** + * Return a new rect that wraps around both rectA and rectB + * @method getBoundingRect + * @memberof axe.commons.math + * @param {DOMRect} rectA + * @param {DOMRect} rectB + * @returns {DOMRect} + */ export function getBoundingRect(rectA, rectB) { const top = Math.min(rectA.top, rectB.top); const right = Math.max(rectA.right, rectB.right); diff --git a/lib/commons/math/get-rect-center.js b/lib/commons/math/get-rect-center.js index eda74cdf02..38133013f2 100644 --- a/lib/commons/math/get-rect-center.js +++ b/lib/commons/math/get-rect-center.js @@ -1,7 +1,9 @@ /** * Return the center point of a rect - * @param DOMRect - * @returns DOMPoint + * @method getRectCenter + * @memberof axe.commons.math + * @param {DOMRect} + * @returns {DOMPoint} */ export function getRectCenter({ left, top, width, height }) { return new window.DOMPoint(left + width / 2, top + height / 2); diff --git a/lib/commons/math/is-point-in-rect.js b/lib/commons/math/is-point-in-rect.js index 003ab14a13..3ba9205458 100644 --- a/lib/commons/math/is-point-in-rect.js +++ b/lib/commons/math/is-point-in-rect.js @@ -1,3 +1,11 @@ +/** + * Check if the DOMPoint is within the DOMRect + * @method isPointInRect + * @memberof axe.commons.math + * @param {DOMPoint} + * @param {DOMRect} + * @returns {boolean} + */ export function isPointInRect({ x, y }, { top, right, bottom, left }) { return y >= top && x <= right && y <= bottom && x >= left; } diff --git a/lib/commons/math/rects-overlap.js b/lib/commons/math/rects-overlap.js index 50b402d340..afdeff1f26 100644 --- a/lib/commons/math/rects-overlap.js +++ b/lib/commons/math/rects-overlap.js @@ -2,8 +2,8 @@ * Determine if two rectangles touch. * @method rectsOverlap * @memberof axe.commons.math - * @param {Rect} rect1 - * @param {Rect} rect2 + * @param {DOMRect} rect1 + * @param {DOMRect} rect2 * @returns {Boolean} */ export default function rectsOverlap(rect1, rect2) { diff --git a/lib/commons/math/split-rects.js b/lib/commons/math/split-rects.js index 15b7abd8be..4816299eee 100644 --- a/lib/commons/math/split-rects.js +++ b/lib/commons/math/split-rects.js @@ -3,9 +3,9 @@ * space that does not overlap. * @method getOffset * @memberof axe.commons.math - * @param {Rect} outerRect - * @param {Rect[]} overlapRects - * @returns uniqueRects {Rect[]} + * @param {DOMRect} outerRect + * @param {DOMRect[]} overlapRects + * @returns {Rect[]} Unique array of rects */ export default function splitRects(outerRect, overlapRects) { let uniqueRects = [outerRect]; diff --git a/test/commons/dom/create-grid.js b/test/commons/dom/create-grid.js index 300cd4c479..3a9d93336c 100644 --- a/test/commons/dom/create-grid.js +++ b/test/commons/dom/create-grid.js @@ -8,16 +8,10 @@ describe('create-grid', function () { function findPositions(grid, vNode) { var positions = []; - - grid.cells.forEach(function (rowCells, rowIndex) { - rowCells.forEach(function (cells, colIndex) { - if (cells.includes(vNode)) { - positions.push({ - x: colIndex + rowCells._negativeIndex, - y: rowIndex + grid.cells._negativeIndex - }); - } - }); + grid.loopGridPosition(grid.boundaries, (cell, position) => { + if (cell.includes(vNode)) { + positions.push(position); + } }); return positions; } @@ -41,7 +35,7 @@ describe('create-grid', function () { fixture = fixtureSetup('Hello world'); createGrid(); var positions = findPositions(fixture._grid, fixture.children[0]); - assert.deepEqual(positions, [{ x: 0, y: 0 }]); + assert.deepEqual(positions, [{ col: 0, row: 0 }]); }); it('adds large elements to multiple cell', function () { @@ -53,10 +47,10 @@ describe('create-grid', function () { var positions = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(positions, [ - { x: 0, y: 0 }, - { x: 1, y: 0 }, - { x: 0, y: 1 }, - { x: 1, y: 1 } + { col: 0, row: 0 }, + { col: 1, row: 0 }, + { col: 0, row: 1 }, + { col: 1, row: 1 } ]); }); @@ -93,8 +87,8 @@ describe('create-grid', function () { createGrid(); var position = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(position, [ - { x: 0, y: -1 }, - { x: 0, y: 0 } + { col: 0, row: -1 }, + { col: 0, row: 0 } ]); }); }); @@ -128,7 +122,7 @@ describe('create-grid', function () { childElms.forEach((child, index) => { assert.isDefined(child._grid, `Expect child ${index} to be defined`); var position = findPositions(child._grid, child); - assert.deepEqual(position, [{ x: 0, y: index - gridScroll }]); + assert.deepEqual(position, [{ col: 0, row: index - gridScroll }]); }); }); }); @@ -182,8 +176,8 @@ describe('create-grid', function () { var vSpan = vOverflow.children[0]; var position = findPositions(vOverflow._subGrid, vSpan); assert.deepEqual(position, [ - { x: 0, y: 0 }, - { x: 0, y: 1 } + { col: 0, row: 0 }, + { col: 0, row: 1 } ]); }); }); From 4afcc6d4277c10d9ad4558dadc36f2f56c36fd41 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 10 Nov 2022 18:46:39 +0100 Subject: [PATCH 06/14] Add vorizontal scroll test --- test/commons/dom/create-grid.js | 38 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/test/commons/dom/create-grid.js b/test/commons/dom/create-grid.js index 3a9d93336c..5b344d3a55 100644 --- a/test/commons/dom/create-grid.js +++ b/test/commons/dom/create-grid.js @@ -102,15 +102,15 @@ describe('create-grid', function () { document.body.removeAttribute('style'); }); - it('adds elements scrolled out of view', function () { + it('adds elements vertically scrolled out of view', function () { const gridScroll = 2; fixture = fixtureSetup(`
-
T1
-
T2
-
T3
-
T4
-
T5
+
V1
+
V2
+
V3
+
V4
+
V5
`); const scroller = fixture.children[0]; scroller.actualNode.scroll(0, gridSize * gridScroll); @@ -125,6 +125,32 @@ describe('create-grid', function () { assert.deepEqual(position, [{ col: 0, row: index - gridScroll }]); }); }); + + it('adds elements horizontally scrolled out of view', function () { + const gridScroll = 2; + fixture = + fixtureSetup(`
+
+
H1
+
H2
+
H3
+
H4
+
H5
+
+
`); + const scroller = fixture.children[0]; + scroller.actualNode.scroll(gridSize * gridScroll, 0); + const childElms = scroller.children[0].children.filter( + ({ props }) => props.nodeName === 'span' + ); + + createGrid(); + childElms.forEach((child, index) => { + assert.isDefined(child._grid, `Expect child ${index} to be defined`); + var position = findPositions(child._grid, child); + assert.deepEqual(position, [{ col: index - gridScroll, row: 0 }]); + }); + }); }); describe('subGrids', function () { From 5a2364903567a62c5f5c685f0fd622e0d2185620 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 10 Nov 2022 18:53:54 +0100 Subject: [PATCH 07/14] Assert on grid boundary existing --- lib/commons/dom/create-grid.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index efd1f15240..d752d8f70b 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -5,6 +5,7 @@ import VirtualNode from '../../core/base/virtual-node/virtual-node'; import { getNodeFromTree, getScroll, isShadowRoot } from '../../core/utils'; import constants from '../../core/constants'; import cache from '../../core/base/cache'; +import assert from '../../core/utils/assert'; /** * Setup the 2d grid and add every element to it, even elements not @@ -350,11 +351,13 @@ class Grid { * @returns {Array|null} */ getCellFromPoint({ x, y }) { + assert(this.boundary, 'Grid does not have cells added'); const rowIndex = this.toGridIndex(y); const colIndex = this.toGridIndex(x); - if (!isPointInRect({ y: rowIndex, x: colIndex }, this.boundaries)) { - throw new Error('Element midpoint exceeds the grid bounds'); - } + assert( + isPointInRect({ y: rowIndex, x: colIndex }, this.boundaries), + 'Element midpoint exceeds the grid bounds' + ); const row = this.cells[rowIndex - this.cells._negativeIndex] ?? []; return row[colIndex - row._negativeIndex] ?? null; } From f320b13a05ff74fd2c74677190d5a46bf60c0ccb Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 10 Nov 2022 18:54:57 +0100 Subject: [PATCH 08/14] Fix typo --- test/commons/math/is-point-in-rect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commons/math/is-point-in-rect.js b/test/commons/math/is-point-in-rect.js index fc23a89b1e..ee35fe6bec 100644 --- a/test/commons/math/is-point-in-rect.js +++ b/test/commons/math/is-point-in-rect.js @@ -19,7 +19,7 @@ describe('isPointInRect', () => { assert.isFalse(isPointInRect(point, rect)); }); - it('returns false when the point is vertically outside the rect', () => { + it('returns false when the point is horizontally outside the rect', () => { const rect = new DOMRect(10, 20, 10, 20); const point = new DOMPoint(25, 30); assert.isFalse(isPointInRect(point, rect)); From d76ccd9623d1c7b910bb506bc18a4c66dc241027 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 10 Nov 2022 19:01:32 +0100 Subject: [PATCH 09/14] typo --- 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 d752d8f70b..866ac822cc 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -351,7 +351,7 @@ class Grid { * @returns {Array|null} */ getCellFromPoint({ x, y }) { - assert(this.boundary, 'Grid does not have cells added'); + assert(this.boundaries, 'Grid does not have cells added'); const rowIndex = this.toGridIndex(y); const colIndex = this.toGridIndex(x); assert( From 28c96768a3a08eadfe0251de052595410bdfc1e0 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 10 Nov 2022 15:55:30 -0700 Subject: [PATCH 10/14] add scrolling integration test --- .../integration/full/all-rules/all-rules.html | 142 ++++++++++++++++++ test/integration/full/all-rules/all-rules.js | 22 +++ .../full/all-rules/frames/focusable.html | 11 ++ 3 files changed, 175 insertions(+) create mode 100644 test/integration/full/all-rules/all-rules.html create mode 100644 test/integration/full/all-rules/all-rules.js create mode 100644 test/integration/full/all-rules/frames/focusable.html diff --git a/test/integration/full/all-rules/all-rules.html b/test/integration/full/all-rules/all-rules.html new file mode 100644 index 0000000000..05c32b4ad4 --- /dev/null +++ b/test/integration/full/all-rules/all-rules.html @@ -0,0 +1,142 @@ + + + + all rules test + + + + + + + + + + +
+ bad link 1 + +
+
+ + monkeys + +
Foo
+
+
Home
+
+ +
+
+
+
+
Item 1
+
+ +
Some text and some more text
+
Newspaper
+
Copy this content
+
+
Item
+
+ + +

Banana error

+

text

+ +
+
foo
+
bar
+
+
+

Ok

+

Ok

+ + + + +
Ok
+ + + + + I am a circle + + +
English
+ +
  • Hello
  • +
    +
    + +
    + + +

    Paragraph.

    + + + This content is inside a marquee. +
    +
    +
    +
    + + + + +
    + +
    +
    + + + + +

    Paragraph with a link.

    +
      +
    • Hello
    • +
    • World
    • +
    + +
    Large scroll area
    + +
    +
    + + + + + + diff --git a/test/integration/full/all-rules/all-rules.js b/test/integration/full/all-rules/all-rules.js new file mode 100644 index 0000000000..72a039f0de --- /dev/null +++ b/test/integration/full/all-rules/all-rules.js @@ -0,0 +1,22 @@ +describe('all rules test', () => { + let results; + before(done => { + axe.testUtils.awaitNestedLoad(async () => { + results = await axe.run(); + done(); + }); + }); + + it('should run all rules', async () => { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find same results when scrolled', async () => { + const endButton = document.querySelector('#end-of-page'); + endButton.focus(); + const scrollResults = await axe.run(); + scrollResults.testEnvironment = results.testEnvironment; + scrollResults.timestamp = results.timestamp; + assert.deepEqual(results, scrollResults); + }); +}); diff --git a/test/integration/full/all-rules/frames/focusable.html b/test/integration/full/all-rules/frames/focusable.html new file mode 100644 index 0000000000..de5374ba35 --- /dev/null +++ b/test/integration/full/all-rules/frames/focusable.html @@ -0,0 +1,11 @@ + + + + Hello + + + + + + + From 5d0c35527b5f1978a6dfb958f1037865db0d45bd Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 10 Nov 2022 15:56:20 -0700 Subject: [PATCH 11/14] remove async --- test/integration/full/all-rules/all-rules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/full/all-rules/all-rules.js b/test/integration/full/all-rules/all-rules.js index 72a039f0de..4daeaa736d 100644 --- a/test/integration/full/all-rules/all-rules.js +++ b/test/integration/full/all-rules/all-rules.js @@ -7,7 +7,7 @@ describe('all rules test', () => { }); }); - it('should run all rules', async () => { + it('should run all rules', () => { assert.lengthOf(results.inapplicable, 0); }); From 261242808cdc8f8b22d7e68136cea310785588c0 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 11 Nov 2022 11:25:43 +0100 Subject: [PATCH 12/14] Doc fix --- lib/commons/dom/create-grid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 866ac822cc..5e019c6009 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -383,9 +383,9 @@ class Grid { /** * Scale the rect to the position within the grid - * @param {DOMRect} param0 + * @param {DOMRect} clientOrBoundingRect * @param {number} margin Offset outside the rect, default 0 - * @returns + * @returns {DOMRect} gridPosition */ getGridPositionOfRect({ top, right, bottom, left }, margin = 0) { top = this.toGridIndex(top - margin); From aacde52f30950f47b607d3992ca1de14b16b683a Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 11 Nov 2022 13:28:30 +0100 Subject: [PATCH 13/14] Docs! :-( --- 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 5e019c6009..6dd89b253e 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -365,7 +365,7 @@ class Grid { /** * Loop over all cells within the gridPosition rect * @param {DOMRect} gridPosition - * @param {(vNode:AbstractVirtualNode, place: { row: number, col: number}): any} callback + * @param {Function} callback */ loopGridPosition(gridPosition, callback) { const { left, right, top, bottom } = gridPosition; From 46ca4462d7c55161ce301c2b77d705efeb87234f Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Mon, 14 Nov 2022 17:07:31 +0100 Subject: [PATCH 14/14] Tweak as suggested --- lib/commons/dom/create-grid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 6dd89b253e..f4c0278502 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -348,7 +348,7 @@ class Grid { /** * Return an an array of nodes available at a particular grid coordinate * @param {DOMPoint} gridPosition - * @returns {Array|null} + * @returns {Array} */ getCellFromPoint({ x, y }) { assert(this.boundaries, 'Grid does not have cells added'); @@ -359,7 +359,7 @@ class Grid { 'Element midpoint exceeds the grid bounds' ); const row = this.cells[rowIndex - this.cells._negativeIndex] ?? []; - return row[colIndex - row._negativeIndex] ?? null; + return row[colIndex - row._negativeIndex] ?? []; } /**