diff --git a/.changeset/treeview-typeahead-nested-controls.md b/.changeset/treeview-typeahead-nested-controls.md
new file mode 100644
index 00000000000..f10abb09a7d
--- /dev/null
+++ b/.changeset/treeview-typeahead-nested-controls.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': patch
+---
+
+TreeView: Prevent typeahead errors and focus changes when typing in nested controls.
diff --git a/packages/react/src/TreeView/TreeView.test.tsx b/packages/react/src/TreeView/TreeView.test.tsx
index 045e6d6757f..aa9de62940d 100644
--- a/packages/react/src/TreeView/TreeView.test.tsx
+++ b/packages/react/src/TreeView/TreeView.test.tsx
@@ -1,4 +1,5 @@
import {fireEvent, render, act, screen, waitFor} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
import {afterEach, describe, it, expect, vi} from 'vitest'
import React from 'react'
import type {SubTreeState} from './TreeView'
@@ -1282,6 +1283,94 @@ describe('Keyboard interactions', () => {
})
describe('Typeahead', () => {
+ it.each(['z', 'c'])('ignores rapid "%s" key presses from a nested button', key => {
+ vi.useFakeTimers()
+ const onKeyDown = vi.fn()
+ render(
+
+
+ Apple
+
+
+ Banana
+ Cherry
+ ,
+ )
+
+ const apple = screen.getByRole('treeitem', {name: 'Apple'})
+ const button = screen.getByRole('button', {name: 'Nested action'})
+ const cherry = screen.getByRole('treeitem', {name: 'Cherry'})
+
+ act(() => apple.focus())
+ act(() => button.focus())
+ expect(button).toHaveFocus()
+
+ expect(fireEvent.keyDown(button, {key})).toBe(true)
+ expect(fireEvent.keyDown(button, {key})).toBe(true)
+ expect(button).toHaveFocus()
+ expect(onKeyDown).toHaveBeenCalledTimes(2)
+
+ act(() => apple.focus())
+ fireEvent.keyDown(apple, {key: 'c'})
+ expect(cherry).toHaveFocus()
+ })
+
+ it('allows typing in a nested input without moving focus', async () => {
+ const user = userEvent.setup()
+ render(
+
+
+ Apple
+
+
+ Banana
+ Cherry
+ ,
+ )
+
+ const input = screen.getByRole('textbox', {name: 'Filter'})
+
+ await user.type(input, 'ch')
+
+ expect(input).toHaveValue('ch')
+ expect(input).toHaveFocus()
+ })
+
+ it('does not add nested-control keystrokes to an ongoing search', () => {
+ vi.useFakeTimers()
+ render(
+
+
+ Apple
+
+
+ Cherry
+ Cantalope
+ ,
+ )
+
+ const apple = screen.getByRole('treeitem', {name: 'Apple'})
+ const button = screen.getByRole('button', {name: 'Nested action'})
+ const cherry = screen.getByRole('treeitem', {name: 'Cherry'})
+ const cantalope = screen.getByRole('treeitem', {name: 'Cantalope'})
+
+ act(() => apple.focus())
+ fireEvent.keyDown(apple, {key: 'c'})
+ expect(cherry).toHaveFocus()
+
+ act(() => button.focus())
+ expect(fireEvent.keyDown(button, {key: 'z'})).toBe(true)
+ expect(button).toHaveFocus()
+
+ act(() => cherry.focus())
+ fireEvent.keyDown(cherry, {key: 'a'})
+ expect(cantalope).toHaveFocus()
+ })
+
it('moves focus to the next item that matches the typed character', () => {
render(
diff --git a/packages/react/src/TreeView/useTypeahead.ts b/packages/react/src/TreeView/useTypeahead.ts
index 128b6f967eb..dba62025059 100644
--- a/packages/react/src/TreeView/useTypeahead.ts
+++ b/packages/react/src/TreeView/useTypeahead.ts
@@ -63,6 +63,9 @@ export function useTypeahead({containerRef, onFocusChange}: TypeaheadOptions) {
const container = containerRef.current
function onKeyDown(event: KeyboardEvent) {
+ // Leave keyboard interaction on nested controls to the controls themselves.
+ if (!(event.target instanceof HTMLElement) || event.target.getAttribute('role') !== 'treeitem') return
+
// Ignore key presses that don't produce a character value
if (!event.key || event.key.length > 1 || event.key === ' ') return