Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Editor] Make the stamp annotations alt text readable by either VO or NVDA (bug 1912001) #18658

Merged
merged 1 commit into from
Sep 3, 2024
Merged
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
21 changes: 18 additions & 3 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
/** @typedef {import("../../web/interfaces").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
// eslint-disable-next-line max-len
/** @typedef {import("../../web/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */

import {
AnnotationBorderStyleType,
Expand Down Expand Up @@ -2952,6 +2954,7 @@ class StampAnnotationElement extends AnnotationElement {

render() {
this.container.classList.add("stampAnnotation");
this.container.setAttribute("role", "img");

if (!this.data.popupRef && this.hasPopupData) {
this._createPopup();
Expand Down Expand Up @@ -3059,6 +3062,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
* @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
* @property {StructTreeLayerBuilder} [structTreeLayer]
*/

/**
Expand All @@ -3071,17 +3075,21 @@ class AnnotationLayer {

#editableAnnotations = new Map();

#structTreeLayer = null;

constructor({
div,
accessibilityManager,
annotationCanvasMap,
annotationEditorUIManager,
page,
viewport,
structTreeLayer,
}) {
this.div = div;
this.#accessibilityManager = accessibilityManager;
this.#annotationCanvasMap = annotationCanvasMap;
this.#structTreeLayer = structTreeLayer || null;
this.page = page;
this.viewport = viewport;
this.zIndex = 0;
Expand All @@ -3104,9 +3112,16 @@ class AnnotationLayer {
return this.#editableAnnotations.size > 0;
}

#appendElement(element, id) {
async #appendElement(element, id) {
const contentElement = element.firstChild || element;
contentElement.id = `${AnnotationPrefix}${id}`;
const annotationId = (contentElement.id = `${AnnotationPrefix}${id}`);
const ariaAttributes =
await this.#structTreeLayer?.getAriaAttributes(annotationId);
if (ariaAttributes) {
for (const [key, value] of ariaAttributes) {
contentElement.setAttribute(key, value);
}
}

this.div.append(element);
this.#accessibilityManager?.moveElementInDOM(
Expand Down Expand Up @@ -3183,7 +3198,7 @@ class AnnotationLayer {
if (data.hidden) {
rendered.style.visibility = "hidden";
}
this.#appendElement(rendered, data.id);
await this.#appendElement(rendered, data.id);

if (element._isEditable) {
this.#editableAnnotations.set(element.data.id, element);
Expand Down
57 changes: 41 additions & 16 deletions test/integration/accessibility_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,26 @@
* limitations under the License.
*/

import { closePages, loadAndWait } from "./test_utils.mjs";
import {
awaitPromise,
closePages,
loadAndWait,
waitForPageRendered,
} from "./test_utils.mjs";

const isStructTreeVisible = async page => {
await page.waitForSelector(".structTree");
return page.evaluate(() => {
let elem = document.querySelector(".structTree");
while (elem) {
if (elem.getAttribute("aria-hidden") === "true") {
return false;
}
elem = elem.parentElement;
}
return true;
});
};

describe("accessibility", () => {
describe("structure tree", () => {
Expand All @@ -30,19 +49,9 @@ describe("accessibility", () => {
it("must build structure that maps to text layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".structTree");
const isVisible = await page.evaluate(() => {
let elem = document.querySelector(".structTree");
while (elem) {
if (elem.getAttribute("aria-hidden") === "true") {
return false;
}
elem = elem.parentElement;
}
return true;
});

expect(isVisible).withContext(`In ${browserName}`).toBeTrue();
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();

// Check the headings match up.
const head1 = await page.$eval(
Expand Down Expand Up @@ -77,6 +86,22 @@ describe("accessibility", () => {
})
);
});

it("must check that the struct tree is still there after zooming", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
for (let i = 0; i < 8; i++) {
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();

const handle = await waitForPageRendered(page);
await page.click(`#zoom${i < 4 ? "In" : "Out"}`);
await awaitPromise(handle);
}
})
);
});
});

describe("Annotation", () => {
Expand Down Expand Up @@ -184,10 +209,10 @@ describe("accessibility", () => {
it("must check the aria-label linked to the stamp annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".structTree");
await page.waitForSelector(".annotationLayer");

const ariaLabel = await page.$eval(
".structTree [role='figure']",
".annotationLayer section[role='img']",
el => el.getAttribute("aria-label")
);
expect(ariaLabel)
Expand Down
4 changes: 3 additions & 1 deletion web/annotation_layer_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ class AnnotationLayerBuilder {

/**
* @param {PageViewport} viewport
* @param {Object} options
* @param {string} intent (default value is 'display')
* @returns {Promise<void>} A promise that is resolved when rendering of the
* annotations is complete.
*/
async render(viewport, intent = "display") {
async render(viewport, options, intent = "display") {
if (this.div) {
if (this._cancelled || !this.annotationLayer) {
return;
Expand Down Expand Up @@ -136,6 +137,7 @@ class AnnotationLayerBuilder {
annotationEditorUIManager: this._annotationEditorUIManager,
page: this.pdfPage,
viewport: viewport.clone({ dontFlip: true }),
structTreeLayer: options?.structTreeLayer || null,
});

await this.annotationLayer.render({
Expand Down
26 changes: 15 additions & 11 deletions web/pdf_page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,11 @@ class PDFPageView {
async #renderAnnotationLayer() {
let error = null;
try {
await this.annotationLayer.render(this.viewport, "display");
await this.annotationLayer.render(
this.viewport,
{ structTreeLayer: this.structTreeLayer },
"display"
);
} catch (ex) {
console.error(`#renderAnnotationLayer: "${ex}".`);
error = ex;
Expand Down Expand Up @@ -468,16 +472,12 @@ class PDFPageView {
if (!this.textLayer) {
return;
}
this.structTreeLayer ||= new StructTreeLayerBuilder();

const tree = await (!this.structTreeLayer.renderingDone
? this.pdfPage.getStructTree()
: null);
const treeDom = this.structTreeLayer?.render(tree);
if (treeDom) {
const treeDom = await this.structTreeLayer?.render();
if (treeDom && this.canvas && treeDom.parentNode !== this.canvas) {
// Pause translation when inserting the structTree in the DOM.
this.l10n.pause();
this.canvas?.append(treeDom);
this.canvas.append(treeDom);
this.l10n.resume();
}
this.structTreeLayer?.show();
Expand Down Expand Up @@ -760,9 +760,6 @@ class PDFPageView {
this.textLayer.cancel();
this.textLayer = null;
}
if (this.structTreeLayer && !this.textLayer) {
this.structTreeLayer = null;
}
if (
this.annotationLayer &&
(!keepAnnotationLayer || !this.annotationLayer.div)
Expand All @@ -771,6 +768,9 @@ class PDFPageView {
this.annotationLayer = null;
this._annotationCanvasMap = null;
}
if (this.structTreeLayer && !(this.textLayer || this.annotationLayer)) {
this.structTreeLayer = null;
}
if (
this.annotationEditorLayer &&
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
Expand Down Expand Up @@ -1067,6 +1067,10 @@ class PDFPageView {
showCanvas?.(true);
await this.#finishRenderTask(renderTask);

if (this.textLayer || this.annotationLayer) {
this.structTreeLayer ||= new StructTreeLayerBuilder(pdfPage);
}

this.#renderTextLayer();

if (this.annotationLayer) {
Expand Down
39 changes: 33 additions & 6 deletions web/struct_tree_layer_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,29 @@ const PDF_ROLE_TO_HTML_ROLE = {
const HEADING_PATTERN = /^H(\d+)$/;

class StructTreeLayerBuilder {
#promise;

#treeDom = undefined;
calixteman marked this conversation as resolved.
Show resolved Hide resolved

get renderingDone() {
return this.#treeDom !== undefined;
#elementAttributes = new Map();

constructor(pdfPage) {
this.#promise = pdfPage.getStructTree();
}

render(structTree) {
async render() {
if (this.#treeDom !== undefined) {
return this.#treeDom;
}
const treeDom = this.#walk(structTree);
const treeDom = (this.#treeDom = this.#walk(await this.#promise));
this.#promise = null;
treeDom?.classList.add("structTree");
return (this.#treeDom = treeDom);
return treeDom;
}

async getAriaAttributes(annotationId) {
await this.render();
return this.#elementAttributes.get(annotationId);
}

hide() {
Expand All @@ -104,7 +114,24 @@ class StructTreeLayerBuilder {
#setAttributes(structElement, htmlElement) {
const { alt, id, lang } = structElement;
if (alt !== undefined) {
htmlElement.setAttribute("aria-label", removeNullCharacters(alt));
// Don't add the label in the struct tree layer but on the annotation
// in the annotation layer.
let added = false;
const label = removeNullCharacters(alt);
for (const child of structElement.children) {
if (child.type === "annotation") {
let attrs = this.#elementAttributes.get(child.id);
if (!attrs) {
attrs = new Map();
this.#elementAttributes.set(child.id, attrs);
}
attrs.set("aria-label", label);
added = true;
}
}
if (!added) {
htmlElement.setAttribute("aria-label", label);
}
}
if (id !== undefined) {
htmlElement.setAttribute("aria-owns", id);
Expand Down