fix(postit): restore Markdown editor and preview scrolling
Constrain the blog page layout, separate list and article scrolling, and remove content height limits and nested editor scrolling. Add six headless regression cases and update the changelog and layout documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
cd7351a783
commit
ebdf51f468
6 changed files with 205 additions and 67 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -10,8 +10,18 @@
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
* [PostIt] La liste des billets et l'éditeur Markdown se partagent
|
||||||
|
l'espace disponible et défilent séparément ; un séparateur permet
|
||||||
|
de redimensionner les deux zones.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
* [PostIt] Défilement des articles longs en édition et en aperçu :
|
||||||
|
suppression de la limite de hauteur qui masquait le contenu Markdown
|
||||||
|
et du défilement imbriqué autour d'AvaloniaEdit. Six tests headless
|
||||||
|
couvrent les barres automatiques, le redimensionnement et le
|
||||||
|
défilement indépendant de la liste.
|
||||||
|
|
||||||
## [1.0.8-rc23] - unstable
|
## [1.0.8-rc23] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,20 @@ le DI est construit. Ordre, dans cet ordre :
|
||||||
> à cette table, *ne pas* ajouter de registration pour
|
> à cette table, *ne pas* ajouter de registration pour
|
||||||
> `Settings` ailleurs que la ligne `AddSingleton(settings)`.
|
> `Settings` ailleurs que la ligne `AddSingleton(settings)`.
|
||||||
|
|
||||||
|
## Défilement de la page des billets
|
||||||
|
|
||||||
|
La page `BlogsPage` utilise une grille à hauteur contrainte : l'en-tête
|
||||||
|
et les commandes restent visibles, tandis que la liste des billets et
|
||||||
|
l'éditeur se partagent l'espace disponible. Un `GridSplitter` permet
|
||||||
|
de modifier cette répartition. La liste et l'article défilent séparément.
|
||||||
|
|
||||||
|
Dans `MarkdownEditorControl`, AvaloniaEdit gère son propre défilement ;
|
||||||
|
l'aperçu Markdown est placé dans un `ScrollViewer`. Ne pas limiter
|
||||||
|
la hauteur du `MarkdownViewer` lui-même : son contenu doit pouvoir
|
||||||
|
dépasser le viewport pour que les barres automatiques s'activent.
|
||||||
|
Ne pas placer cette grille dans un `StackPanel` vertical ou un
|
||||||
|
`ScrollViewer` de page, qui lui donnerait une hauteur de mesure illimitée.
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
Le host de navigation est un `NavigationPage x:Name="NavRoot"`
|
Le host de navigation est un `NavigationPage x:Name="NavRoot"`
|
||||||
|
|
|
||||||
143
src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs
Normal file
143
src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Headless.XUnit;
|
||||||
|
using Avalonia.LogicalTree;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using AvaloniaEdit;
|
||||||
|
using PostIt.Controls;
|
||||||
|
using PostIt.Views.Blogs;
|
||||||
|
|
||||||
|
namespace PostIt.Tests;
|
||||||
|
|
||||||
|
public class MarkdownEditorScrollTests
|
||||||
|
{
|
||||||
|
private static readonly string LongArticle =
|
||||||
|
string.Join("\n\n", Enumerable.Range(1, 100).Select(i => $"Paragraph {i}: article content."));
|
||||||
|
|
||||||
|
[AvaloniaTheory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void Long_article_scrolls_to_the_end_and_adapts_to_resizing(bool preview)
|
||||||
|
{
|
||||||
|
var editor = new MarkdownEditorControl
|
||||||
|
{
|
||||||
|
MarkdownText = LongArticle,
|
||||||
|
IsPreviewVisible = preview
|
||||||
|
};
|
||||||
|
var window = new Window { Width = 600, Height = 400, Content = editor };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
window.Show();
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
window.UpdateLayout();
|
||||||
|
|
||||||
|
var scroller = FindArticleScroller(editor, preview);
|
||||||
|
AssertScrollable(scroller);
|
||||||
|
var initialViewport = scroller.Viewport.Height;
|
||||||
|
|
||||||
|
window.Height = 300;
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
window.UpdateLayout();
|
||||||
|
|
||||||
|
Assert.True(scroller.Viewport.Height < initialViewport);
|
||||||
|
AssertScrollable(scroller);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseWindow(window, editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaTheory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void Blog_page_bounds_the_article_viewport(bool preview)
|
||||||
|
{
|
||||||
|
var page = new BlogsPage();
|
||||||
|
var editor = page.GetLogicalDescendants().OfType<MarkdownEditorControl>().Single();
|
||||||
|
editor.MarkdownText = LongArticle;
|
||||||
|
editor.IsPreviewVisible = preview;
|
||||||
|
var posts = page.FindControl<ListBox>("PostsListBox")!;
|
||||||
|
posts.ItemsSource = Enumerable.Range(1, 50)
|
||||||
|
.Select(i => new Yavsc.Blogspot.BlogPostDto { Title = $"Post {i}" }).ToArray();
|
||||||
|
var navigation = new NavigationPage();
|
||||||
|
navigation.PushAsync(page).GetAwaiter().GetResult();
|
||||||
|
var window = new Window { Width = 800, Height = 700, Content = navigation };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
window.Show();
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
window.UpdateLayout();
|
||||||
|
|
||||||
|
var scroller = FindArticleScroller(editor, preview);
|
||||||
|
Assert.InRange(scroller.Viewport.Height, 1, window.ClientSize.Height);
|
||||||
|
AssertScrollable(scroller);
|
||||||
|
AssertScrollable(posts.GetVisualDescendants().OfType<ScrollViewer>().Single());
|
||||||
|
var editorBottom = editor.TranslatePoint(new Point(0, editor.Bounds.Height), window);
|
||||||
|
Assert.NotNull(editorBottom);
|
||||||
|
Assert.InRange(editorBottom.Value.Y, 1, window.ClientSize.Height);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseWindow(window, editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaTheory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void Short_article_needs_no_vertical_scrollbar(bool preview)
|
||||||
|
{
|
||||||
|
var editor = new MarkdownEditorControl
|
||||||
|
{
|
||||||
|
MarkdownText = "Short article",
|
||||||
|
IsPreviewVisible = preview
|
||||||
|
};
|
||||||
|
var window = new Window { Width = 600, Height = 400, Content = editor };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
window.Show();
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
window.UpdateLayout();
|
||||||
|
|
||||||
|
var scroller = FindArticleScroller(editor, preview);
|
||||||
|
Assert.True(scroller.Extent.Height <= scroller.Viewport.Height);
|
||||||
|
Assert.DoesNotContain(scroller.GetVisualDescendants().OfType<ScrollBar>(),
|
||||||
|
bar => bar.Orientation == Avalonia.Layout.Orientation.Vertical && bar.IsVisible);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseWindow(window, editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CloseWindow(Window window, MarkdownEditorControl editor)
|
||||||
|
{
|
||||||
|
// The package's shared renderer caches the visual; detach it between headless applications.
|
||||||
|
editor.FindControl<MarkdownViewer.Core.Controls.MarkdownViewer>("PreviewViewer")!.Content = null;
|
||||||
|
window.UpdateLayout();
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScrollViewer FindArticleScroller(MarkdownEditorControl editor, bool preview)
|
||||||
|
{
|
||||||
|
if (!preview)
|
||||||
|
return editor.GetVisualDescendants().OfType<TextEditor>().Single()
|
||||||
|
.GetVisualDescendants().OfType<ScrollViewer>().Single();
|
||||||
|
|
||||||
|
return editor.FindControl<ScrollViewer>("PreviewScrollViewer")!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertScrollable(ScrollViewer scroller)
|
||||||
|
{
|
||||||
|
Assert.True(scroller.Extent.Height > scroller.Viewport.Height,
|
||||||
|
$"Expected overflow, but extent={scroller.Extent.Height}, viewport={scroller.Viewport.Height}.");
|
||||||
|
Assert.Contains(scroller.GetVisualDescendants().OfType<ScrollBar>(),
|
||||||
|
bar => bar.Orientation == Avalonia.Layout.Orientation.Vertical && bar.IsVisible);
|
||||||
|
scroller.ScrollToEnd();
|
||||||
|
Assert.True(scroller.Offset.Y > 0);
|
||||||
|
Assert.Equal(scroller.Extent.Height - scroller.Viewport.Height, scroller.Offset.Y, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,20 +7,13 @@
|
||||||
x:Name="Root"
|
x:Name="Root"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="260">
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
<Border
|
<Border
|
||||||
BorderBrush="LightGray"
|
BorderBrush="LightGray"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="5"
|
CornerRadius="5"
|
||||||
IsVisible="{Binding !IsPreviewVisible, ElementName=Root}"
|
IsVisible="{Binding !IsPreviewVisible, ElementName=Root}"
|
||||||
Padding="0"
|
Padding="0">
|
||||||
MinHeight="260">
|
|
||||||
<ScrollViewer
|
|
||||||
BringIntoViewOnFocusChange="False"
|
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
<AvaloniaEdit:TextEditor
|
<AvaloniaEdit:TextEditor
|
||||||
x:Name="SourceEditor"
|
x:Name="SourceEditor"
|
||||||
Document="{Binding Document, ElementName=Root, Mode=TwoWay}"
|
Document="{Binding Document, ElementName=Root, Mode=TwoWay}"
|
||||||
|
|
@ -35,7 +28,6 @@
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
TextInputOptions.Multiline="True"
|
TextInputOptions.Multiline="True"
|
||||||
TextInputOptions.ReturnKeyType="Default" />
|
TextInputOptions.ReturnKeyType="Default" />
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
|
|
@ -43,18 +35,21 @@
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="5"
|
CornerRadius="5"
|
||||||
IsVisible="{Binding IsPreviewVisible, ElementName=Root}"
|
IsVisible="{Binding IsPreviewVisible, ElementName=Root}"
|
||||||
Padding="8"
|
Padding="8">
|
||||||
MinHeight="260">
|
|
||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
|
x:Name="PreviewScrollViewer"
|
||||||
BringIntoViewOnFocusChange="False"
|
BringIntoViewOnFocusChange="False"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalScrollBarVisibility="Auto">
|
||||||
<md:MarkdownViewer
|
<md:MarkdownViewer
|
||||||
x:Name="PreviewViewer"
|
x:Name="PreviewViewer"
|
||||||
MarkdownText="{Binding MarkdownText, ElementName=Root}"
|
HorizontalContentAlignment="Stretch"
|
||||||
ClipToBounds="False" />
|
VerticalAlignment="Top"
|
||||||
|
ClipToBounds="False"
|
||||||
|
UseLayoutRounding="True"
|
||||||
|
MarkdownText="{Binding MarkdownText, ElementName=Root}" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ public partial class MarkdownEditorControl : UserControl
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
PropertyChanged += OnPropertyChanged;
|
PropertyChanged += OnPropertyChanged;
|
||||||
SizeChanged += OnSizeChanged;
|
|
||||||
UpdateEditorLimits();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
|
|
@ -62,27 +60,4 @@ public partial class MarkdownEditorControl : UserControl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
|
||||||
{
|
|
||||||
UpdateEditorLimits();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateEditorLimits()
|
|
||||||
{
|
|
||||||
var visibleHeight = Bounds.Height > 0 ? Bounds.Height - 24 : 260;
|
|
||||||
visibleHeight = Math.Max(visibleHeight, 80);
|
|
||||||
|
|
||||||
if (SourceEditor is not null)
|
|
||||||
{
|
|
||||||
SourceEditor.MaxHeight = visibleHeight;
|
|
||||||
SourceEditor.Height = double.NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PreviewViewer is not null)
|
|
||||||
{
|
|
||||||
PreviewViewer.MaxHeight = visibleHeight;
|
|
||||||
PreviewViewer.Height = double.NaN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,11 @@
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<vm:BlogsViewModel />
|
<vm:BlogsViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<ScrollViewer
|
<Grid
|
||||||
|
RowDefinitions="Auto,Auto,*"
|
||||||
Margin="5,5,10,5"
|
Margin="5,5,10,5"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch">
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
|
||||||
BringIntoViewOnFocusChange="False">
|
|
||||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
|
||||||
|
|
||||||
<WrapPanel x:Name="BlogsHeaderPanel"
|
<WrapPanel x:Name="BlogsHeaderPanel"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
|
|
@ -39,6 +36,7 @@
|
||||||
Text="{Binding SearchText, Mode=TwoWay}" />
|
Text="{Binding SearchText, Mode=TwoWay}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<CommandBar x:Name="BlogsCommandBar"
|
<CommandBar x:Name="BlogsCommandBar"
|
||||||
|
Grid.Row="1"
|
||||||
HorizontalAlignment="Right" IsDynamicOverflowEnabled="True">
|
HorizontalAlignment="Right" IsDynamicOverflowEnabled="True">
|
||||||
<CommandBar.PrimaryCommands>
|
<CommandBar.PrimaryCommands>
|
||||||
<CommandBarButton Command="{Binding RefreshAsync}" Label="Refresh">
|
<CommandBarButton Command="{Binding RefreshAsync}" Label="Refresh">
|
||||||
|
|
@ -107,16 +105,14 @@
|
||||||
</CommandBar.PrimaryCommands>
|
</CommandBar.PrimaryCommands>
|
||||||
</CommandBar>
|
</CommandBar>
|
||||||
|
|
||||||
<Grid Margin="12" RowSpacing="12" x:Name="MainGrid" MinHeight="500">
|
<Grid Grid.Row="2" Margin="12" RowSpacing="12" x:Name="MainGrid">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="4" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="2*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Border
|
<Border
|
||||||
Grid.Row="1"
|
Grid.Row="0"
|
||||||
Padding="8"
|
Padding="8"
|
||||||
BorderBrush="Gray"
|
BorderBrush="Gray"
|
||||||
BorderThickness="1">
|
BorderThickness="1">
|
||||||
|
|
@ -161,6 +157,11 @@
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<GridSplitter
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ResizeDirection="Rows" />
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
|
|
@ -171,6 +172,7 @@
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
|
@ -217,6 +219,5 @@
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</Grid>
|
||||||
</ScrollViewer>
|
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue