Skip to content

Commit 30ed527

Browse files
authored
Merge pull request #20677 from calixteman/bug2016007
Add the possibility to navigate with the keyboard to go from a checkbox to an other in the thumbnail view (bug 2016007)
2 parents f609ee8 + 0149527 commit 30ed527

4 files changed

Lines changed: 164 additions & 81 deletions

File tree

test/integration/thumbnail_view_spec.mjs

Lines changed: 114 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,6 @@ describe("PDF Thumbnail View", () => {
140140
await closePages(pages);
141141
});
142142

143-
async function isElementFocused(page, selector) {
144-
await page.waitForSelector(selector, { visible: true });
145-
146-
return page.$eval(selector, el => el === document.activeElement);
147-
}
148-
149143
it("should navigate with the keyboard", async () => {
150144
await Promise.all(
151145
pages.map(async ([browserName, page]) => {
@@ -156,57 +150,40 @@ describe("PDF Thumbnail View", () => {
156150
await waitForThumbnailVisible(page, 3);
157151

158152
await kbFocusNext(page);
159-
expect(await isElementFocused(page, "#viewsManagerSelectorButton"))
160-
.withContext(`In ${browserName}`)
161-
.toBe(true);
153+
await page.waitForSelector("#viewsManagerSelectorButton:focus", {
154+
visible: true,
155+
});
162156

163157
await kbFocusNext(page);
164-
expect(
165-
await isElementFocused(page, "#viewsManagerStatusActionButton")
166-
)
167-
.withContext(`In ${browserName}`)
168-
.toBe(true);
158+
await page.waitForSelector("#viewsManagerStatusActionButton:focus", {
159+
visible: true,
160+
});
169161

170162
await kbFocusNext(page);
171-
expect(
172-
await isElementFocused(
173-
page,
174-
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
175-
)
176-
)
177-
.withContext(`In ${browserName}`)
178-
.toBe(true);
163+
await page.waitForSelector(
164+
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']:focus`,
165+
{ visible: true }
166+
);
179167

180168
await page.keyboard.press("ArrowDown");
181-
expect(
182-
await isElementFocused(
183-
page,
184-
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":2}']`
185-
)
186-
)
187-
.withContext(`In ${browserName}`)
188-
.toBe(true);
169+
await page.waitForSelector(
170+
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":2}']:focus`,
171+
{ visible: true }
172+
);
189173

190174
await page.keyboard.press("ArrowUp");
191-
expect(
192-
await isElementFocused(
193-
page,
194-
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
195-
)
196-
)
197-
.withContext(`In ${browserName}`)
198-
.toBe(true);
175+
await page.waitForSelector(
176+
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']:focus`,
177+
{ visible: true }
178+
);
199179

200180
await page.keyboard.press("ArrowDown");
201181
await page.keyboard.press("ArrowDown");
202-
expect(
203-
await isElementFocused(
204-
page,
205-
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":3}']`
206-
)
207-
)
208-
.withContext(`In ${browserName}`)
209-
.toBe(true);
182+
await page.waitForSelector(
183+
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":3}']:focus`,
184+
{ visible: true }
185+
);
186+
210187
await page.keyboard.press("Enter");
211188
const currentPage = await page.$eval(
212189
"#pageNumber",
@@ -215,24 +192,16 @@ describe("PDF Thumbnail View", () => {
215192
expect(currentPage).withContext(`In ${browserName}`).toBe(3);
216193

217194
await page.keyboard.press("End");
218-
expect(
219-
await isElementFocused(
220-
page,
221-
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":14}']`
222-
)
223-
)
224-
.withContext(`In ${browserName}`)
225-
.toBe(true);
195+
await page.waitForSelector(
196+
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":14}']:focus`,
197+
{ visible: true }
198+
);
226199

227200
await page.keyboard.press("Home");
228-
expect(
229-
await isElementFocused(
230-
page,
231-
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
232-
)
233-
)
234-
.withContext(`In ${browserName}`)
235-
.toBe(true);
201+
await page.waitForSelector(
202+
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']:focus`,
203+
{ visible: true }
204+
);
236205
})
237206
);
238207
});
@@ -443,4 +412,87 @@ describe("PDF Thumbnail View", () => {
443412
);
444413
});
445414
});
415+
416+
describe("Checkbox keyboard navigation", () => {
417+
let pages;
418+
419+
beforeEach(async () => {
420+
pages = await loadAndWait(
421+
"tracemonkey.pdf",
422+
"#viewsManagerToggleButton",
423+
null,
424+
null,
425+
{ enableSplitMerge: true }
426+
);
427+
});
428+
429+
afterEach(async () => {
430+
await closePages(pages);
431+
});
432+
433+
it("should focus checkboxes with Tab key", async () => {
434+
await Promise.all(
435+
pages.map(async ([browserName, page]) => {
436+
await page.click("#viewsManagerToggleButton");
437+
438+
await waitForThumbnailVisible(page, 1);
439+
440+
// Focus the first thumbnail button
441+
await kbFocusNext(page);
442+
await kbFocusNext(page);
443+
await kbFocusNext(page);
444+
445+
// Verify we're on the first thumbnail
446+
await page.waitForSelector(
447+
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']:focus`,
448+
{ visible: true }
449+
);
450+
451+
// Tab to checkbox
452+
await kbFocusNext(page);
453+
await page.waitForSelector(
454+
`.thumbnail[page-number="1"] input[type="checkbox"]:focus`,
455+
{ visible: true }
456+
);
457+
})
458+
);
459+
});
460+
461+
it("should navigate checkboxes with arrow keys", async () => {
462+
await Promise.all(
463+
pages.map(async ([browserName, page]) => {
464+
await page.click("#viewsManagerToggleButton");
465+
466+
await waitForThumbnailVisible(page, 1);
467+
await waitForThumbnailVisible(page, 2);
468+
469+
// Navigate to first checkbox
470+
await kbFocusNext(page);
471+
await kbFocusNext(page);
472+
await kbFocusNext(page);
473+
await kbFocusNext(page);
474+
475+
// Verify first checkbox is focused
476+
await page.waitForSelector(
477+
`.thumbnail[page-number="1"] input[type="checkbox"]:focus`,
478+
{ visible: true }
479+
);
480+
481+
// Navigate to next checkbox with ArrowDown
482+
await page.keyboard.press("ArrowDown");
483+
await page.waitForSelector(
484+
`.thumbnail[page-number="2"] input[type="checkbox"]:focus`,
485+
{ visible: true }
486+
);
487+
488+
// Navigate back with ArrowUp
489+
await page.keyboard.press("ArrowUp");
490+
await page.waitForSelector(
491+
`.thumbnail[page-number="1"] input[type="checkbox"]:focus`,
492+
{ visible: true }
493+
);
494+
})
495+
);
496+
});
497+
});
446498
});

web/pdf_thumbnail_view.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,6 @@ class PDFThumbnailView extends RenderableView {
121121
thumbnailContainer.setAttribute("page-number", id);
122122
thumbnailContainer.setAttribute("page-id", id);
123123

124-
if (enableSplitMerge) {
125-
const checkbox = (this.checkbox = document.createElement("input"));
126-
checkbox.type = "checkbox";
127-
checkbox.tabIndex = -1;
128-
checkbox.setAttribute("data-l10n-id", "pdfjs-thumb-page-checkbox");
129-
checkbox.setAttribute("data-l10n-args", this.#pageL10nArgs);
130-
thumbnailContainer.append(checkbox);
131-
this.pasteButton = null;
132-
}
133-
134124
const imageContainer = (this.imageContainer =
135125
document.createElement("div"));
136126
thumbnailContainer.append(imageContainer);
@@ -145,6 +135,17 @@ class PDFThumbnailView extends RenderableView {
145135

146136
const image = (this.image = document.createElement("img"));
147137
imageContainer.append(image);
138+
139+
if (enableSplitMerge) {
140+
const checkbox = (this.checkbox = document.createElement("input"));
141+
checkbox.type = "checkbox";
142+
checkbox.tabIndex = -1;
143+
checkbox.setAttribute("data-l10n-id", "pdfjs-thumb-page-checkbox");
144+
checkbox.setAttribute("data-l10n-args", this.#pageL10nArgs);
145+
thumbnailContainer.append(checkbox);
146+
this.pasteButton = null;
147+
}
148+
148149
this.#updateDims();
149150

150151
container.append(thumbnailContainer);
@@ -271,9 +272,15 @@ class PDFThumbnailView extends RenderableView {
271272
if (isCurrent) {
272273
imageContainer.ariaCurrent = "page";
273274
imageContainer.tabIndex = 0;
275+
if (this.checkbox) {
276+
this.checkbox.tabIndex = 0;
277+
}
274278
} else {
275279
imageContainer.ariaCurrent = false;
276280
imageContainer.tabIndex = -1;
281+
if (this.checkbox) {
282+
this.checkbox.tabIndex = -1;
283+
}
277284
}
278285
}
279286

web/pdf_thumbnail_viewer.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -848,34 +848,41 @@ class PDFThumbnailViewer {
848848
}
849849
});
850850
this.container.addEventListener("keydown", e => {
851+
const { target } = e;
852+
const isCheckbox =
853+
target instanceof HTMLInputElement && target.type === "checkbox";
854+
851855
switch (e.key) {
852856
case "ArrowLeft":
853-
this.#goToNextItem(e.target, false, true);
857+
this.#goToNextItem(target, false, true, isCheckbox);
854858
stopEvent(e);
855859
break;
856860
case "ArrowRight":
857-
this.#goToNextItem(e.target, true, true);
861+
this.#goToNextItem(target, true, true, isCheckbox);
858862
stopEvent(e);
859863
break;
860864
case "ArrowDown":
861-
this.#goToNextItem(e.target, true, false);
865+
this.#goToNextItem(target, true, false, isCheckbox);
862866
stopEvent(e);
863867
break;
864868
case "ArrowUp":
865-
this.#goToNextItem(e.target, false, false);
869+
this.#goToNextItem(target, false, false, isCheckbox);
866870
stopEvent(e);
867871
break;
868872
case "Home":
869-
this._thumbnails[0].imageContainer.focus();
873+
this.#focusThumbnailElement(this._thumbnails[0], isCheckbox);
870874
stopEvent(e);
871875
break;
872876
case "End":
873-
this._thumbnails.at(-1).imageContainer.focus();
877+
this.#focusThumbnailElement(this._thumbnails.at(-1), isCheckbox);
874878
stopEvent(e);
875879
break;
876880
case "Enter":
877881
case " ":
878-
this.#goToPage(e);
882+
if (!isCheckbox) {
883+
this.#goToPage(e);
884+
}
885+
// For checkboxes, let the default behavior handle toggling
879886
break;
880887
}
881888
});
@@ -1048,13 +1055,29 @@ class PDFThumbnailViewer {
10481055
}
10491056
}
10501057

1058+
/**
1059+
* Focus either the checkbox or image of a thumbnail.
1060+
* @param {PDFThumbnailView} thumbnail
1061+
* @param {boolean} focusCheckbox - If true, focus checkbox; otherwise focus
1062+
* image
1063+
*/
1064+
#focusThumbnailElement(thumbnail, focusCheckbox) {
1065+
if (focusCheckbox && thumbnail.checkbox) {
1066+
thumbnail.checkbox.focus();
1067+
} else {
1068+
thumbnail.imageContainer.focus();
1069+
}
1070+
}
1071+
10511072
/**
10521073
* Go to the next/previous menu item.
10531074
* @param {HTMLElement} element
10541075
* @param {boolean} forward
10551076
* @param {boolean} horizontal
1077+
* @param {boolean} navigateCheckboxes - If true, focus checkboxes;
1078+
* otherwise focus images
10561079
*/
1057-
#goToNextItem(element, forward, horizontal) {
1080+
#goToNextItem(element, forward, horizontal, navigateCheckboxes = false) {
10581081
let currentPageNumber = parseInt(
10591082
element.parentElement.getAttribute("page-number"),
10601083
10
@@ -1097,7 +1120,7 @@ class PDFThumbnailViewer {
10971120
}
10981121
}
10991122
if (nextThumbnail) {
1100-
nextThumbnail.imageContainer.focus();
1123+
this.#focusThumbnailElement(nextThumbnail, navigateCheckboxes);
11011124
}
11021125
}
11031126

web/views_manager.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@
590590
display: inline-flex;
591591
justify-content: center;
592592
align-items: center;
593+
flex-direction: row-reverse;
593594
gap: 16px;
594595
width: auto;
595596
height: auto;

0 commit comments

Comments
 (0)