Skip to content

Commit 02407cd

Browse files
mohnjilesclaude
andcommitted
fix: reset the docs viewer to the top when the page changes
Page changes swap the document inside a reused scroll viewer, so following a ? button while scrolled down landed the reader part-way into a page they had never seen. Resets synchronously on the content change; anchor navigation re-scrolls on a later layout pass, so heading deep links still win. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9ab7b01 commit 02407cd

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

StabilityMatrix.Avalonia/Controls/DocumentationMarkdownViewer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
117117
{
118118
ApplyContentZoom();
119119
}
120+
else if (change.Property == MarkdownProperty)
121+
{
122+
// Page changes swap the document inside the same scroll viewer, which otherwise
123+
// keeps the previous page's offset and drops the reader mid-way down a page they
124+
// have never seen. Anchor navigation re-scrolls on a later layout pass, so a
125+
// deep link to a heading still wins over this.
126+
ScrollValue = new Vector(ScrollValue.X, 0);
127+
}
120128
}
121129

122130
private void ApplyContentZoom()
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using Avalonia;
2+
using Avalonia.Controls;
3+
using Avalonia.Threading;
4+
using StabilityMatrix.Avalonia.Controls;
5+
6+
namespace StabilityMatrix.UITests;
7+
8+
[Collection("TempDir")]
9+
public class DocumentationMarkdownViewerTests : TestBase
10+
{
11+
private static string LongDocument(string title) =>
12+
$"# {title}\n\n"
13+
+ string.Join("\n\n", Enumerable.Range(1, 120).Select(i => $"Paragraph {i} of {title}."));
14+
15+
/// <summary>
16+
/// Pages swap the document inside a reused scroll viewer, so without an explicit reset the
17+
/// reader lands part-way down a page they have never seen.
18+
/// </summary>
19+
[AvaloniaFact]
20+
public void Markdown_ResetsScrollToTop_OnPageChange()
21+
{
22+
var viewer = new DocumentationMarkdownViewer { Markdown = LongDocument("First Page") };
23+
var window = new Window
24+
{
25+
Width = 600,
26+
Height = 400,
27+
Content = viewer,
28+
};
29+
30+
window.Show();
31+
Dispatcher.UIThread.RunJobs();
32+
33+
// Reader scrolls part-way down the first page.
34+
viewer.ScrollValue = new Vector(0, 500);
35+
Dispatcher.UIThread.RunJobs();
36+
Assert.True(viewer.ScrollValue.Y > 0, "Precondition: the viewer should be scrolled down.");
37+
38+
// Following a ? button loads a different page into the same viewer.
39+
viewer.Markdown = LongDocument("Second Page");
40+
Dispatcher.UIThread.RunJobs();
41+
42+
Assert.Equal(0, viewer.ScrollValue.Y);
43+
}
44+
45+
/// <summary>
46+
/// The reset above runs synchronously on the content change while anchor scrolling is
47+
/// deferred to a layout pass, so a deep link to a heading must still win over it.
48+
/// </summary>
49+
[AvaloniaFact]
50+
public void ScrollToAnchor_StillWins_AfterContentChange()
51+
{
52+
var document =
53+
"# Top\n\n"
54+
+ string.Join("\n\n", Enumerable.Range(1, 80).Select(i => $"Filler paragraph {i}."))
55+
+ "\n\n## Deep Heading\n\nContent under the deep heading.";
56+
57+
var viewer = new DocumentationMarkdownViewer();
58+
var window = new Window
59+
{
60+
Width = 600,
61+
Height = 400,
62+
Content = viewer,
63+
};
64+
65+
window.Show();
66+
67+
// Mirrors a ? deep link: content is set, then the anchor is requested.
68+
viewer.Markdown = document;
69+
Dispatcher.UIThread.RunJobs();
70+
viewer.ScrollToAnchor("deep-heading");
71+
72+
Dispatcher.UIThread.RunJobs();
73+
74+
Assert.True(
75+
viewer.ScrollValue.Y > 0,
76+
$"Anchor scroll should survive the page-change reset, got Y={viewer.ScrollValue.Y}."
77+
);
78+
}
79+
}

0 commit comments

Comments
 (0)