Skip to content

Commit 20594c7

Browse files
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>
1 parent 5431c04 commit 20594c7

2 files changed

Lines changed: 115 additions & 6 deletions

File tree

src/scripts/clipperUI/components/sectionPicker.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,52 @@ export class SectionPickerClass extends ComponentBase<SectionPickerState, Sectio
5858
this.props.onPopupToggle(shouldNowBeOpen);
5959
if (shouldNowBeOpen) {
6060
// After the popup renders, move keyboard focus to the currently selected section item
61-
// so that keyboard users can identify which item is selected and navigate from there
61+
// so that keyboard users can identify which item is selected and navigate from there.
62+
// Also attach an arrow key handler so keyboard users can navigate the list with Up/Down.
6263
setTimeout(() => {
64+
let sectionPickerPopup = document.getElementById("sectionPickerContainer");
65+
6366
let curSectionId = this.state.curSection && this.state.curSection.section ? this.state.curSection.section.id : undefined;
6467
let elementToFocus: HTMLElement;
6568
if (curSectionId) {
6669
elementToFocus = document.getElementById(curSectionId) as HTMLElement;
6770
}
68-
if (!elementToFocus) {
71+
if (!elementToFocus && sectionPickerPopup) {
6972
// Fall back to the first keyboard-navigable item in the section picker popup
70-
let sectionPickerPopup = document.getElementById("sectionPickerContainer");
71-
if (sectionPickerPopup) {
72-
elementToFocus = sectionPickerPopup.querySelector("[tabindex]:not([tabindex=\"-1\"])") as HTMLElement;
73-
}
73+
elementToFocus = sectionPickerPopup.querySelector("[tabindex]:not([tabindex=\"-1\"])") as HTMLElement;
7474
}
7575
if (elementToFocus) {
7676
elementToFocus.focus();
7777
}
78+
79+
// Attach Up/Down arrow key navigation for the popup list.
80+
// The OneNotePicker library only handles Enter/Tab, so we add arrow key support here.
81+
// The listener is attached to the popup element which is removed from the DOM when the
82+
// popup closes, so there is no need to explicitly clean it up.
83+
// Guard against attaching multiple listeners if onPopupToggle(true) is called more than once.
84+
if (sectionPickerPopup && !sectionPickerPopup.getAttribute("data-arrow-key-handler-attached")) {
85+
sectionPickerPopup.setAttribute("data-arrow-key-handler-attached", "true");
86+
sectionPickerPopup.addEventListener("keydown", (e: KeyboardEvent) => {
87+
if (e.which !== Constants.KeyCodes.up && e.which !== Constants.KeyCodes.down) {
88+
return;
89+
}
90+
e.preventDefault();
91+
let focusableItems = Array.from(
92+
sectionPickerPopup.querySelectorAll("[tabindex]:not([tabindex=\"-1\"])")
93+
) as HTMLElement[];
94+
if (focusableItems.length === 0) {
95+
return;
96+
}
97+
let currentIndex = focusableItems.indexOf(document.activeElement as HTMLElement);
98+
if (e.which === Constants.KeyCodes.up) {
99+
let prevIndex = currentIndex <= 0 ? 0 : currentIndex - 1;
100+
focusableItems[prevIndex].focus();
101+
} else {
102+
let nextIndex = currentIndex >= focusableItems.length - 1 ? focusableItems.length - 1 : currentIndex + 1;
103+
focusableItems[nextIndex].focus();
104+
}
105+
});
106+
}
78107
}, 0);
79108
}
80109
}

src/tests/clipperUI/components/sectionPicker_tests.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,86 @@ export class SectionPickerTests extends TestModule {
370370
clock.restore();
371371
done();
372372
});
373+
374+
test("onPopupToggle should enable Down arrow key to move focus to the next item in the popup", (assert: QUnitAssert) => {
375+
let done = assert.async();
376+
let clock = sinon.useFakeTimers();
377+
378+
let clipperState = MockProps.getMockClipperState();
379+
initializeClipperStorage(undefined, undefined);
380+
381+
let component = <SectionPicker onPopupToggle={() => {}} clipperState={clipperState} />;
382+
let controllerInstance = MithrilUtils.mountToFixture(component);
383+
384+
// Create a fake popup with two items
385+
let sectionPickerPopup = document.createElement("div");
386+
sectionPickerPopup.id = "sectionPickerContainer";
387+
let firstItem = document.createElement("li");
388+
firstItem.tabIndex = 70;
389+
let secondItemFocusCalled = false;
390+
let secondItem = document.createElement("li");
391+
secondItem.tabIndex = 70;
392+
secondItem.focus = () => { secondItemFocusCalled = true; };
393+
sectionPickerPopup.appendChild(firstItem);
394+
sectionPickerPopup.appendChild(secondItem);
395+
document.body.appendChild(sectionPickerPopup);
396+
397+
controllerInstance.onPopupToggle(true);
398+
clock.tick(0);
399+
400+
// Simulate focus on first item and press Down arrow
401+
firstItem.focus();
402+
let downKeyEvent = document.createEvent("KeyboardEvent");
403+
downKeyEvent.initEvent("keydown", true, true);
404+
Object.defineProperty(downKeyEvent, "which", { value: 40 });
405+
sectionPickerPopup.dispatchEvent(downKeyEvent);
406+
407+
ok(secondItemFocusCalled, "Down arrow key should move focus to the next item in the popup");
408+
409+
document.body.removeChild(sectionPickerPopup);
410+
clock.restore();
411+
done();
412+
});
413+
414+
test("onPopupToggle should enable Up arrow key to move focus to the previous item in the popup", (assert: QUnitAssert) => {
415+
let done = assert.async();
416+
let clock = sinon.useFakeTimers();
417+
418+
let clipperState = MockProps.getMockClipperState();
419+
initializeClipperStorage(undefined, undefined);
420+
421+
let component = <SectionPicker onPopupToggle={() => {}} clipperState={clipperState} />;
422+
let controllerInstance = MithrilUtils.mountToFixture(component);
423+
424+
// Create a fake popup with two items
425+
let sectionPickerPopup = document.createElement("div");
426+
sectionPickerPopup.id = "sectionPickerContainer";
427+
let firstItemFocusCalled = false;
428+
let firstItem = document.createElement("li");
429+
firstItem.tabIndex = 70;
430+
firstItem.focus = () => { firstItemFocusCalled = true; };
431+
let secondItem = document.createElement("li");
432+
secondItem.tabIndex = 70;
433+
sectionPickerPopup.appendChild(firstItem);
434+
sectionPickerPopup.appendChild(secondItem);
435+
document.body.appendChild(sectionPickerPopup);
436+
437+
controllerInstance.onPopupToggle(true);
438+
clock.tick(0);
439+
440+
// Simulate focus on second item and press Up arrow
441+
secondItem.focus();
442+
let upKeyEvent = document.createEvent("KeyboardEvent");
443+
upKeyEvent.initEvent("keydown", true, true);
444+
Object.defineProperty(upKeyEvent, "which", { value: 38 });
445+
sectionPickerPopup.dispatchEvent(upKeyEvent);
446+
447+
ok(firstItemFocusCalled, "Up arrow key should move focus to the previous item in the popup");
448+
449+
document.body.removeChild(sectionPickerPopup);
450+
clock.restore();
451+
done();
452+
});
373453
}
374454
}
375455

0 commit comments

Comments
 (0)