From 7f246ecb291e0f798bd5ec92c97a6008120e8cbf Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 24 Aug 2026 20:51:22 +0000 Subject: [PATCH] Generate anonymous table cells for non-table-internal children of tables --- packages/blitz-dom/src/layout/table.rs | 57 ++++++++------- .../blitz-tests/tests/table_anonymous_cell.rs | 72 +++++++++++++++++++ 2 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 tests/blitz-tests/tests/table_anonymous_cell.rs diff --git a/packages/blitz-dom/src/layout/table.rs b/packages/blitz-dom/src/layout/table.rs index 709178471..7c90ade80 100644 --- a/packages/blitz-dom/src/layout/table.rs +++ b/packages/blitz-dom/src/layout/table.rs @@ -259,24 +259,42 @@ pub(crate) fn collect_table_cells( } doc.nodes[node_id].children = children; } - DisplayInside::TableCell => { + // Table-cell children, plus non-table-internal children which, per the + // CSS tables spec, generate an anonymous table cell around them. + DisplayInside::TableCell + | DisplayInside::Flow + | DisplayInside::FlowRoot + | DisplayInside::Flex + | DisplayInside::Grid => { // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); + let is_cell = display.inside() == DisplayInside::TableCell; let stylo_style = &node.primary_styles().unwrap(); - let colspan: u16 = node - .attr(local_name!("colspan")) - .and_then(|val| val.parse().ok()) - .unwrap_or(1); - let rowspan: u16 = node - .attr(local_name!("rowspan")) - .and_then(|val| val.parse::().ok()) - .map(|v| v.clamp(1, 65534)) - .unwrap_or(1); + let colspan: u16 = if is_cell { + node.attr(local_name!("colspan")) + .and_then(|val| val.parse().ok()) + .unwrap_or(1) + } else { + 1 + }; + let rowspan: u16 = if is_cell { + node.attr(local_name!("rowspan")) + .and_then(|val| val.parse::().ok()) + .map(|v| v.clamp(1, 65534)) + .unwrap_or(1) + } else { + 1 + }; let mut style = stylo_taffy::to_taffy_style(stylo_style); - if first_cell_border.is_none() { + if is_cell && first_cell_border.is_none() { *first_cell_border = Some(stylo_style.clone_border()); } + // Cells occurring before any row are placed in an anonymous row + if *row == 0 { + *row = 1; + } + if *row == 1 { let column = match style.size.width.tag() { taffy::CompactLength::LENGTH_TAG => { @@ -308,12 +326,14 @@ pub(crate) fn collect_table_cells( // Zero-out cell borders is BorderCollapse is Collapse // Borders are handled at the table level in this mode - if border_collapse == BorderCollapse::Collapse { + if is_cell && border_collapse == BorderCollapse::Collapse { style.border = taffy::Rect::ZERO.map(style_helpers::length); } // The margin properties do not apply to table-internal elements - style.margin = taffy::Rect::ZERO.map(style_helpers::length); + if is_cell { + style.margin = taffy::Rect::ZERO.map(style_helpers::length); + } // Let Taffy auto-place the column. Combined with // `grid_auto_flow: RowDense` set on the table root, each cell @@ -333,17 +353,6 @@ pub(crate) fn collect_table_cells( *col += colspan; } - DisplayInside::Flow - | DisplayInside::FlowRoot - | DisplayInside::Flex - | DisplayInside::Grid => { - node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - // Probably a table caption: ignore - // println!( - // "Warning: ignoring non-table typed descendent of table ({:?})", - // display.inside() - // ); - } DisplayInside::TableColumnGroup | DisplayInside::TableColumn | DisplayInside::Table => { node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); //Ignore diff --git a/tests/blitz-tests/tests/table_anonymous_cell.rs b/tests/blitz-tests/tests/table_anonymous_cell.rs new file mode 100644 index 000000000..c65439faf --- /dev/null +++ b/tests/blitz-tests/tests/table_anonymous_cell.rs @@ -0,0 +1,72 @@ +//! Non-table-internal children of a `display: table` element generate an +//! anonymous table cell (CSS 2.2 ยง17.2.1), so they must be laid out and +//! hit-testable. Regression test for the gov.uk search box, whose input +//! sits inside plain block `
`s that are direct children of a +//! `display: table` wrapper. + +use blitz_test_harness::{Harness, HarnessOptions}; + +fn harness(html: &str) -> Harness { + Harness::from_html_with( + html, + HarnessOptions { + width: 400, + height: 200, + ..Default::default() + }, + ) +} + +#[test] +fn block_children_of_table_are_laid_out_as_anonymous_cells() { + let harness = harness( + r#" +
+
+ +
+
+ +
+
+ "#, + ); + + let wrapper_rect = harness.layout_rect("#input-wrapper"); + assert!( + wrapper_rect.width > 0.0 && wrapper_rect.height > 0.0, + "block child of table should be laid out, got {wrapper_rect:?}" + ); + + let input_rect = harness.layout_rect("#search"); + assert!( + input_rect.width > 0.0 && input_rect.height > 0.0, + "input inside table's block child should be laid out, got {input_rect:?}" + ); + + let input = harness.node("#search"); + let (cx, cy) = harness.center_of("#search"); + assert_eq!( + harness.hit_node(cx, cy), + input, + "input inside table's block child should be hit-testable" + ); +} + +#[test] +fn clicking_input_inside_table_block_child_focuses_it() { + let mut harness = harness( + r#" +
+
+ +
+
+ "#, + ); + + let input = harness.node("#search"); + let (cx, cy) = harness.center_of("#search"); + harness.click_at(cx, cy); + assert_eq!(harness.focused(), Some(input)); +}