@a11yfred/neighbor 0.3.0 → 1.0.3

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.
@@ -1,146 +1,146 @@
1
- /**
2
- * neighbor/lib/helpers-angular.js
3
- * Helpers for Angular template AST via @angular-eslint/template-parser.
4
- *
5
- * Parser: @angular-eslint/eslint-plugin-template provides its own rule format.
6
- * Node type for elements: 'Element$1' (from @angular-eslint/template-parser, the class is
7
- * TmplAstElement). The node has:
8
- * node.nametag name string
9
- * node.attributesTmplAstTextAttribute[] (static attrs)
10
- * node.inputsTmplAstBoundAttribute[] (property bindings)
11
- * node.childrenTmplAstNode[]
12
- *
13
- * We only inspect static (non-bound) attributes for ARIA checks.
14
- * Bound attributes ([aria-label]="expr") cannot be statically analyzed.
15
- *
16
- * Angular ESLint rule visitor uses standard ESLint `create(context)` but the
17
- * parser emits different node types. The visitor key from @angular-eslint is
18
- * 'Element$1' (the internal class name exposed via the parser).
19
- */
20
-
21
- import {
22
- INTERACTIVE_ELEMENTS,
23
- INTERACTIVE_ROLES,
24
- } from './helpers.js'
25
-
26
- /** Find a static TmplAstTextAttribute by name. */
27
- export function getAttr(tmplElement, name) {
28
- return (tmplElement.attributes ?? []).find(a => a.name === name) ?? null
29
- }
30
-
31
- /** TmplAstTextAttribute.value is a plain string. */
32
- export function getAttrStringValue(attr) {
33
- if (!attr) return null
34
- return typeof attr.value === 'string' ? attr.value || null : null
35
- }
36
-
37
- export function getElementName(tmplElement) {
38
- const raw = tmplElement.name ?? ''
39
- if (!raw) return null
40
- // Angular custom component selectors are typically kebab-case (app-button),
41
- // but guard against PascalCase as well to avoid native element name collisions.
42
- if (raw[0] !== raw[0].toLowerCase()) return null
43
- return raw.toLowerCase()
44
- }
45
-
46
- export function hasAttr(tmplElement, name) {
47
- return getAttr(tmplElement, name) !== null
48
- }
49
-
50
- export function getRoleValue(tmplElement) {
51
- return getAttrStringValue(getAttr(tmplElement, 'role'))
52
- }
53
-
54
- export function hasAccessibleName(tmplElement) {
55
- return hasAttr(tmplElement, 'aria-label') || hasAttr(tmplElement, 'aria-labelledby')
56
- }
57
-
58
- export function isInteractiveElement(tmplElement) {
59
- const el = getElementName(tmplElement)
60
- if (el && INTERACTIVE_ELEMENTS.has(el)) return true
61
- const role = getRoleValue(tmplElement)
62
- return !!(role && INTERACTIVE_ROLES.has(role))
63
- }
64
-
65
- export function hasOnlyHiddenChildren(tmplElement) {
66
- const children = tmplElement.children ?? []
67
- if (children.length === 0) return false
68
- return children.every(child => {
69
- if (child.constructor?.name === 'TmplAstText') return (child.value ?? '').trim() === ''
70
- if (child.constructor?.name === 'TmplAstElement') {
71
- return (child.attributes ?? []).some(a => a.name === 'aria-hidden')
72
- }
73
- return false
74
- })
75
- }
76
-
77
- const NEW_TAB_PATTERN = /new.tab|new.window|opens in/i
78
-
79
- function collectAngularText(node) {
80
- if (!node) return ''
81
- if (node.constructor?.name === 'TmplAstText') return node.value ?? ''
82
- if (node.constructor?.name === 'TmplAstElement') return (node.children ?? []).map(collectAngularText).join('')
83
- return ''
84
- }
85
-
86
- export function hasNewTabWarning(tmplElement) {
87
- const labelVal = (getAttrStringValue(getAttr(tmplElement, 'aria-label')) ?? '').toLowerCase()
88
- if (NEW_TAB_PATTERN.test(labelVal)) return true
89
- const childText = (tmplElement.children ?? []).map(collectAngularText).join('')
90
- return NEW_TAB_PATTERN.test(childText)
91
- }
92
-
93
- export function getParent(tmplElement) {
94
- // @angular-eslint/template-parser does not set parent on AST nodes
95
- // ancestor walking is not available without tracking the stack ourselves.
96
- // Rules needing ancestor traversal are skipped for Angular.
97
- return null
98
- }
99
-
100
- export function* getAncestors(_tmplElement) { /* not available */ }
101
-
102
- export function* getChildOpeningElements(tmplElement) {
103
- for (const child of tmplElement.children ?? []) {
104
- if (child.constructor?.name === 'TmplAstElement') yield child
105
- }
106
- }
107
-
108
- export function getClassName(tmplElement) {
109
- return getAttrStringValue(getAttr(tmplElement, 'class'))
110
- }
111
-
112
- /**
113
- * Returns the [innerHTML] bound input node if present, else null.
114
- * In @angular-eslint/template-parser, property bindings live on node.inputs
115
- * as TmplAstBoundAttribute nodes with { name: 'innerHTML' }.
116
- */
117
- export function getInnerHtmlAttr(tmplElement) {
118
- return (tmplElement.inputs ?? []).find(i => i.name === 'innerHTML') ?? null
119
- }
120
-
121
- export function getInnerHtmlAttrName(_tmplElement) {
122
- return '[innerHTML]'
123
- }
124
-
125
- export const h = {
126
- getAttr,
127
- getAttrStringValue,
128
- getElementName,
129
- hasAttr,
130
- getRoleValue,
131
- hasAccessibleName,
132
- isInteractiveElement,
133
- hasOnlyHiddenChildren,
134
- hasNewTabWarning,
135
- getParent,
136
- getAncestors,
137
- getChildOpeningElements,
138
- getClassName,
139
- getInnerHtmlAttr,
140
- getInnerHtmlAttrName,
141
- elementVisitor: 'Element$1',
142
- elementWithChildrenVisitor: 'Element$1',
143
- getOpeningElement: (node) => node,
144
- getChildOpeningElementsFromWrapper: (node) =>
145
- (node.children ?? []).filter(c => c.constructor?.name === 'TmplAstElement'),
146
- }
1
+ /**
2
+ * neighbor/lib/helpers-angular.js
3
+ * Helpers for Angular template AST via @angular-eslint/template-parser.
4
+ *
5
+ * Parser: @angular-eslint/eslint-plugin-template provides its own rule format.
6
+ * Node type for elements: 'Element$1' (from @angular-eslint/template-parser, the class is
7
+ * TmplAstElement). The node has:
8
+ * node.name - tag name string
9
+ * node.attributes - TmplAstTextAttribute[] (static attrs)
10
+ * node.inputs - TmplAstBoundAttribute[] (property bindings)
11
+ * node.children - TmplAstNode[]
12
+ *
13
+ * We only inspect static (non-bound) attributes for ARIA checks.
14
+ * Bound attributes ([aria-label]="expr") cannot be statically analyzed.
15
+ *
16
+ * Angular ESLint rule visitor uses standard ESLint `create(context)` but the
17
+ * parser emits different node types. The visitor key from @angular-eslint is
18
+ * 'Element$1' (the internal class name exposed via the parser).
19
+ */
20
+
21
+ import {
22
+ INTERACTIVE_ELEMENTS,
23
+ INTERACTIVE_ROLES,
24
+ } from './helpers.js'
25
+
26
+ /** Find a static TmplAstTextAttribute by name. */
27
+ export function getAttr(tmplElement, name) {
28
+ return (tmplElement.attributes ?? []).find(a => a.name === name) ?? null
29
+ }
30
+
31
+ /** TmplAstTextAttribute.value is a plain string. */
32
+ export function getAttrStringValue(attr) {
33
+ if (!attr) return null
34
+ return typeof attr.value === 'string' ? attr.value || null : null
35
+ }
36
+
37
+ export function getElementName(tmplElement) {
38
+ const raw = tmplElement.name ?? ''
39
+ if (!raw) return null
40
+ // Angular custom component selectors are typically kebab-case (app-button),
41
+ // but guard against PascalCase as well to avoid native element name collisions.
42
+ if (raw[0] !== raw[0].toLowerCase()) return null
43
+ return raw.toLowerCase()
44
+ }
45
+
46
+ export function hasAttr(tmplElement, name) {
47
+ return getAttr(tmplElement, name) !== null
48
+ }
49
+
50
+ export function getRoleValue(tmplElement) {
51
+ return getAttrStringValue(getAttr(tmplElement, 'role'))
52
+ }
53
+
54
+ export function hasAccessibleName(tmplElement) {
55
+ return hasAttr(tmplElement, 'aria-label') || hasAttr(tmplElement, 'aria-labelledby')
56
+ }
57
+
58
+ export function isInteractiveElement(tmplElement) {
59
+ const el = getElementName(tmplElement)
60
+ if (el && INTERACTIVE_ELEMENTS.has(el)) return true
61
+ const role = getRoleValue(tmplElement)
62
+ return !!(role && INTERACTIVE_ROLES.has(role))
63
+ }
64
+
65
+ export function hasOnlyHiddenChildren(tmplElement) {
66
+ const children = tmplElement.children ?? []
67
+ if (children.length === 0) return false
68
+ return children.every(child => {
69
+ if (child.constructor?.name === 'TmplAstText') return (child.value ?? '').trim() === ''
70
+ if (child.constructor?.name === 'TmplAstElement') {
71
+ return (child.attributes ?? []).some(a => a.name === 'aria-hidden')
72
+ }
73
+ return false
74
+ })
75
+ }
76
+
77
+ const NEW_TAB_PATTERN = /new.tab|new.window|opens in/i
78
+
79
+ function collectAngularText(node) {
80
+ if (!node) return ''
81
+ if (node.constructor?.name === 'TmplAstText') return node.value ?? ''
82
+ if (node.constructor?.name === 'TmplAstElement') return (node.children ?? []).map(collectAngularText).join('')
83
+ return ''
84
+ }
85
+
86
+ export function hasNewTabWarning(tmplElement) {
87
+ const labelVal = (getAttrStringValue(getAttr(tmplElement, 'aria-label')) ?? '').toLowerCase()
88
+ if (NEW_TAB_PATTERN.test(labelVal)) return true
89
+ const childText = (tmplElement.children ?? []).map(collectAngularText).join('')
90
+ return NEW_TAB_PATTERN.test(childText)
91
+ }
92
+
93
+ export function getParent(tmplElement) {
94
+ // @angular-eslint/template-parser does not set parent on AST nodes -
95
+ // ancestor walking is not available without tracking the stack ourselves.
96
+ // Rules needing ancestor traversal are skipped for Angular.
97
+ return null
98
+ }
99
+
100
+ export function* getAncestors(_tmplElement) { /* not available */ }
101
+
102
+ export function* getChildOpeningElements(tmplElement) {
103
+ for (const child of tmplElement.children ?? []) {
104
+ if (child.constructor?.name === 'TmplAstElement') yield child
105
+ }
106
+ }
107
+
108
+ export function getClassName(tmplElement) {
109
+ return getAttrStringValue(getAttr(tmplElement, 'class'))
110
+ }
111
+
112
+ /**
113
+ * Returns the [innerHTML] bound input node if present, else null.
114
+ * In @angular-eslint/template-parser, property bindings live on node.inputs
115
+ * as TmplAstBoundAttribute nodes with { name: 'innerHTML' }.
116
+ */
117
+ export function getInnerHtmlAttr(tmplElement) {
118
+ return (tmplElement.inputs ?? []).find(i => i.name === 'innerHTML') ?? null
119
+ }
120
+
121
+ export function getInnerHtmlAttrName(_tmplElement) {
122
+ return '[innerHTML]'
123
+ }
124
+
125
+ export const h = {
126
+ getAttr,
127
+ getAttrStringValue,
128
+ getElementName,
129
+ hasAttr,
130
+ getRoleValue,
131
+ hasAccessibleName,
132
+ isInteractiveElement,
133
+ hasOnlyHiddenChildren,
134
+ hasNewTabWarning,
135
+ getParent,
136
+ getAncestors,
137
+ getChildOpeningElements,
138
+ getClassName,
139
+ getInnerHtmlAttr,
140
+ getInnerHtmlAttrName,
141
+ elementVisitor: 'Element$1',
142
+ elementWithChildrenVisitor: 'Element$1',
143
+ getOpeningElement: (node) => node,
144
+ getChildOpeningElementsFromWrapper: (node) =>
145
+ (node.children ?? []).filter(c => c.constructor?.name === 'TmplAstElement'),
146
+ }