diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 34c8afc7fb6e2d09010ce349b5220f53055b6c10..ecfb9e5a989e3e423eae3e4edf7df90e7279daad 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 2e9779ad9a2c60387395cef5e0a6635a6163b897..b2418c29d6372100be4fd8b67746e9158212b985 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 1ba1c0174daef280bd7f6b9feef9407da58bd8fe..b954498b481e68d266807677768de321379011f9 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 b856c58b6124b08a2b45d835ef53490d5ffb1ce8..5cd0688202a8f104e7313db7f0a7697f79bcc4eb 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 0000000000000000000000000000000000000000..8a357e526e6854fa736f77e8666f09971b52e0b1 --- /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 91a5051b6c1a91a68737f41cce1231864fce791b..71bb468d49ad24251d58826822edd7568a63a330 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