diff --git a/src/Components/Web.JS/src/Virtualize.ts b/src/Components/Web.JS/src/Virtualize.ts index 81dbce67a508..5e21c1729490 100644 --- a/src/Components/Web.JS/src/Virtualize.ts +++ b/src/Components/Web.JS/src/Virtualize.ts @@ -94,16 +94,58 @@ function init(dotNetHelper: DotNet.DotNetObject, spacerBefore: HTMLElement, spac // scrolling glitches. rangeBetweenSpacers.setStartAfter(spacerBefore); rangeBetweenSpacers.setEndBefore(spacerAfter); - const spacerSeparation = rangeBetweenSpacers.getBoundingClientRect().height; + const rangeRect = rangeBetweenSpacers.getBoundingClientRect(); + const spacerSeparation = rangeRect.height; const containerSize = entry.rootBounds?.height; + // Accumulate scale factors from all parent elements as they multiply together + let scaleFactor = 1.0; + + // Check for CSS scale/zoom/transform properties on parent elements (including body and html) + let element = spacerBefore.parentElement; + while (element) { + const computedStyle = getComputedStyle(element); + + // Check for zoom property (applies uniform scaling) + if (computedStyle.zoom && computedStyle.zoom !== 'normal' && computedStyle.zoom !== '1') { + scaleFactor *= parseFloat(computedStyle.zoom); + } + + // Check for scale property (can have separate X/Y values) + if (computedStyle.scale && computedStyle.scale !== 'none' && computedStyle.scale !== '1') { + const parts = computedStyle.scale.split(' '); + const scaleX = parseFloat(parts[0]); + const scaleY = parts.length > 1 ? parseFloat(parts[1]) : scaleX; + scaleFactor *= scaleY; // Use vertical scale for vertical scrolling + } + + // Check for transform property (matrix form) + if (computedStyle.transform && computedStyle.transform !== 'none') { + // A 2D transform matrix has 6 values: matrix(scaleX, skewY, skewX, scaleY, translateX, translateY) + const match = computedStyle.transform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+)/); + if (match) { + const scaleY = parseFloat(match[1]); + scaleFactor *= scaleY; + } + } + element = element.parentElement; + } + + // Divide by scale factor to convert from physical pixels to logical pixels. + const unscaledSpacerSeparation = spacerSeparation / scaleFactor; + const unscaledContainerSize = containerSize !== null && containerSize !== undefined ? containerSize / scaleFactor : null; + if (entry.target === spacerBefore) { - dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', entry.intersectionRect.top - entry.boundingClientRect.top, spacerSeparation, containerSize); + const spacerSize = entry.intersectionRect.top - entry.boundingClientRect.top; + const unscaledSpacerSize = spacerSize / scaleFactor; + dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', unscaledSpacerSize, unscaledSpacerSeparation, unscaledContainerSize); } else if (entry.target === spacerAfter && spacerAfter.offsetHeight > 0) { // When we first start up, both the "before" and "after" spacers will be visible, but it's only relevant to raise a // single event to load the initial data. To avoid raising two events, skip the one for the "after" spacer if we know // it's meaningless to talk about any overlap into it. - dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, spacerSeparation, containerSize); + const spacerSize = entry.boundingClientRect.bottom - entry.intersectionRect.bottom; + const unscaledSpacerSize = spacerSize / scaleFactor; + dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', unscaledSpacerSize, unscaledSpacerSeparation, unscaledContainerSize); } }); } diff --git a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs index 4fa21ef3fe11..bb41229abc2a 100644 --- a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs +++ b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs @@ -426,6 +426,18 @@ public void CanRefreshItemsProviderResultsInPlace() name => Assert.Equal("Person 3", name)); } + [Fact] + public void CanScrollWhenAppliedScale() + { + Browser.MountTestComponent(); + var container = Browser.Exists(By.Id("virtualize")); + + var people = GetPeopleNames(container); + Assert.True(GetPeopleNames(container).Length > 0); + ScrollTopToEnd(Browser, container); + Assert.True(GetPeopleNames(container).Length > 0); + } + [Theory] [InlineData("simple-scroll-horizontal")] [InlineData("complex-scroll-horizontal")] diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index f9cc718f7ca1..3f5b57ed57cc 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -123,6 +123,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridVirtualizeComponent.razor b/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridVirtualizeComponent.razor index b875fad0eee3..f7b6e894a506 100644 --- a/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridVirtualizeComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridVirtualizeComponent.razor @@ -29,7 +29,7 @@ Interlocked.Increment(ref ItemsProviderCallCount); StateHasChanged(); return GridItemsProviderResult.From( - items: Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}" }).ToList(), + items: Enumerable.Range(request.StartIndex, request.Count ?? 10).Select(i => new Person { Id = i, Name = $"Person {i}" }).ToList(), totalItemCount: 100); }; } diff --git a/src/Components/test/testassets/BasicTestApp/VirtualizationScale.razor b/src/Components/test/testassets/BasicTestApp/VirtualizationScale.razor new file mode 100644 index 000000000000..935646f76ea9 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/VirtualizationScale.razor @@ -0,0 +1,33 @@ +
+ + @context.Name + +
+ +@code { + internal class Person + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + private ItemsProviderDelegate itemsProvider = default!; + + protected override void OnInitialized() + { + itemsProvider = async request => + { + await Task.CompletedTask; + return new ItemsProviderResult( + items: Enumerable.Range(request.StartIndex, request.Count).Select(i => new Person { Id = i, Name = $"Person {i}" }).ToList(), + totalItemCount: 100); + }; + } +} + +