diff --git a/package-lock.json b/package-lock.json index bd88cc63..6be860be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25001,7 +25001,7 @@ "requires": { "debuglog": "^1.0.1", "dezalgo": "^1.0.0", - "graceful-fs": "4.2.2", + "graceful-fs": "^4.1.2", "once": "^1.3.0" } }, diff --git a/src/scripts/clipperUI/components/sectionPicker.tsx b/src/scripts/clipperUI/components/sectionPicker.tsx index 43339d23..309f67b6 100644 --- a/src/scripts/clipperUI/components/sectionPicker.tsx +++ b/src/scripts/clipperUI/components/sectionPicker.tsx @@ -21,6 +21,7 @@ export interface SectionPickerState { path: string; section: OneNoteApi.Section; }; + keyboardNavigationHandler?: (e: KeyboardEvent) => void; } interface SectionPickerProp extends ClipperStateProp { @@ -248,6 +249,206 @@ export class SectionPickerClass extends ComponentBase { + const target = e.target as HTMLElement; + + // Only handle keyboard events when focus is within the picker tree + const pickerContainer = document.getElementById(Constants.Ids.sectionLocationContainer); + if (!pickerContainer || !pickerContainer.contains(target)) { + return; + } + + // Get all focusable tree items (notebooks and sections) + const treeItemsNodeList = document.querySelectorAll('[role="treeitem"]'); + const treeItems: HTMLElement[] = []; + for (let i = 0; i < treeItemsNodeList.length; i++) { + treeItems.push(treeItemsNodeList[i] as HTMLElement); + } + if (treeItems.length === 0) { + return; + } + + // Find currently focused item + let currentIndex = -1; + for (let i = 0; i < treeItems.length; i++) { + if (treeItems[i] === document.activeElement || treeItems[i].contains(document.activeElement)) { + currentIndex = i; + break; + } + } + if (currentIndex === -1) { + currentIndex = 0; + } + + let handled = false; + + switch (e.which) { + case Constants.KeyCodes.down: + // Move to next visible tree item + if (currentIndex < treeItems.length - 1) { + let nextIndex = currentIndex + 1; + while (nextIndex < treeItems.length) { + // Check if the item is visible + const nextItem = treeItems[nextIndex]; + if (this.isTreeItemVisible(nextItem)) { + nextItem.focus(); + break; + } + nextIndex++; + } + } + handled = true; + break; + + case Constants.KeyCodes.up: + // Move to previous visible tree item + if (currentIndex > 0) { + let prevIndex = currentIndex - 1; + while (prevIndex >= 0) { + // Check if the item is visible + const prevItem = treeItems[prevIndex]; + if (this.isTreeItemVisible(prevItem)) { + prevItem.focus(); + break; + } + prevIndex--; + } + } + handled = true; + break; + + case Constants.KeyCodes.right: + // Expand collapsed item or move to first child + const currentItem = treeItems[currentIndex]; + const isExpanded = currentItem.getAttribute("aria-expanded") === "true"; + if (currentItem.hasAttribute("aria-expanded") && !isExpanded) { + // Item can be expanded, click it to expand + currentItem.click(); + } else if (isExpanded && currentIndex < treeItems.length - 1) { + // Already expanded, move to first child + let nextIndex = currentIndex + 1; + const nextItem = treeItems[nextIndex]; + if (this.isTreeItemVisible(nextItem) && this.isChildOf(nextItem, currentItem)) { + nextItem.focus(); + } + } + handled = true; + break; + + case Constants.KeyCodes.left: + // Collapse expanded item or move to parent + const currentItemLeft = treeItems[currentIndex]; + const isExpandedLeft = currentItemLeft.getAttribute("aria-expanded") === "true"; + if (currentItemLeft.hasAttribute("aria-expanded") && isExpandedLeft) { + // Item is expanded, collapse it + currentItemLeft.click(); + } else { + // Move to parent item + const parentItem = this.findParentTreeItem(currentItemLeft); + if (parentItem) { + parentItem.focus(); + } + } + handled = true; + break; + + case Constants.KeyCodes.home: + // Move to first tree item + if (treeItems.length > 0 && this.isTreeItemVisible(treeItems[0])) { + treeItems[0].focus(); + } + handled = true; + break; + + case Constants.KeyCodes.end: + // Move to last visible tree item + for (let i = treeItems.length - 1; i >= 0; i--) { + if (this.isTreeItemVisible(treeItems[i])) { + treeItems[i].focus(); + break; + } + } + handled = true; + break; + + default: + // No action needed for other keys + break; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + }; + + // Store handler reference and add event listener + this.setState({ keyboardNavigationHandler: keydownHandler }); + document.addEventListener("keydown", keydownHandler, true); + } + + // Helper function to check if a tree item is visible + private isTreeItemVisible(item: HTMLElement): boolean { + if (!item) { + return false; + } + // Check if item or its parent containers are hidden + let current: HTMLElement = item; + while (current && current !== document.body) { + const style = window.getComputedStyle(current); + if (style.display === "none" || style.visibility === "hidden") { + return false; + } + // Check if parent is collapsed + const parent = current.parentElement; + if (parent && parent.classList.contains("Closed")) { + return false; + } + current = parent; + } + return true; + } + + // Helper function to check if an item is a child of another + private isChildOf(child: HTMLElement, parent: HTMLElement): boolean { + let current = child.parentElement; + while (current && current !== document.body) { + if (current === parent) { + return true; + } + // Check if we've moved up to a sibling tree item + if (current.getAttribute("role") === "treeitem" && current !== parent) { + return false; + } + current = current.parentElement; + } + return false; + } + + // Helper function to find parent tree item + private findParentTreeItem(item: HTMLElement): HTMLElement | undefined { + // Walk up the DOM to find the parent tree item + let current = item.parentElement; + while (current && current !== document.body) { + if (current.getAttribute("role") === "treeitem") { + return current; + } + current = current.parentElement; + } + return undefined; + } + render() { if (this.dataSourceUninitialized()) { // This logic gets executed on app launch (if already signed in) and whenever the user signs in or out ... @@ -295,7 +496,7 @@ export class SectionPickerClass extends ComponentBase +