diff --git a/test/integration/accessibility_spec.mjs b/test/integration/accessibility_spec.mjs index 41f84adb100da..2c04b5f00c0b3 100644 --- a/test/integration/accessibility_spec.mjs +++ b/test/integration/accessibility_spec.mjs @@ -498,4 +498,50 @@ describe("accessibility", () => { ); }); }); + + describe("Text elements must be aria-hidden when there's MathML and annotations", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("bug2009627.pdf", ".textLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the text in text layer is aria-hidden", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const isSanitizerSupported = await page.evaluate(() => { + try { + // eslint-disable-next-line no-undef + return typeof Sanitizer !== "undefined"; + } catch { + return false; + } + }); + const ariaHidden = await page.evaluate(() => + Array.from( + document.querySelectorAll(".structTree :has(> math)") + ).map(el => + document + .getElementById(el.getAttribute("aria-owns")) + .getAttribute("aria-hidden") + ) + ); + if (isSanitizerSupported) { + expect(ariaHidden) + .withContext(`In ${browserName}`) + .toEqual(["true", "true", "true"]); + } else { + // eslint-disable-next-line no-console + console.log( + `Pending in Chrome: Sanitizer API (in ${browserName}) is not supported` + ); + } + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 7b1a154a0ef1c..7c5d8ab4bd448 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -867,3 +867,4 @@ !bitmap-trailing-7fff-stripped.pdf !bitmap.pdf !bomb_giant.pdf +!bug2009627.pdf diff --git a/test/pdfs/bug2009627.pdf b/test/pdfs/bug2009627.pdf new file mode 100755 index 0000000000000..0b8189108f9b0 Binary files /dev/null and b/test/pdfs/bug2009627.pdf differ diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index fc75a9fd036ff..15968a8f28b51 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -493,7 +493,7 @@ class PDFPageView extends BasePDFPageView { const treeDom = await this.structTreeLayer?.render(); if (treeDom) { this.l10n.pause(); - this.structTreeLayer?.addElementsToTextLayer(); + this.structTreeLayer?.updateTextLayer(); if (this.canvas && treeDom.parentNode !== this.canvas) { // Pause translation when inserting the structTree in the DOM. this.canvas.append(treeDom); diff --git a/web/struct_tree_layer_builder.js b/web/struct_tree_layer_builder.js index a2d1524a79f8c..8f561eac5d319 100644 --- a/web/struct_tree_layer_builder.js +++ b/web/struct_tree_layer_builder.js @@ -184,6 +184,10 @@ class StructTreeLayerBuilder { #elementsToAddToTextLayer = null; + #elementsToHideInTextLayer = null; + + #elementsToStealFromTextLayer = null; + /** * @param {StructTreeLayerBuilderOptions} options */ @@ -304,15 +308,49 @@ class StructTreeLayerBuilder { return true; } - addElementsToTextLayer() { - if (!this.#elementsToAddToTextLayer) { - return; + updateTextLayer() { + if (this.#elementsToAddToTextLayer) { + for (const [id, img] of this.#elementsToAddToTextLayer) { + document.getElementById(id)?.append(img); + } + this.#elementsToAddToTextLayer.clear(); + this.#elementsToAddToTextLayer = null; } - for (const [id, img] of this.#elementsToAddToTextLayer) { - document.getElementById(id)?.append(img); + if (this.#elementsToHideInTextLayer) { + for (const id of this.#elementsToHideInTextLayer) { + const elem = document.getElementById(id); + if (elem) { + elem.ariaHidden = true; + } + } + this.#elementsToHideInTextLayer.length = 0; + this.#elementsToHideInTextLayer = null; + } + if (this.#elementsToStealFromTextLayer) { + for ( + let i = 0, ii = this.#elementsToStealFromTextLayer.length; + i < ii; + i += 2 + ) { + const element = this.#elementsToStealFromTextLayer[i]; + const ids = this.#elementsToStealFromTextLayer[i + 1]; + let textContent = ""; + for (const id of ids) { + const elem = document.getElementById(id); + if (elem) { + textContent += elem.textContent.trim() || ""; + // Aria-hide the element in order to avoid duplicate reading of the + // math content by screen readers. + elem.ariaHidden = "true"; + } + } + if (textContent) { + element.textContent = textContent; + } + } + this.#elementsToStealFromTextLayer.length = 0; + this.#elementsToStealFromTextLayer = null; } - this.#elementsToAddToTextLayer.clear(); - this.#elementsToAddToTextLayer = null; } #walk(node) { @@ -325,21 +363,13 @@ class StructTreeLayerBuilder { const { role } = node; if (MathMLElements.has(role)) { element = document.createElementNS(MathMLNamespace, role); - let text = ""; + const ids = []; + (this.#elementsToStealFromTextLayer ||= []).push(element, ids); for (const { type, id } of node.children || []) { - if (type !== "content" || !id) { - continue; + if (type === "content" && id) { + ids.push(id); } - const elem = document.getElementById(id); - if (!elem) { - continue; - } - text += elem.textContent.trim() || ""; - // Aria-hide the element in order to avoid duplicate reading of the - // math content by screen readers. - elem.ariaHidden = "true"; } - element.textContent = text; } else { element = document.createElement("span"); } @@ -365,10 +395,7 @@ class StructTreeLayerBuilder { if (!id) { continue; } - const elem = document.getElementById(id); - if (elem) { - elem.ariaHidden = true; - } + (this.#elementsToHideInTextLayer ||= []).push(id); } // For now, we don't want to keep the alt text if there's valid // MathML (see https://github.com/w3c/mathml-aam/issues/37).