From 1229f6232cbd0c5c87b583d1bc347feac18670d4 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:22:34 +0100 Subject: [PATCH 01/42] TimeSpanPicker: initial --- Radzen.Blazor/Common.cs | 34 + Radzen.Blazor/RadzenTimeSpanPicker.razor | 147 +++ Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 899 ++++++++++++++++++ Radzen.Blazor/Rendering/Popup.razor | 14 +- Radzen.Blazor/Rendering/PopupOrInline.razor | 39 + Radzen.Blazor/themes/_components.scss | 1 + Radzen.Blazor/themes/_css-variables.scss | 21 + .../components/blazor/_timespanpicker.scss | 176 ++++ Radzen.Blazor/themes/dark-base.scss | 3 + Radzen.Blazor/themes/dark.scss | 3 + .../themes/humanistic-dark-base.scss | 3 + Radzen.Blazor/themes/humanistic-dark.scss | 3 + Radzen.Blazor/themes/material-base.scss | 12 + Radzen.Blazor/themes/material.scss | 12 + Radzen.Blazor/themes/software-dark-base.scss | 3 + Radzen.Blazor/themes/software-dark.scss | 3 + Radzen.Blazor/themes/standard-base.scss | 5 + Radzen.Blazor/themes/standard-dark-base.scss | 6 + Radzen.Blazor/themes/standard-dark.scss | 6 + Radzen.Blazor/themes/standard.scss | 5 + .../Properties/launchSettings.json | 24 +- .../Pages/TimeSpanPickerBindValue.razor | 8 + .../Pages/TimeSpanPickerChangeEvent.razor | 18 + .../Pages/TimeSpanPickerPage.razor | 50 + 24 files changed, 1476 insertions(+), 19 deletions(-) create mode 100644 Radzen.Blazor/RadzenTimeSpanPicker.razor create mode 100644 Radzen.Blazor/RadzenTimeSpanPicker.razor.cs create mode 100644 Radzen.Blazor/Rendering/PopupOrInline.razor create mode 100644 Radzen.Blazor/themes/components/blazor/_timespanpicker.scss create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor diff --git a/Radzen.Blazor/Common.cs b/Radzen.Blazor/Common.cs index a09204b8aa0..8f95f206e83 100644 --- a/Radzen.Blazor/Common.cs +++ b/Radzen.Blazor/Common.cs @@ -372,6 +372,40 @@ public enum Month December = 11, } + /// + /// Specifies the time unit of . + /// + public enum TimeSpanUnit + { + /// + /// Day. + /// + Day = 0, + /// + /// Hour. + /// + Hour = 1, + /// + /// Minute. + /// + Minute = 2, + /// + /// Second. + /// + Second = 3, + /// + /// Millisecond. + /// + Millisecond = 4 + #if NET7_0_OR_GREATER + , + /// + /// Microsecond. + /// + Microsecond = 5 + #endif + } + /// /// Html editor mode (Rendered or Raw). Also used for toolbar buttons to enable/disable according to mode. /// diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor new file mode 100644 index 00000000000..75983a235eb --- /dev/null +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -0,0 +1,147 @@ +@using Radzen +@using Radzen.Blazor.Rendering +@using Microsoft.AspNetCore.Components.Forms +@using System.Linq.Expressions +@using System.Globalization +@using Microsoft.JSInterop + +@typeparam TValue +@inherits RadzenComponent +@implements IRadzenFormComponent + +@if (!Visible) +{ + return; +} + +
+ @if (!Inline) + { + + @if (ShowPopupButton) + { + + } + @if (AllowClear && HasValue && (_isNullable || ConfirmedValue != DefaultNonNullValue) && !Disabled && !ReadOnly) + { + + } + } + +
+ + + + + + + + @if (_canBeEitherPositiveOrNegative is false) + { +
+ @(_isUnconfirmedValueNegative ? NegativeValueText : PositiveValueText) +
+ } + @if (FieldPrecision >= TimeSpanUnit.Day && TimeFieldsMaxValues[TimeSpanUnit.Day] > 0) + { +
+ + +
+ } + @if (FieldPrecision >= TimeSpanUnit.Hour && TimeFieldsMaxValues[TimeSpanUnit.Hour] > 0) + { +
+ + +
+ } + @if (FieldPrecision >= TimeSpanUnit.Minute && TimeFieldsMaxValues[TimeSpanUnit.Minute] > 0) + { +
+ + +
+ } + @if (FieldPrecision >= TimeSpanUnit.Second && TimeFieldsMaxValues[TimeSpanUnit.Second] > 0) + { +
+ + +
+ } + @if (FieldPrecision >= TimeSpanUnit.Millisecond && TimeFieldsMaxValues[TimeSpanUnit.Millisecond] > 0) + { +
+ + +
+ } + @{ + #if NET7_0_OR_GREATER + if (FieldPrecision >= TimeSpanUnit.Microsecond && TimeFieldsMaxValues[TimeSpanUnit.Microsecond] > 0) + { +
+ + +
+ } + #endif + } + + @if (ShowConfirmationButton) + { + + } +
+
+
diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs new file mode 100644 index 00000000000..514ad607639 --- /dev/null +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -0,0 +1,899 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; +using Radzen.Blazor.Rendering; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace Radzen.Blazor +{ + /// + /// RadzenTimeSpanPicker component. + /// + /// and nullable are supported. + /// + /// + /// <RadzenTimeSpanPicker @bind-Value="@someValue" TValue="TimeSpan" Change=@(args => Console.WriteLine($"Selected time span: {args}")) /> + /// + /// + public partial class RadzenTimeSpanPicker : RadzenComponent, IRadzenFormComponent + { + #region Parameters: value + private TValue _value; + /// + /// Specifies the value of the component. + /// + [Parameter] + public TValue Value + { + get => _value; + set + { + if (EqualityComparer.Default.Equals(ConfirmedValue, _value)) + { + return; + } + + _value = value; + + if (value is null) + { + ConfirmedValue = null; + return; + } + + ConfirmedValue = (TimeSpan?) (object) value; + } + } + /// + /// Specifies the minimum time stamp allowed. + /// + [Parameter] + public TimeSpan Min { get; set; } = TimeSpan.MinValue; + + /// + /// Specifies the maximum time stamp allowed. + /// + [Parameter] + public TimeSpan Max { get; set; } = TimeSpan.MaxValue; + #endregion + + #region Parameters: input config + /// + /// Specifies additional custom attributes that will be rendered by the input. + /// + [Parameter] + public IReadOnlyDictionary InputAttributes { get; set; } + + /// + /// Specifies whether the value can be cleared. + /// Set to true by default if is nullable, false otherwise. + /// + /// true if value can be cleared; otherwise, false. + [Parameter] + public bool AllowClear { get; set; } = _isNullable; + + /// + /// Specifies whether input in the input field is allowed. + /// + /// true if input is allowed; otherwise, false. + [Parameter] + public bool AllowInput { get; set; } = true; + + /// + /// Specifies whether the input field is disabled. + /// + /// true if disabled; otherwise, false. + [Parameter] + public bool Disabled { get; set; } + + /// + /// Specifies whether the input field is read only. + /// + /// true if read only; otherwise, false. + [Parameter] + public bool ReadOnly { get; set; } + + /// + /// Specifies the time span format in the input field. + /// For more details, see the documentation of + /// standard + /// and custom + /// time span format strings. + /// + [Parameter] + public string TimeSpanFormat { get; set; } + + /// + /// Specifies the most precise time unit field in the picker panel. Set to by default. + /// + [Parameter] + public TimeSpanUnit FieldPrecision { get; set; } = TimeSpanUnit.Second; + + /// + /// Specifies the step of the days field in the picker panel. + /// + [Parameter] + public string DaysStep { get; set; } + + /// + /// Specifies the step of the hours field in the picker panel. + /// + [Parameter] + public string HoursStep { get; set; } + + /// + /// Specifies the step of the minutes field in the picker panel. + /// + [Parameter] + public string MinutesStep { get; set; } + + /// + /// Specifies the step of the seconds field in the picker panel. + /// + [Parameter] + public string SecondsStep { get; set; } + + /// + /// Specifies the step of the milliseconds field in the picker panel. + /// + [Parameter] + public string MillisecondsStep { get; set; } + + #if NET7_0_OR_GREATER + /// + /// Specifies the step of the microseconds field in the picker panel. + /// + [Parameter] + public string MicrosecondsStep { get; set; } + #endif + + /// + /// Specifies the tab index. + /// + [Parameter] + public int TabIndex { get; set; } = 0; + + /// + /// Specifies custom function to parse the input. + /// If it's not defined or the function it returns null, a built-in parser us used instead. + /// + [Parameter] + public Func ParseInput { get; set; } + #endregion + + #region Parameters: appearance + /// + /// Specifies whether the component is inline or shows a popup. + /// + /// true if inline; false if shows a popup. + [Parameter] + public bool Inline { get; set; } + + /// + /// Specifies whether to display popup icon button in the input field. + /// + /// true to display the button to open the popup; + /// false to hide the button to open the popup, clicking the input field opens the popup instead. + [Parameter] + public bool ShowPopupButton { get; set; } = true; + + /// + /// Specifies whether to display the confirmation button in the panel to accept changes. + /// + /// true if the confirmation button is shown; otherwise, false. + [Parameter] + public bool ShowConfirmationButton { get; set; } = true; + + /// + /// Specifies whether the time fields in the panel, except for the days field, are padded with leading zeros. + /// + /// true if fields are padded; otherwise, false. + [Parameter] + public bool PadTimeValues { get; set; } + + /// + /// Specifies the input CSS classes, separated with spaces. + /// + [Parameter] + public string InputClass { get; set; } + + /// + /// Specifies the popup toggle button CSS classes, separated with spaces. + /// + [Parameter] + public string TogglePopupButtonClass { get; set; } + #endregion + + #region Parameters: labels + /// + /// Specifies the input placeholder. + /// + [Parameter] + public string Placeholder { get; set; } + + /// + /// Specifies the aria label for the toggle popup button. + /// + [Parameter] + public string TogglePopupAriaLabel { get; set; } = "Toggle popup"; + + /// + /// Specifies the label of the confirmation button. + /// + [Parameter] + public string ConfirmationButtonLabel { get; set; } = "OK"; + + /// + /// Specifies the label of the positive value button. + /// + [Parameter] + public string PositiveButtonLabel { get; set; } = "+"; + + /// + /// Specifies the label of the negative value button. + /// + [Parameter] + public string NegativeButtonLabel { get; set; } = "-"; + + /// + /// Specifies the text displayed before the fields in the panel when the value is positive and there's no value sign picker. + /// + [Parameter] + public string PositiveValueText { get; set; } = string.Empty; + + /// + /// Specifies the text displayed before the fields in the panel when the value is negative and there's no value sign picker. + /// + [Parameter] + public string NegativeValueText { get; set; } = "-"; + + /// + /// Specifies the text displayed next to the days field. + /// + [Parameter] + public string DaysUnitText { get; set; } = "Days"; + + /// + /// Specifies the text displayed next to the hours field. + /// + [Parameter] + public string HoursUnitText { get; set; } = "Hours"; + + /// + /// Specifies the text displayed next to the minutes field. + /// + [Parameter] + public string MinutesUnitText { get; set; } = "Minutes"; + + /// + /// Specifies the text displayed next to the seconds field. + /// + [Parameter] + public string SecondsUnitText { get; set; } = "Seconds"; + + /// + /// Specifies the text displayed next to the milliseconds field. + /// + [Parameter] + public string MillisecondsUnitText { get; set; } = "Milliseconds"; + + #if NET7_0_OR_GREATER + /// + /// Specifies the text displayed next to the microseconds field. + /// + [Parameter] + public string MicrosecondsUnitText { get; set; } = "Microseconds"; + #endif + #endregion + + #region Parameters: other config + /// + /// Specifies the name of the form component. + /// + [Parameter] + public string Name { get; set; } + + /// + /// Specifies the render mode of the popup. + /// + [Parameter] + public PopupRenderMode PopupRenderMode { get; set; } = PopupRenderMode.Initial; + + /// + /// Specifies the value expression used while creating the . + /// + [Parameter] + public Expression> ValueExpression { get; set; } + #endregion + + #region Parameters: events + /// + /// Specifies the callback of the value change. + /// + [Parameter] + public EventCallback ValueChanged { get; set; } + + /// + /// Specifies the callback of the underlying nullable value. + /// + [Parameter] + public EventCallback Change { get; set; } + + /// + /// Specifies the callback of the unconfirmed time span (in a panel with a confirmation button) change. + /// + [Parameter] + public EventCallback UnconfirmedValueChanged { get; set; } + #endregion + + + #region Form fields + private IRadzenForm _form; + /// + /// Specifies the form this component belongs to. + /// + [CascadingParameter] + public IRadzenForm Form + { + get => _form; + set + { + if (_form == value || value is null) + return; + + _form = value; + _form.AddComponent(this); + } + } + + /// + /// Specifies the edit context of this component. + /// + [CascadingParameter] + public EditContext EditContext { get; set; } + + /// + /// Specifies the context of this component. + /// + public IFormFieldContext FormFieldContext { get; set; } = null; + #endregion + + + #region Calculated properties and references + private static readonly bool _isNullable = Nullable.GetUnderlyingType(typeof(TValue)) is not null; + + /// + /// Indicates whether this instance is bound callback has delegate). + /// + /// true if this instance is bound; otherwise, false. + public bool IsBound => ValueChanged.HasDelegate; + + /// + /// Indicates whether this instance has a confirmed value. + /// + /// true if this instance has value; otherwise, false. + public bool HasValue => ConfirmedValue.HasValue; + + /// + /// Gets the formatted value. + /// + public string FormattedValue => HasValue ? string.Format(Culture, "{0:" + TimeSpanFormat + "}", Value) : ""; + + /// + /// Gets the field identifier. + /// + public FieldIdentifier FieldIdentifier { get; private set; } + + /// + /// Gets the input reference. + /// + protected ElementReference input; + #endregion + + + #region Underlying values + private TimeSpan? _confirmedValue; + private TimeSpan? ConfirmedValue + { + get => _confirmedValue; + set + { + if (_confirmedValue == value) + { + return; + } + + _confirmedValue = value.HasValue ? AdjustToBounds(value.Value) : null; + var publicValue = _confirmedValue is null + ? _isNullable ? null : DefaultNonNullValue + : _confirmedValue; + Value = (TValue) (object) publicValue; + ResetUnconfirmedValue(); + } + } + + private TimeSpan _unconfirmedValue; + private TimeSpan UnconfirmedValue + { + get => _unconfirmedValue; + set + { + if (_unconfirmedValue == value) + { + return; + } + + var newValue = AdjustToBounds(value); + + if (newValue != TimeSpan.Zero || _canBeEitherPositiveOrNegative is false) + { + _isUnconfirmedValueNegative = newValue < TimeSpan.Zero; + } + + _unconfirmedValue = newValue; + } + } + + private bool _isUnconfirmedValueNegative = false; + private int UnconformedValueSign => _isUnconfirmedValueNegative ? -1 : 1; + + private TimeSpan DefaultNonNullValue => AdjustToBounds(TimeSpan.Zero); + + private void ResetUnconfirmedValue() => UnconfirmedValue = ConfirmedValue ?? DefaultNonNullValue; + private TimeSpan AdjustToBounds(TimeSpan value) => value < Min ? Min : value > Max ? Max : value; + #endregion + + + #region Methods: component general + private bool _firstRender; + /// + protected override Task OnAfterRenderAsync(bool firstRender) + { + _firstRender = firstRender; + return base.OnAfterRenderAsync(firstRender); + } + + /// + public override async Task SetParametersAsync(ParameterView parameters) + { + if (parameters.DidParameterChange(nameof(Min), Min) || parameters.DidParameterChange(nameof(Max), Max)) + { + var min = parameters.GetValueOrDefault(nameof(Min)); + var max = parameters.GetValueOrDefault(nameof(Max)); + + SetPanelFieldsSetup(min, max); + } + + var shouldClose = + parameters.DidParameterChange(nameof(Visible), Visible) && parameters.GetValueOrDefault(nameof(Visible)) is false + || parameters.DidParameterChange(nameof(Disabled), Disabled) && parameters.GetValueOrDefault(nameof(Disabled)) + || parameters.DidParameterChange(nameof(ReadOnly), ReadOnly) && parameters.GetValueOrDefault(nameof(ReadOnly)); + + await base.SetParametersAsync(parameters); + + if (shouldClose && !_firstRender && IsJSRuntimeAvailable) + { + await ClosePopup(); + } + + if (EditContext != null && ValueExpression != null && FieldIdentifier.Model != EditContext.Model) + { + FieldIdentifier = FieldIdentifier.Create(ValueExpression); + EditContext.OnValidationStateChanged -= ValidationStateChanged; + EditContext.OnValidationStateChanged += ValidationStateChanged; + } + } + + /// + public override void Dispose() + { + base.Dispose(); + + if (EditContext != null) + { + EditContext.OnValidationStateChanged -= ValidationStateChanged; + } + + Form?.RemoveComponent(this); + } + + private async Task OnChange() + { + await ValueChanged.InvokeAsync(Value); + await Change.InvokeAsync(ConfirmedValue); + + if (FieldIdentifier.FieldName != null) + { + EditContext?.NotifyFieldChanged(FieldIdentifier); + } + } + + private void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e) + { + StateHasChanged(); + } + #endregion + + #region Methods: frontend interactions used externally + /// + /// Closes this instance popup. + /// + public async Task Close() + { + if (Disabled || ReadOnly || Inline) + return; + + await ClosePopup(); + + StateHasChanged(); + } + + /// + public async ValueTask FocusAsync() + { + try + { + await input.FocusAsync(); + } + catch + { } + } + #endregion + + #region Methods: other + /// + /// Gets the value of the component. + /// + /// System.Object. + public object GetValue() => Value; + #endregion + + + #region Internal: input text handling + private bool IsInputAllowed => ReadOnly || !AllowInput; + + private async Task Clear() + { + if (Disabled || ReadOnly) + { + return; + } + + ConfirmedValue = null; + await ValueChanged.InvokeAsync(Value); + await Change.InvokeAsync(ConfirmedValue); + + if (FieldIdentifier.FieldName != null) + { + EditContext?.NotifyFieldChanged(FieldIdentifier); + } + + StateHasChanged(); + } + /// + /// Parses the time span. + /// + private async Task ParseTimeSpan() + { + TimeSpan? newValue; + var inputValue = await JSRuntime.InvokeAsync("Radzen.getInputValue", input); + bool valid = TryParseInput(inputValue, out TimeSpan value); + + var nullable = AllowClear || Nullable.GetUnderlyingType(typeof(TValue)) != null; + + if (valid) + { + newValue = value; + } + else + { + newValue = null; + + if (nullable) + { + await JSRuntime.InvokeAsync("Radzen.setInputValue", input, ""); + } + else + { + await JSRuntime.InvokeAsync("Radzen.setInputValue", input, FormattedValue); + } + } + + if (ConfirmedValue != newValue && (newValue != null || nullable)) + { + ConfirmedValue = newValue; + await ValueChanged.InvokeAsync(Value); + await Change.InvokeAsync(ConfirmedValue); + + if (FieldIdentifier.FieldName != null) + { + EditContext?.NotifyFieldChanged(FieldIdentifier); + } + + StateHasChanged(); + } + } + + private bool TryParseInput(string inputValue, out TimeSpan value) + { + value = TimeSpan.Zero; + bool valid = false; + + if (ParseInput != null) + { + TimeSpan? custom = ParseInput.Invoke(inputValue); + + if (custom.HasValue) + { + valid = true; + value = custom.Value; + } + } + else + { + valid = TimeSpan.TryParseExact(inputValue, TimeSpanFormat, null, TimeSpanStyles.None, out value); + + if (!valid) + { + valid = TimeSpan.TryParse(inputValue, out value); + } + } + + return valid; + } + #endregion + + #region Internal: input mouse and keyboard events + private async Task ClickPopupButton() + { + if (!Disabled && !ReadOnly && !Inline) + { + await TogglePopup(); + await FocusAsync(); + } + } + + private Task ClickInputField() + => ShowPopupButton ? Task.CompletedTask : ClickPopupButton(); + + private bool _preventKeyPress = false; + private async Task PressKey(KeyboardEventArgs args) + { + var key = args.Code ?? args.Key; + + if (key == "Enter") + { + _preventKeyPress = true; + + await TogglePopup(); + } + else if (key == "Escape") + { + _preventKeyPress = false; + + await ClosePopup(); + await FocusAsync(); + } + else + { + _preventKeyPress = false; + } + } + #endregion + + #region Internal: popup general actions + private PopupOrInline popupHolder; + + private Task TogglePopup() + => Inline ? Task.CompletedTask : popupHolder.Popup?.ToggleAsync(Element) ?? Task.CompletedTask; + + private Task ClosePopup() + => Inline ? Task.CompletedTask : popupHolder.Popup?.CloseAsync(Element) ?? Task.CompletedTask; + + private async Task PopupKeyDown(KeyboardEventArgs args) + { + var key = args.Code ?? args.Key; + if (key == "Escape") + { + _preventKeyPress = false; + + await ClosePopup(); + await FocusAsync(); + } + } + #endregion + + #region Internal: panel fields setup + private static readonly Dictionary _timeUnitMaxAbsoluteValues = new() + { + { TimeSpanUnit.Day, TimeSpan.MaxValue.Days }, + { TimeSpanUnit.Hour, 23 }, + { TimeSpanUnit.Minute, 59 }, + { TimeSpanUnit.Second, 59 }, + { TimeSpanUnit.Millisecond, 999 } + #if NET7_0_OR_GREATER + , { TimeSpanUnit.Microsecond, 999 } + #endif + }; + private static readonly Dictionary _timeUnitZeroValues = Enum + .GetValues() + .ToDictionary(x => x, x => 0); + + private Dictionary _negativeTimeFieldsMaxValues = new(_timeUnitMaxAbsoluteValues); + private Dictionary _positiveTimeFieldsMaxValues = new(_timeUnitMaxAbsoluteValues); + private Dictionary TimeFieldsMaxValues + => _isUnconfirmedValueNegative ? _negativeTimeFieldsMaxValues : _positiveTimeFieldsMaxValues; + + private bool _canBeEitherPositiveOrNegative = true; + + private void SetPanelFieldsSetup(TimeSpan min, TimeSpan max) + { + var canBeNegative = min < TimeSpan.Zero; + var canBePositive = max > TimeSpan.Zero; + _canBeEitherPositiveOrNegative = canBeNegative && canBePositive; + + _negativeTimeFieldsMaxValues = canBeNegative ? GetTimeUnitMaxValues(min) : new (_timeUnitZeroValues); + _positiveTimeFieldsMaxValues = canBePositive ? GetTimeUnitMaxValues(max) : new(_timeUnitZeroValues); + } + + private Dictionary GetTimeUnitMaxValues(TimeSpan boundary) + { + var timeUnitMaxValues = new Dictionary(_timeUnitMaxAbsoluteValues); + + if (boundary.Days != 0) + { + timeUnitMaxValues[TimeSpanUnit.Day] = Math.Abs(boundary.Days); + return timeUnitMaxValues; + } + timeUnitMaxValues[TimeSpanUnit.Day] = 0; + + if (boundary.Hours != 0) + { + timeUnitMaxValues[TimeSpanUnit.Hour] = Math.Abs(boundary.Hours); + return timeUnitMaxValues; + } + timeUnitMaxValues[TimeSpanUnit.Hour] = 0; + + if (boundary.Minutes != 0) + { + timeUnitMaxValues[TimeSpanUnit.Minute] = Math.Abs(boundary.Minutes); + return timeUnitMaxValues; + } + timeUnitMaxValues[TimeSpanUnit.Minute] = 0; + + if (boundary.Seconds != 0) + { + timeUnitMaxValues[TimeSpanUnit.Second] = Math.Abs(boundary.Seconds); + return timeUnitMaxValues; + } + timeUnitMaxValues[TimeSpanUnit.Second] = 0; + + if (boundary.Milliseconds != 0) + { + timeUnitMaxValues[TimeSpanUnit.Millisecond] = Math.Abs(boundary.Milliseconds); + return timeUnitMaxValues; + } + timeUnitMaxValues[TimeSpanUnit.Millisecond] = 0; + +#if NET7_0_OR_GREATER + if (boundary.Microseconds != 0) + { + timeUnitMaxValues[TimeSpanUnit.Microsecond] = Math.Abs(boundary.Microseconds); + return timeUnitMaxValues; + } + timeUnitMaxValues[TimeSpanUnit.Microsecond] = 0; +#endif + + return timeUnitMaxValues; + } +#endregion + + #region Internal: panel value changes + private Task UpdateSign(bool isNegative) + { + if (isNegative && UnconfirmedValue < TimeSpan.Zero + || !isNegative && UnconfirmedValue > TimeSpan.Zero) + { + return Task.CompletedTask; + } + + if (UnconfirmedValue == TimeSpan.Zero) + { + _isUnconfirmedValueNegative = isNegative; + return Task.CompletedTask; + } + + return UpdateValueFromPanelFields(UnconfirmedValue.Negate()); + } + + private Task UpdateDays(int days) + { + var newValue = UnconfirmedValue + - TimeSpan.FromDays(UnconfirmedValue.Days) + + TimeSpan.FromDays(days * UnconformedValueSign); + + return UpdateValueFromPanelFields(newValue); + } + private Task UpdateHours(int hours) + { + var newValue = UnconfirmedValue + - TimeSpan.FromHours(UnconfirmedValue.Hours) + + TimeSpan.FromHours(hours * UnconformedValueSign); + + return UpdateValueFromPanelFields(newValue); + } + private Task UpdateMinutes(int minutes) + { + var newValue = UnconfirmedValue + - TimeSpan.FromMinutes(UnconfirmedValue.Minutes) + + TimeSpan.FromMinutes(minutes * UnconformedValueSign); + + return UpdateValueFromPanelFields(newValue); + } + private Task UpdateSeconds(int seconds) + { + var newValue = UnconfirmedValue + - TimeSpan.FromSeconds(UnconfirmedValue.Seconds) + + TimeSpan.FromSeconds(seconds * UnconformedValueSign); + + return UpdateValueFromPanelFields(newValue); + } + private Task UpdateMilliseconds(int milliseconds) + { + var newValue = UnconfirmedValue + - TimeSpan.FromMilliseconds(UnconfirmedValue.Milliseconds) + + TimeSpan.FromMilliseconds(milliseconds * UnconformedValueSign); + + return UpdateValueFromPanelFields(newValue); + } +#if NET7_0_OR_GREATER + private Task UpdateMicroseconds(int microseconds) + { + var newValue = UnconfirmedValue + - TimeSpan.FromMicroseconds(UnconfirmedValue.Microseconds) + + TimeSpan.FromMicroseconds(microseconds); + + return UpdateValueFromPanelFields(newValue * UnconformedValueSign); + } +#endif + + private Task UpdateValueFromPanelFields(TimeSpan newValue) + { + UnconfirmedValue = newValue; + + if (ShowConfirmationButton) + { + return UnconfirmedValueChanged.InvokeAsync(newValue); + } + + ConfirmedValue = newValue; + return OnChange(); + } + + private async Task ConfirmValue() + { + ConfirmedValue = UnconfirmedValue; + await OnChange(); + await ClosePopup(); + await FocusAsync(); + } + #endregion + + #region Internal: styles + private string ComponentStyle => $"{(Inline ? "overflow:auto;" : "")}{Style ?? ""}"; + + /// + protected override string GetComponentCssClass() + => ClassList.Create() + .Add("rz-timespanpicker-inline", Inline) + .Add(FieldIdentifier, EditContext) + .ToString(); + #endregion + } +} diff --git a/Radzen.Blazor/Rendering/Popup.razor b/Radzen.Blazor/Rendering/Popup.razor index 6b370ce1a72..d4fc4234af2 100644 --- a/Radzen.Blazor/Rendering/Popup.razor +++ b/Radzen.Blazor/Rendering/Popup.razor @@ -1,14 +1,12 @@ @inherits RadzenComponent @using Microsoft.JSInterop
- @if (open || !Lazy) + @if (IsOpen || !Lazy) { @ChildContent }
@code { - bool open; - [Parameter] public bool Lazy { get; set; } @@ -30,12 +28,14 @@ [Parameter] public EventCallback Close { get; set; } + public bool IsOpen { get; private set; } + public async Task ToggleAsync(ElementReference target, bool disableSmartPosition = false) { - open = !open; + IsOpen = !IsOpen; this.target = target; - if (open) + if (IsOpen) { await Open.InvokeAsync(null); await JSRuntime.InvokeVoidAsync("Radzen.openPopup", target, GetId(), false, null, null, null, Reference, nameof(OnClose), true, AutoFocusFirstElement, disableSmartPosition); @@ -48,7 +48,7 @@ public async Task CloseAsync(ElementReference target) { - open = false; + IsOpen = false; this.target = target; await CloseAsync(); @@ -59,7 +59,7 @@ [JSInvokable] public async Task OnClose() { - open = false; + IsOpen = false; await Close.InvokeAsync(null); } diff --git a/Radzen.Blazor/Rendering/PopupOrInline.razor b/Radzen.Blazor/Rendering/PopupOrInline.razor new file mode 100644 index 00000000000..e77edf43126 --- /dev/null +++ b/Radzen.Blazor/Rendering/PopupOrInline.razor @@ -0,0 +1,39 @@ +@inherits RadzenComponent + +@if (Inline) +{ + @ChildContent +} +else { + + @ChildContent + +} + + +@code { + [Parameter] + public RenderFragment ChildContent { get; set; } + + [Parameter] + public bool Inline { get; set; } + + [Parameter] + public bool PopupLazy { get; set; } + + [Parameter] + public bool PopupAutoFocusFirstElement { get; set; } = true; + + [Parameter] + public bool PopupPreventDefault { get; set; } + + [Parameter] + public EventCallback PopupOpen { get; set; } + + [Parameter] + public EventCallback PopupClose { get; set; } + + public Popup Popup { get; private set; } +} diff --git a/Radzen.Blazor/themes/_components.scss b/Radzen.Blazor/themes/_components.scss index 49ee151c907..01ba78540d8 100644 --- a/Radzen.Blazor/themes/_components.scss +++ b/Radzen.Blazor/themes/_components.scss @@ -75,4 +75,5 @@ @import 'components/blazor/timeline'; @import 'components/blazor/picklist'; @import 'components/blazor/carousel'; +@import 'components/blazor/timespanpicker'; @import 'css-variables'; \ No newline at end of file diff --git a/Radzen.Blazor/themes/_css-variables.scss b/Radzen.Blazor/themes/_css-variables.scss index ff9b1d467f6..07008501acd 100644 --- a/Radzen.Blazor/themes/_css-variables.scss +++ b/Radzen.Blazor/themes/_css-variables.scss @@ -1141,6 +1141,27 @@ --rz-timeline-line-width: #{$rz-timeline-line-width}; --rz-timeline-line-border-radius: #{$rz-timeline-line-border-radius}; + /* TimestampPicker */ + --rz-timespanpicker-line-height: #{$timespanpicker-line-height}; + --rz-timespanpicker-trigger-icon-width: #{$timespanpicker-trigger-icon-width}; + --rz-timespanpicker-trigger-icon-height: #{$timespanpicker-trigger-icon-height}; + --rz-timespanpicker-trigger-icon-color: #{$timespanpicker-trigger-icon-color}; + --rz-timespanpicker-trigger-icon-hover-color: #{$timespanpicker-trigger-icon-hover-color}; + --rz-timespanpicker-focus-outline: #{$timespanpicker-focus-outline}; + --rz-timespanpicker-focus-outline-offset: #{$timespanpicker-focus-outline-offset}; + --rz-timespanpicker-popup-border: #{$timespanpicker-popup-border}; + --rz-timespanpicker-popup-shadow: #{$timespanpicker-popup-shadow}; + --rz-timespanpicker-popup-margin: #{$timespanpicker-popup-margin}; + --rz-timespanpicker-popup-width: #{$timespanpicker-popup-width}; + --rz-timespanpicker-panel-background-color: #{$timespanpicker-panel-background-color}; + --rz-timespanpicker-panel-text-color: #{$timespanpicker-panel-text-color}; + --rz-timespanpicker-panel-padding-block: #{$timespanpicker-panel-padding-block}; + --rz-timespanpicker-panel-padding-inline: #{$timespanpicker-panel-padding-inline}; + --rz-timespanpicker-panel-gap: #{$timespanpicker-panel-gap}; + --rz-timespanpicker-panel-unit-color: #{$timespanpicker-panel-unit-color}; + --rz-timespanpicker-panel-unit-gap: #{$timespanpicker-panel-unit-gap}; + --rz-timespanpicker-panel-field-min-width: #{$timespanpicker-panel-field-min-width}; + /* Tooltip */ --rz-tooltip-background-color: #{$tooltip-background-color}; --rz-tooltip-color: #{$tooltip-color}; diff --git a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss new file mode 100644 index 00000000000..1dc0403061e --- /dev/null +++ b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss @@ -0,0 +1,176 @@ +$timespanpicker-line-height: 1.25rem !default; +$timespanpicker-trigger-icon-width: var(--rz-icon-size) !default; +$timespanpicker-trigger-icon-height: $timespanpicker-trigger-icon-width !default; +$timespanpicker-trigger-icon-color: var(--rz-input-value-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-input-value-color) !default; +$timespanpicker-focus-outline: var(--rz-outline-focus) !default; +$timespanpicker-focus-outline-offset: calc(-1 * var(--rz-outline-width)) !default; +$timespanpicker-popup-border: var(--rz-border-normal) !default; +$timespanpicker-popup-shadow: 0 6px 14px 0 rgba(0, 0, 0, 0.06) !default; +$timespanpicker-popup-margin: 0 !default; +$timespanpicker-popup-width: fit-content !default; +$timespanpicker-panel-background-color: var(--rz-base-background-color) !default; +$timespanpicker-panel-text-color: var(--rz-text-color) !default; +$timespanpicker-panel-padding-block: 0.5rem !default; +$timespanpicker-panel-padding-inline: 0.5rem !default; +$timespanpicker-panel-gap: 0.5rem !default; +$timespanpicker-panel-unit-color: var(--rz-text-color) !default; +$timespanpicker-panel-unit-gap: 0 0.25rem !default; +$timespanpicker-panel-field-min-width: 4rem !default; + +.rz-timespanpicker { + display: inline-block; + position: relative; + + .rz-readonly { + cursor: pointer; + } + + > .rz-inputtext { + @extend %input; + width: 100%; + line-height: var(--rz-timespanpicker-line-height); + } + + &:has(.rz-timespanpicker-trigger) > .rz-inputtext { + padding-inline-end: calc(1rem + var(--rz-timespanpicker-trigger-icon-width)); + } + + &:has(.rz-dropdown-clear-icon) > .rz-inputtext { + padding-inline-end: calc(1rem + var(--rz-dropdown-trigger-icon-width)); + } + + &:has(.rz-timespanpicker-trigger):has(.rz-dropdown-clear-icon) > .rz-inputtext { + padding-inline-end: calc(var(--rz-dropdown-trigger-icon-width) + 1rem + var(--rz-timespanpicker-trigger-icon-width)); + } + + &:not(.rz-state-disabled) { + &:hover { + .rz-timespanpicker-trigger { + box-shadow: none; + color: var(--rz-timespanpicker-trigger-icon-hover-color); + } + } + } + + &.rz-state-disabled { + > .rz-inputtext { + color: var(--rz-input-disabled-color); + box-shadow: var(--rz-input-disabled-shadow); + background-color: var(--rz-input-disabled-background-color); + border: var(--rz-input-disabled-border); + opacity: var(--rz-input-disabled-opacity); + + &::placeholder { + color: var(--rz-input-disabled-placeholder-color); + } + } + } +} + +/* popup trigger button */ + +.rz-timespanpicker-trigger { + box-shadow: none; + position: absolute; + inset-block-start: 50%; + inset-inline-end: 0.625rem; + transform: translateY(-50%); + background-color: transparent; + padding: 0; + vertical-align: text-top; + + &.rz-state-disabled { + border: none; + box-shadow: none; + cursor: initial; + opacity: 1; + color: var(--rz-input-disabled-color); + } + + color: var(--rz-timespanpicker-trigger-icon-color); + width: var(--rz-timespanpicker-trigger-icon-width); + height: var(--rz-timespanpicker-trigger-icon-height); + font-size: var(--rz-timespanpicker-trigger-icon-height); + + &:not(.rz-state-disabled) { + &:hover { + &:not(:active), + &:active { + background-color: transparent; + } + } + + &:active { + box-shadow: none !important; + background-image: none !important; + } + } + + .rzi-timespan { + font-size: inherit; + vertical-align: top; + } + + .rzi-timespan:before { + content: 'clock_loader_40'; + } + + .rz-button-text { + display: none; + } +} + +/* panel */ + +.rz-timespanpicker-inline { + border: var(--rz-input-border); + background-color: var(--rz-timespanpicker-panel-background-color); +} + +.rz-timespanpicker-popup-container { + display: none; + box-sizing: content-box; + position: absolute; + width: var(--rz-timespanpicker-popup-width); + margin: var(--rz-timespanpicker-popup-margin); + box-shadow: var(--rz-timespanpicker-popup-shadow); + border: var(--rz-timespanpicker-popup-border); + border-radius: var(--rz-border-radius); + background-color: var(--rz-timespanpicker-panel-background-color); +} + +.rz-timespanpicker-panel { + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: center; + gap: var(--rz-timespanpicker-panel-gap); + padding-block: var(--rz-timespanpicker-panel-padding-block); + padding-inline: var(--rz-timespanpicker-panel-padding-inline); + color: var(--rz-timespanpicker-panel-text-color); +} + +.rz-timespanpicker-panel-fieldwithunit { + display: flex; + flex-flow: column; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + gap: var(--rz-timespanpicker-panel-unit-gap); + + .rz-unit { + color: var(--rz-timespanpicker-panel-unit-color); + } +} + +.rz-timespanpicker-days, +.rz-timespanpicker-hours, +.rz-timespanpicker-minutes, +.rz-timespanpicker-seconds, +.rz-timespanpicker-milliseconds, +.rz-timespanpicker-microseconds { + background-color: var(--rz-timespanpicker-panel-background-color); + width: var(--rz-timespanpicker-panel-field-min-width); + min-width: var(--rz-timespanpicker-panel-field-min-width); +} diff --git a/Radzen.Blazor/themes/dark-base.scss b/Radzen.Blazor/themes/dark-base.scss index b633b02db15..b409036356a 100644 --- a/Radzen.Blazor/themes/dark-base.scss +++ b/Radzen.Blazor/themes/dark-base.scss @@ -340,6 +340,9 @@ $datepicker-header-background-color: var(--rz-base-800) !default; $datepicker-calendar-border: var(--rz-border-base-700) !default; $timepicker-background-color: var(--rz-base-900) !default; +// TimeSpanPicker +$timespanpicker-panel-background-color: var(--rz-base-800) !default; + // Upload $upload-button-bar-background-color: var(--rz-base-700) !default; $upload-files-remove-background-color: var(--rz-base-700) !default; diff --git a/Radzen.Blazor/themes/dark.scss b/Radzen.Blazor/themes/dark.scss index ac59bb0b019..a08674b2df1 100644 --- a/Radzen.Blazor/themes/dark.scss +++ b/Radzen.Blazor/themes/dark.scss @@ -340,6 +340,9 @@ $datepicker-header-background-color: var(--rz-base-800) !default; $datepicker-calendar-border: var(--rz-border-base-700) !default; $timepicker-background-color: var(--rz-base-900) !default; +// TimeSpanPicker +$timespanpicker-panel-background-color: var(--rz-base-800) !default; + // Upload $upload-button-bar-background-color: var(--rz-base-700) !default; $upload-files-remove-background-color: var(--rz-base-700) !default; diff --git a/Radzen.Blazor/themes/humanistic-dark-base.scss b/Radzen.Blazor/themes/humanistic-dark-base.scss index da55c148998..4af34065ab6 100644 --- a/Radzen.Blazor/themes/humanistic-dark-base.scss +++ b/Radzen.Blazor/themes/humanistic-dark-base.scss @@ -346,6 +346,9 @@ $datepicker-header-background-color: var(--rz-base-800) !default; $datepicker-calendar-border: var(--rz-border-base-700) !default; $timepicker-background-color: var(--rz-base-900) !default; +// TimeSpanPicker +$timespanpicker-panel-background-color: var(--rz-base-800) !default; + // Upload $upload-button-bar-background-color: var(--rz-base-700) !default; $upload-files-remove-background-color: var(--rz-base-700) !default; diff --git a/Radzen.Blazor/themes/humanistic-dark.scss b/Radzen.Blazor/themes/humanistic-dark.scss index 754c3185bee..1885c57797c 100644 --- a/Radzen.Blazor/themes/humanistic-dark.scss +++ b/Radzen.Blazor/themes/humanistic-dark.scss @@ -345,6 +345,9 @@ $datepicker-header-background-color: var(--rz-base-800) !default; $datepicker-calendar-border: var(--rz-border-base-700) !default; $timepicker-background-color: var(--rz-base-900) !default; +// TimeSpanPicker +$timespanpicker-panel-background-color: var(--rz-base-800) !default; + // Upload $upload-button-bar-background-color: var(--rz-base-700) !default; $upload-files-remove-background-color: var(--rz-base-700) !default; diff --git a/Radzen.Blazor/themes/material-base.scss b/Radzen.Blazor/themes/material-base.scss index eb1af4b877f..2e55922da7d 100644 --- a/Radzen.Blazor/themes/material-base.scss +++ b/Radzen.Blazor/themes/material-base.scss @@ -952,6 +952,18 @@ $timepicker-border: var(--rz-border-base-300) !default; } } +// TimeSpanPicker +$timespanpicker-line-height: 1.5rem !default; +$timespanpicker-trigger-icon-color: var(--rz-text-secondary-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-popup-border: none !default; +$timespanpicker-popup-shadow: var(--rz-shadow-4) !default; +$timespanpicker-panel-padding-block: 0.75rem !default; +$timespanpicker-panel-padding-inline: 0.75rem !default; +$timespanpicker-panel-gap: 0.75rem !default; +$timespanpicker-panel-unit-gap: 0 0.5rem !default; +$timespanpicker-panel-field-min-width: 5rem !default; + // Numeric $numeric-button-background-color: transparent !default; $numeric-button-color: var(--rz-text-color) !default; diff --git a/Radzen.Blazor/themes/material.scss b/Radzen.Blazor/themes/material.scss index 081eea28b58..55bff987da2 100644 --- a/Radzen.Blazor/themes/material.scss +++ b/Radzen.Blazor/themes/material.scss @@ -953,6 +953,18 @@ $timepicker-border: var(--rz-border-base-300) !default; } } +// TimeSpanPicker +$timespanpicker-line-height: 1.5rem !default; +$timespanpicker-trigger-icon-color: var(--rz-text-secondary-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-popup-border: none !default; +$timespanpicker-popup-shadow: var(--rz-shadow-4) !default; +$timespanpicker-panel-padding-block: 0.75rem !default; +$timespanpicker-panel-padding-inline: 0.75rem !default; +$timespanpicker-panel-gap: 0.75rem !default; +$timespanpicker-panel-unit-gap: 0 0.5rem !default; +$timespanpicker-panel-field-min-width: 5rem !default; + // Numeric $numeric-button-background-color: transparent !default; $numeric-button-color: var(--rz-text-color) !default; diff --git a/Radzen.Blazor/themes/software-dark-base.scss b/Radzen.Blazor/themes/software-dark-base.scss index ad3fe67ae85..753749ee974 100644 --- a/Radzen.Blazor/themes/software-dark-base.scss +++ b/Radzen.Blazor/themes/software-dark-base.scss @@ -340,6 +340,9 @@ $datepicker-header-background-color: var(--rz-base-800) !default; $datepicker-calendar-border: var(--rz-border-base-700) !default; $timepicker-background-color: var(--rz-base-900) !default; +// TimeSpanPicker +$timespanpicker-panel-background-color: var(--rz-base-800) !default; + // Upload $upload-button-bar-background-color: var(--rz-base-700) !default; $upload-files-remove-background-color: var(--rz-base-700) !default; diff --git a/Radzen.Blazor/themes/software-dark.scss b/Radzen.Blazor/themes/software-dark.scss index 2669bb38c07..f084d1c0e82 100644 --- a/Radzen.Blazor/themes/software-dark.scss +++ b/Radzen.Blazor/themes/software-dark.scss @@ -339,6 +339,9 @@ $datepicker-header-background-color: var(--rz-base-800) !default; $datepicker-calendar-border: var(--rz-border-base-700) !default; $timepicker-background-color: var(--rz-base-900) !default; +// TimeSpanPicker +$timespanpicker-panel-background-color: var(--rz-base-800) !default; + // Upload $upload-button-bar-background-color: var(--rz-base-700) !default; $upload-files-remove-background-color: var(--rz-base-700) !default; diff --git a/Radzen.Blazor/themes/standard-base.scss b/Radzen.Blazor/themes/standard-base.scss index 15d0b4ae43a..9481998a6bb 100644 --- a/Radzen.Blazor/themes/standard-base.scss +++ b/Radzen.Blazor/themes/standard-base.scss @@ -399,6 +399,11 @@ $timepicker-button-background-color: var(--rz-primary) !default; $timepicker-button-color: var(--rz-on-primary) !default; $timepicker-button-border-radius: var(--rz-border-radius) !default; +// TimeSpanPicker +$timespanpicker-trigger-icon-color: var(--rz-text-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-popup-shadow: $rz-shadow-3 !default; + // Fieldset $fieldset-border: var(--rz-border-base-300) !default; $fieldset-border-radius: var(--rz-border-radius) !default; diff --git a/Radzen.Blazor/themes/standard-dark-base.scss b/Radzen.Blazor/themes/standard-dark-base.scss index aca6bf8fb1c..765cf4f11bf 100644 --- a/Radzen.Blazor/themes/standard-dark-base.scss +++ b/Radzen.Blazor/themes/standard-dark-base.scss @@ -587,6 +587,12 @@ $timepicker-button-background-color: var(--rz-primary) !default; $timepicker-button-color: var(--rz-on-primary) !default; $timepicker-button-border-radius: var(--rz-border-radius) !default; +// TimeSpanPicker +$timespanpicker-trigger-icon-color: var(--rz-text-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-panel-background-color: var(--rz-base-background-color) !default; +$timespanpicker-popup-shadow: $rz-shadow-3 !default; + // Fieldset $fieldset-border: var(--rz-border-base-700) !default; $fieldset-border-radius: var(--rz-border-radius) !default; diff --git a/Radzen.Blazor/themes/standard-dark.scss b/Radzen.Blazor/themes/standard-dark.scss index 63f8f72a814..de141485acc 100644 --- a/Radzen.Blazor/themes/standard-dark.scss +++ b/Radzen.Blazor/themes/standard-dark.scss @@ -586,6 +586,12 @@ $timepicker-button-background-color: var(--rz-primary) !default; $timepicker-button-color: var(--rz-on-primary) !default; $timepicker-button-border-radius: var(--rz-border-radius) !default; +// TimeSpanPicker +$timespanpicker-trigger-icon-color: var(--rz-text-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-panel-background-color: var(--rz-base-background-color) !default; +$timespanpicker-popup-shadow: $rz-shadow-3 !default; + // Fieldset $fieldset-border: var(--rz-border-base-700) !default; $fieldset-border-radius: var(--rz-border-radius) !default; diff --git a/Radzen.Blazor/themes/standard.scss b/Radzen.Blazor/themes/standard.scss index 213570b3a48..dc0d7eeecd8 100644 --- a/Radzen.Blazor/themes/standard.scss +++ b/Radzen.Blazor/themes/standard.scss @@ -401,6 +401,11 @@ $timepicker-button-background-color: var(--rz-primary) !default; $timepicker-button-color: var(--rz-on-primary) !default; $timepicker-button-border-radius: var(--rz-border-radius) !default; +// TimeSpanPicker +$timespanpicker-trigger-icon-color: var(--rz-text-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-popup-shadow: $rz-shadow-3 !default; + // Fieldset $fieldset-border: var(--rz-border-base-300) !default; $fieldset-border-radius: var(--rz-border-radius) !default; diff --git a/RadzenBlazorDemos.Host/Properties/launchSettings.json b/RadzenBlazorDemos.Host/Properties/launchSettings.json index 5b0bd98e948..4e541ad8d4a 100644 --- a/RadzenBlazorDemos.Host/Properties/launchSettings.json +++ b/RadzenBlazorDemos.Host/Properties/launchSettings.json @@ -1,12 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:24219", - "sslPort": 44398 - } - }, "profiles": { "IIS Express": { "commandName": "IISExpress", @@ -18,11 +10,19 @@ "RadzenBlazorDemos": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50309/", + "sslPort": 44309 } } -} +} \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor new file mode 100644 index 00000000000..8085f93ba95 --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor @@ -0,0 +1,8 @@ + + + + + +@code { + TimeSpan? value; +} \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor new file mode 100644 index 00000000000..4b82f219c62 --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor @@ -0,0 +1,18 @@ + + + + + + + +@code { + TimeSpan value; + + EventConsole console; + + void OnChange(TimeSpan newValue) + { + value = newValue; + console.Log($"Value changed to {newValue}"); + } +} \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor new file mode 100644 index 00000000000..15fe2c4883c --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor @@ -0,0 +1,50 @@ +@page "/timespanpicker" +@page "/docs/guides/components/timespanpicker.html" + + + TimeSpanPicker + + + Demonstration and configuration of the Radzen Blazor TimeSpanPicker component. + + + + Get and Set the value of TimeSpanPicker + + + As all Radzen Blazor input components the TimeSpanPicker has a Value property which gets and sets the value of the component. Use @@bind-Value to get the user input. + + + + + + + Get and Set the value of DatePicker using Value and Change event + + + Value property can be used to set the value of the component and Change event to get the user input. + + + + + + + + Keyboard Navigation + + + The following keys or key combinations provide a way for users to navigate and interact with Radzen Blazor TimeSpanPicker component. + + + + +@code { + public List shortcuts = new() + { + new KeyboardShortcut { Key = "Tab", Action = "Navigate to a TimeSpanPicker component." }, + new KeyboardShortcut { Key = "Enter", Action = "Open the popup." }, + new KeyboardShortcut { Key = "Tab on open popup", Action = "Navigate forward across available picker components." }, + new KeyboardShortcut { Key = "Shift + Tab on open popup", Action = "Navigate backward across available picker components." }, + new KeyboardShortcut { Key = "Esc in an opened popup", Action = "Close the popup." } + }; +} From 76ec70b0324b3a6edb1b031501d37a87966ad384 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:38:12 +0100 Subject: [PATCH 02/42] TimeSpanPicker: adjust material themes, handle long labels --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 174 +++++++++--------- .../components/blazor/_timespanpicker.scss | 15 +- Radzen.Blazor/themes/material-base.scss | 10 +- Radzen.Blazor/themes/material-dark-base.scss | 20 ++ Radzen.Blazor/themes/material-dark.scss | 20 ++ Radzen.Blazor/themes/material.scss | 10 +- 6 files changed, 155 insertions(+), 94 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 75983a235eb..1727e297daf 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -43,98 +43,100 @@ class="rz-timespanpicker-popup-container">
- - - - - - +
+ + + + + + - @if (_canBeEitherPositiveOrNegative is false) - { -
- @(_isUnconfirmedValueNegative ? NegativeValueText : PositiveValueText) -
- } - @if (FieldPrecision >= TimeSpanUnit.Day && TimeFieldsMaxValues[TimeSpanUnit.Day] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Hour && TimeFieldsMaxValues[TimeSpanUnit.Hour] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Minute && TimeFieldsMaxValues[TimeSpanUnit.Minute] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Second && TimeFieldsMaxValues[TimeSpanUnit.Second] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Millisecond && TimeFieldsMaxValues[TimeSpanUnit.Millisecond] > 0) - { -
- - -
- } - @{ - #if NET7_0_OR_GREATER - if (FieldPrecision >= TimeSpanUnit.Microsecond && TimeFieldsMaxValues[TimeSpanUnit.Microsecond] > 0) + @if (_canBeEitherPositiveOrNegative is false) + { +
+ @(_isUnconfirmedValueNegative ? NegativeValueText : PositiveValueText) +
+ } + @if (FieldPrecision >= TimeSpanUnit.Day && TimeFieldsMaxValues[TimeSpanUnit.Day] > 0) + { +
+ + +
+ } + @if (FieldPrecision >= TimeSpanUnit.Hour && TimeFieldsMaxValues[TimeSpanUnit.Hour] > 0) {
- - @HoursUnitText + + class="rz-timespanpicker-hours" + Change="@UpdateHours" />
} - #endif - } + @if (FieldPrecision >= TimeSpanUnit.Minute && TimeFieldsMaxValues[TimeSpanUnit.Minute] > 0) + { +
+ + +
+ } + @if (FieldPrecision >= TimeSpanUnit.Second && TimeFieldsMaxValues[TimeSpanUnit.Second] > 0) + { +
+ + +
+ } + @if (FieldPrecision >= TimeSpanUnit.Millisecond && TimeFieldsMaxValues[TimeSpanUnit.Millisecond] > 0) + { +
+ + +
+ } + @{ + #if NET7_0_OR_GREATER + if (FieldPrecision >= TimeSpanUnit.Microsecond && TimeFieldsMaxValues[TimeSpanUnit.Microsecond] > 0) + { +
+ + +
+ } + #endif + } +
@if (ShowConfirmationButton) { diff --git a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss index 1dc0403061e..bfce40e0d9a 100644 --- a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss +++ b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss @@ -15,7 +15,7 @@ $timespanpicker-panel-padding-block: 0.5rem !default; $timespanpicker-panel-padding-inline: 0.5rem !default; $timespanpicker-panel-gap: 0.5rem !default; $timespanpicker-panel-unit-color: var(--rz-text-color) !default; -$timespanpicker-panel-unit-gap: 0 0.25rem !default; +$timespanpicker-panel-unit-gap: 0 !default; $timespanpicker-panel-field-min-width: 4rem !default; .rz-timespanpicker { @@ -141,14 +141,16 @@ $timespanpicker-panel-field-min-width: 4rem !default; } .rz-timespanpicker-panel { + padding-block: var(--rz-timespanpicker-panel-padding-block); + padding-inline: var(--rz-timespanpicker-panel-padding-inline); + color: var(--rz-timespanpicker-panel-text-color); +} + +.rz-timespanpicker-panel, .rz-timespanpicker-panel-fieldcontainer { display: flex; - flex-direction: row; align-items: flex-end; justify-content: center; gap: var(--rz-timespanpicker-panel-gap); - padding-block: var(--rz-timespanpicker-panel-padding-block); - padding-inline: var(--rz-timespanpicker-panel-padding-inline); - color: var(--rz-timespanpicker-panel-text-color); } .rz-timespanpicker-panel-fieldwithunit { @@ -158,6 +160,7 @@ $timespanpicker-panel-field-min-width: 4rem !default; align-items: center; justify-content: flex-start; gap: var(--rz-timespanpicker-panel-unit-gap); + width: min-content; .rz-unit { color: var(--rz-timespanpicker-panel-unit-color); @@ -171,6 +174,6 @@ $timespanpicker-panel-field-min-width: 4rem !default; .rz-timespanpicker-milliseconds, .rz-timespanpicker-microseconds { background-color: var(--rz-timespanpicker-panel-background-color); - width: var(--rz-timespanpicker-panel-field-min-width); + width: fit-content; min-width: var(--rz-timespanpicker-panel-field-min-width); } diff --git a/Radzen.Blazor/themes/material-base.scss b/Radzen.Blazor/themes/material-base.scss index 2e55922da7d..adaac21d1e0 100644 --- a/Radzen.Blazor/themes/material-base.scss +++ b/Radzen.Blazor/themes/material-base.scss @@ -961,9 +961,17 @@ $timespanpicker-popup-shadow: var(--rz-shadow-4) !default; $timespanpicker-panel-padding-block: 0.75rem !default; $timespanpicker-panel-padding-inline: 0.75rem !default; $timespanpicker-panel-gap: 0.75rem !default; -$timespanpicker-panel-unit-gap: 0 0.5rem !default; +$timespanpicker-panel-unit-gap: 0.25rem !default; $timespanpicker-panel-field-min-width: 5rem !default; +.rz-timespanpicker-panel { + flex-direction: column; + + > .rz-button { + align-self: stretch; + } +} + // Numeric $numeric-button-background-color: transparent !default; $numeric-button-color: var(--rz-text-color) !default; diff --git a/Radzen.Blazor/themes/material-dark-base.scss b/Radzen.Blazor/themes/material-dark-base.scss index 3b02a1a74f6..38fdbb8a7e4 100644 --- a/Radzen.Blazor/themes/material-dark-base.scss +++ b/Radzen.Blazor/themes/material-dark-base.scss @@ -977,6 +977,26 @@ $timepicker-border: var(--rz-border-base-600) !default; } } +// TimeSpanPicker +$timespanpicker-line-height: 1.5rem !default; +$timespanpicker-trigger-icon-color: var(--rz-text-secondary-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-popup-border: none !default; +$timespanpicker-popup-shadow: var(--rz-shadow-4) !default; +$timespanpicker-panel-padding-block: 0.75rem !default; +$timespanpicker-panel-padding-inline: 0.75rem !default; +$timespanpicker-panel-gap: 0.75rem !default; +$timespanpicker-panel-unit-gap: 0.25rem !default; +$timespanpicker-panel-field-min-width: 5rem !default; + +.rz-timespanpicker-panel { + flex-direction: column; + + > .rz-button { + align-self: stretch; + } +} + // Numeric $numeric-button-background-color: transparent !default; $numeric-button-color: var(--rz-text-color) !default; diff --git a/Radzen.Blazor/themes/material-dark.scss b/Radzen.Blazor/themes/material-dark.scss index 85632ac7793..a6a9b82b87a 100644 --- a/Radzen.Blazor/themes/material-dark.scss +++ b/Radzen.Blazor/themes/material-dark.scss @@ -978,6 +978,26 @@ $timepicker-border: var(--rz-border-base-600) !default; } } +// TimeSpanPicker +$timespanpicker-line-height: 1.5rem !default; +$timespanpicker-trigger-icon-color: var(--rz-text-secondary-color) !default; +$timespanpicker-trigger-icon-hover-color: var(--rz-text-title-color) !default; +$timespanpicker-popup-border: none !default; +$timespanpicker-popup-shadow: var(--rz-shadow-4) !default; +$timespanpicker-panel-padding-block: 0.75rem !default; +$timespanpicker-panel-padding-inline: 0.75rem !default; +$timespanpicker-panel-gap: 0.75rem !default; +$timespanpicker-panel-unit-gap: 0.25rem !default; +$timespanpicker-panel-field-min-width: 5rem !default; + +.rz-timespanpicker-panel { + flex-direction: column; + + > .rz-button { + align-self: stretch; + } +} + // Numeric $numeric-button-background-color: transparent !default; $numeric-button-color: var(--rz-text-color) !default; diff --git a/Radzen.Blazor/themes/material.scss b/Radzen.Blazor/themes/material.scss index 55bff987da2..3c6e2d5b3ef 100644 --- a/Radzen.Blazor/themes/material.scss +++ b/Radzen.Blazor/themes/material.scss @@ -962,9 +962,17 @@ $timespanpicker-popup-shadow: var(--rz-shadow-4) !default; $timespanpicker-panel-padding-block: 0.75rem !default; $timespanpicker-panel-padding-inline: 0.75rem !default; $timespanpicker-panel-gap: 0.75rem !default; -$timespanpicker-panel-unit-gap: 0 0.5rem !default; +$timespanpicker-panel-unit-gap: 0.25rem !default; $timespanpicker-panel-field-min-width: 5rem !default; +.rz-timespanpicker-panel { + flex-direction: column; + + > .rz-button { + align-self: stretch; + } +} + // Numeric $numeric-button-background-color: transparent !default; $numeric-button-color: var(--rz-text-color) !default; From 1863bfc1e31bc3daf551f0d888433aa83cff6b0d Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:07:34 +0100 Subject: [PATCH 03/42] TimeSpanPicker: fix value set, move invoking Change events for consistency with other components, update demos --- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 11 +++++++---- .../Pages/TimeSpanPickerBindValue.razor | 2 +- .../Pages/TimeSpanPickerChangeEvent.razor | 11 +++-------- .../Pages/TimeSpanPickerFormat.razor | 8 ++++++++ .../Pages/TimeSpanPickerMinMax.razor | 8 ++++++++ .../Pages/TimeSpanPickerPage.razor | 18 ++++++++++++++++-- RadzenBlazorDemos/RadzenBlazorDemos.csproj | 3 +++ 7 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerMinMax.razor diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 514ad607639..6a112fdcaf7 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -34,7 +34,7 @@ public TValue Value get => _value; set { - if (EqualityComparer.Default.Equals(ConfirmedValue, _value)) + if (EqualityComparer.Default.Equals(value, _value)) { return; } @@ -506,12 +506,13 @@ public override void Dispose() private async Task OnChange() { await ValueChanged.InvokeAsync(Value); - await Change.InvokeAsync(ConfirmedValue); if (FieldIdentifier.FieldName != null) { EditContext?.NotifyFieldChanged(FieldIdentifier); } + + await Change.InvokeAsync(ConfirmedValue); } private void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e) @@ -567,13 +568,14 @@ private async Task Clear() ConfirmedValue = null; await ValueChanged.InvokeAsync(Value); - await Change.InvokeAsync(ConfirmedValue); if (FieldIdentifier.FieldName != null) { EditContext?.NotifyFieldChanged(FieldIdentifier); } + await Change.InvokeAsync(ConfirmedValue); + StateHasChanged(); } /// @@ -608,7 +610,6 @@ private async Task ParseTimeSpan() if (ConfirmedValue != newValue && (newValue != null || nullable)) { ConfirmedValue = newValue; - await ValueChanged.InvokeAsync(Value); await Change.InvokeAsync(ConfirmedValue); if (FieldIdentifier.FieldName != null) @@ -616,6 +617,8 @@ private async Task ParseTimeSpan() EditContext?.NotifyFieldChanged(FieldIdentifier); } + await ValueChanged.InvokeAsync(Value); + StateHasChanged(); } } diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor index 8085f93ba95..9d43df7ca31 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerBindValue.razor @@ -1,6 +1,6 @@  - + @code { diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor index 4b82f219c62..15d5e785b5d 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerChangeEvent.razor @@ -1,18 +1,13 @@  - + - - @code { - TimeSpan value; - - EventConsole console; + TimeSpan? value; - void OnChange(TimeSpan newValue) + void OnChange(TimeSpan? newValue) { value = newValue; - console.Log($"Value changed to {newValue}"); } } \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor new file mode 100644 index 00000000000..b75b823272c --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor @@ -0,0 +1,8 @@ + + + + + +@code { + TimeSpan? value = new TimeSpan(1, 12, 30, 0); +} \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerMinMax.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerMinMax.razor new file mode 100644 index 00000000000..e64a118cd3d --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerMinMax.razor @@ -0,0 +1,8 @@ + + + + + +@code { + TimeSpan? value; +} \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor index 15fe2c4883c..f75ca469e78 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor @@ -19,15 +19,29 @@ - Get and Set the value of DatePicker using Value and Change event + Get and Set the value of TimeSpanPicker using Value and Change event. - Value property can be used to set the value of the component and Change event to get the user input. + Value property can be used to set the value of the component and Change event to get the user input. + + Define time span format + + + + + + + Set Min and Max values + + + + + Keyboard Navigation diff --git a/RadzenBlazorDemos/RadzenBlazorDemos.csproj b/RadzenBlazorDemos/RadzenBlazorDemos.csproj index d29ec76f91c..ff9b64202e4 100644 --- a/RadzenBlazorDemos/RadzenBlazorDemos.csproj +++ b/RadzenBlazorDemos/RadzenBlazorDemos.csproj @@ -21,6 +21,9 @@ true + + true + true From a286789d5e1d6f0d442325a5952c274637f3d6c1 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:57:51 +0100 Subject: [PATCH 04/42] TimeSpanPicker: fix preventKeyPress, adjust styles --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 24 ++++++++++------- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 27 ++++++++++--------- Radzen.Blazor/themes/_css-variables.scss | 1 - .../components/blazor/_timespanpicker.scss | 12 ++++----- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 1727e297daf..f561ecf4ebe 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -39,7 +39,7 @@ } }
@@ -56,14 +56,18 @@ @if (_canBeEitherPositiveOrNegative is false) { -
- @(_isUnconfirmedValueNegative ? NegativeValueText : PositiveValueText) -
+ var signText = _isUnconfirmedValueNegative ? NegativeValueText : PositiveValueText; + if (string.IsNullOrWhiteSpace(signText) is false) + { +
+ @signText +
+ } } @if (FieldPrecision >= TimeSpanUnit.Day && TimeFieldsMaxValues[TimeSpanUnit.Day] > 0) {
- + = TimeSpanUnit.Hour && TimeFieldsMaxValues[TimeSpanUnit.Hour] > 0) {
- + = TimeSpanUnit.Minute && TimeFieldsMaxValues[TimeSpanUnit.Minute] > 0) {
- + = TimeSpanUnit.Second && TimeFieldsMaxValues[TimeSpanUnit.Second] > 0) {
- + = TimeSpanUnit.Millisecond && TimeFieldsMaxValues[TimeSpanUnit.Millisecond] > 0) {
- + = TimeSpanUnit.Microsecond && TimeFieldsMaxValues[TimeSpanUnit.Microsecond] > 0) {
- + /// Specifies the time span format in the input field. /// For more details, see the documentation of - /// standard - /// and custom + /// standard + /// and custom /// time span format strings. ///
[Parameter] @@ -188,7 +188,7 @@ public TValue Value /// /// true if the confirmation button is shown; otherwise, false. [Parameter] - public bool ShowConfirmationButton { get; set; } = true; + public bool ShowConfirmationButton { get; set; } = false; /// /// Specifies whether the time fields in the panel, except for the days field, are padded with leading zeros. @@ -672,21 +672,13 @@ private async Task PressKey(KeyboardEventArgs args) if (key == "Enter") { - _preventKeyPress = true; - await TogglePopup(); } else if (key == "Escape") { - _preventKeyPress = false; - await ClosePopup(); await FocusAsync(); } - else - { - _preventKeyPress = false; - } } #endregion @@ -704,12 +696,21 @@ private async Task PopupKeyDown(KeyboardEventArgs args) var key = args.Code ?? args.Key; if (key == "Escape") { - _preventKeyPress = false; - await ClosePopup(); await FocusAsync(); } } + + private void OnPopupOpen() + { + ResetUnconfirmedValue(); + _preventKeyPress = true; + } + private void OnPopupClose() + { + ResetUnconfirmedValue(); + _preventKeyPress = false; + } #endregion #region Internal: panel fields setup diff --git a/Radzen.Blazor/themes/_css-variables.scss b/Radzen.Blazor/themes/_css-variables.scss index 07008501acd..2cbcf160a50 100644 --- a/Radzen.Blazor/themes/_css-variables.scss +++ b/Radzen.Blazor/themes/_css-variables.scss @@ -1158,7 +1158,6 @@ --rz-timespanpicker-panel-padding-block: #{$timespanpicker-panel-padding-block}; --rz-timespanpicker-panel-padding-inline: #{$timespanpicker-panel-padding-inline}; --rz-timespanpicker-panel-gap: #{$timespanpicker-panel-gap}; - --rz-timespanpicker-panel-unit-color: #{$timespanpicker-panel-unit-color}; --rz-timespanpicker-panel-unit-gap: #{$timespanpicker-panel-unit-gap}; --rz-timespanpicker-panel-field-min-width: #{$timespanpicker-panel-field-min-width}; diff --git a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss index bfce40e0d9a..a490a62019b 100644 --- a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss +++ b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss @@ -14,10 +14,11 @@ $timespanpicker-panel-text-color: var(--rz-text-color) !default; $timespanpicker-panel-padding-block: 0.5rem !default; $timespanpicker-panel-padding-inline: 0.5rem !default; $timespanpicker-panel-gap: 0.5rem !default; -$timespanpicker-panel-unit-color: var(--rz-text-color) !default; $timespanpicker-panel-unit-gap: 0 !default; $timespanpicker-panel-field-min-width: 4rem !default; +/* input field */ + .rz-timespanpicker { display: inline-block; position: relative; @@ -66,6 +67,10 @@ $timespanpicker-panel-field-min-width: 4rem !default; } } } + + &:not(:has(.rz-timespanpicker-trigger)) .rz-dropdown-clear-icon { + inset-inline-end: 0.5rem; + } } /* popup trigger button */ @@ -158,13 +163,8 @@ $timespanpicker-panel-field-min-width: 4rem !default; flex-flow: column; flex-wrap: nowrap; align-items: center; - justify-content: flex-start; gap: var(--rz-timespanpicker-panel-unit-gap); width: min-content; - - .rz-unit { - color: var(--rz-timespanpicker-panel-unit-color); - } } .rz-timespanpicker-days, From ceb4e94a620e7a967c7ad583ee8d2558418c0865 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:58:33 +0100 Subject: [PATCH 05/42] TimeSpanPicker: adjust demos and add new --- .../Pages/TimeSpanPickerConfig.razor | 57 +++++++++++++++++++ .../Pages/TimeSpanPickerFormat.razor | 3 +- .../Pages/TimeSpanPickerInline.razor | 8 +++ .../Pages/TimeSpanPickerPage.razor | 28 ++++++++- .../Pages/TimeSpanPickerParseInput.razor | 56 ++++++++++++++++++ 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor create mode 100644 RadzenBlazorDemos/Pages/TimeSpanPickerParseInput.razor diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor new file mode 100644 index 00000000000..0ec59b22222 --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@code { + TimeSpan? value; + + bool allowClear = true; + bool allowInput = true; + bool readOnly = false; + bool disabled = false; + bool padTimeValues = false; + bool showPopupButton = false; + bool showConfirmationButton = false; + TimeSpanUnit fieldPrecision = TimeSpanUnit.Second; +} \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor index b75b823272c..89e53f4742f 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerFormat.razor @@ -1,8 +1,9 @@  - + @code { + string timeSpanFormat => (value < TimeSpan.Zero ? "'-'" : "") + "d'd 'h'h 'm'min 's's'"; TimeSpan? value = new TimeSpan(1, 12, 30, 0); } \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor new file mode 100644 index 00000000000..63c30a40928 --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor @@ -0,0 +1,8 @@ + + +
@value
+
+ +@code { + TimeSpan? value; +} \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor index f75ca469e78..211abcbab2b 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor @@ -35,13 +35,39 @@ - + Set Min and Max values + + Inline picker + + + + + + + Various configurations + + + + + + + Define custom input parsing + + + The parameter named ParseInput allows a fully custom input parsing method.
+ This way you can accept inputs like '1d 20h 15min' or '-120s' or support more than one input format.
+ If your method returns null, the default parser is be used instead. +
+ + + + Keyboard Navigation diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerParseInput.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerParseInput.razor new file mode 100644 index 00000000000..5417e176508 --- /dev/null +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerParseInput.razor @@ -0,0 +1,56 @@ +@using System.Text.RegularExpressions + + + + + +@code { + TimeSpan? value; + + string[] standardFormats = { "c", "g", "G" }; + Regex customTimeSpanRegex = new Regex(@"(?:(?-?\d+)\w?d)|(?:(?-?\d+)\w?h)|(?:(?-?\d+)\w?min)|(?:(?-?\d+)\w?s)"); + + public TimeSpan? ParseTimeSpan(string input) + { + foreach (var format in standardFormats) + { + if (TimeSpan.TryParseExact(input, format, null, System.Globalization.TimeSpanStyles.None, out var standardResult)) + { + return standardResult; + } + } + + var regexGroups = customTimeSpanRegex.Matches(input.Trim()) + .Where(x => x.Success) + .SelectMany(x => x.Groups.Cast()) + .Where(x => x.Success) + .ToArray(); + + if (regexGroups.Length == 0) + { + return null; + } + + var timeUnitToValue = new Dictionary() { + {"days", 0}, + {"hours", 0}, + {"minutes", 0}, + {"seconds", 0} + }; + + foreach (var timeUnitWithValue in timeUnitToValue) + { + var unit = timeUnitWithValue.Key; + var valueString = regexGroups.FirstOrDefault(x => x.Name == unit)?.Value ?? "0"; + + if (Int32.TryParse(valueString, out int value)) + { + timeUnitToValue[unit] = value; + } + } + + var result = new TimeSpan(timeUnitToValue["days"], timeUnitToValue["hours"], timeUnitToValue["minutes"], timeUnitToValue["seconds"]); + + return result; + } +} \ No newline at end of file From f2e1102e1059184fef8d75bb83f2861b40e9b999 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:32:28 +0100 Subject: [PATCH 06/42] PopupOrInline: handle Inline parameter change --- Radzen.Blazor/Rendering/PopupOrInline.razor | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Radzen.Blazor/Rendering/PopupOrInline.razor b/Radzen.Blazor/Rendering/PopupOrInline.razor index e77edf43126..1dafaf77597 100644 --- a/Radzen.Blazor/Rendering/PopupOrInline.razor +++ b/Radzen.Blazor/Rendering/PopupOrInline.razor @@ -6,8 +6,8 @@ } else { + Lazy="@PopupLazy" AutoFocusFirstElement="@PopupAutoFocusFirstElement" PreventDefault="@PopupPreventDefault" + Open="@PopupOpen" Close="@PopupClose"> @ChildContent } @@ -36,4 +36,14 @@ else { public EventCallback PopupClose { get; set; } public Popup Popup { get; private set; } + + public override async Task SetParametersAsync(ParameterView parameters) + { + if (Popup?.IsOpen is true && parameters.DidParameterChange(nameof(Inline), Inline) && parameters.GetValueOrDefault(nameof(Inline)) is true) + { + await Popup.CloseAsync(); + } + + await base.SetParametersAsync(parameters); + } } From 13efd9f4ffc70650059a254c41f23a86f812ace7 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:33:51 +0100 Subject: [PATCH 07/42] TimeSpanPicker: change icon and improve handling Disabled/ReadMe parameters in panel --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 41 ++++++++++--------- .../components/blazor/_timespanpicker.scss | 6 ++- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index f561ecf4ebe..33b00357940 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -44,24 +44,23 @@
- - - - - - - - @if (_canBeEitherPositiveOrNegative is false) + @if (_canBeEitherPositiveOrNegative && ReadOnly is false) { + + + + + + + } + else { var signText = _isUnconfirmedValueNegative ? NegativeValueText : PositiveValueText; if (string.IsNullOrWhiteSpace(signText) is false) { -
- @signText -
+
@signText
} } @if (FieldPrecision >= TimeSpanUnit.Day && TimeFieldsMaxValues[TimeSpanUnit.Day] > 0) @@ -119,7 +118,7 @@
@@ -133,7 +132,7 @@
@@ -142,11 +141,13 @@ } - @if (ShowConfirmationButton) + @if (ShowConfirmationButton && ReadOnly is false) { - + + @ConfirmationButtonLabel + } diff --git a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss index a490a62019b..f0b42ca6f1e 100644 --- a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss +++ b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss @@ -118,7 +118,7 @@ $timespanpicker-panel-field-min-width: 4rem !default; } .rzi-timespan:before { - content: 'clock_loader_40'; + content: 'timelapse'; } .rz-button-text { @@ -177,3 +177,7 @@ $timespanpicker-panel-field-min-width: 4rem !default; width: fit-content; min-width: var(--rz-timespanpicker-panel-field-min-width); } + +.rz-timespanpicker-sign { + line-height: var(--rz-input-height); +} \ No newline at end of file From 537970be7cf4116ac76199cf21716cc00c02a720 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:34:08 +0100 Subject: [PATCH 08/42] TimeSpanPicker: update demos --- .../Pages/TimeSpanPickerConfig.razor | 9 ++--- .../Pages/TimeSpanPickerInline.razor | 4 +-- .../Pages/TimeSpanPickerPage.razor | 34 +++++++++++-------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor index 0ec59b22222..6fd61e36f39 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerConfig.razor @@ -40,18 +40,19 @@ - + @code { - TimeSpan? value; + TimeSpan? value = new TimeSpan(1, 12, 30, 0); bool allowClear = true; bool allowInput = true; bool readOnly = false; bool disabled = false; + bool showPopupButton = true; + + TimeSpanUnit fieldPrecision = TimeSpanUnit.Second; bool padTimeValues = false; - bool showPopupButton = false; bool showConfirmationButton = false; - TimeSpanUnit fieldPrecision = TimeSpanUnit.Second; } \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor index 63c30a40928..2d2964ea8dd 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerInline.razor @@ -1,8 +1,8 @@  -
@value
+
Current value: @value
@code { - TimeSpan? value; + TimeSpan value = TimeSpan.Zero; } \ No newline at end of file diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor index 211abcbab2b..8393c5f6c6b 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor @@ -8,11 +8,11 @@ Demonstration and configuration of the Radzen Blazor TimeSpanPicker component.
- - Get and Set the value of TimeSpanPicker + + Bind the value of TimeSpanPicker - As all Radzen Blazor input components the TimeSpanPicker has a Value property which gets and sets the value of the component. Use @@bind-Value to get the user input. + As all Radzen Blazor input components, the TimeSpanPicker has a Value property which gets and sets the value of the component. Use @@bind-Value to get the user input. @@ -28,15 +28,12 @@ - - Define time span format - - - - - - Set Min and Max values + Min and Max values + + + In this example, you can only set a value between 0 and 12h.
+ Note that the + and - buttons are hidden because you cannot choose a negative value.
@@ -56,13 +53,20 @@ + + Time span format + + + + + - Define custom input parsing + Custom input parsing - The parameter named ParseInput allows a fully custom input parsing method.
- This way you can accept inputs like '1d 20h 15min' or '-120s' or support more than one input format.
- If your method returns null, the default parser is be used instead. + The ParseInput parameter allows you to use a custom input parsing method.
+ This way you can accept inputs like '30h 15min' or '-120s', or support more than one input format.
+ If your method returns null, the default parser is used instead.
From 033a178e2d1700889296680597ab361621d8e766 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:34:40 +0100 Subject: [PATCH 09/42] TimeSpanPicker: add to the example panel --- RadzenBlazorDemos/Services/ExampleService.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RadzenBlazorDemos/Services/ExampleService.cs b/RadzenBlazorDemos/Services/ExampleService.cs index 0a5981169b6..dc08ab91846 100644 --- a/RadzenBlazorDemos/Services/ExampleService.cs +++ b/RadzenBlazorDemos/Services/ExampleService.cs @@ -1549,6 +1549,15 @@ public class ExampleService Tags = new [] { "input", "form", "edit" } }, new Example + { + Name = "TimeSpanPicker", + Path = "timespanpicker", + New = true, + Description = "Demonstration and configuration of the Radzen Blazor TimeSpanPicker component.", + Icon = "", + Tags = new [] { "duration", "form", "edit" } + }, + new Example { Name = "Upload", Description = "Demonstration and configuration of the Radzen Blazor Upload component.", From e3af46bd7f5e4178877f7dc36946b16570e79cb3 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:36:11 +0100 Subject: [PATCH 10/42] DatePicker: add a tag and info about time picker --- RadzenBlazorDemos/Services/ExampleService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RadzenBlazorDemos/Services/ExampleService.cs b/RadzenBlazorDemos/Services/ExampleService.cs index dc08ab91846..295656f8980 100644 --- a/RadzenBlazorDemos/Services/ExampleService.cs +++ b/RadzenBlazorDemos/Services/ExampleService.cs @@ -1313,9 +1313,9 @@ public class ExampleService Name = "DatePicker", Path = "datepicker", Updated = true, - Description = "Demonstration and configuration of the Radzen Blazor Datepicker component with calendar mode.", + Description = "Demonstration and configuration of the Radzen Blazor Datepicker component with calendar mode. Time Picker.", Icon = "", - Tags = new [] { "calendar", "form", "edit" } + Tags = new [] { "calendar", "time", "form", "edit" } }, new Example { From 139b67d0d8bd8726634a4266ab2477046b1f9f5b Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:54:23 +0100 Subject: [PATCH 11/42] TimeSpanPicker: minor code cleanup, adjust styles, open popup on click if input not allowed --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 19 +++++++++---------- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 7 +++---- .../components/blazor/_timespanpicker.scss | 5 +++-- RadzenBlazorDemos/Services/ExampleService.cs | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 33b00357940..04e32a33707 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -14,20 +14,19 @@ return; } -
+
@if (!Inline) { @if (ShowPopupButton) { - } @@ -63,61 +63,61 @@ } @if (FieldPrecision >= TimeSpanUnit.Day && TimeFieldsMaxValues[TimeSpanUnit.Day] > 0) { -
+
} @if (FieldPrecision >= TimeSpanUnit.Hour && TimeFieldsMaxValues[TimeSpanUnit.Hour] > 0) { -
+
} @if (FieldPrecision >= TimeSpanUnit.Minute && TimeFieldsMaxValues[TimeSpanUnit.Minute] > 0) { -
+
} @if (FieldPrecision >= TimeSpanUnit.Second && TimeFieldsMaxValues[TimeSpanUnit.Second] > 0) { -
+
} @if (FieldPrecision >= TimeSpanUnit.Millisecond && TimeFieldsMaxValues[TimeSpanUnit.Millisecond] > 0) { -
+
} @@ -125,13 +125,13 @@ #if NET7_0_OR_GREATER if (FieldPrecision >= TimeSpanUnit.Microsecond && TimeFieldsMaxValues[TimeSpanUnit.Microsecond] > 0) { -
+
} diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 175c16d0391..4cf30f1c844 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -239,7 +239,7 @@ public TValue Value /// Specifies the label of the negative value button. ///
[Parameter] - public string NegativeButtonLabel { get; set; } = "-"; + public string NegativeButtonLabel { get; set; } = "−"; /// /// Specifies the text displayed before the fields in the panel when the value is positive and there's no value sign picker. @@ -251,7 +251,7 @@ public TValue Value /// Specifies the text displayed before the fields in the panel when the value is negative and there's no value sign picker. /// [Parameter] - public string NegativeValueText { get; set; } = "-"; + public string NegativeValueText { get; set; } = "−"; /// /// Specifies the text displayed next to the days field. @@ -898,7 +898,8 @@ private async Task ConfirmValue() protected override string GetComponentCssClass() => ClassList.Create("rz-timespanpicker") .Add("rz-timespanpicker-inline", Inline) - .Add("rz-state-disabled", Disabled) + .AddDisabled(Disabled) + .Add("rz-state-empty", !HasValue) .Add(FieldIdentifier, EditContext) .ToString(); @@ -907,6 +908,12 @@ private string GetInputClass() .Add(InputClass) .Add("rz-readonly", ReadOnly && !Disabled) .ToString(); + + private string GetTogglePopupButtonClass() + => ClassList.Create("rz-timespanpicker-trigger rz-button rz-button-icon-only") + .Add(TogglePopupButtonClass) + .Add("rz-state-disabled", Disabled) + .ToString(); #endregion } } diff --git a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss index 00c561f99ed..74d255140c9 100644 --- a/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss +++ b/Radzen.Blazor/themes/components/blazor/_timespanpicker.scss @@ -169,12 +169,7 @@ $timespanpicker-panel-field-min-width: 4rem !default; width: min-content; } -.rz-timespanpicker-days, -.rz-timespanpicker-hours, -.rz-timespanpicker-minutes, -.rz-timespanpicker-seconds, -.rz-timespanpicker-milliseconds, -.rz-timespanpicker-microseconds { +.rz-timespanpicker-unitvaluepicker { background-color: var(--rz-timespanpicker-panel-background-color); width: fit-content; min-width: var(--rz-timespanpicker-panel-field-min-width); From ec953b97fd51c9ba772fa5b9d3fdd75976683438 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:54:35 +0100 Subject: [PATCH 15/42] TimeSpanPicker: * read input value without using JS * fix Value and ConfirmedValue inconsistency --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 4 +-- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 39 +++++++++------------ 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 95a1ec88220..566d7df82b1 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -20,7 +20,7 @@ @if (ShowPopupButton) @@ -36,7 +36,7 @@ } } -
@@ -383,7 +383,7 @@ public IRadzenForm Form /// /// Gets the formatted value. /// - public string FormattedValue => HasValue ? string.Format(Culture, "{0:" + TimeSpanFormat + "}", Value) : ""; + public string FormattedValue => HasValue ? string.Format(Culture, "{0:" + TimeSpanFormat + "}", ConfirmedValue) : ""; /// /// Gets the field identifier. @@ -455,6 +455,14 @@ private void ResetUnconfirmedValue() #region Methods: component general + /// + protected override void OnInitialized() + { + // initial synchronization: necessary when T is not nullable and Value is default(T) + ConfirmedValue = (TimeSpan?)(object)Value; + base.OnInitialized(); + } + private bool _firstRender; /// protected override Task OnAfterRenderAsync(bool firstRender) @@ -582,17 +590,12 @@ private async Task Clear() StateHasChanged(); } - /// - /// Parses the time span. - /// - private async Task ParseTimeSpan() + + private async Task ParseTimeSpan(string inputValue) { TimeSpan? newValue; - var inputValue = await JSRuntime.InvokeAsync("Radzen.getInputValue", input); bool valid = TryParseInput(inputValue, out TimeSpan value); - var nullable = AllowClear || Nullable.GetUnderlyingType(typeof(TValue)) != null; - if (valid) { newValue = value; @@ -600,18 +603,10 @@ private async Task ParseTimeSpan() else { newValue = null; - - if (nullable) - { - await JSRuntime.InvokeAsync("Radzen.setInputValue", input, ""); - } - else - { - await JSRuntime.InvokeAsync("Radzen.setInputValue", input, FormattedValue); - } + await JSRuntime.InvokeAsync("Radzen.setInputValue", input, FormattedValue); } - if (ConfirmedValue != newValue && (newValue != null || nullable)) + if (ConfirmedValue != newValue && (newValue is not null || _isNullable)) { ConfirmedValue = newValue; await Change.InvokeAsync(ConfirmedValue); @@ -687,13 +682,13 @@ private async Task PressKey(KeyboardEventArgs args) #endregion #region Internal: popup general actions - private PopupOrInline popupHolder; + private PopupOrInline _popupHolder; private Task TogglePopup() - => Inline ? Task.CompletedTask : popupHolder.Popup?.ToggleAsync(Element) ?? Task.CompletedTask; + => Inline ? Task.CompletedTask : _popupHolder.Popup?.ToggleAsync(Element) ?? Task.CompletedTask; private Task ClosePopup() - => Inline ? Task.CompletedTask : popupHolder.Popup?.CloseAsync(Element) ?? Task.CompletedTask; + => Inline ? Task.CompletedTask : _popupHolder.Popup?.CloseAsync(Element) ?? Task.CompletedTask; private async Task PopupKeyDown(KeyboardEventArgs args) { From ef948d311a4894a7590feadfb069ec49d3164fae Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:09:22 +0100 Subject: [PATCH 16/42] TimeSpanPicker: always refresh the displayed value on input change --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 2 +- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 566d7df82b1..bf3d32b7982 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -20,7 +20,7 @@ @if (ShowPopupButton) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index b49eeafbb58..d0ead9e6f79 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -591,20 +591,11 @@ private async Task Clear() StateHasChanged(); } - private async Task ParseTimeSpan(string inputValue) + private async Task SetValueFromInput(string inputValue) { - TimeSpan? newValue; bool valid = TryParseInput(inputValue, out TimeSpan value); - if (valid) - { - newValue = value; - } - else - { - newValue = null; - await JSRuntime.InvokeAsync("Radzen.setInputValue", input, FormattedValue); - } + TimeSpan? newValue = valid ? value : null; if (ConfirmedValue != newValue && (newValue is not null || _isNullable)) { @@ -620,6 +611,10 @@ private async Task ParseTimeSpan(string inputValue) StateHasChanged(); } + else + { + await JSRuntime.InvokeAsync("Radzen.setInputValue", input, FormattedValue); + } } private bool TryParseInput(string inputValue, out TimeSpan value) From 7833c51908e0524f2251c56c5a55690bb619d5ed Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:01:02 +0100 Subject: [PATCH 17/42] TimeSpanPicker: add some tests (not finished yet) --- Radzen.Blazor.Tests/TimeSpanPickerTests.cs | 294 +++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 Radzen.Blazor.Tests/TimeSpanPickerTests.cs diff --git a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs new file mode 100644 index 00000000000..8f8dba2b517 --- /dev/null +++ b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs @@ -0,0 +1,294 @@ +using AngleSharp.Dom; +using Bunit; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Xunit; + +namespace Radzen.Blazor.Tests +{ + public class TimeSpanPickerTests + { + [Fact] + public void TimeSpanPicker_Renders_CssClass() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + Assert.Contains(@$"rz-timespanpicker", component.Markup); + } + + [Fact] + public void TimeSpanPicker_Renders_EmptyCssClass_WhenValueIsEmpty() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, null)); + + Assert.Contains(@$"rz-state-empty", component.Markup); + } + + [Fact] + public void TimeSpanPicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var value = TimeSpan.FromHours(1); + component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, value)); + + Assert.DoesNotContain(@$"rz-state-empty", component.Markup); + } + + [Fact] + public void TimeSpanPicker_Respects_MinParameter_OnInput() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var minValue = TimeSpan.FromMinutes(-15); + var initialValue = TimeSpan.Zero; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Min, minValue); + parameters.Add(p => p.Value, initialValue); + }); + + var valueToSet = TimeSpan.FromHours(-1); + + component.Find(".rz-inputtext").Change(valueToSet); + + Assert.Equal(minValue, component.Instance.Value); + } + + [Fact] + public void TimeSpanPicker_Respects_MaxParameter_OnInput() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var maxValue = TimeSpan.FromMinutes(15); + var initialValue = TimeSpan.Zero; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Max, maxValue); + parameters.Add(p => p.Value, initialValue); + }); + + var valueToSet = TimeSpan.FromHours(1); + + component.Find(".rz-inputtext").Change(valueToSet); + + Assert.Equal(maxValue, component.Instance.Value); + } + + [Fact] + public void TimeSpanPicker_Renders_UnmatchedParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + component.SetParametersAndRender(parameters => parameters.AddUnmatched("autofocus", "")); + + Assert.Contains(@$"autofocus", component.Markup); + } + + [Fact] + public void TimeSpanPicker_Renders_AllowClearParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.AllowClear, true); + parameters.Add(p => p.Value, TimeSpan.FromDays(1)); + }); + + Assert.Contains(@$">(); + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.AllowInput, false); + }); + + var inputFieldMarkup = component.Find(".rz-inputtext").ToMarkup(); + Assert.Contains("readonly", inputFieldMarkup); + } + + [Fact] + public void TimeSpanPicker_Renders_DisabledParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Disabled, true); + }); + + var inputFieldMarkup = component.Find(".rz-inputtext").ToMarkup(); + Assert.Contains("disabled", inputFieldMarkup); + Assert.Contains("rz-state-disabled", component.Markup); + } + + [Fact] + public void TimeSpanPicker_Renders_ReadOnlyParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.ReadOnly, true); + }); + + var inputFieldMarkup = component.Find(".rz-inputtext").ToMarkup(); + Assert.Contains("readonly", inputFieldMarkup); + Assert.Contains("rz-readonly", component.Markup); + } + + [Fact] + public void TimeSpanPicker_Renders_TimeSpanFormatParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var format = "d'd 'h'h 'm'min 's's'"; + var value = new TimeSpan(1, 6, 30, 15); + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.TimeSpanFormat, format); + parameters.Add(p => p.Value, value); + }); + + var formattedValue = value.ToString(format); + + Assert.Contains(@$"value=""{formattedValue}""", component.Markup); + } + + [Fact] + public void TimeSpanPicker_Parses_Input_Using_TimeSpanFormat() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + var format = "h'-'m'-'s"; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.TimeSpanFormat, format); + }); + + var expectedValue = new TimeSpan(15, 5, 30); + var input = expectedValue.ToString(format); + + var inputElement = component.Find(".rz-inputtext"); + inputElement.Change(input); + + Assert.Equal(expectedValue, component.Instance.Value); + } + + [Theory] + [InlineData( + TimeSpanUnit.Day, + new string[] { "rz-timespanpicker-days" }, + new string[] { "rz-timespanpicker-hours", "rz-timespanpicker-minutes", "rz-timespanpicker-seconds", "rz-timespanpicker-milliseconds", "rz-timespanpicker-microseconds" }) + ] + [InlineData( + TimeSpanUnit.Minute, + new string[] { "rz-timespanpicker-days", "rz-timespanpicker-hours", "rz-timespanpicker-minutes" }, + new string[] { "rz-timespanpicker-seconds", "rz-timespanpicker-milliseconds", "rz-timespanpicker-microseconds" }) + ] + [InlineData( + TimeSpanUnit.Microsecond, + new string[] { "rz-timespanpicker-days", "rz-timespanpicker-hours", "rz-timespanpicker-minutes", "rz-timespanpicker-seconds", "rz-timespanpicker-milliseconds", "rz-timespanpicker-microseconds" }, + new string[0]) + ] + public void TimeSpanPicker_Renders_FieldPrecisionParameter(TimeSpanUnit precision, string[] elementsToRender, string[] elementsNotToRender) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.FieldPrecision, precision); + }); + + foreach (var element in elementsToRender) + { + Assert.Contains(element, component.Markup); + } + foreach (var element in elementsNotToRender) + { + Assert.DoesNotContain(element, component.Markup); + } + } + + + + + [Fact] + public void TimeSpanPicker_Renders_TabIndexParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var value = 1; + + component.SetParametersAndRender(parameters => parameters.Add(p => p.TabIndex, value)); + + Assert.Contains(@$"tabindex=""{value}""", component.Markup); + } + } +} From f79b736da01703d84ecac7c7b85974f47cc249ae Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:09:02 +0100 Subject: [PATCH 18/42] Numeric: fix culture-dependent test fail --- Radzen.Blazor.Tests/NumericTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Radzen.Blazor.Tests/NumericTests.cs b/Radzen.Blazor.Tests/NumericTests.cs index f84e85e9808..a2289b03f08 100644 --- a/Radzen.Blazor.Tests/NumericTests.cs +++ b/Radzen.Blazor.Tests/NumericTests.cs @@ -612,7 +612,7 @@ public void Numeric_Supports_IComparable() }); }); - component.Find("input").Change("13.53"); + component.Find("input").Change(13.53.ToString()); var maxDollars = new Dollars(2); Assert.Contains($" value=\"{maxDollars.ToString()}\"", component.Markup); From 6f1d99998247baf4ab6b1c07c07c409116d5e97d Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:55:14 +0100 Subject: [PATCH 19/42] TimeSpanPicker: reorganize parameters --- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 108 ++++++++++---------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index d0ead9e6f79..55cec0b53eb 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -63,13 +63,19 @@ public TValue Value public TimeSpan Max { get; set; } = TimeSpan.MaxValue; #endregion - #region Parameters: input config + #region Parameters: input field config /// /// Specifies additional custom attributes that will be rendered by the input. /// [Parameter] public IReadOnlyDictionary InputAttributes { get; set; } + /// + /// Specifies the input CSS classes, separated with spaces. + /// + [Parameter] + public string InputClass { get; set; } + /// /// Specifies whether the value can be cleared. /// Set to true by default if is nullable, false otherwise. @@ -100,114 +106,114 @@ public TValue Value public bool ReadOnly { get; set; } /// - /// Specifies the time span format in the input field. - /// For more details, see the documentation of - /// standard - /// and custom - /// time span format strings. + /// Specifies whether to display popup icon button in the input field. /// + /// true to display the button to open the popup; + /// false to hide the button to open the popup, clicking the input field opens the popup instead. [Parameter] - public string TimeSpanFormat { get; set; } + public bool ShowPopupButton { get; set; } = true; /// - /// Specifies the most precise time unit field in the picker panel. Set to by default. + /// Specifies the popup toggle button CSS classes, separated with spaces. /// [Parameter] - public TimeSpanUnit FieldPrecision { get; set; } = TimeSpanUnit.Second; + public string PopupButtonClass { get; set; } /// - /// Specifies the step of the days field in the picker panel. + /// Specifies the time span format in the input field. + /// For more details, see the documentation of + /// standard + /// and custom + /// time span format strings. /// [Parameter] - public string DaysStep { get; set; } + public string TimeSpanFormat { get; set; } /// - /// Specifies the step of the hours field in the picker panel. + /// Specifies the tab index. /// [Parameter] - public string HoursStep { get; set; } + public int TabIndex { get; set; } = 0; /// - /// Specifies the step of the minutes field in the picker panel. + /// Specifies custom function to parse the input. + /// If it's not defined or the function it returns null, a built-in parser us used instead. /// [Parameter] - public string MinutesStep { get; set; } + public Func ParseInput { get; set; } + #endregion + #region Parameters: panel config /// - /// Specifies the step of the seconds field in the picker panel. + /// Specifies the render mode of the popup. /// [Parameter] - public string SecondsStep { get; set; } + public PopupRenderMode PopupRenderMode { get; set; } = PopupRenderMode.Initial; /// - /// Specifies the step of the milliseconds field in the picker panel. + /// Specifies whether the component is inline or shows a popup. /// + /// true if inline; false if shows a popup. [Parameter] - public string MillisecondsStep { get; set; } + public bool Inline { get; set; } - #if NET7_0_OR_GREATER /// - /// Specifies the step of the microseconds field in the picker panel. + /// Specifies whether to display the confirmation button in the panel to accept changes. /// + /// true if the confirmation button is shown; otherwise, false. [Parameter] - public string MicrosecondsStep { get; set; } - #endif + public bool ShowConfirmationButton { get; set; } = false; /// - /// Specifies the tab index. + /// Specifies whether the time fields in the panel, except for the days field, are padded with leading zeros. /// + /// true if fields are padded; otherwise, false. [Parameter] - public int TabIndex { get; set; } = 0; + public bool PadTimeValues { get; set; } /// - /// Specifies custom function to parse the input. - /// If it's not defined or the function it returns null, a built-in parser us used instead. + /// Specifies the most precise time unit field in the picker panel. Set to by default. /// [Parameter] - public Func ParseInput { get; set; } - #endregion + public TimeSpanUnit FieldPrecision { get; set; } = TimeSpanUnit.Second; - #region Parameters: appearance /// - /// Specifies whether the component is inline or shows a popup. + /// Specifies the step of the days field in the picker panel. /// - /// true if inline; false if shows a popup. [Parameter] - public bool Inline { get; set; } + public string DaysStep { get; set; } /// - /// Specifies whether to display popup icon button in the input field. + /// Specifies the step of the hours field in the picker panel. /// - /// true to display the button to open the popup; - /// false to hide the button to open the popup, clicking the input field opens the popup instead. [Parameter] - public bool ShowPopupButton { get; set; } = true; + public string HoursStep { get; set; } /// - /// Specifies whether to display the confirmation button in the panel to accept changes. + /// Specifies the step of the minutes field in the picker panel. /// - /// true if the confirmation button is shown; otherwise, false. [Parameter] - public bool ShowConfirmationButton { get; set; } = false; + public string MinutesStep { get; set; } /// - /// Specifies whether the time fields in the panel, except for the days field, are padded with leading zeros. + /// Specifies the step of the seconds field in the picker panel. /// - /// true if fields are padded; otherwise, false. [Parameter] - public bool PadTimeValues { get; set; } + public string SecondsStep { get; set; } /// - /// Specifies the input CSS classes, separated with spaces. + /// Specifies the step of the milliseconds field in the picker panel. /// [Parameter] - public string InputClass { get; set; } + public string MillisecondsStep { get; set; } + #if NET7_0_OR_GREATER /// - /// Specifies the popup toggle button CSS classes, separated with spaces. + /// Specifies the step of the microseconds field in the picker panel. /// [Parameter] - public string TogglePopupButtonClass { get; set; } + public string MicrosecondsStep { get; set; } + #endif #endregion #region Parameters: labels @@ -299,12 +305,6 @@ public TValue Value [Parameter] public string Name { get; set; } - /// - /// Specifies the render mode of the popup. - /// - [Parameter] - public PopupRenderMode PopupRenderMode { get; set; } = PopupRenderMode.Initial; - /// /// Specifies the value expression used while creating the . /// @@ -901,7 +901,7 @@ private string GetInputClass() private string GetTogglePopupButtonClass() => ClassList.Create("rz-timespanpicker-trigger rz-button rz-button-icon-only") - .Add(TogglePopupButtonClass) + .Add(PopupButtonClass) .Add("rz-state-disabled", Disabled) .ToString(); #endregion From 73bafe62873545b6b7c3f8d621343355dd72bfb3 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sat, 8 Feb 2025 18:12:28 +0100 Subject: [PATCH 20/42] TimeSpanPicker tests: cover input parameters, add regions --- Radzen.Blazor.Tests/TimeSpanPickerTests.cs | 157 ++++++++++++++++++--- 1 file changed, 135 insertions(+), 22 deletions(-) diff --git a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs index 8f8dba2b517..9533b4e0eef 100644 --- a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs +++ b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs @@ -10,6 +10,7 @@ namespace Radzen.Blazor.Tests { public class TimeSpanPickerTests { + #region Component general [Fact] public void TimeSpanPicker_Renders_CssClass() { @@ -22,6 +23,24 @@ public void TimeSpanPicker_Renders_CssClass() Assert.Contains(@$"rz-timespanpicker", component.Markup); } + [Fact] + public void TimeSpanPicker_Renders_StyleParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var value = "width: 200px"; + + component.SetParametersAndRender(parameters => { + parameters.Add(p => p.Style, value); + }); + + Assert.Contains(@$"style=""{value}""", component.Markup); + } + [Fact] public void TimeSpanPicker_Renders_EmptyCssClass_WhenValueIsEmpty() { @@ -50,7 +69,9 @@ public void TimeSpanPicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty() Assert.DoesNotContain(@$"rz-state-empty", component.Markup); } + #endregion + #region Input field [Fact] public void TimeSpanPicker_Respects_MinParameter_OnInput() { @@ -108,9 +129,31 @@ public void TimeSpanPicker_Renders_UnmatchedParameter() var component = ctx.RenderComponent>(); - component.SetParametersAndRender(parameters => parameters.AddUnmatched("autofocus", "")); + component.SetParametersAndRender(parameters => + { + parameters.AddUnmatched("autofocus", ""); + }); - Assert.Contains(@$"autofocus", component.Markup); + Assert.Contains("autofocus", component.Markup); + } + + [Fact] + public void TimeSpanPicker_Renders_InputClassParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var inputClass = "test-class"; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.InputClass, inputClass); + }); + + var inputField = component.Find(".rz-inputtext"); + Assert.Contains(inputClass, inputField.ClassList); } [Fact] @@ -128,7 +171,8 @@ public void TimeSpanPicker_Renders_AllowClearParameter() parameters.Add(p => p.Value, TimeSpan.FromDays(1)); }); - Assert.Contains(@$">(); + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.ShowPopupButton, true); + }); + + var triggerButtons = component.FindAll(".rz-timespanpicker-trigger"); + Assert.Equal(1, triggerButtons.Count); + } + + [Fact] + public void TimeSpanPicker_Renders_PopupButtonClassParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var inputClass = "test-class"; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.PopupButtonClass, inputClass); + }); + + var popupButton = component.Find(".rz-timespanpicker-trigger"); + Assert.Contains(inputClass, popupButton.ClassList); + } + + [Fact] + public void TimeSpanPicker_Renders_TabIndexParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var value = 1; + + component.SetParametersAndRender(parameters => parameters.Add(p => p.TabIndex, value)); + + Assert.Contains(@$"tabindex=""{value}""", component.Markup); + } + [Fact] public void TimeSpanPicker_Renders_TimeSpanFormatParameter() { @@ -233,6 +330,40 @@ public void TimeSpanPicker_Parses_Input_Using_TimeSpanFormat() Assert.Equal(expectedValue, component.Instance.Value); } + [Fact] + public void TimeSpanPicker_Parses_Input_Using_ParseInput() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + Func customParseInput = (input) => + { + if (TimeSpan.TryParseExact(input, "'- 'h'h 'm'min 's's'", null, TimeSpanStyles.AssumeNegative, out var result)) + { + return result; + } + return null; + }; + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.ParseInput, customParseInput); + }); + + var inputElement = component.Find(".rz-inputtext"); + + string input = "- 15h 5min 30s"; + TimeSpan expectedValue = new TimeSpan(15, 5, 30).Negate(); + + inputElement.Change(input); + + Assert.Equal(expectedValue, component.Instance.Value); + } + #endregion + + #region Panel [Theory] [InlineData( TimeSpanUnit.Day, @@ -271,24 +402,6 @@ public void TimeSpanPicker_Renders_FieldPrecisionParameter(TimeSpanUnit precisio Assert.DoesNotContain(element, component.Markup); } } - - - - - [Fact] - public void TimeSpanPicker_Renders_TabIndexParameter() - { - using var ctx = new TestContext(); - ctx.JSInterop.Mode = JSRuntimeMode.Loose; - ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); - - var component = ctx.RenderComponent>(); - - var value = 1; - - component.SetParametersAndRender(parameters => parameters.Add(p => p.TabIndex, value)); - - Assert.Contains(@$"tabindex=""{value}""", component.Markup); - } + #endregion } } From 2e13b49aee558236462b0c215127d1f87bb2904e Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sat, 8 Feb 2025 20:44:12 +0100 Subject: [PATCH 21/42] TimeSpanPicker: tweak regions of input and panel parameters --- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 74 +++++++++++---------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 55cec0b53eb..26e5f5277f0 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -64,18 +64,6 @@ public TValue Value #endregion #region Parameters: input field config - /// - /// Specifies additional custom attributes that will be rendered by the input. - /// - [Parameter] - public IReadOnlyDictionary InputAttributes { get; set; } - - /// - /// Specifies the input CSS classes, separated with spaces. - /// - [Parameter] - public string InputClass { get; set; } - /// /// Specifies whether the value can be cleared. /// Set to true by default if is nullable, false otherwise. @@ -119,6 +107,30 @@ public TValue Value [Parameter] public string PopupButtonClass { get; set; } + /// + /// Specifies additional custom attributes that will be rendered by the input. + /// + [Parameter] + public IReadOnlyDictionary InputAttributes { get; set; } + + /// + /// Specifies the input CSS classes, separated with spaces. + /// + [Parameter] + public string InputClass { get; set; } + + /// + /// Specifies the name of the input field. + /// + [Parameter] + public string Name { get; set; } + + /// + /// Specifies the tab index. + /// + [Parameter] + public int TabIndex { get; set; } = 0; + /// /// Specifies the time span format in the input field. /// For more details, see the documentation of @@ -130,17 +142,25 @@ public TValue Value public string TimeSpanFormat { get; set; } /// - /// Specifies the tab index. + /// Specifies custom function to parse the input. + /// If it's not defined or the function it returns null, a built-in parser us used instead. /// [Parameter] - public int TabIndex { get; set; } = 0; + public Func ParseInput { get; set; } + #endregion + #region Parameters: input field labels /// - /// Specifies custom function to parse the input. - /// If it's not defined or the function it returns null, a built-in parser us used instead. + /// Specifies the input placeholder. /// [Parameter] - public Func ParseInput { get; set; } + public string Placeholder { get; set; } + + /// + /// Specifies the aria label for the toggle popup button. + /// + [Parameter] + public string TogglePopupAriaLabel { get; set; } = "Toggle popup"; #endregion #region Parameters: panel config @@ -216,19 +236,7 @@ public TValue Value #endif #endregion - #region Parameters: labels - /// - /// Specifies the input placeholder. - /// - [Parameter] - public string Placeholder { get; set; } - - /// - /// Specifies the aria label for the toggle popup button. - /// - [Parameter] - public string TogglePopupAriaLabel { get; set; } = "Toggle popup"; - + #region Parameters: panel labels /// /// Specifies the label of the confirmation button. /// @@ -299,12 +307,6 @@ public TValue Value #endregion #region Parameters: other config - /// - /// Specifies the name of the form component. - /// - [Parameter] - public string Name { get; set; } - /// /// Specifies the value expression used while creating the . /// From 550d611bd572d76c7f26194a4b28ff26abbcc242 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sat, 8 Feb 2025 21:01:29 +0100 Subject: [PATCH 22/42] TimeSpanPicker tests: reorganize regions, use more specific selectors --- Radzen.Blazor.Tests/TimeSpanPickerTests.cs | 238 ++++++++++++++------- 1 file changed, 164 insertions(+), 74 deletions(-) diff --git a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs index 9533b4e0eef..f9509d7487d 100644 --- a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs +++ b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs @@ -1,18 +1,22 @@ -using AngleSharp.Dom; +using AngleSharp.Css.Dom; +using AngleSharp.Dom; using Bunit; using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using Xunit; namespace Radzen.Blazor.Tests { public class TimeSpanPickerTests { - #region Component general + const string _pickerClassSelector = ".rz-timespanpicker"; + const string _inputFieldClassSelector = ".rz-inputtext"; + const string _panelClassSelector = ".rz-timespanpicker-panel"; + + #region Component general look [Fact] - public void TimeSpanPicker_Renders_CssClass() + public void TimeSpanPicker_Renders_StyleParameter() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; @@ -20,43 +24,54 @@ public void TimeSpanPicker_Renders_CssClass() var component = ctx.RenderComponent>(); - Assert.Contains(@$"rz-timespanpicker", component.Markup); + var style = "width: 200px"; + component.SetParametersAndRender(parameters => { + parameters.Add(p => p.Style, style); + }); + + var picker = component.Find(_pickerClassSelector); + Assert.Contains(style, picker.GetStyle().CssText); } [Fact] - public void TimeSpanPicker_Renders_StyleParameter() + public void TimeSpanPicker_DoesNotRender_Component_WhenVisibleParameterIsFalse() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); - var component = ctx.RenderComponent>(); - - var value = "width: 200px"; + var component = ctx.RenderComponent>(); component.SetParametersAndRender(parameters => { - parameters.Add(p => p.Style, value); + parameters.Add(p => p.Visible, false); }); - Assert.Contains(@$"style=""{value}""", component.Markup); + var pickerElements = component.FindAll(_pickerClassSelector); + Assert.Equal(0, pickerElements.Count); } [Fact] - public void TimeSpanPicker_Renders_EmptyCssClass_WhenValueIsEmpty() + public void TimeSpanPicker_Renders_UnmatchedParameter() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); - var component = ctx.RenderComponent>(); + var component = ctx.RenderComponent>(); - component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, null)); + var parameterName = "autofocus"; + var parameterValue = "true"; + component.SetParametersAndRender(parameters => + { + parameters.AddUnmatched(parameterName, parameterValue); + }); - Assert.Contains(@$"rz-state-empty", component.Markup); + var picker = component.Find(_pickerClassSelector); + Assert.Equal(parameterValue, picker.GetAttribute(parameterName)); } [Fact] - public void TimeSpanPicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty() + public void TimeSpanPicker_Renders_EmptyCssClass_WhenValueIsEmpty() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; @@ -64,40 +79,38 @@ public void TimeSpanPicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty() var component = ctx.RenderComponent>(); - var value = TimeSpan.FromHours(1); - component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, value)); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, null); + }); - Assert.DoesNotContain(@$"rz-state-empty", component.Markup); + var picker = component.Find(_pickerClassSelector); + Assert.Contains("rz-state-empty", picker.ClassList); } - #endregion - #region Input field [Fact] - public void TimeSpanPicker_Respects_MinParameter_OnInput() + public void TimeSpanPicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); - var component = ctx.RenderComponent>(); + var component = ctx.RenderComponent>(); - var minValue = TimeSpan.FromMinutes(-15); - var initialValue = TimeSpan.Zero; + var value = TimeSpan.FromHours(1); component.SetParametersAndRender(parameters => { - parameters.Add(p => p.Min, minValue); - parameters.Add(p => p.Value, initialValue); + parameters.Add(p => p.Value, value); }); - var valueToSet = TimeSpan.FromHours(-1); - - component.Find(".rz-inputtext").Change(valueToSet); - - Assert.Equal(minValue, component.Instance.Value); + var picker = component.Find(_pickerClassSelector); + Assert.DoesNotContain("rz-state-empty", picker.ClassList); } + #endregion + #region Input field look [Fact] - public void TimeSpanPicker_Respects_MaxParameter_OnInput() + public void TimeSpanPicker_Renders_InputAttributesParameter() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; @@ -105,23 +118,19 @@ public void TimeSpanPicker_Respects_MaxParameter_OnInput() var component = ctx.RenderComponent>(); - var maxValue = TimeSpan.FromMinutes(15); - var initialValue = TimeSpan.Zero; + var parameterName = "autofocus"; + var parameterValue = "true"; component.SetParametersAndRender(parameters => { - parameters.Add(p => p.Max, maxValue); - parameters.Add(p => p.Value, initialValue); + parameters.Add(p => p.InputAttributes, new Dictionary(){{ parameterName, parameterValue } }); }); - var valueToSet = TimeSpan.FromHours(1); - - component.Find(".rz-inputtext").Change(valueToSet); - - Assert.Equal(maxValue, component.Instance.Value); + var inputField = component.Find(_inputFieldClassSelector); + Assert.Equal(parameterValue, inputField.GetAttribute(parameterName)); } [Fact] - public void TimeSpanPicker_Renders_UnmatchedParameter() + public void TimeSpanPicker_Renders_InputClassParameter() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; @@ -129,16 +138,18 @@ public void TimeSpanPicker_Renders_UnmatchedParameter() var component = ctx.RenderComponent>(); + var inputClass = "test-class"; component.SetParametersAndRender(parameters => { - parameters.AddUnmatched("autofocus", ""); + parameters.Add(p => p.InputClass, inputClass); }); - Assert.Contains("autofocus", component.Markup); + var inputField = component.Find(_inputFieldClassSelector); + Assert.Contains(inputClass, inputField.ClassList); } [Fact] - public void TimeSpanPicker_Renders_InputClassParameter() + public void TimeSpanPicker_Renders_NameParameter() { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; @@ -146,14 +157,14 @@ public void TimeSpanPicker_Renders_InputClassParameter() var component = ctx.RenderComponent>(); - var inputClass = "test-class"; + var name = "timespanpicker-test"; component.SetParametersAndRender(parameters => { - parameters.Add(p => p.InputClass, inputClass); + parameters.Add(p => p.Name, name); }); - var inputField = component.Find(".rz-inputtext"); - Assert.Contains(inputClass, inputField.ClassList); + var inputField = component.Find(_inputFieldClassSelector); + Assert.Equal(name, inputField.GetAttribute("name")); } [Fact] @@ -189,8 +200,9 @@ public void TimeSpanPicker_Renders_AllowInputParameter_WhenFalse() parameters.Add(p => p.AllowInput, false); }); - var inputFieldMarkup = component.Find(".rz-inputtext").ToMarkup(); - Assert.Contains("readonly", inputFieldMarkup); + var inputField = component.Find(_inputFieldClassSelector); + + Assert.True(inputField.HasAttribute("readonly")); } [Fact] @@ -207,9 +219,11 @@ public void TimeSpanPicker_Renders_DisabledParameter() parameters.Add(p => p.Disabled, true); }); - var inputFieldMarkup = component.Find(".rz-inputtext").ToMarkup(); - Assert.Contains("disabled", inputFieldMarkup); - Assert.Contains("rz-state-disabled", component.Markup); + var picker = component.Find(_pickerClassSelector); + var inputField = component.Find(_inputFieldClassSelector); + + Assert.Contains("rz-state-disabled", picker.ClassList); + Assert.True(inputField.HasAttribute("disabled")); } [Fact] @@ -226,9 +240,10 @@ public void TimeSpanPicker_Renders_ReadOnlyParameter() parameters.Add(p => p.ReadOnly, true); }); - var inputFieldMarkup = component.Find(".rz-inputtext").ToMarkup(); - Assert.Contains("readonly", inputFieldMarkup); - Assert.Contains("rz-readonly", component.Markup); + var inputField = component.Find(_inputFieldClassSelector); + + Assert.Contains("rz-readonly", inputField.ClassList); + Assert.True(inputField.HasAttribute("readonly")); } [Fact] @@ -261,6 +276,7 @@ public void TimeSpanPicker_Renders_PopupButtonClassParameter() var inputClass = "test-class"; component.SetParametersAndRender(parameters => { + parameters.Add(p => p.ShowPopupButton, true); parameters.Add(p => p.PopupButtonClass, inputClass); }); @@ -277,11 +293,14 @@ public void TimeSpanPicker_Renders_TabIndexParameter() var component = ctx.RenderComponent>(); - var value = 1; - - component.SetParametersAndRender(parameters => parameters.Add(p => p.TabIndex, value)); + var tabIndex = 15; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.TabIndex, tabIndex); + }); - Assert.Contains(@$"tabindex=""{value}""", component.Markup); + var inputField = component.Find(_inputFieldClassSelector); + Assert.Equal(tabIndex.ToString(), inputField.GetAttribute("tabindex")); } [Fact] @@ -295,6 +314,7 @@ public void TimeSpanPicker_Renders_TimeSpanFormatParameter() var format = "d'd 'h'h 'm'min 's's'"; var value = new TimeSpan(1, 6, 30, 15); + var formattedValue = value.ToString(format); component.SetParametersAndRender(parameters => { @@ -302,9 +322,77 @@ public void TimeSpanPicker_Renders_TimeSpanFormatParameter() parameters.Add(p => p.Value, value); }); - var formattedValue = value.ToString(format); + var inputField = component.Find(_inputFieldClassSelector); + Assert.Equal(formattedValue, inputField.GetAttribute("value")); + } + + [Fact] + public void TimeSpanPicker_Renders_PlaceholderParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var placeholder = "placeholder test"; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Placeholder, placeholder); + }); - Assert.Contains(@$"value=""{formattedValue}""", component.Markup); + var inputField = component.Find(_inputFieldClassSelector); + Assert.Equal(placeholder, inputField.GetAttribute("placeholder")); + } + #endregion + + #region Input field behavior + [Fact] + public void TimeSpanPicker_Respects_MinParameter_OnInput() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var minValue = TimeSpan.FromMinutes(-15); + var initialValue = TimeSpan.Zero; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Min, minValue); + parameters.Add(p => p.Value, initialValue); + }); + + var valueToSet = TimeSpan.FromHours(-1); + + component.Find(_inputFieldClassSelector).Change(valueToSet); + + Assert.Equal(minValue, component.Instance.Value); + } + + [Fact] + public void TimeSpanPicker_Respects_MaxParameter_OnInput() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var maxValue = TimeSpan.FromMinutes(15); + var initialValue = TimeSpan.Zero; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Max, maxValue); + parameters.Add(p => p.Value, initialValue); + }); + + var valueToSet = TimeSpan.FromHours(1); + + component.Find(_inputFieldClassSelector).Change(valueToSet); + + Assert.Equal(maxValue, component.Instance.Value); } [Fact] @@ -324,7 +412,7 @@ public void TimeSpanPicker_Parses_Input_Using_TimeSpanFormat() var expectedValue = new TimeSpan(15, 5, 30); var input = expectedValue.ToString(format); - var inputElement = component.Find(".rz-inputtext"); + var inputElement = component.Find(_inputFieldClassSelector); inputElement.Change(input); Assert.Equal(expectedValue, component.Instance.Value); @@ -352,7 +440,7 @@ public void TimeSpanPicker_Parses_Input_Using_ParseInput() parameters.Add(p => p.ParseInput, customParseInput); }); - var inputElement = component.Find(".rz-inputtext"); + var inputElement = component.Find(_inputFieldClassSelector); string input = "- 15h 5min 30s"; TimeSpan expectedValue = new TimeSpan(15, 5, 30).Negate(); @@ -363,21 +451,21 @@ public void TimeSpanPicker_Parses_Input_Using_ParseInput() } #endregion - #region Panel + #region Panel look [Theory] [InlineData( TimeSpanUnit.Day, - new string[] { "rz-timespanpicker-days" }, - new string[] { "rz-timespanpicker-hours", "rz-timespanpicker-minutes", "rz-timespanpicker-seconds", "rz-timespanpicker-milliseconds", "rz-timespanpicker-microseconds" }) + new string[] { ".rz-timespanpicker-days" }, + new string[] { ".rz-timespanpicker-hours", ".rz-timespanpicker-minutes", ".rz-timespanpicker-seconds", ".rz-timespanpicker-milliseconds", ".rz-timespanpicker-microseconds" }) ] [InlineData( TimeSpanUnit.Minute, - new string[] { "rz-timespanpicker-days", "rz-timespanpicker-hours", "rz-timespanpicker-minutes" }, - new string[] { "rz-timespanpicker-seconds", "rz-timespanpicker-milliseconds", "rz-timespanpicker-microseconds" }) + new string[] { ".rz-timespanpicker-days", ".rz-timespanpicker-hours", ".rz-timespanpicker-minutes" }, + new string[] { ".rz-timespanpicker-seconds", ".rz-timespanpicker-milliseconds", ".rz-timespanpicker-microseconds" }) ] [InlineData( TimeSpanUnit.Microsecond, - new string[] { "rz-timespanpicker-days", "rz-timespanpicker-hours", "rz-timespanpicker-minutes", "rz-timespanpicker-seconds", "rz-timespanpicker-milliseconds", "rz-timespanpicker-microseconds" }, + new string[] { ".rz-timespanpicker-days", ".rz-timespanpicker-hours", ".rz-timespanpicker-minutes", ".rz-timespanpicker-seconds", ".rz-timespanpicker-milliseconds", ".rz-timespanpicker-microseconds" }, new string[0]) ] public void TimeSpanPicker_Renders_FieldPrecisionParameter(TimeSpanUnit precision, string[] elementsToRender, string[] elementsNotToRender) @@ -395,11 +483,13 @@ public void TimeSpanPicker_Renders_FieldPrecisionParameter(TimeSpanUnit precisio foreach (var element in elementsToRender) { - Assert.Contains(element, component.Markup); + var foundElements = component.FindAll(element); + Assert.Equal(1, foundElements.Count); } foreach (var element in elementsNotToRender) { - Assert.DoesNotContain(element, component.Markup); + var foundElements = component.FindAll(element); + Assert.Equal(0, foundElements.Count); } } #endregion From 979adfd8e0aab238fa567df067debcd10d37f2ff Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 9 Feb 2025 13:02:09 +0100 Subject: [PATCH 23/42] TimeSpanPicker: improve value change and popup toggle prevention --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 3 +- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 42 ++++++++++++++----- Radzen.Blazor/Rendering/PopupOrInline.razor | 9 +++- .../Pages/TimeSpanPickerPage.razor | 3 +- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index bf3d32b7982..9c3e881e3c8 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -36,7 +36,8 @@ } } -
- /// Indicates whether this instance is bound callback has delegate). - ///
- /// true if this instance is bound; otherwise, false. - public bool IsBound => ValueChanged.HasDelegate; + private bool PreventValueChange => Disabled || ReadOnly; + private bool PreventPopupToggle => Disabled || ReadOnly || Inline; /// /// Indicates whether this instance has a confirmed value. @@ -382,6 +379,12 @@ public IRadzenForm Form /// true if this instance has value; otherwise, false. public bool HasValue => ConfirmedValue.HasValue; + /// + /// Indicates whether this instance is bound callback has delegate). + /// + /// true if this instance is bound; otherwise, false. + public bool IsBound => ValueChanged.HasDelegate; + /// /// Gets the formatted value. /// @@ -541,8 +544,10 @@ private void ValidationStateChanged(object sender, ValidationStateChangedEventAr /// public async Task Close() { - if (Disabled || ReadOnly || Inline) + if (Inline) + { return; + } await ClosePopup(); @@ -575,7 +580,7 @@ public async ValueTask FocusAsync() private async Task Clear() { - if (Disabled || ReadOnly) + if (PreventValueChange) { return; } @@ -595,6 +600,11 @@ private async Task Clear() private async Task SetValueFromInput(string inputValue) { + if (PreventValueChange) + { + return; + } + bool valid = TryParseInput(inputValue, out TimeSpan value); TimeSpan? newValue = valid ? value : null; @@ -651,11 +661,13 @@ private bool TryParseInput(string inputValue, out TimeSpan value) #region Internal: input mouse and keyboard events private async Task ClickPopupButton() { - if (!Disabled && !ReadOnly && !Inline) + if (PreventPopupToggle) { - await TogglePopup(); - await FocusAsync(); + return; } + + await TogglePopup(); + await FocusAsync(); } private Task ClickInputField() @@ -664,6 +676,11 @@ private Task ClickInputField() private bool _preventKeyPress = false; private async Task PressKey(KeyboardEventArgs args) { + if (PreventPopupToggle) + { + return; + } + var key = args.Code ?? args.Key; if (key == "Enter") @@ -865,6 +882,11 @@ private Task UpdateMicroseconds(int microseconds) private Task UpdateValueFromPanelFields(TimeSpan newValue) { + if (PreventValueChange) + { + return Task.CompletedTask; + } + UnconfirmedValue = newValue; if (ShowConfirmationButton) diff --git a/Radzen.Blazor/Rendering/PopupOrInline.razor b/Radzen.Blazor/Rendering/PopupOrInline.razor index 1dafaf77597..6d512ec6a6f 100644 --- a/Radzen.Blazor/Rendering/PopupOrInline.razor +++ b/Radzen.Blazor/Rendering/PopupOrInline.razor @@ -1,10 +1,16 @@ @inherits RadzenComponent +@if (!Visible) +{ + return; +} + @if (Inline) { @ChildContent } -else { +else +{ @@ -12,7 +18,6 @@ else { } - @code { [Parameter] public RenderFragment ChildContent { get; set; } diff --git a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor index 8393c5f6c6b..4b6d5b939cf 100644 --- a/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor +++ b/RadzenBlazorDemos/Pages/TimeSpanPickerPage.razor @@ -65,8 +65,7 @@ The ParseInput parameter allows you to use a custom input parsing method.
- This way you can accept inputs like '30h 15min' or '-120s', or support more than one input format.
- If your method returns null, the default parser is used instead. + This way you can accept inputs like '30h 15min' or '-120s', or support more than one input format.
From 29074c1dc31dc4a65f50f65ef8ced47ad96513fa Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 9 Feb 2025 21:00:25 +0100 Subject: [PATCH 24/42] TimeSpanPicker: * use culture while parsing * use consistent culture in tests --- Radzen.Blazor.Tests/TimeSpanPickerTests.cs | 101 ++++++++++++++++++-- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 4 +- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs index f9509d7487d..dc81e5d46b0 100644 --- a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs +++ b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs @@ -11,8 +11,11 @@ namespace Radzen.Blazor.Tests public class TimeSpanPickerTests { const string _pickerClassSelector = ".rz-timespanpicker"; + const string _popupButtonClassSelector = ".rz-timespanpicker-trigger"; + const string _clearButtonClassSelector = ".rz-dropdown-clear-icon"; const string _inputFieldClassSelector = ".rz-inputtext"; const string _panelClassSelector = ".rz-timespanpicker-panel"; + static CultureInfo _cultureInfo = CultureInfo.InvariantCulture; #region Component general look [Fact] @@ -182,7 +185,7 @@ public void TimeSpanPicker_Renders_AllowClearParameter() parameters.Add(p => p.Value, TimeSpan.FromDays(1)); }); - var clearButtons = component.FindAll(".rz-dropdown-clear-icon"); + var clearButtons = component.FindAll(_clearButtonClassSelector); Assert.Equal(1, clearButtons.Count); } @@ -260,7 +263,7 @@ public void TimeSpanPicker_Renders_ShowPopupButtonParameter() parameters.Add(p => p.ShowPopupButton, true); }); - var triggerButtons = component.FindAll(".rz-timespanpicker-trigger"); + var triggerButtons = component.FindAll(_popupButtonClassSelector); Assert.Equal(1, triggerButtons.Count); } @@ -300,7 +303,7 @@ public void TimeSpanPicker_Renders_TabIndexParameter() }); var inputField = component.Find(_inputFieldClassSelector); - Assert.Equal(tabIndex.ToString(), inputField.GetAttribute("tabindex")); + Assert.Equal(tabIndex.ToString(_cultureInfo), inputField.GetAttribute("tabindex")); } [Fact] @@ -314,12 +317,13 @@ public void TimeSpanPicker_Renders_TimeSpanFormatParameter() var format = "d'd 'h'h 'm'min 's's'"; var value = new TimeSpan(1, 6, 30, 15); - var formattedValue = value.ToString(format); + var formattedValue = value.ToString(format, _cultureInfo); component.SetParametersAndRender(parameters => { parameters.Add(p => p.TimeSpanFormat, format); parameters.Add(p => p.Value, value); + parameters.Add(p => p.Culture, _cultureInfo); }); var inputField = component.Find(_inputFieldClassSelector); @@ -407,10 +411,11 @@ public void TimeSpanPicker_Parses_Input_Using_TimeSpanFormat() component.SetParametersAndRender(parameters => { parameters.Add(p => p.TimeSpanFormat, format); + parameters.Add(p => p.Culture, _cultureInfo); }); var expectedValue = new TimeSpan(15, 5, 30); - var input = expectedValue.ToString(format); + var input = expectedValue.ToString(format, _cultureInfo); var inputElement = component.Find(_inputFieldClassSelector); inputElement.Change(input); @@ -449,6 +454,88 @@ public void TimeSpanPicker_Parses_Input_Using_ParseInput() Assert.Equal(expectedValue, component.Instance.Value); } + + [Fact] + public void TimeSpanPicker_Raises_ValueChanged_OnInputChange() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + bool raised = false; + TimeSpan newValue = TimeSpan.Zero; + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; }); + parameters.Add(p => p.Culture, _cultureInfo); + }); + + TimeSpan inputValue = new TimeSpan(3, 15, 30); + var input = inputValue.ToString(null, _cultureInfo); + + var inputElement = component.Find(_inputFieldClassSelector); + + inputElement.Change(input); + + Assert.True(raised); + Assert.Equal(inputValue, newValue); + } + + [Fact] + public void TimeSpanPicker_Raises_Change_OnInputChange() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + bool raised = false; + TimeSpan? newValue = null; + + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Change, args => { raised = true; newValue = args; }); + parameters.Add(p => p.Culture, _cultureInfo); + }); + + TimeSpan inputValue = new TimeSpan(3, 15, 30); + var input = inputValue.ToString(null, _cultureInfo); + + var inputElement = component.Find(_inputFieldClassSelector); + + inputElement.Change(input); + + Assert.True(raised); + Assert.Equal(inputValue, newValue); + } + + // TimeSpanPicker_Opens_Popup_OnPopupButtonClick – I don't know how to test it if I can't check if an element is visible + + [Fact] + public void TimeSpanPicker_Raises_ChangeEvent_OnClearButtonClick() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + var raised = false; + TimeSpan? value = new TimeSpan(3, 15, 30); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.AllowClear, true); + parameters.Add(p => p.Value, value); + parameters.Add(p => p.Change, args => { raised = true; value = args; }); + }); + + var clearButton = component.Find(_clearButtonClassSelector); + clearButton.Click(); + + Assert.True(raised); + Assert.Null(value); + } #endregion #region Panel look @@ -483,12 +570,12 @@ public void TimeSpanPicker_Renders_FieldPrecisionParameter(TimeSpanUnit precisio foreach (var element in elementsToRender) { - var foundElements = component.FindAll(element); + var foundElements = component.FindAll($"{_panelClassSelector} {element}"); Assert.Equal(1, foundElements.Count); } foreach (var element in elementsNotToRender) { - var foundElements = component.FindAll(element); + var foundElements = component.FindAll($"{_panelClassSelector} {element}"); Assert.Equal(0, foundElements.Count); } } diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 07bcd78bb99..1ce00e21f2e 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -646,11 +646,11 @@ private bool TryParseInput(string inputValue, out TimeSpan value) } else { - valid = TimeSpan.TryParseExact(inputValue, TimeSpanFormat, null, TimeSpanStyles.None, out value); + valid = TimeSpan.TryParseExact(inputValue, TimeSpanFormat, Culture, TimeSpanStyles.None, out value); if (!valid) { - valid = TimeSpan.TryParse(inputValue, out value); + valid = TimeSpan.TryParse(inputValue, Culture, out value); } } From 855e72f78a3d0b8d1b04813913c753dc46a5d3a4 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:27:57 +0100 Subject: [PATCH 25/42] FormField: add TimeSpanPicker in the demo --- RadzenBlazorDemos/Pages/FormFieldInputTypes.razor | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor b/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor index dc49f7b4696..e2edf083638 100644 --- a/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor +++ b/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor @@ -34,6 +34,9 @@ + + + @@ -65,6 +68,7 @@ string autoCompleteValue = ""; string color = "rgb(68, 58, 110)"; DateTime? date = DateTime.Today; + TimeSpan? timeSpan = new TimeSpan(5, 15, 30); IEnumerable companyNames; From 677de1a77b6d5787bdb4127d2afa12c4320f12ee Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:49:42 +0100 Subject: [PATCH 26/42] TimeSpanPicker: more tests --- Radzen.Blazor.Tests/TimeSpanPickerTests.cs | 269 ++++++++++++++++++--- Radzen.Blazor/RadzenTimeSpanPicker.razor | 4 +- 2 files changed, 239 insertions(+), 34 deletions(-) diff --git a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs index dc81e5d46b0..cb5bebe8912 100644 --- a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs +++ b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs @@ -1,20 +1,26 @@ using AngleSharp.Css.Dom; -using AngleSharp.Dom; using Bunit; using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Xunit; namespace Radzen.Blazor.Tests { public class TimeSpanPickerTests { - const string _pickerClassSelector = ".rz-timespanpicker"; - const string _popupButtonClassSelector = ".rz-timespanpicker-trigger"; - const string _clearButtonClassSelector = ".rz-dropdown-clear-icon"; - const string _inputFieldClassSelector = ".rz-inputtext"; - const string _panelClassSelector = ".rz-timespanpicker-panel"; + const string _pickerSelector = ".rz-timespanpicker"; + const string _popupButtonSelector = $".rz-timespanpicker-trigger"; + const string _clearButtonSelector = $"{_pickerSelector} > .rz-dropdown-clear-icon"; + const string _inputFieldSelector = $"{_pickerSelector} > .rz-inputtext"; + const string _panelPopupContainerSelector = ".rz-timespanpicker-popup-container"; + const string _panelSelector = ".rz-timespanpicker-panel"; + const string _confirmationButtonSelector = ".rz-timespanpicker-confirmationbutton"; + const string _unitValuePickerSelector = ".rz-timespanpicker-unitvaluepicker"; + const string _signPickerSelector = ".rz-timespanpicker-signpicker"; + const string _positiveSignPickerSelector = $"{_signPickerSelector} > .rz-button:first-child"; + const string _negativeSignPickerSelector = $"{_signPickerSelector} > .rz-button:last-child"; static CultureInfo _cultureInfo = CultureInfo.InvariantCulture; #region Component general look @@ -32,7 +38,7 @@ public void TimeSpanPicker_Renders_StyleParameter() parameters.Add(p => p.Style, style); }); - var picker = component.Find(_pickerClassSelector); + var picker = component.Find(_pickerSelector); Assert.Contains(style, picker.GetStyle().CssText); } @@ -49,7 +55,7 @@ public void TimeSpanPicker_DoesNotRender_Component_WhenVisibleParameterIsFalse() parameters.Add(p => p.Visible, false); }); - var pickerElements = component.FindAll(_pickerClassSelector); + var pickerElements = component.FindAll(_pickerSelector); Assert.Equal(0, pickerElements.Count); } @@ -69,7 +75,7 @@ public void TimeSpanPicker_Renders_UnmatchedParameter() parameters.AddUnmatched(parameterName, parameterValue); }); - var picker = component.Find(_pickerClassSelector); + var picker = component.Find(_pickerSelector); Assert.Equal(parameterValue, picker.GetAttribute(parameterName)); } @@ -87,7 +93,7 @@ public void TimeSpanPicker_Renders_EmptyCssClass_WhenValueIsEmpty() parameters.Add(p => p.Value, null); }); - var picker = component.Find(_pickerClassSelector); + var picker = component.Find(_pickerSelector); Assert.Contains("rz-state-empty", picker.ClassList); } @@ -106,7 +112,7 @@ public void TimeSpanPicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty() parameters.Add(p => p.Value, value); }); - var picker = component.Find(_pickerClassSelector); + var picker = component.Find(_pickerSelector); Assert.DoesNotContain("rz-state-empty", picker.ClassList); } #endregion @@ -128,7 +134,7 @@ public void TimeSpanPicker_Renders_InputAttributesParameter() parameters.Add(p => p.InputAttributes, new Dictionary(){{ parameterName, parameterValue } }); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Equal(parameterValue, inputField.GetAttribute(parameterName)); } @@ -147,7 +153,7 @@ public void TimeSpanPicker_Renders_InputClassParameter() parameters.Add(p => p.InputClass, inputClass); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Contains(inputClass, inputField.ClassList); } @@ -166,7 +172,7 @@ public void TimeSpanPicker_Renders_NameParameter() parameters.Add(p => p.Name, name); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Equal(name, inputField.GetAttribute("name")); } @@ -185,7 +191,7 @@ public void TimeSpanPicker_Renders_AllowClearParameter() parameters.Add(p => p.Value, TimeSpan.FromDays(1)); }); - var clearButtons = component.FindAll(_clearButtonClassSelector); + var clearButtons = component.FindAll(_clearButtonSelector); Assert.Equal(1, clearButtons.Count); } @@ -203,7 +209,7 @@ public void TimeSpanPicker_Renders_AllowInputParameter_WhenFalse() parameters.Add(p => p.AllowInput, false); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.True(inputField.HasAttribute("readonly")); } @@ -222,8 +228,8 @@ public void TimeSpanPicker_Renders_DisabledParameter() parameters.Add(p => p.Disabled, true); }); - var picker = component.Find(_pickerClassSelector); - var inputField = component.Find(_inputFieldClassSelector); + var picker = component.Find(_pickerSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Contains("rz-state-disabled", picker.ClassList); Assert.True(inputField.HasAttribute("disabled")); @@ -243,7 +249,7 @@ public void TimeSpanPicker_Renders_ReadOnlyParameter() parameters.Add(p => p.ReadOnly, true); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Contains("rz-readonly", inputField.ClassList); Assert.True(inputField.HasAttribute("readonly")); @@ -263,7 +269,7 @@ public void TimeSpanPicker_Renders_ShowPopupButtonParameter() parameters.Add(p => p.ShowPopupButton, true); }); - var triggerButtons = component.FindAll(_popupButtonClassSelector); + var triggerButtons = component.FindAll(_popupButtonSelector); Assert.Equal(1, triggerButtons.Count); } @@ -302,7 +308,7 @@ public void TimeSpanPicker_Renders_TabIndexParameter() parameters.Add(p => p.TabIndex, tabIndex); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Equal(tabIndex.ToString(_cultureInfo), inputField.GetAttribute("tabindex")); } @@ -326,7 +332,7 @@ public void TimeSpanPicker_Renders_TimeSpanFormatParameter() parameters.Add(p => p.Culture, _cultureInfo); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Equal(formattedValue, inputField.GetAttribute("value")); } @@ -345,9 +351,29 @@ public void TimeSpanPicker_Renders_PlaceholderParameter() parameters.Add(p => p.Placeholder, placeholder); }); - var inputField = component.Find(_inputFieldClassSelector); + var inputField = component.Find(_inputFieldSelector); Assert.Equal(placeholder, inputField.GetAttribute("placeholder")); } + + [Fact] + public void TimeSpanPicker_Renders_TogglePopupAriaLabelParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js"); + + var component = ctx.RenderComponent>(); + + var ariaLabel = "aria label test"; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.ShowPopupButton, true); + parameters.Add(p => p.TogglePopupAriaLabel, ariaLabel); + }); + + var inputField = component.Find(_popupButtonSelector); + Assert.Equal(ariaLabel, inputField.GetAttribute("aria-label")); + } #endregion #region Input field behavior @@ -370,7 +396,7 @@ public void TimeSpanPicker_Respects_MinParameter_OnInput() var valueToSet = TimeSpan.FromHours(-1); - component.Find(_inputFieldClassSelector).Change(valueToSet); + component.Find(_inputFieldSelector).Change(valueToSet); Assert.Equal(minValue, component.Instance.Value); } @@ -394,7 +420,7 @@ public void TimeSpanPicker_Respects_MaxParameter_OnInput() var valueToSet = TimeSpan.FromHours(1); - component.Find(_inputFieldClassSelector).Change(valueToSet); + component.Find(_inputFieldSelector).Change(valueToSet); Assert.Equal(maxValue, component.Instance.Value); } @@ -417,7 +443,7 @@ public void TimeSpanPicker_Parses_Input_Using_TimeSpanFormat() var expectedValue = new TimeSpan(15, 5, 30); var input = expectedValue.ToString(format, _cultureInfo); - var inputElement = component.Find(_inputFieldClassSelector); + var inputElement = component.Find(_inputFieldSelector); inputElement.Change(input); Assert.Equal(expectedValue, component.Instance.Value); @@ -445,7 +471,7 @@ public void TimeSpanPicker_Parses_Input_Using_ParseInput() parameters.Add(p => p.ParseInput, customParseInput); }); - var inputElement = component.Find(_inputFieldClassSelector); + var inputElement = component.Find(_inputFieldSelector); string input = "- 15h 5min 30s"; TimeSpan expectedValue = new TimeSpan(15, 5, 30).Negate(); @@ -475,7 +501,7 @@ public void TimeSpanPicker_Raises_ValueChanged_OnInputChange() TimeSpan inputValue = new TimeSpan(3, 15, 30); var input = inputValue.ToString(null, _cultureInfo); - var inputElement = component.Find(_inputFieldClassSelector); + var inputElement = component.Find(_inputFieldSelector); inputElement.Change(input); @@ -503,7 +529,7 @@ public void TimeSpanPicker_Raises_Change_OnInputChange() TimeSpan inputValue = new TimeSpan(3, 15, 30); var input = inputValue.ToString(null, _cultureInfo); - var inputElement = component.Find(_inputFieldClassSelector); + var inputElement = component.Find(_inputFieldSelector); inputElement.Change(input); @@ -530,7 +556,7 @@ public void TimeSpanPicker_Raises_ChangeEvent_OnClearButtonClick() parameters.Add(p => p.Change, args => { raised = true; value = args; }); }); - var clearButton = component.Find(_clearButtonClassSelector); + var clearButton = component.Find(_clearButtonSelector); clearButton.Click(); Assert.True(raised); @@ -539,6 +565,156 @@ public void TimeSpanPicker_Raises_ChangeEvent_OnClearButtonClick() #endregion #region Panel look + [Fact] + public void TimeSpanPicker_Renders_SignButtons_WhenMinNegative_WhenMaxPositive() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + var min = TimeSpan.FromDays(-1); + var max = TimeSpan.FromDays(1); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Min, min); + parameters.Add(p => p.Max, max); + }); + + var signPickers = component.FindAll(_signPickerSelector); + + Assert.Equal(1, signPickers.Count); + } + + [Theory] + [InlineData(-10, -1)] + [InlineData(-10, 0)] + [InlineData(1, 10)] + [InlineData(0, 10)] + public void TimeSpanPicker_DoesNotRender_SignButtons_WhenMinAndMaxHaveSameSignOrZero(int minDays, int maxDays) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + var min = TimeSpan.FromDays(minDays); + var max = TimeSpan.FromDays(maxDays); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Min, min); + parameters.Add(p => p.Max, max); + }); + + var signPickers = component.FindAll(_signPickerSelector); + + Assert.Equal(0, signPickers.Count); + } + + [Fact] + public void TimeSpanPicker_Renders_PopupRenderModeParameter_WhenInitial() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Inline, false); + parameters.Add(p => p.PopupRenderMode, PopupRenderMode.Initial); + }); + + var popupContainer = component.Find(_panelPopupContainerSelector); + var panels = component.FindAll(_panelSelector); + + Assert.Equal(1, panels.Count); + } + + [Fact] + public void TimeSpanPicker_Renders_PopupRenderModeParameter_WhenOnDemand() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Inline, false); + parameters.Add(p => p.PopupRenderMode, PopupRenderMode.OnDemand); + }); + + var popupContainer = component.Find(_panelPopupContainerSelector); + + Assert.False(popupContainer.HasChildNodes); + } + + [Fact] + public void TimeSpanPicker_Renders_InlineParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Inline, true); + }); + + var inputFields = component.FindAll(_inputFieldSelector); + var popupContainers = component.FindAll(_panelPopupContainerSelector); + var panels = component.FindAll(_panelSelector); + + Assert.Equal(0, inputFields.Count); + Assert.Equal(0, popupContainers.Count); + Assert.Equal(1, panels.Count); + } + + [Fact] + public void TimeSpanPicker_Renders_ConfirmationButtonParameter() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.ShowConfirmationButton, true); + }); + + var confirmationButtons = component.FindAll(_confirmationButtonSelector); + + Assert.Equal(1, confirmationButtons.Count); + } + + [Theory] + [InlineData(".rz-timespanpicker-hours", "00")] + [InlineData(".rz-timespanpicker-minutes", "00")] + [InlineData(".rz-timespanpicker-seconds", "00")] + [InlineData(".rz-timespanpicker-milliseconds", "000")] + [InlineData(".rz-timespanpicker-microseconds", "000")] + public void TimeSpanPicker_Renders_PadTimeValuesParameter(string unitSelector, string format) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + var number = 5; + var value = new TimeSpan(number, number, number, number, number, number); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.PadTimeValues, true); + parameters.Add(p => p.Value, value); + parameters.Add(p => p.FieldPrecision, TimeSpanUnit.Microsecond); + parameters.Add(p => p.Culture, _cultureInfo); + }); + + var formattedNumber = number.ToString(format, _cultureInfo); + + var field = component.Find($"{unitSelector} > {_unitValuePickerSelector}"); + + Assert.Contains($"value=\"{formattedNumber}\"", field.ToMarkup()); + } + [Theory] [InlineData( TimeSpanUnit.Day, @@ -570,15 +746,44 @@ public void TimeSpanPicker_Renders_FieldPrecisionParameter(TimeSpanUnit precisio foreach (var element in elementsToRender) { - var foundElements = component.FindAll($"{_panelClassSelector} {element}"); + var foundElements = component.FindAll($"{_panelSelector} {element}"); Assert.Equal(1, foundElements.Count); } foreach (var element in elementsNotToRender) { - var foundElements = component.FindAll($"{_panelClassSelector} {element}"); + var foundElements = component.FindAll($"{_panelSelector} {element}"); Assert.Equal(0, foundElements.Count); } } + + // steps here + + [Fact] + public void TimeSpanPicker_Renders_SignButtonLabelParemeters() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + var min = TimeSpan.FromDays(-1); + var max = TimeSpan.FromDays(1); + var positiveLabel = "positive + test"; + var negativeLabel = "negative - test"; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Min, min); + parameters.Add(p => p.Max, max); + parameters.Add(p => p.PositiveButtonLabel, positiveLabel); + parameters.Add(p => p.NegativeButtonLabel, negativeLabel); + }); + + var positiveSignPicker = component.Find(_positiveSignPickerSelector); + var negativeSignPicker = component.Find(_negativeSignPickerSelector); + + Assert.Contains(positiveLabel, positiveSignPicker.ToMarkup()); + Assert.Contains(negativeLabel, negativeSignPicker.ToMarkup()); + } #endregion } } diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 9c3e881e3c8..34b2a343915 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -47,7 +47,7 @@ { @@ -143,7 +143,7 @@ @if (ShowConfirmationButton && ReadOnly is false) { @ConfirmationButtonLabel From e1ccc178be4b1cb070be1d1e60cd5887de4b0e82 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:57:58 +0100 Subject: [PATCH 27/42] revert unrelated and unintended changed --- Radzen.Blazor.Tests/NumericTests.cs | 2 +- Radzen.Blazor/RadzenDatePicker.razor | 2 +- .../Properties/launchSettings.json | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Radzen.Blazor.Tests/NumericTests.cs b/Radzen.Blazor.Tests/NumericTests.cs index a2289b03f08..f84e85e9808 100644 --- a/Radzen.Blazor.Tests/NumericTests.cs +++ b/Radzen.Blazor.Tests/NumericTests.cs @@ -612,7 +612,7 @@ public void Numeric_Supports_IComparable() }); }); - component.Find("input").Change(13.53.ToString()); + component.Find("input").Change("13.53"); var maxDollars = new Dollars(2); Assert.Contains($" value=\"{maxDollars.ToString()}\"", component.Markup); diff --git a/Radzen.Blazor/RadzenDatePicker.razor b/Radzen.Blazor/RadzenDatePicker.razor index 97ac356a06f..a3c21c1e2fd 100644 --- a/Radzen.Blazor/RadzenDatePicker.razor +++ b/Radzen.Blazor/RadzenDatePicker.razor @@ -11,7 +11,7 @@ { + class="rz-inputtext @InputClass @(ReadOnly ? "rz-readonly" : "") @(!ShowButton ? "rz-input-trigger" : "")" id="@Name" placeholder="@CurrentPlaceholder" /> @if (ShowButton) {
diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 1ce00e21f2e..5e89c3bfb05 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -149,7 +149,7 @@ public TValue Value public Func ParseInput { get; set; } #endregion - #region Parameters: input field labels + #region Parameters: input field texts /// /// Specifies the input placeholder. /// @@ -233,73 +233,73 @@ public TValue Value ///
[Parameter] public string MicrosecondsStep { get; set; } - #endif +#endif #endregion - #region Parameters: panel labels + #region Parameters: panel texts /// - /// Specifies the label of the confirmation button. + /// Specifies the text of the confirmation button. Used only if is true. /// [Parameter] - public string ConfirmationButtonLabel { get; set; } = "OK"; + public string ConfirmationButtonText { get; set; } = "OK"; /// - /// Specifies the label of the positive value button. + /// Specifies the text of the positive value button. /// [Parameter] - public string PositiveButtonLabel { get; set; } = "+"; + public string PositiveButtonText { get; set; } = "+"; /// - /// Specifies the label of the negative value button. + /// Specifies the text of the negative value button. /// [Parameter] - public string NegativeButtonLabel { get; set; } = "−"; + public string NegativeButtonText { get; set; } = "−"; /// - /// Specifies the text displayed before the fields in the panel when the value is positive and there's no value sign picker. + /// Specifies the text displayed next to the fields in the panel when the value is positive and there's no sign picker. /// [Parameter] public string PositiveValueText { get; set; } = string.Empty; /// - /// Specifies the text displayed before the fields in the panel when the value is negative and there's no value sign picker. + /// Specifies the text displayed next to the fields in the panel when the value is negative and there's no sign picker. /// [Parameter] public string NegativeValueText { get; set; } = "−"; /// - /// Specifies the text displayed next to the days field. + /// Specifies the days label text. /// [Parameter] public string DaysUnitText { get; set; } = "Days"; /// - /// Specifies the text displayed next to the hours field. + /// Specifies the hours label text. /// [Parameter] public string HoursUnitText { get; set; } = "Hours"; /// - /// Specifies the text displayed next to the minutes field. + /// Specifies the minutes label text. /// [Parameter] public string MinutesUnitText { get; set; } = "Minutes"; /// - /// Specifies the text displayed next to the seconds field. + /// Specifies the seconds label text. /// [Parameter] public string SecondsUnitText { get; set; } = "Seconds"; /// - /// Specifies the text displayed next to the milliseconds field. + /// Specifies the milliseconds label text. /// [Parameter] public string MillisecondsUnitText { get; set; } = "Milliseconds"; - #if NET7_0_OR_GREATER +#if NET7_0_OR_GREATER /// - /// Specifies the text displayed next to the microseconds field. + /// Specifies the microseconds label text. /// [Parameter] public string MicrosecondsUnitText { get; set; } = "Microseconds"; @@ -759,7 +759,7 @@ private void SetPanelFieldsSetup(TimeSpan min, TimeSpan max) _positiveTimeFieldsMaxValues = canBePositive ? GetTimeUnitMaxValues(max) : new(_timeUnitZeroValues); } - private Dictionary GetTimeUnitMaxValues(TimeSpan boundary) + private static Dictionary GetTimeUnitMaxValues(TimeSpan boundary) { var timeUnitMaxValues = new Dictionary(_timeUnitMaxAbsoluteValues); From 8342e66a5232b2bbf22ca216f910d4ea0ec86a3c Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sat, 15 Feb 2025 19:06:39 +0100 Subject: [PATCH 33/42] TimeSpanPicker: remove UnconfirmedValueChanged param TimeSpanPicker test: add event-related tests for the panel, simplify some tests related to timespan units --- Radzen.Blazor.Tests/TimeSpanPickerTests.cs | 236 ++++++++++++++++++-- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 8 +- 2 files changed, 216 insertions(+), 28 deletions(-) diff --git a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs index 6c2aba46899..0a220bd8e76 100644 --- a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs +++ b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs @@ -27,11 +27,22 @@ public class TimeSpanPickerTests { TimeSpanUnit.Millisecond, ".rz-timespanpicker-milliseconds" }, { TimeSpanUnit.Microsecond, ".rz-timespanpicker-microseconds" } }; - const string _unitValuePickerSelector = ".rz-timespanpicker-unitvaluepicker"; const string _signPickerSelector = ".rz-timespanpicker-signpicker"; const string _positiveSignPickerSelector = $"{_signPickerSelector} > .rz-button:first-child"; const string _negativeSignPickerSelector = $"{_signPickerSelector} > .rz-button:last-child"; + static readonly Dictionary _unitSpans = new() + { + { TimeSpanUnit.Day, TimeSpan.FromDays(1) }, + { TimeSpanUnit.Hour, TimeSpan.FromHours(1) }, + { TimeSpanUnit.Minute, TimeSpan.FromMinutes(1) }, + { TimeSpanUnit.Second, TimeSpan.FromSeconds(1) }, + { TimeSpanUnit.Millisecond, TimeSpan.FromMilliseconds(1) }, + { TimeSpanUnit.Microsecond, TimeSpan.FromMicroseconds(1) } + }; + public static IEnumerable TimeSpanUnitsForTheory + => Enum.GetValues().Select(x => new object[] { x }); + static readonly CultureInfo _cultureInfo = CultureInfo.InvariantCulture; #region Component general look @@ -515,7 +526,7 @@ public void TimeSpanPicker_Raises_ValueChanged_OnInputChange() parameters.Add(p => p.Culture, _cultureInfo); }); - var inputValue = new TimeSpan(3, 15, 30); + var inputValue = new TimeSpan(5, 15, 30); var input = inputValue.ToString(null, _cultureInfo); var inputElement = component.Find(_inputFieldSelector); @@ -543,7 +554,7 @@ public void TimeSpanPicker_Raises_Change_OnInputChange() parameters.Add(p => p.Culture, _cultureInfo); }); - var inputValue = new TimeSpan(3, 15, 30); + var inputValue = new TimeSpan(5, 15, 30); var input = inputValue.ToString(null, _cultureInfo); var inputElement = component.Find(_inputFieldSelector); @@ -565,7 +576,7 @@ public void TimeSpanPicker_Raises_ChangeEvent_OnClearButtonClick() var component = ctx.RenderComponent>(); var raised = false; - TimeSpan? value = new TimeSpan(3, 15, 30); + TimeSpan? value = new TimeSpan(5, 15, 30); component.SetParametersAndRender(parameters => { parameters.Add(p => p.AllowClear, true); @@ -739,15 +750,13 @@ public void TimeSpanPicker_Renders_PadTimeValuesParameter(TimeSpanUnit unit, str var formattedNumber = number.ToString(format, _cultureInfo); - var field = component.Find($"{_unitElementSelectors[unit]} > {_unitValuePickerSelector}"); + var field = component.Find($"{_unitElementSelectors[unit]} input"); - Assert.Contains($"value=\"{formattedNumber}\"", field.ToMarkup()); + Assert.Equal(formattedNumber, field.GetAttribute("value")); } - public static IEnumerable TimeSpanPicker_Renders_FieldPrecisionParameter_Data - => Enum.GetValues().Select(x => new object[] { x }); [Theory] - [MemberData(nameof(TimeSpanPicker_Renders_FieldPrecisionParameter_Data))] + [MemberData(nameof(TimeSpanUnitsForTheory))] public void TimeSpanPicker_Renders_FieldPrecisionParameter(TimeSpanUnit precision) { using var ctx = new TestContext(); @@ -905,15 +914,200 @@ public void TimeSpanPicker_Renders_MicrosecondsUnitTextParameter() #endregion #region Panel behavior + [Theory] + [MemberData(nameof(TimeSpanUnitsForTheory))] + public void TimeSpanPicker_Raises_ChangeAndValueChanged_OnPanelInputChange(TimeSpanUnit unit) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + TimeSpan value = TimeSpan.Zero; + TimeSpan? newValue = TimeSpan.Zero; + bool raisedChange = false; + bool raisedValueChanged = false; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, value); + parameters.Add(p => p.Change, args => { raisedChange = true; newValue = args; }); + parameters.Add(p => p.ValueChanged, args => { raisedValueChanged = true; newValue = args; }); + parameters.Add(p => p.Culture, _cultureInfo); + parameters.Add(p => p.FieldPrecision, TimeSpanUnit.Microsecond); + }); + + var inputValue = 15; + var input = inputValue.ToString(null, _cultureInfo); + var expectedValue = value + inputValue * _unitSpans[unit]; + + var unitInputFieldSelector = $"{_unitElementSelectors[unit]} input"; + var inputElement = component.Find(unitInputFieldSelector); + + inputElement.Change(input); + + Assert.True(raisedChange, "Change not raised"); + Assert.True(raisedValueChanged, "ValueChanged not raised"); + Assert.Equal(expectedValue, newValue); + } + + [Theory] + [MemberData(nameof(TimeSpanUnitsForTheory))] + public void TimeSpanPicker_DoesNotRaise_ChangeOrValueChanged_OnPanelInputChange_IfConfirmationButtonShown(TimeSpanUnit unit) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + TimeSpan initialValue = TimeSpan.Zero; + bool raisedChange = false; + bool raisedValueChanged = false; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, initialValue); + parameters.Add(p => p.Change, args => { raisedChange = true; }); + parameters.Add(p => p.ValueChanged, args => { raisedValueChanged = true; }); + parameters.Add(p => p.Culture, _cultureInfo); + parameters.Add(p => p.FieldPrecision, TimeSpanUnit.Microsecond); + parameters.Add(p => p.ShowConfirmationButton, true); + }); + + var inputValue = 15; + var input = inputValue.ToString(null, _cultureInfo); + + var unitInputFieldSelector = $"{_unitElementSelectors[unit]} input"; + var inputElement = component.Find(unitInputFieldSelector); + + inputElement.Change(input); + + Assert.False(raisedChange, "Change raised"); + Assert.False(raisedValueChanged, "ValueChanged raised"); + Assert.Equal(initialValue, component.Instance.Value); + } + + private static void TimeSpanPicker_Raises_ChangeAndValueChanged_OnClickSignButton( + TimeSpan initialValue, TimeSpan expectedValue, bool isPositiveButton) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + TimeSpan min = TimeSpan.MinValue; + TimeSpan max = TimeSpan.MaxValue; + TimeSpan? newValue = TimeSpan.Zero; + bool raisedChange = false; + bool raisedValueChanged = false; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, initialValue); + parameters.Add(p => p.Min, min); + parameters.Add(p => p.Max, max); + parameters.Add(p => p.Change, args => { raisedChange = true; newValue = args; }); + parameters.Add(p => p.ValueChanged, args => { raisedValueChanged = true; newValue = args; }); + }); + + var signButton = component.Find(isPositiveButton ? _positiveSignPickerSelector : _negativeSignPickerSelector); + signButton.Click(); + + Assert.True(raisedChange, "Change not raised"); + Assert.True(raisedValueChanged, "ValueChanged not raised"); + Assert.Equal(expectedValue, newValue); + } + [Fact] + public void TimeSpanPicker_Raises_ChangeOrValueChanged_OnClickPositiveSignButton() + => TimeSpanPicker_Raises_ChangeAndValueChanged_OnClickSignButton( + + new TimeSpan(5, 15, 30).Negate(), new TimeSpan(5, 15, 30), true); + [Fact] + public void TimeSpanPicker_Raises_ChangeAndValueChanged_OnClickNegativeSignButton() + => TimeSpanPicker_Raises_ChangeAndValueChanged_OnClickSignButton( + new TimeSpan(5, 15, 30), new TimeSpan(5, 15, 30).Negate(), false); + + private static void TimeSpanPicker_DoesNotRaise_ChangeOrValueChanged_OnClickSignButton_IfConfirmationButtonShown( + TimeSpan initialValue, bool isPositiveButton) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + TimeSpan min = TimeSpan.MinValue; + TimeSpan max = TimeSpan.MaxValue; + bool raisedChange = false; + bool raisedValueChanged = false; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, initialValue); + parameters.Add(p => p.Min, min); + parameters.Add(p => p.Max, max); + parameters.Add(p => p.Change, args => { raisedChange = true; }); + parameters.Add(p => p.ValueChanged, args => { raisedValueChanged = true; }); + parameters.Add(p => p.ShowConfirmationButton, true); + }); + + var signButton = component.Find(isPositiveButton ? _positiveSignPickerSelector : _negativeSignPickerSelector); + signButton.Click(); + + Assert.False(raisedChange, "Change raised"); + Assert.False(raisedValueChanged, "ValueChanged raised"); + Assert.Equal(initialValue, component.Instance.Value); + } + [Fact] + public void TimeSpanPicker_DoesNotRaise_ChangeOrValueChanged_OnClickPositiveSignButton_IfConfirmationButtonShown() + => TimeSpanPicker_DoesNotRaise_ChangeOrValueChanged_OnClickSignButton_IfConfirmationButtonShown( + -new TimeSpan(5, 15, 30), true); + [Fact] + public void TimeSpanPicker_DoesNotRaise_ChangeOrValueChanged_OnClickNegativeSignButton_IfConfirmationButtonShown() + => TimeSpanPicker_DoesNotRaise_ChangeOrValueChanged_OnClickSignButton_IfConfirmationButtonShown( + new TimeSpan(5, 15, 30), false); + + [Fact] + public void TimeSpanPicker_Raises_ChangeAndValueChanged_OnClickConfirmationButton() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + var component = ctx.RenderComponent>(); + + TimeSpan initialValue = new TimeSpan(5, 15, 30); + TimeSpan? newValue = TimeSpan.Zero; + bool raisedChange = false; + bool raisedValueChanged = false; + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, initialValue); + parameters.Add(p => p.Change, args => { raisedChange = true; newValue = args; }); + parameters.Add(p => p.ValueChanged, args => { raisedValueChanged = true; newValue = args; }); + parameters.Add(p => p.Culture, _cultureInfo); + parameters.Add(p => p.ShowConfirmationButton, true); + }); + + var inputValue = 20; + var input = inputValue.ToString(null, _cultureInfo); + var expectedValue = new TimeSpan(inputValue, 15, 30); + + var unitInputFieldSelector = $"{_unitElementSelectors[TimeSpanUnit.Hour]} input"; + var inputElement = component.Find(unitInputFieldSelector); + inputElement.Change(input); + + var confirmationButton = component.Find(_confirmationButtonSelector); + confirmationButton.Click(); + + Assert.True(raisedChange, "Change not raised"); + Assert.True(raisedValueChanged, "ValueChanged not raised"); + Assert.Equal(expectedValue, newValue); + } + private static void TimeSpanPicker_Respects_StepParameter( - Expression, string>> stepParameterSelector, TimeSpanUnit unit, long ticksPerUnit) + Expression, string>> stepParameterSelector, TimeSpanUnit unit) { using var ctx = new TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; var component = ctx.RenderComponent>(); var step = 5; - var stepTicks = step * ticksPerUnit; + var stepSpan = step * _unitSpans[unit]; var initialValue = new TimeSpan(1, 2, 3, 4, 5, 6); component.SetParametersAndRender(parameters => { @@ -923,35 +1117,35 @@ private static void TimeSpanPicker_Respects_StepParameter( }); var unitSelector = _unitElementSelectors[unit]; - var expectedValueUp = initialValue.Add(new TimeSpan(2 * stepTicks)); - var fieldUpButton = component.Find($"{unitSelector} > {_unitValuePickerSelector} > .rz-numeric-up"); + var expectedValueUp = initialValue + 2 * stepSpan; + var fieldUpButton = component.Find($"{unitSelector} .rz-numeric-up"); fieldUpButton.Click(); fieldUpButton.Click(); Assert.Equal(expectedValueUp, component.Instance.Value); - var expectedValueDown = expectedValueUp.Add(new TimeSpan(-stepTicks)); - var fieldDownButton = component.Find($"{unitSelector} > {_unitValuePickerSelector} > .rz-numeric-down"); + var expectedValueDown = expectedValueUp - stepSpan; + var fieldDownButton = component.Find($"{unitSelector} .rz-numeric-down"); fieldDownButton.Click(); Assert.Equal(expectedValueDown, component.Instance.Value); } [Fact] public void TimeSpanPicker_Respects_DaysStepParameter() - => TimeSpanPicker_Respects_StepParameter(x => x.DaysStep, TimeSpanUnit.Day, TimeSpan.TicksPerDay); + => TimeSpanPicker_Respects_StepParameter(x => x.DaysStep, TimeSpanUnit.Day); [Fact] public void TimeSpanPicker_Respects_HoursStepParameter() - => TimeSpanPicker_Respects_StepParameter(x => x.HoursStep, TimeSpanUnit.Hour, TimeSpan.TicksPerHour); + => TimeSpanPicker_Respects_StepParameter(x => x.HoursStep, TimeSpanUnit.Hour); [Fact] public void TimeSpanPicker_Respects_MinutesStepParameter() - => TimeSpanPicker_Respects_StepParameter(x => x.MinutesStep, TimeSpanUnit.Minute, TimeSpan.TicksPerMinute); + => TimeSpanPicker_Respects_StepParameter(x => x.MinutesStep, TimeSpanUnit.Minute); [Fact] public void TimeSpanPicker_Respects_SecondsStepParameter() - => TimeSpanPicker_Respects_StepParameter(x => x.SecondsStep, TimeSpanUnit.Second, TimeSpan.TicksPerSecond); + => TimeSpanPicker_Respects_StepParameter(x => x.SecondsStep, TimeSpanUnit.Second); [Fact] public void TimeSpanPicker_Respects_MillisecondsStepParameter() - => TimeSpanPicker_Respects_StepParameter(x => x.MillisecondsStep, TimeSpanUnit.Millisecond, TimeSpan.TicksPerMillisecond); + => TimeSpanPicker_Respects_StepParameter(x => x.MillisecondsStep, TimeSpanUnit.Millisecond); [Fact] public void TimeSpanPicker_Respects_MicrosecondsStepParameter() - => TimeSpanPicker_Respects_StepParameter(x => x.MicrosecondsStep, TimeSpanUnit.Microsecond, TimeSpan.TicksPerMicrosecond); + => TimeSpanPicker_Respects_StepParameter(x => x.MicrosecondsStep, TimeSpanUnit.Microsecond); #endregion } } diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 5e89c3bfb05..67742664d6e 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -326,12 +326,6 @@ public TValue Value /// [Parameter] public EventCallback Change { get; set; } - - /// - /// Specifies the callback of the unconfirmed time span (in a panel with a confirmation button) change. - /// - [Parameter] - public EventCallback UnconfirmedValueChanged { get; set; } #endregion @@ -891,7 +885,7 @@ private Task UpdateValueFromPanelFields(TimeSpan newValue) if (ShowConfirmationButton) { - return UnconfirmedValueChanged.InvokeAsync(newValue); + return Task.CompletedTask; } ConfirmedValue = newValue; From 8a003067cbae7b6d616ff1b359bd0f19034092f4 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 16 Feb 2025 09:07:47 +0100 Subject: [PATCH 34/42] TimeSpanPicker: pass culture to numeric fields, add panel field rendering tests --- Radzen.Blazor.Tests/TimeSpanPickerTests.cs | 36 ++++++++++++++++++++++ Radzen.Blazor/RadzenTimeSpanPicker.razor | 12 ++++---- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs index 0a220bd8e76..3a81efd8814 100644 --- a/Radzen.Blazor.Tests/TimeSpanPickerTests.cs +++ b/Radzen.Blazor.Tests/TimeSpanPickerTests.cs @@ -593,6 +593,42 @@ public void TimeSpanPicker_Raises_ChangeEvent_OnClearButtonClick() #endregion #region Panel look + private static void TimeSpanPicker_Renders_CorrectValuesInPanelInputs(TimeSpanUnit unit, bool negative) + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + + + var number = 5; + var value = _unitSpans[unit] * number * (negative ? -1 : 1); // zeros in all units except for one + var min = TimeSpan.MinValue; + var max = TimeSpan.MaxValue; + var component = ctx.RenderComponent>(); + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, value); + parameters.Add(p => p.FieldPrecision, TimeSpanUnit.Microsecond); + parameters.Add(p => p.Min, min); + parameters.Add(p => p.Max, max); + parameters.Add(p => p.Culture, _cultureInfo); + }); + + var expectedValue = number.ToString(_cultureInfo); + + var field = component.Find($"{_unitElementSelectors[unit]} input"); + var fieldValue = field.GetAttribute("value"); + + Assert.Equal(expectedValue, fieldValue); + } + [Theory] + [MemberData(nameof(TimeSpanUnitsForTheory))] + public void TimeSpanPicker_Renders_CorrectValuesInPanelInputs_IfPositiveValue(TimeSpanUnit unit) + => TimeSpanPicker_Renders_CorrectValuesInPanelInputs(unit, false); + [Theory] + [MemberData(nameof(TimeSpanUnitsForTheory))] + public void TimeSpanPicker_Renders_CorrectValuesInPanelInputs_IfNegativeValue(TimeSpanUnit unit) + => TimeSpanPicker_Renders_CorrectValuesInPanelInputs(unit, true); + [Fact] public void TimeSpanPicker_Renders_SignButtons_WhenMinNegative_WhenMaxPositive() { diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 3769fd62c0a..4d8a82dd9c4 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -66,7 +66,7 @@ {
- - - - - - Date: Sun, 16 Feb 2025 13:03:27 +0100 Subject: [PATCH 35/42] TimeSpanPicker: unify unit fields under one RenderFragment function --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 114 ++++++++--------------- 1 file changed, 39 insertions(+), 75 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 4d8a82dd9c4..38aececf5d3 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -62,81 +62,21 @@
@signText
} } - @if (FieldPrecision >= TimeSpanUnit.Day && TimeFieldsMaxValues[TimeSpanUnit.Day] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Hour && TimeFieldsMaxValues[TimeSpanUnit.Hour] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Minute && TimeFieldsMaxValues[TimeSpanUnit.Minute] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Second && TimeFieldsMaxValues[TimeSpanUnit.Second] > 0) - { -
- - -
- } - @if (FieldPrecision >= TimeSpanUnit.Millisecond && TimeFieldsMaxValues[TimeSpanUnit.Millisecond] > 0) - { -
- - -
- } + + @RenderUnitField(TimeSpanUnit.Day, DaysUnitText, "d", UnconfirmedValue.Days, DaysStep, null, UpdateDays) + + @RenderUnitField(TimeSpanUnit.Hour, HoursUnitText, "h", UnconfirmedValue.Hours, HoursStep, "00", UpdateHours) + + @RenderUnitField(TimeSpanUnit.Minute, MinutesUnitText, "min", UnconfirmedValue.Minutes, MinutesStep, "00", UpdateMinutes) + + @RenderUnitField(TimeSpanUnit.Second, SecondsUnitText, "s", UnconfirmedValue.Seconds, SecondsStep, "00", UpdateSeconds) + + @RenderUnitField(TimeSpanUnit.Millisecond, MillisecondsUnitText, "ms", UnconfirmedValue.Milliseconds, MillisecondsStep, "000", UpdateMilliseconds) + @{ - #if NET7_0_OR_GREATER - if (FieldPrecision >= TimeSpanUnit.Microsecond && TimeFieldsMaxValues[TimeSpanUnit.Microsecond] > 0) - { -
- - -
- } - #endif + #if NET7_0_OR_GREATER + RenderUnitField(TimeSpanUnit.Microsecond, MicrosecondsUnitText, "us", UnconfirmedValue.Microseconds, MicrosecondsStep, "000", UpdateMicroseconds); + #endif }
@@ -144,10 +84,34 @@ { + Click="@(() => ConfirmValue())"> @ConfirmationButtonText } + +@code { + internal RenderFragment RenderUnitField(TimeSpanUnit unit, string label, string unitSymbol, int value, string step, string valuePaddingFormat, Func change) + { + return __builder => + { + + @if (FieldPrecision >= unit && TimeFieldsMaxValues[unit] > 0) + { + var inputName = $"{UniqueID}-{unitSymbol}"; +
+ + +
+ } +
+ }; + } +} \ No newline at end of file From 7947d442a216e9d2c32456ad05dcfe7ef21e5714 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sun, 16 Feb 2025 13:19:23 +0100 Subject: [PATCH 36/42] TimeSpanPicker: fix missing microseconds picker, fix classes of input fields --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 38aececf5d3..1c443ea9ace 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -75,7 +75,9 @@ @{ #if NET7_0_OR_GREATER - RenderUnitField(TimeSpanUnit.Microsecond, MicrosecondsUnitText, "us", UnconfirmedValue.Microseconds, MicrosecondsStep, "000", UpdateMicroseconds); + { + @RenderUnitField(TimeSpanUnit.Microsecond, MicrosecondsUnitText, "us", UnconfirmedValue.Microseconds, MicrosecondsStep, "000", UpdateMicroseconds) + } #endif } @@ -101,7 +103,7 @@ @if (FieldPrecision >= unit && TimeFieldsMaxValues[unit] > 0) { var inputName = $"{UniqueID}-{unitSymbol}"; -
+
Date: Sun, 16 Feb 2025 13:20:42 +0100 Subject: [PATCH 37/42] TimeSpanPicker: * prevent from changing UnconfirmedValue externally by changing Value, * prevent from raising ValueChanged and Change events if the value haven't actually change --- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 52 ++++++++++++--------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 67742664d6e..133e9daf788 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -408,12 +408,20 @@ private TimeSpan? ConfirmedValue return; } - _confirmedValue = value.HasValue ? AdjustToBounds(value.Value) : null; - var publicValue = _confirmedValue is null - ? _isNullable ? null : DefaultNonNullValue - : _confirmedValue; - Value = (TValue) (object) publicValue; - ResetUnconfirmedValue(); + TimeSpan? newValue = value.HasValue ? AdjustToBounds(value.Value) : + _isNullable ? null : DefaultNonNullValue; + if (_confirmedValue == newValue) + { + return; + } + _confirmedValue = newValue; + + Value = (TValue) (object)_confirmedValue; + + if (ShowConfirmationButton is false) + { + UnconfirmedValue = ConfirmedValue ?? DefaultNonNullValue; + } } } @@ -429,6 +437,10 @@ private TimeSpan UnconfirmedValue } var newValue = AdjustToBounds(value); + if (_unconfirmedValue == newValue) + { + return; + } if (newValue != TimeSpan.Zero || _canBeEitherPositiveOrNegative is false) { @@ -544,8 +556,6 @@ public async Task Close() } await ClosePopup(); - - StateHasChanged(); } /// @@ -588,8 +598,6 @@ private async Task Clear() } await Change.InvokeAsync(ConfirmedValue); - - StateHasChanged(); } private async Task SetValueFromInput(string inputValue) @@ -601,21 +609,16 @@ private async Task SetValueFromInput(string inputValue) bool valid = TryParseInput(inputValue, out TimeSpan value); - TimeSpan? newValue = valid ? value : null; + TimeSpan? newValue = valid ? AdjustToBounds(value) : null; if (ConfirmedValue != newValue && (newValue is not null || _isNullable)) { ConfirmedValue = newValue; - await Change.InvokeAsync(ConfirmedValue); - - if (FieldIdentifier.FieldName != null) - { - EditContext?.NotifyFieldChanged(FieldIdentifier); - } - await ValueChanged.InvokeAsync(Value); + // sometimes onchange is triggered after the popup opens so it won't synchronize the value while opening + UnconfirmedValue = ConfirmedValue ?? DefaultNonNullValue; - StateHasChanged(); + await OnChange(); } else { @@ -883,19 +886,22 @@ private Task UpdateValueFromPanelFields(TimeSpan newValue) UnconfirmedValue = newValue; - if (ShowConfirmationButton) + if (ShowConfirmationButton || UnconfirmedValue == ConfirmedValue) { return Task.CompletedTask; } - ConfirmedValue = newValue; + ConfirmedValue = UnconfirmedValue; return OnChange(); } private async Task ConfirmValue() { - ConfirmedValue = UnconfirmedValue; - await OnChange(); + if (ConfirmedValue != UnconfirmedValue) + { + ConfirmedValue = UnconfirmedValue; + await OnChange(); + } await ClosePopup(); await FocusAsync(); } From c027425e4eefb3e1075105aba39bb25eceef2c73 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:03:47 +0100 Subject: [PATCH 38/42] revert unintended changes --- RadzenBlazorDemos.Host/Properties/launchSettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RadzenBlazorDemos.Host/Properties/launchSettings.json b/RadzenBlazorDemos.Host/Properties/launchSettings.json index e148b4bdf17..5b0bd98e948 100644 --- a/RadzenBlazorDemos.Host/Properties/launchSettings.json +++ b/RadzenBlazorDemos.Host/Properties/launchSettings.json @@ -22,7 +22,7 @@ "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + } } } -} \ No newline at end of file +} From 9184cfa427f1981aa76e5ab7cd14cc42c575a4d0 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Fri, 7 Mar 2025 18:29:03 +0100 Subject: [PATCH 39/42] TimeSpanPicker, DatePicker: fix icon positions in FormFields having filled and flat variants --- .../themes/components/blazor/_form-field.scss | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Radzen.Blazor/themes/components/blazor/_form-field.scss b/Radzen.Blazor/themes/components/blazor/_form-field.scss index ffce6e14f89..a9514df340d 100644 --- a/Radzen.Blazor/themes/components/blazor/_form-field.scss +++ b/Radzen.Blazor/themes/components/blazor/_form-field.scss @@ -125,7 +125,7 @@ $form-field-helper-padding: 0 0.5rem !default; .rz-form-field-start { padding-inline-end: 0; } - + .rz-form-field-end { padding-inline-start: 0; } @@ -156,12 +156,17 @@ $form-field-helper-padding: 0 0.5rem !default; .rz-textarea { margin-top: 1rem; - } - .rz-datepicker-trigger { + .rz-datepicker-trigger, + .rz-timespanpicker-trigger { top: calc(50% + 0.4375rem); } + + .rz-datepicker .rz-dropdown-clear-icon, + .rz-timespanpicker .rz-dropdown-clear-icon { + top: 0.4375rem; + } } } @@ -173,7 +178,7 @@ $form-field-helper-padding: 0 0.5rem !default; box-shadow: var(--rz-input-hover-shadow); } } - + &.rz-state-focused { .rz-form-field-content { border: var(--rz-input-focus-border); @@ -203,7 +208,7 @@ $form-field-helper-padding: 0 0.5rem !default; background-color: var(--rz-form-field-filled-hover-background-color); } } - + &.rz-state-focused, &.rz-state-focused:hover { .rz-form-field-content { @@ -238,7 +243,7 @@ $form-field-helper-padding: 0 0.5rem !default; .rz-form-field-start { padding-inline-start: 0; } - + .rz-form-field-end { padding-inline-end: 0; } @@ -262,7 +267,7 @@ $form-field-helper-padding: 0 0.5rem !default; inset-inline-end var(--rz-transition), border-width var(--rz-transition); } - + &:after { content: ""; position: absolute; @@ -272,7 +277,7 @@ $form-field-helper-padding: 0 0.5rem !default; border-bottom: var(--rz-input-border); } } - + &:hover { .rz-form-field-content:after { border-bottom: var(--rz-input-hover-border); @@ -285,7 +290,7 @@ $form-field-helper-padding: 0 0.5rem !default; border-bottom: var(--rz-input-disabled-border); } } - + &.rz-state-focused:not(.rz-state-disabled) { .rz-form-field-content:before { inset-inline-start: calc(-1 * var(--rz-border-width)); @@ -304,7 +309,6 @@ $form-field-helper-padding: 0 0.5rem !default; .rz-numeric:focus-within .rz-numeric-button { display: block; } - } .rz-form-field-label { From 597bac4b1354418143ff998ebdbb58df97e6f8f1 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:14:50 +0100 Subject: [PATCH 40/42] TimeSpanPicker: make AllowClear false by default, like in DatePicker --- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index 133e9daf788..f91a59df38e 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -66,14 +66,14 @@ public TValue Value #region Parameters: input field config /// /// Specifies whether the value can be cleared. - /// Set to true by default if is nullable, false otherwise. /// /// true if value can be cleared; otherwise, false. [Parameter] - public bool AllowClear { get; set; } = _isNullable; + public bool AllowClear { get; set; } /// /// Specifies whether input in the input field is allowed. + /// Set to true by default. /// /// true if input is allowed; otherwise, false. [Parameter] From 7427fceab12f694e7b61802a2db5cc3f7ddd9e78 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:16:32 +0100 Subject: [PATCH 41/42] revert unintended change in demos project --- RadzenBlazorDemos/RadzenBlazorDemos.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/RadzenBlazorDemos/RadzenBlazorDemos.csproj b/RadzenBlazorDemos/RadzenBlazorDemos.csproj index ff9b64202e4..d29ec76f91c 100644 --- a/RadzenBlazorDemos/RadzenBlazorDemos.csproj +++ b/RadzenBlazorDemos/RadzenBlazorDemos.csproj @@ -21,9 +21,6 @@ true - - true - true From 888c4c8db9054ed7bf35bd88595d6ce30edbff92 Mon Sep 17 00:00:00 2001 From: Cosmatevs <44393502+Cosmatevs@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:16:10 +0100 Subject: [PATCH 42/42] * remove PopupOrInline component * revert changes in Popup component * TimeSpanPicker: reimplement inline/popup handling --- Radzen.Blazor/RadzenTimeSpanPicker.razor | 119 +++++++++++--------- Radzen.Blazor/RadzenTimeSpanPicker.razor.cs | 7 +- Radzen.Blazor/Rendering/Popup.razor | 14 +-- Radzen.Blazor/Rendering/PopupOrInline.razor | 54 --------- 4 files changed, 76 insertions(+), 118 deletions(-) delete mode 100644 Radzen.Blazor/Rendering/PopupOrInline.razor diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor b/Radzen.Blazor/RadzenTimeSpanPicker.razor index 1c443ea9ace..c4885f9c07a 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor @@ -15,14 +15,18 @@ }
- @if (!Inline) + @if (Inline) + { + @RenderPanel() + } + else { + disabled="@Disabled" readonly="@IsInputAllowed" autocomplete="off" class="@GetInputClass()" + placeholder="@Placeholder" tabindex="@(Disabled ? "-1" : TabIndex.ToString())" @attributes="@InputAttributes" + @onchange="@(args => SetValueFromInput(args.Value?.ToString()))" + @onclick="@(() => ClickInputField())" @onkeydown="@PressKey" + @onkeydown:preventDefault="@_preventKeyPress" @onkeydown:stopPropagation /> @if (ShowPopupButton) {
- -@code { - internal RenderFragment RenderUnitField(TimeSpanUnit unit, string label, string unitSymbol, int value, string step, string valuePaddingFormat, Func change) + private RenderFragment RenderUnitField(TimeSpanUnit unit, string label, string unitSymbol, int value, string step, string valuePaddingFormat, Func change) { return __builder => { diff --git a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs index f91a59df38e..1c6b232e756 100644 --- a/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs +++ b/Radzen.Blazor/RadzenTimeSpanPicker.razor.cs @@ -495,6 +495,7 @@ public override async Task SetParametersAsync(ParameterView parameters) var shouldClose = parameters.DidParameterChange(nameof(Visible), Visible) && parameters.GetValueOrDefault(nameof(Visible)) is false + || parameters.DidParameterChange(nameof(Inline), Inline) && parameters.GetValueOrDefault(nameof(Inline)) || parameters.DidParameterChange(nameof(Disabled), Disabled) && parameters.GetValueOrDefault(nameof(Disabled)) || parameters.DidParameterChange(nameof(ReadOnly), ReadOnly) && parameters.GetValueOrDefault(nameof(ReadOnly)); @@ -693,13 +694,13 @@ private async Task PressKey(KeyboardEventArgs args) #endregion #region Internal: popup general actions - private PopupOrInline _popupHolder; + private Popup _popup; private Task TogglePopup() - => Inline ? Task.CompletedTask : _popupHolder.Popup?.ToggleAsync(Element) ?? Task.CompletedTask; + => Inline ? Task.CompletedTask : _popup?.ToggleAsync(Element) ?? Task.CompletedTask; private Task ClosePopup() - => Inline ? Task.CompletedTask : _popupHolder.Popup?.CloseAsync(Element) ?? Task.CompletedTask; + => Inline ? Task.CompletedTask : _popup?.CloseAsync(Element) ?? Task.CompletedTask; private async Task PopupKeyDown(KeyboardEventArgs args) { diff --git a/Radzen.Blazor/Rendering/Popup.razor b/Radzen.Blazor/Rendering/Popup.razor index d4fc4234af2..6b370ce1a72 100644 --- a/Radzen.Blazor/Rendering/Popup.razor +++ b/Radzen.Blazor/Rendering/Popup.razor @@ -1,12 +1,14 @@ @inherits RadzenComponent @using Microsoft.JSInterop
- @if (IsOpen || !Lazy) + @if (open || !Lazy) { @ChildContent }
@code { + bool open; + [Parameter] public bool Lazy { get; set; } @@ -28,14 +30,12 @@ [Parameter] public EventCallback Close { get; set; } - public bool IsOpen { get; private set; } - public async Task ToggleAsync(ElementReference target, bool disableSmartPosition = false) { - IsOpen = !IsOpen; + open = !open; this.target = target; - if (IsOpen) + if (open) { await Open.InvokeAsync(null); await JSRuntime.InvokeVoidAsync("Radzen.openPopup", target, GetId(), false, null, null, null, Reference, nameof(OnClose), true, AutoFocusFirstElement, disableSmartPosition); @@ -48,7 +48,7 @@ public async Task CloseAsync(ElementReference target) { - IsOpen = false; + open = false; this.target = target; await CloseAsync(); @@ -59,7 +59,7 @@ [JSInvokable] public async Task OnClose() { - IsOpen = false; + open = false; await Close.InvokeAsync(null); } diff --git a/Radzen.Blazor/Rendering/PopupOrInline.razor b/Radzen.Blazor/Rendering/PopupOrInline.razor deleted file mode 100644 index 6d512ec6a6f..00000000000 --- a/Radzen.Blazor/Rendering/PopupOrInline.razor +++ /dev/null @@ -1,54 +0,0 @@ -@inherits RadzenComponent - -@if (!Visible) -{ - return; -} - -@if (Inline) -{ - @ChildContent -} -else -{ - - @ChildContent - -} - -@code { - [Parameter] - public RenderFragment ChildContent { get; set; } - - [Parameter] - public bool Inline { get; set; } - - [Parameter] - public bool PopupLazy { get; set; } - - [Parameter] - public bool PopupAutoFocusFirstElement { get; set; } = true; - - [Parameter] - public bool PopupPreventDefault { get; set; } - - [Parameter] - public EventCallback PopupOpen { get; set; } - - [Parameter] - public EventCallback PopupClose { get; set; } - - public Popup Popup { get; private set; } - - public override async Task SetParametersAsync(ParameterView parameters) - { - if (Popup?.IsOpen is true && parameters.DidParameterChange(nameof(Inline), Inline) && parameters.GetValueOrDefault(nameof(Inline)) is true) - { - await Popup.CloseAsync(); - } - - await base.SetParametersAsync(parameters); - } -}