Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/gorgeous-zebras-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@primer/behaviors": patch
---

Use a binary search to find the insertion index for new elements managed by the focus zone.
For a use case with 1000 elements managed by the focus zone, added one at a time (by react),
this takes us from 500,000 calls to `compareDocumentPosition` over 1000ms to 8,000 calls
over 16ms.
38 changes: 33 additions & 5 deletions src/focus-zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,8 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
if (filteredElements.length === 0) {
return
}
// Insert all elements atomically. Assume that all passed elements are well-ordered.
const insertIndex = focusableElements.findIndex(
e => (e.compareDocumentPosition(filteredElements[0]) & Node.DOCUMENT_POSITION_PRECEDING) > 0
)
focusableElements.splice(insertIndex === -1 ? focusableElements.length : insertIndex, 0, ...filteredElements)
// Insert all elements atomically.
focusableElements.splice(findInsertionIndex(filteredElements), 0, ...filteredElements)
for (const element of filteredElements) {
// Set tabindex="-1" on all tabbable elements, but save the original
// value in case we need to disable the behavior
Expand All @@ -452,6 +449,37 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
}
}

function findInsertionIndex(elementsToInsert: HTMLElement[]) {
// Assume that all passed elements are well-ordered.
const firstElementToInsert = elementsToInsert[0]

if (focusableElements.length === 0) return 0

// Because the focusable elements are in document order,
// we can do a binary search to find the insertion index.
let iMin = 0
let iMax = focusableElements.length - 1
while (iMin <= iMax) {
const i = Math.floor((iMin + iMax) / 2)
const element = focusableElements[i]

if (followsInDocument(firstElementToInsert, element)) {
iMax = i - 1
} else {
iMin = i + 1
}
}

return iMin
}

/**
* @returns true if the second argument follows the first argument in the document
*/
function followsInDocument(first: HTMLElement, second: HTMLElement) {
return (second.compareDocumentPosition(first) & Node.DOCUMENT_POSITION_PRECEDING) > 0
}

function endFocusManagement(...elements: HTMLElement[]) {
for (const element of elements) {
const focusableElementIndex = focusableElements.indexOf(element)
Expand Down