Skip to content

Commit

Permalink
fix(focusZone): handle complex child reordering (#1225)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgreif committed May 13, 2021
1 parent 33b3d6e commit 84c7e77
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/yellow-dolphins-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/components": patch
---

handle complex child reordering within a focusZone
49 changes: 49 additions & 0 deletions src/__tests__/behaviors/focusZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ async function nextTick() {
return new Promise(resolve => setTimeout(resolve, 0))
}

const moveDown = () => userEvent.type(document.activeElement!, '{arrowdown}')
const moveUp = () => userEvent.type(document.activeElement!, '{arrowup}')

// Since we use strict `isTabbable` checks within focus trap, we need to mock these
// properties that Jest does not populate.
beforeAll(() => {
Expand Down Expand Up @@ -472,3 +475,49 @@ it('Should set aria-activedescendant correctly', () => {

controller.abort()
})

it('Should handle elements being reordered', async () => {
const {container} = render(
<div>
<div id="focusZone">
<button tabIndex={0}>Apple</button>
<button tabIndex={0}>Banana</button>
<button tabIndex={0}>Cantaloupe</button>
<button tabIndex={0}>Durian</button>
</div>
</div>
)

const focusZoneContainer = container.querySelector<HTMLElement>('#focusZone')!
const [firstButton, secondButton, thirdButton, fourthButton] = focusZoneContainer.querySelectorAll('button')
const controller = focusZone(focusZoneContainer)

firstButton.focus()
expect(document.activeElement).toEqual(firstButton)

moveDown()
expect(document.activeElement).toEqual(secondButton)

moveUp()
expect(document.activeElement).toEqual(firstButton)

// move secondButton and thirdButton to the end of the zone, in reverse order
focusZoneContainer.appendChild(thirdButton)
focusZoneContainer.appendChild(secondButton)

// The mutation observer fires asynchronously
await nextTick()

expect(document.activeElement).toEqual(firstButton)

moveDown()
expect(document.activeElement).toEqual(fourthButton)

moveDown()
expect(document.activeElement).toEqual(thirdButton)

moveDown()
expect(document.activeElement).toEqual(secondButton)

controller.abort()
})
13 changes: 8 additions & 5 deletions src/behaviors/focusZone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,18 +440,21 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
// If the DOM structure of the container changes, make sure we keep our state up-to-date
// with respect to the focusable elements cache and its order
const observer = new MutationObserver(mutations => {
// Perform all removals first, in case element order has simply changed
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode instanceof HTMLElement) {
beginFocusManagement(...iterateFocusableElements(addedNode))
}
}
for (const removedNode of mutation.removedNodes) {
if (removedNode instanceof HTMLElement) {
endFocusManagement(...iterateFocusableElements(removedNode))
}
}
}
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode instanceof HTMLElement) {
beginFocusManagement(...iterateFocusableElements(addedNode))
}
}
}
})

observer.observe(container, {
Expand Down

1 comment on commit 84c7e77

@vercel
Copy link

@vercel vercel bot commented on 84c7e77 May 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.