Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions src/Components/Web.JS/src/Virtualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is redundant, rangeRect is not used anywhere.

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*([^,]+)/);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
}
});
}
Expand Down
12 changes: 12 additions & 0 deletions src/Components/test/E2ETest/Tests/VirtualizationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,18 @@ public void CanRefreshItemsProviderResultsInPlace()
name => Assert.Equal("Person 3", name));
}

[Fact]
public void CanScrollWhenAppliedScale()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this fail without the change? The check that I used for detecting the glitch was much more complex. Some solutions did not prevent the full loading of data but still, resulted in the glitch on the way to the bottom.

{
Browser.MountTestComponent<VirtualizationScale>();
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")]
Expand Down
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
<option value="BasicTestApp.VirtualizationMaxItemCount">Virtualization MaxItemCount</option>
<option value="BasicTestApp.VirtualizationMaxItemCount_AppContext">Virtualization MaxItemCount (via AppContext)</option>
<option value="BasicTestApp.VirtualizationTable">Virtualization HTML table</option>
<option value="BasicTestApp.VirtualizationScale">Virtualization with scale</option>
<option value="BasicTestApp.VirtualizationLargeOverscan">Virtualization large overscan</option>
<option value="BasicTestApp.HotReload.RenderOnHotReload">Render on hot reload</option>
<option value="BasicTestApp.SectionsTest.ParentComponentWithTwoChildren">Sections test</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div id="virtualize" style="height: 200px; overflow: auto">
<Virtualize ItemsProvider="@itemsProvider">
<tr class="person"><span>@context.Name</span></tr>
</Virtualize>
</div>

@code {
internal class Person
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}

private ItemsProviderDelegate<Person> itemsProvider = default!;

protected override void OnInitialized()
{
itemsProvider = async request =>
{
await Task.CompletedTask;
return new ItemsProviderResult<Person>(
items: Enumerable.Range(request.StartIndex, request.Count).Select(i => new Person { Id = i, Name = $"Person {i}" }).ToList(),
totalItemCount: 100);
};
}
}

<style>
body {
scale: 2 0.5;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in the algo we focus on y-axis value, the tests check mainly 50% scaling (decreasing). Would such an approach work, so that we can test increasing and decreasing cases?

@code {
    [Parameter]
    public double ScaleX { get; set; } = 1.0;
    [Parameter]
    public double ScaleY { get; set; } = 1.0;
}

<style>
    body {
             scale: @ScaleX, @ScaleY;
             transform-origin: top left;
     }
</style>

I'm not sure how to pass these params to the component, though. The component is mounted directly, not navigated to. Maybe we can add path to it and then use SetUrlViaPushState("/path?scaleX=1&scaleY=1"); to pass the query values?

Skip if it's too much test logic changes and you tested both cases. We can always add additional tests after the fix lands.

transform-origin: top left;
}
</style>
Loading