diff --git a/MainDemo.Wpf/Domain/SmartHintViewModel.cs b/MainDemo.Wpf/Domain/SmartHintViewModel.cs index 84213c1f29..0d03158578 100644 --- a/MainDemo.Wpf/Domain/SmartHintViewModel.cs +++ b/MainDemo.Wpf/Domain/SmartHintViewModel.cs @@ -10,14 +10,16 @@ internal class SmartHintViewModel : ViewModelBase private bool _showClearButton = true; private bool _showLeadingIcon = true; private string _hintText = "Hint text"; - private Point _selectedFloatingOffset = new (0, -16); + private string _helperText = "Helper text"; + private Point _selectedFloatingOffset = new(0, -16); + private bool _applyCustomPadding; + private Thickness _selectedCustomPadding = new(5); public IEnumerable HorizontalAlignmentOptions { get; } = Enum.GetValues(typeof(FloatingHintHorizontalAlignment)).OfType(); public IEnumerable FloatingScaleOptions { get; } = new[] {0.25, 0.5, 0.75, 1.0}; - public IEnumerable FloatingOffsetOptions { get; } = new[] { new Point(0, -16), new Point(0, 16), new Point(16, 16), new Point(-16, -16) }; - public IEnumerable ComboBoxOptions { get; } = new[] {"Option 1", "Option 2", "Option 3"}; + public IEnumerable CustomPaddingOptions { get; } = new [] { new Thickness(0), new Thickness(5), new Thickness(10), new Thickness(15) }; public bool FloatHint { @@ -88,4 +90,34 @@ public string HintText OnPropertyChanged(); } } + + public string HelperText + { + get => _helperText; + set + { + _helperText = value; + OnPropertyChanged(); + } + } + + public bool ApplyCustomPadding + { + get => _applyCustomPadding; + set + { + _applyCustomPadding = value; + OnPropertyChanged(); + } + } + + public Thickness SelectedCustomPadding + { + get => _selectedCustomPadding; + set + { + _selectedCustomPadding = value; + OnPropertyChanged(); + } + } } diff --git a/MainDemo.Wpf/SmartHint.xaml b/MainDemo.Wpf/SmartHint.xaml index 4f762f242c..72fb5f9928 100644 --- a/MainDemo.Wpf/SmartHint.xaml +++ b/MainDemo.Wpf/SmartHint.xaml @@ -6,6 +6,7 @@ xmlns:smtx="clr-namespace:ShowMeTheXAML;assembly=ShowMeTheXAML" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:domain="clr-namespace:MaterialDesignDemo.Domain" + xmlns:local="clr-namespace:MaterialDesignDemo" mc:Ignorable="d" x:Name="_root" d:DataContext="{d:DesignInstance domain:SmartHintViewModel, IsDesignTimeCreatable=False}" @@ -13,8 +14,9 @@ + @@ -117,6 +162,16 @@ + + + + + + + + + + @@ -148,6 +203,16 @@ + + + + + + + + + + @@ -181,6 +246,16 @@ + + + + + + + + + + @@ -215,6 +290,16 @@ + + + + + + + + + + @@ -246,6 +331,16 @@ + + + + + + + + + + @@ -277,6 +372,16 @@ + + + + + + + + + + @@ -311,6 +416,16 @@ + + + + + + + + + + @@ -342,6 +457,16 @@ + + + + + + + + + + @@ -373,6 +498,16 @@ + + + + + + + + + + @@ -407,8 +542,18 @@ + + + + + + + + + + @@ -440,8 +585,18 @@ + + + + + + + + + + @@ -473,8 +628,18 @@ + + + + + + + + + + @@ -499,6 +664,252 @@ + + + + + + + + + + + + + + + + MaterialDesignFloatingHintDatePicker + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFilledDatePicker + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignOutlinedDatePicker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFloatingHintTimePicker + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFilledTimePicker + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignOutlinedTimePicker + + + + + + + + + + + + + diff --git a/MainDemo.Wpf/SmartHint.xaml.cs b/MainDemo.Wpf/SmartHint.xaml.cs index 6fba47dd90..8d6ccad771 100644 --- a/MainDemo.Wpf/SmartHint.xaml.cs +++ b/MainDemo.Wpf/SmartHint.xaml.cs @@ -1,4 +1,8 @@ -using MaterialDesignDemo.Domain; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; +using MaterialDesignDemo.Domain; +using MaterialDesignThemes.Wpf; namespace MaterialDesignDemo; @@ -7,9 +11,89 @@ namespace MaterialDesignDemo; /// public partial class SmartHint : UserControl { + // Attached property used for binding in the RichTextBox to enable triggering of validation errors. + internal static readonly DependencyProperty RichTextBoxTextProperty = DependencyProperty.RegisterAttached( + "RichTextBoxText", typeof(object), typeof(SmartHint), new PropertyMetadata(null)); + internal static void SetRichTextBoxText(DependencyObject element, object value) => element.SetValue(RichTextBoxTextProperty, value); + internal static object GetRichTextBoxText(DependencyObject element) => element.GetValue(RichTextBoxTextProperty); + public SmartHint() { DataContext = new SmartHintViewModel(); InitializeComponent(); } + + private void HasErrors_OnToggled(object sender, RoutedEventArgs e) + { + CheckBox c = (CheckBox) sender; + + foreach (FrameworkElement element in FindVisualChildren(ControlsPanel)) + { + var binding = GetBinding(element); + if (binding is null) + continue; + + if (c.IsChecked.GetValueOrDefault(false)) + { + var error = new ValidationError(new ExceptionValidationRule(), binding) + { + ErrorContent = "Invalid input, please fix it!" + }; + Validation.MarkInvalid(binding, error); + } + else + { + Validation.ClearInvalid(binding); + } + } + } + + private static BindingExpression? GetBinding(FrameworkElement element) + { + if (element is TextBox textBox) + return textBox.GetBindingExpression(TextBox.TextProperty); + if (element is RichTextBox richTextBox) + return richTextBox.GetBindingExpression(RichTextBoxTextProperty); + if (element is PasswordBox passwordBox) + return passwordBox.GetBindingExpression(PasswordBoxAssist.PasswordProperty); + if (element is ComboBox comboBox) + return comboBox.GetBindingExpression(ComboBox.TextProperty); + if (element is DatePicker datePicker) + return datePicker.GetBindingExpression(DatePicker.TextProperty); + if (element is TimePicker timePicker) + return timePicker.GetBindingExpression(TimePicker.TextProperty); + return default; + } + + private static IEnumerable FindVisualChildren(DependencyObject? dependencyObject) where T : DependencyObject + { + if (dependencyObject is null) + yield break; + + if (dependencyObject is T obj) + yield return obj; + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++) + { + DependencyObject child = VisualTreeHelper.GetChild(dependencyObject, i); + foreach (T childOfChild in FindVisualChildren(child)) + { + yield return childOfChild; + } + } + } +} + +internal class CustomPaddingConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + Thickness? defaultPadding = values[0] as Thickness?; + bool applyCustomPadding = (bool) values[1]; + Thickness customPadding = (Thickness) values[2]; + return applyCustomPadding ? customPadding : defaultPadding ?? DependencyProperty.UnsetValue; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + => throw new NotImplementedException(); } diff --git a/MaterialDesignThemes.UITests/WPF/ComboBoxes/ComboBoxTests.cs b/MaterialDesignThemes.UITests/WPF/ComboBoxes/ComboBoxTests.cs index 83efa0cdbb..31578f0693 100644 --- a/MaterialDesignThemes.UITests/WPF/ComboBoxes/ComboBoxTests.cs +++ b/MaterialDesignThemes.UITests/WPF/ComboBoxes/ComboBoxTests.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using MaterialDesignThemes.UITests.WPF.TextBoxes; namespace MaterialDesignThemes.UITests.WPF.ComboBoxes; @@ -166,4 +167,94 @@ public async Task OnEditableComboBox_ClickInTextArea_FocusesTextBox() recorder.Success(); } + + [Theory] + [InlineData("MaterialDesignFloatingHintComboBox", null)] + [InlineData("MaterialDesignFloatingHintComboBox", 5)] + [InlineData("MaterialDesignFloatingHintComboBox", 20)] + [InlineData("MaterialDesignFilledComboBox", null)] + [InlineData("MaterialDesignFilledComboBox", 5)] + [InlineData("MaterialDesignFilledComboBox", 20)] + [InlineData("MaterialDesignOutlinedComboBox", null)] + [InlineData("MaterialDesignOutlinedComboBox", 5)] + [InlineData("MaterialDesignOutlinedComboBox", 20)] + public async Task ComboBox_WithHintAndHelperText_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var comboBox = await LoadXaml($@" + +"); + + var contentHost = await comboBox.GetElement("PART_ContentHost"); + var hint = await comboBox.GetElement("Hint"); + var helperText = await comboBox.GetElement("HelperTextTextBlock"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? helperTextCoordinates = await helperText.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } + + [Theory] + [InlineData("MaterialDesignFloatingHintComboBox", null)] + [InlineData("MaterialDesignFloatingHintComboBox", 5)] + [InlineData("MaterialDesignFloatingHintComboBox", 20)] + [InlineData("MaterialDesignFilledComboBox", null)] + [InlineData("MaterialDesignFilledComboBox", 5)] + [InlineData("MaterialDesignFilledComboBox", 20)] + [InlineData("MaterialDesignOutlinedComboBox", null)] + [InlineData("MaterialDesignOutlinedComboBox", 5)] + [InlineData("MaterialDesignOutlinedComboBox", 20)] + public async Task ComboBox_WithHintAndValidationError_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var comboBox = await LoadXaml($@" + + + + + + + + + +", ("local", typeof(NotEmptyValidationRule))); + + var contentHost = await comboBox.GetElement("PART_ContentHost"); + var hint = await comboBox.GetElement("Hint"); + var errorViewer = await comboBox.GetElement("DefaultErrorViewer"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? errorViewerCoordinates = await errorViewer.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } } diff --git a/MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs b/MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs index a61813eefa..6766953788 100644 --- a/MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs +++ b/MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Globalization; +using MaterialDesignThemes.UITests.WPF.TextBoxes; namespace MaterialDesignThemes.UITests.WPF.DatePickers; @@ -112,6 +113,96 @@ public async Task OutlinedDatePicker_RespectsActiveAndInactiveBorderThickness_Wh recorder.Success(); } + + [Theory] + [InlineData("MaterialDesignFloatingHintDatePicker", null)] + [InlineData("MaterialDesignFloatingHintDatePicker", 5)] + [InlineData("MaterialDesignFloatingHintDatePicker", 20)] + [InlineData("MaterialDesignFilledDatePicker", null)] + [InlineData("MaterialDesignFilledDatePicker", 5)] + [InlineData("MaterialDesignFilledDatePicker", 20)] + [InlineData("MaterialDesignOutlinedDatePicker", null)] + [InlineData("MaterialDesignOutlinedDatePicker", 5)] + [InlineData("MaterialDesignOutlinedDatePicker", 20)] + public async Task DatePicker_WithHintAndHelperText_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var datePicker = await LoadXaml($@" + +"); + + var contentHost = await datePicker.GetElement("PART_ContentHost"); + var hint = await datePicker.GetElement("Hint"); + var helperText = await datePicker.GetElement("HelperTextTextBlock"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? helperTextCoordinates = await helperText.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } + + [Theory] + [InlineData("MaterialDesignFloatingHintDatePicker", null)] + [InlineData("MaterialDesignFloatingHintDatePicker", 5)] + [InlineData("MaterialDesignFloatingHintDatePicker", 20)] + [InlineData("MaterialDesignFilledDatePicker", null)] + [InlineData("MaterialDesignFilledDatePicker", 5)] + [InlineData("MaterialDesignFilledDatePicker", 20)] + [InlineData("MaterialDesignOutlinedDatePicker", null)] + [InlineData("MaterialDesignOutlinedDatePicker", 5)] + [InlineData("MaterialDesignOutlinedDatePicker", 20)] + public async Task DatePicker_WithHintAndValidationError_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var datePicker = await LoadXaml($@" + + + + + + + + + +", ("local", typeof(NotEmptyValidationRule))); + + var contentHost = await datePicker.GetElement("PART_ContentHost"); + var hint = await datePicker.GetElement("Hint"); + var errorViewer = await datePicker.GetElement("DefaultErrorViewer"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? errorViewerCoordinates = await errorViewer.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } } public class FutureDateValidationRule : ValidationRule diff --git a/MaterialDesignThemes.UITests/WPF/PasswordBoxes/PasswordBoxTests.cs b/MaterialDesignThemes.UITests/WPF/PasswordBoxes/PasswordBoxTests.cs index e67d3bbe01..196cfa70c9 100644 --- a/MaterialDesignThemes.UITests/WPF/PasswordBoxes/PasswordBoxTests.cs +++ b/MaterialDesignThemes.UITests/WPF/PasswordBoxes/PasswordBoxTests.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using MaterialDesignThemes.UITests.Samples.PasswordBox; +using MaterialDesignThemes.UITests.WPF.TextBoxes; namespace MaterialDesignThemes.UITests.WPF.PasswordBoxes; @@ -186,4 +187,112 @@ public async Task PasswordBox_WithRevealStyle_RespectsMaxLength() recorder.Success(); } + + [Theory] + [InlineData("MaterialDesignFloatingHintPasswordBox", null)] + [InlineData("MaterialDesignFloatingHintPasswordBox", 5)] + [InlineData("MaterialDesignFloatingHintPasswordBox", 20)] + [InlineData("MaterialDesignFloatingHintRevealPasswordBox", null)] + [InlineData("MaterialDesignFloatingHintRevealPasswordBox", 5)] + [InlineData("MaterialDesignFloatingHintRevealPasswordBox", 20)] + [InlineData("MaterialDesignFilledPasswordBox", null)] + [InlineData("MaterialDesignFilledPasswordBox", 5)] + [InlineData("MaterialDesignFilledPasswordBox", 20)] + [InlineData("MaterialDesignFilledRevealPasswordBox", null)] + [InlineData("MaterialDesignFilledRevealPasswordBox", 5)] + [InlineData("MaterialDesignFilledRevealPasswordBox", 20)] + [InlineData("MaterialDesignOutlinedPasswordBox", null)] + [InlineData("MaterialDesignOutlinedPasswordBox", 5)] + [InlineData("MaterialDesignOutlinedPasswordBox", 20)] + [InlineData("MaterialDesignOutlinedRevealPasswordBox", null)] + [InlineData("MaterialDesignOutlinedRevealPasswordBox", 5)] + [InlineData("MaterialDesignOutlinedRevealPasswordBox", 20)] + public async Task PasswordBox_WithHintAndHelperText_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var passwordBox = await LoadXaml($@" + +"); + + var contentHost = await passwordBox.GetElement("PART_ContentHost"); + var hint = await passwordBox.GetElement("Hint"); + var helperText = await passwordBox.GetElement("HelperTextTextBlock"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? helperTextCoordinates = await helperText.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } + + [Theory] + [InlineData("MaterialDesignFloatingHintPasswordBox", null)] + [InlineData("MaterialDesignFloatingHintPasswordBox", 5)] + [InlineData("MaterialDesignFloatingHintPasswordBox", 20)] + [InlineData("MaterialDesignFloatingHintRevealPasswordBox", null)] + [InlineData("MaterialDesignFloatingHintRevealPasswordBox", 5)] + [InlineData("MaterialDesignFloatingHintRevealPasswordBox", 20)] + [InlineData("MaterialDesignFilledPasswordBox", null)] + [InlineData("MaterialDesignFilledPasswordBox", 5)] + [InlineData("MaterialDesignFilledPasswordBox", 20)] + [InlineData("MaterialDesignFilledRevealPasswordBox", null)] + [InlineData("MaterialDesignFilledRevealPasswordBox", 5)] + [InlineData("MaterialDesignFilledRevealPasswordBox", 20)] + [InlineData("MaterialDesignOutlinedPasswordBox", null)] + [InlineData("MaterialDesignOutlinedPasswordBox", 5)] + [InlineData("MaterialDesignOutlinedPasswordBox", 20)] + [InlineData("MaterialDesignOutlinedRevealPasswordBox", null)] + [InlineData("MaterialDesignOutlinedRevealPasswordBox", 5)] + [InlineData("MaterialDesignOutlinedRevealPasswordBox", 20)] + public async Task PasswordBox_WithHintAndValidationError_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var passwordBox = await LoadXaml($@" + + + + + + + + + +", ("local", typeof(NotEmptyValidationRule))); + + var contentHost = await passwordBox.GetElement("PART_ContentHost"); + var hint = await passwordBox.GetElement("Hint"); + var errorViewer = await passwordBox.GetElement("DefaultErrorViewer"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? errorViewerCoordinates = await errorViewer.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } } diff --git a/MaterialDesignThemes.UITests/WPF/TextBoxes/TextBoxTests.cs b/MaterialDesignThemes.UITests/WPF/TextBoxes/TextBoxTests.cs index 3256cdc4a5..9e34efdf66 100644 --- a/MaterialDesignThemes.UITests/WPF/TextBoxes/TextBoxTests.cs +++ b/MaterialDesignThemes.UITests/WPF/TextBoxes/TextBoxTests.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Globalization; using System.Windows.Media; @@ -472,6 +472,96 @@ public async Task FilledTextBox_ValidationErrorMargin_MatchesHelperTextMargin() recorder.Success(); } + + [Theory] + [InlineData("MaterialDesignFloatingHintTextBox", null)] + [InlineData("MaterialDesignFloatingHintTextBox", 5)] + [InlineData("MaterialDesignFloatingHintTextBox", 20)] + [InlineData("MaterialDesignFilledTextBox", null)] + [InlineData("MaterialDesignFilledTextBox", 5)] + [InlineData("MaterialDesignFilledTextBox", 20)] + [InlineData("MaterialDesignOutlinedTextBox", null)] + [InlineData("MaterialDesignOutlinedTextBox", 5)] + [InlineData("MaterialDesignOutlinedTextBox", 20)] + public async Task TextBox_WithHintAndHelperText_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var textBox = await LoadXaml($@" + +"); + + var contentHost = await textBox.GetElement("PART_ContentHost"); + var hint = await textBox.GetElement("Hint"); + var helperText = await textBox.GetElement("HelperTextTextBlock"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? helperTextCoordinates = await helperText.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } + + [Theory] + [InlineData("MaterialDesignFloatingHintTextBox", null)] + [InlineData("MaterialDesignFloatingHintTextBox", 5)] + [InlineData("MaterialDesignFloatingHintTextBox", 20)] + [InlineData("MaterialDesignFilledTextBox", null)] + [InlineData("MaterialDesignFilledTextBox", 5)] + [InlineData("MaterialDesignFilledTextBox", 20)] + [InlineData("MaterialDesignOutlinedTextBox", null)] + [InlineData("MaterialDesignOutlinedTextBox", 5)] + [InlineData("MaterialDesignOutlinedTextBox", 20)] + public async Task TextBox_WithHintAndValidationError_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var textBox = await LoadXaml($@" + + + + + + + + + +", ("local", typeof(NotEmptyValidationRule))); + + var contentHost = await textBox.GetElement("PART_ContentHost"); + var hint = await textBox.GetElement("Hint"); + var errorViewer = await textBox.GetElement("DefaultErrorViewer"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? errorViewerCoordinates = await errorViewer.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } } public class NotEmptyValidationRule : ValidationRule diff --git a/MaterialDesignThemes.UITests/WPF/TimePickers/TimePickerTests.cs b/MaterialDesignThemes.UITests/WPF/TimePickers/TimePickerTests.cs index 23998a2442..fb6cd67561 100644 --- a/MaterialDesignThemes.UITests/WPF/TimePickers/TimePickerTests.cs +++ b/MaterialDesignThemes.UITests/WPF/TimePickers/TimePickerTests.cs @@ -1,5 +1,7 @@ using System.ComponentModel; using System.Globalization; +using MaterialDesignThemes.UITests.WPF.TextBoxes; + namespace MaterialDesignThemes.UITests.WPF.TimePickers; public class TimePickerTests : TestBase @@ -390,6 +392,96 @@ public async Task OutlinedTimePicker_RespectsActiveAndInactiveBorderThickness_Wh recorder.Success(); } + + [Theory] + [InlineData("MaterialDesignFloatingHintTimePicker", null)] + [InlineData("MaterialDesignFloatingHintTimePicker", 5)] + [InlineData("MaterialDesignFloatingHintTimePicker", 20)] + [InlineData("MaterialDesignFilledTimePicker", null)] + [InlineData("MaterialDesignFilledTimePicker", 5)] + [InlineData("MaterialDesignFilledTimePicker", 20)] + [InlineData("MaterialDesignOutlinedTimePicker", null)] + [InlineData("MaterialDesignOutlinedTimePicker", 5)] + [InlineData("MaterialDesignOutlinedTimePicker", 20)] + public async Task TimePicker_WithHintAndHelperText_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var timePicker = await LoadXaml($@" + +"); + + var contentHost = await timePicker.GetElement("PART_ContentHost"); + var hint = await timePicker.GetElement("Hint"); + var helperText = await timePicker.GetElement("HelperTextTextBlock"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? helperTextCoordinates = await helperText.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } + + [Theory] + [InlineData("MaterialDesignFloatingHintTimePicker", null)] + [InlineData("MaterialDesignFloatingHintTimePicker", 5)] + [InlineData("MaterialDesignFloatingHintTimePicker", 20)] + [InlineData("MaterialDesignFilledTimePicker", null)] + [InlineData("MaterialDesignFilledTimePicker", 5)] + [InlineData("MaterialDesignFilledTimePicker", 20)] + [InlineData("MaterialDesignOutlinedTimePicker", null)] + [InlineData("MaterialDesignOutlinedTimePicker", 5)] + [InlineData("MaterialDesignOutlinedTimePicker", 20)] + public async Task TimePicker_WithHintAndValidationError_RespectsPadding(string styleName, int? padding) + { + await using var recorder = new TestRecorder(App); + + // FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases. + const double tolerance = 1.5; + + string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\""; + string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty; + + var timePicker = await LoadXaml($@" + + + + + + + + + +", ("local", typeof(NotEmptyValidationRule))); + + var contentHost = await timePicker.GetElement("PART_ContentHost"); + var hint = await timePicker.GetElement("Hint"); + var errorViewer = await timePicker.GetElement("DefaultErrorViewer"); + + Rect? contentHostCoordinates = await contentHost.GetCoordinates(); + Rect? hintCoordinates = await hint.GetCoordinates(); + Rect? errorViewerCoordinates = await errorViewer.GetCoordinates(); + + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance); + Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance); + + recorder.Success(); + } } public class OnlyTenOClockValidationRule : ValidationRule diff --git a/MaterialDesignThemes.Wpf/Converters/ThicknessCloneConverter.cs b/MaterialDesignThemes.Wpf/Converters/ThicknessCloneConverter.cs new file mode 100644 index 0000000000..8b3a0152b4 --- /dev/null +++ b/MaterialDesignThemes.Wpf/Converters/ThicknessCloneConverter.cs @@ -0,0 +1,43 @@ +using System.Globalization; +using System.Windows.Data; + +namespace MaterialDesignThemes.Wpf.Converters; + +public class ThicknessCloneConverter : IValueConverter +{ + public ThicknessEdges CloneEdges { get; set; } = ThicknessEdges.All; + + public double NonClonedEdgeValue { get; set; } + + public double? FixedLeft { get; set; } + public double? FixedTop { get; set; } + public double? FixedRight { get; set; } + public double? FixedBottom { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Thickness thickness) + { + double left = FixedLeft ?? (CloneEdges.HasFlag(ThicknessEdges.Left) ? thickness.Left : NonClonedEdgeValue); + double top = FixedTop ?? (CloneEdges.HasFlag(ThicknessEdges.Top) ? thickness.Top : NonClonedEdgeValue); + double right = FixedRight ?? (CloneEdges.HasFlag(ThicknessEdges.Right) ? thickness.Right : NonClonedEdgeValue); + double bottom = FixedBottom ?? (CloneEdges.HasFlag(ThicknessEdges.Bottom) ? thickness.Bottom : NonClonedEdgeValue); + return new Thickness(left, top, right, bottom); + } + return DependencyProperty.UnsetValue; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); +} + +[Flags] +public enum ThicknessEdges +{ + None = 0, + Left = 1, + Top = 2, + Right = 4, + Bottom = 8, + All = Left | Top | Right | Bottom +} diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml index cc559e7f10..4ecddf9b16 100644 --- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml +++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml @@ -26,6 +26,7 @@ + 4 8 @@ -363,7 +364,6 @@ - - + - - - - diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DatePicker.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DatePicker.xaml index ae48fe027e..f4f13f9dfa 100644 --- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DatePicker.xaml +++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DatePicker.xaml @@ -19,6 +19,7 @@ + diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.PasswordBox.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.PasswordBox.xaml index cb741df511..6c05be4662 100644 --- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.PasswordBox.xaml +++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.PasswordBox.xaml @@ -21,6 +21,7 @@ + diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ValidationErrorTemplate.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ValidationErrorTemplate.xaml index 91def091e5..af2bc9d478 100644 --- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ValidationErrorTemplate.xaml +++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ValidationErrorTemplate.xaml @@ -1,10 +1,15 @@  + xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf" + xmlns:converters="clr-namespace:MaterialDesignThemes.Wpf.Converters"> + + + + - + - +