Skip to content

Commit aa9ffc4

Browse files
authored
Merge pull request #473 from primer/tylerjdev/fix-duplicate-sentinels
Focus-trap: Prevent duplicate sentinels
2 parents 87e7cc4 + f93faab commit aa9ffc4

File tree

3 files changed

+55
-2
lines changed

3 files changed

+55
-2
lines changed

.changeset/large-poets-run.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/behaviors': patch
3+
---
4+
5+
Prevent duplicate sentinels from being added if some already exist in the container of the focus trap.

src/__tests__/focus-trap.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,42 @@ it('should remove the mutation observer when the focus trap is released', async
328328
expect(document.activeElement).not.toEqual(newLastButton)
329329
expect(trapContainer.lastElementChild).toEqual(newLastButton)
330330
})
331+
332+
it('Should only have one set of sentinels', async () => {
333+
const {container} = render(
334+
<div>
335+
<div id="trapContainer">
336+
<button tabIndex={0}>Apple</button>
337+
</div>
338+
<button id="durian" tabIndex={0}>
339+
Durian
340+
</button>
341+
</div>,
342+
)
343+
344+
const trapContainer = container.querySelector<HTMLElement>('#trapContainer')!
345+
const durianButton = container.querySelector<HTMLElement>('#durian')!
346+
const firstButton = trapContainer.querySelector('button')!
347+
348+
focusTrap(trapContainer)
349+
350+
durianButton.focus()
351+
expect(document.activeElement).toEqual(firstButton)
352+
353+
trapContainer.insertAdjacentHTML(
354+
'afterbegin',
355+
'<div id="newTrapContainer"><button id="newFirst" tabindex="0">New first button</button></div>',
356+
)
357+
358+
const newTrapContainer = trapContainer.querySelector<HTMLElement>('#newTrapContainer')!
359+
const newFirstButton = newTrapContainer.querySelector<HTMLElement>('#newFirst')!
360+
361+
const controller = focusTrap(newTrapContainer)
362+
expect(document.activeElement).toEqual(newFirstButton)
363+
expect(document.querySelectorAll('.sentinel').length).toEqual(4)
364+
365+
controller?.abort()
366+
367+
trapContainer.removeChild(newTrapContainer)
368+
expect(document.querySelectorAll('.sentinel').length).toEqual(2)
369+
})

src/focus-trap.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,17 @@ export function focusTrap(
9898
const firstFocusableChild = getFocusableChild(container)
9999
firstFocusableChild?.focus()
100100
}
101-
container.prepend(sentinelStart)
102-
container.append(sentinelEnd)
101+
102+
// If the container already has sentinels as direct children, don't add more.
103+
// The mutation observer will take care of moving existing sentinels to the correct position.
104+
const existingSentinels = Array.from(container.children).filter(
105+
e => e.classList.contains('sentinel') && e.tagName === 'SPAN',
106+
)
107+
108+
if (!existingSentinels.length) {
109+
container.prepend(sentinelStart)
110+
container.append(sentinelEnd)
111+
}
103112

104113
const observer = observeFocusTrap(container, [sentinelStart, sentinelEnd])
105114

0 commit comments

Comments
 (0)