Skip to content

Commit

Permalink
refactor(query): update findOrCreateLiveRegion when using global help…
Browse files Browse the repository at this point in the history
…ers (#33)

* refactor(query): update findOrCreateLiveRegion when using global helpers

* chore: add changeset
  • Loading branch information
joshblack authored Mar 25, 2024
1 parent 8825537 commit 9adf94d
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-oranges-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/live-region-element": minor
---

Update logic for finding, or creating, live regions to work while within dialog elements
99 changes: 99 additions & 0 deletions packages/live-region-element/src/__tests__/query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {describe, test, expect, afterEach} from 'vitest'
import '../define'
import {findOrCreateLiveRegion, getClosestLiveRegion} from '../query'

afterEach(() => {
document.body.innerHTML = ''
})

describe('findOrCreateLiveRegion', () => {
test('no live region', () => {
expect(document.querySelector('live-region')).toBe(null)
const liveRegion = findOrCreateLiveRegion()
expect(liveRegion).toBeInTheDocument()
expect(document.querySelector('live-region')).toBe(liveRegion)
})

test('existing live region', () => {
const liveRegion = document.createElement('live-region')
document.body.appendChild(liveRegion)
expect(findOrCreateLiveRegion()).toBe(liveRegion)
})

test('in dialog with live region', () => {
document.body.innerHTML = `
<dialog>
<div id="target"></div>
<live-region id="local"></live-region>
</dialog>
<live-region id="global"></live-region>
`
const liveRegion = findOrCreateLiveRegion(document.getElementById('target')!)
expect(liveRegion).toBe(document.getElementById('local'))
})

test('in dialog with no live region', () => {
document.body.innerHTML = `
<dialog>
<div id="target"></div>
</dialog>
<live-region id="global"></live-region>
`
const dialog = document.querySelector('dialog')!
expect(dialog.querySelector('live-region')).toBe(null)

const liveRegion = findOrCreateLiveRegion(document.getElementById('target')!)
expect(dialog).toContainElement(liveRegion)
})
})

describe('getClosestLiveRegion', () => {
test('no live region', () => {
const element = document.createElement('div')
document.body.appendChild(element)
expect(getClosestLiveRegion(element)).toBe(null)
})

test('live region in document.body', () => {
const element = document.createElement('div')
document.body.appendChild(element)

const liveRegion = document.createElement('live-region')
document.body.appendChild(liveRegion)

expect(getClosestLiveRegion(element)).toBe(liveRegion)
})

test('live region as sibling', () => {
document.body.innerHTML = `
<div>
<div id="target"></div>
<live-region id="sibling"></live-region>
</div>
<live-region id="global"></live-region>
`

expect(getClosestLiveRegion(document.getElementById('target')!)).toBe(document.getElementById('sibling'))
})

test('live region within dialog', () => {
document.body.innerHTML = `
<dialog>
<div id="target"></div>
<live-region id="local"></live-region>
</dialog>
<live-region id="global"></live-region>
`
expect(getClosestLiveRegion(document.getElementById('target')!)).toBe(document.getElementById('local'))
})

test('no live region within dialog', () => {
document.body.innerHTML = `
<dialog>
<div id="target"></div>
</dialog>
<live-region id="global"></live-region>
`
expect(getClosestLiveRegion(document.getElementById('target')!)).toBe(null)
})
})
49 changes: 1 addition & 48 deletions packages/live-region-element/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import './define'
import {LiveRegionElement, templateContent, type AnnounceOptions} from './live-region-element'
import {findOrCreateLiveRegion} from './query'

type GlobalAnnounceOptions = AnnounceOptions & {
/**
Expand Down Expand Up @@ -32,52 +33,4 @@ export function announceFromElement(element: HTMLElement, options: GlobalAnnounc
return liveRegion.announceFromElement(element, options)
}

let liveRegion: LiveRegionElement | null = null

function findOrCreateLiveRegion(from?: HTMLElement, appendTo?: HTMLElement): LiveRegionElement {
// Check to see if we have already created a `<live-region>` element and that
// it is currently connected to the DOM.
if (liveRegion !== null && liveRegion.isConnected) {
return liveRegion
}

// If `from` is defined, try to find the closest `<live-region>` element
// relative to the given element
liveRegion = from ? getClosestLiveRegion(from) : null
if (liveRegion !== null) {
return liveRegion
}

// Otherwise, try to find any `<live-region>` element in the document
liveRegion = document.querySelector('live-region')
if (liveRegion !== null) {
return liveRegion
}

// Finally, if none exist, create a new `<live-region>` element and append it
// to the given `appendTo` element, if one exists
liveRegion = document.createElement('live-region') as LiveRegionElement
if (appendTo) {
appendTo.appendChild(liveRegion)
} else {
document.body.appendChild(liveRegion)
}

return liveRegion
}

function getClosestLiveRegion(from: HTMLElement): LiveRegionElement | null {
let current: HTMLElement | null = from

while ((current = current.parentElement)) {
for (const child of current.childNodes) {
if (child instanceof LiveRegionElement) {
return child
}
}
}

return null
}

export {LiveRegionElement, templateContent}
60 changes: 60 additions & 0 deletions packages/live-region-element/src/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {LiveRegionElement} from './live-region-element'

export function findOrCreateLiveRegion(from?: HTMLElement, appendTo?: HTMLElement): LiveRegionElement {
let liveRegion: LiveRegionElement | null = null

// If `from` is defined, try to find the closest `<live-region>` element
// relative to the given element
liveRegion = from ? getClosestLiveRegion(from) : null
if (liveRegion !== null) {
return liveRegion
}

// Get the containing element for the live region. If we know we are within a
// dialog element, then live regions must live within that element.
let container = document.body
if (from) {
const dialog = from.closest('dialog')
if (dialog) {
container = dialog
}
}

// Otherwise, try to find any `<live-region>` element in the document
liveRegion = container.querySelector('live-region')
if (liveRegion !== null) {
return liveRegion
}

// Finally, if none exist, create a new `<live-region>` element and append it
// to the given `appendTo` element, if one exists
liveRegion = document.createElement('live-region')
if (appendTo) {
appendTo.appendChild(liveRegion)
} else {
container.appendChild(liveRegion)
}

return liveRegion
}

export function getClosestLiveRegion(from: HTMLElement): LiveRegionElement | null {
const dialog = from.closest('dialog')
let current: HTMLElement | null = from

while ((current = current.parentElement)) {
// If the element exists within a <dialog>, we can only use a live region
// within that element
if (dialog && !dialog.contains(current)) {
break
}

for (const child of current.childNodes) {
if (child instanceof LiveRegionElement) {
return child
}
}
}

return null
}

0 comments on commit 9adf94d

Please sign in to comment.