From 07edd3c16f353f877b44b5f8572ad9f382db1a30 Mon Sep 17 00:00:00 2001
From: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
Date: Mon, 27 Jun 2022 16:31:16 +0100
Subject: [PATCH] Support configuring spacer element. Fixes #42206

---
 .../Web/src/PublicAPI.Unshipped.txt           |  2 ++
 .../Web/src/Virtualization/Virtualize.cs      | 16 +++++++++++--
 .../test/E2ETest/Tests/VirtualizationTest.cs  | 24 +++++++++++++++++++
 .../test/testassets/BasicTestApp/Index.razor  |  1 +
 .../BasicTestApp/VirtualizationTable.razor    | 23 ++++++++++++++++++
 src/Shared/E2ETesting/selenium-config.json    |  2 +-
 6 files changed, 65 insertions(+), 3 deletions(-)
 create mode 100644 src/Components/test/testassets/BasicTestApp/VirtualizationTable.razor

diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt
index 34c8afc7fb6..ecfb9e5a989 100644
--- a/src/Components/Web/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Web/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,5 @@
 #nullable enable
 Microsoft.AspNetCore.Components.Forms.InputRadio<TValue>.Element.get -> Microsoft.AspNetCore.Components.ElementReference?
 Microsoft.AspNetCore.Components.Forms.InputRadio<TValue>.Element.set -> void
+Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.SpacerElement.get -> string!
+Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.SpacerElement.set -> void
diff --git a/src/Components/Web/src/Virtualization/Virtualize.cs b/src/Components/Web/src/Virtualization/Virtualize.cs
index 2e9779ad9a2..b2418c29d63 100644
--- a/src/Components/Web/src/Virtualization/Virtualize.cs
+++ b/src/Components/Web/src/Virtualization/Virtualize.cs
@@ -95,6 +95,18 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
     [Parameter]
     public int OverscanCount { get; set; } = 3;
 
+    /// <summary>
+    /// Gets or sets the tag name of the HTML element that will be used as the virtualization spacer.
+    /// One such element will be rendered before the visible items, and one more after them, using
+    /// an explicit "height" style to control the scroll range.
+    ///
+    /// The default value is "div". If you are placing the <see cref="Virtualize{TItem}"/> instance inside
+    /// an element that requires a specific child tag name, consider setting that here. For example when
+    /// rendering inside a "tbody", consider setting <see cref="SpacerElement"/> to the value "tr".
+    /// </summary>
+    [Parameter]
+    public string SpacerElement { get; set; } = "div";
+
     /// <summary>
     /// Instructs the component to re-request data from its <see cref="ItemsProvider"/>.
     /// This is useful if external data may have changed. There is no need to call this
@@ -178,7 +190,7 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
             throw oldRefreshException;
         }
 
-        builder.OpenElement(0, "div");
+        builder.OpenElement(0, SpacerElement);
         builder.AddAttribute(1, "style", GetSpacerStyle(_itemsBefore));
         builder.AddElementReferenceCapture(2, elementReference => _spacerBefore = elementReference);
         builder.CloseElement();
@@ -235,7 +247,7 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
 
         var itemsAfter = Math.Max(0, _itemCount - _visibleItemCapacity - _itemsBefore);
 
-        builder.OpenElement(6, "div");
+        builder.OpenElement(6, SpacerElement);
         builder.AddAttribute(7, "style", GetSpacerStyle(itemsAfter));
         builder.AddElementReferenceCapture(8, elementReference => _spacerAfter = elementReference);
 
diff --git a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs
index 1ba1c0174da..b954498b481 100644
--- a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs
+++ b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs
@@ -238,6 +238,30 @@ public class VirtualizationTest : ServerTestBase<ToggleExecutionModeServerFixtur
         int GetItemCount() => Browser.FindElements(By.ClassName("incorrect-size-item")).Count;
     }
 
+    [Fact]
+    public void CanRenderHtmlTable()
+    {
+        Browser.MountTestComponent<VirtualizationTable>();
+        var expectedInitialSpacerStyle = "height: 0px; flex-shrink: 0;";
+        var topSpacer = Browser.Exists(By.CssSelector("#virtualized-table > tbody > :first-child"));
+        var bottomSpacer = Browser.Exists(By.CssSelector("#virtualized-table > tbody > :last-child"));
+
+        // We can override the tag name of the spacer
+        Assert.Equal("tr", topSpacer.TagName.ToLowerInvariant());
+        Assert.Equal("tr", bottomSpacer.TagName.ToLowerInvariant());
+        Assert.Contains(expectedInitialSpacerStyle, topSpacer.GetAttribute("style"));
+
+        // Check scrolling document element works
+        Browser.DoesNotExist(By.Id("row-999"));
+        Browser.ExecuteJavaScript("window.scrollTo(0, document.body.scrollHeight);");
+        var lastElement = Browser.Exists(By.Id("row-999"));
+        Browser.True(() => lastElement.Displayed);
+
+        // Validate that the top spacer has expanded, and bottom one has collapsed
+        Browser.False(() => topSpacer.GetAttribute("style").Contains(expectedInitialSpacerStyle));
+        Assert.Contains(expectedInitialSpacerStyle, bottomSpacer.GetAttribute("style"));
+    }
+
     [Fact]
     public void CanMutateDataInPlace_Sync()
     {
diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor
index b856c58b612..5cd0688202a 100644
--- a/src/Components/test/testassets/BasicTestApp/Index.razor
+++ b/src/Components/test/testassets/BasicTestApp/Index.razor
@@ -101,6 +101,7 @@
         <option value="BasicTestApp.TouchEventComponent">Touch events</option>
         <option value="BasicTestApp.VirtualizationComponent">Virtualization</option>
         <option value="BasicTestApp.VirtualizationDataChanges">Virtualization data changes</option>
+        <option value="BasicTestApp.VirtualizationTable">Virtualization HTML table</option>
         <option value="BasicTestApp.HotReload.RenderOnHotReload">Render on hot reload</option>
     </select>
 
diff --git a/src/Components/test/testassets/BasicTestApp/VirtualizationTable.razor b/src/Components/test/testassets/BasicTestApp/VirtualizationTable.razor
new file mode 100644
index 00000000000..8a357e526e6
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/VirtualizationTable.razor
@@ -0,0 +1,23 @@
+<p>This is to show we can use an HTML table with Virtualize, despite it having particular rules about the element hierarchy.</p>
+<p>We're also using the document root as the scroll container. Other tests cover having a different scroll container, such as a div with overflow:scroll.</p>
+
+<table id="virtualized-table">
+    <thead style="position: sticky; top: 0; background-color: silver">
+        <tr>
+            <th>Item</th>
+            <th>Another col</th>
+        </tr>
+    </thead>
+    <tbody>
+        <Virtualize Items="@fixedItems" ItemSize="30" SpacerElement="tr">
+            <tr @key="context" style="height: 30px;" id="row-@context">
+                <td>Item @context</td>
+                <td>Another value</td>
+            </tr>
+        </Virtualize>
+    </tbody>
+</table>
+
+@code {
+    List<int> fixedItems = Enumerable.Range(0, 1000).ToList();
+}
diff --git a/src/Shared/E2ETesting/selenium-config.json b/src/Shared/E2ETesting/selenium-config.json
index 91a5051b6c1..71bb468d49a 100644
--- a/src/Shared/E2ETesting/selenium-config.json
+++ b/src/Shared/E2ETesting/selenium-config.json
@@ -1,7 +1,7 @@
 {
   "drivers": {
     "chrome": {
-      "version" : "100.0.4896.60"
+      "version" : "103.0.5060.53"
     }
   },
   "ignoreExtraDrivers": true
-- 
GitLab