Skip to content
Draft
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 38 additions & 1 deletion src/scripts/clipperUI/components/sectionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface SectionPickerProp extends ClipperStateProp {

export class SectionPickerClass extends ComponentBase<SectionPickerState, SectionPickerProp> {
static dataSource: OneNotePicker.OneNotePickerDataSource;
private popupIsOpen: boolean = false;
private escWasPressed: boolean = false;

getInitialState(): SectionPickerState {
return {
Expand All @@ -54,6 +56,20 @@ export class SectionPickerClass extends ComponentBase<SectionPickerState, Sectio
// If the user selects a section, onPopupToggle will fire because it closes the popup, even though it wasn't a click
// so logging only when they open it is potentially the next best thing
Clipper.logger.logClickEvent(Log.Click.Label.sectionPickerLocationContainer);
this.popupIsOpen = true;
} else {
this.popupIsOpen = false;
// If ESC was pressed to close the popup, restore focus to the trigger element
if (this.escWasPressed) {
this.escWasPressed = false;
// Delay focus restoration slightly to ensure the popup has fully closed
setTimeout(() => {
const sectionLocationContainer = document.getElementById(Constants.Ids.sectionLocationContainer);
if (sectionLocationContainer) {
sectionLocationContainer.focus();
}
}, 0);
}
}
this.props.onPopupToggle(shouldNowBeOpen);
}
Expand Down Expand Up @@ -248,6 +264,27 @@ export class SectionPickerClass extends ComponentBase<SectionPickerState, Sectio
pickerLinkElement.insertBefore(srDiv, pickerLinkElement.firstChild);
}

attachEscapeHandler(element: HTMLElement, isInitialized: boolean, context: any) {
if (!isInitialized) {
// Attach ESC key listener at initialization
let oldOnKeyDown = document.onkeydown;
document.onkeydown = (ev: KeyboardEvent) => {
if (ev.keyCode === Constants.KeyCodes.esc && this.popupIsOpen) {
// Mark that ESC was pressed so we can restore focus after the popup closes
this.escWasPressed = true;
}
if (oldOnKeyDown) {
oldOnKeyDown.call(document, ev);
}
};

// Remove listener when this element is unmounted
context.onunload = () => {
document.onkeydown = oldOnKeyDown ? oldOnKeyDown.bind(document) : undefined;
};
}
}

render() {
if (this.dataSourceUninitialized()) {
// This logic gets executed on app launch (if already signed in) and whenever the user signs in or out ...
Expand Down Expand Up @@ -295,7 +332,7 @@ export class SectionPickerClass extends ComponentBase<SectionPickerState, Sectio
let locationString = Localization.getLocalizedString("WebClipper.Label.ClipLocation");

return (
<div id={Constants.Ids.locationPickerContainer} {...this.onElementFirstDraw(this.addSrOnlyLocationDiv)}>
<div id={Constants.Ids.locationPickerContainer} {...this.onElementFirstDraw(this.addSrOnlyLocationDiv)} config={this.attachEscapeHandler.bind(this)}>
<div id={Constants.Ids.optionLabel} className="optionLabel">
<label htmlFor={Constants.Ids.sectionLocationContainer} aria-label={locationString} className="buttonLabelFont" style={Localization.getFontFamilyAsStyle(Localization.FontFamily.Regular)}>
<span aria-hidden="true">{locationString}</span>
Expand Down
67 changes: 67 additions & 0 deletions src/tests/clipperUI/components/sectionPicker_tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,73 @@ export class SectionPickerSinonTests extends TestModule {
done();
});
});

test("onPopupToggle should restore focus to the section location container when the popup closes via ESC key", (assert: QUnitAssert) => {
let done = assert.async();

let clipperState = MockProps.getMockClipperState();
let mockNotebooks = MockProps.getMockNotebooks();
initializeClipperStorage(JSON.stringify(mockNotebooks), undefined, TestConstants.defaultUserInfoAsJsonString);

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

// Simulate opening the popup
controllerInstance.onPopupToggle(true);
strictEqual(controllerInstance.popupIsOpen, true, "The popupIsOpen flag should be true when popup opens");

// Create and dispatch an ESC key event
let escEvent: KeyboardEvent;
try {
// Try modern KeyboardEvent constructor first
escEvent = new KeyboardEvent("keydown", {
keyCode: 27,
bubbles: true,
cancelable: true
} as any);
} catch (e) {
// Fallback to deprecated initKeyboardEvent for older test environments
if (document.createEvent) {
escEvent = document.createEvent("KeyboardEvent") as KeyboardEvent;
escEvent.initKeyboardEvent(
"keydown",
true,
true,
/* tslint:disable:no-null-keyword */
null,
/* tslint:enable:no-null-keyword */
false,
false,
false,
false,
27, // ESC key code
0);
}
}
document.dispatchEvent(escEvent);

// The escWasPressed flag should be set
strictEqual(controllerInstance.escWasPressed, true, "The escWasPressed flag should be true after ESC is pressed");

// Simulate the popup closing
controllerInstance.onPopupToggle(false);

// Use a short delay to allow focus restoration to complete
setTimeout(() => {
strictEqual(controllerInstance.popupIsOpen, false, "The popupIsOpen flag should be false when popup closes");
strictEqual(controllerInstance.escWasPressed, false, "The escWasPressed flag should be reset after focus restoration");

// Verify focus was set (in a real browser, document.activeElement would be the section container)
let sectionContainer = document.getElementById(TestConstants.Ids.sectionLocationContainer);
if (sectionContainer) {
// In test environment, just verify the element exists and is accessible
ok(sectionContainer, "The section location container should exist and be accessible for focus");
}
done();
}, 10);
});
}
}

Expand Down