Skip to content

Commit

Permalink
[Editor] Make the stamp annotations alt text readable by either VO or…
Browse files Browse the repository at this point in the history
… NVDA (bug 1912001)
  • Loading branch information
calixteman committed Sep 3, 2024
1 parent 0676ea1 commit deedbd1
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 37 deletions.
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;

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

0 comments on commit deedbd1

Please sign in to comment.