From ea2e13370c24fb7ecf620f8c8ef087b267b3c621 Mon Sep 17 00:00:00 2001
From: Eric Kaden <608341+EricKaden@users.noreply.github.com>
Date: Tue, 10 Nov 2026 02:22:02 -0500
Subject: [PATCH] M12: dynamic XFA forms tab in their template's traversal
order (#4149)
---
crates/engine/src/tests.rs | 14 ++
crates/xfa/src/fixtures.rs | 93 ++++++++
crates/xfa/src/layout.rs | 12 +
crates/xfa/src/layout/flow.rs | 8 +-
crates/xfa/src/lib.rs | 1 +
crates/xfa/src/model.rs | 13 +
crates/xfa/src/parse.rs | 22 +-
crates/xfa/src/tests.rs | 161 ++++++++++++-
crates/xfa/src/traverse.rs | 435 ++++++++++++++++++++++++++++++++++
parity/acrobat-features.toml | 4 +-
10 files changed, 753 insertions(+), 10 deletions(-)
create mode 100644 crates/xfa/src/traverse.rs
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
index 741730453c..51018d6b37 100644
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -8550,6 +8550,20 @@ fn an_xfa_script_that_fails_partway_keeps_what_it_changed_before_the_error() {
s.apply(id, Edit::SetFieldValue { name: "leave".into(), value: FieldValue::Check(true) }).expect("can be checked now");
}
+/// A laid-out XFA form tabs the way its template's traversal says: the probe form, which
+/// another viewer tabs `start A2 A3 A4 A1 B1 B3 B2 B4 C3 D1 D4 D6 finish C1 C2`.
+#[test]
+fn xfa_forms_tab_in_their_templates_traversal_order() {
+ let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::traversal_template()));
+ let mut s = Session::new();
+ let id = s.open("tab.pdf", None, bytes, None).unwrap();
+ let doc = s.get(id).unwrap();
+ let mut tabs: Vec<(usize, &str)> = doc.form.iter().flat_map(|f| f.widgets.iter().map(move |w| (w.tab, f.name.as_str()))).collect();
+ tabs.sort_unstable();
+ let names: Vec<&str> = tabs.iter().map(|t| t.1).filter(|n| !["log", "D5"].contains(n)).collect();
+ assert_eq!(names, ["start", "A2", "A3", "A4", "A1", "B1", "B3", "B2", "B4", "C3", "D1", "D4", "D6", "finish", "C1", "C2"]);
+}
+
/// A scripted form end to end: initialize and calculate on open, recalculation after a change,
/// a failing validation's message, buttons that add and remove rows and show messages, a check
/// box that shows a subform (undoable), FormCalc, and a save and reopen that keep it all.
diff --git a/crates/xfa/src/fixtures.rs b/crates/xfa/src/fixtures.rs
index 0f6cc3a696..05f2aec654 100644
--- a/crates/xfa/src/fixtures.rs
+++ b/crates/xfa/src/fixtures.rs
@@ -286,6 +286,99 @@ pub fn unnamed_hidden_template() -> String {
.to_string()
}
+/// The Tab-order probe: fields placed out of document order (A), `next` links (B), a subform
+/// whose `first` link goes to a draw that links on (C), hidden, invisible, read-only and
+/// protected fields (D), and a protected `log` at the top. Another viewer tabs it `start A2 A3
+/// A4 A1 B1 B3 B2 B4 C3 D1 D4 D6 finish C1 C2`, then back to `start`.
+pub fn traversal_template() -> String {
+ r##"
+