diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java index cd526b4279c..21713146776 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java @@ -26,9 +26,19 @@ /** * Component representing a <table> element. + *

+ * Deprecated. This component extends {@link HtmlContainer} and therefore + * exposes a generic {@code add(Component)} API that allows constructing + * structurally invalid tables. Use {@link Table} instead, which extends + * {@link com.vaadin.flow.component.HtmlComponent} and exposes only + * spec-compliant operations (see + * WHATWG + * HTML). * * @since 24.4 + * @deprecated since 25.2; use {@link Table} instead. */ +@Deprecated @Tag(Tag.TABLE) public class NativeTable extends HtmlContainer implements ClickNotifier { diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java index fedbee72c76..eb92fd3d368 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java @@ -24,7 +24,9 @@ * Component representing a <tbody> element. * * @since 24.4 + * @deprecated since 25.2; use {@link TableBody} instead. */ +@Deprecated @Tag(Tag.TBODY) public class NativeTableBody extends HtmlContainer implements NativeTableRowContainer, ClickNotifier { diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java index 628056f39f0..81a4d0eee33 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java @@ -22,7 +22,9 @@ * Represents the table caption element ({@code }). * * @since 24.4 + * @deprecated since 25.2; use {@link TableCaption} instead. */ +@Deprecated @Tag(Tag.CAPTION) public class NativeTableCaption extends HtmlContainer { } diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java index 16eb226d858..eb24b4e2b14 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java @@ -27,7 +27,9 @@ * Component representing a <td> element. * * @since 24.4 + * @deprecated since 25.2; use {@link TableDataCell} instead. */ +@Deprecated @Tag(Tag.TD) public class NativeTableCell extends HtmlContainer implements ClickNotifier { diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java index 3ddd68c984f..2334a7066a2 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java @@ -24,7 +24,9 @@ * Component representing a <tfoot> element. * * @since 24.4 + * @deprecated since 25.2; use {@link TableFoot} instead. */ +@Deprecated @Tag(Tag.TFOOT) public class NativeTableFooter extends HtmlContainer implements NativeTableRowContainer, ClickNotifier { diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java index ad797a64e7b..892b92dd32a 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java @@ -24,7 +24,9 @@ * Component representing a <thead> element. * * @since 24.4 + * @deprecated since 25.2; use {@link TableHead} instead. */ +@Deprecated @Tag(Tag.THEAD) public class NativeTableHeader extends HtmlContainer implements NativeTableRowContainer, ClickNotifier { diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java index 81b4d8b03e4..5117651cdd0 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java @@ -27,7 +27,9 @@ * Component representing a <th> element. * * @since 24.4 + * @deprecated since 25.2; use {@link TableHeaderCell} instead. */ +@Deprecated @Tag(Tag.TH) public class NativeTableHeaderCell extends HtmlContainer implements ClickNotifier { diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java index 2d022697a76..e3d67036dcf 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java @@ -29,7 +29,9 @@ * Component representing a <tr> element. * * @since 24.4 + * @deprecated since 25.2; use {@link TableRow} instead. */ +@Deprecated @Tag(Tag.TR) public class NativeTableRow extends HtmlContainer implements HasOrderedComponents, ClickNotifier { diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java index 0fd3159f5f3..fcd1ad5824c 100644 --- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java @@ -26,7 +26,11 @@ * A container of <tr> elements. * * @since 24.4 + * @deprecated since 25.2; use {@link TableRowContainer} (and the corresponding + * {@link TableHead}, {@link TableBody}, and {@link TableFoot} + * components) instead. */ +@Deprecated interface NativeTableRowContainer extends HasOrderedComponents { /** diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/Table.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/Table.java new file mode 100644 index 00000000000..b563b03af75 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/Table.java @@ -0,0 +1,576 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HtmlComponent; +import com.vaadin.flow.component.Tag; + +/** + * Component representing a <table> element — a + * two-dimensional grid of cells with optional header, body and footer sections, + * captioning and column-level styling. + *

+ * Per the WHATWG + * HTML specification, a <table> may contain (in order): + * an optional <caption>, zero or more + * <colgroup> elements, an optional + * <thead>, zero or more <tbody> elements, + * and an optional <tfoot>. This component therefore extends + * {@link HtmlComponent} (rather than + * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only the + * structured operations required to build a valid table — child components are + * inserted at the correct position automatically. + * + * @see MDN: + * <table> — The Table element + * @since 25.2 + */ +@Tag(Tag.TABLE) +public class Table extends HtmlComponent implements ClickNotifier { + + private TableCaption caption; + private final List columnGroups = new LinkedList<>(); + private TableHead head; + private final List bodies = new LinkedList<>(); + private TableFoot foot; + + /** + * Creates a new empty table. + */ + public Table() { + super(); + } + + /** + * Return the table's caption component. Creates a new instance if no + * caption is present. + * + * @return the table's caption. + */ + public TableCaption getCaption() { + if (caption == null) { + caption = new TableCaption(); + getElement().insertChild(0, caption.getElement()); + } + return caption; + } + + /** + * Returns the caption component if one has been set. + * + * @return an {@link Optional} containing the caption, or empty if none. + */ + public Optional findCaption() { + return Optional.ofNullable(caption); + } + + /** + * Returns the caption text for this table, or an empty string if no caption + * has been set. + * + * @return the table's caption text. + */ + public String getCaptionText() { + return caption == null ? "" : caption.getText(); + } + + /** + * Sets the caption text for this table. Creates a caption element if none + * exists. + * + * @param text + * the caption's text + */ + public void setCaptionText(String text) { + getCaption().setText(text); + } + + /** + * Appends the given components to this table's caption, creating it if none + * exists yet. Useful for richer captions containing inline markup. + * + * @param components + * the components to append. + * @return the caption. + */ + public TableCaption addCaption(Component... components) { + return addCaption(Arrays.asList(components)); + } + + /** + * List equivalent of {@link #addCaption(Component...)}. + * + * @param components + * the components to append. + * @return the caption. + */ + public TableCaption addCaption(List components) { + TableCaption c = getCaption(); + c.add(components.toArray(Component[]::new)); + return c; + } + + /** + * Remove the caption from this table. + */ + public void removeCaption() { + if (caption != null) { + getElement().removeChild(caption.getElement()); + caption = null; + } + } + + /** + * Appends a new empty {@code } to this table. + * + * @return the newly created column group. + */ + public TableColumnGroup addColumnGroup() { + TableColumnGroup group = new TableColumnGroup(); + getElement().insertChild(columnGroupAppendIndex(), group.getElement()); + columnGroups.add(group); + return group; + } + + /** + * Appends an existing {@code } to this table. + * + * @param group + * the column group to add. + * @return the same group, for fluent chaining. + */ + public TableColumnGroup addColumnGroup(TableColumnGroup group) { + getElement().insertChild(columnGroupAppendIndex(), group.getElement()); + columnGroups.add(group); + return group; + } + + /** + * Appends a new {@code } populated with the given columns. + * + * @param columns + * the columns to place inside the new group. + * @return the newly created column group. + */ + public TableColumnGroup addColumnGroup(TableColumn... columns) { + return addColumnGroup(Arrays.asList(columns)); + } + + /** + * List equivalent of {@link #addColumnGroup(TableColumn...)}. + * + * @param columns + * the columns to place inside the new group. + * @return the newly created column group. + */ + public TableColumnGroup addColumnGroup( + List columns) { + TableColumnGroup group = new TableColumnGroup(); + getElement().insertChild(columnGroupAppendIndex(), group.getElement()); + columnGroups.add(group); + group.addColumns(columns); + return group; + } + + /** + * Returns the column groups attached to this table, in document order. + * + * @return an unmodifiable list of column groups. + */ + public List getColumnGroups() { + return Collections.unmodifiableList(new ArrayList<>(columnGroups)); + } + + /** + * Removes a column group from this table. + * + * @param group + * the group to remove. + */ + public void removeColumnGroup(TableColumnGroup group) { + if (columnGroups.remove(group)) { + getElement().removeChild(group.getElement()); + } + } + + /** + * Returns the head of this table. Creates a new one if none was present, + * inserted at the correct position (after the caption and any column + * groups). + * + * @return this table's {@code } element. + */ + public TableHead getHead() { + if (head == null) { + head = new TableHead(); + getElement().insertChild(headIndex(), head.getElement()); + } + return head; + } + + /** + * Returns the head if one has been set. + * + * @return an {@link Optional} containing the head, or empty if none. + */ + public Optional findHead() { + return Optional.ofNullable(head); + } + + /** + * Remove the head from this table, if present. + */ + public void removeHead() { + if (head != null) { + getElement().removeChild(head.getElement()); + head = null; + } + } + + /** + * Returns the {@code } element of this table. Creates a new one if + * none was present, appended at the end of the table per the HTML spec. + * + * @return the {@code } element of this table. + */ + public TableFoot getFoot() { + if (foot == null) { + foot = new TableFoot(); + getElement().appendChild(foot.getElement()); + } + return foot; + } + + /** + * Returns the foot if one has been set. + * + * @return an {@link Optional} containing the foot, or empty if none. + */ + public Optional findFoot() { + return Optional.ofNullable(foot); + } + + /** + * Removes the foot from this table, if present. + */ + public void removeFoot() { + if (foot != null) { + getElement().removeChild(foot.getElement()); + foot = null; + } + } + + /** + * Returns the list of {@code } elements in this table. + * + * @return an unmodifiable list of body elements. + */ + public List getBodies() { + return Collections.unmodifiableList(new ArrayList<>(bodies)); + } + + /** + * Returns the first body element in this table. Creates one if there's + * none. + * + * @return the first {@code } element in the table. + */ + public TableBody getBody() { + if (bodies.isEmpty()) { + return addBody(); + } + return bodies.get(0); + } + + /** + * Adds a new body element to the table, positioned after the existing + * bodies and before the foot (if any). + * + * @return the new body. + */ + public TableBody addBody() { + TableBody body = new TableBody(); + getElement().insertChild(bodyAppendIndex(), body.getElement()); + bodies.add(body); + return body; + } + + /** + * Removes a body element from the table. + * + * @param body + * the body component to remove. + */ + public void removeBody(TableBody body) { + if (bodies.remove(body)) { + getElement().removeChild(body.getElement()); + } + } + + /** + * Returns every {@link TableRow} in this table — the head's rows, then the + * rows of each body in order, then the foot's rows — matching the document + * order exposed by the browser DOM's {@code HTMLTableElement.rows}. Useful + * for "iterate all rows" or "count rows" cases; for structural work go + * through {@link #getHead()}, {@link #getBody()} or {@link #getFoot()} + * directly. + * + * @return an unmodifiable list of all rows in the table. + */ + public List getRows() { + List all = new ArrayList<>(); + if (head != null) { + all.addAll(head.getRows()); + } + for (TableBody body : bodies) { + all.addAll(body.getRows()); + } + if (foot != null) { + all.addAll(foot.getRows()); + } + return Collections.unmodifiableList(all); + } + + /** + * Removes every row from this table's head, bodies and foot. The section + * elements themselves ({@code }, {@code }, {@code }) + * and any column groups are kept; use {@link #removeHead()}, + * {@link #removeBody(TableBody)} or {@link #removeFoot()} to drop those. + */ + public void removeAllRows() { + if (head != null) { + head.removeAllRows(); + } + for (TableBody body : bodies) { + body.removeAllRows(); + } + if (foot != null) { + foot.removeAllRows(); + } + } + + /** + * Appends a new empty row to this table's body, creating an implicit + * {@code } if none exists yet. Mirrors the HTML pattern of placing + * <tr> elements directly inside a + * <table> (the browser auto-wraps them in + * {@code }). + * + * @return the newly created row. + */ + public TableRow addRow() { + return getBody().addRow(); + } + + /** + * Appends a new row containing the given texts as data cells + * (<td>) to this table's body, creating an implicit + * {@code } if none exists yet. + * + * @param cellTexts + * the text content for each data cell. + * @return the newly created row. + */ + public TableRow addRow(String... cellTexts) { + return addRow(Arrays.asList(cellTexts)); + } + + /** + * List equivalent of {@link #addRow(String...)}. + * + * @param cellTexts + * the text content for each data cell. + * @return the newly created row. + */ + public TableRow addRow(List cellTexts) { + TableRow row = getBody().addRow(); + row.addDataCells(cellTexts); + return row; + } + + /** + * Appends the given rows to this table's body, creating an implicit + * {@code } if none exists yet. + * + * @param rows + * the rows to add. + */ + public void addRows(TableRow... rows) { + getBody().addRows(rows); + } + + /** + * List equivalent of {@link #addRows(TableRow...)}. + * + * @param rows + * the rows to add. + */ + public void addRows(List rows) { + getBody().addRows(rows); + } + + /** + * Appends a new empty row to this table's {@code }, creating it if + * none exists yet. + * + * @return the newly created row. + */ + public TableRow addHeaderRow() { + return getHead().addRow(); + } + + /** + * Appends a new row containing the given texts as header cells + * (<th>) to this table's {@code }, creating it if + * none exists yet. + * + * @param cellTexts + * the text content for each header cell. + * @return the newly created row. + */ + public TableRow addHeaderRow(String... cellTexts) { + return addHeaderRow(Arrays.asList(cellTexts)); + } + + /** + * List equivalent of {@link #addHeaderRow(String...)}. + * + * @param cellTexts + * the text content for each header cell. + * @return the newly created row. + */ + public TableRow addHeaderRow(List cellTexts) { + TableRow row = getHead().addRow(); + row.addHeaderCells(cellTexts); + return row; + } + + /** + * Appends the given rows to this table's {@code }, creating it if + * none exists yet. + * + * @param rows + * the rows to add. + */ + public void addHeaderRows(TableRow... rows) { + getHead().addRows(rows); + } + + /** + * List equivalent of {@link #addHeaderRows(TableRow...)}. + * + * @param rows + * the rows to add. + */ + public void addHeaderRows(List rows) { + getHead().addRows(rows); + } + + /** + * Appends a new empty row to this table's {@code }, creating it if + * none exists yet. + * + * @return the newly created row. + */ + public TableRow addFooterRow() { + return getFoot().addRow(); + } + + /** + * Appends a new row containing the given texts as data cells + * (<td>) to this table's {@code }, creating it if + * none exists yet. + * + * @param cellTexts + * the text content for each data cell. + * @return the newly created row. + */ + public TableRow addFooterRow(String... cellTexts) { + return addFooterRow(Arrays.asList(cellTexts)); + } + + /** + * List equivalent of {@link #addFooterRow(String...)}. + * + * @param cellTexts + * the text content for each data cell. + * @return the newly created row. + */ + public TableRow addFooterRow(List cellTexts) { + TableRow row = getFoot().addRow(); + row.addDataCells(cellTexts); + return row; + } + + /** + * Appends the given rows to this table's {@code }, creating it if + * none exists yet. + * + * @param rows + * the rows to add. + */ + public void addFooterRows(TableRow... rows) { + getFoot().addRows(rows); + } + + /** + * List equivalent of {@link #addFooterRows(TableRow...)}. + * + * @param rows + * the rows to add. + */ + public void addFooterRows(List rows) { + getFoot().addRows(rows); + } + + private int columnGroupAppendIndex() { + int index = columnGroups.size(); + if (caption != null) { + index++; + } + return index; + } + + private int headIndex() { + int index = columnGroups.size(); + if (caption != null) { + index++; + } + return index; + } + + private int bodyAppendIndex() { + int index = bodies.size() + columnGroups.size(); + if (caption != null) { + index++; + } + if (head != null) { + index++; + } + return index; + } + +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableBody.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableBody.java new file mode 100644 index 00000000000..5cef4a95679 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableBody.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.List; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.HtmlComponent; +import com.vaadin.flow.component.Tag; + +/** + * Component representing a <tbody> element. + *

+ * Per the WHATWG + * HTML specification, a {@code

} may only contain + * <tr> elements. This component therefore extends + * {@link HtmlComponent} (rather than + * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only + * {@link TableRow}-specific operations through {@link TableRowContainer}. + * + * @since 25.2 + */ +@Tag(Tag.TBODY) +public class TableBody extends HtmlComponent + implements TableRowContainer, ClickNotifier { + + /** + * Creates a new empty table body. + */ + public TableBody() { + super(); + } + + /** + * Creates a new table body with the given rows. + * + * @param rows + * the rows to add. + */ + public TableBody(TableRow... rows) { + super(); + addRows(rows); + } + + /** + * List equivalent of {@link #TableBody(TableRow...)}. + * + * @param rows + * the rows to add. + */ + public TableBody(List rows) { + super(); + addRows(rows); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCaption.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCaption.java new file mode 100644 index 00000000000..185d28e2001 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCaption.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import com.vaadin.flow.component.HtmlContainer; +import com.vaadin.flow.component.Tag; + +/** + * Represents the table caption element ({@code }, {@code } or + * {@code }, even if implicitly defined). Browsers clip values above + * 65534. + * + * @param rowspan + * a non-negative integer. + */ + public void setRowspan(int rowspan) { + if (rowspan < 0) { + throw new IllegalArgumentException( + "rowspan must be a non-negative integer value"); + } + getElement().setAttribute(ATTRIBUTE_ROWSPAN, String.valueOf(rowspan)); + } + + /** + * Returns the rowspan value of this cell. + * + * @return the current value of the rowspan attribute. Default is 1. + */ + public int getRowspan() { + String rowspan = getElement().getAttribute(ATTRIBUTE_ROWSPAN); + if (rowspan == null) { + rowspan = "1"; + } + return Integer.parseInt(rowspan); + } + + /** + * Resets the rowspan to its default value of 1. + */ + public void resetRowspan() { + getElement().removeAttribute(ATTRIBUTE_ROWSPAN); + } + + /** + * Sets the {@code headers} attribute — a list of ids referring to the + * <th> cells that label this cell. Assistive + * technologies use it to read out the right headers when navigating complex + * tables, where {@link TableHeaderCell#setScope(TableHeaderCell.Scope) + * scope} alone isn't enough to disambiguate. + *

+ * Passing no arguments (or an empty array) removes the attribute. + * + * @param ids + * the ids of the header cells, in any order. + */ + public void setHeaders(String... ids) { + setHeaders(ids == null ? List.of() : Arrays.asList(ids)); + } + + /** + * List equivalent of {@link #setHeaders(String...)}. An empty list (or + * {@code null}) clears the attribute. + * + * @param ids + * the ids of the header cells, in any order. + */ + public void setHeaders(List ids) { + if (ids == null || ids.isEmpty()) { + getElement().removeAttribute(ATTRIBUTE_HEADERS); + return; + } + for (String id : ids) { + Objects.requireNonNull(id, "header id must not be null"); + } + getElement().setAttribute(ATTRIBUTE_HEADERS, String.join(" ", ids)); + } + + /** + * Convenience overload that takes header cells directly and uses their + * {@code id} attributes. Each cell must have an id set. + * + * @param headerCells + * the header cells whose ids should be referenced. + * @throws IllegalArgumentException + * if any of the given cells does not have an id set. + */ + public void setHeaders(TableHeaderCell... headerCells) { + setHeadersByCells( + headerCells == null ? List.of() : Arrays.asList(headerCells)); + } + + /** + * List equivalent of {@link #setHeaders(TableHeaderCell...)}. + * + * @param headerCells + * the header cells whose ids should be referenced. + * @throws IllegalArgumentException + * if any of the given cells does not have an id set. + */ + public void setHeadersByCells(List headerCells) { + if (headerCells == null || headerCells.isEmpty()) { + getElement().removeAttribute(ATTRIBUTE_HEADERS); + return; + } + List ids = new ArrayList<>(headerCells.size()); + for (TableHeaderCell cell : headerCells) { + ids.add(cell.getId().orElseThrow(() -> new IllegalArgumentException( + "Header cell must have an id to be referenced via the headers attribute"))); + } + setHeaders(ids); + } + + /** + * Returns the IDs of the header cells associated with this cell via the + * {@code headers} attribute, or an empty {@link Optional} if the attribute + * is not set. + * + * @return the parsed list of header IDs. + */ + public Optional getHeaders() { + String value = getElement().getAttribute(ATTRIBUTE_HEADERS); + if (value == null || value.isEmpty()) { + return Optional.empty(); + } + return Optional.of(value.split("\\s+")); + } + + /** + * Removes the {@code headers} attribute from this cell. + */ + public void resetHeaders() { + getElement().removeAttribute(ATTRIBUTE_HEADERS); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumn.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumn.java new file mode 100644 index 00000000000..58bd5e77f51 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumn.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import com.vaadin.flow.component.HtmlComponent; +import com.vaadin.flow.component.Tag; + +/** + * Component representing a <col> element — a column (or + * range of columns, via {@link #setSpan(int)}) inside a + * {@link TableColumnGroup}. Use it to apply column-wide styling without + * repeating it on every cell: a class or id on a {@code

} can target all + * the data cells in that column. + *

+ * {@code

} is a void element (no children, no end tag) and is only valid + * inside a {@code } that does not itself carry a {@code span} + * attribute. Only a limited subset of CSS applies to columns: + * {@code background}, {@code border} (with {@code border-collapse: collapse}), + * {@code visibility: collapse} and {@code width}. Text and font properties do + * not inherit into the cells — style those on <td> or + * <th> instead. + * + * @see MDN: + * <col> — The Table Column element + * @since 25.2 + */ +@Tag(Tag.COL) +public class TableColumn extends HtmlComponent { + + private static final String ATTRIBUTE_SPAN = "span"; + + /** + * Creates a new column component spanning a single column. + */ + public TableColumn() { + super(); + } + + /** + * Creates a new column component spanning the given number of columns. + * + * @param span + * the number of consecutive columns this {@code } element + * applies to. Must be a positive integer. + */ + public TableColumn(int span) { + super(); + setSpan(span); + } + + /** + * Sets the {@code span} attribute — how many consecutive columns this + * {@code } element covers. The default is {@code 1}. Use it to apply + * the same styling or attributes across a range of columns without writing + * one {@code } per column. + * + * @param span + * a positive integer. + */ + public void setSpan(int span) { + if (span < 1) { + throw new IllegalArgumentException( + "span must be a positive integer value"); + } + getElement().setAttribute(ATTRIBUTE_SPAN, String.valueOf(span)); + } + + /** + * Returns the value of the {@code span} attribute. + * + * @return the current span. Default is 1. + */ + public int getSpan() { + String span = getElement().getAttribute(ATTRIBUTE_SPAN); + if (span == null) { + span = "1"; + } + return Integer.parseInt(span); + } + + /** + * Resets the span to its default value of 1. + */ + public void resetSpan() { + getElement().removeAttribute(ATTRIBUTE_SPAN); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumnGroup.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumnGroup.java new file mode 100644 index 00000000000..3bbfbb5539b --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumnGroup.java @@ -0,0 +1,192 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.vaadin.flow.component.HtmlComponent; +import com.vaadin.flow.component.Tag; + +/** + * Component representing a <colgroup> element — a group of + * columns inside a {@link Table}, used to apply attributes (most often a class + * for CSS) to several columns at once. Only a limited subset of CSS applies to + * column groups: {@code background}, {@code border} (with + * {@code border-collapse: collapse}), {@code visibility: collapse} and + * {@code width}. + *

+ * Per the WHATWG + * HTML specification, a {@code

} is used in one of two modes: + * either it carries a {@code span} attribute and has no children, or it + * contains zero or more {@code } children and has no {@code span} + * attribute. This component therefore extends {@link HtmlComponent} (rather + * than {@link com.vaadin.flow.component.HtmlContainer}) and exposes only + * operations for managing {@link TableColumn} children plus the {@code span} + * attribute. {@code } elements must be placed after the optional + * {@code }, {@code }, + * {@code } or <tr>; the {@link Table} inserts them at + * the correct position automatically. + * + * @see MDN: + * <colgroup> — The Table Column Group element + * @since 25.2 + */ +@Tag(Tag.COLGROUP) +public class TableColumnGroup extends HtmlComponent { + + private static final String ATTRIBUTE_SPAN = "span"; + + /** + * Creates a new empty column group. + */ + public TableColumnGroup() { + super(); + } + + /** + * Creates a new column group with the given columns appended. + * + * @param columns + * the columns to add. + */ + public TableColumnGroup(TableColumn... columns) { + this(Arrays.asList(columns)); + } + + /** + * List equivalent of {@link #TableColumnGroup(TableColumn...)}. + * + * @param columns + * the columns to add. + */ + public TableColumnGroup(List columns) { + super(); + addColumns(columns); + } + + /** + * Appends a new empty {@code } child to this group. + * + * @return the new column. + */ + public TableColumn addColumn() { + TableColumn column = new TableColumn(); + getElement().appendChild(column.getElement()); + return column; + } + + /** + * Appends a new {@code } child with the given span to this group. + * + * @param span + * the number of columns to span. + * @return the new column. + */ + public TableColumn addColumn(int span) { + TableColumn column = new TableColumn(span); + getElement().appendChild(column.getElement()); + return column; + } + + /** + * Appends the given columns to this group. + * + * @param columns + * the columns to add. + */ + public void addColumns(TableColumn... columns) { + addColumns(Arrays.asList(columns)); + } + + /** + * List equivalent of {@link #addColumns(TableColumn...)}. + * + * @param columns + * the columns to add. + */ + public void addColumns(List columns) { + for (TableColumn column : columns) { + getElement().appendChild(column.getElement()); + } + } + + /** + * Returns the columns inside this group. + * + * @return the list of {@code } children. + */ + public List getColumns() { + return getChildren().filter(c -> c instanceof TableColumn) + .map(c -> (TableColumn) c).collect(Collectors.toList()); + } + + /** + * Removes a column from this group. + * + * @param column + * the column to remove. + */ + public void removeColumn(TableColumn column) { + getElement().removeChild(column.getElement()); + } + + /** + * Removes all columns from this group. + */ + public void removeAllColumns() { + getElement().removeAllChildren(); + } + + /** + * Sets the {@code span} attribute — how many consecutive columns this group + * covers when used without {@link TableColumn} children. Per the HTML + * specification, {@code span} is only valid on a {@code } that + * has no {@code } children. The default is {@code 1}. + * + * @param span + * a positive integer. + */ + public void setSpan(int span) { + if (span < 1) { + throw new IllegalArgumentException( + "span must be a positive integer value"); + } + getElement().setAttribute(ATTRIBUTE_SPAN, String.valueOf(span)); + } + + /** + * Returns the value of the {@code span} attribute. + * + * @return the current span. Default is 1. + */ + public int getSpan() { + String span = getElement().getAttribute(ATTRIBUTE_SPAN); + if (span == null) { + span = "1"; + } + return Integer.parseInt(span); + } + + /** + * Removes the {@code span} attribute, restoring the default value of 1. + */ + public void resetSpan() { + getElement().removeAttribute(ATTRIBUTE_SPAN); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableDataCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableDataCell.java new file mode 100644 index 00000000000..67af668f607 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableDataCell.java @@ -0,0 +1,94 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.List; +import java.util.Objects; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.signals.Signal; + +/** + * Component representing a <td> element (a table data cell). + * Inherits {@code colspan}/{@code rowspan} support from {@link TableCell}. + * + * @since 25.2 + */ +@Tag(Tag.TD) +public class TableDataCell extends TableCell + implements ClickNotifier { + + /** + * Creates a new empty table cell component. + */ + public TableDataCell() { + super(); + } + + /** + * Creates a new table cell with the given children components. + * + * @param components + * the children components. + */ + public TableDataCell(Component... components) { + super(components); + } + + /** + * List equivalent of {@link #TableDataCell(Component...)}. + * + * @param components + * the children components. + */ + public TableDataCell(List components) { + super(components); + } + + /** + * Creates a new table cell with the given text. + * + * @param text + * the text. + */ + public TableDataCell(String text) { + super(); + setText(text); + } + + /** + * Creates a new table cell with its text content bound to the given signal. + *

+ * While a binding for the text content is active, any attempt to set the + * text manually throws + * {@link com.vaadin.flow.signals.BindingActiveException}. The same happens + * when trying to bind a new Signal while one is already bound. + *

+ * Bindings are lifecycle-aware and only active while this component is in + * the attached state; they are deactivated while the component is in the + * detached state. + * + * @param textSignal + * the signal to bind, not null + * @see #bindText(Signal) + */ + public TableDataCell(Signal textSignal) { + Objects.requireNonNull(textSignal, "textSignal must not be null"); + bindText(textSignal); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableFoot.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableFoot.java new file mode 100644 index 00000000000..4acc7fd2967 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableFoot.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.List; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.HtmlComponent; +import com.vaadin.flow.component.Tag; + +/** + * Component representing a <tfoot> element (the table foot). + *

+ * Per the WHATWG + * HTML specification, a {@code

} may only contain + * <tr> elements. This component therefore extends + * {@link HtmlComponent} (rather than + * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only + * {@link TableRow}-specific operations through {@link TableRowContainer}. + * + * @since 25.2 + */ +@Tag(Tag.TFOOT) +public class TableFoot extends HtmlComponent + implements TableRowContainer, ClickNotifier { + + /** + * Creates a new empty table foot component. + */ + public TableFoot() { + super(); + } + + /** + * Creates a new table foot with the given rows. + * + * @param rows + * the rows to add. + */ + public TableFoot(TableRow... rows) { + super(); + addRows(rows); + } + + /** + * List equivalent of {@link #TableFoot(TableRow...)}. + * + * @param rows + * the rows to add. + */ + public TableFoot(List rows) { + super(); + addRows(rows); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHead.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHead.java new file mode 100644 index 00000000000..c1da1f6b716 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHead.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.List; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.HtmlComponent; +import com.vaadin.flow.component.Tag; + +/** + * Component representing a <thead> element (the table head). + *

+ * Per the WHATWG + * HTML specification, a {@code

} may only contain + * <tr> elements. This component therefore extends + * {@link HtmlComponent} (rather than + * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only + * {@link TableRow}-specific operations through {@link TableRowContainer}. + * + * @since 25.2 + */ +@Tag(Tag.THEAD) +public class TableHead extends HtmlComponent + implements TableRowContainer, ClickNotifier { + + /** + * Creates a new empty table head component. + */ + public TableHead() { + super(); + } + + /** + * Creates a new table head with the given rows. + * + * @param rows + * the rows to add. + */ + public TableHead(TableRow... rows) { + super(); + addRows(rows); + } + + /** + * List equivalent of {@link #TableHead(TableRow...)}. + * + * @param rows + * the rows to add. + */ + public TableHead(List rows) { + super(); + addRows(rows); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHeaderCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHeaderCell.java new file mode 100644 index 00000000000..54c980710ee --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHeaderCell.java @@ -0,0 +1,199 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.signals.Signal; + +/** + * Component representing a <th> element — a cell that labels + * a group of other cells in a {@link Table}. The exact group is defined by the + * {@link #setScope(Scope) scope} attribute (which row, column, row group, or + * column group the header applies to) and/or by + * {@link TableCell#setHeaders(String...) headers} attributes on the data cells + * that reference this header by id. + *

+ * Inherits {@code colspan}, {@code rowspan} and {@code headers} support from + * {@link TableCell}, since those attributes apply equally to + * <td> and <th>. + * + * @see MDN: + * <th> — The Table Header element + * @since 25.2 + */ +@Tag(Tag.TH) +public class TableHeaderCell extends TableCell + implements ClickNotifier { + + /** + * Creates a new empty header cell component. + */ + public TableHeaderCell() { + super(); + } + + /** + * Creates a new header cell with the given children components. + * + * @param components + * the children components. + */ + public TableHeaderCell(Component... components) { + super(components); + } + + /** + * List equivalent of {@link #TableHeaderCell(Component...)}. + * + * @param components + * the children components. + */ + public TableHeaderCell(List components) { + super(components); + } + + /** + * Creates a new header cell with the given text. + * + * @param text + * the text. + */ + public TableHeaderCell(String text) { + super(); + setText(text); + } + + /** + * Creates a new header cell with its text content bound to the given + * signal. + * + * @param textSignal + * the signal to bind, not {@code null} + * @see #bindText(Signal) + */ + public TableHeaderCell(Signal textSignal) { + Objects.requireNonNull(textSignal, "textSignal must not be null"); + bindText(textSignal); + } + + /** + * Defines the cells that a <th> header relates to — + * effectively the answer to "which other cells does this header label?". + * Setting it correctly is what lets a screen reader read out the right + * column or row header when a user navigates into a data cell. + *

+ * If no scope is set (or the value is unrecognized), browsers automatically + * infer the scope from the table structure. Per the HTML + * spec, the standard keywords are {@link #ROW row}, {@link #COL col}, + * {@link #ROWGROUP rowgroup}, and {@link #COLGROUP colgroup}; {@link #AUTO + * auto} writes the literal {@code "auto"} attribute value, which browsers + * treat the same as omitting the attribute. + */ + public enum Scope { + /** + * The header relates to all cells of the row it belongs to. Use this + * for a leading <th> that labels its row (e.g. the + * row's name or category). + */ + ROW("row"), + /** + * The header relates to all cells of the column it belongs to. Use this + * for a header at the top of a column (the most common case in + * <thead>). + */ + COL("col"), + /** + * The header belongs to a row group ({@code

} or + * {@code }/{@code }) and relates to all cells in that + * group. + */ + ROWGROUP("rowgroup"), + /** + * The header belongs to a column group ({@link TableColumnGroup}) and + * relates to all cells in that group. + */ + COLGROUP("colgroup"), + /** + * Writes the literal {@code "auto"} value. Behaves the same as leaving + * the attribute unset — the browser infers the scope from the table + * structure. + */ + AUTO("auto"); + + private final String value; + + Scope(String value) { + this.value = value; + } + + /** + * Returns the attribute value as it appears in the rendered HTML. + */ + public String getValue() { + return value; + } + + static Scope fromValue(String value) { + if (value == null) { + return null; + } + for (Scope s : values()) { + if (s.value.equals(value)) { + return s; + } + } + return null; + } + } + + /** + * Sets the {@code scope} attribute, declaring which cells this header + * labels. Critical for accessibility — screen readers use this to announce + * the right header when a user navigates into a data cell. Pass + * {@code null} to remove the attribute (browsers will then infer scope from + * structure). + * + * @param scope + * the scope, or {@code null} to clear the attribute. + * @see Scope + */ + public void setScope(Scope scope) { + if (scope == null) { + getElement().removeAttribute("scope"); + } else { + getElement().setAttribute("scope", scope.getValue()); + } + } + + /** + * Returns the {@code scope} attribute value, if set. + * + * @return the scope wrapped in an {@link Optional}, or empty if unset (or + * the value is unrecognized). + */ + public Optional getScope() { + return Optional.ofNullable( + Scope.fromValue(getElement().getAttribute("scope"))); + } +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRow.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRow.java new file mode 100644 index 00000000000..0acee79ec8b --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRow.java @@ -0,0 +1,304 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HtmlComponent; +import com.vaadin.flow.component.Tag; + +/** + * Component representing a <tr> element. + *

+ * Per the + * WHATWG HTML + * specification, a {@code

} may only contain {@code }, {@code } and {@code }. + *

+ * Implementers must also be {@link Component} instances. + * + * @since 25.2 + */ +interface TableRowContainer extends HasElement { + + private Stream rowComponents() { + if (this instanceof Component component) { + return component.getChildren(); + } + throw new UnsupportedOperationException( + "TableRowContainer must be implemented by a Component"); + } + + /** + * Returns a list of all the rows. + * + * @return all the rows in the container. + */ + default List getRows() { + return rowComponents().filter(c -> c instanceof TableRow) + .map(c -> (TableRow) c).collect(Collectors.toList()); + } + + /** + * Appends a list of rows to the container. + * + * @param rows + * the rows to append. + */ + default void addRows(TableRow... rows) { + addRows(Arrays.asList(rows)); + } + + /** + * List equivalent of {@link #addRows(TableRow...)}. + * + * @param rows + * the rows to append. + */ + default void addRows(List rows) { + for (TableRow row : rows) { + getElement().appendChild(row.getElement()); + } + } + + /** + * Create and append a row to the end of the container. + * + * @return the new row. + */ + default TableRow addRow() { + TableRow row = new TableRow(); + getElement().appendChild(row.getElement()); + return row; + } + + /** + * Create and insert a row at a given position. + * + * @param position + * a value greater than or equal to 0 and less than or equal to + * the container's size. + * @return the new row. + */ + default TableRow insertRow(int position) { + TableRow row = new TableRow(); + getElement().insertChild(position, row.getElement()); + return row; + } + + /** + * Remove a list of rows from the container. + * + * @param rows + * the rows to remove. + */ + default void removeRows(TableRow... rows) { + removeRows(Arrays.asList(rows)); + } + + /** + * List equivalent of {@link #removeRows(TableRow...)}. + * + * @param rows + * the rows to remove. + */ + default void removeRows(List rows) { + for (TableRow row : rows) { + getElement().removeChild(row.getElement()); + } + } + + /** + * Remove all the rows in the container. + */ + default void removeAllRows() { + getElement().removeAllChildren(); + } + + /** + * Replaces the row at a given position with a new one. + * + * @param index + * the index of the row to replace. + * @param row + * the new row to insert at the position of the old row. + */ + default void replaceRow(int index, TableRow row) { + getElement().setChild(index, row.getElement()); + } + +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java index 86c901d6e8f..88401b21a64 100644 --- a/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java @@ -75,8 +75,10 @@ class HtmlComponentSmokeTest { testValues.put(IFrame.SandboxType[].class, new IFrame.SandboxType[] { IFrame.SandboxType.ALLOW_POPUPS, IFrame.SandboxType.ALLOW_MODALS }); + testValues.put(String[].class, new String[] { "a", "b" }); testValues.put(Component.class, new Paragraph("Component")); testValues.put(HasText.WhiteSpace.class, HasText.WhiteSpace.PRE_LINE); + testValues.put(TableHeaderCell.Scope.class, TableHeaderCell.Scope.COL); } private static final Map, Map, Object>> specialTestValues = new HashMap<>(); @@ -240,12 +242,25 @@ private static boolean isSpecialSetter(Method method) { return true; } - // NativeTable delegates caption text to the nested

}). + * + * @since 25.2 + */ +@Tag(Tag.CAPTION) +public class TableCaption extends HtmlContainer { +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCell.java new file mode 100644 index 00000000000..f0b02d8c2c5 --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCell.java @@ -0,0 +1,239 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HtmlContainer; + +/** + * Common superclass for table cell components ({@link TableDataCell} and + * {@link TableHeaderCell}). Provides shared support for the attributes that + * apply equally to <td> and <th> per the + * WHATWG HTML + * specification: {@code colspan}, {@code rowspan} and {@code headers}. + * + * @see MDN: + * <td> + * @see MDN: + * <th> + * @since 25.2 + */ +public abstract class TableCell extends HtmlContainer { + + private static final String ATTRIBUTE_COLSPAN = "colspan"; + private static final String ATTRIBUTE_ROWSPAN = "rowspan"; + private static final String ATTRIBUTE_HEADERS = "headers"; + + /** + * Creates a new empty cell component. + */ + protected TableCell() { + super(); + } + + /** + * Creates a new cell with the given children components. + * + * @param components + * the children components. + */ + protected TableCell(Component... components) { + super(components); + } + + /** + * List equivalent of {@link #TableCell(Component...)}. + * + * @param components + * the children components. + */ + protected TableCell(List components) { + super(components.toArray(Component[]::new)); + } + + /** + * Sets the {@code colspan} attribute — how many columns this cell spans. + * The default is {@code 1}. Browsers clamp values higher than 1000 back to + * {@code 1}. + * + * @param colspan + * a non-negative integer. + */ + public void setColspan(int colspan) { + if (colspan < 0) { + throw new IllegalArgumentException( + "colspan must be a non-negative integer value"); + } + getElement().setAttribute(ATTRIBUTE_COLSPAN, String.valueOf(colspan)); + } + + /** + * Returns the colspan value of this cell. + * + * @return the current value of the colspan attribute. Default is 1. + */ + public int getColspan() { + String colspan = getElement().getAttribute(ATTRIBUTE_COLSPAN); + if (colspan == null) { + colspan = "1"; + } + return Integer.parseInt(colspan); + } + + /** + * Reset colspan to its default value of 1. + */ + public void resetColspan() { + getElement().removeAttribute(ATTRIBUTE_COLSPAN); + } + + /** + * Sets the {@code rowspan} attribute — how many rows this cell spans. The + * default is {@code 1}. A value of {@code 0} extends the cell until the end + * of its grouping section ({@code } and before any {@code
} or + * {@code } elements. This component therefore extends + * {@link HtmlComponent} (rather than + * {@link com.vaadin.flow.component.HtmlContainer}) so the structural + * invariant is preserved. + *

+ * The {@link #TableRow(Component...) constructor} and {@link #addCells} accept + * any {@link Component}: {@link TableCell} subclasses ({@link TableDataCell}, + * {@link TableHeaderCell}) are placed as-is, and any other component is + * automatically wrapped in a new {@link TableDataCell}. + * + * @since 25.2 + */ +@Tag(Tag.TR) +public class TableRow extends HtmlComponent + implements ClickNotifier { + + /** + * Creates a new empty table row component. + */ + public TableRow() { + super(); + } + + /** + * Creates a new table row with the given children. Any {@link TableCell} + * argument ({@link TableDataCell} or {@link TableHeaderCell}) is added + * as-is; any other component is wrapped in a new {@link TableDataCell} + * — convenient for building rows from arbitrary content without the + * boilerplate of explicit {@code new TableDataCell(...)} wrappers. + * + * @param components + * the cells (used as-is) or other components (wrapped in + * {@code

}) to place in this row. + */ + public TableRow(Component... components) { + this(Arrays.asList(components)); + } + + /** + * List equivalent of {@link #TableRow(Component...)}. + * + * @param components + * the cells or wrap-target components for this row. + */ + public TableRow(List components) { + super(); + appendAsCells(components); + } + + /** + * Add a header cell to this row. + * + * @return the new {@code } element. + */ + public TableHeaderCell addHeaderCell() { + TableHeaderCell cell = new TableHeaderCell(); + getElement().appendChild(cell.getElement()); + return cell; + } + + /** + * Add a header cell to this row with the given text content. + * + * @param text + * the text content. + * @return the new {@code } element. + */ + public TableHeaderCell addHeaderCell(String text) { + TableHeaderCell cell = new TableHeaderCell(text); + getElement().appendChild(cell.getElement()); + return cell; + } + + /** + * Add a header cell to this row that labels the row itself, with + * {@code scope="row"} set on the resulting {@code }. This is a + * shortcut for the common pattern of using a leading {@code } as a + * row label, which assistive technologies announce as the header for + * the data cells in the same row. + * + * @param text + * the text content. + * @return the new {@code } element with {@code scope="row"}. + */ + public TableHeaderCell addRowHeaderCell(String text) { + TableHeaderCell cell = new TableHeaderCell(text); + cell.setScope(TableHeaderCell.Scope.ROW); + getElement().appendChild(cell.getElement()); + return cell; + } + + /** + * Insert a new header cell into a given position. + * + * @param position + * the position into which the header cell must be added. + * @return the new header cell. + */ + public TableHeaderCell insertHeaderCell(int position) { + TableHeaderCell headerCell = new TableHeaderCell(); + getElement().insertChild(position, headerCell.getElement()); + return headerCell; + } + + /** + * Add a data cell to this row. + * + * @return the new {@code } element. + */ + public TableDataCell addDataCell() { + TableDataCell cell = new TableDataCell(); + getElement().appendChild(cell.getElement()); + return cell; + } + + /** + * Add a data cell to this row with the given text content. + * + * @param text + * the text content. + * @return the new {@code } element. + */ + public TableDataCell addDataCell(String text) { + TableDataCell cell = new TableDataCell(text); + getElement().appendChild(cell.getElement()); + return cell; + } + + /** + * Insert a new data cell into a given position. + * + * @param position + * the position into which the data cell must be added. + * @return the new data cell. + */ + public TableDataCell insertDataCell(int position) { + TableDataCell tableCell = new TableDataCell(); + getElement().insertChild(position, tableCell.getElement()); + return tableCell; + } + + /** + * Returns a list of all header cells in this row. + * + * @return A list of all header cells in this row. + */ + public List getHeaderCells() { + return getChildren().filter(c -> c instanceof TableHeaderCell) + .map(c -> (TableHeaderCell) c).collect(Collectors.toList()); + } + + /** + * Returns a list of all data cells in this row. + * + * @return A list of all data cells in this row. + */ + public List getDataCells() { + return getChildren().filter(c -> c instanceof TableDataCell) + .map(c -> (TableDataCell) c).collect(Collectors.toList()); + } + + /** + * Returns all cells in this row, in document order — both + * {@link TableDataCell} and {@link TableHeaderCell} entries combined. For + * kind-specific lists use {@link #getDataCells()} or + * {@link #getHeaderCells()}; index into any of these lists with + * {@code .get(i)}. + * + * @return a list of all cells in this row. + */ + public List getCells() { + return getChildren().filter(c -> c instanceof TableCell) + .map(c -> (TableCell) c).collect(Collectors.toList()); + } + + /** + * Removes a cell from this row. + * + * @param cell + * the cell to remove. + */ + public void removeCell(TableCell cell) { + getElement().removeChild(cell.getElement()); + } + + /** + * Appends a sequence of data cells ({@code }) with the given text + * contents to this row. + * + * @param cellTexts + * the text content for each data cell. + * @return this row, for fluent chaining. + */ + public TableRow addDataCells(String... cellTexts) { + return addDataCells(Arrays.asList(cellTexts)); + } + + /** + * List equivalent of {@link #addDataCells(String...)}. + * + * @param cellTexts + * the text content for each data cell. + * @return this row, for fluent chaining. + */ + public TableRow addDataCells(List cellTexts) { + for (String text : cellTexts) { + addDataCell(text); + } + return this; + } + + /** + * Appends a sequence of header cells ({@code }) with the given text + * contents to this row. + * + * @param cellTexts + * the text content for each header cell. + * @return this row, for fluent chaining. + */ + public TableRow addHeaderCells(String... cellTexts) { + return addHeaderCells(Arrays.asList(cellTexts)); + } + + /** + * List equivalent of {@link #addHeaderCells(String...)}. + * + * @param cellTexts + * the text content for each header cell. + * @return this row, for fluent chaining. + */ + public TableRow addHeaderCells(List cellTexts) { + for (String text : cellTexts) { + addHeaderCell(text); + } + return this; + } + + /** + * Appends children to this row. Any {@link TableCell} argument + * ({@link TableDataCell} or {@link TableHeaderCell}) is added as-is; + * any other component is wrapped in a new {@link TableDataCell}. + * + * @param components + * the cells (used as-is) or other components (wrapped in + * {@code }) to append. + * @return this row, for fluent chaining. + */ + public TableRow addCells(Component... components) { + return addCells(Arrays.asList(components)); + } + + /** + * List equivalent of {@link #addCells(Component...)}. + * + * @param components + * the cells or wrap-target components. + * @return this row, for fluent chaining. + */ + public TableRow addCells(List components) { + appendAsCells(components); + return this; + } + + private void appendAsCells(Iterable components) { + for (Component c : components) { + TableCell cell = (c instanceof TableCell tc) ? tc + : new TableDataCell(c); + getElement().appendChild(cell.getElement()); + } + } + +} diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRowContainer.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRowContainer.java new file mode 100644 index 00000000000..797344c685c --- /dev/null +++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRowContainer.java @@ -0,0 +1,145 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasElement; + +/** + * A container of <tr> elements. Implemented by + * {@link TableHead}, {@link TableBody} and {@link TableFoot}. + *

+ * Only {@link TableRow} children are accepted, matching the WHATWG HTML + * structural rules for {@code

element - if (method.getDeclaringClass() == NativeTable.class + // NativeTable / Table delegates caption text to the nested + // element + if ((method.getDeclaringClass() == NativeTable.class + || method.getDeclaringClass() == Table.class) && method.getName().startsWith("setCaptionText")) { return true; } + // TableCell.setHeaders has multiple overloads (String..., String list, + // TableHeaderCell...). The String[] variant is exercised normally to + // cover the bean property; the others have non-matching getter types + // or no matching getter and are covered by focused unit tests. + if (method.getDeclaringClass() == TableCell.class && (method.getName() + .equals("setHeadersByCells") + || (method.getName().equals("setHeaders") + && method.getParameterTypes()[0] != String[].class))) { + return true; + } + if (method.getDeclaringClass() == FieldSet.class && method.getName().startsWith("setContent")) { return true; @@ -408,7 +423,8 @@ private static boolean isClassFile(Path path) { } private static boolean isHtmlComponentSubclass(Class cls) { - return HtmlComponent.class.isAssignableFrom(cls); + return HtmlComponent.class.isAssignableFrom(cls) + && !Modifier.isAbstract(cls.getModifiers()); } private static Class asHtmlComponentSubclass( diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableBodyTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableBodyTest.java new file mode 100644 index 00000000000..dc91a4d05c9 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableBodyTest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +public class TableBodyTest extends ComponentTest { + // Actual test methods in super class + + @Override + protected void addProperties() { + // Component defines no new properties + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableCaptionTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableCaptionTest.java new file mode 100644 index 00000000000..783f71d5e53 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableCaptionTest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +public class TableCaptionTest extends ComponentTest { + // Actual test methods in super class + + @Override + protected void addProperties() { + // Component defines no new properties + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnGroupTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnGroupTest.java new file mode 100644 index 00000000000..98f98d0640f --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnGroupTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TableColumnGroupTest extends ComponentTest { + // Property tests in super class + + @Override + protected void addProperties() { + addProperty("span", int.class, 1, 2, false, false); + } + + @Test + void addColumn_appendsChild() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + TableColumn col = group.addColumn(); + assertEquals(1, group.getColumns().size()); + assertEquals(group, col.getParent().orElseThrow()); + } + + @Test + void addColumnWithSpan_appendsChildWithSpan() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + TableColumn col = group.addColumn(2); + assertEquals(2, col.getSpan()); + assertEquals(1, group.getColumns().size()); + } + + @Test + void addColumns_appendsExistingColumns() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + TableColumn c1 = new TableColumn(); + TableColumn c2 = new TableColumn(3); + group.addColumns(c1, c2); + List columns = group.getColumns(); + assertEquals(2, columns.size()); + assertEquals(c1, columns.get(0)); + assertEquals(c2, columns.get(1)); + } + + @Test + void varargsConstructor() { + TableColumn c1 = new TableColumn(); + TableColumn c2 = new TableColumn(2); + TableColumnGroup group = new TableColumnGroup(c1, c2); + assertEquals(2, group.getColumns().size()); + } + + @Test + void removeColumn() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + TableColumn c = group.addColumn(); + group.removeColumn(c); + assertTrue(group.getColumns().isEmpty()); + assertTrue(c.getParent().isEmpty()); + } + + @Test + void removeAllColumns() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + group.addColumn(); + group.addColumn(); + group.addColumn(); + group.removeAllColumns(); + assertTrue(group.getColumns().isEmpty()); + } + + @Test + void setSpan_writesAttribute() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + group.setSpan(4); + assertEquals("4", group.getElement().getAttribute("span")); + assertEquals(4, group.getSpan()); + } + + @Test + void setSpan_rejectsNonPositive() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + assertThrows(IllegalArgumentException.class, () -> group.setSpan(0)); + } + + @Test + void resetSpan_removesAttribute() { + TableColumnGroup group = (TableColumnGroup) getComponent(); + group.setSpan(2); + group.resetSpan(); + assertNull(group.getElement().getAttribute("span")); + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnTest.java new file mode 100644 index 00000000000..ed6ac6c17e4 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TableColumnTest extends ComponentTest { + // Actual test methods in super class + + @Override + protected void addProperties() { + addProperty("span", int.class, 1, 2, false, false); + } + + @Test + void defaultSpanIsOne() { + TableColumn col = (TableColumn) getComponent(); + assertEquals(1, col.getSpan()); + } + + @Test + void setSpan_writesAttribute() { + TableColumn col = (TableColumn) getComponent(); + col.setSpan(3); + assertEquals("3", col.getElement().getAttribute("span")); + assertEquals(3, col.getSpan()); + } + + @Test + void setSpan_rejectsNonPositive() { + TableColumn col = (TableColumn) getComponent(); + assertThrows(IllegalArgumentException.class, () -> col.setSpan(0)); + assertThrows(IllegalArgumentException.class, () -> col.setSpan(-1)); + } + + @Test + void resetSpan_removesAttribute() { + TableColumn col = (TableColumn) getComponent(); + col.setSpan(4); + col.resetSpan(); + assertNull(col.getElement().getAttribute("span")); + assertEquals(1, col.getSpan()); + } + + @Test + void spanConstructor() { + TableColumn col = new TableColumn(5); + assertEquals(5, col.getSpan()); + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellBindTextTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellBindTextTest.java new file mode 100644 index 00000000000..f83bec55817 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellBindTextTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import org.junit.jupiter.api.Test; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.dom.SignalsUnitTest; +import com.vaadin.flow.signals.BindingActiveException; +import com.vaadin.flow.signals.local.ValueSignal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TableDataCellBindTextTest extends SignalsUnitTest { + + @Test + void bindText_updatesTextOnSignalChange() { + TableDataCell cell = new TableDataCell(); + UI.getCurrent().add(cell); + + ValueSignal signal = new ValueSignal<>(""); + cell.bindText(signal); + + signal.set("text-1"); + assertEquals("text-1", cell.getText()); + + signal.set("text-2"); + assertEquals("text-2", cell.getText()); + } + + @Test + void bindText_setTextWhileBindingActive_throws() { + TableDataCell cell = new TableDataCell(); + UI.getCurrent().add(cell); + + ValueSignal signal = new ValueSignal<>("initial"); + cell.bindText(signal); + + assertThrows(BindingActiveException.class, + () -> cell.setText("manual")); + } + + @Test + void bindText_nullSignal_throwsNPE() { + TableDataCell cell = new TableDataCell(); + UI.getCurrent().add(cell); + + assertThrows(NullPointerException.class, () -> cell.bindText(null)); + } + + @Test + void constructorWithSignal_bindsText() { + ValueSignal signal = new ValueSignal<>("initial"); + TableDataCell cell = new TableDataCell(signal); + UI.getCurrent().add(cell); + + assertEquals("initial", cell.getText()); + + signal.set("updated"); + assertEquals("updated", cell.getText()); + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellTest.java new file mode 100644 index 00000000000..72f213f266b --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import org.junit.jupiter.api.Test; + +import static com.vaadin.flow.component.html.AssertUtils.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TableDataCellTest extends ComponentTest { + // Actual test methods in super class + + @Override + protected void addProperties() { + addProperty("colspan", int.class, 1, 2, false, false); + addProperty("rowspan", int.class, 1, 2, false, false); + addProperty("headers", String[].class, null, new String[] { "a", "b" }, + true, true); + } + + @Test + void colspanMustBeNonNegative() { + TableDataCell cell = (TableDataCell) getComponent(); + assertThrows(IllegalArgumentException.class, () -> cell.setColspan(-1)); + } + + @Test + void setColspan() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.setColspan(2); + assertEquals("2", cell.getElement().getAttribute("colspan"), + "Colspan should be 2"); + } + + @Test + void getDefaultColspan() { + TableDataCell cell = (TableDataCell) getComponent(); + int colspan = cell.getColspan(); + assertEquals(1, colspan, "Default colspan should be 1"); + } + + @Test + void getColspan() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.getElement().setAttribute("colspan", "2"); + assertEquals(2, cell.getColspan(), "Colspan should be 2"); + } + + @Test + void resetColspan() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.getElement().setAttribute("colspan", "2"); + cell.resetColspan(); + assertNull(cell.getElement().getAttribute("colspan"), + "Element should not have colspan attribute"); + } + + @Test + void rowspanMustNonNegative() { + TableDataCell cell = (TableDataCell) getComponent(); + assertThrows(IllegalArgumentException.class, () -> cell.setRowspan(-1)); + } + + @Test + void setRowspan() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.setRowspan(2); + assertEquals("2", cell.getElement().getAttribute("rowspan"), + "Rowspan should be 2"); + } + + @Test + void getDefaultRowspan() { + TableDataCell cell = (TableDataCell) getComponent(); + int rowspan = cell.getRowspan(); + assertEquals(1, rowspan, "Default rowspan should be 1"); + } + + @Test + void getRowspan() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.getElement().setAttribute("rowspan", "2"); + assertEquals(2, cell.getRowspan(), "Rowspan should be 2"); + } + + @Test + void resetRowspan() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.setRowspan(2); + cell.resetRowspan(); + assertNull(cell.getElement().getAttribute("rowspan"), + "Element should not have rowspan attribute"); + } + + @Test + void headers_unsetByDefault() { + TableDataCell cell = (TableDataCell) getComponent(); + org.junit.jupiter.api.Assertions + .assertTrue(cell.getHeaders().isEmpty()); + } + + @Test + void setHeaders_writesSpaceJoinedAttribute() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.setHeaders("name", "age"); + assertEquals("name age", cell.getElement().getAttribute("headers"), + "headers attribute should be space-joined"); + org.junit.jupiter.api.Assertions.assertArrayEquals( + new String[] { "name", "age" }, + cell.getHeaders().orElseThrow()); + } + + @Test + void setHeaders_emptyClearsAttribute() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.setHeaders("name"); + cell.setHeaders(new String[0]); + assertNull(cell.getElement().getAttribute("headers"), + "Empty array should clear the attribute"); + org.junit.jupiter.api.Assertions + .assertTrue(cell.getHeaders().isEmpty()); + } + + @Test + void setHeaders_fromHeaderCells_resolvesIds() { + TableDataCell cell = (TableDataCell) getComponent(); + TableHeaderCell h1 = new TableHeaderCell("Name"); + TableHeaderCell h2 = new TableHeaderCell("Age"); + h1.setId("name-h"); + h2.setId("age-h"); + cell.setHeaders(h1, h2); + assertEquals("name-h age-h", cell.getElement().getAttribute("headers"), + "headers attribute should reference the cells' ids"); + } + + @Test + void setHeaders_fromHeaderCells_throwsIfMissingId() { + TableDataCell cell = (TableDataCell) getComponent(); + TableHeaderCell h1 = new TableHeaderCell("Name"); + // No id set + assertThrows(IllegalArgumentException.class, () -> cell.setHeaders(h1)); + } + + @Test + void resetHeaders_removesAttribute() { + TableDataCell cell = (TableDataCell) getComponent(); + cell.setHeaders("name"); + cell.resetHeaders(); + assertNull(cell.getElement().getAttribute("headers")); + } + +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableFootTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableFootTest.java new file mode 100644 index 00000000000..b34c4fe98d3 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableFootTest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +public class TableFootTest extends ComponentTest { + // Actual test methods in super class + + @Override + protected void addProperties() { + // Component defines no new properties + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeadTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeadTest.java new file mode 100644 index 00000000000..3588c8c7cf4 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeadTest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +public class TableHeadTest extends ComponentTest { + // Actual test methods in super class + + @Override + protected void addProperties() { + // Component defines no new properties + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeaderCellTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeaderCellTest.java new file mode 100644 index 00000000000..3500cbc3b26 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeaderCellTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.beans.IntrospectionException; +import java.lang.reflect.InvocationTargetException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TableHeaderCellTest extends ComponentTest { + // Most tests in super class + + @BeforeEach + @Override + void setup() throws IntrospectionException, InstantiationException, + IllegalAccessException, ClassNotFoundException, + InvocationTargetException, NoSuchMethodException { + whitelistProperty("scope"); + super.setup(); + } + + @Override + protected void addProperties() { + super.addProperties(); + // Inherited from TableCell — same semantics as TableDataCell + addProperty("colspan", int.class, 1, 2, false, false); + addProperty("rowspan", int.class, 1, 2, false, false); + addProperty("headers", String[].class, null, new String[] { "a", "b" }, + true, true); + } + + @Test + void scope_unsetByDefault() { + TableHeaderCell th = (TableHeaderCell) getComponent(); + assertTrue(th.getScope().isEmpty()); + } + + @Test + void scope_setAndGet() { + TableHeaderCell th = (TableHeaderCell) getComponent(); + th.setScope(TableHeaderCell.Scope.COL); + assertEquals(TableHeaderCell.Scope.COL, th.getScope().orElseThrow()); + assertEquals("col", th.getElement().getAttribute("scope")); + } + + @Test + void scope_setNullClearsAttribute() { + TableHeaderCell th = (TableHeaderCell) getComponent(); + th.setScope(TableHeaderCell.Scope.ROW); + th.setScope(null); + assertTrue(th.getScope().isEmpty()); + assertEquals(null, th.getElement().getAttribute("scope")); + } + + @Test + void colspan_inheritedFromTableCell() { + TableHeaderCell th = (TableHeaderCell) getComponent(); + assertEquals(1, th.getColspan()); + th.setColspan(3); + assertEquals(3, th.getColspan()); + assertEquals("3", th.getElement().getAttribute("colspan")); + } + + @Test + void rowspan_inheritedFromTableCell() { + TableHeaderCell th = (TableHeaderCell) getComponent(); + assertEquals(1, th.getRowspan()); + th.setRowspan(2); + assertEquals(2, th.getRowspan()); + assertEquals("2", th.getElement().getAttribute("rowspan")); + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowContainerTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowContainerTest.java new file mode 100644 index 00000000000..c706f9550a7 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowContainerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TableRowContainerTest { + + private TableBody container; + + @BeforeEach + void setUp() { + container = new TableBody(); + } + + @Test + void addRow() { + var children = container.getElement().getChildren().toList(); + assertEquals(0, children.size()); + var row = container.addRow(); + children = container.getElement().getChildren().toList(); + assertEquals(1, children.size()); + AssertUtils.assertEquals(children.get(0), row.getElement(), + "Child is not added row"); + row = container.addRow(); + children = container.getElement().getChildren().toList(); + assertEquals(2, children.size()); + AssertUtils.assertEquals(children.get(1), row.getElement(), + "Child is not added row"); + } + + @Test + void getRows() { + for (int i = 0; i < 10; i++) { + container.addRow(); + } + var rows = container.getRows(); + assertEquals(10, rows.size()); + for (int i = 0; i < 10; i++) { + AssertUtils.assertEquals(rows.get(i).getElement(), + container.getElement().getChild(i), "row does not match"); + } + } + + @Test + void insertRow() { + var row0 = new TableRow(); + var row1 = new TableRow(); + var row2 = new TableRow(); + container.addRows(row0, row1, row2); + var newRow = container.insertRow(1); + var rows = container.getRows(); + assertEquals(4, rows.size()); + AssertUtils.assertEquals(newRow, rows.get(1), + "New row must be inserted at given position"); + } + + @Test + void removeAllRows() { + container.addRow(); + container.addRow(); + container.addRow(); + container.removeAllRows(); + assertEquals(0, container.getRows().size()); + } + + @Test + void removeRowsByReference() { + var row0 = container.addRow(); + var row1 = container.addRow(); + var row2 = container.addRow(); + var row3 = container.addRow(); + var row4 = container.addRow(); + container.removeRows(row1, row3); + assertTrue(row1.getParent().isEmpty()); + assertTrue(row3.getParent().isEmpty()); + assertEquals(3, container.getRows().size()); + AssertUtils.assertEquals(container, row0.getParent().orElseThrow(), + "row0 must not be removed"); + AssertUtils.assertEquals(container, row2.getParent().orElseThrow(), + "row2 must not be removed"); + AssertUtils.assertEquals(container, row4.getParent().orElseThrow(), + "row4 must not be removed"); + } + + @Test + void replaceRow() { + container.addRow(); + container.addRow(); + container.addRow(); + var newRow = new TableRow(); + container.replaceRow(1, newRow); + var rows = container.getRows(); + assertEquals(3, rows.size()); + AssertUtils.assertEquals(newRow, rows.get(1), + "Row must be replaced with new row"); + } + +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowTest.java new file mode 100644 index 00000000000..69c88664986 --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import org.junit.jupiter.api.Test; + +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.HasOrderedComponents; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class TableRowTest extends ComponentTest { + // Most tests in super class + + @Override + protected void addProperties() { + // Component defines no new properties + } + + @Test + void doesNotExposeGenericAddComponent() { + TableRow row = (TableRow) getComponent(); + assertFalse(row instanceof HasComponents, + "TableRow must not implement HasComponents"); + assertFalse(row instanceof HasOrderedComponents, + "TableRow must not implement HasOrderedComponents"); + } + + @Test + void constructor_acceptsCells() { + TableDataCell td = new TableDataCell("data"); + TableHeaderCell th = new TableHeaderCell("hdr"); + TableRow row = new TableRow(th, td); + assertEquals(2, row.getCells().size()); + assertEquals(1, row.getDataCells().size()); + assertEquals(1, row.getHeaderCells().size()); + } + + @Test + void addDataCell_appendsTd() { + TableRow row = new TableRow(); + TableDataCell td = row.addDataCell(); + assertEquals(1, row.getDataCells().size()); + assertEquals(td, row.getDataCells().get(0)); + } + + @Test + void addHeaderCell_appendsTh() { + TableRow row = new TableRow(); + TableHeaderCell th = row.addHeaderCell("Name"); + assertEquals(1, row.getHeaderCells().size()); + assertEquals(th, row.getHeaderCells().get(0)); + assertEquals("Name", th.getText()); + } + + @Test + void addDataCells_appendsAll() { + TableRow row = new TableRow(); + TableRow result = row.addDataCells("a", "b", "c"); + assertEquals(row, result); + assertEquals(3, row.getDataCells().size()); + assertEquals("a", row.getDataCells().get(0).getText()); + assertEquals("b", row.getDataCells().get(1).getText()); + assertEquals("c", row.getDataCells().get(2).getText()); + } + + @Test + void addHeaderCells_appendsAll() { + TableRow row = new TableRow(); + TableRow result = row.addHeaderCells("Name", "Age"); + assertEquals(row, result); + assertEquals(2, row.getHeaderCells().size()); + } + + @Test + void addCells_appendsPreBuiltCells() { + TableRow row = new TableRow(); + TableHeaderCell th = new TableHeaderCell("Name"); + TableDataCell td = new TableDataCell("Alice"); + row.addCells(th, td); + assertEquals(1, row.getHeaderCells().size()); + assertEquals(1, row.getDataCells().size()); + } + + @Test + void constructor_wrapsNonCellComponentsInDataCell() { + Span span = new Span("hi"); + TableHeaderCell th = new TableHeaderCell("Name"); + TableRow row = new TableRow(span, th); + + assertEquals(2, row.getCells().size()); + // span got wrapped in a new TableDataCell + TableDataCell wrapper = row.getDataCells().get(0); + assertEquals(span, wrapper.getChildren().findFirst().orElseThrow()); + // header cell preserved as-is + assertEquals(th, row.getHeaderCells().get(0)); + } + + @Test + void addCells_wrapsNonCellComponentsInDataCell() { + TableRow row = new TableRow(); + Span span = new Span("hi"); + row.addCells(span); + + assertEquals(1, row.getDataCells().size()); + assertEquals(span, row.getDataCells().get(0).getChildren().findFirst() + .orElseThrow()); + } + + @Test + void addCells_listOverloadMatchesVarargs() { + TableRow row = new TableRow(); + TableDataCell td = new TableDataCell("a"); + TableHeaderCell th = new TableHeaderCell("h"); + row.addCells(java.util.List.of(td, th)); + + assertEquals(2, row.getCells().size()); + assertEquals(td, row.getCells().get(0)); + assertEquals(th, row.getCells().get(1)); + } + + @Test + void addDataCells_listOverloadMatchesVarargs() { + TableRow row = new TableRow(); + row.addDataCells(java.util.List.of("a", "b", "c")); + assertEquals(3, row.getDataCells().size()); + assertEquals("a", row.getDataCells().get(0).getText()); + assertEquals("c", row.getDataCells().get(2).getText()); + } + + @Test + void removeCell_dropsFromRow() { + TableRow row = new TableRow(); + TableHeaderCell th = row.addHeaderCell("Name"); + TableDataCell td = row.addDataCell("Alice"); + row.removeCell(th); + assertEquals(1, row.getCells().size()); + assertEquals(td, row.getCells().get(0)); + } + + @Test + void addRowHeaderCell_setsScopeRow() { + TableRow row = new TableRow(); + TableHeaderCell th = row.addRowHeaderCell("Cucumber"); + assertEquals("Cucumber", th.getText()); + assertEquals(TableHeaderCell.Scope.ROW, th.getScope().orElseThrow()); + assertEquals(1, row.getHeaderCells().size()); + assertEquals(th, row.getHeaderCells().get(0)); + } +} diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableTest.java new file mode 100644 index 00000000000..80e6637628d --- /dev/null +++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableTest.java @@ -0,0 +1,454 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.html; + +import java.beans.IntrospectionException; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.HasOrderedComponents; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TableTest extends ComponentTest { + // Actual test methods in super class + + @BeforeEach + @Override + void setup() throws IntrospectionException, InstantiationException, + IllegalAccessException, ClassNotFoundException, + InvocationTargetException, NoSuchMethodException { + whitelistProperty("captionText"); + super.setup(); + } + + @Test + void getCaption() { + var component = (Table) getComponent(); + TableCaption caption = component.getCaption(); + AssertUtils.assertEquals(component.getChildren().toList().get(0), + caption, "Caption does not match"); + } + + @Test + void addsCaptionAsFirstChild() { + var component = (Table) getComponent(); + assertEquals(0, component.getChildren().count()); + component.getHead(); + component.addBody(); + component.getFoot(); + var caption = component.getCaption(); + assertEquals(4, component.getChildren().count()); + AssertUtils.assertEquals(caption, + component.getChildren().findFirst().orElseThrow(), + "Caption is not the first child"); + AssertUtils.assertEquals(caption.getParent().orElseThrow(), component, + "Table is not the caption's father"); + } + + @Test + void setCaptionText() { + var component = (Table) getComponent(); + String expectedText = "Test caption text."; + component.setCaptionText(expectedText); + var caption = component.getCaption(); + assertEquals(expectedText, caption.getText()); + } + + @Test + void getCaptionText_emptyWhenNoCaption() { + var component = (Table) getComponent(); + assertEquals("", component.getCaptionText()); + assertTrue(component.findCaption().isEmpty()); + } + + @Test + void getCaptionText_returnsCaptionText() { + var component = (Table) getComponent(); + String expectedText = "Test caption text."; + var caption = component.getCaption(); + caption.setText(expectedText); + assertEquals(expectedText, component.getCaptionText()); + } + + @Test + void removeCaption() { + var component = (Table) getComponent(); + var caption = component.getCaption(); + component.removeCaption(); + assertTrue(caption.getParent().isEmpty()); + assertTrue(component.findCaption().isEmpty()); + } + + @Test + void getHead() { + var component = (Table) getComponent(); + assertEquals(0, component.getChildren().count()); + TableHead head = component.getHead(); + AssertUtils.assertEquals(component, head.getParent().orElseThrow(), + "head was not added"); + } + + @Test + void addHeadAfterCaption() { + var component = (Table) getComponent(); + component.getCaption(); + var head = component.getHead(); + assertEquals(2, component.getChildren().count()); + int headIndex = component.getChildren().toList().indexOf(head); + assertEquals(1, headIndex); + } + + @Test + void removeHead() { + var component = (Table) getComponent(); + TableHead head = component.getHead(); + component.removeHead(); + assertTrue(head.getParent().isEmpty()); + assertTrue(component.findHead().isEmpty()); + } + + @Test + void getFoot() { + var component = (Table) getComponent(); + assertEquals(0, component.getChildren().count()); + TableFoot footer = component.getFoot(); + AssertUtils.assertEquals(component, footer.getParent().orElseThrow(), + "footer was not added"); + } + + @Test + void removeFoot() { + var component = (Table) getComponent(); + TableFoot footer = component.getFoot(); + component.removeFoot(); + assertTrue(footer.getParent().isEmpty()); + assertTrue(component.findFoot().isEmpty()); + } + + @Test + void addBody() { + var component = (Table) getComponent(); + component.addBody(); + assertEquals(1, component.getChildren().count()); + component.addBody(); + assertEquals(2, component.getChildren().count()); + } + + @Test + void addBodyAfterCaption() { + var component = (Table) getComponent(); + component.getCaption(); + var body = component.addBody(); + assertEquals(1, component.getChildren().toList().indexOf(body)); + } + + @Test + void addBodyAfterHeader() { + var component = (Table) getComponent(); + component.getHead(); + var body = component.addBody(); + assertEquals(1, component.getChildren().toList().indexOf(body)); + } + + @Test + void addBodyAfterBothCaptionAndHeader() { + var component = (Table) getComponent(); + component.getCaption(); + component.getHead(); + var body = component.addBody(); + assertEquals(2, component.getChildren().toList().indexOf(body)); + } + + @Test + void addBodyBeforeFoot() { + var component = (Table) getComponent(); + component.getFoot(); + var body = component.addBody(); + assertEquals(0, component.getChildren().toList().indexOf(body)); + assertEquals(1, + component.getChildren().toList().indexOf(component.getFoot())); + } + + @Test + void getBody() { + var component = (Table) getComponent(); + var body = component.getBody(); + assertEquals(1, component.getChildren().count()); + component.addBody(); + assertEquals(2, component.getChildren().count()); + var secondCallBody = component.getBody(); + AssertUtils.assertEquals(body, secondCallBody, + "No new body should've been created"); + } + + @Test + void getBodies() { + var component = (Table) getComponent(); + for (int i = 0; i < 10; i++) { + component.addBody(); + } + List bodies = component.getBodies(); + for (TableBody body : bodies) { + AssertUtils.assertEquals(component, body.getParent().orElseThrow(), + "Body is not a child of table"); + } + } + + @Test + void removeBodyByReference() { + var component = (Table) getComponent(); + var body0 = component.addBody(); + var body1 = component.addBody(); + var body2 = component.addBody(); + component.removeBody(body1); + assertTrue(body0.getParent().isPresent()); + assertTrue(body1.getParent().isEmpty()); + assertTrue(body2.getParent().isPresent()); + } + + @Test + void addRow_autoCreatesBody() { + var table = (Table) getComponent(); + TableRow row = table.addRow(); + assertTrue(table.findHead().isEmpty()); + assertEquals(1, table.getBodies().size()); + assertEquals(1, table.getBody().getRows().size()); + AssertUtils.assertEquals(table.getBody(), row.getParent().orElseThrow(), + "row must live inside the auto-created tbody"); + } + + @Test + void addRow_withCellTexts_createsDataCells() { + var table = (Table) getComponent(); + TableRow row = table.addRow("Alice", "30", "Blue"); + assertEquals(3, row.getDataCells().size()); + assertEquals("Alice", row.getDataCells().get(0).getText()); + assertEquals("30", row.getDataCells().get(1).getText()); + assertEquals("Blue", row.getDataCells().get(2).getText()); + assertEquals(0, row.getHeaderCells().size()); + } + + @Test + void addHeaderRow_autoCreatesThead() { + var table = (Table) getComponent(); + TableRow row = table.addHeaderRow("Name", "Age"); + assertTrue(table.findHead().isPresent()); + assertEquals(1, table.getHead().getRows().size()); + assertEquals(2, row.getHeaderCells().size()); + assertEquals("Name", row.getHeaderCells().get(0).getText()); + } + + @Test + void addFooterRow_autoCreatesTfoot() { + var table = (Table) getComponent(); + TableRow row = table.addFooterRow("Total", "55"); + assertTrue(table.findFoot().isPresent()); + assertEquals(1, table.getFoot().getRows().size()); + assertEquals(2, row.getDataCells().size()); + } + + @Test + void mdnTutorialStyleConstruction() { + // Mirrors the MDN "HTML table basics" walkthrough: caption, header + // row, body rows. Verifies the resulting structure is spec-compliant + // (caption first, thead before tbody) and that all rows landed in + // the right wrappers. + var table = (Table) getComponent(); + table.setCaptionText("People"); + table.addHeaderRow("Name", "Age", "Color"); + table.addRow("Alice", "30", "Blue"); + table.addRow("Bob", "25", "Green"); + + assertEquals("People", table.getCaptionText()); + assertEquals(1, table.getHead().getRows().size()); + assertEquals(2, table.getBody().getRows().size()); + + var children = table.getChildren().toList(); + assertEquals(table.getCaption(), children.get(0)); + assertEquals(table.getHead(), children.get(1)); + assertEquals(table.getBody(), children.get(2)); + } + + @Test + void addRows_addsExistingRowsToBody() { + var table = (Table) getComponent(); + var r1 = new TableRow(); + var r2 = new TableRow(); + table.addRows(r1, r2); + assertEquals(2, table.getBody().getRows().size()); + AssertUtils.assertEquals(table.getBody(), r1.getParent().orElseThrow(), + "r1 must be a child of tbody"); + AssertUtils.assertEquals(table.getBody(), r2.getParent().orElseThrow(), + "r2 must be a child of tbody"); + } + + @Test + void addCaption_createsAndAppendsComponents() { + var table = (Table) getComponent(); + var span = new Span("Cars"); + var caption = table.addCaption(span); + assertEquals(1, caption.getComponentCount()); + assertEquals(span, caption.getComponentAt(0)); + assertEquals(table.getCaption(), caption); + } + + @Test + void addColumnGroup_insertedAfterCaptionBeforeHead() { + var table = (Table) getComponent(); + table.getCaption(); + table.getHead(); + var group = table.addColumnGroup(); + var children = table.getChildren().toList(); + assertEquals(table.getCaption(), children.get(0)); + assertEquals(group, children.get(1)); + assertEquals(table.getHead(), children.get(2)); + } + + @Test + void addColumnGroup_beforeHeadEvenIfHeadAddedLater() { + var table = (Table) getComponent(); + var group = table.addColumnGroup(); + var head = table.getHead(); + var children = table.getChildren().toList(); + assertEquals(group, children.get(0)); + assertEquals(head, children.get(1)); + } + + @Test + void addColumnGroup_withColumns() { + var table = (Table) getComponent(); + var c1 = new TableColumn(); + var c2 = new TableColumn(2); + var group = table.addColumnGroup(c1, c2); + assertEquals(2, group.getColumns().size()); + assertEquals(List.of(group), table.getColumnGroups()); + } + + @Test + void multipleColumnGroups_appearInInsertionOrder() { + var table = (Table) getComponent(); + var g1 = table.addColumnGroup(); + var g2 = table.addColumnGroup(); + var children = table.getChildren().toList(); + assertEquals(g1, children.get(0)); + assertEquals(g2, children.get(1)); + } + + @Test + void removeColumnGroup() { + var table = (Table) getComponent(); + var g1 = table.addColumnGroup(); + var g2 = table.addColumnGroup(); + table.removeColumnGroup(g1); + assertEquals(List.of(g2), table.getColumnGroups()); + assertTrue(g1.getParent().isEmpty()); + } + + @Test + void bodyAppendIndex_accountsForColumnGroups() { + // caption + 2 colgroups + thead → tbody must land at index 4 + var table = (Table) getComponent(); + table.setCaptionText("x"); + table.addColumnGroup(); + table.addColumnGroup(); + table.getHead(); + var body = table.addBody(); + assertEquals(4, table.getChildren().toList().indexOf(body)); + } + + @Test + void getRows_emptyWhenNoSections() { + var table = (Table) getComponent(); + assertTrue(table.getRows().isEmpty()); + } + + @Test + void getRows_returnsHeadBodiesFootInOrder() { + var table = (Table) getComponent(); + var headRow = table.addHeaderRow("Name"); + var bodyRow1 = table.addRow("Alice"); + var bodyRow2 = table.addRow("Bob"); + var footRow = table.addFooterRow("Total"); + + var rows = table.getRows(); + assertEquals(List.of(headRow, bodyRow1, bodyRow2, footRow), rows); + } + + @Test + void getRows_concatenatesMultipleBodies() { + var table = (Table) getComponent(); + var b1Row = table.getBody().addRow(); + var b2Row = table.addBody().addRow(); + var rows = table.getRows(); + assertEquals(List.of(b1Row, b2Row), rows); + } + + @Test + void getRows_isUnmodifiable() { + var table = (Table) getComponent(); + table.addRow(); + var rows = table.getRows(); + assertThrows(UnsupportedOperationException.class, + () -> rows.add(new TableRow())); + } + + @Test + void removeAllRows_clearsRowsButKeepsSections() { + var table = (Table) getComponent(); + table.addHeaderRow("h"); + table.addRow("b1"); + table.addRow("b2"); + table.addFooterRow("f"); + + table.removeAllRows(); + + assertTrue(table.getRows().isEmpty()); + // Sections themselves remain + assertTrue(table.findHead().isPresent()); + assertEquals(1, table.getBodies().size()); + assertTrue(table.findFoot().isPresent()); + assertEquals(0, table.getHead().getRows().size()); + assertEquals(0, table.getBody().getRows().size()); + assertEquals(0, table.getFoot().getRows().size()); + } + + @Test + void removeAllRows_isNoOpOnEmptyTable() { + var table = (Table) getComponent(); + table.removeAllRows(); + assertTrue(table.getRows().isEmpty()); + } + + @Test + void doesNotExposeGenericAddComponent() { + var component = (Table) getComponent(); + // The strict Table API must not expose generic add(Component) or + // any of the other arbitrary-child container helpers. + assertFalse(component instanceof HasComponents, + "Table must not implement HasComponents"); + assertFalse(component instanceof HasOrderedComponents, + "Table must not implement HasOrderedComponents"); + } + +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Tag.java b/flow-server/src/main/java/com/vaadin/flow/component/Tag.java index e122c5eddc3..6a0898f6832 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/Tag.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/Tag.java @@ -66,6 +66,14 @@ * Tag for a <code>. */ String CODE = "code"; + /** + * Tag for a <col>. + */ + String COL = "col"; + /** + * Tag for a <colgroup>. + */ + String COLGROUP = "colgroup"; /** * Tag for an <dd>. */