Skip to content

Commit 9adf94d

Browse files
authored
refactor(query): update findOrCreateLiveRegion when using global helpers (#33)
* refactor(query): update findOrCreateLiveRegion when using global helpers * chore: add changeset
1 parent 8825537 commit 9adf94d

File tree

4 files changed

+165
-48
lines changed

4 files changed

+165
-48
lines changed

.changeset/three-oranges-brake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/live-region-element": minor
3+
---
4+
5+
Update logic for finding, or creating, live regions to work while within dialog elements
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {describe, test, expect, afterEach} from 'vitest'
2+
import '../define'
3+
import {findOrCreateLiveRegion, getClosestLiveRegion} from '../query'
4+
5+
afterEach(() => {
6+
document.body.innerHTML = ''
7+
})
8+
9+
describe('findOrCreateLiveRegion', () => {
10+
test('no live region', () => {
11+
expect(document.querySelector('live-region')).toBe(null)
12+
const liveRegion = findOrCreateLiveRegion()
13+
expect(liveRegion).toBeInTheDocument()
14+
expect(document.querySelector('live-region')).toBe(liveRegion)
15+
})
16+
17+
test('existing live region', () => {
18+
const liveRegion = document.createElement('live-region')
19+
document.body.appendChild(liveRegion)
20+
expect(findOrCreateLiveRegion()).toBe(liveRegion)
21+
})
22+
23+
test('in dialog with live region', () => {
24+
document.body.innerHTML = `
25+
<dialog>
26+
<div id="target"></div>
27+
<live-region id="local"></live-region>
28+
</dialog>
29+
<live-region id="global"></live-region>
30+
`
31+
const liveRegion = findOrCreateLiveRegion(document.getElementById('target')!)
32+
expect(liveRegion).toBe(document.getElementById('local'))
33+
})
34+
35+
test('in dialog with no live region', () => {
36+
document.body.innerHTML = `
37+
<dialog>
38+
<div id="target"></div>
39+
</dialog>
40+
<live-region id="global"></live-region>
41+
`
42+
const dialog = document.querySelector('dialog')!
43+
expect(dialog.querySelector('live-region')).toBe(null)
44+
45+
const liveRegion = findOrCreateLiveRegion(document.getElementById('target')!)
46+
expect(dialog).toContainElement(liveRegion)
47+
})
48+
})
49+
50+
describe('getClosestLiveRegion', () => {
51+
test('no live region', () => {
52+
const element = document.createElement('div')
53+
document.body.appendChild(element)
54+
expect(getClosestLiveRegion(element)).toBe(null)
55+
})
56+
57+
test('live region in document.body', () => {
58+
const element = document.createElement('div')
59+
document.body.appendChild(element)
60+
61+
const liveRegion = document.createElement('live-region')
62+
document.body.appendChild(liveRegion)
63+
64+
expect(getClosestLiveRegion(element)).toBe(liveRegion)
65+
})
66+
67+
test('live region as sibling', () => {
68+
document.body.innerHTML = `
69+
<div>
70+
<div id="target"></div>
71+
<live-region id="sibling"></live-region>
72+
</div>
73+
<live-region id="global"></live-region>
74+
`
75+
76+
expect(getClosestLiveRegion(document.getElementById('target')!)).toBe(document.getElementById('sibling'))
77+
})
78+
79+
test('live region within dialog', () => {
80+
document.body.innerHTML = `
81+
<dialog>
82+
<div id="target"></div>
83+
<live-region id="local"></live-region>
84+
</dialog>
85+
<live-region id="global"></live-region>
86+
`
87+
expect(getClosestLiveRegion(document.getElementById('target')!)).toBe(document.getElementById('local'))
88+
})
89+
90+
test('no live region within dialog', () => {
91+
document.body.innerHTML = `
92+
<dialog>
93+
<div id="target"></div>
94+
</dialog>
95+
<live-region id="global"></live-region>
96+
`
97+
expect(getClosestLiveRegion(document.getElementById('target')!)).toBe(null)
98+
})
99+
})
+1-48
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import './define'
22
import {LiveRegionElement, templateContent, type AnnounceOptions} from './live-region-element'
3+
import {findOrCreateLiveRegion} from './query'
34

45
type GlobalAnnounceOptions = AnnounceOptions & {
56
/**
@@ -32,52 +33,4 @@ export function announceFromElement(element: HTMLElement, options: GlobalAnnounc
3233
return liveRegion.announceFromElement(element, options)
3334
}
3435

35-
let liveRegion: LiveRegionElement | null = null
36-
37-
function findOrCreateLiveRegion(from?: HTMLElement, appendTo?: HTMLElement): LiveRegionElement {
38-
// Check to see if we have already created a `<live-region>` element and that
39-
// it is currently connected to the DOM.
40-
if (liveRegion !== null && liveRegion.isConnected) {
41-
return liveRegion
42-
}
43-
44-
// If `from` is defined, try to find the closest `<live-region>` element
45-
// relative to the given element
46-
liveRegion = from ? getClosestLiveRegion(from) : null
47-
if (liveRegion !== null) {
48-
return liveRegion
49-
}
50-
51-
// Otherwise, try to find any `<live-region>` element in the document
52-
liveRegion = document.querySelector('live-region')
53-
if (liveRegion !== null) {
54-
return liveRegion
55-
}
56-
57-
// Finally, if none exist, create a new `<live-region>` element and append it
58-
// to the given `appendTo` element, if one exists
59-
liveRegion = document.createElement('live-region') as LiveRegionElement
60-
if (appendTo) {
61-
appendTo.appendChild(liveRegion)
62-
} else {
63-
document.body.appendChild(liveRegion)
64-
}
65-
66-
return liveRegion
67-
}
68-
69-
function getClosestLiveRegion(from: HTMLElement): LiveRegionElement | null {
70-
let current: HTMLElement | null = from
71-
72-
while ((current = current.parentElement)) {
73-
for (const child of current.childNodes) {
74-
if (child instanceof LiveRegionElement) {
75-
return child
76-
}
77-
}
78-
}
79-
80-
return null
81-
}
82-
8336
export {LiveRegionElement, templateContent}
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {LiveRegionElement} from './live-region-element'
2+
3+
export function findOrCreateLiveRegion(from?: HTMLElement, appendTo?: HTMLElement): LiveRegionElement {
4+
let liveRegion: LiveRegionElement | null = null
5+
6+
// If `from` is defined, try to find the closest `<live-region>` element
7+
// relative to the given element
8+
liveRegion = from ? getClosestLiveRegion(from) : null
9+
if (liveRegion !== null) {
10+
return liveRegion
11+
}
12+
13+
// Get the containing element for the live region. If we know we are within a
14+
// dialog element, then live regions must live within that element.
15+
let container = document.body
16+
if (from) {
17+
const dialog = from.closest('dialog')
18+
if (dialog) {
19+
container = dialog
20+
}
21+
}
22+
23+
// Otherwise, try to find any `<live-region>` element in the document
24+
liveRegion = container.querySelector('live-region')
25+
if (liveRegion !== null) {
26+
return liveRegion
27+
}
28+
29+
// Finally, if none exist, create a new `<live-region>` element and append it
30+
// to the given `appendTo` element, if one exists
31+
liveRegion = document.createElement('live-region')
32+
if (appendTo) {
33+
appendTo.appendChild(liveRegion)
34+
} else {
35+
container.appendChild(liveRegion)
36+
}
37+
38+
return liveRegion
39+
}
40+
41+
export function getClosestLiveRegion(from: HTMLElement): LiveRegionElement | null {
42+
const dialog = from.closest('dialog')
43+
let current: HTMLElement | null = from
44+
45+
while ((current = current.parentElement)) {
46+
// If the element exists within a <dialog>, we can only use a live region
47+
// within that element
48+
if (dialog && !dialog.contains(current)) {
49+
break
50+
}
51+
52+
for (const child of current.childNodes) {
53+
if (child instanceof LiveRegionElement) {
54+
return child
55+
}
56+
}
57+
}
58+
59+
return null
60+
}

0 commit comments

Comments
 (0)