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##"
+
+
+
+
+
+
+
+ Section A: no links (written A1 A2 A3 A4, placed A2 A3 / A4 A1)
+
+
+
+
+ Section B: next links B1 to B3, B3 to B2, B2 to B4
+
+
+
+
+ Section C: the subform's first link goes to a label, which links to C3
+
+ C:
+
+
+
+
+ Section D: D2 hidden, D3 invisible, D4 read-only, D5 protected
+
+
+
+
+
+
+
+
+
+
+
+"##
+ .to_string()
+}
+
+/// The second Tab-order probe: in each row, the first field links to an object between the
+/// second and third that links on to the fourth: a hidden subform (E), a hidden field (F), an
+/// invisible field (G) and a protected field (H). Another viewer tabs it `start E1 E2 E3 E4 F1
+/// F4 G1 G4 H1 H4 finish F2 F3 G2 G3 H2 H3`, then back to `start`.
+pub fn traversal_targets_template() -> String {
+ r##"
+
+
+
+
+
+
+ Section E: E1 links to ET, a hidden subform between E2 and E3, which links to E4
+
+
+
+
+
+
+
+ Section F: F1 links to FT, a hidden field between F2 and F3, which links to F4
+
+
+
+
+
+ Section G: G1 links to GT, an invisible field between G2 and G3, which links to G4
+
+
+
+
+
+ Section H: H1 links to HT, a protected field between H2 and H3, which links to H4
+
+
+
+
+
+
+
+
+
+
+"##
+ .to_string()
+}
+
/// A scripted form (JavaScript events): `qty` (initialised to 2, validated ≤ 100) × `price`
/// (5) = `total` (calculated); a check box `more` that shows the hidden `details` subform;
/// a table whose `row` (item, amount) repeats, with `addRow`, `removeRow` and `hello` buttons;
diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs
index 6eac6757e0..24682caf9f 100644
--- a/crates/xfa/src/layout.rs
+++ b/crates/xfa/src/layout.rs
@@ -215,6 +215,16 @@ pub fn instance_count(occur: &Occur, from_data: usize) -> usize {
n.min(occur.max.unwrap_or(usize::MAX)).clamp(1, MAX_INSTANCES)
}
+/// How many instances of `sf` the layout makes under the container at `parent_som`: its `occur`
+/// and the instances `data` holds ([`instance_count`]). The traversal counts them the same way.
+pub(crate) fn subform_instances(tpl: &Template, data: Option<&DataNode>, sf: &Subform, parent_som: &str) -> usize {
+ let from_data = match (&sf.common.name, data) {
+ (Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&data_path(tpl, parent_som), name),
+ _ => 0,
+ };
+ instance_count(&sf.occur, from_data)
+}
+
/// Lay a template out, with the values and repeat counts of `data` (the `xfa:data` element).
pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result