refactor: extract keyboard handlers to reduce cognitive complexity (S3776)
Extract per-key arrow handler logic into standalone functions outside the component in SidebarTree.tsx and TreeView.tsx, reducing handleKeyDown cognitive complexity from 31 to below the 15-unit maximum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,52 @@ function flattenVisibleNodes(
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Keyboard nav helpers ─────────────────────────────────────────────────────
|
||||
|
||||
function handleArrowDown(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
}
|
||||
|
||||
function handleArrowUp(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||
const prev = visibleNodes[currentIndex - 1]
|
||||
if (prev) focusNode(prev.node.id)
|
||||
}
|
||||
|
||||
function handleArrowRight(
|
||||
current: FlatNode | undefined,
|
||||
currentIndex: number,
|
||||
expandedSet: Set<string>,
|
||||
visibleNodes: FlatNode[],
|
||||
handleToggle: (id: string) => void,
|
||||
focusNode: (id: string) => void,
|
||||
) {
|
||||
if (!current) return
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (!hasChildren) return
|
||||
if (!expandedSet.has(current.node.id)) {
|
||||
handleToggle(current.node.id)
|
||||
} else {
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleArrowLeft(
|
||||
current: FlatNode | undefined,
|
||||
expandedSet: Set<string>,
|
||||
handleToggle: (id: string) => void,
|
||||
focusNode: (id: string) => void,
|
||||
) {
|
||||
if (!current) return
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (hasChildren && expandedSet.has(current.node.id)) {
|
||||
handleToggle(current.node.id)
|
||||
} else if (current.parentId !== null) {
|
||||
focusNode(current.parentId)
|
||||
}
|
||||
}
|
||||
|
||||
interface TreeViewProps {
|
||||
nodes: TreeNode[]
|
||||
onSelect?: (id: string) => void
|
||||
@@ -105,68 +151,13 @@ export function TreeView({
|
||||
const current = visibleNodes[currentIndex]
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown': {
|
||||
e.preventDefault()
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
e.preventDefault()
|
||||
const prev = visibleNodes[currentIndex - 1]
|
||||
if (prev) focusNode(prev.node.id)
|
||||
break
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
e.preventDefault()
|
||||
if (!current) break
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (hasChildren) {
|
||||
if (!expandedSet.has(current.node.id)) {
|
||||
// Expand it
|
||||
handleToggle(current.node.id)
|
||||
} else {
|
||||
// Move to first child (it will be the next visible node)
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
e.preventDefault()
|
||||
if (!current) break
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (hasChildren && expandedSet.has(current.node.id)) {
|
||||
// Collapse
|
||||
handleToggle(current.node.id)
|
||||
} else if (current.parentId !== null) {
|
||||
// Move to parent
|
||||
focusNode(current.parentId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Enter': {
|
||||
e.preventDefault()
|
||||
if (current) {
|
||||
onSelect?.(current.node.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Home': {
|
||||
e.preventDefault()
|
||||
if (visibleNodes.length > 0) {
|
||||
focusNode(visibleNodes[0].node.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'End': {
|
||||
e.preventDefault()
|
||||
if (visibleNodes.length > 0) {
|
||||
focusNode(visibleNodes[visibleNodes.length - 1].node.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowDown': { e.preventDefault(); handleArrowDown(visibleNodes, currentIndex, focusNode); break }
|
||||
case 'ArrowUp': { e.preventDefault(); handleArrowUp(visibleNodes, currentIndex, focusNode); break }
|
||||
case 'ArrowRight': { e.preventDefault(); handleArrowRight(current, currentIndex, expandedSet, visibleNodes, handleToggle, focusNode); break }
|
||||
case 'ArrowLeft': { e.preventDefault(); handleArrowLeft(current, expandedSet, handleToggle, focusNode); break }
|
||||
case 'Enter': { e.preventDefault(); if (current) onSelect?.(current.node.id); break }
|
||||
case 'Home': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[0].node.id); break }
|
||||
case 'End': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[visibleNodes.length - 1].node.id); break }
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -124,6 +124,52 @@ function filterNodes(
|
||||
return { filtered: walk(nodes), matchedParentIds }
|
||||
}
|
||||
|
||||
// ── Keyboard nav helpers ─────────────────────────────────────────────────────
|
||||
|
||||
function handleArrowDown(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
}
|
||||
|
||||
function handleArrowUp(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||
const prev = visibleNodes[currentIndex - 1]
|
||||
if (prev) focusNode(prev.node.id)
|
||||
}
|
||||
|
||||
function handleArrowRight(
|
||||
current: FlatNode | undefined,
|
||||
currentIndex: number,
|
||||
expandedSet: Set<string>,
|
||||
visibleNodes: FlatNode[],
|
||||
handleToggle: (id: string) => void,
|
||||
focusNode: (id: string) => void,
|
||||
) {
|
||||
if (!current) return
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (!hasChildren) return
|
||||
if (!expandedSet.has(current.node.id)) {
|
||||
handleToggle(current.node.id)
|
||||
} else {
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleArrowLeft(
|
||||
current: FlatNode | undefined,
|
||||
expandedSet: Set<string>,
|
||||
handleToggle: (id: string) => void,
|
||||
focusNode: (id: string) => void,
|
||||
) {
|
||||
if (!current) return
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (hasChildren && expandedSet.has(current.node.id)) {
|
||||
handleToggle(current.node.id)
|
||||
} else if (current.parentId !== null) {
|
||||
focusNode(current.parentId)
|
||||
}
|
||||
}
|
||||
|
||||
// ── SidebarTree ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function SidebarTree({
|
||||
@@ -222,64 +268,13 @@ export function SidebarTree({
|
||||
const current = visibleNodes[currentIndex]
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown': {
|
||||
e.preventDefault()
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
e.preventDefault()
|
||||
const prev = visibleNodes[currentIndex - 1]
|
||||
if (prev) focusNode(prev.node.id)
|
||||
break
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
e.preventDefault()
|
||||
if (!current) break
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (hasChildren) {
|
||||
if (!expandedSet.has(current.node.id)) {
|
||||
handleToggle(current.node.id)
|
||||
} else {
|
||||
const next = visibleNodes[currentIndex + 1]
|
||||
if (next) focusNode(next.node.id)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
e.preventDefault()
|
||||
if (!current) break
|
||||
const hasChildren = current.node.children && current.node.children.length > 0
|
||||
if (hasChildren && expandedSet.has(current.node.id)) {
|
||||
handleToggle(current.node.id)
|
||||
} else if (current.parentId !== null) {
|
||||
focusNode(current.parentId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Enter': {
|
||||
e.preventDefault()
|
||||
if (current?.node.path) {
|
||||
navigate(current.node.path)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Home': {
|
||||
e.preventDefault()
|
||||
if (visibleNodes.length > 0) {
|
||||
focusNode(visibleNodes[0].node.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'End': {
|
||||
e.preventDefault()
|
||||
if (visibleNodes.length > 0) {
|
||||
focusNode(visibleNodes[visibleNodes.length - 1].node.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowDown': { e.preventDefault(); handleArrowDown(visibleNodes, currentIndex, focusNode); break }
|
||||
case 'ArrowUp': { e.preventDefault(); handleArrowUp(visibleNodes, currentIndex, focusNode); break }
|
||||
case 'ArrowRight': { e.preventDefault(); handleArrowRight(current, currentIndex, expandedSet, visibleNodes, handleToggle, focusNode); break }
|
||||
case 'ArrowLeft': { e.preventDefault(); handleArrowLeft(current, expandedSet, handleToggle, focusNode); break }
|
||||
case 'Enter': { e.preventDefault(); if (current?.node.path) navigate(current.node.path); break }
|
||||
case 'Home': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[0].node.id); break }
|
||||
case 'End': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[visibleNodes.length - 1].node.id); break }
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
Reference in New Issue
Block a user