Skip to content

Commit 626f847

Browse files
authored
refactor: export label helpers to its own file (#790)
1 parent a386d87 commit 626f847

File tree

4 files changed

+136
-101
lines changed

4 files changed

+136
-101
lines changed

src/__tests__/suggestions.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,44 @@ test('should suggest hidden option if element is not in the accessibilty tree',
578578
]
579579
`)
580580
})
581+
582+
test('should find label text using the aria-labelledby', () => {
583+
const {container} = renderIntoDocument(`
584+
<div>
585+
<div>
586+
<input id="sixth-label-one" value="6th one"/>
587+
<input id="sixth-label-two" value="6th two"/>
588+
<label id="sixth-label-three">6th three</label>
589+
<input aria-labelledby="sixth-label-one sixth-label-two sixth-label-three" id="sixth-id" />
590+
</div>
591+
</div>
592+
`)
593+
594+
expect(
595+
getSuggestedQuery(
596+
container.querySelector('[id="sixth-id"]'),
597+
'get',
598+
'labelText',
599+
),
600+
).toMatchInlineSnapshot(
601+
{
602+
queryArgs: [/6th one 6th two 6th three/i],
603+
queryMethod: 'getByLabelText',
604+
queryName: 'LabelText',
605+
variant: 'get',
606+
warning: '',
607+
},
608+
`
609+
Object {
610+
"queryArgs": Array [
611+
Object {},
612+
],
613+
"queryMethod": "getByLabelText",
614+
"queryName": "LabelText",
615+
"toString": [Function],
616+
"variant": "get",
617+
"warning": "",
618+
}
619+
`,
620+
)
621+
})

src/label-helpers.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {TEXT_NODE} from './helpers'
2+
3+
const labelledNodeNames = [
4+
'button',
5+
'meter',
6+
'output',
7+
'progress',
8+
'select',
9+
'textarea',
10+
'input',
11+
]
12+
13+
function getTextContent(node) {
14+
if (labelledNodeNames.includes(node.nodeName.toLowerCase())) {
15+
return ''
16+
}
17+
18+
if (node.nodeType === TEXT_NODE) return node.textContent
19+
20+
return Array.from(node.childNodes)
21+
.map(childNode => getTextContent(childNode))
22+
.join('')
23+
}
24+
25+
function getLabelContent(node) {
26+
let textContent
27+
if (node.tagName.toLowerCase() === 'label') {
28+
textContent = getTextContent(node)
29+
} else {
30+
textContent = node.value || node.textContent
31+
}
32+
return textContent
33+
}
34+
35+
// Based on https://github.com/eps1lon/dom-accessibility-api/pull/352
36+
function getRealLabels(element) {
37+
if (element.labels !== undefined) return element.labels
38+
39+
if (!isLabelable(element)) return []
40+
41+
const labels = element.ownerDocument.querySelectorAll('label')
42+
return Array.from(labels).filter(label => label.control === element)
43+
}
44+
45+
function isLabelable(element) {
46+
return (
47+
element.tagName.match(/BUTTON|METER|OUTPUT|PROGRESS|SELECT|TEXTAREA/) ||
48+
(element.tagName === 'INPUT' && element.getAttribute('type') !== 'hidden')
49+
)
50+
}
51+
52+
function getLabels(container, element, {selector = '*'} = {}) {
53+
const labelsId = element.getAttribute('aria-labelledby')
54+
? element.getAttribute('aria-labelledby').split(' ')
55+
: []
56+
return labelsId.length
57+
? labelsId.map(labelId => {
58+
const labellingElement = container.querySelector(`[id="${labelId}"]`)
59+
return labellingElement
60+
? {content: getLabelContent(labellingElement), formControl: null}
61+
: {content: '', formControl: null}
62+
})
63+
: Array.from(getRealLabels(element)).map(label => {
64+
const textToMatch = getLabelContent(label)
65+
const formControlSelector =
66+
'button, input, meter, output, progress, select, textarea'
67+
const labelledFormControl = Array.from(
68+
label.querySelectorAll(formControlSelector),
69+
).filter(formControlElement => formControlElement.matches(selector))[0]
70+
71+
return {content: textToMatch, formControl: labelledFormControl}
72+
})
73+
}
74+
75+
export {getLabels, getRealLabels, getLabelContent}

src/queries/label-text.js

Lines changed: 16 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {getConfig} from '../config'
2-
import {checkContainerType, TEXT_NODE} from '../helpers'
2+
import {checkContainerType} from '../helpers'
3+
import {getLabels, getRealLabels, getLabelContent} from '../label-helpers'
34
import {
45
fuzzyMatches,
56
matches,
@@ -11,16 +12,6 @@ import {
1112
wrapSingleQueryWithSuggestion,
1213
} from './all-utils'
1314

14-
const labelledNodeNames = [
15-
'button',
16-
'meter',
17-
'output',
18-
'progress',
19-
'select',
20-
'textarea',
21-
'input',
22-
]
23-
2415
function queryAllLabels(container) {
2516
return Array.from(container.querySelectorAll('label,input'))
2617
.map(node => {
@@ -46,28 +37,6 @@ function queryAllLabelsByText(
4637
.map(({node}) => node)
4738
}
4839

49-
function getTextContent(node) {
50-
if (labelledNodeNames.includes(node.nodeName.toLowerCase())) {
51-
return ''
52-
}
53-
54-
if (node.nodeType === TEXT_NODE) return node.textContent
55-
56-
return Array.from(node.childNodes)
57-
.map(childNode => getTextContent(childNode))
58-
.join('')
59-
}
60-
61-
function getLabelContent(node) {
62-
let textContent
63-
if (node.tagName.toLowerCase() === 'label') {
64-
textContent = getTextContent(node)
65-
} else {
66-
textContent = node.value || node.textContent
67-
}
68-
return textContent
69-
}
70-
7140
function queryAllByLabelText(
7241
container,
7342
text,
@@ -79,34 +48,21 @@ function queryAllByLabelText(
7948
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
8049
const matchingLabelledElements = Array.from(container.querySelectorAll('*'))
8150
.filter(element => {
82-
return getLabels(element) || element.hasAttribute('aria-labelledby')
51+
return (
52+
getRealLabels(element).length || element.hasAttribute('aria-labelledby')
53+
)
8354
})
8455
.reduce((labelledElements, labelledElement) => {
85-
const labelsId = labelledElement.getAttribute('aria-labelledby')
86-
? labelledElement.getAttribute('aria-labelledby').split(' ')
87-
: []
88-
let labelsValue = labelsId.length
89-
? labelsId.map(labelId => {
90-
const labellingElement = container.querySelector(
91-
`[id="${labelId}"]`,
92-
)
93-
return labellingElement ? getLabelContent(labellingElement) : ''
94-
})
95-
: Array.from(getLabels(labelledElement)).map(label => {
96-
const textToMatch = getLabelContent(label)
97-
const formControlSelector = labelledNodeNames.join(',')
98-
const labelledFormControl = Array.from(
99-
label.querySelectorAll(formControlSelector),
100-
).filter(element => element.matches(selector))[0]
101-
if (labelledFormControl) {
102-
if (
103-
matcher(textToMatch, labelledFormControl, text, matchNormalizer)
104-
)
105-
labelledElements.push(labelledFormControl)
106-
}
107-
return textToMatch
108-
})
109-
labelsValue = labelsValue.filter(Boolean)
56+
const labelList = getLabels(container, labelledElement, {selector})
57+
labelList
58+
.filter(label => Boolean(label.formControl))
59+
.forEach(label => {
60+
if (matcher(label.content, label.formControl, text, matchNormalizer))
61+
labelledElements.push(label.formControl)
62+
})
63+
const labelsValue = labelList
64+
.filter(label => Boolean(label.content))
65+
.map(label => label.content)
11066
if (
11167
matcher(labelsValue.join(' '), labelledElement, text, matchNormalizer)
11268
)
@@ -232,6 +188,7 @@ const queryAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion(
232188
queryAllByLabelText.name,
233189
'queryAll',
234190
)
191+
235192
export {
236193
queryAllByLabelTextWithSuggestions as queryAllByLabelText,
237194
queryByLabelText,
@@ -240,20 +197,3 @@ export {
240197
findAllByLabelText,
241198
findByLabelText,
242199
}
243-
244-
// Based on https://github.com/eps1lon/dom-accessibility-api/pull/352
245-
function getLabels(element) {
246-
if (element.labels !== undefined) return element.labels
247-
248-
if (!isLabelable(element)) return null
249-
250-
const labels = element.ownerDocument.querySelectorAll('label')
251-
return Array.from(labels).filter(label => label.control === element)
252-
}
253-
254-
function isLabelable(element) {
255-
return (
256-
element.tagName.match(/BUTTON|METER|OUTPUT|PROGRESS|SELECT|TEXTAREA/) ||
257-
(element.tagName === 'INPUT' && element.getAttribute('type') !== 'hidden')
258-
)
259-
}

src/suggestions.js

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,10 @@ import {getDefaultNormalizer} from './matches'
33
import {getNodeText} from './get-node-text'
44
import {DEFAULT_IGNORE_TAGS, getConfig} from './config'
55
import {getImplicitAriaRoles, isInaccessible} from './role-helpers'
6+
import {getLabels} from './label-helpers'
67

78
const normalize = getDefaultNormalizer()
89

9-
function getLabelTextFor(element) {
10-
let label =
11-
element.labels &&
12-
Array.from(element.labels).find(el => Boolean(normalize(el.textContent)))
13-
14-
// non form elements that are using aria-labelledby won't be included in `element.labels`
15-
if (!label) {
16-
const ariaLabelledBy = element.getAttribute('aria-labelledby')
17-
if (ariaLabelledBy) {
18-
// this is only a temporary fix. The problem is that at the moment @testing-library/dom
19-
// not support label concatenation
20-
// see https://github.com/testing-library/dom-testing-library/issues/545
21-
const firstId = ariaLabelledBy.split(' ')[0]
22-
// we're using this notation because with the # selector we would have to escape special characters e.g. user.name
23-
// see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector#Escaping_special_characters
24-
label = document.querySelector(`[id="${firstId}"]`)
25-
}
26-
}
27-
28-
if (label) {
29-
return label.textContent
30-
}
31-
return undefined
32-
}
3310
function escapeRegExp(string) {
3411
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
3512
}
@@ -113,7 +90,9 @@ export function getSuggestedQuery(element, variant = 'get', method) {
11390
})
11491
}
11592

116-
const labelText = getLabelTextFor(element)
93+
const labelText = getLabels(document, element)
94+
.map(label => label.content)
95+
.join(' ')
11796
if (canSuggest('LabelText', method, labelText)) {
11897
return makeSuggestion('LabelText', element, labelText, {variant})
11998
}

0 commit comments

Comments
 (0)