diff --git a/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs b/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs index 32063921..69ec0edc 100644 --- a/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs +++ b/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs @@ -31,6 +31,7 @@ public class NavigationStore .Add( new( nameof( LumexChip ), ComponentStatus.New ) ) .Add( new( nameof( LumexCollapse ) ) ) .Add( new( nameof( LumexDataGrid ) ) ) + .Add( new( nameof( LumexDateInput ) ) ) .Add( new( nameof( LumexDivider ) ) ) .Add( new( nameof( LumexDropdown ) ) ) .Add( new( nameof( LumexIcon ) ) ) @@ -66,6 +67,7 @@ public class NavigationStore .Add( new( nameof( LumexComponent ) ) ) //.Add( nameof( LumexComponentBase ) ) //.Add( nameof( LumexDebouncedInputBase ) ) + .Add( new( nameof( LumexDateInput ) ) ) .Add( new( nameof( LumexDivider ) ) ) .Add( new( nameof( LumexDropdown ) ) ) .Add( new( nameof( LumexDropdownItem ) ) ) diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/DateInput.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/DateInput.razor new file mode 100644 index 00000000..fa4366af --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/DateInput.razor @@ -0,0 +1,216 @@ +@page "/docs/components/date-input" +@layout DocsContentLayout + +@using LumexUI.Docs.Client.Pages.Components.DateInput.PreviewCodes + + +

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

+ + + + +

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

+ + +
+ + +

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

+ + +
+ + +

+ Mark the date input 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 input component supports multiple sizes to + fit different layouts and design needs. +

+ + +
+ + +

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

+ + +
+ + +

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

+ + +
+ + +

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

+ + +
+ + +

+ Position the label either inside or outside the date input + 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 input for added functionality. +

+ + +
+ + +

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

+ + +
+ + +

+ Display an error message below the date input 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 input value, + reducing the frequency of changes and improving performance. + You can achieve this by setting the DebounceDelay value and + Behavior to OnInput. +

+ + +
+ + +

+ The date input 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. +

+ + +
+
+ + +
+

Textbox

+ +
    +
  • + Class: The CSS class name that styles the wrapper of the textbox. +
  • + +
  • + Classes: The CSS class names for the textbox slots that style entire textbox. +
  • +
+
+ +
+ + + +@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.Root), "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(LumexDateInput), + nameof(LumexIcon) + }; + + protected override void OnInitialized() + { + Layout.Initialize( + title: "Date Input", + category: "Components", + description: "Date input allows users to input a specific date.", + headings: _headings, + linksProps: new ComponentLinksProps("DateInput", isServer: false) + ); + } +} diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/ClearButton.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/ClearButton.razor new file mode 100644 index 00000000..bb3a88cc --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/ClearButton.razor @@ -0,0 +1,24 @@ +
+ +
+ +

+ @_text +

+ +@code { + private string? _text; + private DateTime? _value; + + private void Notify() + { + _value = null; + _text = "Input is cleared!"; + } +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Colors.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Colors.razor new file mode 100644 index 00000000..f4ea3ccc --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Colors.razor @@ -0,0 +1,19 @@ +
+ @foreach (var color in _colors) + { +
+ + + + @color + +
+ } +
+ +@code { + private ThemeColor[] _colors = Enum.GetValues()[1..]; +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/CustomStyles.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/CustomStyles.razor new file mode 100644 index 00000000..772436ab --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/CustomStyles.razor @@ -0,0 +1,35 @@ + + +@code { + private RenderFragment _searchIcon = + @ + ; + + 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/DateInput/Examples/DebounceDelay.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/DebounceDelay.razor new file mode 100644 index 00000000..a3f835a9 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/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/DateInput/Examples/Description.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Description.razor new file mode 100644 index 00000000..9a98535f --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Description.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Disabled.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Disabled.razor new file mode 100644 index 00000000..af324aac --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Disabled.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/ErrorMessage.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/ErrorMessage.razor new file mode 100644 index 00000000..826d4ed0 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/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/DateInput/Examples/LabelPlacements.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/LabelPlacements.razor new file mode 100644 index 00000000..19325cd7 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/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/DateInput/Examples/ReadOnly.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/ReadOnly.razor new file mode 100644 index 00000000..a6f81092 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/ReadOnly.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Required.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Required.razor new file mode 100644 index 00000000..aba340f6 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Required.razor @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Sizes.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Sizes.razor new file mode 100644 index 00000000..5c542c71 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Sizes.razor @@ -0,0 +1,21 @@ +
+ @foreach (var size in _sizes) + { +
+
+ + + +
+ + @size +
+ } +
+ +@code { + private Size[] _sizes = Enum.GetValues(); +} diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/StartEndContent.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/StartEndContent.razor new file mode 100644 index 00000000..b4bad972 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/StartEndContent.razor @@ -0,0 +1,114 @@ +
+
+
+
+ + + + + +
+ + + 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/DateInput/Examples/TwoWayDataBinding.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/TwoWayDataBinding.razor new file mode 100644 index 00000000..b6fc2d5c --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/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/DateInput/Examples/Usage.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Usage.razor new file mode 100644 index 00000000..c157d71e --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Usage.razor @@ -0,0 +1,6 @@ +
+ + + +
\ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Variants.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Variants.razor new file mode 100644 index 00000000..a2921d4c --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/Variants.razor @@ -0,0 +1,23 @@ +
+ @foreach (var variant in _variants) + { +
+
+ + + +
+ + + @variant + +
+ } +
+ +@code { + private InputVariant[] _variants = Enum.GetValues(); +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/_Radius.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/_Radius.razor new file mode 100644 index 00000000..d5172177 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/Examples/_Radius.razor @@ -0,0 +1,18 @@ +
+ @foreach (var radius in _radiuses) + { +
+ + + + @radius + +
+ } +
+ +@code { + private Radius[] _radiuses = Enum.GetValues(); +} \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ClearButton.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ClearButton.razor new file mode 100644 index 00000000..b815e7f8 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ClearButton.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Colors.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Colors.razor new file mode 100644 index 00000000..817397a8 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Colors.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/CustomStyles.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/CustomStyles.razor new file mode 100644 index 00000000..274a3e38 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/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/DateInput/PreviewCodes/DebounceDelay.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/DebounceDelay.razor new file mode 100644 index 00000000..95224bf4 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/DebounceDelay.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Description.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Description.razor new file mode 100644 index 00000000..5804f5ee --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Description.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Disabled.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Disabled.razor new file mode 100644 index 00000000..3e094398 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Disabled.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ErrorMessage.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ErrorMessage.razor new file mode 100644 index 00000000..4a7e9436 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ErrorMessage.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/LabelPlacements.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/LabelPlacements.razor new file mode 100644 index 00000000..7657761f --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/LabelPlacements.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Radius.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Radius.razor new file mode 100644 index 00000000..456912f9 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Radius.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ReadOnly.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ReadOnly.razor new file mode 100644 index 00000000..de961f68 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/ReadOnly.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Required.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Required.razor new file mode 100644 index 00000000..f103775c --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Required.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Sizes.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Sizes.razor new file mode 100644 index 00000000..b6b29047 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Sizes.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/StartEndContent.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/StartEndContent.razor new file mode 100644 index 00000000..798251f6 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/StartEndContent.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/TwoWayDataBinding.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/TwoWayDataBinding.razor new file mode 100644 index 00000000..db8136f2 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/TwoWayDataBinding.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Usage.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Usage.razor new file mode 100644 index 00000000..4d5e55a0 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Usage.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Variants.razor b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Variants.razor new file mode 100644 index 00000000..8a1a2083 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/DateInput/PreviewCodes/Variants.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Examples/LabelPlacements.razor b/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Examples/LabelPlacements.razor index 6003ae67..5dc04b68 100644 --- a/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Examples/LabelPlacements.razor +++ b/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Examples/LabelPlacements.razor @@ -12,8 +12,5 @@ @code { - private LabelPlacement[] _labelPlacements = [ - LabelPlacement.Inside, - LabelPlacement.Outside - ]; -} \ No newline at end of file + private LabelPlacement[] _labelPlacements = Enum.GetValues(); +} diff --git a/src/LumexUI/Common/Enums/InputDateType.cs b/src/LumexUI/Common/Enums/InputDateType.cs new file mode 100644 index 00000000..8354192c --- /dev/null +++ b/src/LumexUI/Common/Enums/InputDateType.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 InputDateType +{ + /// + /// A date input sub-type for selecting a date + /// + [Description( "date" )] + Date +} diff --git a/src/LumexUI/Components/DateInput/LumexDateInput.razor b/src/LumexUI/Components/DateInput/LumexDateInput.razor new file mode 100644 index 00000000..6c933912 --- /dev/null +++ b/src/LumexUI/Components/DateInput/LumexDateInput.razor @@ -0,0 +1,6 @@ +@namespace LumexUI +@inherits LumexInputFieldBase + +@{ + base.BuildRenderTree(__builder); +} diff --git a/src/LumexUI/Components/DateInput/LumexDateInput.razor.cs b/src/LumexUI/Components/DateInput/LumexDateInput.razor.cs new file mode 100644 index 00000000..9046b0ec --- /dev/null +++ b/src/LumexUI/Components/DateInput/LumexDateInput.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 Microsoft.AspNetCore.Components; +using System.Globalization; + +namespace LumexUI; + +/// +/// A component that represents an input field for entering values. +/// +public partial class LumexDateInput : LumexInputFieldBase +{ + private const string DateFormat = "yyyy-MM-dd"; + private const string DefaultParsingErrorMessage = "The {0} field must be a date."; + + /// + /// Gets or sets the date input type of the. + /// + /// + /// The default value is + /// + [Parameter] + public InputDateType DateInputType { get; set; } = InputDateType.Date; + + /// + /// Optional custom parsing error message + /// + [Parameter] + public string? ParsingErrorMessage { get; set; } + + private string _parsingErrorText = DefaultParsingErrorMessage; + + /// + protected override void OnParametersSet() + { + if( DateInputType != InputDateType.Date ) + { + throw new InvalidOperationException( + $"LumexDateInput does not currently support {DateInputType}. 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; +} diff --git a/tests/LumexUI.Tests/Components/DateInput/DateInputTests.razor b/tests/LumexUI.Tests/Components/DateInput/DateInputTests.razor new file mode 100644 index 00000000..9232cf94 --- /dev/null +++ b/tests/LumexUI.Tests/Components/DateInput/DateInputTests.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 DateInputTests() + { + 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(); + } +}