html-validate 7.5.0 → 7.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/browser.d.ts +1 -1
- package/dist/cjs/browser.js +5 -0
- package/dist/cjs/browser.js.map +1 -1
- package/dist/cjs/core.d.ts +52 -1
- package/dist/cjs/core.js +363 -130
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/jest.d.ts +1 -1
- package/dist/cjs/test-utils.d.ts +1 -1
- package/dist/es/browser.d.ts +1 -1
- package/dist/es/browser.js +1 -1
- package/dist/es/cli.js +1 -1
- package/dist/es/core.d.ts +52 -1
- package/dist/es/core.js +355 -123
- package/dist/es/core.js.map +1 -1
- package/dist/es/html-validate.js +1 -1
- package/dist/es/index.d.ts +2 -2
- package/dist/es/index.js +1 -1
- package/dist/es/jest-lib.js +1 -1
- package/dist/es/jest.d.ts +1 -1
- package/dist/es/test-utils.d.ts +1 -1
- package/package.json +9 -9
package/dist/es/core.js
CHANGED
|
@@ -1736,9 +1736,19 @@ function factory(name, context) {
|
|
|
1736
1736
|
function stripslashes(value) {
|
|
1737
1737
|
return value.replace(/\\(.)/g, "$1");
|
|
1738
1738
|
}
|
|
1739
|
+
/**
|
|
1740
|
+
* @internal
|
|
1741
|
+
*/
|
|
1739
1742
|
function escapeSelectorComponent(text) {
|
|
1740
1743
|
return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
|
|
1741
1744
|
}
|
|
1745
|
+
/**
|
|
1746
|
+
* @internal
|
|
1747
|
+
*/
|
|
1748
|
+
function generateIdSelector(id) {
|
|
1749
|
+
const escaped = escapeSelectorComponent(id);
|
|
1750
|
+
return escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
1751
|
+
}
|
|
1742
1752
|
/**
|
|
1743
1753
|
* Returns true if the character is a delimiter for different kinds of selectors:
|
|
1744
1754
|
*
|
|
@@ -2115,6 +2125,25 @@ class HtmlElement extends DOMNode {
|
|
|
2115
2125
|
return `<${this.tagName}>`;
|
|
2116
2126
|
}
|
|
2117
2127
|
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Get list of IDs referenced by `aria-labelledby`.
|
|
2130
|
+
*
|
|
2131
|
+
* If the attribute is unset or empty this getter returns null.
|
|
2132
|
+
* If the attribute is dynamic the original {@link DynamicValue} is returned.
|
|
2133
|
+
*
|
|
2134
|
+
* @public
|
|
2135
|
+
*/
|
|
2136
|
+
get ariaLabelledby() {
|
|
2137
|
+
const attr = this.getAttribute("aria-labelledby");
|
|
2138
|
+
if (!attr || !attr.value) {
|
|
2139
|
+
return null;
|
|
2140
|
+
}
|
|
2141
|
+
if (attr.value instanceof DynamicValue) {
|
|
2142
|
+
return attr.value;
|
|
2143
|
+
}
|
|
2144
|
+
const list = new DOMTokenList(attr.value, attr.valueLocation);
|
|
2145
|
+
return list.length ? Array.from(list) : null;
|
|
2146
|
+
}
|
|
2118
2147
|
/**
|
|
2119
2148
|
* Similar to childNodes but only elements.
|
|
2120
2149
|
*/
|
|
@@ -2156,8 +2185,7 @@ class HtmlElement extends DOMNode {
|
|
|
2156
2185
|
for (let cur = this; cur.parent; cur = cur.parent) {
|
|
2157
2186
|
/* if a unique id is present, use it and short-circuit */
|
|
2158
2187
|
if (cur.id) {
|
|
2159
|
-
const
|
|
2160
|
-
const selector = escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
2188
|
+
const selector = generateIdSelector(cur.id);
|
|
2161
2189
|
const matches = root.querySelectorAll(selector);
|
|
2162
2190
|
if (matches.length === 1) {
|
|
2163
2191
|
parts.push(selector);
|
|
@@ -3163,7 +3191,7 @@ var TRANSFORMER_API;
|
|
|
3163
3191
|
/** @public */
|
|
3164
3192
|
const name = "html-validate";
|
|
3165
3193
|
/** @public */
|
|
3166
|
-
const version = "7.
|
|
3194
|
+
const version = "7.6.0";
|
|
3167
3195
|
/** @public */
|
|
3168
3196
|
const homepage = "https://html-validate.org";
|
|
3169
3197
|
/** @public */
|
|
@@ -5386,13 +5414,122 @@ class ElementPermittedOrder extends Rule {
|
|
|
5386
5414
|
}
|
|
5387
5415
|
}
|
|
5388
5416
|
|
|
5389
|
-
const
|
|
5417
|
+
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
5418
|
+
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
5419
|
+
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
5420
|
+
/**
|
|
5421
|
+
* Tests if this element is present in the accessibility tree.
|
|
5422
|
+
*
|
|
5423
|
+
* In practice it tests whenever the element or its parents has
|
|
5424
|
+
* `role="presentation"` or `aria-hidden="false"`. Dynamic values counts as
|
|
5425
|
+
* visible since the element might be in the visibility tree sometimes.
|
|
5426
|
+
*/
|
|
5427
|
+
function inAccessibilityTree(node) {
|
|
5428
|
+
return !isAriaHidden(node) && !isPresentation(node);
|
|
5429
|
+
}
|
|
5430
|
+
function isAriaHiddenImpl(node) {
|
|
5431
|
+
const isHidden = (node) => {
|
|
5432
|
+
const ariaHidden = node.getAttribute("aria-hidden");
|
|
5433
|
+
return Boolean(ariaHidden && ariaHidden.value === "true");
|
|
5434
|
+
};
|
|
5435
|
+
return {
|
|
5436
|
+
byParent: node.parent ? isAriaHidden(node.parent) : false,
|
|
5437
|
+
bySelf: isHidden(node),
|
|
5438
|
+
};
|
|
5439
|
+
}
|
|
5440
|
+
function isAriaHidden(node, details) {
|
|
5441
|
+
const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
|
|
5442
|
+
if (cached) {
|
|
5443
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
5444
|
+
}
|
|
5445
|
+
const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
|
|
5446
|
+
return details ? result : result.byParent || result.bySelf;
|
|
5447
|
+
}
|
|
5448
|
+
function isHTMLHiddenImpl(node) {
|
|
5449
|
+
const isHidden = (node) => {
|
|
5450
|
+
const hidden = node.getAttribute("hidden");
|
|
5451
|
+
return hidden !== null && hidden.isStatic;
|
|
5452
|
+
};
|
|
5453
|
+
return {
|
|
5454
|
+
byParent: node.parent ? isHTMLHidden(node.parent) : false,
|
|
5455
|
+
bySelf: isHidden(node),
|
|
5456
|
+
};
|
|
5457
|
+
}
|
|
5458
|
+
function isHTMLHidden(node, details) {
|
|
5459
|
+
const cached = node.cacheGet(HTML_HIDDEN_CACHE);
|
|
5460
|
+
if (cached) {
|
|
5461
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
5462
|
+
}
|
|
5463
|
+
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
5464
|
+
return details ? result : result.byParent || result.bySelf;
|
|
5465
|
+
}
|
|
5466
|
+
/**
|
|
5467
|
+
* Tests if this element or a parent element has role="presentation".
|
|
5468
|
+
*
|
|
5469
|
+
* Dynamic values yields `false` just as if the attribute wasn't present.
|
|
5470
|
+
*/
|
|
5471
|
+
function isPresentation(node) {
|
|
5472
|
+
if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
|
|
5473
|
+
return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
|
|
5474
|
+
}
|
|
5475
|
+
let cur = node;
|
|
5476
|
+
do {
|
|
5477
|
+
const role = cur.getAttribute("role");
|
|
5478
|
+
/* role="presentation" */
|
|
5479
|
+
if (role && role.value === "presentation") {
|
|
5480
|
+
return cur.cacheSet(ROLE_PRESENTATION_CACHE, true);
|
|
5481
|
+
}
|
|
5482
|
+
/* sanity check: break if no parent is present, normally not an issue as the
|
|
5483
|
+
* root element should be found first */
|
|
5484
|
+
if (!cur.parent) {
|
|
5485
|
+
break;
|
|
5486
|
+
}
|
|
5487
|
+
/* check parents */
|
|
5488
|
+
cur = cur.parent;
|
|
5489
|
+
} while (!cur.isRootElement());
|
|
5490
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5493
|
+
const cachePrefix = classifyNodeText.name;
|
|
5494
|
+
const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
|
|
5495
|
+
const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
|
|
5496
|
+
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
5497
|
+
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
5498
|
+
/**
|
|
5499
|
+
* @public
|
|
5500
|
+
*/
|
|
5390
5501
|
var TextClassification;
|
|
5391
5502
|
(function (TextClassification) {
|
|
5392
5503
|
TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
5393
5504
|
TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
5394
5505
|
TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
5395
5506
|
})(TextClassification || (TextClassification = {}));
|
|
5507
|
+
function getCachekey(options = {}) {
|
|
5508
|
+
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
5509
|
+
if (accessible && ignoreHiddenRoot) {
|
|
5510
|
+
return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
|
|
5511
|
+
}
|
|
5512
|
+
else if (ignoreHiddenRoot) {
|
|
5513
|
+
return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
|
|
5514
|
+
}
|
|
5515
|
+
else if (accessible) {
|
|
5516
|
+
return A11Y_CACHE_KEY;
|
|
5517
|
+
}
|
|
5518
|
+
else {
|
|
5519
|
+
return HTML_CACHE_KEY;
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
/* While I cannot find a reference about this in the standard the <select>
|
|
5523
|
+
* element kinda acts as if there is no text content, most particularly it
|
|
5524
|
+
* doesn't receive and accessible name. The `.textContent` property does
|
|
5525
|
+
* however include the <option> childrens text. But for the sake of the
|
|
5526
|
+
* validator it is probably best if the classification acts as if there is no
|
|
5527
|
+
* text as I think that is what is expected of the return values. Might have
|
|
5528
|
+
* to revisit this at some point or if someone could clarify what section of
|
|
5529
|
+
* the standard deals with this. */
|
|
5530
|
+
function isSpecialEmpty(node) {
|
|
5531
|
+
return node.is("select") || node.is("textarea");
|
|
5532
|
+
}
|
|
5396
5533
|
/**
|
|
5397
5534
|
* Checks text content of an element.
|
|
5398
5535
|
*
|
|
@@ -5400,33 +5537,54 @@ var TextClassification;
|
|
|
5400
5537
|
* ignored.
|
|
5401
5538
|
*
|
|
5402
5539
|
* If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
|
|
5540
|
+
*
|
|
5541
|
+
* @public
|
|
5403
5542
|
*/
|
|
5404
|
-
function classifyNodeText(node) {
|
|
5405
|
-
|
|
5406
|
-
|
|
5543
|
+
function classifyNodeText(node, options = {}) {
|
|
5544
|
+
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
5545
|
+
const cacheKey = getCachekey(options);
|
|
5546
|
+
if (node.cacheExists(cacheKey)) {
|
|
5547
|
+
return node.cacheGet(cacheKey);
|
|
5548
|
+
}
|
|
5549
|
+
if (!ignoreHiddenRoot && isHTMLHidden(node)) {
|
|
5550
|
+
return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
|
|
5551
|
+
}
|
|
5552
|
+
if (!ignoreHiddenRoot && accessible && isAriaHidden(node)) {
|
|
5553
|
+
return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
|
|
5407
5554
|
}
|
|
5408
|
-
|
|
5555
|
+
if (isSpecialEmpty(node)) {
|
|
5556
|
+
return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
|
|
5557
|
+
}
|
|
5558
|
+
const text = findTextNodes(node, {
|
|
5559
|
+
...options,
|
|
5560
|
+
ignoreHiddenRoot: false,
|
|
5561
|
+
});
|
|
5409
5562
|
/* if any text is dynamic classify as dynamic */
|
|
5410
5563
|
if (text.some((cur) => cur.isDynamic)) {
|
|
5411
|
-
return node.cacheSet(
|
|
5564
|
+
return node.cacheSet(cacheKey, TextClassification.DYNAMIC_TEXT);
|
|
5412
5565
|
}
|
|
5413
5566
|
/* if any text has non-whitespace character classify as static */
|
|
5414
5567
|
if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
|
|
5415
|
-
return node.cacheSet(
|
|
5568
|
+
return node.cacheSet(cacheKey, TextClassification.STATIC_TEXT);
|
|
5416
5569
|
}
|
|
5417
5570
|
/* default to empty */
|
|
5418
|
-
return node.cacheSet(
|
|
5571
|
+
return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
|
|
5419
5572
|
}
|
|
5420
|
-
function findTextNodes(node) {
|
|
5573
|
+
function findTextNodes(node, options) {
|
|
5574
|
+
const { accessible = false } = options;
|
|
5421
5575
|
let text = [];
|
|
5422
5576
|
for (const child of node.childNodes) {
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5577
|
+
if (isTextNode(child)) {
|
|
5578
|
+
text.push(child);
|
|
5579
|
+
}
|
|
5580
|
+
else if (isElementNode(child)) {
|
|
5581
|
+
if (isHTMLHidden(child, true).bySelf) {
|
|
5582
|
+
continue;
|
|
5583
|
+
}
|
|
5584
|
+
if (accessible && isAriaHidden(child, true).bySelf) {
|
|
5585
|
+
continue;
|
|
5586
|
+
}
|
|
5587
|
+
text = text.concat(findTextNodes(child, options));
|
|
5430
5588
|
}
|
|
5431
5589
|
}
|
|
5432
5590
|
return text;
|
|
@@ -5702,6 +5860,17 @@ class ElementRequiredContent extends Rule {
|
|
|
5702
5860
|
}
|
|
5703
5861
|
|
|
5704
5862
|
const selector = ["h1", "h2", "h3", "h4", "h5", "h6"].join(",");
|
|
5863
|
+
function hasImgAltText$1(node) {
|
|
5864
|
+
if (node.is("img")) {
|
|
5865
|
+
return hasAltText(node);
|
|
5866
|
+
}
|
|
5867
|
+
else if (node.is("svg")) {
|
|
5868
|
+
return node.textContent.trim() !== "";
|
|
5869
|
+
}
|
|
5870
|
+
else {
|
|
5871
|
+
return false;
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5705
5874
|
class EmptyHeading extends Rule {
|
|
5706
5875
|
documentation() {
|
|
5707
5876
|
return {
|
|
@@ -5713,19 +5882,28 @@ class EmptyHeading extends Rule {
|
|
|
5713
5882
|
this.on("dom:ready", ({ document }) => {
|
|
5714
5883
|
const headings = document.querySelectorAll(selector);
|
|
5715
5884
|
for (const heading of headings) {
|
|
5716
|
-
|
|
5717
|
-
case TextClassification.DYNAMIC_TEXT:
|
|
5718
|
-
case TextClassification.STATIC_TEXT:
|
|
5719
|
-
/* have some text content, consider ok */
|
|
5720
|
-
break;
|
|
5721
|
-
case TextClassification.EMPTY_TEXT:
|
|
5722
|
-
/* no content or whitespace only */
|
|
5723
|
-
this.report(heading, `<${heading.tagName}> cannot be empty, must have text content`);
|
|
5724
|
-
break;
|
|
5725
|
-
}
|
|
5885
|
+
this.validateHeading(heading);
|
|
5726
5886
|
}
|
|
5727
5887
|
});
|
|
5728
5888
|
}
|
|
5889
|
+
validateHeading(heading) {
|
|
5890
|
+
const images = heading.querySelectorAll("img, svg");
|
|
5891
|
+
for (const child of images) {
|
|
5892
|
+
if (hasImgAltText$1(child)) {
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
switch (classifyNodeText(heading)) {
|
|
5897
|
+
case TextClassification.DYNAMIC_TEXT:
|
|
5898
|
+
case TextClassification.STATIC_TEXT:
|
|
5899
|
+
/* have some text content, consider ok */
|
|
5900
|
+
break;
|
|
5901
|
+
case TextClassification.EMPTY_TEXT:
|
|
5902
|
+
/* no content or whitespace only */
|
|
5903
|
+
this.report(heading, `<${heading.tagName}> cannot be empty, must have text content`);
|
|
5904
|
+
break;
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5729
5907
|
}
|
|
5730
5908
|
|
|
5731
5909
|
class EmptyTitle extends Rule {
|
|
@@ -6146,104 +6324,148 @@ class InputAttributes extends Rule {
|
|
|
6146
6324
|
}
|
|
6147
6325
|
}
|
|
6148
6326
|
|
|
6149
|
-
const
|
|
6150
|
-
|
|
6151
|
-
const
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
*/
|
|
6159
|
-
function inAccessibilityTree(node) {
|
|
6160
|
-
return !isAriaHidden(node) && !isPresentation(node);
|
|
6327
|
+
const HAS_ACCESSIBLE_TEXT_CACHE = Symbol(hasAccessibleName.name);
|
|
6328
|
+
function isHidden(node, context) {
|
|
6329
|
+
const { reference } = context;
|
|
6330
|
+
if (reference && reference.isSameNode(node)) {
|
|
6331
|
+
return false;
|
|
6332
|
+
}
|
|
6333
|
+
else {
|
|
6334
|
+
return isHTMLHidden(node) || !inAccessibilityTree(node);
|
|
6335
|
+
}
|
|
6161
6336
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
* Dynamic values yields `false` since the element will conditionally be in the
|
|
6166
|
-
* accessibility tree and must fulfill it's conditions.
|
|
6167
|
-
*/
|
|
6168
|
-
function isAriaHidden(node) {
|
|
6169
|
-
if (node.cacheExists(ARIA_HIDDEN_CACHE)) {
|
|
6170
|
-
return Boolean(node.cacheGet(ARIA_HIDDEN_CACHE));
|
|
6337
|
+
function hasImgAltText(node, context) {
|
|
6338
|
+
if (node.is("img")) {
|
|
6339
|
+
return hasAltText(node);
|
|
6171
6340
|
}
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
if (!cur.parent) {
|
|
6182
|
-
break;
|
|
6341
|
+
else if (node.is("svg")) {
|
|
6342
|
+
return node.textContent.trim() !== "";
|
|
6343
|
+
}
|
|
6344
|
+
else {
|
|
6345
|
+
for (const img of node.querySelectorAll("img, svg")) {
|
|
6346
|
+
const hasName = hasAccessibleNameImpl(img, context);
|
|
6347
|
+
if (hasName) {
|
|
6348
|
+
return true;
|
|
6349
|
+
}
|
|
6183
6350
|
}
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6351
|
+
return false;
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
function hasLabel(node) {
|
|
6355
|
+
var _a;
|
|
6356
|
+
const value = (_a = node.getAttributeValue("aria-label")) !== null && _a !== void 0 ? _a : "";
|
|
6357
|
+
return Boolean(value.trim());
|
|
6358
|
+
}
|
|
6359
|
+
function isLabelledby(node, context) {
|
|
6360
|
+
const { document, reference } = context;
|
|
6361
|
+
/* if we already have resolved one level of reference we don't resolve another
|
|
6362
|
+
* level (as per accname step 2B) */
|
|
6363
|
+
if (reference) {
|
|
6364
|
+
return false;
|
|
6365
|
+
}
|
|
6366
|
+
const ariaLabelledby = node.ariaLabelledby;
|
|
6367
|
+
/* consider dynamic aria-labelledby as having a name as we cannot resolve it
|
|
6368
|
+
* so no way to prove correctness */
|
|
6369
|
+
if (ariaLabelledby instanceof DynamicValue) {
|
|
6370
|
+
return true;
|
|
6371
|
+
}
|
|
6372
|
+
/* ignore elements without aria-labelledby */
|
|
6373
|
+
if (ariaLabelledby === null) {
|
|
6374
|
+
return false;
|
|
6375
|
+
}
|
|
6376
|
+
return ariaLabelledby.some((id) => {
|
|
6377
|
+
const selector = generateIdSelector(id);
|
|
6378
|
+
return document.querySelectorAll(selector).some((child) => {
|
|
6379
|
+
return hasAccessibleNameImpl(child, {
|
|
6380
|
+
document,
|
|
6381
|
+
reference: child,
|
|
6382
|
+
});
|
|
6383
|
+
});
|
|
6384
|
+
});
|
|
6188
6385
|
}
|
|
6189
6386
|
/**
|
|
6190
|
-
*
|
|
6387
|
+
* This algorithm is based on ["Accessible Name and Description Computation
|
|
6388
|
+
* 1.2"][accname] with some exceptions:
|
|
6389
|
+
*
|
|
6390
|
+
* It doesn't compute the actual name but only the presence of one, e.g. if a
|
|
6391
|
+
* non-empty flat string is present the algorithm terminates with a positive
|
|
6392
|
+
* result.
|
|
6191
6393
|
*
|
|
6192
|
-
*
|
|
6193
|
-
*
|
|
6394
|
+
* It takes some optimization shortcuts such as starting with step F as it
|
|
6395
|
+
* would be more common usage and as there is no actual name being computed
|
|
6396
|
+
* the order wont matter.
|
|
6397
|
+
*
|
|
6398
|
+
* [accname]: https://w3c.github.io/accname
|
|
6194
6399
|
*/
|
|
6195
|
-
function
|
|
6196
|
-
|
|
6197
|
-
|
|
6400
|
+
function hasAccessibleNameImpl(current, context) {
|
|
6401
|
+
const { reference } = context;
|
|
6402
|
+
/* if this element is hidden (see function for exceptions) it does not have an accessible name */
|
|
6403
|
+
if (isHidden(current, context)) {
|
|
6404
|
+
return false;
|
|
6198
6405
|
}
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6406
|
+
/* special case: when this element is directly referenced by aria-labelledby
|
|
6407
|
+
* we ignore `hidden` */
|
|
6408
|
+
const ignoreHiddenRoot = Boolean(reference && reference.isSameNode(current));
|
|
6409
|
+
const text = classifyNodeText(current, { accessible: true, ignoreHiddenRoot });
|
|
6410
|
+
if (text !== TextClassification.EMPTY_TEXT) {
|
|
6411
|
+
return true;
|
|
6412
|
+
}
|
|
6413
|
+
if (hasImgAltText(current, context)) {
|
|
6414
|
+
return true;
|
|
6415
|
+
}
|
|
6416
|
+
if (hasLabel(current)) {
|
|
6417
|
+
return true;
|
|
6418
|
+
}
|
|
6419
|
+
if (isLabelledby(current, context)) {
|
|
6420
|
+
return true;
|
|
6421
|
+
}
|
|
6422
|
+
return false;
|
|
6215
6423
|
}
|
|
6216
6424
|
/**
|
|
6217
|
-
*
|
|
6425
|
+
* Returns `true` if the element has an accessible name.
|
|
6218
6426
|
*
|
|
6219
|
-
*
|
|
6427
|
+
* It does not yet consider if the elements role prohibits naming, e.g. a `<p>`
|
|
6428
|
+
* element will still show up as having an accessible name.
|
|
6429
|
+
*
|
|
6430
|
+
* @public
|
|
6431
|
+
* @param document - Document element.
|
|
6432
|
+
* @param current - The element to get accessible name for
|
|
6433
|
+
* @returns `true` if the element has an accessible name.
|
|
6220
6434
|
*/
|
|
6221
|
-
function
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
}
|
|
6232
|
-
/* sanity check: break if no parent is present, normally not an issue as the
|
|
6233
|
-
* root element should be found first */
|
|
6234
|
-
if (!cur.parent) {
|
|
6235
|
-
break;
|
|
6236
|
-
}
|
|
6237
|
-
/* check parents */
|
|
6238
|
-
cur = cur.parent;
|
|
6239
|
-
} while (!cur.isRootElement());
|
|
6240
|
-
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
6435
|
+
function hasAccessibleName(document, current) {
|
|
6436
|
+
/* istanbul ignore next: we're not testing cache */
|
|
6437
|
+
if (current.cacheExists(HAS_ACCESSIBLE_TEXT_CACHE)) {
|
|
6438
|
+
return Boolean(current.cacheGet(HAS_ACCESSIBLE_TEXT_CACHE));
|
|
6439
|
+
}
|
|
6440
|
+
const result = hasAccessibleNameImpl(current, {
|
|
6441
|
+
document,
|
|
6442
|
+
reference: null,
|
|
6443
|
+
});
|
|
6444
|
+
return current.cacheSet(HAS_ACCESSIBLE_TEXT_CACHE, result);
|
|
6241
6445
|
}
|
|
6242
6446
|
|
|
6447
|
+
function isIgnored(node) {
|
|
6448
|
+
var _a;
|
|
6449
|
+
if (node.is("input")) {
|
|
6450
|
+
const type = (_a = node.getAttributeValue("type")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
6451
|
+
const ignored = ["hidden", "submit", "reset", "button"];
|
|
6452
|
+
return Boolean(type && ignored.includes(type));
|
|
6453
|
+
}
|
|
6454
|
+
return false;
|
|
6455
|
+
}
|
|
6243
6456
|
class InputMissingLabel extends Rule {
|
|
6244
6457
|
documentation() {
|
|
6245
6458
|
return {
|
|
6246
|
-
description:
|
|
6459
|
+
description: [
|
|
6460
|
+
"Each form element must have an a label or accessible name.",
|
|
6461
|
+
'Typically this is implemented using a `<label for="..">` element describing the purpose of the form element.',
|
|
6462
|
+
"",
|
|
6463
|
+
"This can be resolved in one of the following ways:",
|
|
6464
|
+
"",
|
|
6465
|
+
' - Use an associated `<label for="..">` element.',
|
|
6466
|
+
" - Use a nested `<label>` as parent element.",
|
|
6467
|
+
" - Use `aria-label` or `aria-labelledby` attributes.",
|
|
6468
|
+
].join("\n"),
|
|
6247
6469
|
url: ruleDocumentationUrl("@/rules/input-missing-label.ts"),
|
|
6248
6470
|
};
|
|
6249
6471
|
}
|
|
@@ -6256,38 +6478,48 @@ class InputMissingLabel extends Rule {
|
|
|
6256
6478
|
});
|
|
6257
6479
|
}
|
|
6258
6480
|
validateInput(root, elem) {
|
|
6259
|
-
var _a;
|
|
6260
6481
|
if (isHTMLHidden(elem) || isAriaHidden(elem)) {
|
|
6261
6482
|
return;
|
|
6262
6483
|
}
|
|
6263
6484
|
/* hidden, submit, reset or button should not have label */
|
|
6264
|
-
if (elem
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
}
|
|
6485
|
+
if (isIgnored(elem)) {
|
|
6486
|
+
return;
|
|
6487
|
+
}
|
|
6488
|
+
if (hasAccessibleName(root, elem)) {
|
|
6489
|
+
return;
|
|
6270
6490
|
}
|
|
6271
6491
|
let label = [];
|
|
6272
6492
|
/* try to find label by id */
|
|
6273
6493
|
if ((label = findLabelById(root, elem.id)).length > 0) {
|
|
6274
|
-
this.validateLabel(elem, label);
|
|
6494
|
+
this.validateLabel(root, elem, label);
|
|
6275
6495
|
return;
|
|
6276
6496
|
}
|
|
6277
6497
|
/* try to find parent label (input nested in label) */
|
|
6278
6498
|
if ((label = findLabelByParent(elem)).length > 0) {
|
|
6279
|
-
this.validateLabel(elem, label);
|
|
6499
|
+
this.validateLabel(root, elem, label);
|
|
6280
6500
|
return;
|
|
6281
6501
|
}
|
|
6282
|
-
|
|
6502
|
+
if (elem.hasAttribute("aria-label")) {
|
|
6503
|
+
this.report(elem, `<${elem.tagName}> element has aria-label but label has no text`);
|
|
6504
|
+
}
|
|
6505
|
+
else if (elem.hasAttribute("aria-labelledby")) {
|
|
6506
|
+
this.report(elem, `<${elem.tagName}> element has aria-labelledby but referenced element has no text`);
|
|
6507
|
+
}
|
|
6508
|
+
else {
|
|
6509
|
+
this.report(elem, `<${elem.tagName}> element does not have a <label>`);
|
|
6510
|
+
}
|
|
6283
6511
|
}
|
|
6284
6512
|
/**
|
|
6285
6513
|
* Reports error if none of the labels are accessible.
|
|
6286
6514
|
*/
|
|
6287
|
-
validateLabel(elem, labels) {
|
|
6515
|
+
validateLabel(root, elem, labels) {
|
|
6288
6516
|
const visible = labels.filter(isVisible);
|
|
6289
6517
|
if (visible.length === 0) {
|
|
6290
|
-
this.report(elem, `<${elem.tagName}> element has label but <label> element is hidden`);
|
|
6518
|
+
this.report(elem, `<${elem.tagName}> element has <label> but <label> element is hidden`);
|
|
6519
|
+
return;
|
|
6520
|
+
}
|
|
6521
|
+
if (!labels.some((label) => hasAccessibleName(root, label))) {
|
|
6522
|
+
this.report(elem, `<${elem.tagName}> element has <label> but <label> has no text`);
|
|
6291
6523
|
}
|
|
6292
6524
|
}
|
|
6293
6525
|
}
|
|
@@ -13095,5 +13327,5 @@ function getFormatter(name) {
|
|
|
13095
13327
|
return (_a = availableFormatters[name]) !== null && _a !== void 0 ? _a : null;
|
|
13096
13328
|
}
|
|
13097
13329
|
|
|
13098
|
-
export { Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g,
|
|
13330
|
+
export { Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g, classifyNodeText as h, TextClassification as i, Reporter as j, TemplateExtractor as k, getFormatter as l, legacyRequire as m, ensureError as n, configDataFromFile as o, presets as p, compatibilityCheck as q, ruleExists as r, codeframe as s, name as t, bugs as u, version as v };
|
|
13099
13331
|
//# sourceMappingURL=core.js.map
|