Skip to content

Commit c7e0631

Browse files
Release 5.1.0 (#142)
1 parent fd6b33f commit c7e0631

File tree

4 files changed

+262
-3
lines changed

4 files changed

+262
-3
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { getNodeFromTree, memoize } from '../../core/utils';
2+
import { sanitize } from '../text';
3+
import { getIntersectionRect, getRectCenter, isPointInRect } from '../math';
4+
import getOverflowHiddenAncestors from './get-overflow-hidden-ancestors';
5+
import cache from '../../core/base/cache';
6+
7+
/**
8+
* Get the visible text client rects of a node.
9+
* @method getVisibleChildTextRect
10+
* @memberof axe.commons.dom
11+
* @instance
12+
* @param {Element} node
13+
*/
14+
const getVisibleChildTextRect = memoize(
15+
function getVisibleChildTextRectMemoized(node) {
16+
const vNode = getNodeFromTree(node);
17+
const nodeRect = vNode.boundingClientRect;
18+
const clientRects = [];
19+
const overflowHiddenNodes = getOverflowHiddenAncestors(vNode);
20+
21+
node.childNodes.forEach(textNode => {
22+
if (textNode.nodeType !== 3 || sanitize(textNode.nodeValue) === '') {
23+
return;
24+
}
25+
26+
const contentRects = getContentRects(textNode);
27+
if (isOutsideNodeBounds(contentRects, nodeRect) && !cache.get('ruleId')) {
28+
return;
29+
}
30+
31+
clientRects.push(...filterHiddenRects(contentRects, overflowHiddenNodes));
32+
});
33+
34+
// a11y-engine-domforge change
35+
if (clientRects.length <= 0) {
36+
return [];
37+
}
38+
/**
39+
* if all text rects are larger than the bounds of the node,
40+
* or goes outside of the bounds of the node, we need to use
41+
* the nodes bounding rect so we stay within the bounds of the
42+
* element.
43+
*
44+
* @see https://github.com/dequelabs/axe-core/issues/2178
45+
* @see https://github.com/dequelabs/axe-core/issues/2483
46+
* @see https://github.com/dequelabs/axe-core/issues/2681
47+
*
48+
* also need to resize the nodeRect to fit within the bounds of any overflow: hidden ancestors.
49+
*
50+
* @see https://github.com/dequelabs/axe-core/issues/4253
51+
*/
52+
return clientRects.length
53+
? clientRects
54+
: filterHiddenRects([nodeRect], overflowHiddenNodes);
55+
}
56+
);
57+
export default getVisibleChildTextRect;
58+
59+
function getContentRects(node) {
60+
const range = document.createRange();
61+
range.selectNodeContents(node);
62+
return Array.from(range.getClientRects());
63+
}
64+
65+
/**
66+
* Check to see if the text rect size is outside the of the
67+
* nodes bounding rect. Since we use the midpoint of the element
68+
* when determining the rect stack we will also use the midpoint
69+
* of the text rect to determine out of bounds
70+
*/
71+
function isOutsideNodeBounds(rects, nodeRect) {
72+
return rects.some(rect => {
73+
const centerPoint = getRectCenter(rect);
74+
return !isPointInRect(centerPoint, nodeRect);
75+
});
76+
}
77+
78+
/**
79+
* Filter out 0 width and height rects (newline characters) and
80+
* any rects that are outside the bounds of overflow hidden
81+
* ancestors
82+
*/
83+
function filterHiddenRects(contentRects, overflowHiddenNodes) {
84+
const visibleRects = [];
85+
contentRects.forEach(contentRect => {
86+
// ie11 has newline characters return 0.00998, so we'll say if the
87+
// line is < 1 it shouldn't be counted
88+
if (contentRect.width < 1 || contentRect.height < 1) {
89+
return;
90+
}
91+
92+
// update the rect size to fit inside the bounds of all overflow
93+
// hidden ancestors
94+
const visibleRect = overflowHiddenNodes.reduce((rect, overflowNode) => {
95+
return rect && getIntersectionRect(rect, overflowNode.boundingClientRect);
96+
}, contentRect);
97+
98+
if (visibleRect) {
99+
visibleRects.push(visibleRect);
100+
}
101+
});
102+
103+
return visibleRects;
104+
}

lib/commons/dom/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export { default as getTargetSize } from './get-target-size';
2222
export { default as getTextElementStack } from './get-text-element-stack';
2323
export { default as getViewportSize } from './get-viewport-size';
2424
export { default as getVisibleChildTextRects } from './get-visible-child-text-rects';
25+
export { default as getVisibleChildTextRect } from './get-visible-child-text-rect';
2526
export { default as hasContentVirtual } from './has-content-virtual';
2627
export { default as hasContent } from './has-content';
2728
export { default as hasLangText } from './has-lang-text';

lib/core/utils/dq-element.js

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,154 @@ function truncate(str, maxLength) {
1818
return str;
1919
}
2020

21+
/**
22+
* Escapes a string for use in CSS selectors
23+
* @param {String} str - The string to escape
24+
* @returns {String} The escaped string
25+
*/
26+
function escapeCSSSelector(str) {
27+
// Use the CSS.escape method if available
28+
if (window.CSS && window.CSS.escape) {
29+
return window.CSS.escape(str);
30+
}
31+
// Simple fallback for browsers that don't support CSS.escape
32+
return str
33+
.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&')
34+
.replace(/^\d/, '\\3$& ');
35+
}
36+
function generateSelectorWithShadow(elm) {
37+
const selectors = getShadowSelector(elm);
38+
if (typeof selectors === 'string') {
39+
return selectors;
40+
} else {
41+
// merge selectors of an array with ,
42+
return selectors.join(',').replace(/,$/, '');
43+
}
44+
}
45+
46+
function getShadowSelector(elm) {
47+
if (!elm) {
48+
return '';
49+
}
50+
let doc = (elm.getRootNode && elm.getRootNode()) || document;
51+
// Not a DOCUMENT_FRAGMENT - shadow DOM
52+
if (doc.nodeType !== 11) {
53+
return getFullPathSelector(elm);
54+
}
55+
56+
const stack = [];
57+
while (doc.nodeType === 11) {
58+
if (!doc.host) {
59+
return '';
60+
}
61+
stack.unshift({ elm, doc });
62+
elm = doc.host;
63+
doc = elm.getRootNode();
64+
}
65+
66+
stack.unshift({ elm, doc });
67+
return stack.map(item => getFullPathSelector(item.elm));
68+
}
69+
70+
function getFullPathSelector(elm) {
71+
if (elm.nodeName === 'HTML' || elm.nodeName === 'BODY') {
72+
return elm.nodeName.toLowerCase();
73+
}
74+
75+
if (cache.get('getFullPathSelector') === undefined) {
76+
cache.set('getFullPathSelector', new WeakMap());
77+
}
78+
79+
// Check cache first
80+
const sourceCache = cache.get('getFullPathSelector');
81+
if (sourceCache.has(elm)) {
82+
return sourceCache.get(elm);
83+
}
84+
85+
const element = elm;
86+
const names = [];
87+
while (elm.parentElement && elm.nodeName !== 'BODY') {
88+
if (sourceCache.has(elm)) {
89+
names.unshift(sourceCache.get(elm));
90+
break;
91+
} else if (elm.id) {
92+
// Check if the ID is unique in the document before using it
93+
const escapedId = escapeCSSSelector(elm.getAttribute('id'));
94+
const elementsWithSameId = document.querySelectorAll(`#${escapedId}`);
95+
if (elementsWithSameId.length === 1) {
96+
// ID is unique, safe to use
97+
names.unshift('#' + escapedId);
98+
break;
99+
} else {
100+
// ID is not unique, fallback to position-based selector
101+
let c = 1;
102+
let e = elm;
103+
for (; e.previousElementSibling; e = e.previousElementSibling, c++) {
104+
// Increment counter for each previous sibling
105+
}
106+
names.unshift(`${elm.nodeName.toLowerCase()}:nth-child(${c})`);
107+
}
108+
} else {
109+
let c = 1;
110+
let e = elm;
111+
for (; e.previousElementSibling; e = e.previousElementSibling, c++) {
112+
// Increment counter for each previous sibling
113+
}
114+
names.unshift(`${elm.nodeName.toLowerCase()}:nth-child(${c})`);
115+
}
116+
elm = elm.parentElement;
117+
}
118+
119+
const selector = names.join('>');
120+
sourceCache.set(element, selector);
121+
return selector;
122+
}
123+
124+
function getSourceOpt(element) {
125+
if (!element) {
126+
return '';
127+
}
128+
129+
// Initialize cache if needed
130+
if (cache.get('getSourceEfficient') === undefined) {
131+
cache.set('getSourceEfficient', new WeakMap());
132+
}
133+
134+
// Check cache first
135+
const sourceCache = cache.get('getSourceEfficient');
136+
if (sourceCache.has(element)) {
137+
return sourceCache.get(element);
138+
}
139+
140+
// Compute value if not cached
141+
const tagName = element.nodeName?.toLowerCase();
142+
if (!tagName) {
143+
return '';
144+
}
145+
146+
let result;
147+
try {
148+
const attributes = Array.from(element.attributes || [])
149+
.filter(attr => !attr.name.startsWith('data-percy-'))
150+
.map(attr => `${attr.name}="${attr.value}"`)
151+
.join(' ');
152+
const closingTag = element.children.length ? false : true;
153+
if (closingTag) {
154+
result = `<${tagName} ${attributes}>${element.textContent}</${tagName}>`;
155+
} else {
156+
result = attributes ? `<${tagName} ${attributes}>` : `<${tagName}>`;
157+
}
158+
result = truncate(result, 300); // Truncate to 300 characters
159+
// Store in cache
160+
sourceCache.set(element, result);
161+
} catch (e) {
162+
// Handle potential errors (like accessing attributes on non-element nodes)
163+
result = `<${tagName || 'unknown'}>`;
164+
}
165+
166+
return result;
167+
}
168+
21169
function getSource(element) {
22170
if (!element?.outerHTML) {
23171
return '';
@@ -84,7 +232,11 @@ function DqElement(elm, options = null, spec = {}) {
84232
this.source = null;
85233
// TODO: es-modules_audit
86234
if (!axe._audit.noHtml) {
87-
this.source = this.spec.source ?? getSource(this._element);
235+
if (axe._cache.get('runTypeAOpt')) {
236+
this.source = this.spec.source ?? getSourceOpt(this._element);
237+
} else {
238+
this.source = this.spec.source ?? getSource(this._element);
239+
}
88240
}
89241
}
90242

@@ -94,6 +246,9 @@ DqElement.prototype = {
94246
* @return {String}
95247
*/
96248
get selector() {
249+
if (axe._cache.get('runTypeAOpt')) {
250+
return this.spec.selector || [generateSelectorWithShadow(this.element)];
251+
}
97252
return this.spec.selector || [getSelector(this.element, this._options)];
98253
},
99254

lib/rules/autocomplete-valid.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
"EN-301-549",
1010
"EN-9.1.3.5",
1111
"ACT",
12-
"a11y-engine",
13-
"a11y-engine-experimental"
12+
"a11y-engine"
1413
],
1514
"actIds": ["73f2c2"],
1615
"metadata": {

0 commit comments

Comments
 (0)