@a11yfred/neighbor 0.2.0 → 1.0.1

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,151 +1,151 @@
1
- /**
2
- * neighbor/lib/helpers-vue.js
3
- * Helpers for Vue SFC AST via vue-eslint-parser.
4
- * Nodes: VElement (has startTag.attributes), VAttribute, VLiteral, VExpressionContainer
5
- *
6
- * vue-eslint-parser docs: https://github.com/vuejs/vue-eslint-parser/blob/master/docs/ast.md
7
- * Rule visitor: 'VElement' (fired for every HTML element in the template)
8
- */
9
-
10
- import {
11
- INTERACTIVE_ELEMENTS,
12
- INTERACTIVE_ROLES,
13
- } from './helpers.js'
14
-
15
- /**
16
- * In vue-eslint-parser, an attribute is a VAttribute node.
17
- * Plain attribute: { type: 'VAttribute', key: { name: 'role' }, value: { type: 'VLiteral', value: '"dialog"' } }
18
- * v-bind shorthand: { type: 'VAttribute', directive: true, key: { argument: { name: 'role' } }, value: VExpressionContainer }
19
- * We only read static (non-directive) attributes for ARIA checks.
20
- */
21
- export function getAttr(vElement, name) {
22
- const attrs = vElement.startTag?.attributes ?? []
23
- return attrs.find(a => !a.directive && a.key?.name === name) ?? null
24
- }
25
-
26
- export function getAttrStringValue(attr) {
27
- if (!attr) return null
28
- // VLiteral.value includes surrounding quotesstrip them
29
- const raw = attr.value?.value
30
- if (typeof raw === 'string') return raw.replace(/^["']|["']$/g, '')
31
- return null
32
- }
33
-
34
- export function getElementName(vElement) {
35
- const raw = vElement.rawName ?? vElement.name ?? ''
36
- if (!raw) return null
37
- // Vue custom components can be PascalCase (MyButton) or kebab-case (my-button).
38
- // PascalCase names would lowercase to a native element name collision (e.g. Button → button).
39
- // Return null for PascalCase so rules don't treat custom components as native elements.
40
- if (raw[0] !== raw[0].toLowerCase()) return null
41
- return raw.toLowerCase()
42
- }
43
-
44
- export function hasAttr(vElement, name) {
45
- return getAttr(vElement, name) !== null
46
- }
47
-
48
- export function getRoleValue(vElement) {
49
- return getAttrStringValue(getAttr(vElement, 'role'))
50
- }
51
-
52
- export function hasAccessibleName(vElement) {
53
- return hasAttr(vElement, 'aria-label') || hasAttr(vElement, 'aria-labelledby')
54
- }
55
-
56
- export function isInteractiveElement(vElement) {
57
- const el = getElementName(vElement)
58
- if (el && INTERACTIVE_ELEMENTS.has(el)) return true
59
- const role = getRoleValue(vElement)
60
- return !!(role && INTERACTIVE_ROLES.has(role))
61
- }
62
-
63
- /** Returns true if every child element is aria-hidden or the element has no visible text children. */
64
- export function hasOnlyHiddenChildren(vElement) {
65
- const children = vElement.children ?? []
66
- if (children.length === 0) return false
67
- return children.every(child => {
68
- if (child.type === 'VText') return child.value.trim() === ''
69
- if (child.type === 'VElement') {
70
- const attrs = child.startTag?.attributes ?? []
71
- return attrs.some(a => !a.directive && a.key?.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 collectVueText(node) {
80
- if (!node) return ''
81
- if (node.type === 'VText') return node.value ?? ''
82
- if (node.type === 'VElement') return (node.children ?? []).map(collectVueText).join('')
83
- return ''
84
- }
85
-
86
- export function hasNewTabWarning(vElement) {
87
- const labelVal = (getAttrStringValue(getAttr(vElement, 'aria-label')) ?? '').toLowerCase()
88
- if (NEW_TAB_PATTERN.test(labelVal)) return true
89
- const childText = (vElement.children ?? []).map(collectVueText).join('')
90
- return NEW_TAB_PATTERN.test(childText)
91
- }
92
-
93
- /** Parent VElement, or null. */
94
- export function getParent(vElement) {
95
- const p = vElement.parent
96
- return p?.type === 'VElement' ? p : null
97
- }
98
-
99
- export function* getAncestors(vElement) {
100
- let cur = getParent(vElement)
101
- while (cur) {
102
- yield cur
103
- cur = getParent(cur)
104
- }
105
- }
106
-
107
- export function* getChildOpeningElements(vElement) {
108
- for (const child of vElement.children ?? []) {
109
- if (child.type === 'VElement') yield child
110
- }
111
- }
112
-
113
- export function getClassName(vElement) {
114
- return getAttrStringValue(getAttr(vElement, 'class'))
115
- }
116
-
117
- /**
118
- * Returns the v-html directive attribute node if present, else null.
119
- * v-html is a directive (a.directive === true, key.name === 'html'),
120
- * not a plain static attributeso we search differently.
121
- */
122
- export function getInnerHtmlAttr(vElement) {
123
- const attrs = vElement.startTag?.attributes ?? []
124
- return attrs.find(a => a.directive && a.key?.name === 'html') ?? null
125
- }
126
-
127
- export function getInnerHtmlAttrName(_vElement) {
128
- return 'v-html'
129
- }
130
-
131
- export const h = {
132
- getAttr,
133
- getAttrStringValue,
134
- getElementName,
135
- hasAttr,
136
- getRoleValue,
137
- hasAccessibleName,
138
- isInteractiveElement,
139
- hasOnlyHiddenChildren,
140
- hasNewTabWarning,
141
- getParent,
142
- getAncestors,
143
- getChildOpeningElements,
144
- getClassName,
145
- getInnerHtmlAttr,
146
- getInnerHtmlAttrName,
147
- elementVisitor: 'VElement',
148
- elementWithChildrenVisitor: 'VElement',
149
- getOpeningElement: (node) => node,
150
- getChildOpeningElementsFromWrapper: (node) => (node.children ?? []).filter(c => c.type === 'VElement'),
151
- }
1
+ /**
2
+ * neighbor/lib/helpers-vue.js
3
+ * Helpers for Vue SFC AST via vue-eslint-parser.
4
+ * Nodes: VElement (has startTag.attributes), VAttribute, VLiteral, VExpressionContainer
5
+ *
6
+ * vue-eslint-parser docs: https://github.com/vuejs/vue-eslint-parser/blob/master/docs/ast.md
7
+ * Rule visitor: 'VElement' (fired for every HTML element in the template)
8
+ */
9
+
10
+ import {
11
+ INTERACTIVE_ELEMENTS,
12
+ INTERACTIVE_ROLES,
13
+ } from './helpers.js'
14
+
15
+ /**
16
+ * In vue-eslint-parser, an attribute is a VAttribute node.
17
+ * Plain attribute: { type: 'VAttribute', key: { name: 'role' }, value: { type: 'VLiteral', value: '"dialog"' } }
18
+ * v-bind shorthand: { type: 'VAttribute', directive: true, key: { argument: { name: 'role' } }, value: VExpressionContainer }
19
+ * We only read static (non-directive) attributes for ARIA checks.
20
+ */
21
+ export function getAttr(vElement, name) {
22
+ const attrs = vElement.startTag?.attributes ?? []
23
+ return attrs.find(a => !a.directive && a.key?.name === name) ?? null
24
+ }
25
+
26
+ export function getAttrStringValue(attr) {
27
+ if (!attr) return null
28
+ // VLiteral.value includes surrounding quotes - strip them
29
+ const raw = attr.value?.value
30
+ if (typeof raw === 'string') return raw.replace(/^["']|["']$/g, '')
31
+ return null
32
+ }
33
+
34
+ export function getElementName(vElement) {
35
+ const raw = vElement.rawName ?? vElement.name ?? ''
36
+ if (!raw) return null
37
+ // Vue custom components can be PascalCase (MyButton) or kebab-case (my-button).
38
+ // PascalCase names would lowercase to a native element name collision (e.g. Button → button).
39
+ // Return null for PascalCase so rules don't treat custom components as native elements.
40
+ if (raw[0] !== raw[0].toLowerCase()) return null
41
+ return raw.toLowerCase()
42
+ }
43
+
44
+ export function hasAttr(vElement, name) {
45
+ return getAttr(vElement, name) !== null
46
+ }
47
+
48
+ export function getRoleValue(vElement) {
49
+ return getAttrStringValue(getAttr(vElement, 'role'))
50
+ }
51
+
52
+ export function hasAccessibleName(vElement) {
53
+ return hasAttr(vElement, 'aria-label') || hasAttr(vElement, 'aria-labelledby')
54
+ }
55
+
56
+ export function isInteractiveElement(vElement) {
57
+ const el = getElementName(vElement)
58
+ if (el && INTERACTIVE_ELEMENTS.has(el)) return true
59
+ const role = getRoleValue(vElement)
60
+ return !!(role && INTERACTIVE_ROLES.has(role))
61
+ }
62
+
63
+ /** Returns true if every child element is aria-hidden or the element has no visible text children. */
64
+ export function hasOnlyHiddenChildren(vElement) {
65
+ const children = vElement.children ?? []
66
+ if (children.length === 0) return false
67
+ return children.every(child => {
68
+ if (child.type === 'VText') return child.value.trim() === ''
69
+ if (child.type === 'VElement') {
70
+ const attrs = child.startTag?.attributes ?? []
71
+ return attrs.some(a => !a.directive && a.key?.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 collectVueText(node) {
80
+ if (!node) return ''
81
+ if (node.type === 'VText') return node.value ?? ''
82
+ if (node.type === 'VElement') return (node.children ?? []).map(collectVueText).join('')
83
+ return ''
84
+ }
85
+
86
+ export function hasNewTabWarning(vElement) {
87
+ const labelVal = (getAttrStringValue(getAttr(vElement, 'aria-label')) ?? '').toLowerCase()
88
+ if (NEW_TAB_PATTERN.test(labelVal)) return true
89
+ const childText = (vElement.children ?? []).map(collectVueText).join('')
90
+ return NEW_TAB_PATTERN.test(childText)
91
+ }
92
+
93
+ /** Parent VElement, or null. */
94
+ export function getParent(vElement) {
95
+ const p = vElement.parent
96
+ return p?.type === 'VElement' ? p : null
97
+ }
98
+
99
+ export function* getAncestors(vElement) {
100
+ let cur = getParent(vElement)
101
+ while (cur) {
102
+ yield cur
103
+ cur = getParent(cur)
104
+ }
105
+ }
106
+
107
+ export function* getChildOpeningElements(vElement) {
108
+ for (const child of vElement.children ?? []) {
109
+ if (child.type === 'VElement') yield child
110
+ }
111
+ }
112
+
113
+ export function getClassName(vElement) {
114
+ return getAttrStringValue(getAttr(vElement, 'class'))
115
+ }
116
+
117
+ /**
118
+ * Returns the v-html directive attribute node if present, else null.
119
+ * v-html is a directive (a.directive === true, key.name === 'html'),
120
+ * not a plain static attribute - so we search differently.
121
+ */
122
+ export function getInnerHtmlAttr(vElement) {
123
+ const attrs = vElement.startTag?.attributes ?? []
124
+ return attrs.find(a => a.directive && a.key?.name === 'html') ?? null
125
+ }
126
+
127
+ export function getInnerHtmlAttrName(_vElement) {
128
+ return 'v-html'
129
+ }
130
+
131
+ export const h = {
132
+ getAttr,
133
+ getAttrStringValue,
134
+ getElementName,
135
+ hasAttr,
136
+ getRoleValue,
137
+ hasAccessibleName,
138
+ isInteractiveElement,
139
+ hasOnlyHiddenChildren,
140
+ hasNewTabWarning,
141
+ getParent,
142
+ getAncestors,
143
+ getChildOpeningElements,
144
+ getClassName,
145
+ getInnerHtmlAttr,
146
+ getInnerHtmlAttrName,
147
+ elementVisitor: 'VElement',
148
+ elementWithChildrenVisitor: 'VElement',
149
+ getOpeningElement: (node) => node,
150
+ getChildOpeningElementsFromWrapper: (node) => (node.children ?? []).filter(c => c.type === 'VElement'),
151
+ }
package/lib/helpers.js CHANGED
@@ -1,37 +1,37 @@
1
- /**
2
- * neighbor/lib/helpers.js
3
- * Shared constants and pure datano AST dependency.
4
- */
5
-
6
- export const INTERACTIVE_ELEMENTS = new Set([
7
- 'a', 'button', 'input', 'select', 'textarea', 'details', 'summary',
8
- ])
9
-
10
- export const INTERACTIVE_ROLES = new Set([
11
- 'button', 'link', 'checkbox', 'radio', 'textbox', 'combobox', 'listbox',
12
- 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'tab', 'switch',
13
- 'slider', 'spinbutton', 'searchbox', 'gridcell', 'treeitem',
14
- ])
15
-
16
- // Truly generic containersnot landmarks, not interactive, not sectioning
17
- export const GENERIC_CONTAINERS = new Set(['div', 'span', 'p'])
18
-
19
- // Void/leaf elements that cannot have children (aria-owns on these is wrong)
20
- export const VOID_ELEMENTS = new Set([
21
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
22
- 'meta', 'param', 'source', 'track', 'wbr',
23
- ])
24
-
25
- export const HEADING_ELEMENTS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
26
-
27
- export const NAV_MENU_ROLES = new Set([
28
- 'menu', 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio',
29
- ])
30
-
31
- export const ROLES_REQUIRING_NAME = new Set([
32
- 'region', 'dialog', 'alertdialog', 'application', 'marquee', 'searchbox',
33
- ])
34
-
35
- export const FORM_ELEMENTS = new Set(['input', 'select', 'textarea'])
36
-
37
- export const CAROUSEL_CLASS_PATTERN = /carousel|slider|slideshow|rotator/i
1
+ /**
2
+ * neighbor/lib/helpers.js
3
+ * Shared constants and pure data - no AST dependency.
4
+ */
5
+
6
+ export const INTERACTIVE_ELEMENTS = new Set([
7
+ 'a', 'button', 'input', 'select', 'textarea', 'details', 'summary',
8
+ ])
9
+
10
+ export const INTERACTIVE_ROLES = new Set([
11
+ 'button', 'link', 'checkbox', 'radio', 'textbox', 'combobox', 'listbox',
12
+ 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'tab', 'switch',
13
+ 'slider', 'spinbutton', 'searchbox', 'gridcell', 'treeitem',
14
+ ])
15
+
16
+ // Truly generic containers - not landmarks, not interactive, not sectioning
17
+ export const GENERIC_CONTAINERS = new Set(['div', 'span', 'p'])
18
+
19
+ // Void/leaf elements that cannot have children (aria-owns on these is wrong)
20
+ export const VOID_ELEMENTS = new Set([
21
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
22
+ 'meta', 'param', 'source', 'track', 'wbr',
23
+ ])
24
+
25
+ export const HEADING_ELEMENTS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
26
+
27
+ export const NAV_MENU_ROLES = new Set([
28
+ 'menu', 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio',
29
+ ])
30
+
31
+ export const ROLES_REQUIRING_NAME = new Set([
32
+ 'region', 'dialog', 'alertdialog', 'application', 'marquee', 'searchbox',
33
+ ])
34
+
35
+ export const FORM_ELEMENTS = new Set(['input', 'select', 'textarea'])
36
+
37
+ export const CAROUSEL_CLASS_PATTERN = /carousel|slider|slideshow|rotator/i