Skip to content

Commit c26fcc5

Browse files
Fix horizontal ScrollBar placement for multi-line TextBox (#3934)
* Add simple issue repro to Home page * WIP on fix. Scroll position is now synced correctly, but the visibility property still needs to be respected properly. * TextBox.HorizontalScrollBarVisibility is now respected * Add null guards and possibly not-needed method SizeChanged() invoke * Remove not needed invocation and null forgiving operators * Place custom scrollbar correctly using converters * Cleanup * Take hover/focus state into account + ignore margins on hidden elements * Only consider hover/focus when style is outlined * Removing simple repro from Home page * Update demo app with TextBox.TextWrapping option on Field page * Fix initial state of custom ScrollBar * Renames property for clarity. Renames `isOutlinedStyle` to `hasOutlinedTextField` in `TextBoxHorizontalScrollBarMarginConverter` for better clarity and consistency. * Improves UI test reliability Adds retry logic to clear button click in date picker test; captures a screenshot of the split button popup during UI test. * Moved converters into "internal" namespace * Moved behavior into new "internals" namespace for behaviors --------- Co-authored-by: Kevin Bost <kitokeboo@gmail.com>
1 parent bdde277 commit c26fcc5

File tree

8 files changed

+328
-89
lines changed

8 files changed

+328
-89
lines changed

src/MainDemo.Wpf/Fields.xaml

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<sys:Double x:Key="FieldWidth">200</sys:Double>
2020
<Thickness x:Key="IconMargin">0,0,8,0</Thickness>
2121
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
22+
<converters:BoolToTextWrappingConverter x:Key="BoolToTextWrappingConverter" />
2223

2324
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource MaterialDesignTextBox}">
2425
<Setter Property="Margin" Value="0,8" />
@@ -172,15 +173,28 @@
172173

173174
<StackPanel>
174175
<smtx:XamlDisplay Margin="0,0,0,32" UniqueKey="fields_3">
175-
<TextBox Height="80"
176-
MinWidth="280"
177-
VerticalAlignment="Stretch"
178-
materialDesign:HintAssist.Hint="Multiline text"
179-
AcceptsReturn="True"
180-
SpellCheck.IsEnabled="True"
181-
Text="Multiline. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. The quick brown fox jumps over the lazy dog. War and peace. Keep going. Go on. For how long? Not long. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
182-
TextWrapping="Wrap"
183-
VerticalScrollBarVisibility="Auto" />
176+
<Grid>
177+
<Grid.RowDefinitions>
178+
<RowDefinition Height="Auto" />
179+
<RowDefinition />
180+
</Grid.RowDefinitions>
181+
182+
<CheckBox x:Name="TextBoxWrapTextCheckBox"
183+
Content="Wrap text"
184+
IsChecked="True" />
185+
186+
<TextBox Grid.Row="1"
187+
Height="80"
188+
MinWidth="280"
189+
VerticalAlignment="Stretch"
190+
materialDesign:HintAssist.Hint="Multiline text"
191+
AcceptsReturn="True"
192+
SpellCheck.IsEnabled="True"
193+
Text="Multiline. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. The quick brown fox jumps over the lazy dog. War and peace. Keep going. Go on. For how long? Not long. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
194+
TextWrapping="{Binding ElementName=TextBoxWrapTextCheckBox, Path=IsChecked, Converter={StaticResource BoolToTextWrappingConverter}}"
195+
VerticalScrollBarVisibility="Auto"
196+
HorizontalScrollBarVisibility="Auto"/>
197+
</Grid>
184198
</smtx:XamlDisplay>
185199

186200
<smtx:XamlDisplay UniqueKey="fields_32">
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
4+
namespace MaterialDesignDemo.Shared.Converters;
5+
6+
public class BoolToTextWrappingConverter : IValueConverter
7+
{
8+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
9+
{
10+
if (value is bool b)
11+
{
12+
return b ? TextWrapping.Wrap : TextWrapping.NoWrap;
13+
}
14+
return TextWrapping.Wrap;
15+
}
16+
17+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
18+
=> throw new NotImplementedException();
19+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using Microsoft.Xaml.Behaviors;
2+
3+
namespace MaterialDesignThemes.Wpf.Behaviors.Internal;
4+
5+
public class TextBoxHorizontalScrollBarBehavior : Behavior<ScrollViewer>
6+
{
7+
private ScrollBar? _builtInScrollBar;
8+
9+
public static readonly DependencyProperty TargetScrollBarProperty =
10+
DependencyProperty.Register(nameof(TargetScrollBar), typeof(ScrollBar), typeof(TextBoxHorizontalScrollBarBehavior), new PropertyMetadata(null, TargetScrollBarChanged));
11+
public ScrollBar TargetScrollBar
12+
{
13+
get => (ScrollBar)GetValue(TargetScrollBarProperty);
14+
set => SetValue(TargetScrollBarProperty, value);
15+
}
16+
17+
private static void TargetScrollBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
18+
{
19+
TextBoxHorizontalScrollBarBehavior b = (TextBoxHorizontalScrollBarBehavior)d;
20+
21+
if (e.OldValue is ScrollBar oldValue)
22+
{
23+
oldValue.Scroll -= b.TargetScrollBar_OnScroll;
24+
}
25+
if (e.NewValue is ScrollBar newValue)
26+
{
27+
newValue.Scroll += b.TargetScrollBar_OnScroll;
28+
}
29+
}
30+
31+
public static readonly DependencyProperty TargetScrollBarVisibilityProperty =
32+
DependencyProperty.Register(nameof(TargetScrollBarVisibility), typeof(ScrollBarVisibility), typeof(TextBoxHorizontalScrollBarBehavior), new PropertyMetadata(ScrollBarVisibility.Hidden));
33+
public ScrollBarVisibility TargetScrollBarVisibility
34+
{
35+
get => (ScrollBarVisibility)GetValue(TargetScrollBarVisibilityProperty);
36+
set => SetValue(TargetScrollBarVisibilityProperty, value);
37+
}
38+
39+
private void TargetScrollBar_OnScroll(object sender, ScrollEventArgs e)
40+
{
41+
if (AssociatedObject is not { } ao) return;
42+
ao.ScrollToHorizontalOffset(e.NewValue);
43+
}
44+
45+
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
46+
{
47+
AssociatedObject.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
48+
_builtInScrollBar = AssociatedObject.FindChild<ScrollBar>("PART_HorizontalScrollBar");
49+
}
50+
51+
private void AssociatedObject_SizeChanged(object sender, SizeChangedEventArgs e)
52+
{
53+
if (TargetScrollBar is not { } ts || _builtInScrollBar is null) return;
54+
55+
ts.ViewportSize = AssociatedObject.ViewportWidth;
56+
ts.Value = AssociatedObject.HorizontalOffset;
57+
ts.Maximum = _builtInScrollBar.Maximum;
58+
UpdateTargetScrollBarVisibility(_builtInScrollBar.Maximum > 0);
59+
}
60+
61+
private void AssociatedObject_ScrollChanged(object sender, ScrollChangedEventArgs e)
62+
{
63+
if (TargetScrollBar is not { } ts || _builtInScrollBar is null) return;
64+
65+
ts.Value = AssociatedObject.HorizontalOffset;
66+
ts.Maximum = _builtInScrollBar.Maximum;
67+
UpdateTargetScrollBarVisibility(_builtInScrollBar.Maximum > 0);
68+
}
69+
70+
private void UpdateTargetScrollBarVisibility(bool showIfRequired)
71+
{
72+
if (TargetScrollBar is not { } ts) return;
73+
74+
AssociatedObject.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
75+
ts.Visibility = TargetScrollBarVisibility switch
76+
{
77+
ScrollBarVisibility.Hidden or ScrollBarVisibility.Disabled => Visibility.Collapsed,
78+
ScrollBarVisibility.Visible => Visibility.Visible,
79+
_ => showIfRequired ? Visibility.Visible : Visibility.Collapsed,
80+
};
81+
}
82+
83+
protected override void OnAttached()
84+
{
85+
base.OnAttached();
86+
AssociatedObject.Loaded += AssociatedObject_Loaded;
87+
AssociatedObject.SizeChanged += AssociatedObject_SizeChanged;
88+
AssociatedObject.ScrollChanged += AssociatedObject_ScrollChanged;
89+
}
90+
91+
protected override void OnDetaching()
92+
{
93+
if (AssociatedObject is { } ao)
94+
{
95+
ao.Loaded -= AssociatedObject_Loaded;
96+
ao.SizeChanged -= AssociatedObject_SizeChanged;
97+
ao.ScrollChanged -= AssociatedObject_ScrollChanged;
98+
}
99+
base.OnDetaching();
100+
}
101+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
4+
namespace MaterialDesignThemes.Wpf.Converters.Internal;
5+
6+
public class TextBoxHorizontalScrollBarMarginConverter : IMultiValueConverter
7+
{
8+
public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture)
9+
{
10+
if (values is [
11+
double leadingIconWidth,
12+
Thickness leadingIconMargin,
13+
double prefixTextWidth,
14+
Thickness prefixTextMargin,
15+
bool isMouseOver,
16+
bool hasKeyboardFocus,
17+
bool hasOutlinedTextField,
18+
Thickness normalBorder,
19+
Thickness activeBorder])
20+
{
21+
double iconMargin = leadingIconWidth > 0 ? leadingIconMargin.Left + leadingIconMargin.Right : 0;
22+
double prefixMargin = prefixTextWidth > 0 ? prefixTextMargin.Left + prefixTextMargin.Right : 0;
23+
double offset = leadingIconWidth + iconMargin + prefixTextWidth + prefixMargin;
24+
double bottomOffset = 0;
25+
double topOffset = 0;
26+
27+
if (hasOutlinedTextField && (isMouseOver || hasKeyboardFocus))
28+
{
29+
double horizDelta = activeBorder.Left - normalBorder.Left;
30+
double vertDeltaTop = activeBorder.Top - normalBorder.Top;
31+
double vertDeltaBottom = activeBorder.Bottom - normalBorder.Bottom;
32+
offset -= horizDelta;
33+
topOffset += vertDeltaTop;
34+
bottomOffset -= vertDeltaBottom;
35+
}
36+
return new Thickness(offset, topOffset, 0, bottomOffset);
37+
}
38+
return new Thickness(0);
39+
}
40+
41+
public object?[]? ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture)
42+
=> throw new NotImplementedException();
43+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
4+
namespace MaterialDesignThemes.Wpf.Converters.Internal;
5+
6+
public class TextBoxHorizontalScrollBarWidthConverter : IMultiValueConverter
7+
{
8+
public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture)
9+
{
10+
if (values is [double contentHostWidth, Visibility verticalScrollBarVisibility])
11+
{
12+
return Math.Max(0, contentHostWidth - (verticalScrollBarVisibility == Visibility.Visible ? SystemParameters.VerticalScrollBarWidth : 0));
13+
}
14+
return double.NaN;
15+
}
16+
17+
public object?[]? ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture)
18+
=> throw new NotImplementedException();
19+
}

0 commit comments

Comments
 (0)