Skip to content

Commit 557bb1b

Browse files
Cleanup: Aligning hint, helper text and validation error based on padding (#3086)
* Fix padding issues with DatePicker and TimePicker * Extend SmartHint demo page with custom padding and helper text options * Enable toggling validation errors on/off on SmartHint demo page * Add failing UI tests for TextBox * Add failing UI tests for TextBox with validation error * Fix positioning of helper text and validation error in TextBox The helper text and the validation error now respects a custom padding * Added UI test for PasswordBox (some failing some passing) * Fix helper text alignment for PasswordBox with custom padding * Add UI test for ComboBox * Remove wrong usage of TextFieldAssist.TextBoxViewMargin * Add DatePicker and TimePicker to SmartHint demo page * Add failing DatePicker UI tests * Fix helper text placement in DatePicker * Add validation error to DatePicker/TimePicker in SmartHint demo page * Add failing TimePicker UI tests * Fix helper text placement in TimePicker * Minor cleanup
1 parent 9b0c22a commit 557bb1b

15 files changed

+1101
-75
lines changed

MainDemo.Wpf/Domain/SmartHintViewModel.cs

+35-3
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ internal class SmartHintViewModel : ViewModelBase
1010
private bool _showClearButton = true;
1111
private bool _showLeadingIcon = true;
1212
private string _hintText = "Hint text";
13-
private Point _selectedFloatingOffset = new (0, -16);
13+
private string _helperText = "Helper text";
14+
private Point _selectedFloatingOffset = new(0, -16);
15+
private bool _applyCustomPadding;
16+
private Thickness _selectedCustomPadding = new(5);
1417

1518
public IEnumerable<FloatingHintHorizontalAlignment> HorizontalAlignmentOptions { get; } = Enum.GetValues(typeof(FloatingHintHorizontalAlignment)).OfType<FloatingHintHorizontalAlignment>();
1619
public IEnumerable<double> FloatingScaleOptions { get; } = new[] {0.25, 0.5, 0.75, 1.0};
17-
1820
public IEnumerable<Point> FloatingOffsetOptions { get; } = new[] { new Point(0, -16), new Point(0, 16), new Point(16, 16), new Point(-16, -16) };
19-
2021
public IEnumerable<string> ComboBoxOptions { get; } = new[] {"Option 1", "Option 2", "Option 3"};
22+
public IEnumerable<Thickness> CustomPaddingOptions { get; } = new [] { new Thickness(0), new Thickness(5), new Thickness(10), new Thickness(15) };
2123

2224
public bool FloatHint
2325
{
@@ -88,4 +90,34 @@ public string HintText
8890
OnPropertyChanged();
8991
}
9092
}
93+
94+
public string HelperText
95+
{
96+
get => _helperText;
97+
set
98+
{
99+
_helperText = value;
100+
OnPropertyChanged();
101+
}
102+
}
103+
104+
public bool ApplyCustomPadding
105+
{
106+
get => _applyCustomPadding;
107+
set
108+
{
109+
_applyCustomPadding = value;
110+
OnPropertyChanged();
111+
}
112+
}
113+
114+
public Thickness SelectedCustomPadding
115+
{
116+
get => _selectedCustomPadding;
117+
set
118+
{
119+
_selectedCustomPadding = value;
120+
OnPropertyChanged();
121+
}
122+
}
91123
}

MainDemo.Wpf/SmartHint.xaml

+425-14
Large diffs are not rendered by default.

MainDemo.Wpf/SmartHint.xaml.cs

+85-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using MaterialDesignDemo.Domain;
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
using System.Windows.Media;
4+
using MaterialDesignDemo.Domain;
5+
using MaterialDesignThemes.Wpf;
26

37
namespace MaterialDesignDemo;
48

@@ -7,9 +11,89 @@ namespace MaterialDesignDemo;
711
/// </summary>
812
public partial class SmartHint : UserControl
913
{
14+
// Attached property used for binding in the RichTextBox to enable triggering of validation errors.
15+
internal static readonly DependencyProperty RichTextBoxTextProperty = DependencyProperty.RegisterAttached(
16+
"RichTextBoxText", typeof(object), typeof(SmartHint), new PropertyMetadata(null));
17+
internal static void SetRichTextBoxText(DependencyObject element, object value) => element.SetValue(RichTextBoxTextProperty, value);
18+
internal static object GetRichTextBoxText(DependencyObject element) => element.GetValue(RichTextBoxTextProperty);
19+
1020
public SmartHint()
1121
{
1222
DataContext = new SmartHintViewModel();
1323
InitializeComponent();
1424
}
25+
26+
private void HasErrors_OnToggled(object sender, RoutedEventArgs e)
27+
{
28+
CheckBox c = (CheckBox) sender;
29+
30+
foreach (FrameworkElement element in FindVisualChildren<FrameworkElement>(ControlsPanel))
31+
{
32+
var binding = GetBinding(element);
33+
if (binding is null)
34+
continue;
35+
36+
if (c.IsChecked.GetValueOrDefault(false))
37+
{
38+
var error = new ValidationError(new ExceptionValidationRule(), binding)
39+
{
40+
ErrorContent = "Invalid input, please fix it!"
41+
};
42+
Validation.MarkInvalid(binding, error);
43+
}
44+
else
45+
{
46+
Validation.ClearInvalid(binding);
47+
}
48+
}
49+
}
50+
51+
private static BindingExpression? GetBinding(FrameworkElement element)
52+
{
53+
if (element is TextBox textBox)
54+
return textBox.GetBindingExpression(TextBox.TextProperty);
55+
if (element is RichTextBox richTextBox)
56+
return richTextBox.GetBindingExpression(RichTextBoxTextProperty);
57+
if (element is PasswordBox passwordBox)
58+
return passwordBox.GetBindingExpression(PasswordBoxAssist.PasswordProperty);
59+
if (element is ComboBox comboBox)
60+
return comboBox.GetBindingExpression(ComboBox.TextProperty);
61+
if (element is DatePicker datePicker)
62+
return datePicker.GetBindingExpression(DatePicker.TextProperty);
63+
if (element is TimePicker timePicker)
64+
return timePicker.GetBindingExpression(TimePicker.TextProperty);
65+
return default;
66+
}
67+
68+
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObject) where T : DependencyObject
69+
{
70+
if (dependencyObject is null)
71+
yield break;
72+
73+
if (dependencyObject is T obj)
74+
yield return obj;
75+
76+
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
77+
{
78+
DependencyObject child = VisualTreeHelper.GetChild(dependencyObject, i);
79+
foreach (T childOfChild in FindVisualChildren<T>(child))
80+
{
81+
yield return childOfChild;
82+
}
83+
}
84+
}
85+
}
86+
87+
internal class CustomPaddingConverter : IMultiValueConverter
88+
{
89+
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
90+
{
91+
Thickness? defaultPadding = values[0] as Thickness?;
92+
bool applyCustomPadding = (bool) values[1];
93+
Thickness customPadding = (Thickness) values[2];
94+
return applyCustomPadding ? customPadding : defaultPadding ?? DependencyProperty.UnsetValue;
95+
}
96+
97+
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
98+
=> throw new NotImplementedException();
1599
}

MaterialDesignThemes.UITests/WPF/ComboBoxes/ComboBoxTests.cs

+91
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel;
2+
using MaterialDesignThemes.UITests.WPF.TextBoxes;
23

34
namespace MaterialDesignThemes.UITests.WPF.ComboBoxes;
45

@@ -166,4 +167,94 @@ public async Task OnEditableComboBox_ClickInTextArea_FocusesTextBox()
166167

167168
recorder.Success();
168169
}
170+
171+
[Theory]
172+
[InlineData("MaterialDesignFloatingHintComboBox", null)]
173+
[InlineData("MaterialDesignFloatingHintComboBox", 5)]
174+
[InlineData("MaterialDesignFloatingHintComboBox", 20)]
175+
[InlineData("MaterialDesignFilledComboBox", null)]
176+
[InlineData("MaterialDesignFilledComboBox", 5)]
177+
[InlineData("MaterialDesignFilledComboBox", 20)]
178+
[InlineData("MaterialDesignOutlinedComboBox", null)]
179+
[InlineData("MaterialDesignOutlinedComboBox", 5)]
180+
[InlineData("MaterialDesignOutlinedComboBox", 20)]
181+
public async Task ComboBox_WithHintAndHelperText_RespectsPadding(string styleName, int? padding)
182+
{
183+
await using var recorder = new TestRecorder(App);
184+
185+
// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
186+
const double tolerance = 1.5;
187+
188+
string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
189+
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;
190+
191+
var comboBox = await LoadXaml<ComboBox>($@"
192+
<ComboBox {styleAttribute} {paddingAttribute}
193+
materialDesign:HintAssist.Hint=""Hint text""
194+
materialDesign:HintAssist.HelperText=""Helper text""
195+
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"" />
196+
");
197+
198+
var contentHost = await comboBox.GetElement<ScrollViewer>("PART_ContentHost");
199+
var hint = await comboBox.GetElement<SmartHint>("Hint");
200+
var helperText = await comboBox.GetElement<TextBlock>("HelperTextTextBlock");
201+
202+
Rect? contentHostCoordinates = await contentHost.GetCoordinates();
203+
Rect? hintCoordinates = await hint.GetCoordinates();
204+
Rect? helperTextCoordinates = await helperText.GetCoordinates();
205+
206+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
207+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance);
208+
209+
recorder.Success();
210+
}
211+
212+
[Theory]
213+
[InlineData("MaterialDesignFloatingHintComboBox", null)]
214+
[InlineData("MaterialDesignFloatingHintComboBox", 5)]
215+
[InlineData("MaterialDesignFloatingHintComboBox", 20)]
216+
[InlineData("MaterialDesignFilledComboBox", null)]
217+
[InlineData("MaterialDesignFilledComboBox", 5)]
218+
[InlineData("MaterialDesignFilledComboBox", 20)]
219+
[InlineData("MaterialDesignOutlinedComboBox", null)]
220+
[InlineData("MaterialDesignOutlinedComboBox", 5)]
221+
[InlineData("MaterialDesignOutlinedComboBox", 20)]
222+
public async Task ComboBox_WithHintAndValidationError_RespectsPadding(string styleName, int? padding)
223+
{
224+
await using var recorder = new TestRecorder(App);
225+
226+
// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
227+
const double tolerance = 1.5;
228+
229+
string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
230+
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;
231+
232+
var comboBox = await LoadXaml<ComboBox>($@"
233+
<ComboBox {styleAttribute} {paddingAttribute}
234+
materialDesign:HintAssist.Hint=""Hint text""
235+
materialDesign:HintAssist.HelperText=""Helper text""
236+
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"">
237+
<ComboBox.Text>
238+
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
239+
<Binding.ValidationRules>
240+
<local:NotEmptyValidationRule ValidatesOnTargetUpdated=""True""/>
241+
</Binding.ValidationRules>
242+
</Binding>
243+
</ComboBox.Text>
244+
</ComboBox>
245+
", ("local", typeof(NotEmptyValidationRule)));
246+
247+
var contentHost = await comboBox.GetElement<ScrollViewer>("PART_ContentHost");
248+
var hint = await comboBox.GetElement<SmartHint>("Hint");
249+
var errorViewer = await comboBox.GetElement<Border>("DefaultErrorViewer");
250+
251+
Rect? contentHostCoordinates = await contentHost.GetCoordinates();
252+
Rect? hintCoordinates = await hint.GetCoordinates();
253+
Rect? errorViewerCoordinates = await errorViewer.GetCoordinates();
254+
255+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
256+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance);
257+
258+
recorder.Success();
259+
}
169260
}

MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs

+91
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.ComponentModel;
22
using System.Globalization;
3+
using MaterialDesignThemes.UITests.WPF.TextBoxes;
34

45
namespace MaterialDesignThemes.UITests.WPF.DatePickers;
56

@@ -112,6 +113,96 @@ public async Task OutlinedDatePicker_RespectsActiveAndInactiveBorderThickness_Wh
112113

113114
recorder.Success();
114115
}
116+
117+
[Theory]
118+
[InlineData("MaterialDesignFloatingHintDatePicker", null)]
119+
[InlineData("MaterialDesignFloatingHintDatePicker", 5)]
120+
[InlineData("MaterialDesignFloatingHintDatePicker", 20)]
121+
[InlineData("MaterialDesignFilledDatePicker", null)]
122+
[InlineData("MaterialDesignFilledDatePicker", 5)]
123+
[InlineData("MaterialDesignFilledDatePicker", 20)]
124+
[InlineData("MaterialDesignOutlinedDatePicker", null)]
125+
[InlineData("MaterialDesignOutlinedDatePicker", 5)]
126+
[InlineData("MaterialDesignOutlinedDatePicker", 20)]
127+
public async Task DatePicker_WithHintAndHelperText_RespectsPadding(string styleName, int? padding)
128+
{
129+
await using var recorder = new TestRecorder(App);
130+
131+
// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
132+
const double tolerance = 1.5;
133+
134+
string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
135+
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;
136+
137+
var datePicker = await LoadXaml<DatePicker>($@"
138+
<DatePicker {styleAttribute} {paddingAttribute}
139+
materialDesign:HintAssist.Hint=""Hint text""
140+
materialDesign:HintAssist.HelperText=""Helper text""
141+
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"" />
142+
");
143+
144+
var contentHost = await datePicker.GetElement<ScrollViewer>("PART_ContentHost");
145+
var hint = await datePicker.GetElement<SmartHint>("Hint");
146+
var helperText = await datePicker.GetElement<TextBlock>("HelperTextTextBlock");
147+
148+
Rect? contentHostCoordinates = await contentHost.GetCoordinates();
149+
Rect? hintCoordinates = await hint.GetCoordinates();
150+
Rect? helperTextCoordinates = await helperText.GetCoordinates();
151+
152+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
153+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance);
154+
155+
recorder.Success();
156+
}
157+
158+
[Theory]
159+
[InlineData("MaterialDesignFloatingHintDatePicker", null)]
160+
[InlineData("MaterialDesignFloatingHintDatePicker", 5)]
161+
[InlineData("MaterialDesignFloatingHintDatePicker", 20)]
162+
[InlineData("MaterialDesignFilledDatePicker", null)]
163+
[InlineData("MaterialDesignFilledDatePicker", 5)]
164+
[InlineData("MaterialDesignFilledDatePicker", 20)]
165+
[InlineData("MaterialDesignOutlinedDatePicker", null)]
166+
[InlineData("MaterialDesignOutlinedDatePicker", 5)]
167+
[InlineData("MaterialDesignOutlinedDatePicker", 20)]
168+
public async Task DatePicker_WithHintAndValidationError_RespectsPadding(string styleName, int? padding)
169+
{
170+
await using var recorder = new TestRecorder(App);
171+
172+
// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
173+
const double tolerance = 1.5;
174+
175+
string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
176+
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;
177+
178+
var datePicker = await LoadXaml<DatePicker>($@"
179+
<DatePicker {styleAttribute} {paddingAttribute}
180+
materialDesign:HintAssist.Hint=""Hint text""
181+
materialDesign:HintAssist.HelperText=""Helper text""
182+
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"">
183+
<DatePicker.Text>
184+
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
185+
<Binding.ValidationRules>
186+
<local:NotEmptyValidationRule ValidatesOnTargetUpdated=""True""/>
187+
</Binding.ValidationRules>
188+
</Binding>
189+
</DatePicker.Text>
190+
</DatePicker>
191+
", ("local", typeof(NotEmptyValidationRule)));
192+
193+
var contentHost = await datePicker.GetElement<ScrollViewer>("PART_ContentHost");
194+
var hint = await datePicker.GetElement<SmartHint>("Hint");
195+
var errorViewer = await datePicker.GetElement<Border>("DefaultErrorViewer");
196+
197+
Rect? contentHostCoordinates = await contentHost.GetCoordinates();
198+
Rect? hintCoordinates = await hint.GetCoordinates();
199+
Rect? errorViewerCoordinates = await errorViewer.GetCoordinates();
200+
201+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
202+
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance);
203+
204+
recorder.Success();
205+
}
115206
}
116207

117208
public class FutureDateValidationRule : ValidationRule

0 commit comments

Comments
 (0)