Skip to content

Commit 280a021

Browse files
committed
In tagged pdfs, TH can be either a column header or a row header (bug 2014080)
1 parent 58ac273 commit 280a021

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

test/integration/accessibility_spec.mjs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,4 +544,71 @@ describe("accessibility", () => {
544544
);
545545
});
546546
});
547+
548+
describe("A TH in a TR itself in a TBody is rowheader", () => {
549+
let pages;
550+
551+
beforeEach(async () => {
552+
pages = await loadAndWait("bug2014080.pdf", ".textLayer");
553+
});
554+
555+
afterEach(async () => {
556+
await closePages(pages);
557+
});
558+
559+
it("must check that the table has the right structure", async () => {
560+
await Promise.all(
561+
pages.map(async ([browserName, page]) => {
562+
let elementRole = await page.evaluate(() =>
563+
Array.from(
564+
document.querySelector(".structTree [role='table']").children
565+
).map(child => child.getAttribute("role"))
566+
);
567+
568+
// THeader and TBody must be rowgroup.
569+
expect(elementRole)
570+
.withContext(`In ${browserName}`)
571+
.toEqual(["rowgroup", "rowgroup"]);
572+
573+
elementRole = await page.evaluate(() =>
574+
Array.from(
575+
document.querySelector(
576+
".structTree [role='table'] > [role='rowgroup'] > [role='row']"
577+
).children
578+
).map(child => child.getAttribute("role"))
579+
);
580+
581+
// THeader has 3 columnheader.
582+
expect(elementRole)
583+
.withContext(`In ${browserName}`)
584+
.toEqual(["columnheader", "columnheader", "columnheader"]);
585+
586+
elementRole = await page.evaluate(() =>
587+
Array.from(
588+
document.querySelector(
589+
".structTree [role='table'] > [role='rowgroup']:nth-child(2)"
590+
).children
591+
).map(child => child.getAttribute("role"))
592+
);
593+
594+
// TBody has 5 rows.
595+
expect(elementRole)
596+
.withContext(`In ${browserName}`)
597+
.toEqual(["row", "row", "row", "row", "row"]);
598+
599+
elementRole = await page.evaluate(() =>
600+
Array.from(
601+
document.querySelector(
602+
".structTree [role='table'] > [role='rowgroup']:nth-child(2) > [role='row']:first-child"
603+
).children
604+
).map(child => child.getAttribute("role"))
605+
);
606+
// First row has a rowheader and 2 cells.
607+
expect(elementRole)
608+
.withContext(`In ${browserName}`)
609+
.toEqual(["rowheader", "cell", "cell"]);
610+
})
611+
);
612+
});
613+
});
547614
});

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,3 +872,4 @@
872872
!page_with_number_and_link.pdf
873873
!Brotli-Prototype-FileA.pdf
874874
!bug2013793.pdf
875+
!bug2014080.pdf

test/pdfs/bug2014080.pdf

32.5 KB
Binary file not shown.

web/struct_tree_layer_builder.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ const PDF_ROLE_TO_HTML_ROLE = {
6161
TR: "row",
6262
TH: "columnheader",
6363
TD: "cell",
64-
THead: "columnheader",
65-
TBody: null,
64+
THead: "rowgroup",
65+
TBody: "rowgroup",
6666
TFoot: null,
6767
// Standard structure type Caption
6868
Caption: null,
@@ -353,7 +353,7 @@ class StructTreeLayerBuilder {
353353
}
354354
}
355355

356-
#walk(node) {
356+
#walk(node, parentNodes = []) {
357357
if (!node) {
358358
return null;
359359
}
@@ -378,7 +378,14 @@ class StructTreeLayerBuilder {
378378
element.setAttribute("role", "heading");
379379
element.setAttribute("aria-level", match[1]);
380380
} else if (PDF_ROLE_TO_HTML_ROLE[role]) {
381-
element.setAttribute("role", PDF_ROLE_TO_HTML_ROLE[role]);
381+
element.setAttribute(
382+
"role",
383+
role === "TH" &&
384+
parentNodes.at(-1)?.role === "TR" &&
385+
parentNodes.at(-2)?.role === "TBody"
386+
? "rowheader" // TH inside TR itself in TBody is a rowheader.
387+
: PDF_ROLE_TO_HTML_ROLE[role]
388+
);
382389
}
383390
if (role === "Figure" && this.#addImageInTextLayer(node, element)) {
384391
return element;
@@ -423,9 +430,11 @@ class StructTreeLayerBuilder {
423430
// parent node to avoid creating an extra span.
424431
this.#setAttributes(node.children[0], element);
425432
} else {
433+
parentNodes.push(node);
426434
for (const kid of node.children) {
427-
element.append(this.#walk(kid));
435+
element.append(this.#walk(kid, parentNodes));
428436
}
437+
parentNodes.pop();
429438
}
430439
}
431440
return element;

0 commit comments

Comments
 (0)