From b57856e4ed9c435c4a57db8923e27a1f1c2e1339 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:55:42 +0000 Subject: [PATCH 1/5] Initial plan From 5431c04e799d42d8a0f3df59ff450b9847eea879 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:08:21 +0000 Subject: [PATCH 2/5] Fix keyboard focus for location dropdown when opened via keyboard Agent-Logs-Url: https://github.com/OneNoteDev/WebClipper/sessions/a4bc18dd-5306-49f5-accf-24b9644af25c Co-authored-by: KethanaReddy7 <257986085+KethanaReddy7@users.noreply.github.com> --- .../clipperUI/components/sectionPicker.tsx | 21 ++++ .../components/sectionPicker_tests.tsx | 98 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/src/scripts/clipperUI/components/sectionPicker.tsx b/src/scripts/clipperUI/components/sectionPicker.tsx index 43339d23..6aca43fc 100644 --- a/src/scripts/clipperUI/components/sectionPicker.tsx +++ b/src/scripts/clipperUI/components/sectionPicker.tsx @@ -56,6 +56,27 @@ export class SectionPickerClass extends ComponentBase { + let curSectionId = this.state.curSection && this.state.curSection.section ? this.state.curSection.section.id : undefined; + let elementToFocus: HTMLElement; + if (curSectionId) { + elementToFocus = document.getElementById(curSectionId) as HTMLElement; + } + if (!elementToFocus) { + // Fall back to the first keyboard-navigable item in the section picker popup + let sectionPickerPopup = document.getElementById("sectionPickerContainer"); + if (sectionPickerPopup) { + elementToFocus = sectionPickerPopup.querySelector("[tabindex]:not([tabindex=\"-1\"])") as HTMLElement; + } + } + if (elementToFocus) { + elementToFocus.focus(); + } + }, 0); + } } // Returns true if successful; false otherwise diff --git a/src/tests/clipperUI/components/sectionPicker_tests.tsx b/src/tests/clipperUI/components/sectionPicker_tests.tsx index 00a67e55..47116270 100644 --- a/src/tests/clipperUI/components/sectionPicker_tests.tsx +++ b/src/tests/clipperUI/components/sectionPicker_tests.tsx @@ -272,6 +272,104 @@ export class SectionPickerTests extends TestModule { let actual = SectionPickerClass.formatSectionInfoForStorage([]); strictEqual(actual, undefined, "The section info should be formatted correctly"); }); + + test("onPopupToggle should focus the currently selected section element when the popup opens and a curSection is set", (assert: QUnitAssert) => { + let done = assert.async(); + let clock = sinon.useFakeTimers(); + + let clipperState = MockProps.getMockClipperState(); + let mockNotebooks = MockProps.getMockNotebooks(); + let mockSection = { + section: mockNotebooks[0].sections[0], + path: "Clipper Test > Full Page", + parentId: mockNotebooks[0].id + }; + initializeClipperStorage(JSON.stringify(mockNotebooks), JSON.stringify(mockSection)); + + let component = {}} clipperState={clipperState} />; + let controllerInstance = MithrilUtils.mountToFixture(component); + + // Create a fake section element in the DOM that matches the selected section id + let sectionElement = document.createElement("li"); + sectionElement.id = mockSection.section.id; + sectionElement.tabIndex = 70; + let focusCalled = false; + sectionElement.focus = () => { focusCalled = true; }; + document.body.appendChild(sectionElement); + + controllerInstance.onPopupToggle(true); + clock.tick(0); + + ok(focusCalled, "The selected section element should have been focused when the popup opens"); + + document.body.removeChild(sectionElement); + clock.restore(); + done(); + }); + + test("onPopupToggle should focus the first focusable item in the picker popup when the popup opens and no curSection is set", (assert: QUnitAssert) => { + let done = assert.async(); + let clock = sinon.useFakeTimers(); + + let clipperState = MockProps.getMockClipperState(); + initializeClipperStorage(undefined, undefined); + + let component = {}} clipperState={clipperState} />; + let controllerInstance = MithrilUtils.mountToFixture(component); + + // Create a fake popup container and a focusable item inside it + let sectionPickerPopup = document.createElement("div"); + sectionPickerPopup.id = "sectionPickerContainer"; + let firstItem = document.createElement("li"); + firstItem.tabIndex = 70; + let focusCalled = false; + firstItem.focus = () => { focusCalled = true; }; + sectionPickerPopup.appendChild(firstItem); + document.body.appendChild(sectionPickerPopup); + + controllerInstance.onPopupToggle(true); + clock.tick(0); + + ok(focusCalled, "The first focusable item in the picker popup should have been focused when no section is selected"); + + document.body.removeChild(sectionPickerPopup); + clock.restore(); + done(); + }); + + test("onPopupToggle should not change focus when the popup closes", (assert: QUnitAssert) => { + let done = assert.async(); + let clock = sinon.useFakeTimers(); + + let clipperState = MockProps.getMockClipperState(); + let mockNotebooks = MockProps.getMockNotebooks(); + let mockSection = { + section: mockNotebooks[0].sections[0], + path: "Clipper Test > Full Page", + parentId: mockNotebooks[0].id + }; + initializeClipperStorage(JSON.stringify(mockNotebooks), JSON.stringify(mockSection)); + + let component = {}} clipperState={clipperState} />; + let controllerInstance = MithrilUtils.mountToFixture(component); + + // Create a fake section element to catch any unexpected focus calls + let sectionElement = document.createElement("li"); + sectionElement.id = mockSection.section.id; + sectionElement.tabIndex = 70; + let focusCalled = false; + sectionElement.focus = () => { focusCalled = true; }; + document.body.appendChild(sectionElement); + + controllerInstance.onPopupToggle(false); + clock.tick(0); + + ok(!focusCalled, "No focus change should occur when the popup closes"); + + document.body.removeChild(sectionElement); + clock.restore(); + done(); + }); } } From 20594c7665e3082a4c1c38bd4732eb53c6143880 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:26:57 +0000 Subject: [PATCH 3/5] Add Up/Down arrow key navigation in location dropdown popup Agent-Logs-Url: https://github.com/OneNoteDev/WebClipper/sessions/029fac6a-7d39-4214-b537-4cab9afd25e1 Co-authored-by: KethanaReddy7 <257986085+KethanaReddy7@users.noreply.github.com> --- .../clipperUI/components/sectionPicker.tsx | 41 ++++++++-- .../components/sectionPicker_tests.tsx | 80 +++++++++++++++++++ 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/scripts/clipperUI/components/sectionPicker.tsx b/src/scripts/clipperUI/components/sectionPicker.tsx index 6aca43fc..b0e5ac64 100644 --- a/src/scripts/clipperUI/components/sectionPicker.tsx +++ b/src/scripts/clipperUI/components/sectionPicker.tsx @@ -58,23 +58,52 @@ export class SectionPickerClass extends ComponentBase { + let sectionPickerPopup = document.getElementById("sectionPickerContainer"); + let curSectionId = this.state.curSection && this.state.curSection.section ? this.state.curSection.section.id : undefined; let elementToFocus: HTMLElement; if (curSectionId) { elementToFocus = document.getElementById(curSectionId) as HTMLElement; } - if (!elementToFocus) { + if (!elementToFocus && sectionPickerPopup) { // Fall back to the first keyboard-navigable item in the section picker popup - let sectionPickerPopup = document.getElementById("sectionPickerContainer"); - if (sectionPickerPopup) { - elementToFocus = sectionPickerPopup.querySelector("[tabindex]:not([tabindex=\"-1\"])") as HTMLElement; - } + elementToFocus = sectionPickerPopup.querySelector("[tabindex]:not([tabindex=\"-1\"])") as HTMLElement; } if (elementToFocus) { elementToFocus.focus(); } + + // Attach Up/Down arrow key navigation for the popup list. + // The OneNotePicker library only handles Enter/Tab, so we add arrow key support here. + // The listener is attached to the popup element which is removed from the DOM when the + // popup closes, so there is no need to explicitly clean it up. + // Guard against attaching multiple listeners if onPopupToggle(true) is called more than once. + if (sectionPickerPopup && !sectionPickerPopup.getAttribute("data-arrow-key-handler-attached")) { + sectionPickerPopup.setAttribute("data-arrow-key-handler-attached", "true"); + sectionPickerPopup.addEventListener("keydown", (e: KeyboardEvent) => { + if (e.which !== Constants.KeyCodes.up && e.which !== Constants.KeyCodes.down) { + return; + } + e.preventDefault(); + let focusableItems = Array.from( + sectionPickerPopup.querySelectorAll("[tabindex]:not([tabindex=\"-1\"])") + ) as HTMLElement[]; + if (focusableItems.length === 0) { + return; + } + let currentIndex = focusableItems.indexOf(document.activeElement as HTMLElement); + if (e.which === Constants.KeyCodes.up) { + let prevIndex = currentIndex <= 0 ? 0 : currentIndex - 1; + focusableItems[prevIndex].focus(); + } else { + let nextIndex = currentIndex >= focusableItems.length - 1 ? focusableItems.length - 1 : currentIndex + 1; + focusableItems[nextIndex].focus(); + } + }); + } }, 0); } } diff --git a/src/tests/clipperUI/components/sectionPicker_tests.tsx b/src/tests/clipperUI/components/sectionPicker_tests.tsx index 47116270..83f18f64 100644 --- a/src/tests/clipperUI/components/sectionPicker_tests.tsx +++ b/src/tests/clipperUI/components/sectionPicker_tests.tsx @@ -370,6 +370,86 @@ export class SectionPickerTests extends TestModule { clock.restore(); done(); }); + + test("onPopupToggle should enable Down arrow key to move focus to the next item in the popup", (assert: QUnitAssert) => { + let done = assert.async(); + let clock = sinon.useFakeTimers(); + + let clipperState = MockProps.getMockClipperState(); + initializeClipperStorage(undefined, undefined); + + let component = {}} clipperState={clipperState} />; + let controllerInstance = MithrilUtils.mountToFixture(component); + + // Create a fake popup with two items + let sectionPickerPopup = document.createElement("div"); + sectionPickerPopup.id = "sectionPickerContainer"; + let firstItem = document.createElement("li"); + firstItem.tabIndex = 70; + let secondItemFocusCalled = false; + let secondItem = document.createElement("li"); + secondItem.tabIndex = 70; + secondItem.focus = () => { secondItemFocusCalled = true; }; + sectionPickerPopup.appendChild(firstItem); + sectionPickerPopup.appendChild(secondItem); + document.body.appendChild(sectionPickerPopup); + + controllerInstance.onPopupToggle(true); + clock.tick(0); + + // Simulate focus on first item and press Down arrow + firstItem.focus(); + let downKeyEvent = document.createEvent("KeyboardEvent"); + downKeyEvent.initEvent("keydown", true, true); + Object.defineProperty(downKeyEvent, "which", { value: 40 }); + sectionPickerPopup.dispatchEvent(downKeyEvent); + + ok(secondItemFocusCalled, "Down arrow key should move focus to the next item in the popup"); + + document.body.removeChild(sectionPickerPopup); + clock.restore(); + done(); + }); + + test("onPopupToggle should enable Up arrow key to move focus to the previous item in the popup", (assert: QUnitAssert) => { + let done = assert.async(); + let clock = sinon.useFakeTimers(); + + let clipperState = MockProps.getMockClipperState(); + initializeClipperStorage(undefined, undefined); + + let component = {}} clipperState={clipperState} />; + let controllerInstance = MithrilUtils.mountToFixture(component); + + // Create a fake popup with two items + let sectionPickerPopup = document.createElement("div"); + sectionPickerPopup.id = "sectionPickerContainer"; + let firstItemFocusCalled = false; + let firstItem = document.createElement("li"); + firstItem.tabIndex = 70; + firstItem.focus = () => { firstItemFocusCalled = true; }; + let secondItem = document.createElement("li"); + secondItem.tabIndex = 70; + sectionPickerPopup.appendChild(firstItem); + sectionPickerPopup.appendChild(secondItem); + document.body.appendChild(sectionPickerPopup); + + controllerInstance.onPopupToggle(true); + clock.tick(0); + + // Simulate focus on second item and press Up arrow + secondItem.focus(); + let upKeyEvent = document.createEvent("KeyboardEvent"); + upKeyEvent.initEvent("keydown", true, true); + Object.defineProperty(upKeyEvent, "which", { value: 38 }); + sectionPickerPopup.dispatchEvent(upKeyEvent); + + ok(firstItemFocusCalled, "Up arrow key should move focus to the previous item in the popup"); + + document.body.removeChild(sectionPickerPopup); + clock.restore(); + done(); + }); } } From 155b3d7b059e695aec36e7898b949f02fc1b4845 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:55:04 +0000 Subject: [PATCH 4/5] Fix arrow key navigation to skip hidden items inside collapsed notebooks Agent-Logs-Url: https://github.com/OneNoteDev/WebClipper/sessions/cd89f9f3-38d1-4b38-b360-6bc769e5bbbf Co-authored-by: KethanaReddy7 <257986085+KethanaReddy7@users.noreply.github.com> --- .../clipperUI/components/sectionPicker.tsx | 7 ++- .../components/sectionPicker_tests.tsx | 59 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/scripts/clipperUI/components/sectionPicker.tsx b/src/scripts/clipperUI/components/sectionPicker.tsx index b0e5ac64..8d389ac7 100644 --- a/src/scripts/clipperUI/components/sectionPicker.tsx +++ b/src/scripts/clipperUI/components/sectionPicker.tsx @@ -88,9 +88,14 @@ export class SectionPickerClass extends ComponentBase