-
Notifications
You must be signed in to change notification settings - Fork 603
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
feat(Announce): add support for computing text equivalence #5724
Draft
joshblack
wants to merge
4
commits into
main
Choose a base branch
from
feat/add-text-equivalence-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+253
−11
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ded2108
feat(Announce): add support for computing text equivalence
joshblack 8e193c5
chore: add stories with custom element
joshblack 9247d3f
Merge branch 'main' of github.com:primer/react into feat/add-text-equ…
joshblack c4d1053
test: add tests
joshblack File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
packages/react/src/live-region/__tests__/computeTextEquivalent.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import {computeTextEquivalent} from '../computeTextEquivalent' | ||
|
||
describe('computeTextEquivalent', () => { | ||
afterEach(() => { | ||
document.body.innerHTML = '' | ||
}) | ||
|
||
test('Text', () => { | ||
expect(computeTextEquivalent(new Text('test'))).toEqual('test') | ||
}) | ||
|
||
test('HTMLElement', () => { | ||
const element = document.createElement('div') | ||
|
||
expect(computeTextEquivalent(element)).toEqual('') | ||
|
||
element.textContent = 'test' | ||
expect(computeTextEquivalent(element)).toEqual('test') | ||
}) | ||
|
||
test('HTMLElement with children', () => { | ||
const element = document.createElement('div') | ||
element.innerHTML = `<div>test</div>` | ||
|
||
expect(computeTextEquivalent(element)).toEqual('test') | ||
}) | ||
|
||
test('HTMLElement with text and element children', () => { | ||
const element = document.createElement('div') | ||
element.innerHTML = `before <div>test</div> after` | ||
|
||
expect(computeTextEquivalent(element)).toEqual('before test after') | ||
}) | ||
|
||
test('HTMLElement with aria-label', () => { | ||
const element = document.createElement('div') | ||
element.setAttribute('aria-label', 'test') | ||
expect(computeTextEquivalent(element)).toEqual('test') | ||
}) | ||
|
||
test('HTMLElement with aria-labelledby', () => { | ||
const label = document.createElement('div') | ||
label.textContent = 'test' | ||
label.id = 'label' | ||
|
||
const element = document.createElement('div') | ||
element.setAttribute('aria-labelledby', 'label') | ||
|
||
document.body.appendChild(label) | ||
document.body.appendChild(element) | ||
|
||
expect(computeTextEquivalent(element)).toEqual('test') | ||
}) | ||
|
||
test('HTMLElement with aria-labelledby but no node with id', () => { | ||
const element = document.createElement('div') | ||
element.setAttribute('aria-labelledby', 'label') | ||
|
||
document.body.appendChild(element) | ||
|
||
expect(computeTextEquivalent(element)).toEqual('') | ||
}) | ||
|
||
test('HTMLElement with aria-labelledby with node that is aria-hidden="true"', () => { | ||
const label = document.createElement('div') | ||
label.textContent = 'test' | ||
label.id = 'label' | ||
label.setAttribute('aria-hidden', 'true') | ||
|
||
const element = document.createElement('div') | ||
element.setAttribute('aria-labelledby', 'label') | ||
|
||
document.body.appendChild(label) | ||
document.body.appendChild(element) | ||
|
||
expect(computeTextEquivalent(element)).toEqual('test') | ||
}) | ||
|
||
test('HTMLElement with aria-labelledby that points to multiple nodes', () => { | ||
const label1 = document.createElement('div') | ||
label1.textContent = 'test1' | ||
label1.id = 'label1' | ||
|
||
const label2 = document.createElement('div') | ||
label2.textContent = 'test2' | ||
label2.id = 'label2' | ||
|
||
const element = document.createElement('div') | ||
element.setAttribute('aria-labelledby', 'label1 label2') | ||
|
||
document.body.appendChild(label1) | ||
document.body.appendChild(label2) | ||
document.body.appendChild(element) | ||
|
||
expect(computeTextEquivalent(element)).toEqual('test1 test2') | ||
}) | ||
|
||
test('HTMLElement with aria-hidden', () => { | ||
const element = document.createElement('div') | ||
element.setAttribute('aria-hidden', 'true') | ||
element.textContent = 'test' | ||
expect(computeTextEquivalent(element)).toEqual('') | ||
}) | ||
|
||
test('HTMLElement with aria-hidden and allowAriaHidden', () => { | ||
const element = document.createElement('div') | ||
element.setAttribute('aria-hidden', 'true') | ||
element.textContent = 'test' | ||
expect(computeTextEquivalent(element, {allowAriaHidden: true})).toEqual('test') | ||
}) | ||
|
||
test('HTMLElement with display: none', () => { | ||
const element = document.createElement('div') | ||
element.style.display = 'none' | ||
element.textContent = 'test' | ||
expect(computeTextEquivalent(element)).toEqual('') | ||
}) | ||
|
||
test('HTMLElement with visibility: hidden', () => { | ||
const element = document.createElement('div') | ||
element.style.visibility = 'hidden' | ||
element.textContent = 'test' | ||
expect(computeTextEquivalent(element)).toEqual('') | ||
}) | ||
|
||
test('HTMLElement with shadowRoot', () => { | ||
const element = document.createElement('div') | ||
const shadowRoot = element.attachShadow({mode: 'open'}) | ||
shadowRoot.innerHTML = 'shadow' | ||
expect(computeTextEquivalent(element)).toEqual('shadow') | ||
}) | ||
|
||
test('HTMLElement with shadowRoot', () => { | ||
const element = document.createElement('div') | ||
const shadowRoot = element.attachShadow({mode: 'open'}) | ||
shadowRoot.innerHTML = 'shadow' | ||
expect(computeTextEquivalent(element)).toEqual('shadow') | ||
}) | ||
|
||
test('HTMLElement with shadowRoot and internal id', () => { | ||
const element = document.createElement('div') | ||
const shadowRoot = element.attachShadow({mode: 'open'}) | ||
|
||
const label = document.createElement('div') | ||
label.textContent = 'shadow test' | ||
label.id = 'label' | ||
shadowRoot.appendChild(label) | ||
|
||
const childElement = document.createElement('div') | ||
childElement.setAttribute('aria-labelledby', 'label') | ||
shadowRoot.appendChild(childElement) | ||
|
||
expect(computeTextEquivalent(element)).toEqual('shadow test') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
type TextEquivalentOptions = { | ||
allowAriaHidden: boolean | ||
} | ||
|
||
const defaultOptions: TextEquivalentOptions = { | ||
allowAriaHidden: false, | ||
} | ||
|
||
/** | ||
* Simplified version of the algorithm to compute the text equivalent of an | ||
* element. We do not include support for getting the text equivalence for | ||
* various roles | ||
* | ||
* @see https://www.w3.org/TR/accname-1.2/#computation-steps | ||
*/ | ||
function computeTextEquivalent( | ||
elementOrText: HTMLElement | Text, | ||
options: TextEquivalentOptions = defaultOptions, | ||
parentShadowRoot?: ShadowRoot, | ||
): string { | ||
if (elementOrText instanceof HTMLElement && elementOrText.shadowRoot) { | ||
const shadowRoot = elementOrText.shadowRoot! | ||
return Array.from(elementOrText.shadowRoot.childNodes) | ||
.map(node => { | ||
if (node instanceof Text) { | ||
return computeTextEquivalent(node, options, shadowRoot) | ||
} | ||
|
||
if (node instanceof HTMLElement) { | ||
return computeTextEquivalent(node, options, shadowRoot) | ||
} | ||
|
||
return null | ||
}) | ||
.filter(Boolean) | ||
.join(' ') | ||
} | ||
|
||
if (elementOrText instanceof Text) { | ||
return elementOrText.textContent?.trim() ?? '' | ||
} | ||
|
||
const style = window.getComputedStyle(elementOrText) | ||
if (style.display === 'none' || style.visibility === 'hidden') { | ||
return '' | ||
} | ||
|
||
if (options.allowAriaHidden === false && elementOrText.getAttribute('aria-hidden') === 'true') { | ||
return '' | ||
} | ||
|
||
if (elementOrText.hasAttribute('aria-labelledby')) { | ||
const idrefs = elementOrText.getAttribute('aria-labelledby')! | ||
const context = parentShadowRoot ?? document | ||
return idrefs | ||
.split(' ') | ||
.map(idref => { | ||
const item = context.getElementById(idref) | ||
if (item) { | ||
return computeTextEquivalent(item, {allowAriaHidden: true}) | ||
} | ||
return null | ||
}) | ||
.filter(Boolean) | ||
.join(' ') | ||
} | ||
|
||
if (elementOrText.hasAttribute('aria-label')) { | ||
return elementOrText.getAttribute('aria-label')!.trim() | ||
} | ||
|
||
if (elementOrText.childNodes.length > 0) { | ||
return Array.from(elementOrText.childNodes) | ||
.map(node => { | ||
if (node instanceof Text || node instanceof HTMLElement) { | ||
return computeTextEquivalent(node, options) | ||
} | ||
return null | ||
}) | ||
.filter(Boolean) | ||
.join(' ') | ||
} | ||
|
||
return elementOrText.textContent?.trim() ?? '' | ||
} | ||
|
||
export {computeTextEquivalent} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Note, the announcement in this story will still be the default date (and not the "on ..." message) since when the story renders and gets announced the custom element hasn't updated yet.