Skip to content

Commit

Permalink
Added BorderThickness attached DP for outlined date/time pickers (#2805)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
nicolaihenriksen authored Aug 17, 2022
1 parent 4cd847f commit e133232
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 19 deletions.
54 changes: 54 additions & 0 deletions MainDemo.Wpf/Pickers.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,60 @@
materialDesign:HintAssist.HelperText="Helper text"
Style="{StaticResource MaterialDesignOutlinedDatePicker}"/>
</smtx:XamlDisplay>

<smtx:XamlDisplay
UniqueKey="pickers_unchanging_borderthickness"
HorizontalAlignment="Left"
Margin="0 16 0 0">
<DatePicker
Width="140"
materialDesign:HintAssist.Hint="Pick Date"
materialDesign:HintAssist.HelperText="Helper text"
materialDesign:DatePickerAssist.OutlinedBorderInactiveThickness="2"
materialDesign:HintAssist.FloatingOffset="0,-22"
Style="{StaticResource MaterialDesignOutlinedDatePicker}"/>
</smtx:XamlDisplay>

<smtx:XamlDisplay
UniqueKey="pickers_14_custom_borderthickness"
HorizontalAlignment="Left"
Margin="0 16 0 0">
<DatePicker
Width="140"
materialDesign:HintAssist.Hint="Pick Date"
materialDesign:HintAssist.HelperText="Helper text"
materialDesign:HintAssist.FloatingOffset="0,-23"
materialDesign:DatePickerAssist.OutlinedBorderInactiveThickness="3"
materialDesign:DatePickerAssist.OutlinedBorderActiveThickness="3"
Style="{StaticResource MaterialDesignOutlinedDatePicker}"/>
</smtx:XamlDisplay>

<smtx:XamlDisplay
UniqueKey="pickers_time"
HorizontalAlignment="Left"
Margin="0 16 0 0">
<materialDesign:TimePicker
Is24Hours="True"
Width="140"
materialDesign:HintAssist.Hint="Pick Time"
materialDesign:HintAssist.HelperText="Helper text"
Style="{StaticResource MaterialDesignOutlinedTimePicker}"/>
</smtx:XamlDisplay>

<smtx:XamlDisplay
UniqueKey="pickers_time_custom_borderthickness"
HorizontalAlignment="Left"
Margin="0 16 0 0">
<materialDesign:TimePicker
Is24Hours="True"
Width="140"
materialDesign:HintAssist.Hint="Pick Time"
materialDesign:HintAssist.HelperText="Helper text"
materialDesign:HintAssist.FloatingOffset="0,-23"
materialDesign:TimePickerAssist.OutlinedBorderInactiveThickness="3"
materialDesign:TimePickerAssist.OutlinedBorderActiveThickness="3"
Style="{StaticResource MaterialDesignOutlinedTimePicker}"/>
</smtx:XamlDisplay>
</StackPanel>

<smtx:XamlDisplay
Expand Down
4 changes: 2 additions & 2 deletions MaterialDesignThemes.UITests/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ protected async Task<Color> GetThemeColor(string name)
return resource.GetAs<Color?>() ?? throw new Exception($"Failed to convert resource '{name}' to color");
}

protected async Task<IVisualElement<T>> LoadXaml<T>(string xaml)
protected async Task<IVisualElement<T>> LoadXaml<T>(string xaml, params (string namespacePrefix, Type type)[] additionalNamespaceDeclarations)
{
await App.InitializeWithMaterialDesign();
return await App.CreateWindowWith<T>(xaml);
return await App.CreateWindowWith<T>(xaml, additionalNamespaceDeclarations);
}

protected async Task<IVisualElement> LoadUserControl<TControl>()
Expand Down
72 changes: 71 additions & 1 deletion MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
using System.Globalization;
using System.Threading.Tasks;
Expand Down Expand Up @@ -64,5 +64,75 @@ await Wait.For(async () =>

recorder.Success();
}

[Fact]
[Description("Issue 2737")]
public async Task OutlinedDatePicker_RespectsActiveAndInactiveBorderThickness_WhenAttachedPropertiesAreSet()
{
await using var recorder = new TestRecorder(App);

// Arrange
var expectedInactiveBorderThickness = new Thickness(4, 3, 2, 1);
var expectedActiveBorderThickness = new Thickness(1, 2, 3, 4);
var stackPanel = await LoadXaml<StackPanel>($@"
<StackPanel>
<DatePicker Style=""{{StaticResource MaterialDesignOutlinedDatePicker}}""
materialDesign:DatePickerAssist.OutlinedBorderInactiveThickness=""{expectedInactiveBorderThickness}""
materialDesign:DatePickerAssist.OutlinedBorderActiveThickness=""{expectedActiveBorderThickness}"">
<DatePicker.SelectedDate>
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
<Binding.ValidationRules>
<local:FutureDateValidationRule ValidatesOnTargetUpdated=""True""/>
</Binding.ValidationRules>
</Binding>
</DatePicker.SelectedDate>
</DatePicker>
<Button x:Name=""Button"" Content=""Some Button"" Margin=""0,20,0,0"" />
</StackPanel>", ("local", typeof(FutureDateValidationRule)));
var datePicker = await stackPanel.GetElement<DatePicker>("/DatePicker");
await datePicker.SetProperty(DatePicker.SelectedDateProperty, DateTime.Now.AddDays(1));
var datePickerTextBox = await datePicker.GetElement<TextBox>("PART_TextBox");
var button = await stackPanel.GetElement<Button>("Button");

// Act
await button.MoveCursorTo();
await Task.Delay(50); // Wait for the visual change
var inactiveBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
await datePickerTextBox.MoveCursorTo();
await Task.Delay(50); // Wait for the visual change
var hoverBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
await datePickerTextBox.LeftClick();
await Task.Delay(50); // Wait for the visual change
var focusedBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);

// 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.
await datePicker.SetProperty(DatePicker.SelectedDateProperty, DateTime.Now);
await Task.Delay(50); // Wait for the visual change
var withErrorBorderThickness = await datePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);

// Assert
Assert.Equal(expectedInactiveBorderThickness, inactiveBorderThickness);
Assert.Equal(expectedActiveBorderThickness, hoverBorderThickness);
Assert.Equal(expectedActiveBorderThickness, focusedBorderThickness);
Assert.Equal(expectedActiveBorderThickness, withErrorBorderThickness);

recorder.Success();
}
}

public class FutureDateValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
DateTime time;
if (!DateTime.TryParse((value ?? "").ToString(),
CultureInfo.CurrentCulture,
DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces,
out time)) return new ValidationResult(false, "Invalid date");

return time.Date <= DateTime.Now.Date
? new ValidationResult(false, "Future date required")
: ValidationResult.ValidResult;
}
}
}
61 changes: 61 additions & 0 deletions MaterialDesignThemes.UITests/WPF/TimePickers/TimePickerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Threading.Tasks;
namespace MaterialDesignThemes.UITests.WPF.TimePickers;

Expand Down Expand Up @@ -337,4 +338,64 @@ public async Task OnTimePickerHelperTextFontSize_ChangesHelperTextFontSize()
Assert.Equal(20, fontSize);
recorder.Success();
}

[Fact]
[Description("Issue 2737")]
public async Task OutlinedTimePicker_RespectsActiveAndInactiveBorderThickness_WhenAttachedPropertiesAreSet()
{
await using var recorder = new TestRecorder(App);

// Arrange
var expectedInactiveBorderThickness = new Thickness(4, 3, 2, 1);
var expectedActiveBorderThickness = new Thickness(1, 2, 3, 4);
var stackPanel = await LoadXaml<StackPanel>($@"
<StackPanel>
<materialDesign:TimePicker Style=""{{StaticResource MaterialDesignOutlinedTimePicker}}""
materialDesign:TimePickerAssist.OutlinedBorderInactiveThickness=""{expectedInactiveBorderThickness}""
materialDesign:TimePickerAssist.OutlinedBorderActiveThickness=""{expectedActiveBorderThickness}"">
<materialDesign:TimePicker.Text>
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
<Binding.ValidationRules>
<local:OnlyTenOClockValidationRule ValidatesOnTargetUpdated=""True""/>
</Binding.ValidationRules>
</Binding>
</materialDesign:TimePicker.Text>
</materialDesign:TimePicker>
<Button x:Name=""Button"" Content=""Some Button"" Margin=""0,20,0,0"" />
</StackPanel>", ("local", typeof(OnlyTenOClockValidationRule)));
var timePicker = await stackPanel.GetElement<TimePicker>("/TimePicker");
await timePicker.SetProperty(TimePicker.TextProperty, "10:00");
var timePickerTextBox = await timePicker.GetElement<TimePickerTextBox>("/TimePickerTextBox");
var button = await stackPanel.GetElement<Button>("Button");

// Act
await button.MoveCursorTo();
await Task.Delay(50); // Wait for the visual change
var inactiveBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
await timePickerTextBox.MoveCursorTo();
await Task.Delay(50); // Wait for the visual change
var hoverBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);
await timePickerTextBox.LeftClick();
await Task.Delay(50); // Wait for the visual change
var focusedBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);

// 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.
await timePicker.SetProperty(TimePicker.TextProperty, "11:00");
await Task.Delay(50); // Wait for the visual change
var withErrorBorderThickness = await timePickerTextBox.GetProperty<Thickness>(Control.BorderThicknessProperty);

// Assert
Assert.Equal(expectedInactiveBorderThickness, inactiveBorderThickness);
Assert.Equal(expectedActiveBorderThickness, hoverBorderThickness);
Assert.Equal(expectedActiveBorderThickness, focusedBorderThickness);
Assert.Equal(expectedActiveBorderThickness, withErrorBorderThickness);

recorder.Success();
}
}

public class OnlyTenOClockValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
=> value is not "10:00" ? new ValidationResult(false, "Only 10 o'clock allowed") : ValidationResult.ValidResult;
}
10 changes: 9 additions & 1 deletion MaterialDesignThemes.UITests/XamlTestMixins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using System.Windows.Controls;
using MaterialDesignColors;
using MaterialDesignThemes.UITests.WPF.DatePickers;
using MaterialDesignThemes.Wpf;
using XamlTest;

Expand Down Expand Up @@ -40,14 +41,21 @@ await app.Initialize(applicationResourceXaml,
Assembly.GetExecutingAssembly().Location);
}

public static async Task<IVisualElement<T>> CreateWindowWith<T>(this IApp app, string xaml)
public static async Task<IVisualElement<T>> CreateWindowWith<T>(this IApp app, string xaml, params (string namespacePrefix, Type type)[] additionalNamespaceDeclarations)
{
var extraNamespaceDeclarations = new StringBuilder("");
foreach ((string namespacePrefix, Type type) in additionalNamespaceDeclarations)
{
extraNamespaceDeclarations.AppendLine($@"xmlns:{namespacePrefix}=""clr-namespace:{type.Namespace};assembly={type.Assembly.GetName().Name}""");
}
string windowXaml = @$"<Window
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:d=""http://schemas.microsoft.com/expression/blend/2008""
xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006""
xmlns:materialDesign=""http://materialdesigninxaml.net/winfx/xaml/themes""
{extraNamespaceDeclarations}
mc:Ignorable=""d""
Height=""800"" Width=""1100""
TextElement.Foreground=""{{DynamicResource MaterialDesignBody}}""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Globalization;
using System.Windows.Data;

namespace MaterialDesignThemes.Wpf.Converters;

public class OutlinedDateTimePickerActiveBorderThicknessConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2
&& values[0] is Thickness baseThickness
&& values[1] is Thickness thicknessToSubtract)
{
var thickness = new Thickness(baseThickness.Left - thicknessToSubtract.Left,
baseThickness.Top - thicknessToSubtract.Top,
baseThickness.Right - thicknessToSubtract.Right,
baseThickness.Bottom - thicknessToSubtract.Bottom);
return thickness;
}
return default(Thickness);
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
33 changes: 33 additions & 0 deletions MaterialDesignThemes.Wpf/DatePickerAssist.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace MaterialDesignThemes.Wpf;

public static class DatePickerAssist
{
private static Thickness DefaultOutlinedBorderInactiveThickness { get; } = new(1);
private static Thickness DefaultOutlinedBorderActiveThickness { get; } = new(2);

public static readonly DependencyProperty OutlinedBorderInactiveThicknessProperty = DependencyProperty.RegisterAttached(
"OutlinedBorderInactiveThickness", typeof(Thickness), typeof(DatePickerAssist), new FrameworkPropertyMetadata(DefaultOutlinedBorderInactiveThickness, FrameworkPropertyMetadataOptions.Inherits));

public static void SetOutlinedBorderInactiveThickness(DependencyObject element, Thickness value)
{
element.SetValue(OutlinedBorderInactiveThicknessProperty, value);
}

public static Thickness GetOutlinedBorderInactiveThickness(DependencyObject element)
{
return (Thickness) element.GetValue(OutlinedBorderInactiveThicknessProperty);
}

public static readonly DependencyProperty OutlinedBorderActiveThicknessProperty = DependencyProperty.RegisterAttached(
"OutlinedBorderActiveThickness", typeof(Thickness), typeof(DatePickerAssist), new FrameworkPropertyMetadata(DefaultOutlinedBorderActiveThickness, FrameworkPropertyMetadataOptions.Inherits));

public static void SetOutlinedBorderActiveThickness(DependencyObject element, Thickness value)
{
element.SetValue(OutlinedBorderActiveThicknessProperty, value);
}

public static Thickness GetOutlinedBorderActiveThickness(DependencyObject element)
{
return (Thickness) element.GetValue(OutlinedBorderActiveThicknessProperty);
}
}
Loading

0 comments on commit e133232

Please sign in to comment.