{closeButtonToRender}
diff --git a/src/tests/clipperUI/mainController_tests.tsx b/src/tests/clipperUI/mainController_tests.tsx index 3ba143f1..0f27f925 100644 --- a/src/tests/clipperUI/mainController_tests.tsx +++ b/src/tests/clipperUI/mainController_tests.tsx @@ -467,6 +467,17 @@ export class MainControllerTests extends TestModule { "The close button should render when the clipper is not clipping to OneNote API"); } }); + + test("The main controller should have role='dialog' and aria-modal='true' for accessibility", () => { + MithrilUtils.mountToFixture(this.defaultComponent); + + let mainController = document.getElementById(Constants.Ids.mainController); + ok(mainController, "The main controller element should exist"); + strictEqual(mainController.getAttribute("role"), "dialog", + "The main controller should have role='dialog' to prevent Voice Access from numbering background controls"); + strictEqual(mainController.getAttribute("aria-modal"), "true", + "The main controller should have aria-modal='true' to mark it as a modal dialog"); + }); } private getMockRequestError(): OneNoteApi.RequestError { From 9fb6f3e9c6f81c2810410fbb28905dc736d3115a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:53:30 +0000 Subject: [PATCH 25/34] Fix Voice Access background controls: set aria-hidden on document.body from inject script Agent-Logs-Url: https://github.com/OneNoteDev/WebClipper/sessions/6ec55267-0506-4b18-a277-54f3f90edcb7 Co-authored-by: KethanaReddy7 <257986085+KethanaReddy7@users.noreply.github.com> --- src/scripts/extensions/clipperInject.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/scripts/extensions/clipperInject.ts b/src/scripts/extensions/clipperInject.ts index 8a66cd3d..9517e08c 100644 --- a/src/scripts/extensions/clipperInject.ts +++ b/src/scripts/extensions/clipperInject.ts @@ -84,6 +84,14 @@ export class ClipperInject extends FrameInjectBase { this.updateUiSizeAttributes(); this.overrideTransformStyles(document.documentElement); + // The clipper frame is now visible; hide page body from assistive technologies + // so that tools like Voice Access do not number background page controls. + // The frame is appended to (not ), so marking + // only hides page content while keeping the clipper iframe accessible. + if (document.body) { + document.body.setAttribute("aria-hidden", "true"); + } + this.logger = new CommunicatorLoggerPure(this.uiCommunicator); this.updatePageInfo(); @@ -316,6 +324,9 @@ export class ClipperInject extends FrameInjectBase { this.uiCommunicator.registerFunction(Constants.FunctionKeys.hideUi, () => { this.frame.style.display = "none"; + if (document.body) { + document.body.removeAttribute("aria-hidden"); + } }); this.uiCommunicator.registerFunction(Constants.FunctionKeys.refreshPage, () => { @@ -375,6 +386,9 @@ export class ClipperInject extends FrameInjectBase { private toggleClipper() { if (this.frame.style.display === "none") { this.frame.style.display = ""; + if (document.body) { + document.body.setAttribute("aria-hidden", "true"); + } } this.uiCommunicator.callRemoteFunction(Constants.FunctionKeys.toggleClipper); } From 271fb4cac401a6fffe5a13b3718eea9b29176c97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:36:19 +0000 Subject: [PATCH 26/34] Add inert alongside aria-hidden on body to fully suppress Voice Access background numbering Agent-Logs-Url: https://github.com/OneNoteDev/WebClipper/sessions/b160429a-a946-4869-9c23-834931415ee1 Co-authored-by: KethanaReddy7 <257986085+KethanaReddy7@users.noreply.github.com> --- src/scripts/extensions/clipperInject.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/scripts/extensions/clipperInject.ts b/src/scripts/extensions/clipperInject.ts index 9517e08c..a493b4ca 100644 --- a/src/scripts/extensions/clipperInject.ts +++ b/src/scripts/extensions/clipperInject.ts @@ -87,9 +87,12 @@ export class ClipperInject extends FrameInjectBase { // The clipper frame is now visible; hide page body from assistive technologies // so that tools like Voice Access do not number background page controls. // The frame is appended to (not ), so marking - // only hides page content while keeping the clipper iframe accessible. + // and inert only hides page content while keeping the clipper iframe accessible. + // aria-hidden removes body from the AT tree; inert also disables pointer events + // so Voice Access cannot enumerate or activate background controls. if (document.body) { document.body.setAttribute("aria-hidden", "true"); + document.body.setAttribute("inert", ""); } this.logger = new CommunicatorLoggerPure(this.uiCommunicator); @@ -326,6 +329,7 @@ export class ClipperInject extends FrameInjectBase { this.frame.style.display = "none"; if (document.body) { document.body.removeAttribute("aria-hidden"); + document.body.removeAttribute("inert"); } }); @@ -388,6 +392,7 @@ export class ClipperInject extends FrameInjectBase { this.frame.style.display = ""; if (document.body) { document.body.setAttribute("aria-hidden", "true"); + document.body.setAttribute("inert", ""); } } this.uiCommunicator.callRemoteFunction(Constants.FunctionKeys.toggleClipper); 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 27/34] 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 28/34] 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 29/34] 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 30/34] 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