mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 14:48:08 +02:00
Merge pull request #18658 from calixteman/bug1912001
[Editor] Make the stamp annotations alt text readable by either VO or NVDA (bug 1912001)
This commit is contained in:
commit
d3698223a8
5 changed files with 110 additions and 37 deletions
|
@ -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,
|
||||
|
@ -2963,6 +2965,7 @@ class StampAnnotationElement extends AnnotationElement {
|
|||
|
||||
render() {
|
||||
this.container.classList.add("stampAnnotation");
|
||||
this.container.setAttribute("role", "img");
|
||||
|
||||
if (!this.data.popupRef && this.hasPopupData) {
|
||||
this._createPopup();
|
||||
|
@ -3070,6 +3073,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
|
|||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -3082,6 +3086,8 @@ class AnnotationLayer {
|
|||
|
||||
#editableAnnotations = new Map();
|
||||
|
||||
#structTreeLayer = null;
|
||||
|
||||
constructor({
|
||||
div,
|
||||
accessibilityManager,
|
||||
|
@ -3089,10 +3095,12 @@ class AnnotationLayer {
|
|||
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;
|
||||
|
@ -3115,9 +3123,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(
|
||||
|
@ -3194,7 +3209,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);
|
||||
|
|
|
@ -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", () => {
|
||||
|
@ -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(
|
||||
|
@ -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", () => {
|
||||
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue