-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathlink-has-valid-href-or-role.js
More file actions
76 lines (64 loc) · 2.57 KB
/
Copy pathlink-has-valid-href-or-role.js
File metadata and controls
76 lines (64 loc) · 2.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* Check if a link is improperly used (missing href or href="#", and not a button).
*
* @param {Node} node The node to evaluate.
* @return {boolean} True if the link is valid or semantically correct, false otherwise.
*/
export default {
id: 'link_has_valid_href_or_role',
evaluate: ( node ) => {
if ( node.nodeName.toLowerCase() !== 'a' ) {
return true;
}
const href = node.getAttribute( 'href' );
const role = node.getAttribute( 'role' ) || '';
// Parse roles once for efficiency (DRY principle)
const roles = role.toLowerCase().split( /\s+/ );
// Allow roles of button or tab
if ( roles.some( ( r ) => [ 'button', 'tab' ].includes( r ) ) ) {
return true;
}
// Allow role="menuitem" with aria-expanded (for expandable menu items)
// See: https://wordpress.org/support/topic/improper-use-of-link-5/
const hasAriaExpanded = node.hasAttribute( 'aria-expanded' );
if ( roles.includes( 'menuitem' ) && hasAriaExpanded ) {
return true;
}
// Allow named anchors used as jump targets: no href, has id/name, no visible content,
// and not keyboard-focusable.
// Per the HTML spec, <a> without href has role="generic" and is not keyboard focusable,
// so it does not create a false link for AT or keyboard users.
// Use hasAttribute instead of !href to exclude href="" which is keyboard focusable.
// Check children.length to exclude anchors wrapping images or other elements.
const id = node.getAttribute( 'id' ) || '';
const name = node.getAttribute( 'name' ) || '';
const hasAnchorTargetName = id.trim() !== '' || name.trim() !== '';
const tabindex = node.getAttribute( 'tabindex' );
const parsedTabindex = null === tabindex ? null : Number.parseInt( tabindex, 10 );
const isKeyboardFocusable =
null !== tabindex && ( Number.isNaN( parsedTabindex ) || parsedTabindex >= 0 );
if ( ! node.hasAttribute( 'href' ) && hasAnchorTargetName && ! isKeyboardFocusable && node.children.length === 0 && node.textContent.trim() === '' ) {
return true;
}
const trimmedHref = href ? href.trim() : '';
const normalizedHref = trimmedHref.toLowerCase();
// Fail if href is missing, just '#', or contains invalid protocols
if ( ! trimmedHref ||
trimmedHref === '#' ||
normalizedHref.startsWith( 'javascript:' ) ||
normalizedHref.startsWith( 'data:' ) ||
normalizedHref.startsWith( 'file:' )
) {
return false;
}
// Optionally validate URL format if it's an absolute URL
if ( trimmedHref.includes( '://' ) ) {
try {
new URL( trimmedHref );
} catch ( e ) {
return false; // Invalid URL formats
}
}
return true;
},
};