Skip to content

Commit

Permalink
feat(components): add popover component (#60)
Browse files Browse the repository at this point in the history
* feat(utils): add an ID generator

* update .editorconfig

* feat(popover): initial implementation

* refactor(extensions): nits

* feat(popover): allow only one popover to be shown at a time

* feat(popover): add outside click event and handler

* feat(popover): allow configure the placement

* feat(popover): add offset configuration

* feat(popover): add arrow visibility configuration

* refactor(scripts): improve scripts code and distribution

* feat(popover): add color configuration

* feat(popover): add text size configuration

* feat(popover): add border radius configuration

* feat(popover): add shadow configuration

* chore(components): nits

* feat(popover): add slots styling support

* chore(button): add focus ring

* test(popover): add tests

* test(components): fix broken tests

* chore(popover): XML docs nits
  • Loading branch information
desmondinho authored Aug 19, 2024
1 parent 3c8c47a commit b6232df
Show file tree
Hide file tree
Showing 40 changed files with 3,185 additions and 48 deletions.
14 changes: 7 additions & 7 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning

# Expression-bodied members
csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_accessors = false
csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_indexers = false
csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
csharp_style_expression_bodied_methods = true
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_lambdas = false
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = false

# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:warning
Expand Down
30 changes: 30 additions & 0 deletions src/LumexUI.Utilities/Identifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.Utilities;

public static class Identifier
{
private static readonly Random _rnd = new();

/// <summary>
/// Generates a new small Id. For example, 'f127d9edf14385adb'.
/// </summary>
/// <remarks>HTML id must start with a letter.</remarks>
/// <returns>A <see cref="string"/> that represents the generated ID.</returns>
public static string New( int length = 8 )
{
if( length > 16 )
{
throw new ArgumentOutOfRangeException( nameof( length ), "length must be less than 16" );
}

if( length <= 8 )
{
return $"f{_rnd.Next():x}";
}

return $"f{_rnd.Next():x}{_rnd.Next():x}"[..length];
}
}
85 changes: 85 additions & 0 deletions src/LumexUI/Common/Enums/PopoverPlacement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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 placement options for a popover component.
/// </summary>
public enum PopoverPlacement
{
/// <summary>
/// Places the popover above the reference.
/// </summary>
[Description( "top" )]
Top,

/// <summary>
/// Places the popover above the reference, aligned to the start (left) of the reference.
/// </summary>
[Description( "top-start" )]
TopStart,

/// <summary>
/// Places the popover above the reference, aligned to the end (right) of the reference.
/// </summary>
[Description( "top-end" )]
TopEnd,

/// <summary>
/// Places the popover to the right of the reference.
/// </summary>
[Description( "right" )]
Right,

/// <summary>
/// Places the popover to the right of the reference, aligned to the start (top) of the reference.
/// </summary>
[Description( "right-start" )]
RightStart,

/// <summary>
/// Places the popover to the right of the reference, aligned to the end (bottom) of the reference.
/// </summary>
[Description( "right-end" )]
RightEnd,

/// <summary>
/// Places the popover below the reference.
/// </summary>
[Description( "bottom" )]
Bottom,

/// <summary>
/// Places the popover below the reference, aligned to the start (left) of the reference.
/// </summary>
[Description( "bottom-start" )]
BottomStart,

/// <summary>
/// Places the popover below the reference, aligned to the end (right) of the reference.
/// </summary>
[Description( "bottom-end" )]
BottomEnd,

/// <summary>
/// Places the popover to the left of the reference.
/// </summary>
[Description( "left" )]
Left,

/// <summary>
/// Places the popover to the left of the reference, aligned to the start (top) of the reference.
/// </summary>
[Description( "left-start" )]
LeftStart,

/// <summary>
/// Places the popover to the left of the reference, aligned to the end (bottom) of reference.
/// </summary>
[Description( "left-end" )]
LeftEnd
}
2 changes: 1 addition & 1 deletion src/LumexUI/Components/Bases/LumexInputFieldBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public abstract partial class LumexInputFieldBase<TValue> : LumexDebouncedInputB
ISlotComponent<InputFieldSlots>,
IAsyncDisposable
{
private const string JavaScriptFile = "./_content/LumexUI/js/input.js";
private const string JavaScriptFile = "./_content/LumexUI/js/components/input.js";

/// <summary>
/// Gets or sets content to be rendered at the start of the textbox.
Expand Down
2 changes: 1 addition & 1 deletion src/LumexUI/Components/Card/LumexCard.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public partial class LumexCard : LumexComponentBase, ISlotComponent<CardSlots>
/// Gets or sets the shadow of the card.
/// </summary>
/// <remarks>
/// Default value is <see cref="Shadow.Medium"/>
/// Default value is <see cref="Shadow.Small"/>
/// </remarks>
[Parameter] public Shadow Shadow { get; set; } = Shadow.Small;

Expand Down
2 changes: 1 addition & 1 deletion src/LumexUI/Components/Navbar/LumexNavbarMenu.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ protected override async Task OnAfterRenderAsync( bool firstRender )
{
if( firstRender )
{
await _collapse!.ElementReference.MoveToAsync();
await _collapse!.ElementReference.PortalToAsync();
}
}
}
11 changes: 1 addition & 10 deletions src/LumexUI/Components/Numbox/LumexNumbox.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,11 @@ public partial class LumexNumbox<TValue> : LumexInputFieldBase<TValue>
{
private static readonly string _stepAttributeValue = GetStepAttributeValue();

/// <inheritdoc />
public override Task SetParametersAsync( ParameterView parameters )
{
parameters.SetParameterProperties( this );

UpdateAdditionalAttributes();

return base.SetParametersAsync( parameters );
}

/// <inheritdoc />
protected override void OnParametersSet()
{
SetInputType( "number" );
UpdateAdditionalAttributes();

base.OnParametersSet();
}
Expand Down
6 changes: 6 additions & 0 deletions src/LumexUI/Components/Popover/LumexPopover.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@namespace LumexUI
@inherits LumexComponentBase

<CascadingValue TValue="PopoverContext" Value="@_context" IsFixed="@true">
@ChildContent
</CascadingValue>
161 changes: 161 additions & 0 deletions src/LumexUI/Components/Popover/LumexPopover.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// 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 LumexUI.Services;
using LumexUI.Utilities;

using Microsoft.AspNetCore.Components;

namespace LumexUI;

/// <summary>
/// A component representing a popover, providing a floating container
/// that displays additional content or information.
/// </summary>
public partial class LumexPopover : LumexComponentBase, ISlotComponent<PopoverSlots>, IDisposable
{
/// <summary>
/// Gets or sets content to be rendered inside the popover.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

/// <summary>
/// Gets or sets a color of the popover.
/// </summary>
/// <remarks>
/// The default value is <see cref="ThemeColor.Default"/>
/// </remarks>
[Parameter] public ThemeColor Color { get; set; } = ThemeColor.Default;

/// <summary>
/// Gets or sets a size of the popover content text.
/// </summary>
/// <remarks>
/// The default value is <see cref="Size.Medium"/>
/// </remarks>
[Parameter] public Size Size { get; set; } = Size.Medium;

/// <summary>
/// Gets or sets a border radius of the popover.
/// </summary>
/// <remarks>
/// The default value is <see cref="Radius.Large"/>
/// </remarks>
[Parameter] public Radius Radius { get; set; } = Radius.Large;

/// <summary>
/// Gets or sets a shadow of the popover.
/// </summary>
/// <remarks>
/// Default value is <see cref="Shadow.Small"/>
/// </remarks>
[Parameter] public Shadow Shadow { get; set; } = Shadow.Small;

/// <summary>
/// Gets or sets a placement of the popover relative to a reference.
/// </summary>
/// <remarks>
/// The default value is <see cref="PopoverPlacement.Top"/>
/// </remarks>
[Parameter] public PopoverPlacement Placement { get; set; }

/// <summary>
/// Gets or sets the offset distance between the popover and the reference, in pixels.
/// </summary>
/// <remarks>
/// The default value is 8
/// </remarks>
[Parameter] public int Offset { get; set; } = 8;

/// <summary>
/// Gets or sets a value indicating whether the popover should display an arrow pointing to the reference.
/// </summary>
[Parameter] public bool ShowArrow { get; set; }

/// <summary>
/// Gets or sets the CSS class names for the popover slots.
/// </summary>
[Parameter] public PopoverSlots? Classes { get; set; }

[Inject] private IPopoverService PopoverService { get; set; } = default!;

internal string Id { get; private set; } = Identifier.New();
internal bool IsShown { get; private set; }
internal PopoverOptions Options { get; private set; }

private readonly PopoverContext _context;
private bool _disposed;

/// <summary>
/// Initializes a new instance of the <see cref="LumexPopover"/>.
/// </summary>
public LumexPopover()
{
_context = new PopoverContext( this );
}

internal bool Show()
{
if( PopoverService.LastShown == this )
{
PopoverService.SetLastShown( null );
return false;
}

IsShown = true;
PopoverService.SetLastShown( this );
return true;
}

internal void Hide()
{
IsShown = false;
StateHasChanged();
}

/// <inheritdoc />
protected override void OnInitialized()
{
PopoverService.Register( this );
}

/// <inheritdoc />
protected override void OnParametersSet()
{
Options = new PopoverOptions( this );
}

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

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

_disposed = true;
}
}

/// <summary>
/// Represents the configuration options for a <see cref="LumexPopover"/> component.
/// </summary>
/// <param name="popover">The <see cref="LumexPopover"/> instance associated with these options.</param>
internal readonly struct PopoverOptions( LumexPopover popover )
{
public int Offset { get; } = popover.Offset;
public bool ShowArrow { get; } = popover.ShowArrow;
public string Placement { get; } = popover.Placement.ToDescription();
}
}
26 changes: 26 additions & 0 deletions src/LumexUI/Components/Popover/LumexPopoverContent.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@namespace LumexUI
@inherits LumexComponentBase

@if( Context.Owner.IsShown )
{
<div style="position: absolute; z-index: 10000;"
data-popover="@Context.Owner.Id"
@onclickoutside="@(() => ClickOutsideAsync())">
<div class="@InnerWrapperClass">
<div role="dialog" tabindex="-1" data-slot="base">
<LumexComponent As="@As"
Class="@RootClass"
Style="@RootStyle"
data-slot="content"
@attributes="@AdditionalAttributes">
@ChildContent
</LumexComponent>
</div>

@if( Context.Owner.ShowArrow )
{
<span class="@ArrowClass" data-slot="arrow" />
}
</div>
</div>
}
Loading

0 comments on commit b6232df

Please sign in to comment.