Repository: Textualize/textual

Issue:
Auto expanding height
You can set a height when you add a row to the DataTable, but there is currently no way to make it expand to fit the content.

I think that `add_row` should grow an option to automatically calculate the optimal height of a row.

If we make this the default, we should be able to explicitly override it with a height. Perhaps a `height` of None means auto-detect, and a integer sets it explicitly. An explicit height will be faster, which might be significant if the user adds a lot of rows.


Discussion:
+1 this would be great to have. Rich's `Table` currently supports this behavior.
This does actually work. These is a edge case with an auto height table, in an auto height container. The max height of the table is set to 100% which prevents the table from expanding. We need a workaround for that/

Resolving patch:
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -33,7 +33,7 @@
 from ..widget import PseudoClasses
 
 CellCacheKey: TypeAlias = (
-    "tuple[RowKey, ColumnKey, Style, bool, bool, int, PseudoClasses]"
+    "tuple[RowKey, ColumnKey, Style, bool, bool, bool, int, PseudoClasses]"
 )
 LineCacheKey: TypeAlias = "tuple[int, int, int, int, Coordinate, Coordinate, Style, CursorType, bool, int, PseudoClasses]"
 RowCacheKey: TypeAlias = "tuple[RowKey, int, Style, Coordinate, Coordinate, CursorType, bool, bool, int, PseudoClasses]"
@@ -187,6 +187,7 @@ class Row:
     key: RowKey
     height: int
     label: Text | None = None
+    auto_height: bool = False
 
 
 class RowRenderables(NamedTuple):
@@ -951,6 +952,7 @@ def _clear_caches(self) -> None:
         self._styles_cache.clear()
         self._offset_cache.clear()
         self._ordered_row_cache.clear()
+        self._get_styles_to_render_cell.cache_clear()
 
     def get_row_height(self, row_key: RowKey) -> int:
         """Given a row key, return the height of that row in terminal cells.
@@ -1190,8 +1192,16 @@ def _update_column_widths(self, updated_cells: set[CellKey]) -> None:
         self._require_update_dimensions = True
 
     def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
-        """Called to recalculate the virtual (scrollable) size."""
+        """Called to recalculate the virtual (scrollable) size.
+
+        This recomputes column widths and then checks if any of the new rows need
+        to have their height computed.
+
+        Args:
+            new_rows: The new rows that will affect the `DataTable` dimensions.
+        """
         console = self.app.console
+        auto_height_rows: list[tuple[int, Row, list[RenderableType]]] = []
         for row_key in new_rows:
             row_index = self._row_locations.get(row_key)
 
@@ -1201,6 +1211,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
                 continue
 
             row = self.rows.get(row_key)
+            assert row is not None
 
             if row.label is not None:
                 self._labelled_row_exists = True
@@ -1215,7 +1226,65 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
                 content_width = measure(console, renderable, 1)
                 column.content_width = max(column.content_width, content_width)
 
-        self._clear_caches()
+            if row.auto_height:
+                auto_height_rows.append((row_index, row, cells_in_row))
+
+        # If there are rows that need to have their height computed, render them correctly
+        # so that we can cache this rendering for later.
+        if auto_height_rows:
+            render_cell = self._render_cell  # This method renders & caches.
+            should_highlight = self._should_highlight
+            cursor_type = self.cursor_type
+            cursor_location = self.cursor_coordinate
+            hover_location = self.hover_coordinate
+            base_style = self.rich_style
+            fixed_style = self.get_component_styles(
+                "datatable--fixed"
+            ).rich_style + Style.from_meta({"fixed": True})
+            ordered_columns = self.ordered_columns
+            fixed_columns = self.fixed_columns
+
+            for row_index, row, cells_in_row in auto_height_rows:
+                height = 0
+                row_style = self._get_row_style(row_index, base_style)
+
+                # As we go through the cells, save their rendering, height, and
+                # column width. After we compute the height of the row, go over the cells
+                # that were rendered with the wrong height and append the missing padding.
+                rendered_cells: list[tuple[SegmentLines, int, int]] = []
+                for column_index, column in enumerate(ordered_columns):
+                    style = fixed_style if column_index < fixed_columns else row_style
+                    cell_location = Coordinate(row_index, column_index)
+                    rendered_cell = render_cell(
+                        row_index,
+                        column_index,
+                        style,
+                        column.render_width,
+                        cursor=should_highlight(
+                            cursor_location, cell_location, cursor_type
+                        ),
+                        hover=should_highlight(
+                            hover_location, cell_location, cursor_type
+                        ),
+                    )
+                    cell_height = len(rendered_cell)
+                    rendered_cells.append(
+                        (rendered_cell, cell_height, column.render_width)
+                    )
+                    height = max(height, cell_height)
+
+                row.height = height
+                # Do surgery on the cache for cells that were rendered with the incorrect
+                # height during the first pass.
+                for cell_renderable, cell_height, column_width in rendered_cells:
+                    if cell_height < height:
+                        first_line_space_style = cell_renderable[0][0].style
+                        cell_renderable.extend(
+                            [
+                                [Segment(" " * column_width, first_line_space_style)]
+                                for _ in range(height - cell_height)
+                            ]
+                        )
 
         data_cells_width = sum(column.render_width for column in self.columns.values())
         total_width = data_cells_width + self._row_label_column_width
@@ -1373,7 +1442,7 @@ def add_column(
     def add_row(
         self,
         *cells: CellType,
-        height: int = 1,
+        height: int | None = 1,
         key: str | None = None,
         label: TextType | None = None,
     ) -> RowKey:
@@ -1381,13 +1450,14 @@ def add_row(
 
         Args:
             *cells: Positional arguments should contain cell data.
-            height: The height of a row (in lines).
+            height: The height of a row (in lines). Use `None` to auto-detect the optimal
+                height.
             key: A key which uniquely identifies this row. If None, it will be generated
                 for you and returned.
             label: The label for the row. Will be displayed to the left if supplied.
 
         Returns:
-            Uniquely identifies this row. Can be used to retrieve this row regardless
+            Unique identifier for this row. Can be used to retrieve this row regardless
                 of its current location in the DataTable (it could have moved after
                 being added due to sorting or insertion/deletion of other rows).
         """
@@ -1407,7 +1477,15 @@ def add_row(
             for column, cell in zip_longest(self.ordered_columns, cells)
         }
         label = Text.from_markup(label) if isinstance(label, str) else label
-        self.rows[row_key] = Row(row_key, height, label)
+        # Rows with auto-height get a height of 0 because 1) we need an integer height
+        # to do some intermediate computations and 2) because 0 doesn't impact the data
+        # table while we don't figure out how tall this row is.
+        self.rows[row_key] = Row(
+            row_key,
+            height or 0,
+            label,
+            height is None,
+        )
         self._new_rows.add(row_key)
         self._require_update_dimensions = True
         self.cursor_coordinate = self.cursor_coordinate
@@ -1546,7 +1624,8 @@ async def _on_idle(self, _: events.Idle) -> None:
 
         if self._require_update_dimensions:
             # Add the new rows *before* updating the column widths, since
-            # cells in a new row may influence the final width of a column
+            # cells in a new row may influence the final width of a column.
+            # Only then can we compute optimal height of rows with "auto" height.
             self._require_update_dimensions = False
             new_rows = self._new_rows.copy()
             self._new_rows.clear()
@@ -1754,7 +1833,7 @@ def _render_cell(
             row_key = self._row_locations.get_key(row_index)
 
         column_key = self._column_locations.get_key(column_index)
-        cell_cache_key = (
+        cell_cache_key: CellCacheKey = (
             row_key,
             column_key,
             base_style,
@@ -1767,7 +1846,6 @@ def _render_cell(
 
         if cell_cache_key not in self._cell_render_cache:
             base_style += Style.from_meta({"row": row_index, "column": column_index})
-            height = self.header_height if is_header_cell else self.rows[row_key].height
             row_label, row_cells = self._get_row_renderables(row_index)
 
             if is_row_label_cell:
@@ -1775,50 +1853,104 @@ def _render_cell(
             else:
                 cell = row_cells[column_index]
 
-            get_component = self.get_component_rich_style
-            show_cursor = self.show_cursor
-            component_style = Style()
-
-            if hover and show_cursor and self._show_hover_cursor:
-                component_style += get_component("datatable--hover")
-                if is_header_cell or is_row_label_cell:
-                    # Apply subtle variation in style for the header/label (blue background by
-                    # default) rows and columns affected by the cursor, to ensure we can
-                    # still differentiate between the labels and the data.
-                    component_style += get_component("datatable--header-hover")
-
-            if cursor and show_cursor:
-                cursor_style = get_component("datatable--cursor")
-                component_style += cursor_style
-                if is_header_cell or is_row_label_cell:
-                    component_style += get_component("datatable--header-cursor")
-                elif is_fixed_style_cell:
-                    component_style += get_component("datatable--fixed-cursor")
-
-            post_foreground = (
-                Style.from_color(color=component_style.color)
-                if self.cursor_foreground_priority == "css"
-                else Style.null()
-            )
-            post_background = (
-                Style.from_color(bgcolor=component_style.bgcolor)
-                if self.cursor_background_priority == "css"
-                else Style.null()
+            component_style, post_style = self._get_styles_to_render_cell(
+                is_header_cell,
+                is_row_label_cell,
+                is_fixed_style_cell,
+                hover,
+                cursor,
+                self.show_cursor,
+                self._show_hover_cursor,
+                self.cursor_foreground_priority == "css",
+                self.cursor_background_priority == "css",
             )
 
+            if is_header_cell:
+                options = self.app.console.options.update_dimensions(
+                    width, self.header_height
+                )
+            else:
+                row = self.rows[row_key]
+                # If an auto-height row hasn't had its height calculated, we don't fix
+                # the value for `height` so that we can measure the height of the cell.
+                if row.auto_height and row.height == 0:
+                    options = self.app.console.options.update_width(width)
+                else:
+                    options = self.app.console.options.update_dimensions(
+                        width, row.height
+                    )
             lines = self.app.console.render_lines(
                 Styled(
                     Padding(cell, (0, 1)),
                     pre_style=base_style + component_style,
-                    post_style=post_foreground + post_background,
+                    post_style=post_style,
                 ),
-                self.app.console.options.update_dimensions(width, height),
+                options,
             )
 
             self._cell_render_cache[cell_cache_key] = lines
 
         return self._cell_render_cache[cell_cache_key]
 
+    @functools.lru_cache(maxsize=32)
+    def _get_styles_to_render_cell(
+        self,
+        is_header_cell: bool,
+        is_row_label_cell: bool,
+        is_fixed_style_cell: bool,
+        hover: bool,
+        cursor: bool,
+        show_cursor: bool,
+        show_hover_cursor: bool,
+        has_css_foreground_priority: bool,
+        has_css_background_priority: bool,
+    ) -> tuple[Style, Style]:
+        """Auxiliary method to compute styles used to render a given cell.
+
+        Args:
+            is_header_cell: Is this a cell from a header?
+            is_row_label_cell: Is this the label of any given row?
+            is_fixed_style_cell: Should this cell be styled like a fixed cell?
+            hover: Does this cell have the hover pseudo class?
+            cursor: Is this cell covered by the cursor?
+            show_cursor: Do we want to show the cursor in the data table?
+            show_hover_cursor: Do we want to show the mouse hover when using the keyboard
+                to move the cursor?
+            has_css_foreground_priority: `self.cursor_foreground_priority == "css"`?
+            has_css_background_priority: `self.cursor_background_priority == "css"`?
+        """
+        get_component = self.get_component_rich_style
+        component_style = Style()
+
+        if hover and show_cursor and show_hover_cursor:
+            component_style += get_component("datatable--hover")
+            if is_header_cell or is_row_label_cell:
+                # Apply subtle variation in style for the header/label (blue background by
+                # default) rows and columns affected by the cursor, to ensure we can
+                # still differentiate between the labels and the data.
+                component_style += get_component("datatable--header-hover")
+
+        if cursor and show_cursor:
+            cursor_style = get_component("datatable--cursor")
+            component_style += cursor_style
+            if is_header_cell or is_row_label_cell:
+                component_style += get_component("datatable--header-cursor")
+            elif is_fixed_style_cell:
+                component_style += get_component("datatable--fixed-cursor")
+
+        post_foreground = (
+            Style.from_color(color=component_style.color)
+            if has_css_foreground_priority
+            else Style.null()
+        )
+        post_background = (
+            Style.from_color(bgcolor=component_style.bgcolor)
+            if has_css_background_priority
+            else Style.null()
+        )
+
+        return component_style, post_foreground + post_background
+
     def _render_line_in_row(
         self,
         row_key: RowKey,
@@ -1859,29 +1991,9 @@ def _render_line_in_row(
         if cache_key in self._row_render_cache:
             return self._row_render_cache[cache_key]
 
-        def _should_highlight(
-            cursor: Coordinate,
-            target_cell: Coordinate,
-            type_of_cursor: CursorType,
-        ) -> bool:
-            """Determine whether we should highlight a cell given the location
-            of the cursor, the location of the cell, and the type of cursor that
-            is currently active."""
-            if type_of_cursor == "cell":
-                return cursor == target_cell
-            elif type_of_cursor == "row":
-                cursor_row, _ = cursor
-                cell_row, _ = target_cell
-                return cursor_row == cell_row
-            elif type_of_cursor == "column":
-                _, cursor_column = cursor
-                _, cell_column = target_cell
-                return cursor_column == cell_column
-            else:
-                return False
-
-        is_header_row = row_key is self._header_row_key
+        should_highlight = self._should_highlight
         render_cell = self._render_cell
+        header_style = self.get_component_styles("datatable--header").rich_style
 
         if row_key in self._row_locations:
             row_index = self._row_locations.get(row_key)
@@ -1890,7 +2002,6 @@ def _should_highlight(
 
         # If the row has a label, add it to fixed_row here with correct style.
         fixed_row = []
-        header_style = self.get_component_styles("datatable--header").rich_style
 
         if self._labelled_row_exists and self.show_row_labels:
             # The width of the row label is updated again on idle
@@ -1900,14 +2011,17 @@ def _should_highlight(
                 -1,
                 header_style,
                 width=self._row_label_column_width,
-                cursor=_should_highlight(cursor_location, cell_location, cursor_type),
-                hover=_should_highlight(hover_location, cell_location, cursor_type),
+                cursor=should_highlight(cursor_location, cell_location, cursor_type),
+                hover=should_highlight(hover_location, cell_location, cursor_type),
             )[line_no]
             fixed_row.append(label_cell_lines)
 
         if self.fixed_columns:
-            fixed_style = self.get_component_styles("datatable--fixed").rich_style
-            fixed_style += Style.from_meta({"fixed": True})
+            if row_key is self._header_row_key:
+                fixed_style = header_style  # We use the header style either way.
+            else:
+                fixed_style = self.get_component_styles("datatable--fixed").rich_style
+                fixed_style += Style.from_meta({"fixed": True})
             for column_index, column in enumerate(
                 self.ordered_columns[: self.fixed_columns]
             ):
@@ -1915,28 +2029,16 @@ def _should_highlight(
                 fixed_cell_lines = render_cell(
                     row_index,
                     column_index,
-                    header_style if is_header_row else fixed_style,
+                    fixed_style,
                     column.render_width,
-                    cursor=_should_highlight(
+                    cursor=should_highlight(
                         cursor_location, cell_location, cursor_type
                     ),
-                    hover=_should_highlight(hover_location, cell_location, cursor_type),
+                    hover=should_highlight(hover_location, cell_location, cursor_type),
                 )[line_no]
                 fixed_row.append(fixed_cell_lines)
 
-        is_header_row = row_key is self._header_row_key
-        if is_header_row:
-            row_style = self.get_component_styles("datatable--header").rich_style
-        elif row_index < self.fixed_rows:
-            row_style = self.get_component_styles("datatable--fixed").rich_style
-        else:
-            if self.zebra_stripes:
-                component_row_style = (
-                    "datatable--odd-row" if row_index % 2 else "datatable--even-row"
-                )
-                row_style = self.get_component_styles(component_row_style).rich_style
-            else:
-                row_style = base_style
+        row_style = self._get_row_style(row_index, base_style)
 
         scrollable_row = []
         for column_index, column in enumerate(self.ordered_columns):
@@ -1946,8 +2048,8 @@ def _should_highlight(
                 column_index,
                 row_style,
                 column.render_width,
-                cursor=_should_highlight(cursor_location, cell_location, cursor_type),
-                hover=_should_highlight(hover_location, cell_location, cursor_type),
+                cursor=should_highlight(cursor_location, cell_location, cursor_type),
+                hover=should_highlight(hover_location, cell_location, cursor_type),
             )[line_no]
             scrollable_row.append(cell_lines)
 
@@ -2075,6 +2177,63 @@ def render_line(self, y: int) -> Strip:
 
         return self._render_line(y, scroll_x, scroll_x + width, self.rich_style)
 
+    def _should_highlight(
+        self,
+        cursor: Coordinate,
+        target_cell: Coordinate,
+        type_of_cursor: CursorType,
+    ) -> bool:
+        """Determine if the given cell should be highlighted because of the cursor.
+
+        This auxiliary method takes the cursor position and type into account when
+        determining whether the cell should be highlighted.
+
+        Args:
+            cursor: The current position of the cursor.
+            target_cell: The cell we're checking for the need to highlight.
+            type_of_cursor: The type of cursor that is currently active.
+
+        Returns:
+            Whether or not the given cell should be highlighted.
+        """
+        if type_of_cursor == "cell":
+            return cursor == target_cell
+        elif type_of_cursor == "row":
+            cursor_row, _ = cursor
+            cell_row, _ = target_cell
+            return cursor_row == cell_row
+        elif type_of_cursor == "column":
+            _, cursor_column = cursor
+            _, cell_column = target_cell
+            return cursor_column == cell_column
+        else:
+            return False
+
+    def _get_row_style(self, row_index: int, base_style: Style) -> Style:
+        """Gets the Style that should be applied to the row at the given index.
+
+        Args:
+            row_index: The index of the row to style.
+            base_style: The base style to use by default.
+
+        Returns:
+            The appropriate style.
+        """
+
+        if row_index == -1:
+            row_style = self.get_component_styles("datatable--header").rich_style
+        elif row_index < self.fixed_rows:
+            row_style = self.get_component_styles("datatable--fixed").rich_style
+        else:
+            if self.zebra_stripes:
+                component_row_style = (
+                    "datatable--odd-row" if row_index % 2 else "datatable--even-row"
+                )
+                row_style = self.get_component_styles(component_row_style).rich_style
+            else:
+                row_style = base_style
+        return row_style
+
     def _on_mouse_move(self, event: events.MouseMove):
         """If the hover cursor is visible, display it by extracting the row
         and column metadata from the segments present in the cells."""


Test patch:
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -12731,6 +12731,322 @@
   
   '''
 # ---
+# name: test_datatable_add_row_auto_height
+  '''
+  <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
+      <!-- Generated with Rich https://www.textualize.io -->
+      <style>
+  
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Regular"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+          font-style: normal;
+          font-weight: 400;
+      }
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Bold"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+          font-style: bold;
+          font-weight: 700;
+      }
+  
+      .terminal-3912008695-matrix {
+          font-family: Fira Code, monospace;
+          font-size: 20px;
+          line-height: 24.4px;
+          font-variant-east-asian: full-width;
+      }
+  
+      .terminal-3912008695-title {
+          font-size: 18px;
+          font-weight: bold;
+          font-family: arial;
+      }
+  
+      .terminal-3912008695-r1 { fill: #dde6ed;font-weight: bold }
+  .terminal-3912008695-r2 { fill: #dde6ed }
+  .terminal-3912008695-r3 { fill: #c5c8c6 }
+  .terminal-3912008695-r4 { fill: #211505 }
+  .terminal-3912008695-r5 { fill: #e1e1e1 }
+      </style>
+  
+      <defs>
+      <clipPath id="terminal-3912008695-clip-terminal">
+        <rect x="0" y="0" width="975.0" height="584.5999999999999" />
+      </clipPath>
+      <clipPath id="terminal-3912008695-line-0">
+      <rect x="0" y="1.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-1">
+      <rect x="0" y="25.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-2">
+      <rect x="0" y="50.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-3">
+      <rect x="0" y="74.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-4">
+      <rect x="0" y="99.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-5">
+      <rect x="0" y="123.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-6">
+      <rect x="0" y="147.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-7">
+      <rect x="0" y="172.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-8">
+      <rect x="0" y="196.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-9">
+      <rect x="0" y="221.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-10">
+      <rect x="0" y="245.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-11">
+      <rect x="0" y="269.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-12">
+      <rect x="0" y="294.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-13">
+      <rect x="0" y="318.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-14">
+      <rect x="0" y="343.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-15">
+      <rect x="0" y="367.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-16">
+      <rect x="0" y="391.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-17">
+      <rect x="0" y="416.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-18">
+      <rect x="0" y="440.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-19">
+      <rect x="0" y="465.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-20">
+      <rect x="0" y="489.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-21">
+      <rect x="0" y="513.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3912008695-line-22">
+      <rect x="0" y="538.3" width="976" height="24.65"/>
+              </clipPath>
+      </defs>
+  
+      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-3912008695-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">AutoHeightRowsApp</text>
+              <g transform="translate(26,22)">
+              <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+              <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+              <circle cx="44" cy="0" r="7" fill="#28c840"/>
+              </g>
+          
+      <g transform="translate(9, 41)" clip-path="url(#terminal-3912008695-clip-terminal)">
+      <rect fill="#004578" x="0" y="1.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="36.6" y="1.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#073b61" x="183" y="1.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="0" y="25.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="25.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="25.9" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="50.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="50.3" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="74.7" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="74.7" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="99.1" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="99.1" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="123.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="123.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="147.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="147.9" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="172.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="172.3" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="196.7" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="196.7" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="221.1" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="221.1" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="245.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="245.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="269.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="269.9" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="294.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="294.3" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="318.7" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="318.7" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="343.1" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="343.1" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="367.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="367.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
+      <g class="terminal-3912008695-matrix">
+      <text class="terminal-3912008695-r1" x="0" y="20" textLength="36.6" clip-path="url(#terminal-3912008695-line-0)">&#160;N&#160;</text><text class="terminal-3912008695-r1" x="36.6" y="20" textLength="146.4" clip-path="url(#terminal-3912008695-line-0)">&#160;Column&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3912008695-line-0)">
+  </text><text class="terminal-3912008695-r4" x="0" y="44.4" textLength="36.6" clip-path="url(#terminal-3912008695-line-1)">&#160;3&#160;</text><text class="terminal-3912008695-r5" x="36.6" y="44.4" textLength="146.4" clip-path="url(#terminal-3912008695-line-1)">&#160;hey&#160;there&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3912008695-line-1)">
+  </text><text class="terminal-3912008695-r5" x="0" y="68.8" textLength="36.6" clip-path="url(#terminal-3912008695-line-2)">&#160;1&#160;</text><text class="terminal-3912008695-r5" x="36.6" y="68.8" textLength="146.4" clip-path="url(#terminal-3912008695-line-2)">&#160;hey&#160;there&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3912008695-line-2)">
+  </text><text class="terminal-3912008695-r5" x="0" y="93.2" textLength="36.6" clip-path="url(#terminal-3912008695-line-3)">&#160;5&#160;</text><text class="terminal-3912008695-r5" x="36.6" y="93.2" textLength="146.4" clip-path="url(#terminal-3912008695-line-3)">&#160;long&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3912008695-line-3)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="117.6" textLength="146.4" clip-path="url(#terminal-3912008695-line-4)">&#160;string&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3912008695-line-4)">
+  </text><text class="terminal-3912008695-r5" x="0" y="142" textLength="36.6" clip-path="url(#terminal-3912008695-line-5)">&#160;2&#160;</text><text class="terminal-3912008695-r5" x="36.6" y="142" textLength="146.4" clip-path="url(#terminal-3912008695-line-5)">&#160;╭───────╮&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3912008695-line-5)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="166.4" textLength="146.4" clip-path="url(#terminal-3912008695-line-6)">&#160;│&#160;Hello&#160;│&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3912008695-line-6)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="190.8" textLength="146.4" clip-path="url(#terminal-3912008695-line-7)">&#160;│&#160;world&#160;│&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3912008695-line-7)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="215.2" textLength="146.4" clip-path="url(#terminal-3912008695-line-8)">&#160;╰───────╯&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-3912008695-line-8)">
+  </text><text class="terminal-3912008695-r5" x="0" y="239.6" textLength="36.6" clip-path="url(#terminal-3912008695-line-9)">&#160;4&#160;</text><text class="terminal-3912008695-r5" x="36.6" y="239.6" textLength="146.4" clip-path="url(#terminal-3912008695-line-9)">&#160;1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-3912008695-line-9)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="264" textLength="146.4" clip-path="url(#terminal-3912008695-line-10)">&#160;2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="264" textLength="12.2" clip-path="url(#terminal-3912008695-line-10)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="288.4" textLength="146.4" clip-path="url(#terminal-3912008695-line-11)">&#160;3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-3912008695-line-11)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="312.8" textLength="146.4" clip-path="url(#terminal-3912008695-line-12)">&#160;4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-3912008695-line-12)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="337.2" textLength="146.4" clip-path="url(#terminal-3912008695-line-13)">&#160;5&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-3912008695-line-13)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="361.6" textLength="146.4" clip-path="url(#terminal-3912008695-line-14)">&#160;6&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-3912008695-line-14)">
+  </text><text class="terminal-3912008695-r5" x="36.6" y="386" textLength="146.4" clip-path="url(#terminal-3912008695-line-15)">&#160;7&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-3912008695-r3" x="976" y="386" textLength="12.2" clip-path="url(#terminal-3912008695-line-15)">
+  </text><text class="terminal-3912008695-r3" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-3912008695-line-16)">
+  </text><text class="terminal-3912008695-r3" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-3912008695-line-17)">
+  </text><text class="terminal-3912008695-r3" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-3912008695-line-18)">
+  </text><text class="terminal-3912008695-r3" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-3912008695-line-19)">
+  </text><text class="terminal-3912008695-r3" x="976" y="508" textLength="12.2" clip-path="url(#terminal-3912008695-line-20)">
+  </text><text class="terminal-3912008695-r3" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-3912008695-line-21)">
+  </text><text class="terminal-3912008695-r3" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-3912008695-line-22)">
+  </text>
+      </g>
+      </g>
+  </svg>
+  
+  '''
+# ---
+# name: test_datatable_add_row_auto_height_sorted
+  '''
+  <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
+      <!-- Generated with Rich https://www.textualize.io -->
+      <style>
+  
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Regular"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+          font-style: normal;
+          font-weight: 400;
+      }
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Bold"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+          font-style: bold;
+          font-weight: 700;
+      }
+  
+      .terminal-210732003-matrix {
+          font-family: Fira Code, monospace;
+          font-size: 20px;
+          line-height: 24.4px;
+          font-variant-east-asian: full-width;
+      }
+  
+      .terminal-210732003-title {
+          font-size: 18px;
+          font-weight: bold;
+          font-family: arial;
+      }
+  
+      .terminal-210732003-r1 { fill: #dde6ed;font-weight: bold }
+  .terminal-210732003-r2 { fill: #dde6ed }
+  .terminal-210732003-r3 { fill: #c5c8c6 }
+  .terminal-210732003-r4 { fill: #211505 }
+  .terminal-210732003-r5 { fill: #e1e1e1 }
+      </style>
+  
+      <defs>
+      <clipPath id="terminal-210732003-clip-terminal">
+        <rect x="0" y="0" width="975.0" height="584.5999999999999" />
+      </clipPath>
+      <clipPath id="terminal-210732003-line-0">
+      <rect x="0" y="1.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-1">
+      <rect x="0" y="25.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-2">
+      <rect x="0" y="50.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-3">
+      <rect x="0" y="74.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-4">
+      <rect x="0" y="99.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-5">
+      <rect x="0" y="123.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-6">
+      <rect x="0" y="147.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-7">
+      <rect x="0" y="172.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-8">
+      <rect x="0" y="196.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-9">
+      <rect x="0" y="221.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-10">
+      <rect x="0" y="245.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-11">
+      <rect x="0" y="269.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-12">
+      <rect x="0" y="294.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-13">
+      <rect x="0" y="318.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-14">
+      <rect x="0" y="343.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-15">
+      <rect x="0" y="367.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-16">
+      <rect x="0" y="391.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-17">
+      <rect x="0" y="416.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-18">
+      <rect x="0" y="440.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-19">
+      <rect x="0" y="465.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-20">
+      <rect x="0" y="489.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-21">
+      <rect x="0" y="513.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-210732003-line-22">
+      <rect x="0" y="538.3" width="976" height="24.65"/>
+              </clipPath>
+      </defs>
+  
+      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-210732003-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">AutoHeightRowsApp</text>
+              <g transform="translate(26,22)">
+              <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+              <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+              <circle cx="44" cy="0" r="7" fill="#28c840"/>
+              </g>
+          
+      <g transform="translate(9, 41)" clip-path="url(#terminal-210732003-clip-terminal)">
+      <rect fill="#004578" x="0" y="1.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="36.6" y="1.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#073b61" x="183" y="1.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="0" y="25.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="25.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="25.9" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="50.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="50.3" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="74.7" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="74.7" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="99.1" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="99.1" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="123.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="123.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="147.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="147.9" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="172.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="172.3" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="196.7" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="196.7" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="221.1" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="221.1" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="245.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="245.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="269.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="269.9" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="294.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="294.3" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="318.7" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="318.7" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="343.1" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="343.1" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="36.6" y="367.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="367.5" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
+      <g class="terminal-210732003-matrix">
+      <text class="terminal-210732003-r1" x="0" y="20" textLength="36.6" clip-path="url(#terminal-210732003-line-0)">&#160;N&#160;</text><text class="terminal-210732003-r1" x="36.6" y="20" textLength="146.4" clip-path="url(#terminal-210732003-line-0)">&#160;Column&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="20" textLength="12.2" clip-path="url(#terminal-210732003-line-0)">
+  </text><text class="terminal-210732003-r4" x="0" y="44.4" textLength="36.6" clip-path="url(#terminal-210732003-line-1)">&#160;1&#160;</text><text class="terminal-210732003-r5" x="36.6" y="44.4" textLength="146.4" clip-path="url(#terminal-210732003-line-1)">&#160;hey&#160;there&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-210732003-line-1)">
+  </text><text class="terminal-210732003-r5" x="0" y="68.8" textLength="36.6" clip-path="url(#terminal-210732003-line-2)">&#160;2&#160;</text><text class="terminal-210732003-r5" x="36.6" y="68.8" textLength="146.4" clip-path="url(#terminal-210732003-line-2)">&#160;╭───────╮&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-210732003-line-2)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="93.2" textLength="146.4" clip-path="url(#terminal-210732003-line-3)">&#160;│&#160;Hello&#160;│&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-210732003-line-3)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="117.6" textLength="146.4" clip-path="url(#terminal-210732003-line-4)">&#160;│&#160;world&#160;│&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-210732003-line-4)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="142" textLength="146.4" clip-path="url(#terminal-210732003-line-5)">&#160;╰───────╯&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="142" textLength="12.2" clip-path="url(#terminal-210732003-line-5)">
+  </text><text class="terminal-210732003-r5" x="0" y="166.4" textLength="36.6" clip-path="url(#terminal-210732003-line-6)">&#160;3&#160;</text><text class="terminal-210732003-r5" x="36.6" y="166.4" textLength="146.4" clip-path="url(#terminal-210732003-line-6)">&#160;hey&#160;there&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-210732003-line-6)">
+  </text><text class="terminal-210732003-r5" x="0" y="190.8" textLength="36.6" clip-path="url(#terminal-210732003-line-7)">&#160;4&#160;</text><text class="terminal-210732003-r5" x="36.6" y="190.8" textLength="146.4" clip-path="url(#terminal-210732003-line-7)">&#160;1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-210732003-line-7)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="215.2" textLength="146.4" clip-path="url(#terminal-210732003-line-8)">&#160;2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-210732003-line-8)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="239.6" textLength="146.4" clip-path="url(#terminal-210732003-line-9)">&#160;3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-210732003-line-9)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="264" textLength="146.4" clip-path="url(#terminal-210732003-line-10)">&#160;4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="264" textLength="12.2" clip-path="url(#terminal-210732003-line-10)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="288.4" textLength="146.4" clip-path="url(#terminal-210732003-line-11)">&#160;5&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-210732003-line-11)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="312.8" textLength="146.4" clip-path="url(#terminal-210732003-line-12)">&#160;6&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-210732003-line-12)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="337.2" textLength="146.4" clip-path="url(#terminal-210732003-line-13)">&#160;7&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-210732003-line-13)">
+  </text><text class="terminal-210732003-r5" x="0" y="361.6" textLength="36.6" clip-path="url(#terminal-210732003-line-14)">&#160;5&#160;</text><text class="terminal-210732003-r5" x="36.6" y="361.6" textLength="146.4" clip-path="url(#terminal-210732003-line-14)">&#160;long&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-210732003-line-14)">
+  </text><text class="terminal-210732003-r5" x="36.6" y="386" textLength="146.4" clip-path="url(#terminal-210732003-line-15)">&#160;string&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-210732003-r3" x="976" y="386" textLength="12.2" clip-path="url(#terminal-210732003-line-15)">
+  </text><text class="terminal-210732003-r3" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-210732003-line-16)">
+  </text><text class="terminal-210732003-r3" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-210732003-line-17)">
+  </text><text class="terminal-210732003-r3" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-210732003-line-18)">
+  </text><text class="terminal-210732003-r3" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-210732003-line-19)">
+  </text><text class="terminal-210732003-r3" x="976" y="508" textLength="12.2" clip-path="url(#terminal-210732003-line-20)">
+  </text><text class="terminal-210732003-r3" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-210732003-line-21)">
+  </text><text class="terminal-210732003-r3" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-210732003-line-22)">
+  </text>
+      </g>
+      </g>
+  </svg>
+  
+  '''
+# ---
 # name: test_datatable_column_cursor_render
   '''
   <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
diff --git a/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py
new file mode 100644
--- /dev/null
+++ b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py
@@ -0,0 +1,25 @@
+from rich.panel import Panel
+from rich.text import Text
+
+from textual.app import App
+from textual.widgets import DataTable
+
+
+class AutoHeightRowsApp(App[None]):
+    def compose(self):
+        table = DataTable()
+        self.column = table.add_column("N")
+        table.add_column("Column", width=10)
+        table.add_row(3, "hey there", height=None)
+        table.add_row(1, Text("hey there"), height=None)
+        table.add_row(5, Text("long string", overflow="fold"), height=None)
+        table.add_row(2, Panel.fit("Hello\nworld"), height=None)
+        table.add_row(4, "1\n2\n3\n4\n5\n6\n7", height=None)
+        yield table
+
+    def key_s(self):
+        self.query_one(DataTable).sort(self.column)
+
+
+if __name__ == "__main__":
+    AutoHeightRowsApp().run()
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -148,6 +148,18 @@ def test_datatable_add_column(snap_compare):
     assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_column.py")
 
 
+def test_datatable_add_row_auto_height(snap_compare):
+    # Check that rows added with auto height computation look right.
+    assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py")
+
+
+def test_datatable_add_row_auto_height_sorted(snap_compare):
+    # Check that rows added with auto height computation look right.
+    assert snap_compare(
+        SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py", press=["s"]
+    )
+
+
 def test_footer_render(snap_compare):
     assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py")
 
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -1,11 +1,12 @@
 from __future__ import annotations
 
 import pytest
+from rich.panel import Panel
 from rich.text import Text
 
 from textual._wait import wait_for_idle
 from textual.actions import SkipAction
-from textual.app import App
+from textual.app import App, RenderableType
 from textual.coordinate import Coordinate
 from textual.geometry import Offset
 from textual.message import Message
@@ -419,11 +420,11 @@ async def test_get_cell_coordinate_returns_coordinate():
         table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2")
         table.add_row("ValR3C1", "ValR3C2", "ValR3C3", key="R3")
 
-        assert table.get_cell_coordinate('R1', 'C1') == Coordinate(0, 0)
-        assert table.get_cell_coordinate('R2', 'C2') == Coordinate(1, 1)
-        assert table.get_cell_coordinate('R1', 'C3') == Coordinate(0, 2)
-        assert table.get_cell_coordinate('R3', 'C1') == Coordinate(2, 0)
-        assert table.get_cell_coordinate('R3', 'C2') == Coordinate(2, 1)
+        assert table.get_cell_coordinate("R1", "C1") == Coordinate(0, 0)
+        assert table.get_cell_coordinate("R2", "C2") == Coordinate(1, 1)
+        assert table.get_cell_coordinate("R1", "C3") == Coordinate(0, 2)
+        assert table.get_cell_coordinate("R3", "C1") == Coordinate(2, 0)
+        assert table.get_cell_coordinate("R3", "C2") == Coordinate(2, 1)
 
 
 async def test_get_cell_coordinate_invalid_row_key():
@@ -434,7 +435,7 @@ async def test_get_cell_coordinate_invalid_row_key():
         table.add_row("TargetValue", key="R1")
 
         with pytest.raises(CellDoesNotExist):
-            coordinate = table.get_cell_coordinate('INVALID_ROW', 'C1')
+            coordinate = table.get_cell_coordinate("INVALID_ROW", "C1")
 
 
 async def test_get_cell_coordinate_invalid_column_key():
@@ -445,7 +446,7 @@ async def test_get_cell_coordinate_invalid_column_key():
         table.add_row("TargetValue", key="R1")
 
         with pytest.raises(CellDoesNotExist):
-            coordinate = table.get_cell_coordinate('R1', 'INVALID_COLUMN')
+            coordinate = table.get_cell_coordinate("R1", "INVALID_COLUMN")
 
 
 async def test_get_cell_at_returns_value_at_cell():
@@ -531,9 +532,9 @@ async def test_get_row_index_returns_index():
         table.add_row("ValR2C1", "ValR2C2", key="R2")
         table.add_row("ValR3C1", "ValR3C2", key="R3")
 
-        assert table.get_row_index('R1') == 0
-        assert table.get_row_index('R2') == 1
-        assert table.get_row_index('R3') == 2
+        assert table.get_row_index("R1") == 0
+        assert table.get_row_index("R2") == 1
+        assert table.get_row_index("R3") == 2
 
 
 async def test_get_row_index_invalid_row_key():
@@ -544,7 +545,7 @@ async def test_get_row_index_invalid_row_key():
         table.add_row("TargetValue", key="R1")
 
         with pytest.raises(RowDoesNotExist):
-            index = table.get_row_index('InvalidRow')
+            index = table.get_row_index("InvalidRow")
 
 
 async def test_get_column():
@@ -591,6 +592,7 @@ async def test_get_column_at_invalid_index(index):
         with pytest.raises(ColumnDoesNotExist):
             list(table.get_column_at(index))
 
+
 async def test_get_column_index_returns_index():
     app = DataTableApp()
     async with app.run_test():
@@ -598,12 +600,12 @@ async def test_get_column_index_returns_index():
         table.add_column("Column1", key="C1")
         table.add_column("Column2", key="C2")
         table.add_column("Column3", key="C3")
-        table.add_row("ValR1C1", "ValR1C2", "ValR1C3",  key="R1")
-        table.add_row("ValR2C1", "ValR2C2", "ValR2C3",  key="R2")
+        table.add_row("ValR1C1", "ValR1C2", "ValR1C3", key="R1")
+        table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2")
 
-        assert table.get_column_index('C1') == 0
-        assert table.get_column_index('C2') == 1
-        assert table.get_column_index('C3') == 2
+        assert table.get_column_index("C1") == 0
+        assert table.get_column_index("C2") == 1
+        assert table.get_column_index("C3") == 2
 
 
 async def test_get_column_index_invalid_column_key():
@@ -613,11 +615,10 @@ async def test_get_column_index_invalid_column_key():
         table.add_column("Column1", key="C1")
         table.add_column("Column2", key="C2")
         table.add_column("Column3", key="C3")
-        table.add_row("TargetValue1", "TargetValue2", "TargetValue3",  key="R1")
+        table.add_row("TargetValue1", "TargetValue2", "TargetValue3", key="R1")
 
         with pytest.raises(ColumnDoesNotExist):
-            index = table.get_column_index('InvalidCol')
-
+            index = table.get_column_index("InvalidCol")
 
 
 async def test_update_cell_cell_exists():
@@ -1161,3 +1162,43 @@ async def test_unset_hover_highlight_when_no_table_cell_under_mouse():
         # the widget, and the hover cursor is hidden
         await pilot.hover(DataTable, offset=Offset(42, 1))
         assert not table._show_hover_cursor
+
+
+@pytest.mark.parametrize(
+    ["cell", "height"],
+    [
+        ("hey there", 1),
+        (Text("hey there"), 1),
+        (Text("long string", overflow="fold"), 2),
+        (Panel.fit("Hello\nworld"), 4),
+        ("1\n2\n3\n4\n5\n6\n7", 7),
+    ],
+)
+async def test_add_row_auto_height(cell: RenderableType, height: int):
+    app = DataTableApp()
+    async with app.run_test() as pilot:
+        table = app.query_one(DataTable)
+        table.add_column("C", width=10)
+        row_key = table.add_row(cell, height=None)
+        row = table.rows.get(row_key)
+        await pilot.pause()
+        assert row.height == height
+
+
+async def test_add_row_expands_column_widths():
+    """Regression test for https://github.com/Textualize/textual/issues/1026."""
+    app = DataTableApp()
+    from textual.widgets._data_table import CELL_X_PADDING
+
+    async with app.run_test() as pilot:
+        table = app.query_one(DataTable)
+        table.add_column("First")
+        table.add_column("Second", width=10)
+        await pilot.pause()
+        assert table.ordered_columns[0].render_width == 5 + CELL_X_PADDING
+        assert table.ordered_columns[1].render_width == 10 + CELL_X_PADDING
+
+        table.add_row("a" * 20, "a" * 20)
+        await pilot.pause()
+        assert table.ordered_columns[0].render_width == 20 + CELL_X_PADDING
+        assert table.ordered_columns[1].render_width == 10 + CELL_X_PADDING