diff --git a/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs b/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs
index daf3008f..1103e691 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..40f76637
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Datebox.razor
@@ -0,0 +1,166 @@
+@page "/docs/components/datebox"
+@layout DocsContentLayout
+
+@using LumexUI.Docs.Client.Pages.Components.Datebox.PreviewCodes
+
+
+
+ The datebox component is almost identical to the
+ textbox
+ but includes additional features specific to date input.
+
+
+ It supports various date types, including
+ DateTime, DateTimeOffset, DateOnly,
+ TimeOnly. Additionally, their nullable counterparts are also supported.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ If the Label parameter is not set, the LabelPlacement
+ parameter will default to Outside.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Display an error message below the datebox 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.
+
+
+
+
+
+
+
+ Use @@bind-Value directive or Value and ValueChanged
+ parameters to manually control datebox value.
+
+
+
+
+
+
+
+
Datebox
+
+
+ -
+
Class: The CSS class names to style the datebox wrapper.
+
+
+ -
+
Classes: The CSS class names to style the datebox slots.
+
+
+
+
+
+
+
+
+@code {
+ [CascadingParameter]
+ private DocsContentLayout Layout { get; set; } = default!;
+
+ private readonly Heading[] _headings = new Heading[]
+ {
+ new("Types"),
+ 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("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: "Datebox",
+ category: "Components",
+ description: "Datebox allows users to enter and edit date values.",
+ 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..8e8f114b
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ClearButton.razor
@@ -0,0 +1,5 @@
+
\ 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..bc0a3e93
--- /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..4402aa4e
--- /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/Description.razor b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Description.razor
new file mode 100644
index 00000000..5040ecc0
--- /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..d17c5d7f
--- /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..d3782424
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/ErrorMessage.razor
@@ -0,0 +1,64 @@
+@using FluentValidation
+@using FluentValidation.Results
+
+
+
+@code {
+ private User _user = new();
+ private UserValidator _userValidator = new();
+
+ protected override void OnInitialized()
+ {
+ _user.Date = DateTime.Today;
+ 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("Birth 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..a30c9a06
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/LabelPlacements.razor
@@ -0,0 +1,22 @@
+
+ @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..c5d43ebe
--- /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..810d0f49
--- /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..e77b92db
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Sizes.razor
@@ -0,0 +1,21 @@
+
+ @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..f76dd5bd
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/StartEndContent.razor
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Start
+
+
+
+
+
+
+
+ End
+
+
+
+
+@code {
+ private RenderFragment _calendarClockIcon =
+ @;
+}
\ 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..684b5b61
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/TwoWayDataBinding.razor
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Value: @_valueOne
+
+
+
+
+
+
+
+ Value: @_valueTwo
+
+
+
+
+@code {
+ private DateTime? _valueOne;
+ private DateTime? _valueTwo = DateTime.Today;
+
+ 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..e3fcf48f
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Usage.razor
@@ -0,0 +1,3 @@
+
\ 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..b8543b0d
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/Variants.razor
@@ -0,0 +1,23 @@
+
+ @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..a4a59eb5
--- /dev/null
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Datebox/Examples/_Radius.razor
@@ -0,0 +1,24 @@
+
+ @foreach (Radius radius in _radii)
+ {
+
+
+
+
+ @radius
+
+
+ }
+
+
+@code {
+ private Radius[] _radii = [
+ 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..a3688150
--- /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 = "mask-none bg-default-50",
+ Preview = "justify-center"
+ };
+}
\ 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/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Numbox.razor b/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Numbox.razor
index 56dd5097..cb724689 100644
--- a/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Numbox.razor
+++ b/docs/LumexUI.Docs.Client/Pages/Components/Numbox/Numbox.razor
@@ -13,9 +13,7 @@
It supports various numeric types, including
short, int, long,
float, double, and decimal.
- Additionally, their nullable counterparts (e.g., int?,
- double?, decimal?) are also supported,
- providing flexibility for scenarios where input values may be optional or undefined.
+ Additionally, their nullable counterparts are also supported.
diff --git a/src/LumexUI/Common/Enums/InputDateType.cs b/src/LumexUI/Common/Enums/InputDateType.cs
new file mode 100644
index 00000000..ad70fb54
--- /dev/null
+++ b/src/LumexUI/Common/Enums/InputDateType.cs
@@ -0,0 +1,37 @@
+// 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
+{
+ ///
+ /// Represents a date value without a time component.
+ ///
+ [Description( "date" )]
+ Date,
+
+ ///
+ /// Represents a date and time value without a time zone.
+ ///
+ [Description( "datetime-local" )]
+ DateTimeLocal,
+
+ ///
+ /// Represents the month component of a date or time value.
+ ///
+ [Description( "month" )]
+ Month,
+
+ ///
+ /// Represents a time value to indicate a specific time of day without a date component.
+ ///
+ [Description( "time" )]
+ Time,
+}
\ No newline at end of file
diff --git a/src/LumexUI/Common/Enums/InputType.cs b/src/LumexUI/Common/Enums/InputType.cs
index 561f9041..8aa2a247 100644
--- a/src/LumexUI/Common/Enums/InputType.cs
+++ b/src/LumexUI/Common/Enums/InputType.cs
@@ -51,11 +51,5 @@ public enum InputType
/// An input field for entering a URL.
///
[Description( "url" )]
- Url,
-
- ///
- /// An input field for selecting a color.
- ///
- [Description( "color" )]
- Color
+ Url
}
diff --git a/src/LumexUI/Components/Bases/LumexDebouncedInputBase.cs b/src/LumexUI/Components/Bases/LumexDebouncedInputBase.cs
index ae86bb0b..2e25ef0e 100644
--- a/src/LumexUI/Components/Bases/LumexDebouncedInputBase.cs
+++ b/src/LumexUI/Components/Bases/LumexDebouncedInputBase.cs
@@ -2,6 +2,8 @@
// 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;
namespace LumexUI;
@@ -10,116 +12,127 @@ namespace LumexUI;
/// Represents a base class for input components with debounced value updates.
///
/// The type of the input value.
-public abstract class LumexDebouncedInputBase : LumexInputBase, IDisposable
+public abstract class LumexDebouncedInputBase : LumexInputFieldBase
{
- ///
- /// Gets or sets the delay, in milliseconds, for debouncing input events.
- ///
- [Parameter] public int DebounceDelay { get; set; }
-
- private readonly Debouncer _debouncer;
-
- private bool _disposed;
-
- ///
- /// Initializes a new instance of the .
- ///
- public LumexDebouncedInputBase()
- {
- _debouncer = new Debouncer();
- }
-
- ///
- /// Handles the input event asynchronously, applying a debounce delay if provided.
- ///
- /// The change event arguments.
- /// A representing the asynchronous value input operation.
- protected virtual Task OnInputAsync( ChangeEventArgs args )
- {
- if( DebounceDelay > 0 )
- {
- return _debouncer.DebounceAsync( SetCurrentValueAsStringAsync, (string?)args.Value, DebounceDelay );
- }
-
- return SetCurrentValueAsStringAsync( (string?)args.Value );
- }
-
- ///
- /// Handles the change event asynchronously.
- ///
- /// The change event arguments.
- /// A representing the asynchronous value change operation.
- protected virtual Task OnChangeAsync( ChangeEventArgs args )
- {
- return SetCurrentValueAsStringAsync( (string?)args.Value );
- }
-
- ///
- public void Dispose()
- {
- Dispose( disposing: true );
- GC.SuppressFinalize( this );
- }
-
- ///
- protected virtual void Dispose( bool disposing )
- {
- if( !_disposed )
- {
- if( disposing )
- {
- _debouncer.Dispose();
- }
-
- _disposed = true;
- }
- }
-
- ///
- /// Represents a debouncer for handling debounced input events.
- ///
- private sealed class Debouncer : IDisposable
- {
- private bool _disposed;
- private CancellationTokenSource? _cts;
-
- public async Task DebounceAsync( Func workItem, string? arg, int milliseconds )
- {
- ArgumentNullException.ThrowIfNull( workItem );
-
- _cts?.Cancel();
- _cts?.Dispose();
-
- var cts = _cts = new CancellationTokenSource();
- using var timer = new PeriodicTimer( TimeSpan.FromMilliseconds( milliseconds ) );
-
- while( await timer.WaitForNextTickAsync( cts.Token ) )
- {
- // Debounce time has passed without further input; trigger the debounced event
- await workItem( arg );
- break;
- }
- }
-
- ///
- public void Dispose()
- {
- Dispose( disposing: true );
- GC.SuppressFinalize( this );
- }
-
- private void Dispose( bool disposing )
- {
- if( !_disposed )
- {
- if( disposing )
- {
- _cts?.Cancel();
- _cts?.Dispose();
- }
-
- _disposed = true;
- }
- }
- }
+ ///
+ /// Gets or sets the delay, in milliseconds, for debouncing input events.
+ ///
+ [Parameter] public int DebounceDelay { get; set; }
+
+ ///
+ /// Gets or sets the input behavior, specifying when the textbox
+ /// updates its value and triggers validation.
+ ///
+ ///
+ /// The default value is
+ ///
+ [Parameter] public InputBehavior Behavior { get; set; } = InputBehavior.OnChange;
+
+ private readonly Debouncer _debouncer;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ public LumexDebouncedInputBase()
+ {
+ _debouncer = new Debouncer();
+ }
+
+ ///
+ protected override void OnParametersSet()
+ {
+ if( DebounceDelay > 0 && Behavior is not InputBehavior.OnInput )
+ {
+ throw new InvalidOperationException(
+ $"{GetType()} requires '{nameof( InputBehavior.OnInput )}' behavior" +
+ $" to be used when '{nameof( DebounceDelay )}' is not zero." );
+ }
+ }
+
+ ///
+ protected override Task OnInputAsync( ChangeEventArgs args )
+ {
+ if( Behavior is not InputBehavior.OnInput )
+ {
+ return Task.CompletedTask;
+ }
+
+ if( DebounceDelay > 0 )
+ {
+ return _debouncer.DebounceAsync( SetCurrentValueAsStringAsync, (string?)args.Value, DebounceDelay );
+ }
+
+ return SetCurrentValueAsStringAsync( (string?)args.Value );
+ }
+
+ ///
+ protected override Task OnChangeAsync( ChangeEventArgs args )
+ {
+ if( Behavior is not InputBehavior.OnChange )
+ {
+ return Task.CompletedTask;
+ }
+
+ return SetCurrentValueAsStringAsync( (string?)args.Value );
+ }
+
+ ///
+ public override async ValueTask DisposeAsync()
+ {
+ _debouncer.Dispose();
+ await base.DisposeAsync();
+ }
+
+ ///
+ /// Represents a debouncer for handling debounced input events.
+ ///
+ private sealed class Debouncer : IDisposable
+ {
+ private bool _disposed;
+ private CancellationTokenSource? _cts;
+
+ public async Task DebounceAsync( Func workItem, string? arg, int milliseconds )
+ {
+ ObjectDisposedException.ThrowIf( _disposed, this );
+ ArgumentNullException.ThrowIfNull( workItem );
+
+ // Cancel/dispose any pending debounce.
+ _cts?.Cancel();
+ _cts?.Dispose();
+
+ var cts = _cts = new CancellationTokenSource();
+
+ if( milliseconds <= 0 )
+ {
+ await workItem( arg );
+ return;
+ }
+
+ try
+ {
+ await Task.Delay( milliseconds, cts.Token );
+
+ // Debounce time has passed without further input; trigger the debounced event.
+ await workItem( arg );
+ }
+ catch( OperationCanceledException ) when( cts.IsCancellationRequested )
+ {
+ // Expected: new input or disposal cancelled the pending debounce.
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if( _disposed )
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ _cts?.Cancel();
+ _cts?.Dispose();
+ }
+ }
}
diff --git a/src/LumexUI/Components/Bases/LumexInputBase.cs b/src/LumexUI/Components/Bases/LumexInputBase.cs
index 5f02ca66..880f320d 100644
--- a/src/LumexUI/Components/Bases/LumexInputBase.cs
+++ b/src/LumexUI/Components/Bases/LumexInputBase.cs
@@ -221,7 +221,6 @@ protected virtual async Task OnBlurAsync( FocusEventArgs args )
/// A string representation of the input value.
protected virtual string? FormatValueAsString( TValue? value ) => value?.ToString();
-
///
/// Asynchronously sets the validation message if any.
///
diff --git a/src/LumexUI/Components/Bases/LumexInputFieldBase.razor b/src/LumexUI/Components/Bases/LumexInputFieldBase.razor
index 4bddcef5..5e83888c 100644
--- a/src/LumexUI/Components/Bases/LumexInputFieldBase.razor
+++ b/src/LumexUI/Components/Bases/LumexInputFieldBase.razor
@@ -1,5 +1,5 @@
@namespace LumexUI
-@inherits LumexDebouncedInputBase
+@inherits LumexInputBase
@typeparam TValue
/// The type of the input value.
-public abstract partial class LumexInputFieldBase : LumexDebouncedInputBase,
- ISlotComponent,
- IAsyncDisposable
+public abstract partial class LumexInputFieldBase : LumexInputBase,
+ ISlotComponent,
+ IAsyncDisposable
{
- private const string JavaScriptFile = "./_content/LumexUI/js/components/input.js";
-
- ///
- /// Gets or sets content to be rendered at the start of the textbox.
- ///
- [Parameter] public RenderFragment? StartContent { get; set; }
-
- ///
- /// Gets or sets content to be rendered at the end of the textbox.
- ///
- [Parameter] public RenderFragment? EndContent { get; set; }
-
- ///
- /// Gets or sets the label for the textbox.
- ///
- [Parameter] public string? Label { get; set; }
-
- ///
- /// Gets or sets the placeholder for the textbox.
- ///
- [Parameter] public string? Placeholder { get; set; }
-
- ///
- /// Gets or sets the description for the textbox.
- ///
- [Parameter] public string? Description { get; set; }
-
- ///
- /// Gets or sets the error message for the textbox.
- /// This message is displayed only when the textbox is invalid.
- ///
- [Parameter] public string? ErrorMessage { get; set; }
-
- ///
- /// Gets or sets the variant for the textbox.
- ///
- ///
- /// The default value is
- ///
- [Parameter] public InputVariant Variant { get; set; }
-
- ///
- /// Gets or sets the input behavior, specifying when the textbox
- /// updates its value and triggers validation.
- ///
- ///
- /// The default value is
- ///
- [Parameter] public InputBehavior Behavior { get; set; } = InputBehavior.OnChange;
-
- ///
- /// Gets or sets the border radius of the textbox.
- ///
- [Parameter] public Radius? Radius { get; set; }
-
- ///
- /// Gets or sets the placement of the label for the textbox.
- ///
- ///
- /// The default value is
- ///
- [Parameter] public LabelPlacement LabelPlacement { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the textbox is full-width.
- ///
- ///
- /// The default value is
- ///
- [Parameter] public bool FullWidth { get; set; } = true;
-
- ///
- /// Gets or sets a value indicating whether the textbox should have a clear button.
- ///
- [Parameter] public bool Clearable { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the textbox should automatically receive focus.
- ///
- [Parameter] public bool Autofocus { get; set; }
-
- ///
- /// Gets or sets a callback that is fired when the value in the textbox is cleared.
- ///
- [Parameter] public EventCallback OnCleared { get; set; }
-
- ///
- /// Gets or sets the CSS class names for the textbox slots.
- ///
- [Parameter] public InputFieldSlots? Classes { get; set; }
-
- [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
-
- ///
- /// Gets or sets the validation error mesage of the input.
- ///
- protected string? ValidationMessage { get; private set; }
-
- private protected override string? RootClass =>
- TwMerge.Merge( InputField.GetStyles( this ) );
-
- private string? LabelClass =>
- TwMerge.Merge( InputField.GetLabelStyles( this ) );
-
- private string? MainWrapperClass =>
- TwMerge.Merge( InputField.GetMainWrapperStyles( this ) );
-
- private string? InputWrapperClass =>
- TwMerge.Merge( InputField.GetInputWrapperStyles( this ) );
-
- private string? InnerWrapperClass =>
- TwMerge.Merge( InputField.GetInnerWrapperStyles( this ) );
-
- private string? InputClass =>
- TwMerge.Merge( InputField.GetInputStyles( this ) );
-
- private string? ClearButtonClass =>
- TwMerge.Merge( InputField.GetClearButtonStyles( this ) );
-
- private string? HelperWrapperClass =>
- TwMerge.Merge( InputField.GetHelperWrapperStyles( this ) );
-
- private string? DescriptionClass =>
- TwMerge.Merge( InputField.GetDescriptionStyles( this ) );
-
- private string? ErrorMessageClass =>
- TwMerge.Merge( InputField.GetErrorMessageStyles( this ) );
-
- private bool HasHelper =>
- !string.IsNullOrEmpty( Description ) ||
- !string.IsNullOrEmpty( ErrorMessage ) ||
- !string.IsNullOrEmpty( ValidationMessage );
- private bool HasValue => !string.IsNullOrEmpty( CurrentValueAsString );
- private bool ClearButtonVisible => Clearable && HasValue;
- private bool FilledOrFocused =>
- Focused ||
- HasValue ||
- StartContent is not null ||
- !string.IsNullOrEmpty( Placeholder );
-
- private readonly RenderFragment _renderMainWrapper;
- private readonly RenderFragment _renderInputWrapper;
- private readonly RenderFragment _renderHelperWrapper;
-
- private string _inputType = default!;
- private IJSObjectReference _jsModule = default!;
-
- ///
- /// Initializes a new instance of the .
- ///
- public LumexInputFieldBase()
- {
- _renderMainWrapper = RenderMainWrapper;
- _renderInputWrapper = RenderInputWrapper;
- _renderHelperWrapper = RenderHelperWrapper;
-
- As = "div";
- }
-
- ///
- public override async Task SetParametersAsync( ParameterView parameters )
- {
- await base.SetParametersAsync( parameters );
-
- if( parameters.TryGetValue( nameof( LabelPlacement ), out var labelPlacement ) )
- {
- LabelPlacement = labelPlacement;
- }
- // Default LabelPlacement to 'Outside' if the label and its placement are not set
- else if( !parameters.TryGetValue( nameof( Label ), out var _ ) )
- {
- LabelPlacement = LabelPlacement.Outside;
- }
- }
-
- ///
- protected override void OnParametersSet()
- {
- if( DebounceDelay > 0 && Behavior is not InputBehavior.OnInput )
- {
- throw new InvalidOperationException(
- $"{GetType()} requires '{nameof( InputBehavior.OnInput )}' behavior" +
- $" to be used when '{nameof( DebounceDelay )}' is not zero." );
- }
- }
-
- ///
- protected override async Task OnAfterRenderAsync( bool firstRender )
- {
- if( firstRender )
- {
- _jsModule = await JSRuntime.InvokeAsync( "import", JavaScriptFile );
- }
-
- if( Autofocus )
- {
- await FocusAsync();
- }
- }
-
- ///
- protected override Task OnInputAsync( ChangeEventArgs args )
- {
- if( Behavior is not InputBehavior.OnInput )
- {
- return Task.CompletedTask;
- }
-
- return base.OnInputAsync( args );
- }
-
- ///
- protected override Task OnChangeAsync( ChangeEventArgs args )
- {
- if( Behavior is not InputBehavior.OnChange )
- {
- return Task.CompletedTask;
- }
-
- return base.OnChangeAsync( args );
- }
-
- ///
- protected override bool TryParseValueFromString( string? value, [MaybeNullWhen( false )] out TValue result )
- {
- return BindConverter.TryConvertTo( value, CultureInfo.InvariantCulture, out result );
- }
-
- ///
- protected override async ValueTask SetValidationMessageAsync()
- {
- ValidationMessage = await _jsModule.InvokeAsync( "input.getValidationMessage", ElementReference );
- Invalid = !string.IsNullOrEmpty( ErrorMessage ) ||
- !string.IsNullOrEmpty( ValidationMessage );
- }
-
- ///
- /// Sets the input type for the input field.
- ///
- /// The input type to set (e.g., "text", "number", "password").
- protected void SetInputType( string type )
- {
- _inputType = type;
- }
-
- private async Task FocusInputAsync()
- {
- if( !Disabled && !ReadOnly )
- {
- await FocusAsync();
- }
- }
-
- private async Task ClearAsync( MouseEventArgs args )
- {
- await ClearAsyncCore();
- }
-
- private async Task ClearAsync( KeyboardEventArgs args )
- {
- if( args.Code is "Enter" or "Space" )
- {
- await ClearAsyncCore();
- }
- }
-
- private async Task ClearAsyncCore()
- {
- await SetCurrentValueAsync( default );
- await OnCleared.InvokeAsync();
- await FocusAsync();
- }
-
- ///
- public async ValueTask DisposeAsync()
- {
- try
- {
- if( _jsModule is not null )
- {
- await _jsModule.DisposeAsync();
- }
-
- Dispose();
- }
- catch( Exception ex ) when( ex is JSDisconnectedException or OperationCanceledException )
- {
- // The JSRuntime side may routinely be gone already if the reason we're disposing is that
- // the client disconnected. This is not an error.
- }
- }
+ private const string JavaScriptFile = "./_content/LumexUI/js/components/input.js";
+
+ ///
+ /// Gets or sets content to be rendered at the start of the textbox.
+ ///
+ [Parameter] public RenderFragment? StartContent { get; set; }
+
+ ///
+ /// Gets or sets content to be rendered at the end of the textbox.
+ ///
+ [Parameter] public RenderFragment? EndContent { get; set; }
+
+ ///
+ /// Gets or sets the label for the textbox.
+ ///
+ [Parameter] public string? Label { get; set; }
+
+ ///
+ /// Gets or sets the placeholder for the textbox.
+ ///
+ [Parameter] public string? Placeholder { get; set; }
+
+ ///
+ /// Gets or sets the description for the textbox.
+ ///
+ [Parameter] public string? Description { get; set; }
+
+ ///
+ /// Gets or sets the error message for the textbox.
+ /// This message is displayed only when the textbox is invalid.
+ ///
+ [Parameter] public string? ErrorMessage { get; set; }
+
+ ///
+ /// Gets or sets the variant for the textbox.
+ ///
+ ///
+ /// The default value is
+ ///
+ [Parameter] public InputVariant Variant { get; set; }
+
+ ///
+ /// Gets or sets the border radius of the textbox.
+ ///
+ [Parameter] public Radius? Radius { get; set; }
+
+ ///
+ /// Gets or sets the placement of the label for the textbox.
+ ///
+ ///
+ /// The default value is
+ ///
+ [Parameter] public LabelPlacement LabelPlacement { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the textbox is full-width.
+ ///
+ ///
+ /// The default value is
+ ///
+ [Parameter] public bool FullWidth { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether the textbox should have a clear button.
+ ///
+ [Parameter] public bool Clearable { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the textbox should automatically receive focus.
+ ///
+ [Parameter] public bool Autofocus { get; set; }
+
+ ///
+ /// Gets or sets a callback that is fired when the value in the textbox is cleared.
+ ///
+ [Parameter] public EventCallback OnCleared { get; set; }
+
+ ///
+ /// Gets or sets the CSS class names for the textbox slots.
+ ///
+ [Parameter] public InputFieldSlots? Classes { get; set; }
+
+ [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
+
+ ///
+ /// Gets or sets the validation error message of the input.
+ ///
+ protected string? ValidationMessage { get; private set; }
+
+ ///
+ /// Gets or sets a value indicating whether the input is filled or focused.
+ ///
+ protected virtual bool FilledOrFocused =>
+ Focused ||
+ HasValue ||
+ StartContent is not null ||
+ !string.IsNullOrEmpty( Placeholder );
+
+ private protected override string? RootClass =>
+ TwMerge.Merge( InputField.GetStyles( this ) );
+
+ private string? LabelClass =>
+ TwMerge.Merge( InputField.GetLabelStyles( this ) );
+
+ private string? MainWrapperClass =>
+ TwMerge.Merge( InputField.GetMainWrapperStyles( this ) );
+
+ private string? InputWrapperClass =>
+ TwMerge.Merge( InputField.GetInputWrapperStyles( this ) );
+
+ private string? InnerWrapperClass =>
+ TwMerge.Merge( InputField.GetInnerWrapperStyles( this ) );
+
+ private string? InputClass =>
+ TwMerge.Merge( InputField.GetInputStyles( this ) );
+
+ private string? ClearButtonClass =>
+ TwMerge.Merge( InputField.GetClearButtonStyles( this ) );
+
+ private string? HelperWrapperClass =>
+ TwMerge.Merge( InputField.GetHelperWrapperStyles( this ) );
+
+ private string? DescriptionClass =>
+ TwMerge.Merge( InputField.GetDescriptionStyles( this ) );
+
+ private string? ErrorMessageClass =>
+ TwMerge.Merge( InputField.GetErrorMessageStyles( this ) );
+
+ private bool HasHelper =>
+ !string.IsNullOrEmpty( Description ) ||
+ !string.IsNullOrEmpty( ErrorMessage ) ||
+ !string.IsNullOrEmpty( ValidationMessage );
+ private bool HasValue => !string.IsNullOrEmpty( CurrentValueAsString );
+ private bool ClearButtonVisible => ( Clearable || OnCleared.HasDelegate ) && HasValue;
+
+ private readonly RenderFragment _renderMainWrapper;
+ private readonly RenderFragment _renderInputWrapper;
+ private readonly RenderFragment _renderHelperWrapper;
+
+ private string _inputType = default!;
+ private IJSObjectReference _jsModule = default!;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ public LumexInputFieldBase()
+ {
+ _renderMainWrapper = RenderMainWrapper;
+ _renderInputWrapper = RenderInputWrapper;
+ _renderHelperWrapper = RenderHelperWrapper;
+
+ As = "div";
+ }
+
+ ///
+ public override async Task SetParametersAsync( ParameterView parameters )
+ {
+ await base.SetParametersAsync( parameters );
+
+ if( parameters.TryGetValue( nameof( LabelPlacement ), out var labelPlacement ) )
+ {
+ LabelPlacement = labelPlacement;
+ }
+ // Default LabelPlacement to 'Outside' if the label and its placement are not set
+ else if( !parameters.TryGetValue( nameof( Label ), out var _ ) )
+ {
+ LabelPlacement = LabelPlacement.Outside;
+ }
+ }
+
+ ///
+ protected override async Task OnAfterRenderAsync( bool firstRender )
+ {
+ if( firstRender )
+ {
+ _jsModule = await JSRuntime.InvokeAsync( "import", JavaScriptFile );
+
+ if( Autofocus )
+ {
+ await FocusAsync();
+ }
+ }
+ }
+
+ ///
+ /// Handles input events asynchronously.
+ ///
+ ///
+ /// Override this method in a derived class to implement custom logic for handling input changes.
+ ///
+ /// The change event arguments.
+ /// A representing the asynchronous value input operation.
+ protected abstract Task OnInputAsync( ChangeEventArgs args );
+
+ ///
+ /// Handles change events asynchronously.
+ ///
+ ///
+ /// Override this method in a derived class to implement custom logic that responds to change events.
+ ///
+ /// The change event arguments.
+ /// A representing the asynchronous value change operation.
+ protected abstract Task OnChangeAsync( ChangeEventArgs args );
+
+ ///
+ protected override bool TryParseValueFromString( string? value, [MaybeNullWhen( false )] out TValue result )
+ {
+ return BindConverter.TryConvertTo( value, CultureInfo.InvariantCulture, out result );
+ }
+
+ ///
+ protected override async ValueTask SetValidationMessageAsync()
+ {
+ ValidationMessage = await _jsModule.InvokeAsync( "input.getValidationMessage", ElementReference );
+ Invalid = !string.IsNullOrEmpty( ErrorMessage ) ||
+ !string.IsNullOrEmpty( ValidationMessage );
+ }
+
+ ///
+ /// Sets the input type for the input field.
+ ///
+ /// The input type to set (e.g., "text", "number", "password").
+ protected void SetInputType( string type )
+ {
+ _inputType = type;
+ }
+
+ private async Task FocusInputAsync()
+ {
+ if( !Disabled && !ReadOnly )
+ {
+ await FocusAsync();
+ }
+ }
+
+ private async Task ClearAsync( MouseEventArgs args )
+ {
+ await ClearAsyncCore();
+ }
+
+ private async Task ClearAsync( KeyboardEventArgs args )
+ {
+ if( args.Code is "Enter" or "Space" )
+ {
+ await ClearAsyncCore();
+ }
+ }
+
+ private async Task ClearAsyncCore()
+ {
+ await SetCurrentValueAsync( default );
+ await OnCleared.InvokeAsync();
+ await FocusAsync();
+ }
+
+ ///
+ public virtual async ValueTask DisposeAsync()
+ {
+ try
+ {
+ if( _jsModule is not null )
+ {
+ await _jsModule.DisposeAsync().ConfigureAwait( false );
+ }
+ }
+ catch( Exception ex ) when( ex is JSDisconnectedException or OperationCanceledException )
+ {
+ // The JSRuntime side may routinely be gone already if the reason we're disposing is that
+ // the client disconnected. This is not an error.
+ }
+ }
}
diff --git a/src/LumexUI/Components/Datebox/LumexDatebox.razor b/src/LumexUI/Components/Datebox/LumexDatebox.razor
new file mode 100644
index 00000000..b8fdc683
--- /dev/null
+++ b/src/LumexUI/Components/Datebox/LumexDatebox.razor
@@ -0,0 +1,7 @@
+@namespace LumexUI
+@inherits LumexInputFieldBase
+@typeparam TValue
+
+@{
+ 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..c73045e7
--- /dev/null
+++ b/src/LumexUI/Components/Datebox/LumexDatebox.razor.cs
@@ -0,0 +1,120 @@
+// 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.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+using LumexUI.Extensions;
+
+using Microsoft.AspNetCore.Components;
+
+using InputDateType = LumexUI.Common.InputDateType;
+
+namespace LumexUI;
+
+///
+/// A component that represents an input field for entering date values.
+///
+///
+/// The supported types for the date value are:
+///
+///
+///
+///
+///
+///
+///
+public partial class LumexDatebox : LumexInputFieldBase
+{
+ private const string DateFormat = "yyyy-MM-dd"; // Compatible with HTML 'date' inputs
+ private const string DateTimeLocalFormat = "yyyy-MM-ddTHH:mm:ss"; // Compatible with HTML 'datetime-local' inputs
+ private const string MonthFormat = "yyyy-MM"; // Compatible with HTML 'month' inputs
+ private const string TimeFormat = "HH:mm:ss"; // Compatible with HTML 'time' inputs
+
+ ///
+ /// Gets or sets the input type of the datebox.
+ ///
+ ///
+ /// The default value is
+ ///
+ [Parameter]
+ public InputDateType Type { get; set; } = InputDateType.Date;
+
+ ///
+ protected override bool FilledOrFocused => true;
+
+ private string _format = default!;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ public LumexDatebox()
+ {
+ var type = Nullable.GetUnderlyingType( typeof( TValue ) ) ?? typeof( TValue );
+
+ if( type != typeof( DateTime ) &&
+ type != typeof( DateTimeOffset ) &&
+ type != typeof( DateOnly ) &&
+ type != typeof( TimeOnly ) )
+ {
+ throw new InvalidOperationException( $"Unsupported {GetType()} type param '{type}'." );
+ }
+ }
+
+ ///
+ protected override void OnParametersSet()
+ {
+ SetInputType( Type.ToDescription() );
+
+ _format = Type switch
+ {
+ InputDateType.Date => DateFormat,
+ InputDateType.DateTimeLocal => DateTimeLocalFormat,
+ InputDateType.Month => MonthFormat,
+ InputDateType.Time => TimeFormat,
+ _ => throw new InvalidOperationException( $"Unsupported {nameof( InputDateType )} '{Type}'." )
+ };
+ }
+
+ ///
+ protected override Task OnInputAsync( ChangeEventArgs args )
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ protected override Task OnChangeAsync( ChangeEventArgs args )
+ {
+ return SetCurrentValueAsStringAsync( (string?)args.Value );
+ }
+
+ ///
+ [ExcludeFromCodeCoverage]
+ protected override string FormatValueAsString( TValue? value )
+ {
+ return value switch
+ {
+ DateTime dateTimeValue => BindConverter.FormatValue( dateTimeValue, _format, CultureInfo.InvariantCulture ),
+ DateTimeOffset dateTimeOffsetValue => BindConverter.FormatValue( dateTimeOffsetValue, _format, CultureInfo.InvariantCulture ),
+ DateOnly dateOnlyValue => BindConverter.FormatValue( dateOnlyValue, _format, CultureInfo.InvariantCulture ),
+ TimeOnly timeOnlyValue => BindConverter.FormatValue( timeOnlyValue, _format, CultureInfo.InvariantCulture ),
+ _ => string.Empty, // Handles null for Nullable, etc.
+ };
+ }
+
+ ///
+ protected override bool TryParseValueFromString( string? value, [MaybeNullWhen( false )] out TValue result )
+ {
+ if( BindConverter.TryConvertTo( value, CultureInfo.InvariantCulture, out result ) )
+ {
+ Debug.Assert( result != null );
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LumexUI/Components/Numbox/LumexNumbox.razor b/src/LumexUI/Components/Numbox/LumexNumbox.razor
index 230d50d1..07a164d2 100644
--- a/src/LumexUI/Components/Numbox/LumexNumbox.razor
+++ b/src/LumexUI/Components/Numbox/LumexNumbox.razor
@@ -1,5 +1,5 @@
@namespace LumexUI
-@inherits LumexInputFieldBase
+@inherits LumexDebouncedInputBase
@typeparam TValue
@{
diff --git a/src/LumexUI/Components/Numbox/LumexNumbox.razor.cs b/src/LumexUI/Components/Numbox/LumexNumbox.razor.cs
index 2d66e8f8..b04d6d45 100644
--- a/src/LumexUI/Components/Numbox/LumexNumbox.razor.cs
+++ b/src/LumexUI/Components/Numbox/LumexNumbox.razor.cs
@@ -15,7 +15,7 @@ namespace LumexUI;
///
/// A component representing an input field for entering/editing numeric types.
///
-public partial class LumexNumbox : LumexInputFieldBase
+public partial class LumexNumbox : LumexDebouncedInputBase
{
private static readonly string _stepAttributeValue = GetStepAttributeValue();
diff --git a/src/LumexUI/Components/Textbox/LumexTextbox.razor b/src/LumexUI/Components/Textbox/LumexTextbox.razor
index ee409450..f0ccd674 100644
--- a/src/LumexUI/Components/Textbox/LumexTextbox.razor
+++ b/src/LumexUI/Components/Textbox/LumexTextbox.razor
@@ -1,5 +1,5 @@
@namespace LumexUI
-@inherits LumexInputFieldBase
+@inherits LumexDebouncedInputBase
@{
base.BuildRenderTree( __builder );
diff --git a/src/LumexUI/Components/Textbox/LumexTextbox.razor.cs b/src/LumexUI/Components/Textbox/LumexTextbox.razor.cs
index 4a384bab..38f03962 100644
--- a/src/LumexUI/Components/Textbox/LumexTextbox.razor.cs
+++ b/src/LumexUI/Components/Textbox/LumexTextbox.razor.cs
@@ -12,7 +12,7 @@ namespace LumexUI;
///
/// A component that represents an input field for entering values.
///
-public partial class LumexTextbox : LumexInputFieldBase
+public partial class LumexTextbox : LumexDebouncedInputBase
{
///
/// Gets or sets the input type of the textbox.
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..f0a4190c
--- /dev/null
+++ b/tests/LumexUI.Tests/Components/Datebox/DateboxTests.razor
@@ -0,0 +1,223 @@
+@namespace LumexUI.Tests.Components
+@inherits TestContext
+
+@using Microsoft.AspNetCore.Components.Web
+
+@code {
+ public DateboxTests()
+ {
+ Services.AddSingleton();
+
+ var module = JSInterop.SetupModule("./_content/LumexUI/js/components/input.js");
+ module.Setup("input.getValidationMessage", _ => true);
+ }
+
+ [Fact]
+ public void ShouldRenderCorrectly()
+ {
+ var action = () => Render(
+ @
+ );
+
+ action.Should().NotThrow();
+ }
+
+ [Fact]
+ public void ShouldThrowIfNotDateType()
+ {
+ var action = () => Render(
+ @
+ );
+
+ action.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData(InputDateType.Date, "date")]
+ [InlineData(InputDateType.DateTimeLocal, "datetime-local")]
+ [InlineData(InputDateType.Month, "month")]
+ [InlineData(InputDateType.Time, "time")]
+ public void ShouldHaveCorrectTypeAttribute(InputDateType type, string actual)
+ {
+ var cut = Render(
+ @
+ );
+
+ var input = cut.Find("input");
+
+ input.GetAttribute("type").Should().Be(actual);
+ }
+
+ [Fact]
+ public void ShouldRenderMainWrapperWhenLabelOutside()
+ {
+ var cut = Render(
+ @
+ );
+
+ cut.Find("[data-slot=main-wrapper]").Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ShouldRenderHelperWrapperWhenDescriptionProvided()
+ {
+ var cut = Render(
+ @
+ );
+
+ cut.Find("[data-slot=helper-wrapper]").Should().NotBeNull();
+ cut.Find("[data-slot=description]").Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ShouldRenderHelperWrapperWhenErrorMessageProvided()
+ {
+ var cut = Render(
+ @
+ );
+ var 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()
+ {
+ var cut = Render(
+ @
+ );
+
+ cut.Find("[data-slot=error-message]").Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ShouldHaveDisabledAttributeWhenDisabled()
+ {
+ var cut = Render(
+ @
+ );
+
+ var input = cut.Find("input");
+
+ input.HasAttribute("disabled").Should().BeTrue();
+ }
+
+ [Fact]
+ public void ShouldRenderClearButtonWhenClearableAndHasValue()
+ {
+ var cut = Render(
+ @
+ );
+
+ var clearButton = cut.Find("[role=button]");
+
+ clearButton.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ShouldClearValueOnClickWhenClearable()
+ {
+ DateTime? value = DateTime.Today;
+ var cut = Render>(
+ @
+ );
+
+ var clearButton = cut.Find("[role=button]");
+ clearButton.Click();
+
+ cut.Instance.Value.Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData("Enter")]
+ [InlineData("Space")]
+ public void ShouldClearValueOnlyWithEnterOrSpaceWhenClearable(string code)
+ {
+ DateTime? value = DateTime.Today;
+ var cut = Render>(
+ @
+ );
+
+ var clearButton = cut.Find("[role=button]");
+
+ clearButton.KeyUp(new KeyboardEventArgs() { Code = "Esc" });
+ cut.Instance.Value.Should().Be(value);
+
+ clearButton.KeyUp(new KeyboardEventArgs() { Code = code });
+ cut.Instance.Value.Should().BeNull();
+ }
+
+ [Fact]
+ public void ShouldTriggerOnClearedCallbackOnClear()
+ {
+ bool isCleared = false;
+ var cut = Render>(
+ @
+ );
+
+ var clearButton = cut.Find("[role=button]");
+ clearButton.Click();
+
+ isCleared.Should().BeTrue();
+ }
+
+ [Fact]
+ public void ShouldFocusInputOnInputWrapperClick()
+ {
+ var cut = Render(
+ @
+ );
+
+ var baseWrapper = cut.Find("[data-slot=base]");
+ var 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)
+ {
+ var cut = Render(
+ @
+ );
+
+ var baseWrapper = cut.Find("[data-slot=base]");
+ var 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 ShouldChangeValueUsingChangeEvent()
+ {
+ var value = DateTime.Today;
+ var cut = Render>(
+ @
+ );
+
+ var input = cut.Find("input");
+ input.Change(value.ToString("yyyy-MM-dd"));
+
+ cut.Instance.Value.Should().Be(value.Date);
+ }
+
+ [Fact]
+ public void ShouldNotChangeValueUsingInputEvent()
+ {
+ var value = DateTime.Today;
+ var cut = Render>(
+ @
+ );
+
+ var input = cut.Find("input");
+ input.Input(value.ToString("yyyy-MM-dd"));
+
+ cut.Instance.Value.Should().BeNull(because: "No-op");
+ }
+}
\ No newline at end of file