From a38d1122d8ee55c44c842fc8acddf9099e2108fa Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 4 Aug 2021 18:40:14 -0700 Subject: [PATCH] XFA - Support aria heading and table structure. (bug 1723421) (bug 1723425) https://bugzilla.mozilla.org/show_bug.cgi?id=1723421 https://bugzilla.mozilla.org/show_bug.cgi?id=1723425 --- src/core/xfa/template.js | 56 ++++++++++----- test/unit/xfa_tohtml_spec.js | 130 ++++++++++++++++++++++++++++++++++- 2 files changed, 167 insertions(+), 19 deletions(-) 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 = ` + + + + + + + + + `; + 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 = ` + + + + + + + + + `; + 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 = `