diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js
index 9fb3bf6ee..3427405c5 100644
--- a/src/core/xfa/template.js
+++ b/src/core/xfa/template.js
@@ -121,6 +121,8 @@ const MAX_EMPTY_PAGES = 3;
// Default value to start with for the tabIndex property.
const DEFAULT_TAB_INDEX = 5000;
+const HEADING_PATTERN = /^H(\d+)$/;
+
function getBorderDims(node) {
if (!node || !node.border) {
return { w: 0, h: 0 };
@@ -210,6 +212,40 @@ function setTabIndex(node) {
}
}
+function applyAssist(obj, attributes) {
+ const assist = obj.assist;
+ if (assist) {
+ const assistTitle = assist[$toHTML]();
+ if (assistTitle) {
+ attributes.title = assistTitle;
+ }
+ const role = assist.role;
+ const match = role.match(HEADING_PATTERN);
+ if (match) {
+ const ariaRole = "heading";
+ const ariaLevel = match[1];
+ attributes.role = ariaRole;
+ attributes["aria-level"] = ariaLevel;
+ }
+ }
+ // XXX: We could end up in a situation where the obj has a heading role and
+ // is also a table. For now prioritize the table role.
+ if (obj.layout === "table") {
+ attributes.role = "table";
+ } else if (obj.layout === "row") {
+ attributes.role = "row";
+ } else {
+ const parent = obj[$getParent]();
+ if (parent.layout === "row") {
+ if (parent.assist && parent.assist.role === "TH") {
+ attributes.role = "columnheader";
+ } else {
+ attributes.role = "cell";
+ }
+ }
+ }
+}
+
function ariaLabel(obj) {
if (!obj.assist) {
return null;
@@ -1849,10 +1885,7 @@ class Draw extends XFAObject {
children: [],
};
- const assist = this.assist ? this.assist[$toHTML]() : null;
- if (assist) {
- html.attributes.title = assist;
- }
+ applyAssist(this, attributes);
const bbox = computeBbox(this, html, availableSpace);
@@ -2475,10 +2508,7 @@ class ExclGroup extends XFAObject {
children,
};
- const assist = this.assist ? this.assist[$toHTML]() : null;
- if (assist) {
- html.attributes.title = assist;
- }
+ applyAssist(this, attributes);
delete this[$extra];
@@ -2816,10 +2846,7 @@ class Field extends XFAObject {
children,
};
- const assist = this.assist ? this.assist[$toHTML]() : null;
- if (assist) {
- html.attributes.title = assist;
- }
+ applyAssist(this, attributes);
const borderStyle = this.border ? this.border[$toStyle]() : null;
const bbox = computeBbox(this, html, availableSpace);
@@ -5105,10 +5132,7 @@ class Subform extends XFAObject {
children,
};
- const assist = this.assist ? this.assist[$toHTML]() : null;
- if (assist) {
- html.attributes.title = assist;
- }
+ applyAssist(this, attributes);
const result = HTMLResult.success(createWrapper(this, html), bbox);
diff --git a/test/unit/xfa_tohtml_spec.js b/test/unit/xfa_tohtml_spec.js
index 0087365af..cb96d6469 100644
--- a/test/unit/xfa_tohtml_spec.js
+++ b/test/unit/xfa_tohtml_spec.js
@@ -17,15 +17,18 @@ import { isNodeJS } from "../../src/shared/is_node.js";
import { XFAFactory } from "../../src/core/xfa/factory.js";
describe("XFAFactory", function () {
- function searchHtmlNode(root, name, value) {
- if (root[name] === value) {
+ function searchHtmlNode(root, name, value, byAttributes = false) {
+ if (
+ (!byAttributes && root[name] === value) ||
+ (byAttributes && root.attributes && root.attributes[name] === value)
+ ) {
return root;
}
if (!root.children) {
return null;
}
for (const child of root.children) {
- const node = searchHtmlNode(child, name, value);
+ const node = searchHtmlNode(child, name, value, byAttributes);
if (node) {
return node;
}
@@ -177,6 +180,127 @@ describe("XFAFactory", function () {
expect(field.attributes.alt).toEqual("alt text");
});
+ it("should have a aria heading role and level", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ const factory = new XFAFactory({ "xdp:xdp": xml });
+
+ expect(factory.numberPages).toEqual(1);
+
+ const pages = factory.getPages();
+ const page1 = pages.children[0];
+ const wrapper = page1.children[0];
+ const draw = wrapper.children[0];
+
+ expect(draw.attributes.role).toEqual("heading");
+ expect(draw.attributes["aria-level"]).toEqual("2");
+ });
+
+ it("should have aria table role", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Header Col 1
+
+
+ Header Col 2
+
+
+
+
+ Cell 1
+
+
+ Cell 2
+
+
+
+
+
+
+
+
+
+
+ `;
+ const factory = new XFAFactory({ "xdp:xdp": xml });
+ factory.setFonts([]);
+
+ expect(factory.numberPages).toEqual(1);
+
+ const pages = factory.getPages();
+ const table = searchHtmlNode(
+ pages,
+ "xfaName",
+ "table",
+ /* byAttributes */ true
+ );
+ expect(table.attributes.role).toEqual("table");
+ const headerRow = searchHtmlNode(
+ pages,
+ "xfaName",
+ "row1",
+ /* byAttributes */ true
+ );
+ expect(headerRow.attributes.role).toEqual("row");
+ const headerCell = searchHtmlNode(
+ pages,
+ "xfaName",
+ "header2",
+ /* byAttributes */ true
+ );
+ expect(headerCell.attributes.role).toEqual("columnheader");
+ const row = searchHtmlNode(
+ pages,
+ "xfaName",
+ "row2",
+ /* byAttributes */ true
+ );
+ expect(row.attributes.role).toEqual("row");
+ const cell = searchHtmlNode(
+ pages,
+ "xfaName",
+ "cell2",
+ /* byAttributes */ true
+ );
+ expect(cell.attributes.role).toEqual("cell");
+ });
+
it("should have a maxLength property", function () {
const xml = `