Skip to content

Renames and API improvements for html table wrappers#24264

Open
mstahv wants to merge 12 commits intomainfrom
feature/proper-table-api-and-naming
Open

Renames and API improvements for html table wrappers#24264
mstahv wants to merge 12 commits intomainfrom
feature/proper-table-api-and-naming

Conversation

@mstahv
Copy link
Copy Markdown
Member

@mstahv mstahv commented May 5, 2026

Brings the API to the level of the original html-table add-on + some naming improvements.

Type of change

  • Bugfix
  • Feature

Usage example for the new API

package in.virit.views;

import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Table;
import com.vaadin.flow.component.html.TableColumn;
import com.vaadin.flow.component.html.TableColumnGroup;
import com.vaadin.flow.component.html.TableHead;
import com.vaadin.flow.component.html.TableHeaderCell;
import com.vaadin.flow.component.html.TableHeaderCell.Scope;
import com.vaadin.flow.component.html.TableRow;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import org.vaadin.firitin.util.VStyleUtil;

/**
 * Replicates the examples from the MDN "HTML table basics" tutorial:
 * https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Structuring_content/HTML_table_basics
 */
@Route
public class HtmlTableTutorialView extends VerticalLayout {

    public HtmlTableTutorialView() {
        VStyleUtil.injectAsFirst("""
                table {
                  border-collapse: collapse;
                  border: 2px solid rgb(200 200 200);
                  letter-spacing: 1px;
                  font-size: 0.8rem;
                }

                td,
                th {
                  border: 1px solid rgb(190 190 190);
                  padding: 10px 20px;
                }

                td {
                  text-align: center;
                }
                .column-background {
                  background-color: #97db9a;
                }

                .column-fixed-width {
                  width: 40px;
                }

                .column-background-border {
                  background-color: #dcc48e;
                  border: 4px solid #c1437a;
                }
                """);

        add(new H2("HTML table basics — examples from MDN"));

        add(new H3("1. Basic table with rows and cells"));
        add(new BasicTable());

        add(new H3("2. Adding headers with <th>"));
        add(new DogsTable());

        add(new H3("3. Allowing cells to span multiple rows and columns"));
        add(new AnimalsTable());

        add(new H3("4. Adding a caption with <caption>, plus <thead>/<tbody>"));
        add(new PlanetsTable());

        add(new H3("5. School timetable styled with <colgroup>/<col>"));
        add(new SchoolTimetable());
    }

    /** Example 1: minimal 2-row, 4-column table — no header. */
    static class BasicTable extends Table {{
        addRow("Hi, I'm your first cell.", "I'm your second cell.",
                "I'm your third cell.", "I'm your fourth cell.");
        addRow("Second row, first cell.", "Cell 2.", "Cell 3.", "Cell 4.");
    }}

    /** Example 2: row-headers and column-headers in the same table. */
    static class DogsTable extends Table {{
        TableRow headerRow = addRow();
        headerRow.addDataCell();
        for (String name : new String[] { "Knocky", "Flor", "Ella", "Juan" }) {
            headerRow.addHeaderCell(name).setScope(Scope.COL);
        }
        addRowWithRowHeader("Breed", "Jack Russell", "Poodle", "Streetdog",
                "Cocker Spaniel");
        addRowWithRowHeader("Age", "16", "9", "10", "5");
        addRowWithRowHeader("Owner", "Mother-in-law", "Me", "Me",
                "Sister-in-law");
        addRowWithRowHeader("Eating habits", "Eats everyone's leftovers",
                "Nibbles at food", "Hearty eater", "Will eat till he explodes");
    }

        private void addRowWithRowHeader(String header, String... data) {
            TableRow row = addRow();
            row.addHeaderCell(header).setScope(Scope.ROW);
            row.addDataCells(data);
        }
    }

    /** Example 3: colspan / rowspan on both {@code <td>} and {@code <th>}. */
    static class AnimalsTable extends Table {{
        addRow().addHeaderCell("Animals").setColspan(2);
        addRow().addHeaderCell("Hippopotamus").setColspan(2);

        TableRow horseRow = addRow();
        TableHeaderCell horse = horseRow.addHeaderCell("Horse");
        horse.setScope(Scope.ROW);
        horse.setRowspan(2);
        horseRow.addDataCell("Mare");
        addRow("Stallion");

        addRow().addHeaderCell("Crocodile").setColspan(2);

        TableRow chickenRow = addRow();
        TableHeaderCell chicken = chickenRow.addHeaderCell("Chicken");
        chicken.setScope(Scope.ROW);
        chicken.setRowspan(2);
        chickenRow.addDataCell("Hen");
        addRow("Rooster");
    }}

    /** Example 4: planet data with caption, thead and rowgroup spans. */
    static class PlanetsTable extends Table {{
        addCaption(new Html("<span>Data about the planets of our solar system"
                + " (Source: <a href=\"https://nssdc.gsfc.nasa.gov/planetary/"
                + "factsheet/\">Nasa's Planetary Fact Sheet - Metric</a>)."
                + "</span>"));

        TableHead head = getHead();
        TableRow headerRow = head.addRow();
        headerRow.addDataCell().setColspan(2);
        addColHeader(headerRow, new Html("<span>Name</span>"));
        addColHeader(headerRow, new Html("<span>Mass (10<sup>24</sup>kg)</span>"));
        addColHeader(headerRow, new Html("<span>Diameter (km)</span>"));
        addColHeader(headerRow, new Html("<span>Density (kg/m<sup>3</sup>)</span>"));
        addColHeader(headerRow, new Html("<span>Gravity (m/s<sup>2</sup>)</span>"));
        addColHeader(headerRow, new Html("<span>Length of day (hours)</span>"));
        addColHeader(headerRow,
                new Html("<span>Distance from Sun (10<sup>6</sup>km)</span>"));
        addColHeader(headerRow, new Html("<span>Mean temperature (°C)</span>"));
        addColHeader(headerRow, new Html("<span>Number of moons</span>"));
        addColHeader(headerRow, new Html("<span>Notes</span>"));

        TableRow mercury = addRow();
        TableHeaderCell terrestrial = mercury.addHeaderCell("Terrestrial planets");
        terrestrial.setScope(Scope.ROWGROUP);
        terrestrial.setColspan(2);
        terrestrial.setRowspan(4);
        addRowHeader(mercury, "Mercury");
        mercury.addDataCells("0.330", "4,879", "5427", "3.7", "4222.6", "57.9",
                "167", "0", "Closest to the Sun");

        TableRow venus = addRow();
        addRowHeader(venus, "Venus");
        venus.addDataCells("4.87", "12,104", "5243", "8.9", "2802.0", "108.2",
                "464", "0", "");

        TableRow earth = addRow();
        addRowHeader(earth, "Earth");
        earth.addDataCells("5.97", "12,756", "5514", "9.8", "24.0", "149.6",
                "15", "1", "Our world");

        TableRow mars = addRow();
        addRowHeader(mars, "Mars");
        mars.addDataCells("0.642", "6,792", "3933", "3.7", "24.7", "227.9",
                "-65", "2", "The red planet");

        TableRow jupiter = addRow();
        TableHeaderCell jovian = jupiter.addHeaderCell("Jovian planets");
        jovian.setScope(Scope.ROWGROUP);
        jovian.setRowspan(4);
        TableHeaderCell gasGiants = jupiter.addHeaderCell("Gas giants");
        gasGiants.setScope(Scope.ROWGROUP);
        gasGiants.setRowspan(2);
        addRowHeader(jupiter, "Jupiter");
        jupiter.addDataCells("1898", "142,984", "1326", "23.1", "9.9", "778.6",
                "-110", "67", "The largest planet");

        TableRow saturn = addRow();
        addRowHeader(saturn, "Saturn");
        saturn.addDataCells("568", "120,536", "687", "9.0", "10.7", "1433.5",
                "-140", "62", "");

        TableRow uranus = addRow();
        TableHeaderCell iceGiants = uranus.addHeaderCell("Ice giants");
        iceGiants.setScope(Scope.ROWGROUP);
        iceGiants.setRowspan(2);
        addRowHeader(uranus, "Uranus");
        uranus.addDataCells("86.8", "51,118", "1271", "8.7", "17.2", "2872.5",
                "-195", "27", "");

        TableRow neptune = addRow();
        addRowHeader(neptune, "Neptune");
        neptune.addDataCells("102", "49,528", "1638", "11.0", "16.1", "4495.1",
                "-200", "14", "");

        TableRow pluto = addRow();
        TableHeaderCell dwarf = pluto.addHeaderCell("Dwarf planets");
        dwarf.setScope(Scope.ROWGROUP);
        dwarf.setColspan(2);
        addRowHeader(pluto, "Pluto");
        pluto.addDataCells("0.0146", "2,370", "2095", "0.7", "153.3", "5906.4",
                "-225", "5",
                "Declassified as a planet in 2006, but this remains controversial.");
    }

        private static void addColHeader(TableRow row, Html content) {
            TableHeaderCell th = row.addHeaderCell();
            th.setScope(Scope.COL);
            th.add(content);
        }

        private static void addRowHeader(TableRow row, String text) {
            row.addHeaderCell(text).setScope(Scope.ROW);
        }
    }

    /**
     * Example 5: school timetable, demonstrating column-level styling via
     * {@code <colgroup>}/{@code <col>}.
     */
    static class SchoolTimetable extends Table {{
        // Mirrors the MDN colgroup: 2 plain cols, then a sequence of styled
        // cols, ending with a 2-col fixed-width group on the right.
        TableColumnGroup group = addColumnGroup();
        group.addColumn(2);
        group.addColumn().addClassName("column-background");
        group.addColumn().addClassName("column-fixed-width");
        group.addColumn().addClassName("column-background");
        group.addColumn().addClassName("column-background-border");
        TableColumn rightPair = group.addColumn(2);
        rightPair.addClassName("column-fixed-width");

        TableRow header = addRow();
        header.addDataCell();
        header.addHeaderCells("Mon", "Tues", "Wed", "Thurs", "Fri", "Sat",
                "Sun");

        addPeriodRow("1st period", "English", "", "", "German", "Dutch", "", "");
        addPeriodRow("2nd period", "English", "English", "", "German", "Dutch",
                "", "");
        addPeriodRow("3rd period", "", "German", "German", "", "Dutch", "", "");
        addPeriodRow("4th period", "", "English", "English", "", "Dutch", "", "");
    }

        private void addPeriodRow(String period, String... days) {
            TableRow row = addRow();
            row.addHeaderCell(period).setScope(Scope.ROW);
            row.addDataCells(days);
        }
    }

}

mstahv and others added 4 commits May 5, 2026 08:30
Adds a new Table component family (Table, TableBody, TableHead, TableFoot,
TableRow, TableHeaderCell, TableDataCell, TableCaption, TableRowContainer)
that exposes only spec-compliant operations, replacing the NativeTable
family which inherited a generic add(Component) API allowing structurally
invalid tables. The NativeTable* classes are deprecated in favor of the
new types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses gaps surfaced from early Table API use:

- Adds TableColumn (`<col>`) and TableColumnGroup (`<colgroup>`),
  wired into Table at the spec-correct position (after caption,
  before head). Multiple groups are supported.
- Introduces an abstract TableCell base class so colspan/rowspan now
  apply to TableHeaderCell as well as TableDataCell, matching the HTML
  spec (both `<td>` and `<th>` accept these attributes).
- Adds Table#addCaption(Component...) for richer captions and
  TableRow#addRowHeaderCell(String) shortcut for `<th scope="row">`,
  the most common row-label pattern in tutorials.
- Adds COL/COLGROUP Tag constants.
- Filters abstract HtmlComponent subclasses out of the smoke test so
  TableCell does not need a synthetic instantiation path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The HTML headers attribute lets a cell point at the ids of the header
cells that label it — assistive technologies use it to announce the
right headers when reading complex tables (MDN's accessibility section
relies on it). Until now users had to fall back to
getElement().setAttribute("headers", "…").

Adds to TableCell:
- setHeaders(String... ids)             primary form, space-joins ids
- setHeaders(TableHeaderCell... cells)  resolves ids from header cells
- getHeaders() returning Optional<String[]>
- resetHeaders()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Scope enum doc said only "specifies which cells the
header relates to" — useful but thin. MDN gives meaningful
per-value semantics, so this commit adopts that level of detail
and adds @see references to MDN element pages where appropriate.

- TableHeaderCell.Scope: per-value javadocs explaining what each
  scope keyword means (row/col/rowgroup/colgroup) and a note that
  AUTO writes the literal "auto" string which browsers treat as
  if the attribute were absent.
- TableHeaderCell, TableCell, TableColumn, TableColumnGroup, Table:
  richer class-level descriptions including accessibility purpose,
  CSS limitations on col/colgroup, browser limits for colspan/rowspan,
  and links to MDN.
- TableCell#setHeaders: spell out the accessibility role and how it
  complements the scope attribute.
- TableHeaderCell#setScope: emphasize accessibility impact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the +1.0.0 label May 5, 2026
Mostly line-wrapping touch-ups from spotless on files left over from
the original Table-API commit. Three files (TableBody, TableFoot,
TableHead) also needed a manual fix where spotless mangled
{@code <tr>} into a multi-line block with an orphan <tr> outside
the inline-code wrapper; rewritten as <code>&lt;tr&gt;</code> to be
robust to the formatter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Test Results

 1 409 files  + 12   1 409 suites  +12   1h 14m 53s ⏱️ -8s
10 298 tests +207  10 228 ✅ +207  70 💤 ±0  0 ❌ ±0 
10 773 runs  +207  10 694 ✅ +207  79 💤 ±0  0 ❌ ±0 

Results for commit bc0c26a. ± Comparison against base commit 724cc06.

♻️ This comment has been updated with latest results.

@knoobie
Copy link
Copy Markdown
Contributor

knoobie commented May 5, 2026

Did the component team gave up? :D #18553 (comment)

@mstahv
Copy link
Copy Markdown
Member Author

mstahv commented May 5, 2026

@knoobie, referring to the "naming" part ("Native" prefix)?

@mstahv
Copy link
Copy Markdown
Member Author

mstahv commented May 5, 2026

I would rename all other "Native*" things to "Html*" if there are conclicts. (I'm probably the one to blame of that original invention of "Native" prefix in V6 era and I don't like it 😊). If there are no conflicts, then package name is enough. Now, after some many years after Table in V7 & 8, I think there is no more reason for avoiding conclicts with those two.

@knoobie
Copy link
Copy Markdown
Contributor

knoobie commented May 5, 2026

I personally don't care :D All I want is consistency

mstahv and others added 7 commits May 5, 2026 10:59
The new TableRow API inherited the getAllCells() name from the
deprecated NativeTableRow, but the "All" prefix is asymmetric with
its singular pair getCell(int) and with the kind-specific pairs
(getDataCell/getDataCells, getHeaderCell/getHeaderCells). Plain
getCells() matches the standard Java collection naming and is
unambiguous since the cell types are already distinct classes.

Safe to rename since the new Table API is unreleased on this branch.
NativeTableRow.getAllCells() is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both walk every section in document order — head, then bodies in
order, then foot — matching the browser DOM's
HTMLTableElement.rows. Convenience for "iterate/count all rows" or
"clear the table" cases without forcing the caller to merge
section-specific lists. The section elements themselves are kept by
removeAllRows(); use removeHead/removeBody/removeFoot to drop them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that TableCell is the shared abstract superclass of TableDataCell
and TableHeaderCell, the row's getCells/getCell return types and
the constructor / addCells parameter types can be narrowed:

- getCells():       List<Component>     → List<TableCell>
- getCell(int):     Optional<Component> → Optional<TableCell>
- TableRow(...):    Component...        → TableCell...
- addCells(...):    Component...        → TableCell...

The runtime instanceof checks (and IllegalArgumentException paths)
in the constructor and addCells become unnecessary — the type system
enforces the constraint at compile time. Removed the two tests that
exercised those runtime guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The indexed get/remove methods duplicated what the corresponding
list accessors already provide via List.get(i):

- getCell(int), getDataCell(int), getHeaderCell(int)        removed
- removeCell(int), removeDataCell(int), removeHeaderCell(int) removed
- removeDataCell(TableDataCell), removeHeaderCell(TableHeaderCell)
  collapsed into a single removeCell(TableCell)

The only behavioral difference removed is Optional<T> on
out-of-bounds vs List.get's IndexOutOfBoundsException — minor, and
List.get's behavior is the standard one. Net effect: API is smaller
and more uniform; callers index into getCells() / getDataCells() /
getHeaderCells() directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tainer

Following the same trim applied to TableRow, removed methods that
duplicate what list accessors already provide:

TableRowContainer (TableHead/TableBody/TableFoot):
- getRow(int)        → getRows().get(i)
- removeRow(int)     → removeRows(getRows().get(i))
- getRowIndex(row)   → getRows().indexOf(row)
- getRowCount()      → getRows().size()
  (the long return type was an oddity vs List.size's int)

Table:
- getBody(int)       → getBodies().get(i)
  Also drops the surprising overload contract where getBody(0)
  *created* a body if none existed; the no-arg getBody() remains
  the documented "first or create" entry point used by addRow,
  addHeaderRow, addFooterRow and friends.
- removeBody(int)    → removeBody(getBodies().get(i))
- removeBody()       (no-arg, "remove first") — minor convenience

Tests updated to call the list equivalents; the obviously-redundant
test methods (getBodyByIndex, getNonExistentBodyByIndex,
removeBody, removeBodyByIndex, getRow, getNonExistentRow,
getRowIndex, removeRowByIndex, getRowCount) removed since their
behavior is now the standard List contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Relax the parameter type from TableCell... back to Component...,
but with smart handling: TableCell instances (TableDataCell or
TableHeaderCell) are placed as-is, anything else is wrapped in a
new TableDataCell. The structural invariant that <tr> only contains
<td>/<th> is still preserved at runtime — wrapping is a one-line
ergonomic shortcut so callers don't have to write
new TableDataCell(component) for every non-cell child.

Now this works:
    new TableRow(new Span("hi"), new TableHeaderCell("h"))
producing <tr><td><span>hi</span></td><th>h</th></tr>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each varargs method and constructor in the new Table API now has a
matching List overload, so callers holding a List<T> don't have to
convert it via toArray. The varargs version delegates to the List
version via Arrays.asList for a single source of truth.

Affected:
- Table: addCaption, addColumnGroup(TableColumn...), addRow,
  addRows, addHeaderRow, addHeaderRows, addFooterRow,
  addFooterRows
- TableRow: constructor, addDataCells, addHeaderCells, addCells
- TableRowContainer: addRows, removeRows
- TableColumnGroup: constructor, addColumns
- TableHead/Body/Foot: constructors
- TableDataCell, TableHeaderCell, TableCell (protected): constructors
- TableCell: setHeaders(String...) gains setHeaders(List<String>);
  setHeaders(TableHeaderCell...) gains setHeadersByCells(
  List<? extends TableHeaderCell>) — different name due to type
  erasure (List<String> and List<TableHeaderCell> share the same
  erased signature).

Smoke test special-cases the new setHeaders/setHeadersByCells
overloads since their parameter types don't match the Optional<String[]>
return of getHeaders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants