Merge pull request #20675 from calixteman/bug2015916

Fix the keyboard accessibility of the manage button in the thumbnails view (bug 2015916)
This commit is contained in:
Tim van der Meij 2026-02-17 20:03:19 +01:00 committed by GitHub
commit 5084e3d0ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 159 additions and 31 deletions

View File

@ -12,6 +12,21 @@ function waitForThumbnailVisible(page, pageNum) {
); );
} }
async function waitForMenu(page, buttonSelector, visible = true) {
return page.waitForFunction(
(selector, vis) => {
const button = document.querySelector(selector);
if (!button) {
return false;
}
return button.getAttribute("aria-expanded") === (vis ? "true" : "false");
},
{},
buttonSelector,
visible
);
}
describe("PDF Thumbnail View", () => { describe("PDF Thumbnail View", () => {
describe("Works without errors", () => { describe("Works without errors", () => {
let pages; let pages;
@ -201,4 +216,105 @@ describe("PDF Thumbnail View", () => {
); );
}); });
}); });
describe("The manage dropdown menu", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
"#viewsManagerToggleButton",
null,
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
async function enableMenuItems(page) {
await page.evaluate(() => {
document
.querySelectorAll("#viewsManagerStatusActionOptions button")
.forEach(button => {
button.disabled = false;
});
});
}
it("should open with Enter key and remain open", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#viewsManagerToggleButton");
await waitForThumbnailVisible(page, 1);
await enableMenuItems(page);
// Focus the manage button
await kbFocusNext(page);
await kbFocusNext(page);
await page.waitForSelector("#viewsManagerStatusActionButton:focus", {
visible: true,
});
// Press Enter to open the menu
await page.keyboard.press("Enter");
await waitForMenu(page, "#viewsManagerStatusActionButton");
// Verify first menu item can be focused
await page.waitForSelector("#viewsManagerStatusActionCopy:focus", {
visible: true,
});
// Close menu with Escape
await page.keyboard.press("Escape");
await waitForMenu(page, "#viewsManagerStatusActionButton", false);
})
);
});
it("should open with Space key and remain open", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#viewsManagerToggleButton");
await waitForThumbnailVisible(page, 1);
await enableMenuItems(page);
// Focus the manage button
await kbFocusNext(page);
await kbFocusNext(page);
await page.waitForSelector("#viewsManagerStatusActionButton:focus", {
visible: true,
});
// Press Space to open the menu
await page.keyboard.press(" ");
await waitForMenu(page, "#viewsManagerStatusActionButton");
// Verify first menu item can be focused
await page.waitForSelector("#viewsManagerStatusActionCopy:focus", {
visible: true,
});
// Navigate menu items with arrow keys
await page.keyboard.press("ArrowDown");
await page.waitForSelector("#viewsManagerStatusActionCut:focus", {
visible: true,
});
// Menu should still be open
await waitForMenu(page, "#viewsManagerStatusActionButton");
// Close menu with Escape
await page.keyboard.press("Escape");
await waitForMenu(page, "#viewsManagerStatusActionButton", false);
})
);
});
});
}); });

View File

@ -71,12 +71,10 @@ class Menu {
} }
/** /**
* Set up the menu. * Open the menu.
*/ */
#setUpMenu() { #openMenu() {
this.#triggeringButton.addEventListener("click", e => {
if (this.#openMenuAC) { if (this.#openMenuAC) {
this.#closeMenu();
return; return;
} }
@ -90,13 +88,29 @@ class Menu {
window.addEventListener( window.addEventListener(
"pointerdown", "pointerdown",
({ target }) => { ({ target }) => {
if (target !== this.#triggeringButton && !menu.contains(target)) { if (
!this.#triggeringButton.contains(target) &&
!menu.contains(target)
) {
this.#closeMenu(); this.#closeMenu();
} }
}, },
{ signal } { signal }
); );
window.addEventListener("blur", this.#closeMenu.bind(this), { signal }); window.addEventListener("blur", this.#closeMenu.bind(this), { signal });
}
/**
* Set up the menu.
*/
#setUpMenu() {
this.#triggeringButton.addEventListener("click", e => {
if (this.#openMenuAC) {
this.#closeMenu();
return;
}
this.#openMenu();
}); });
const { signal } = this.#menuAC; const { signal } = this.#menuAC;
@ -110,12 +124,10 @@ class Menu {
stopEvent(e); stopEvent(e);
break; break;
case "ArrowDown": case "ArrowDown":
case "Tab":
this.#goToNextItem(e.target, true); this.#goToNextItem(e.target, true);
stopEvent(e); stopEvent(e);
break; break;
case "ArrowUp": case "ArrowUp":
case "ShiftTab":
this.#goToNextItem(e.target, false); this.#goToNextItem(e.target, false);
stopEvent(e); stopEvent(e);
break; break;
@ -124,7 +136,7 @@ class Menu {
.find( .find(
item => !item.disabled && !item.classList.contains("hidden") item => !item.disabled && !item.classList.contains("hidden")
) )
.focus(); ?.focus();
stopEvent(e); stopEvent(e);
break; break;
case "End": case "End":
@ -132,7 +144,7 @@ class Menu {
.findLast( .findLast(
item => !item.disabled && !item.classList.contains("hidden") item => !item.disabled && !item.classList.contains("hidden")
) )
.focus(); ?.focus();
stopEvent(e); stopEvent(e);
break; break;
default: default:
@ -159,27 +171,27 @@ class Menu {
case "Enter": case "Enter":
case "ArrowDown": case "ArrowDown":
case "Home": case "Home":
stopEvent(e);
if (!this.#openMenuAC) { if (!this.#openMenuAC) {
this.#triggeringButton.click(); this.#openMenu();
} }
this.#menuItems this.#menuItems
.find( .find(
item => !item.disabled && !item.classList.contains("hidden") item => !item.disabled && !item.classList.contains("hidden")
) )
.focus(); ?.focus();
stopEvent(e);
break; break;
case "ArrowUp": case "ArrowUp":
case "End": case "End":
stopEvent(e);
if (!this.#openMenuAC) { if (!this.#openMenuAC) {
this.#triggeringButton.click(); this.#openMenu();
} }
this.#menuItems this.#menuItems
.findLast( .findLast(
item => !item.disabled && !item.classList.contains("hidden") item => !item.disabled && !item.classList.contains("hidden")
) )
.focus(); ?.focus();
stopEvent(e);
break; break;
case "Escape": case "Escape":
this.#closeMenu(); this.#closeMenu();

View File

@ -207,22 +207,22 @@ See https://github.com/adobe-type-tools/cmap-resources
</button> </button>
<menu id="viewsManagerStatusActionOptions" class="popupMenu"> <menu id="viewsManagerStatusActionOptions" class="popupMenu">
<li> <li>
<button id="viewsManagerStatusActionCopy" class="noIcon" role="menuitem" type="button" tabindex="0" disabled> <button id="viewsManagerStatusActionCopy" class="noIcon" role="menuitem" type="button" tabindex="-1" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-copy-button-label"></span> <span data-l10n-id="pdfjs-views-manager-pages-status-copy-button-label"></span>
</button> </button>
</li> </li>
<li> <li>
<button id="viewsManagerStatusActionCut" class="noIcon" role="menuitem" type="button" tabindex="0" disabled> <button id="viewsManagerStatusActionCut" class="noIcon" role="menuitem" type="button" tabindex="-1" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-cut-button-label"></span> <span data-l10n-id="pdfjs-views-manager-pages-status-cut-button-label"></span>
</button> </button>
</li> </li>
<li> <li>
<button id="viewsManagerStatusActionDelete" class="noIcon" role="menuitem" type="button" tabindex="0" disabled> <button id="viewsManagerStatusActionDelete" class="noIcon" role="menuitem" type="button" tabindex="-1" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-delete-button-label"></span> <span data-l10n-id="pdfjs-views-manager-pages-status-delete-button-label"></span>
</button> </button>
</li> </li>
<li> <li>
<button id="viewsManagerStatusActionSaveAs" class="noIcon" role="menuitem" type="button" tabindex="0" disabled> <button id="viewsManagerStatusActionSaveAs" class="noIcon" role="menuitem" type="button" tabindex="-1" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-save-as-button-label"></span> <span data-l10n-id="pdfjs-views-manager-pages-status-save-as-button-label"></span>
</button> </button>
</li> </li>