diff --git a/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs b/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs index daf3008f..da2ffbe2 100644 --- a/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs +++ b/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs @@ -32,6 +32,7 @@ public class NavigationStore .Add( new( nameof( LumexChip ), PageStatus.New ) ) .Add( new( nameof( LumexCollapse ) ) ) .Add( new( nameof( LumexDataGrid ) ) ) + .Add( new( nameof( LumexDatebox ), PageStatus.New ) ) .Add( new( nameof( LumexDivider ) ) ) .Add( new( nameof( LumexDropdown ) ) ) .Add( new( nameof( LumexLink ) ) ) @@ -67,6 +68,7 @@ public class NavigationStore .Add( new( nameof( LumexComponent ) ) ) //.Add( nameof( LumexComponentBase ) ) //.Add( nameof( LumexDebouncedInputBase ) ) + .Add( new( nameof( LumexDatebox ) ) ) .Add( new( nameof( LumexDivider ) ) ) .Add( new( nameof( LumexDropdown ) ) ) .Add( new( nameof( LumexDropdownItem ) ) ) diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Datebox.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Datebox.razor new file mode 100644 index 00000000..9be06e22 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Datebox.razor @@ -0,0 +1,217 @@ +@page "/docs/components/datebox" +@layout DocsContentLayout + +@using LumexUI.Docs.Client.Pages.Components.Datebox.PreviewCodes + + +

The date box component provides a simple way to input and edit dates.

+ + + + +

+ You can disable a date box to prevent user interaction. + A disabled date box is faded and does not respond to user clicks. +

+ + +
+ + +

+ Set the date box component to read-only to display the + current state without allowing changes. +

+ + +
+ + +

+ Mark the date box as required to indicate that it + must be filled out before form submission. + An asterisk will appear at the end of the label. +

+ + +
+ + +

+ The date box component supports multiple sizes to + fit different layouts and design needs. +

+ + +
+ + +

Adjust the border radius of the date box for rounded or squared corners.

+ + +
+ + +

Customize the date box color to match your theme or emphasize certain fields.

+ + +
+ + +

+ Choose from different date box variants, + such as Flat, Underlined, + or Outlined, for various visual styles. +

+ + +
+ + +

+ Position the label either inside or outside the date box + for flexibility in layout and design. +

+ + +
+ + + + If the Label parameter is not set, the LabelPlacement + parameter will default to Outside. + + + + +

Enable the button to clear the input with a single click.

+ + +
+ + +

+ Add custom content, such as icons or labels, to the + start or end of the date box for added functionality. +

+ + +
+ + +

+ Provide a brief description below the date box to + offer guidance or additional context. +

+ + +
+ + +

+ Display an error message below the date box to indicate validation issues. + You can combine the Invalid and ErrorMessage parameters to show an invalid input. + An error message is shown only when the Invalid parameter is set to true. +

+ + +
+ + +

+ Enable debounced input to delay updates to the date box value, + reducing the frequency of changes and improving performance. + You can achieve this by setting the DebounceDelay value and + Behavior to OnInput. +

+ + +
+ + +

+ The date box component supports two-way data binding, + allowing you to programmatically control the value. + You can achieve this using the @@bind-Value directive, + or the Value and ValueChanged parameters. +

+ + +
+
+ + +
+

Date box

+ +
    +
  • + Class: The CSS class name that styles the wrapper of the date box. +
  • + +
  • + Classes: The CSS class names for the date box slots that style the entire component. +
  • +
+
+ +
+ + + +@code { + [CascadingParameter] + private DocsContentLayout Layout { get; set; } = default!; + + private readonly Heading[] _headings = new Heading[] + { + new("Usage", [ + new("Disabled"), + new("Read-Only"), + new("Required"), + new("Sizes"), + new("Radius"), + new("Colors"), + new("Variants"), + new("Label Placements"), + new("Clear Button"), + new("Start & End Content"), + new("Description"), + new("Error message"), + new("Debounce Delay"), + new("Two-way Data Binding"), + ]), + new("Custom Styles"), + new("API") + }; + + private readonly Slot[] _slots = new Slot[] + { + new(nameof(InputFieldSlots.Base), "The overall wrapper."), + new(nameof(InputFieldSlots.Label), "The label element."), + new(nameof(InputFieldSlots.MainWrapper), "The wrapper of the input wrapper (when the label is outside)."), + new(nameof(InputFieldSlots.InputWrapper), "The wrapper of the label and the inner wrapper (when the label is inside)."), + new(nameof(InputFieldSlots.InnerWrapper), "The wrapper of the input, start/end content."), + new(nameof(InputFieldSlots.Input), "The input element."), + new(nameof(InputFieldSlots.ClearButton), "The clear button element."), + new(nameof(InputFieldSlots.HelperWrapper), "The wrapper of a description and an error message."), + new(nameof(InputFieldSlots.Description), "The description of the input field."), + new(nameof(InputFieldSlots.ErrorMessage), "The error message of the input field.") + }; + + private readonly string[] _apiComponents = new string[] + { + nameof(LumexDatebox) + }; + + protected override void OnInitialized() + { + Layout.Initialize( + title: "Date box", + category: "Components", + description: "Date box allows users to input a specific date.", + headings: _headings, + linksProps: new ComponentLinksProps("Datebox", isServer: false) + ); + } +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ClearButton.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ClearButton.razor new file mode 100644 index 00000000..4beef3e1 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ClearButton.razor @@ -0,0 +1,28 @@ +
+ +
+ +

+ @_text +

+ +@code { + private string? _text; + private DateTime? _value; + + private void Notify() + { + _value = null; + + if (_value is null) + { + _text = "Input is cleared!"; + } + } +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Colors.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Colors.razor new file mode 100644 index 00000000..5df7a1fc --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Colors.razor @@ -0,0 +1,27 @@ +
+ @foreach (ThemeColor color in _colors) + { +
+ + + + @color + +
+ } +
+ +@code { + private ThemeColor[] _colors = [ + ThemeColor.Default, + ThemeColor.Primary, + ThemeColor.Secondary, + ThemeColor.Success, + ThemeColor.Warning, + ThemeColor.Danger, + ThemeColor.Info + ]; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/CustomStyles.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/CustomStyles.razor new file mode 100644 index 00000000..b7160a10 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/CustomStyles.razor @@ -0,0 +1,30 @@ + + + + + + +@code { + private InputFieldSlots _classes = new() + { + Label = "text-default-700", + InnerWrapper = "bg-transparent", + InputWrapper = ElementClass.Empty() + .Add("shadow-xl") + .Add("bg-default-200/50") + .Add("backdrop-blur-xl") + .Add("backdrop-saturate-200") + .Add("hover:bg-default-200/70") + .Add("group-data-[focus=true]:bg-default-200/85") + .ToString(), + Input = ElementClass.Empty() + .Add("bg-transparent") + .Add("text-default-900") + .Add("placeholder:text-default-500") + .ToString() + }; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/DebounceDelay.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/DebounceDelay.razor new file mode 100644 index 00000000..d086d855 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/DebounceDelay.razor @@ -0,0 +1,17 @@ +
+ + +

+ Value: @_value +

+
+ +@code { + private DateTime? _value; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Description.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Description.razor new file mode 100644 index 00000000..5b3b7428 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Description.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Disabled.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Disabled.razor new file mode 100644 index 00000000..b3b967c0 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Disabled.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ErrorMessage.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ErrorMessage.razor new file mode 100644 index 00000000..3317069b --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ErrorMessage.razor @@ -0,0 +1,63 @@ +@using FluentValidation +@using FluentValidation.Results + + + +@code { + private User _user = new(); + private UserValidator _userValidator = new(); + + protected override void OnInitialized() + { + _user.Date = DateTime.UtcNow; + Validate(); + } + + private void OnDateChange(DateTime? value) + { + _user.Date = value; + Validate(); + } + + private void Validate() + { + ValidationResult result = _userValidator.Validate(_user); + + if (!result.IsValid) + { + _userValidator.DateErrorMessage = result.Errors + .Where(failure => failure.PropertyName == nameof(User.Date)) + .Select(failure => failure.ErrorMessage) + .FirstOrDefault(); + } + else + { + _userValidator.DateErrorMessage = null; + } + } + + public class User + { + public DateTime? Date { get; set; } + } + + public class UserValidator : AbstractValidator + { + public string? DateErrorMessage { get; set; } + + public UserValidator() + { + RuleFor(user => user.Date) + .NotEmpty() + .WithMessage("Date is required"); + } + } +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/LabelPlacements.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/LabelPlacements.razor new file mode 100644 index 00000000..a1d063fa --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/LabelPlacements.razor @@ -0,0 +1,26 @@ +
+ @foreach (LabelPlacement placement in _labelPlacements) + { +
+
+ + + +
+ + + @placement + +
+ } +
+ +@code { + private LabelPlacement[] _labelPlacements = [ + LabelPlacement.Inside, + LabelPlacement.Outside + ]; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ReadOnly.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ReadOnly.razor new file mode 100644 index 00000000..096cbb73 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ReadOnly.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Required.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Required.razor new file mode 100644 index 00000000..e1ae4d85 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Required.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Sizes.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Sizes.razor new file mode 100644 index 00000000..937cdbcb --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Sizes.razor @@ -0,0 +1,25 @@ +
+ @foreach (Size size in _sizes) + { +
+
+ + + +
+ + @size +
+ } +
+ +@code { + private Size[] _sizes = [ + Size.Small, + Size.Medium, + Size.Large + ]; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/StartEndContent.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/StartEndContent.razor new file mode 100644 index 00000000..92912383 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/StartEndContent.razor @@ -0,0 +1,100 @@ +
+
+
+
+ + + + + +
+ + + Start + +
+
+ +
+
+
+ + + + + +
+ + + End + +
+
+ +
+
+
+ + + + + +
+ + + Both + +
+
+
+ +@code { + private RenderFragment _mailIcon = @; + private RenderFragment _searchIcon = @; + private RenderFragment _micIcon = @; + + private RenderFragment _protocolContent = + @
+ https:// +
; + + private RenderFragment _domainContent = + @
+ .org +
; + + private RenderFragment _emailDomainContent = + @
+ @doe.com +
; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/TwoWayDataBinding.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/TwoWayDataBinding.razor new file mode 100644 index 00000000..f460713d --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/TwoWayDataBinding.razor @@ -0,0 +1,32 @@ +
+
+ +

+ Value: @_valueOne +

+
+ +
+ +

+ Value: @_valueTwo +

+
+
+ +@code { + private DateTime? _valueOne; + private DateTime? _valueTwo = DateTime.UtcNow; + + private void OnValueChanged(DateTime? value) + { + _valueTwo = value; + } +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Usage.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Usage.razor new file mode 100644 index 00000000..892e7fc4 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Usage.razor @@ -0,0 +1,6 @@ +
+ + + +
\ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Variants.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Variants.razor new file mode 100644 index 00000000..e68072c4 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Variants.razor @@ -0,0 +1,27 @@ +
+ @foreach (InputVariant variant in _variants) + { +
+
+ + + +
+ + + @variant + +
+ } +
+ +@code { + private InputVariant[] _variants = [ + InputVariant.Flat, + InputVariant.Outlined, + InputVariant.Underlined + ]; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/_Radius.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/_Radius.razor new file mode 100644 index 00000000..972c3464 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/_Radius.razor @@ -0,0 +1,23 @@ +
+ @foreach (Radius radius in _radiuses) + { +
+ + + + @radius + +
+ } +
+ +@code { + private Radius[] _radiuses = [ + Radius.None, + Radius.Small, + Radius.Medium, + Radius.Large + ]; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ClearButton.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ClearButton.razor new file mode 100644 index 00000000..a0052c9c --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ClearButton.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Colors.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Colors.razor new file mode 100644 index 00000000..2c1ffff2 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Colors.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/CustomStyles.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/CustomStyles.razor new file mode 100644 index 00000000..915191af --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/CustomStyles.razor @@ -0,0 +1,14 @@ +@rendermode InteractiveWebAssembly + + + + + +@code { + private Preview.Slots _classes = new() + { + Background = "bg-gradient-to-tr from-indigo-200 to-violet-300 [mask-image:none]", + Preview = "justify-center" + }; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/DebounceDelay.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/DebounceDelay.razor new file mode 100644 index 00000000..293b329d --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/DebounceDelay.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Description.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Description.razor new file mode 100644 index 00000000..962aa713 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Description.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Disabled.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Disabled.razor new file mode 100644 index 00000000..314e3e9d --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Disabled.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ErrorMessage.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ErrorMessage.razor new file mode 100644 index 00000000..1f53195b --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ErrorMessage.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/LabelPlacements.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/LabelPlacements.razor new file mode 100644 index 00000000..d80b407d --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/LabelPlacements.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Radius.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Radius.razor new file mode 100644 index 00000000..e140ec05 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Radius.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ReadOnly.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ReadOnly.razor new file mode 100644 index 00000000..8857b959 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/ReadOnly.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Required.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Required.razor new file mode 100644 index 00000000..09284af1 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Required.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Sizes.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Sizes.razor new file mode 100644 index 00000000..7ee671ba --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Sizes.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/StartEndContent.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/StartEndContent.razor new file mode 100644 index 00000000..06102913 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/StartEndContent.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/TwoWayDataBinding.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/TwoWayDataBinding.razor new file mode 100644 index 00000000..6e1d79e2 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/TwoWayDataBinding.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Usage.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Usage.razor new file mode 100644 index 00000000..7f1d77e0 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Usage.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Variants.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Variants.razor new file mode 100644 index 00000000..0ce12f96 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/PreviewCodes/Variants.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + \ No newline at end of file diff --git a/src/LumexUI/Common/Enums/InputDateboxType.cs b/src/LumexUI/Common/Enums/InputDateboxType.cs new file mode 100644 index 00000000..bdcbd18e --- /dev/null +++ b/src/LumexUI/Common/Enums/InputDateboxType.cs @@ -0,0 +1,19 @@ +// Copyright (c) LumexUI 2024 +// LumexUI licenses this file to you under the MIT license +// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE + +using System.ComponentModel; + +namespace LumexUI.Common; + +/// +/// Specifies the type of the . +/// +public enum InputDateboxType +{ + /// + /// A date box sub-type for selecting a date + /// + [Description( "date" )] + Date +} \ No newline at end of file diff --git a/src/LumexUI/Components/Datebox/LumexDatebox.razor b/src/LumexUI/Components/Datebox/LumexDatebox.razor new file mode 100644 index 00000000..c82f7b2d --- /dev/null +++ b/src/LumexUI/Components/Datebox/LumexDatebox.razor @@ -0,0 +1,6 @@ +@namespace LumexUI +@inherits LumexInputFieldBase + +@{ + base.BuildRenderTree(__builder); +} \ No newline at end of file diff --git a/src/LumexUI/Components/Datebox/LumexDatebox.razor.cs b/src/LumexUI/Components/Datebox/LumexDatebox.razor.cs new file mode 100644 index 00000000..c974337b --- /dev/null +++ b/src/LumexUI/Components/Datebox/LumexDatebox.razor.cs @@ -0,0 +1,88 @@ +// Copyright (c) LumexUI 2024 +// LumexUI licenses this file to you under the MIT license +// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE + +using LumexUI.Common; +using System.Globalization; +using Microsoft.AspNetCore.Components; + +namespace LumexUI; + +/// +/// A component that represents an input field for entering values. +/// +public partial class LumexDatebox : LumexInputFieldBase +{ + private const string DateFormat = "yyyy-MM-dd"; + private const string DefaultParsingErrorMessage = "The {0} field must be a date."; + + /// + /// Gets or sets the date box type of the. + /// + /// + /// The default value is + /// + [Parameter] + public InputDateboxType DateboxType { get; set; } = InputDateboxType.Date; + + /// + /// Optional custom parsing error message + /// + [Parameter] + public string? ParsingErrorMessage { get; set; } + + private string _parsingErrorText = DefaultParsingErrorMessage; + + /// + protected override void OnParametersSet() + { + if( DateboxType != InputDateboxType.Date ) + { + throw new InvalidOperationException( + $"LumexDatebox does not currently support {DateboxType}. Only Date is implemented." ); + } + + SetInputType( "date" ); + + _parsingErrorText = string.IsNullOrWhiteSpace( ParsingErrorMessage ) + ? DefaultParsingErrorMessage + : ParsingErrorMessage!; + + if( !Disabled && !ReadOnly ) + { + Focused = true; + } + + base.OnParametersSet(); + } + + /// + protected override bool TryParseValueFromString( string? value, out DateTime? result ) + { + if( string.IsNullOrWhiteSpace( value ) ) + { + result = null; + return true; + } + + if( DateTime.TryParseExact( + value, + DateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out DateTime dt ) ) + { + result = dt; + return true; + } + + result = null; + return false; + } + + /// + protected override string FormatValueAsString( DateTime? value ) + => value.HasValue + ? value.Value.ToString( DateFormat, CultureInfo.InvariantCulture ) + : string.Empty; +} \ No newline at end of file diff --git a/tests/LumexUI.Tests/Components/Bases/InputBaseTests.cs b/tests/LumexUI.Tests/Components/Bases/InputBaseTests.cs index 77de8cac..b85d40ac 100644 --- a/tests/LumexUI.Tests/Components/Bases/InputBaseTests.cs +++ b/tests/LumexUI.Tests/Components/Bases/InputBaseTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) LumexUI 2024 +// Copyright (c) LumexUI 2024 // LumexUI licenses this file to you under the MIT license // See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE @@ -81,7 +81,7 @@ public async Task InputBase_WithValue_ShouldSupplyCurrentValueAsString() public void InputBase_WithValue_ShouldSupplyCurrentValueAsStringWithFormatting() { var model = new TestModel(); - var cut = RenderComponent( p => p + var cut = RenderComponent( p => p .Add( p => p.Value, new DateTime( 1915, 3, 2 ) ) .Add( p => p.ValueExpression, () => model.DateProperty ) ); @@ -93,7 +93,7 @@ public void InputBase_WithValue_ShouldSupplyCurrentValueAsStringWithFormatting() public async Task InputBase_WhenChangedValid_ShouldParseCurrentValueAsString() { var model = new TestModel(); - var cut = RenderComponent( p => p + var cut = RenderComponent( p => p .Add( p => p.Value, new DateTime( 1915, 3, 2 ) ) .Add( p => p.ValueExpression, () => model.DateProperty ) ); @@ -110,7 +110,7 @@ public async Task InputBase_WhenChangedValid_ShouldParseCurrentValueAsString() public async Task InputBase_WhenChangedInvalid_ShouldNotParseCurrentValueAsString() { var model = new TestModel(); - var cut = RenderComponent( p => p + var cut = RenderComponent( p => p .Add( p => p.Value, new DateTime( 1915, 3, 2 ) ) .Add( p => p.ValueExpression, () => model.DateProperty ) ); @@ -197,7 +197,7 @@ protected override bool TryParseValueFromString( string? value, out T result ) } } - private class TestDateInputComponent : TestInputComponent + private class TestDateboxComponent : TestInputComponent { protected override string FormatValueAsString( DateTime value ) => value.ToString( "yyyy/MM/dd", CultureInfo.InvariantCulture ); diff --git a/tests/LumexUI.Tests/Components/Datebox/DateboxTests.razor b/tests/LumexUI.Tests/Components/Datebox/DateboxTests.razor new file mode 100644 index 00000000..9dd18344 --- /dev/null +++ b/tests/LumexUI.Tests/Components/Datebox/DateboxTests.razor @@ -0,0 +1,307 @@ +@namespace LumexUI.Tests.Components +@inherits TestContext + +@using AngleSharp.Dom +@using LumexUI.Common +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.Extensions.DependencyInjection +@using TailwindMerge + +@code { + public DateboxTests() + { + Services.AddSingleton(); + + BunitJSModuleInterop module = JSInterop.SetupModule("./_content/LumexUI/js/components/input.js"); + module.Setup("input.getValidationMessage", _ => true); + } + + [Fact] + public void ShouldRenderCorrectly() + { + Func action = () => Render(@); + + action.Should().NotThrow(); + } + + [Fact] + public void ShouldRenderMainWrapperWhenLabelOutside() + { + IRenderedFragment cut = Render( + @ + ); + + cut.Find("[data-slot=main-wrapper]").Should().NotBeNull(); + } + + [Fact] + public void ShouldRenderHelperWrapperWhenDescriptionProvided() + { + IRenderedFragment cut = Render( + @ + ); + + cut.Find("[data-slot=helper-wrapper]").Should().NotBeNull(); + cut.Find("[data-slot=description]").Should().NotBeNull(); + } + + [Fact] + public void ShouldRenderHelperWrapperWhenErrorMessageProvided() + { + IRenderedFragment cut = Render( + @ + ); + + Func action = () => cut.Find("[data-slot=error-message]"); + + cut.Find("[data-slot=helper-wrapper]").Should().NotBeNull(); + + action.Should().Throw + ( + because: "Error message should be rendered when state is invalid." + ); + } + + [Fact] + public void ShouldRenderErrorMessageWhenInvalid() + { + IRenderedFragment cut = Render( + @ + ); + + cut.Find("[data-slot=error-message]").Should().NotBeNull(); + } + + [Fact] + public void ShouldHaveDisabledAttributeWhenDisabled() + { + IRenderedFragment cut = Render( + @ + ); + + IElement input = cut.Find("input"); + input.HasAttribute("disabled").Should().BeTrue(); + } + + [Fact] + public void ShouldHaveCorrectTypeAttribute() + { + IRenderedFragment cut = Render(@); + + IElement input = cut.Find("input"); + input.HasAttribute("type").Should().BeTrue(); + input.GetAttribute("type").Should().Be("date"); + } + + [Fact] + public void ShouldRenderClearButtonWhenClearableAndHasValue() + { + IRenderedFragment cut = Render( + @ + ); + + IElement clearButton = cut.Find("[role=button]"); + clearButton.Should().NotBeNull(); + } + + [Fact] + public void ShouldClearValueOnClickWhenClearable() + { + IRenderedComponent cut = Render( + @ + ); + + IElement clearButton = cut.Find("[role=button]"); + clearButton.Click(); + + cut.Instance.Value.Should().BeNull(); + } + + [Theory] + [InlineData("Enter")] + [InlineData("Space")] + public void ShouldClearValueOnlyWithEnterOrSpaceWhenClearable(string code) + { + DateTime now = DateTime.UtcNow; + + IRenderedComponent cut = Render( + @ + ); + + IElement clearButton = cut.Find("[role=button]"); + clearButton.KeyUp(new KeyboardEventArgs() { Code = "Esc" }); + + cut.Instance.Value.Should().Be(now); + + clearButton.KeyUp(new KeyboardEventArgs() { Code = code }); + + cut.Instance.Value.Should().BeNull(); + } + + [Fact] + public void ShouldTriggerOnClearedCallbackOnClear() + { + bool isCleared = false; + + IRenderedComponent cut = Render( + @ + ); + + IElement clearButton = cut.Find("[role=button]"); + clearButton.Click(); + + isCleared.Should().BeTrue(); + + cut.Instance.Value.Should().BeNull(); + } + + [Fact] + public void ShouldFocusInputOnInputWrapperClick() + { + IRenderedFragment cut = Render(@); + + IElement baseWrapper = cut.Find("[data-slot=base]"); + IElement inputWrapper = cut.Find("[data-slot=input-wrapper]"); + inputWrapper.Click(); + baseWrapper.GetAttribute("data-focus").Should().Be("true", because: "Internal `Focused` flag is true."); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void ShouldNotFocusInputWhenDisabledOrReadonly(bool disabled, bool @readonly) + { + IRenderedFragment cut = Render( + @ + ); + + IElement baseWrapper = cut.Find("[data-slot=base]"); + IElement inputWrapper = cut.Find("[data-slot=input-wrapper]"); + inputWrapper.Click(); + baseWrapper.GetAttribute("data-focus").Should().Be("false", because: "Internal `Focused` flag is false."); + } + + [Fact] + public void ShouldChangeValueUsingInputEventWhenBehaviorOnInput() + { + IRenderedComponent cut = Render( + @ + ); + + IElement input = cut.Find("input"); + + DateTime utcNow = DateTime.UtcNow; + DateTime expectedUtcNow = new DateTime(utcNow.Ticks - (utcNow.Ticks % TimeSpan.TicksPerSecond), utcNow.Kind); + + input.Input(utcNow); + cut.Instance.Value.Should().Be(expectedUtcNow); + + DateTime now = DateTime.Now; + DateTime expectedNow = new DateTime(now.ToUniversalTime().Ticks - (now.ToUniversalTime().Ticks % TimeSpan.TicksPerSecond), DateTimeKind.Utc); + + input.Change(now); + + cut.Instance.Value.Should().Be(expectedNow); + } + + [Fact] + public void ShouldChangeValueUsingChangeEventWhenBehaviorOnChange() + { + DateTime utcNow = DateTime.UtcNow; + DateTime now = DateTime.Now; + + IRenderedComponent cut = Render( + @ + ); + + IElement input = cut.Find("input"); + + input.Change(utcNow); + + DateTime expectedUtcNow = new DateTime(utcNow.Ticks - (utcNow.Ticks % TimeSpan.TicksPerSecond), utcNow.Kind); + + cut.Instance.Value.Should().Be(expectedUtcNow); + + DateTime expectedNow = new DateTime(now.ToUniversalTime().Ticks - (now.ToUniversalTime().Ticks % TimeSpan.TicksPerSecond), DateTimeKind.Utc); + + input.Input(now); + + cut.Instance.Value.Should().Be(expectedNow); + } + + [Fact] + public void ShouldChangeValueImmediatelyWhenBehaviorOnInput() + { + DateTime date = DateTime.UtcNow; + + IRenderedComponent cut = Render( + @ + ); + + IElement input = cut.Find("input"); + input.Input(date); + + DateTime expectedDate = new DateTime(date.Ticks - (date.Ticks % TimeSpan.TicksPerSecond), date.Kind); + + cut.Instance.Value.Should().Be(expectedDate, "Debounce delay is 0ms."); + } + + [Fact] + public async Task ShouldChangeValueAfterDebounceDelayWhenBehaviorOnInput() + { + IRenderedComponent cut = Render( + @ + ); + + IElement input = cut.Find("input"); + + DateTime now = DateTime.Now; + DateTime expectedNow = new DateTime(now.Ticks - (now.Ticks % TimeSpan.TicksPerSecond), now.Kind); + + input.Input(now); + + cut.Instance.Value.Should().BeNull(because: "Elapsed time (0ms) < 200ms."); + + await Task.Delay(100); + + cut.Instance.Value.Should().BeNull(because: "Elapsed time (100ms) < 200ms."); + + await Task.Delay(150); + + cut.WaitForAssertion(() => cut.Instance.Value.Should().Be(expectedNow, because: "Elapsed time (250ms) > 200ms.")); + } + + [Fact] + public void ShouldThrowWithDebounceDelayAndOnChangeBehavior() + { + Func action = () => Render( + @ + ); + + action.Should().Throw(); + } +} \ No newline at end of file