Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(target-size): update to match new spacing requirements #4117

Merged
merged 18 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/checks/mobile/target-offset-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { getOffset } from '../../commons/math';
const roundingMargin = 0.05;

export default function targetOffsetEvaluate(node, options, vNode) {
const minOffset = options?.minOffset || 24;
const minOffset = (options?.minOffset || 24) / 2;
straker marked this conversation as resolved.
Show resolved Hide resolved
const closeNeighbors = [];
let closestOffset = minOffset;
for (const vNeighbor of findNearbyElms(vNode, minOffset)) {
if (getRoleType(vNeighbor) !== 'widget' || !isFocusable(vNeighbor)) {
continue;
}
const offset = roundToSingleDecimal(getOffset(vNode, vNeighbor));
const offset = roundToSingleDecimal(getOffset(vNode, vNeighbor, minOffset));
if (offset + roundingMargin >= minOffset) {
continue;
}
Expand Down
70 changes: 70 additions & 0 deletions lib/commons/dom/get-target-size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import findNearbyElms from './find-nearby-elms';
import { splitRects, hasVisualOverlap } from '../math';
import memoize from '../../core/utils/memoize';

const roundingMargin = 0.05;

export default memoize(getTargetSize);

/**
* Compute the target size of an element.
* @see https://www.w3.org/TR/WCAG22/#dfn-targets
*/
function getTargetSize(vNode, minSize) {
const nodeRect = vNode.boundingClientRect;
const overlappingVNodes = findNearbyElms(vNode).filter(vNeighbor => {
return (
vNeighbor.getComputedStylePropertyValue('pointer-events') !== 'none' &&
hasVisualOverlap(vNode, vNeighbor)
);
});

if (!overlappingVNodes.length) {
return {
rect: nodeRect
};
straker marked this conversation as resolved.
Show resolved Hide resolved
}

return {
rect: getLargestUnobscuredArea(vNode, overlappingVNodes, minSize)
};
straker marked this conversation as resolved.
Show resolved Hide resolved
}

// Find areas of the target that are not obscured
function getLargestUnobscuredArea(vNode, obscuredNodes, minSize) {
const nodeRect = vNode.boundingClientRect;
if (obscuredNodes.length === 0) {
return null;
}
const obscuringRects = obscuredNodes.map(
({ boundingClientRect: rect }) => rect
);
const unobscuredRects = splitRects(nodeRect, obscuringRects);
if (!unobscuredRects.length) {
return null;
}

// Of the unobscured inner rects, work out the largest
return getLargestRect(unobscuredRects, minSize);
}

// Find the largest rectangle in the array, prioritize ones that meet a minimum size
function getLargestRect(rects, minSize) {
return rects.reduce((rectA, rectB) => {
const rectAisMinimum = rectHasMinimumSize(minSize, rectA);
const rectBisMinimum = rectHasMinimumSize(minSize, rectB);
// Prioritize rects that pass the minimum
if (rectAisMinimum !== rectBisMinimum) {
return rectAisMinimum ? rectA : rectB;
}
const areaA = rectA.width * rectA.height;
const areaB = rectB.width * rectB.height;
return areaA > areaB ? rectA : rectB;
});
}

function rectHasMinimumSize(minSize, { width, height }) {
return (
width + roundingMargin >= minSize && height + roundingMargin >= minSize
);
}
1 change: 1 addition & 0 deletions lib/commons/dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { default as getOverflowHiddenAncestors } from './get-overflow-hidden-anc
export { default as getRootNode } from './get-root-node';
export { default as getScrollOffset } from './get-scroll-offset';
export { default as getTabbableElements } from './get-tabbable-elements';
export { default as getTargetSize } from './get-target-size';
export { default as getTextElementStack } from './get-text-element-stack';
export { default as getViewportSize } from './get-viewport-size';
export { default as getVisibleChildTextRects } from './get-visible-child-text-rects';
Expand Down
168 changes: 44 additions & 124 deletions lib/commons/math/get-offset.js
Original file line number Diff line number Diff line change
@@ -1,91 +1,61 @@
import { getTargetSize } from '../dom';

/**
* Get the offset between node A and node B
* @method getOffset
* @memberof axe.commons.math
* @param {VirtualNode} vNodeA
* @param {VirtualNode} vNodeB
* @param {Number} radius
* @returns {number}
*/
export default function getOffset(vNodeA, vNodeB) {
const rectA = vNodeA.boundingClientRect;
const rectB = vNodeB.boundingClientRect;
const pointA = getFarthestPoint(rectA, rectB);
const pointB = getClosestPoint(pointA, rectA, rectB);
return pointDistance(pointA, pointB);
}

/**
* Get a point on rectA that is farthest away from rectB
* @param {Rect} rectA
* @param {Rect} rectB
* @returns {Point}
*/
function getFarthestPoint(rectA, rectB) {
const dimensionProps = [
['x', 'left', 'right', 'width'],
['y', 'top', 'bottom', 'height']
];
const farthestPoint = {};
dimensionProps.forEach(([axis, start, end, diameter]) => {
if (rectB[start] < rectA[start] && rectB[end] > rectA[end]) {
farthestPoint[axis] = rectA[start] + rectA[diameter] / 2; // center | middle
return;
}
// Work out which edge of A is farthest away from the center of B
const centerB = rectB[start] + rectB[diameter] / 2;
const startDistance = Math.abs(centerB - rectA[start]);
const endDistance = Math.abs(centerB - rectA[end]);
if (startDistance >= endDistance) {
farthestPoint[axis] = rectA[start]; // left | top
} else {
farthestPoint[axis] = rectA[end]; // right | bottom
}
});
return farthestPoint;
}
export default function getOffset(vNodeA, vNodeB, radius = 12) {
straker marked this conversation as resolved.
Show resolved Hide resolved
const { rect: rectA } = getTargetSize(vNodeA);
const { rect: rectB } = getTargetSize(vNodeB);
straker marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get a point on the adjacentRect, that is as close the point given from ownRect
* @param {Point} ownRectPoint
* @param {Rect} ownRect
* @param {Rect} adjacentRect
* @returns {Point}
*/
function getClosestPoint({ x, y }, ownRect, adjacentRect) {
if (pointInRect({ x, y }, adjacentRect)) {
// Check if there is an opposite corner inside the adjacent rectangle
const closestPoint = getCornerInAdjacentRect(
{ x, y },
ownRect,
adjacentRect
);
if (closestPoint !== null) {
return closestPoint;
}
adjacentRect = ownRect;
// one of the rects is fully obscured
if (rectA === null || rectB === null) {
return 0;
}

const { top, right, bottom, left } = adjacentRect;
// Is the adjacent rect horizontally or vertically aligned
const xAligned = x >= left && x <= right;
const yAligned = y >= top && y <= bottom;
// Find the closest edge of the adjacent rect
const closestX = Math.abs(left - x) < Math.abs(right - x) ? left : right;
const closestY = Math.abs(top - y) < Math.abs(bottom - y) ? top : bottom;
const centerA = {
x: rectA.x + rectA.width / 2,
y: rectA.y + rectA.height / 2
};
const centerB = {
x: rectB.x + rectB.width / 2,
y: rectB.y + rectB.height / 2
};
const sideB = getClosestPoint(centerA, rectB);

if (!xAligned && yAligned) {
return { x: closestX, y }; // Closest horizontal point
} else if (xAligned && !yAligned) {
return { x, y: closestY }; // Closest vertical point
} else if (!xAligned && !yAligned) {
return { x: closestX, y: closestY }; // Closest diagonal corner
return Math.min(
// subtract the radius of the circle from the distance
pointDistance(centerA, centerB) - radius,
pointDistance(centerA, sideB)
);
}

function getClosestPoint(point, rect) {
let x;
let y;

if (point.x < rect.left) {
x = rect.left;
} else if (point.x > rect.right) {
x = rect.right;
} else {
x = point.x;
}
// ownRect (partially) obscures adjacentRect
if (Math.abs(x - closestX) < Math.abs(y - closestY)) {
return { x: closestX, y }; // Inside, closest edge is horizontal

if (point.y < rect.top) {
y = rect.top;
} else if (point.y > rect.bottom) {
y = rect.bottom;
} else {
return { x, y: closestY }; // Inside, closest edge is vertical
y = point.y;
}

return { x, y };
}

/**
Expand All @@ -95,55 +65,5 @@ function getClosestPoint({ x, y }, ownRect, adjacentRect) {
* @returns {number}
*/
function pointDistance(pointA, pointB) {
const xDistance = Math.abs(pointA.x - pointB.x);
const yDistance = Math.abs(pointA.y - pointB.y);
if (!xDistance || !yDistance) {
return xDistance || yDistance; // If either is 0, return the other
}
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}

/**
* Return if a point is within a rect
* @param {Point} point
* @param {Rect} rect
* @returns {boolean}
*/
function pointInRect({ x, y }, rect) {
return y >= rect.top && x <= rect.right && y <= rect.bottom && x >= rect.left;
}

/**
*
* @param {Point} ownRectPoint
* @param {Rect} ownRect
* @param {Rect} adjacentRect
* @returns {Point | null} With x and y
*/
function getCornerInAdjacentRect({ x, y }, ownRect, adjacentRect) {
let closestX, closestY;
// Find the opposite corner, if it is inside the adjacent rect;
if (x === ownRect.left && ownRect.right < adjacentRect.right) {
closestX = ownRect.right;
} else if (x === ownRect.right && ownRect.left > adjacentRect.left) {
closestX = ownRect.left;
}
if (y === ownRect.top && ownRect.bottom < adjacentRect.bottom) {
closestY = ownRect.bottom;
} else if (y === ownRect.bottom && ownRect.top > adjacentRect.top) {
closestY = ownRect.top;
}

if (!closestX && !closestY) {
return null; // opposite corners are outside the rect, or {x,y} was a center point
} else if (!closestY) {
return { x: closestX, y };
} else if (!closestX) {
return { x, y: closestY };
}
if (Math.abs(x - closestX) < Math.abs(y - closestY)) {
return { x: closestX, y };
} else {
return { x, y: closestY };
}
return Math.hypot(pointA.x - pointB.x, pointA.y - pointB.y);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function exists in IE11 (I've verified)

}
30 changes: 22 additions & 8 deletions lib/commons/math/split-rects.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @memberof axe.commons.math
* @param {DOMRect} outerRect
* @param {DOMRect[]} overlapRects
* @returns {Rect[]} Unique array of rects
* @returns {DOMRect[]} Unique array of rects
*/
export default function splitRects(outerRect, overlapRects) {
let uniqueRects = [outerRect];
Expand Down Expand Up @@ -37,19 +37,33 @@ function splitRect(inputRect, clipRect) {
rects.push({ top, left, bottom, right: clipRect.left });
}
if (rects.length === 0) {
// Fully overlapping
if (isEnclosedRect(inputRect, clipRect)) {
return [];
}

rects.push(inputRect); // No intersection
}

return rects.map(computeRect); // add x / y / width / height
}

const between = (num, min, max) => num > min && num < max;

function computeRect(baseRect) {
return {
...baseRect,
x: baseRect.left,
y: baseRect.top,
height: baseRect.bottom - baseRect.top,
width: baseRect.right - baseRect.left
};
return new window.DOMRect(
Copy link
Contributor Author

@straker straker Aug 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was weird that split rects works on DOMRects but then doesn't return one, which meant that doing splitRect(rect1, rect2).left would return undefined instead of the x value.

baseRect.left,
baseRect.top,
baseRect.right - baseRect.left,
baseRect.bottom - baseRect.top
);
}

function isEnclosedRect(rectA, rectB) {
return (
rectA.top >= rectB.top &&
rectA.left >= rectB.left &&
rectA.bottom <= rectB.bottom &&
rectA.right <= rectB.right
);
}
Loading