-
Notifications
You must be signed in to change notification settings - Fork 469
get by label concat values #681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4033c6e
b609b32
732a8ab
eb8517d
f3a3c65
df29ead
de06780
9d2be9f
49c6148
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,29 @@ import { | |
wrapAllByQueryWithSuggestion, | ||
wrapSingleQueryWithSuggestion, | ||
} from './all-utils' | ||
import {queryAllByText} from './text' | ||
|
||
function queryAllLabels(container) { | ||
return Array.from(container.querySelectorAll('label,input')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It occurs to me that an element can be labeled by anything, not just That said, I'm not sure how to handle "everything else." It seems to me that rather than searching for all possible elements that can label anything, we should be searching for everything that's labeled first, determine the contents of their label, and then return the elements whose label matches the query. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For what it's worth, I think that searching for labelled items and then for their labels is more feasible than querying for all elements that could be labels. Like you said above! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you'd like to tack a whack at that, I think it would solve a lot of problems. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For sure I would like to do it! |
||
.map(node => { | ||
let textToMatch = | ||
node.tagName.toLowerCase() === 'label' | ||
? node.textContent | ||
: node.value || null | ||
// The children of a textarea are part of `textContent` as well. We | ||
// need to remove them from the string so we can match it afterwards. | ||
Array.from(node.querySelectorAll('textarea')).forEach(textarea => { | ||
textToMatch = textToMatch.replace(textarea.value, '') | ||
}) | ||
|
||
// The children of a select are also part of `textContent`, so we | ||
// need also to remove their text. | ||
Array.from(node.querySelectorAll('select')).forEach(select => { | ||
textToMatch = textToMatch.replace(select.textContent, '') | ||
}) | ||
return {node, textToMatch} | ||
}) | ||
.filter(({textToMatch}) => textToMatch !== null) | ||
} | ||
|
||
function queryAllLabelsByText( | ||
container, | ||
|
@@ -19,23 +41,25 @@ function queryAllLabelsByText( | |
) { | ||
const matcher = exact ? matches : fuzzyMatches | ||
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) | ||
return Array.from(container.querySelectorAll('label')).filter(label => { | ||
let textToMatch = label.textContent | ||
|
||
// The children of a textarea are part of `textContent` as well. We | ||
// need to remove them from the string so we can match it afterwards. | ||
Array.from(label.querySelectorAll('textarea')).forEach(textarea => { | ||
textToMatch = textToMatch.replace(textarea.value, '') | ||
}) | ||
const textToMatchByLabels = queryAllLabels(container) | ||
|
||
// The children of a select are also part of `textContent`, so we | ||
// need also to remove their text. | ||
Array.from(label.querySelectorAll('select')).forEach(select => { | ||
textToMatch = textToMatch.replace(select.textContent, '') | ||
}) | ||
return textToMatchByLabels | ||
.filter(({node, textToMatch}) => | ||
matcher(textToMatch, node, text, matchNormalizer), | ||
) | ||
.map(({node}) => node) | ||
} | ||
|
||
return matcher(textToMatch, label, text, matchNormalizer) | ||
function getLabelContent(label) { | ||
let labelContent = label.getAttribute('value') || label.textContent | ||
Array.from(label.querySelectorAll('textarea')).forEach(textarea => { | ||
labelContent = labelContent.replace(textarea.value, '') | ||
}) | ||
Array.from(label.querySelectorAll('select')).forEach(select => { | ||
labelContent = labelContent.replace(select.textContent, '') | ||
}) | ||
return labelContent | ||
} | ||
|
||
function queryAllByLabelText( | ||
|
@@ -45,74 +69,69 @@ function queryAllByLabelText( | |
) { | ||
checkContainerType(container) | ||
|
||
const matcher = exact ? matches : fuzzyMatches | ||
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) | ||
const labels = queryAllLabelsByText(container, text, { | ||
exact, | ||
normalizer: matchNormalizer, | ||
}) | ||
const labelledElements = labels | ||
.reduce((matchedElements, label) => { | ||
const elementsForLabel = [] | ||
if (label.control) { | ||
elementsForLabel.push(label.control) | ||
} | ||
/* istanbul ignore if */ | ||
if (label.getAttribute('for')) { | ||
// we're using this notation because with the # selector we would have to escape special characters e.g. user.name | ||
// see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector#Escaping_special_characters | ||
// <label for="someId">text</label><input id="someId" /> | ||
|
||
// .control support has landed in jsdom (https://github.com/jsdom/jsdom/issues/2175) | ||
elementsForLabel.push( | ||
container.querySelector(`[id="${label.getAttribute('for')}"]`), | ||
) | ||
} | ||
if (label.getAttribute('id')) { | ||
// <label id="someId">text</label><input aria-labelledby="someId" /> | ||
Array.from( | ||
container.querySelectorAll( | ||
`[aria-labelledby~="${label.getAttribute('id')}"]`, | ||
), | ||
).forEach(element => elementsForLabel.push(element)) | ||
} | ||
if (label.childNodes.length) { | ||
// <label>text: <input /></label> | ||
const formControlSelector = | ||
'button, input, meter, output, progress, select, textarea' | ||
const labelledFormControl = Array.from( | ||
label.querySelectorAll(formControlSelector), | ||
).filter(element => element.matches(selector))[0] | ||
if (labelledFormControl) elementsForLabel.push(labelledFormControl) | ||
const matchingLabelledElements = Array.from(container.querySelectorAll('*')) | ||
.filter( | ||
element => element.labels || element.hasAttribute('aria-labelledby'), | ||
) | ||
.reduce((labelledElements, labelledElement) => { | ||
const labelsId = labelledElement.getAttribute('aria-labelledby') | ||
? labelledElement.getAttribute('aria-labelledby').split(' ') | ||
: [] | ||
const labelsValue = labelsId.length | ||
? labelsId.map(labelId => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if an element had both a .labels property and the aria-labelledby? I don't know if that's valid or not. Is it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aria-labelledby has precedence: https://www.w3.org/TR/accname-1.1/#step2B 2B is for aria-labelledby, 2D for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, I think this PR is good to go! Anyone have other thoughts? |
||
const labellingElement = container.querySelector(`[id=${labelId}]`) | ||
return getLabelContent(labellingElement) | ||
}) | ||
: Array.from(labelledElement.labels).map(label => { | ||
const textToMatch = getLabelContent(label) | ||
const formControlSelector = | ||
'button, input, meter, output, progress, select, textarea' | ||
const labelledFormControl = Array.from( | ||
label.querySelectorAll(formControlSelector), | ||
).filter(element => element.matches(selector))[0] | ||
if (labelledFormControl) { | ||
if ( | ||
matcher(textToMatch, labelledFormControl, text, matchNormalizer) | ||
) | ||
labelledElements.push(labelledFormControl) | ||
} | ||
return textToMatch | ||
}) | ||
if ( | ||
matcher(labelsValue.join(' '), labelledElement, text, matchNormalizer) | ||
) | ||
labelledElements.push(labelledElement) | ||
if (labelsValue.length > 1) { | ||
labelsValue.forEach((labelValue, index) => { | ||
if (matcher(labelValue, labelledElement, text, matchNormalizer)) | ||
labelledElements.push(labelledElement) | ||
|
||
const labelsFiltered = [...labelsValue] | ||
labelsFiltered.splice(index, 1) | ||
|
||
if (labelsFiltered.length > 1) { | ||
if ( | ||
matcher( | ||
labelsFiltered.join(' '), | ||
labelledElement, | ||
text, | ||
matchNormalizer, | ||
) | ||
) | ||
labelledElements.push(labelledElement) | ||
} | ||
}) | ||
} | ||
return matchedElements.concat(elementsForLabel) | ||
|
||
return labelledElements | ||
}, []) | ||
.filter(element => element !== null) | ||
.concat(queryAllByAttribute('aria-label', container, text, {exact})) | ||
|
||
const possibleAriaLabelElements = queryAllByText(container, text, { | ||
exact, | ||
normalizer: matchNormalizer, | ||
}) | ||
|
||
const ariaLabelledElements = possibleAriaLabelElements.reduce( | ||
(allLabelledElements, nextLabelElement) => { | ||
const labelId = nextLabelElement.getAttribute('id') | ||
|
||
if (!labelId) return allLabelledElements | ||
|
||
// ARIA labels can label multiple elements | ||
const labelledNodes = Array.from( | ||
container.querySelectorAll(`[aria-labelledby~="${labelId}"]`), | ||
) | ||
|
||
return allLabelledElements.concat(labelledNodes) | ||
}, | ||
[], | ||
return Array.from(new Set(matchingLabelledElements)).filter(element => | ||
element.matches(selector), | ||
) | ||
|
||
return Array.from( | ||
new Set([...labelledElements, ...ariaLabelledElements]), | ||
).filter(element => element.matches(selector)) | ||
} | ||
|
||
// the getAll* query would normally look like this: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should find
textarea
. And it does so when usinglabel.control
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I am not understanding, you are saying that the right test to be done here (at line 374) was that the labeled element is
textarea
instead of that noinput
element labeled was found?getByLabelText(/alone/)
would have found thetextarea
element, I added the selector to make it return null.Please tell me if I am missing something.