Skip to content

Commit

Permalink
feat(components): add textbox component (#53)
Browse files Browse the repository at this point in the history
* feat(textbox): init the component

* feat(textbox): add the base styles

* feat(textbox): add the `Label` parameter; add label styles

* feat(textbox): add input wrapper styles

* feat(textbox): add inner wrapper styles

* feat(textbox): add input styles

* fix(textbox): implement abstract base method

* feat(textbox): add size styles

* feat(textbox): add the radius styles

* feat(textbox): add the disabled styles

* feat(textbox): add the full-width styles

* feat(textbox): add the `Placeholder` param

* feat(component): add element ref capture

* feat(input base): add the `FocusAsync` method to the API

* fix(textbox): add missing `OnChangeAsync`

* feat(textbox): add the `LabelPlacement` param

* feat(input base): add the `OnBlur` param

* feat(textbox): add label placement styles

* feat(textbox): add detailed styles for the 'inside' label placement

* feat(textbox): add the main wrapper for the 'outside' label placement

* feat(textbox): add the 'label outside' styles

* feat(textbox): add the `Variant` param; add variant styles

* feat(input base): add the `OnFocus` event callback

* feat(textbox): add colored variant styles

* chore(components): nits

* chore(textbox): add underlined & size compound styles

* chore(textbox): adjust label scale

* chore(textbox): adjust outlined & small styles; removed shadow for underlined variant

* feat(textbox): add the `StartContent` and `EndContent` params

* feat(textbox): add support of the clear button

* feat(textbox): add (data)attributes

* feat(textbox): add the `Type` param; add support of different input types

* feat(textbox): add the `Description` param; add helper wrapper

* feat(textbox): add the `Invalid` param; add support for error messages

* chore(input): comment out the check for `ValueExpression` for the time being

* chore(textbox): rename slot 'root' => 'base'

* feat(textbox): add the `Required` param; add required styles

* feat(textbox): add support for the slots styling

* feat(textbox): add support for `oninput` or `onchange` behaviors

* feat(textbox): add support for debounced value input

* chore(textbox): add missing data attributes

* fix(debounced input): debounce only if the delay is greater than zero

* chore(debounced input): nits

* feat(textbox): add `data-hidden` attribute for hidden input type

* test(textbox): add tests; update tests for InputBase
  • Loading branch information
desmondinho authored Aug 6, 2024
1 parent 90b851c commit 6b9b85c
Show file tree
Hide file tree
Showing 15 changed files with 1,635 additions and 30 deletions.
23 changes: 23 additions & 0 deletions src/LumexUI/Common/Enums/InputBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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

namespace LumexUI.Common;

/// <summary>
/// Specifies when the input component updates its value and triggers validation.
/// </summary>
public enum InputBehavior
{
/// <summary>
/// Updates the value and triggers validation
/// on each input event (e.g., when the user types in the input field).
/// </summary>
OnInput,

/// <summary>
/// Updates the value and triggers validation
/// on change events (e.g., when the input field loses focus or the user presses enter).
/// </summary>
OnChange
}
97 changes: 97 additions & 0 deletions src/LumexUI/Common/Enums/InputType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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;

/// <summary>
/// Specifies the different types of input elements.
/// </summary>
public enum InputType
{
/// <summary>
/// A text input field.
/// </summary>
[Description( "text" )]
Text,

/// <summary>
/// A password input field.
/// </summary>
[Description( "password" )]
Password,

/// <summary>
/// An email input field.
/// </summary>
[Description( "email" )]
Email,

/// <summary>
/// A hidden input field.
/// </summary>
[Description( "hidden" )]
Hidden,

/// <summary>
/// A number input field.
/// </summary>
[Description( "number" )]
Number,

/// <summary>
/// A search input field.
/// </summary>
[Description( "search" )]
Search,

/// <summary>
/// A telephone input field.
/// </summary>
[Description( "tel" )]
Telephone,

/// <summary>
/// A URL input field.
/// </summary>
[Description( "url" )]
Url,

/// <summary>
/// A color input field.
/// </summary>
[Description( "color" )]
Color,

/// <summary>
/// A date input field.
/// </summary>
[Description( "date" )]
Date,

/// <summary>
/// A date-time input field (local time).
/// </summary>
[Description( "datetime-local" )]
DateTimeLocal,

/// <summary>
/// A month input field.
/// </summary>
[Description( "month" )]
Month,

/// <summary>
/// A time input field.
/// </summary>
[Description( "time" )]
Time,

/// <summary>
/// A week input field.
/// </summary>
[Description( "week" )]
Week
}
26 changes: 26 additions & 0 deletions src/LumexUI/Common/Enums/InputVariant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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

namespace LumexUI.Common;

/// <summary>
/// Specifies the different variants for an input component.
/// </summary>
public enum InputVariant
{
/// <summary>
/// A flat variant input.
/// </summary>
Flat,

/// <summary>
/// An outlined variant input.
/// </summary>
Outlined,

/// <summary>
/// An underlined variant input.
/// </summary>
Underlined
}
17 changes: 17 additions & 0 deletions src/LumexUI/Common/Enums/LabelPlacement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace LumexUI;

/// <summary>
/// Specifies the placement options for the label of an input component.
/// </summary>
public enum LabelPlacement
{
/// <summary>
/// Places the label inside the input component.
/// </summary>
Inside,

/// <summary>
/// Places the label outside the input component.
/// </summary>
Outside
}
125 changes: 125 additions & 0 deletions src/LumexUI/Components/Bases/LumexDebouncedInputBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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 Microsoft.AspNetCore.Components;

namespace LumexUI;

/// <summary>
/// Represents a base class for input components with debounced value updates.
/// </summary>
/// <typeparam name="TValue">The type of the input value.</typeparam>
public abstract class LumexDebouncedInputBase<TValue> : LumexInputBase<TValue>, IDisposable
{
/// <summary>
/// Gets or sets the delay, in milliseconds, for debouncing input events.
/// </summary>
[Parameter] public int DebounceDelay { get; set; }

private readonly Debouncer _debouncer;

private bool _disposed;

/// <summary>
/// Initializes a new instance of the <see cref="LumexDebouncedInputBase{TValue}"/>.
/// </summary>
public LumexDebouncedInputBase()
{
_debouncer = new Debouncer();
}

/// <summary>
/// Handles the input event asynchronously, applying a debounce delay if provided.
/// </summary>
/// <param name="args">The change event arguments.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous value input operation.</returns>
protected virtual Task OnInputAsync( ChangeEventArgs args )
{
if( DebounceDelay != 0 )
{
return _debouncer.DebounceAsync( SetCurrentValueAsStringAsync, (string?)args.Value, DebounceDelay );
}

return SetCurrentValueAsStringAsync( (string?)args.Value );
}

/// <summary>
/// Handles the change event asynchronously.
/// </summary>
/// <param name="args">The change event arguments.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous value change operation.</returns>
protected virtual Task OnChangeAsync( ChangeEventArgs args )
{
return SetCurrentValueAsStringAsync( (string?)args.Value );
}

/// <inheritdoc />
public void Dispose()
{
Dispose( disposing: true );
GC.SuppressFinalize( this );
}

/// <inheritdoc cref="IDisposable.Dispose" />
protected virtual void Dispose( bool disposing )
{
if( !_disposed )
{
if( disposing )
{
_debouncer.Dispose();
}

_disposed = true;
}
}

/// <summary>
/// Represents a debouncer for handling debounced input events.
/// </summary>
private sealed class Debouncer : IDisposable
{
private bool _disposed;
private CancellationTokenSource? _cts;

public async Task DebounceAsync( Func<string?, Task> 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;
}
}

/// <inheritdoc />
public void Dispose()
{
Dispose( disposing: true );
GC.SuppressFinalize( this );
}

private void Dispose( bool disposing )
{
if( !_disposed )
{
if( disposing )
{
_cts?.Cancel();
_cts?.Dispose();
}

_disposed = true;
}
}
}
}
Loading

0 comments on commit 6b9b85c

Please sign in to comment.