Skip to content

Commit e133232

Browse files
Added BorderThickness attached DP for outlined date/time pickers (#2805)
* Added BorderThickness attached DP for outlined date/time pickers * Added UI tests * Fixed typos in code comment * Increased delay in tests Tests are running green locally, so I presume it has something to do with the delay being too short * Fixed UI tests Initial mouse position was on the pickers, so I added a dummy button to initially move the mouse position away from the pickers. * Updated UI tests to test validation error scenario Ideally I would like XAMLTest to allow me to set a ValidationError "on the fly" if possible. For now, tests are updated with a real ValidationRule and invalid values are set to provoke the validation error.
1 parent 4cd847f commit e133232

File tree

10 files changed

+346
-19
lines changed

10 files changed

+346
-19
lines changed

MainDemo.Wpf/Pickers.xaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,60 @@
198198
materialDesign:HintAssist.HelperText="Helper text"
199199
Style="{StaticResource MaterialDesignOutlinedDatePicker}"/>
200200
</smtx:XamlDisplay>
201+
202+
<smtx:XamlDisplay
203+
UniqueKey="pickers_unchanging_borderthickness"
204+
HorizontalAlignment="Left"
205+
Margin="0 16 0 0">
206+
<DatePicker
207+
Width="140"
208+
materialDesign:HintAssist.Hint="Pick Date"
209+
materialDesign:HintAssist.HelperText="Helper text"
210+
materialDesign:DatePickerAssist.OutlinedBorderInactiveThickness="2"
211+
materialDesign:HintAssist.FloatingOffset="0,-22"
212+
Style="{StaticResource MaterialDesignOutlinedDatePicker}"/>
213+
</smtx:XamlDisplay>
214+
215+
<smtx:XamlDisplay
216+
UniqueKey="pickers_14_custom_borderthickness"
217+
HorizontalAlignment="Left"
218+
Margin="0 16 0 0">
219+
<DatePicker
220+
Width="140"
221+
materialDesign:HintAssist.Hint="Pick Date"
222+
materialDesign:HintAssist.HelperText="Helper text"
223+
materialDesign:HintAssist.FloatingOffset="0,-23"
224+
materialDesign:DatePickerAssist.OutlinedBorderInactiveThickness="3"
225+
materialDesign:DatePickerAssist.OutlinedBorderActiveThickness="3"
226+
Style="{StaticResource MaterialDesignOutlinedDatePicker}"/>
227+
</smtx:XamlDisplay>
228+
229+
<smtx:XamlDisplay
230+
UniqueKey="pickers_time"
231+
HorizontalAlignment="Left"
232+
Margin="0 16 0 0">
233+
<materialDesign:TimePicker
234+
Is24Hours="True"
235+
Width="140"
236+
materialDesign:HintAssist.Hint="Pick Time"
237+
materialDesign:HintAssist.HelperText="Helper text"
238+
Style="{StaticResource MaterialDesignOutlinedTimePicker}"/>
239+
</smtx:XamlDisplay>
240+
241+
<smtx:XamlDisplay
242+
UniqueKey="pickers_time_custom_borderthickness"
243+
HorizontalAlignment="Left"
244+
Margin="0 16 0 0">
245+
<materialDesign:TimePicker
246+
Is24Hours="True"
247+
Width="140"
248+
materialDesign:HintAssist.Hint="Pick Time"
249+
materialDesign:HintAssist.HelperText="Helper text"
250+
materialDesign:HintAssist.FloatingOffset="0,-23"
251+
materialDesign:TimePickerAssist.OutlinedBorderInactiveThickness="3"
252+
materialDesign:TimePickerAssist.OutlinedBorderActiveThickness="3"
253+
Style="{StaticResource MaterialDesignOutlinedTimePicker}"/>
254+
</smtx:XamlDisplay>
201255
</StackPanel>
202256

203257
<smtx:XamlDisplay

MaterialDesignThemes.UITests/TestBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ protected async Task<Color> GetThemeColor(string name)
2424
return resource.GetAs<Color?>() ?? throw new Exception($"Failed to convert resource '{name}' to color");
2525
}
2626

27-
protected async Task<IVisualElement<T>> LoadXaml<T>(string xaml)
27+
protected async Task<IVisualElement<T>> LoadXaml<T>(string xaml, params (string namespacePrefix, Type type)[] additionalNamespaceDeclarations)
2828
{
2929
await App.InitializeWithMaterialDesign();
30-
return await App.CreateWindowWith<T>(xaml);
30+
return await App.CreateWindowWith<T>(xaml, additionalNamespaceDeclarations);
3131
}
3232

3333
protected async Task<IVisualElement> LoadUserControl<TControl>()

MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.ComponentModel;
33
using System.Globalization;
44
using System.Threading.Tasks;
@@ -64,5 +64,75 @@ await Wait.For(async () =>
6464

6565
recorder.Success();
6666
}
67+
68+
[Fact]
69+
[Description("Issue 2737")]
70+
public async Task OutlinedDatePicker_RespectsActiveAndInactiveBorderThickness_WhenAttachedPropertiesAreSet()
71+
{
72+
await using var recorder = new TestRecorder(App);
73+
74+
// Arrange
75+
var expectedInactiveBorderThickness = new Thickness(4, 3, 2, 1);
76+
var expectedActiveBorderThickness = new Thickness(1, 2, 3, 4);
77+
var stackPanel = await LoadXaml<StackPanel>($@"
78+
<StackPanel>
79+
<DatePicker Style=""{{StaticResource MaterialDesignOutlinedDatePicker}}""
80+
materialDesign:DatePickerAssist.OutlinedBorderInactiveThickness=""{expectedInactiveBorderThickness}""
81+
materialDesign:DatePickerAssist.OutlinedBorderActiveThickness=""{expectedActiveBorderThickness}"">
82+
<DatePicker.SelectedDate>
83+
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
84+
<Binding.ValidationRules>
85+
<local:FutureDateValidationRule ValidatesOnTargetUpdated=""True""/>
86+
</Binding.ValidationRules>
87+
</Binding>
88+
</DatePicker.SelectedDate>
89+
</DatePicker>
90+
<Button x:Name=""Button"" Content=""Some Button"" Margin=""0,20,0,0"" />
91+
</StackPanel>", ("local", typeof(FutureDateValidationRule)));
92+
var datePicker = await stackPanel.GetElement<DatePicker>("/DatePicker");
93+
await datePicker.SetProperty(DatePicker.SelectedDateProperty, DateTime.Now.AddDays(1));
94+
var datePickerTextBox = await datePicker.GetElement<TextBox>("PART_TextBox");
95+
var button = await stackPanel.GetElement<Button>("Button");
96+
97+
// Act
98+
await button.MoveCursorTo();
99+
await Task.Delay(50); // Wait for the visual change
100+
var inactiveBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
101+
await datePickerTextBox.MoveCursorTo();
102+
await Task.Delay(50); // Wait for the visual change
103+
var hoverBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
104+
await datePickerTextBox.LeftClick();
105+
await Task.Delay(50); // Wait for the visual change
106+
var focusedBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
107+
108+
// TODO: It would be cool if a validation error could be set via XAMLTest without the need for the Binding and ValidationRules elements in the XAML above.
109+
await datePicker.SetProperty(DatePicker.SelectedDateProperty, DateTime.Now);
110+
await Task.Delay(50); // Wait for the visual change
111+
var withErrorBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
112+
113+
// Assert
114+
Assert.Equal(expectedInactiveBorderThickness, inactiveBorderThickness);
115+
Assert.Equal(expectedActiveBorderThickness, hoverBorderThickness);
116+
Assert.Equal(expectedActiveBorderThickness, focusedBorderThickness);
117+
Assert.Equal(expectedActiveBorderThickness, withErrorBorderThickness);
118+
119+
recorder.Success();
120+
}
121+
}
122+
123+
public class FutureDateValidationRule : ValidationRule
124+
{
125+
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
126+
{
127+
DateTime time;
128+
if (!DateTime.TryParse((value ?? "").ToString(),
129+
CultureInfo.CurrentCulture,
130+
DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces,
131+
out time)) return new ValidationResult(false, "Invalid date");
132+
133+
return time.Date <= DateTime.Now.Date
134+
? new ValidationResult(false, "Future date required")
135+
: ValidationResult.ValidResult;
136+
}
67137
}
68138
}

MaterialDesignThemes.UITests/WPF/TimePickers/TimePickerTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.ComponentModel;
3+
using System.Globalization;
34
using System.Threading.Tasks;
45
namespace MaterialDesignThemes.UITests.WPF.TimePickers;
56

@@ -337,4 +338,64 @@ public async Task OnTimePickerHelperTextFontSize_ChangesHelperTextFontSize()
337338
Assert.Equal(20, fontSize);
338339
recorder.Success();
339340
}
341+
342+
[Fact]
343+
[Description("Issue 2737")]
344+
public async Task OutlinedTimePicker_RespectsActiveAndInactiveBorderThickness_WhenAttachedPropertiesAreSet()
345+
{
346+
await using var recorder = new TestRecorder(App);
347+
348+
// Arrange
349+
var expectedInactiveBorderThickness = new Thickness(4, 3, 2, 1);
350+
var expectedActiveBorderThickness = new Thickness(1, 2, 3, 4);
351+
var stackPanel = await LoadXaml<StackPanel>($@"
352+
<StackPanel>
353+
<materialDesign:TimePicker Style=""{{StaticResource MaterialDesignOutlinedTimePicker}}""
354+
materialDesign:TimePickerAssist.OutlinedBorderInactiveThickness=""{expectedInactiveBorderThickness}""
355+
materialDesign:TimePickerAssist.OutlinedBorderActiveThickness=""{expectedActiveBorderThickness}"">
356+
<materialDesign:TimePicker.Text>
357+
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
358+
<Binding.ValidationRules>
359+
<local:OnlyTenOClockValidationRule ValidatesOnTargetUpdated=""True""/>
360+
</Binding.ValidationRules>
361+
</Binding>
362+
</materialDesign:TimePicker.Text>
363+
</materialDesign:TimePicker>
364+
<Button x:Name=""Button"" Content=""Some Button"" Margin=""0,20,0,0"" />
365+
</StackPanel>", ("local", typeof(OnlyTenOClockValidationRule)));
366+
var timePicker = await stackPanel.GetElement<TimePicker>("/TimePicker");
367+
await timePicker.SetProperty(TimePicker.TextProperty, "10:00");
368+
var timePickerTextBox = await timePicker.GetElement<TimePickerTextBox>("/TimePickerTextBox");
369+
var button = await stackPanel.GetElement<Button>("Button");
370+
371+
// Act
372+
await button.MoveCursorTo();
373+
await Task.Delay(50); // Wait for the visual change
374+
var inactiveBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
375+
await timePickerTextBox.MoveCursorTo();
376+
await Task.Delay(50); // Wait for the visual change
377+
var hoverBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
378+
await timePickerTextBox.LeftClick();
379+
await Task.Delay(50); // Wait for the visual change
380+
var focusedBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
381+
382+
// TODO: It would be cool if a validation error could be set via XAMLTest without the need for the Binding and ValidationRules elements in the XAML above.
383+
await timePicker.SetProperty(TimePicker.TextProperty, "11:00");
384+
await Task.Delay(50); // Wait for the visual change
385+
var withErrorBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
386+
387+
// Assert
388+
Assert.Equal(expectedInactiveBorderThickness, inactiveBorderThickness);
389+
Assert.Equal(expectedActiveBorderThickness, hoverBorderThickness);
390+
Assert.Equal(expectedActiveBorderThickness, focusedBorderThickness);
391+
Assert.Equal(expectedActiveBorderThickness, withErrorBorderThickness);
392+
393+
recorder.Success();
394+
}
395+
}
396+
397+
public class OnlyTenOClockValidationRule : ValidationRule
398+
{
399+
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
400+
=> value is not "10:00" ? new ValidationResult(false, "Only 10 o'clock allowed") : ValidationResult.ValidResult;
340401
}

MaterialDesignThemes.UITests/XamlTestMixins.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks;
44
using System.Windows.Controls;
55
using MaterialDesignColors;
6+
using MaterialDesignThemes.UITests.WPF.DatePickers;
67
using MaterialDesignThemes.Wpf;
78
using XamlTest;
89

@@ -40,14 +41,21 @@ await app.Initialize(applicationResourceXaml,
4041
Assembly.GetExecutingAssembly().Location);
4142
}
4243

43-
public static async Task<IVisualElement<T>> CreateWindowWith<T>(this IApp app, string xaml)
44+
public static async Task<IVisualElement<T>> CreateWindowWith<T>(this IApp app, string xaml, params (string namespacePrefix, Type type)[] additionalNamespaceDeclarations)
4445
{
46+
var extraNamespaceDeclarations = new StringBuilder("");
47+
foreach ((string namespacePrefix, Type type) in additionalNamespaceDeclarations)
48+
{
49+
extraNamespaceDeclarations.AppendLine($@"xmlns:{namespacePrefix}=""clr-namespace:{type.Namespace};assembly={type.Assembly.GetName().Name}""");
50+
}
51+
4552
string windowXaml = @$"<Window
4653
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
4754
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
4855
xmlns:d=""http://schemas.microsoft.com/expression/blend/2008""
4956
xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006""
5057
xmlns:materialDesign=""http://materialdesigninxaml.net/winfx/xaml/themes""
58+
{extraNamespaceDeclarations}
5159
mc:Ignorable=""d""
5260
Height=""800"" Width=""1100""
5361
TextElement.Foreground=""{{DynamicResource MaterialDesignBody}}""
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
4+
namespace MaterialDesignThemes.Wpf.Converters;
5+
6+
public class OutlinedDateTimePickerActiveBorderThicknessConverter : IMultiValueConverter
7+
{
8+
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
9+
{
10+
if (values.Length == 2
11+
&& values[0] is Thickness baseThickness
12+
&& values[1] is Thickness thicknessToSubtract)
13+
{
14+
var thickness = new Thickness(baseThickness.Left - thicknessToSubtract.Left,
15+
baseThickness.Top - thicknessToSubtract.Top,
16+
baseThickness.Right - thicknessToSubtract.Right,
17+
baseThickness.Bottom - thicknessToSubtract.Bottom);
18+
return thickness;
19+
}
20+
return default(Thickness);
21+
}
22+
23+
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace MaterialDesignThemes.Wpf;
2+
3+
public static class DatePickerAssist
4+
{
5+
private static Thickness DefaultOutlinedBorderInactiveThickness { get; } = new(1);
6+
private static Thickness DefaultOutlinedBorderActiveThickness { get; } = new(2);
7+
8+
public static readonly DependencyProperty OutlinedBorderInactiveThicknessProperty = DependencyProperty.RegisterAttached(
9+
"OutlinedBorderInactiveThickness", typeof(Thickness), typeof(DatePickerAssist), new FrameworkPropertyMetadata(DefaultOutlinedBorderInactiveThickness, FrameworkPropertyMetadataOptions.Inherits));
10+
11+
public static void SetOutlinedBorderInactiveThickness(DependencyObject element, Thickness value)
12+
{
13+
element.SetValue(OutlinedBorderInactiveThicknessProperty, value);
14+
}
15+
16+
public static Thickness GetOutlinedBorderInactiveThickness(DependencyObject element)
17+
{
18+
return (Thickness) element.GetValue(OutlinedBorderInactiveThicknessProperty);
19+
}
20+
21+
public static readonly DependencyProperty OutlinedBorderActiveThicknessProperty = DependencyProperty.RegisterAttached(
22+
"OutlinedBorderActiveThickness", typeof(Thickness), typeof(DatePickerAssist), new FrameworkPropertyMetadata(DefaultOutlinedBorderActiveThickness, FrameworkPropertyMetadataOptions.Inherits));
23+
24+
public static void SetOutlinedBorderActiveThickness(DependencyObject element, Thickness value)
25+
{
26+
element.SetValue(OutlinedBorderActiveThicknessProperty, value);
27+
}
28+
29+
public static Thickness GetOutlinedBorderActiveThickness(DependencyObject element)
30+
{
31+
return (Thickness) element.GetValue(OutlinedBorderActiveThicknessProperty);
32+
}
33+
}

0 commit comments

Comments
 (0)