Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/scripts/clipperUI/components/sectionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,32 @@ export class SectionPickerClass extends ComponentBase<SectionPickerState, Sectio
};
}


// Attach escape key handler to return focus to the dropdown button when Escape is pressed
// This is needed because the OneNotePicker component handles Escape internally without calling onPopupToggle
attachEscapeFocusHandler(element: HTMLElement, isInitialized: boolean) {
if (!isInitialized) {
const escKeyCode = 27;
const handleKeyDown = (ev: KeyboardEvent) => {
if (ev.keyCode === escKeyCode) {
// Check if the dropdown popup is currently visible
let sectionPickerPopup = document.querySelector(".SectionPickerPopup");
if (sectionPickerPopup) {
// The popup is open - schedule focus return after it closes
setTimeout(() => {
let locationButton = document.getElementById(Constants.Ids.sectionLocationContainer);
if (locationButton) {
locationButton.focus();
}
}, 10);
}
}
};
// Use capture phase to run before OneNotePicker's handler
document.addEventListener("keydown", handleKeyDown, true);
}
}

addSrOnlyLocationDiv(element: HTMLElement) {
const pickerLinkElement = document.getElementById(Constants.Ids.sectionLocationContainer);
if (!pickerLinkElement) {
Expand All @@ -246,6 +272,9 @@ export class SectionPickerClass extends ComponentBase<SectionPickerState, Sectio
srDiv.setAttribute("class", Constants.Classes.srOnly);
// Make srDiv the first child of pickerLinkElement
pickerLinkElement.insertBefore(srDiv, pickerLinkElement.firstChild);

// Attach escape key handler to return focus to the dropdown button
this.attachEscapeFocusHandler(element, false);
}

render() {
Expand Down
38 changes: 38 additions & 0 deletions src/tests/clipperUI/components/sectionPicker_tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,44 @@ export class SectionPickerTests extends TestModule {
let actual = SectionPickerClass.formatSectionInfoForStorage([]);
strictEqual(actual, undefined, "The section info should be formatted correctly");
});

test("onPopupToggle should restore focus to sectionLocationContainer when popup closes", (assert: QUnitAssert) => {
let done = assert.async();
let clipperState = MockProps.getMockClipperState();
let mockNotebooks = MockProps.getMockNotebooks();
initializeClipperStorage(JSON.stringify(mockNotebooks), undefined);

let popupToggleCalled = false;
let component = <SectionPicker
onPopupToggle={() => { popupToggleCalled = true; }}
clipperState={clipperState} />;
let controllerInstance = MithrilUtils.mountToFixture(component);

// Open the popup first
MithrilUtils.simulateAction(() => {
document.getElementById(TestConstants.Ids.sectionLocationContainer).click();
});

// Focus another element to simulate focus being elsewhere
let anotherElement = document.createElement("button");
document.body.appendChild(anotherElement);
anotherElement.focus();

// Close the popup by calling onPopupToggle with false
controllerInstance.onPopupToggle(false);

// Wait for the deferred focus (setTimeout) to execute
setTimeout(() => {
// Verify focus was restored to the location container
strictEqual(document.activeElement.id, TestConstants.Ids.sectionLocationContainer,
"Focus should be restored to sectionLocationContainer when popup closes");
ok(popupToggleCalled, "The parent onPopupToggle callback should have been called");

// Clean up
document.body.removeChild(anotherElement);
done();
}, 10);
});
}
}

Expand Down
Loading