Skip to content

Commit

Permalink
feat(components): add navlink component (#46)
Browse files Browse the repository at this point in the history
* feat(components): initial implementation of navlink component

* feat(components): add Disabled and Color parameters to the navlink component

* chore(components): update the XML docs in the navlink component

* chore(components): nits

* feat(components): add link base class; inherit link and navlink components from it

* chore(components): add navlink styles

* feat(components): remove the `ActiveClass` parameter from the navlink component; add active state styles; simplify link styles

* test(components): add/update the tests for both `Link` and `NavLink` components
  • Loading branch information
desmondinho authored Jul 19, 2024
1 parent 9675787 commit 65c34cb
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 33 deletions.
34 changes: 34 additions & 0 deletions src/LumexUI/Components/Bases/LumexLinkBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using LumexUI.Common;

using Microsoft.AspNetCore.Components;

namespace LumexUI;

/// <summary>
/// A base class for link components.
/// </summary>
public abstract class LumexLinkBase : LumexComponentBase
{
/// <summary>
/// Gets or sets content to be rendered inside the link.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

/// <summary>
/// Gets or sets a value representing the URL of the link.
/// </summary>
[Parameter] public string Href { get; set; } = "#";

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

/// <summary>
/// Gets or sets a value indicating whether the link is disabled.
/// </summary>
[Parameter] public bool Disabled { get; set; }
}
4 changes: 2 additions & 2 deletions src/LumexUI/Components/Link/LumexLink.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@namespace LumexUI
@inherits LumexComponentBase
@inherits LumexLinkBase

<LumexComponent As="a"
<LumexComponent As="@As"
Class="@RootClass"
Style="@RootStyle"
@attributes="@Attributes">
Expand Down
45 changes: 19 additions & 26 deletions src/LumexUI/Components/Link/LumexLink.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,8 @@

namespace LumexUI;

public partial class LumexLink : LumexComponentBase
public partial class LumexLink : LumexLinkBase
{
/// <summary>
/// Gets or sets content to be rendered inside the link.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

/// <summary>
/// Gets or sets a value representing the URL route to be navigated to.
/// </summary>
[Parameter] public string Href { get; set; } = "#";

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

/// <summary>
/// Gets or sets the underline style for the link.
/// </summary>
Expand All @@ -37,11 +19,6 @@ public partial class LumexLink : LumexComponentBase
/// </remarks>
[Parameter] public Underline Underline { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the link is disabled.
/// </summary>
[Parameter] public bool Disabled { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the link should open in the new tab.
/// </summary>
Expand All @@ -57,9 +34,9 @@ private IReadOnlyDictionary<string, object> Attributes
{
get
{
var attributes = new Dictionary<string, object>( AdditionalAttributes ?? new Dictionary<string, object>() )
var attributes = new Dictionary<string, object>()
{
{ "href", Href }
["href"] = Href
};

if( External )
Expand All @@ -68,7 +45,23 @@ private IReadOnlyDictionary<string, object> Attributes
attributes["rel"] = "noopener noreferrer";
}

if( AdditionalAttributes is not null )
{
foreach( var attribute in AdditionalAttributes )
{
attributes[attribute.Key] = attribute.Value;
}
}

return attributes;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="LumexLink"/>.
/// </summary>
public LumexLink()
{
As = "a";
}
}
14 changes: 14 additions & 0 deletions src/LumexUI/Components/Link/LumexNavLink.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@namespace LumexUI
@inherits LumexLinkBase

<NavLink Match="@Match"
ActiveClass="@string.Empty"
href="@Href"
class="@RootClass"
style="@RootStyle"
data-disabled="@Utils.GetDataAttributeValue( Disabled )"
data-active="@Utils.GetDataAttributeValue( _isActive )"
@attributes="@AdditionalAttributes"
@ref="@_navLink">
@ChildContent
</NavLink>
46 changes: 46 additions & 0 deletions src/LumexUI/Components/Link/LumexNavLink.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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.Runtime.CompilerServices;

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;

namespace LumexUI;

/// <summary>
/// A component representing a navigation link within the application.
/// </summary>
public partial class LumexNavLink : LumexLinkBase
{
/// <summary>
/// Gets or sets a value representing the URL matching behavior.
/// </summary>
/// <remarks>
/// The default value is <see cref="NavLinkMatch.All"/>
/// </remarks>
[Parameter] public NavLinkMatch Match { get; set; } = NavLinkMatch.All;

private protected override string? RootClass =>
TwMerge.Merge( Styles.NavLink.GetStyles( this ) );

private NavLink? _navLink;
private bool _isActive;

/// <inheritdoc />
protected override void OnAfterRender( bool firstRender )
{
if( !_isActive )
{
if( GetActiveState( _navLink! ) )
{
_isActive = true;
StateHasChanged();
}
}
}

[UnsafeAccessor( UnsafeAccessorKind.Field, Name = "_isActive" )]
private static extern ref bool GetActiveState( NavLink navLink );
}
37 changes: 33 additions & 4 deletions src/LumexUI/Styles/Link.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace LumexUI.Styles;

[ExcludeFromCodeCoverage]
internal readonly record struct Link
internal readonly record struct LinkBase
{
private readonly static string _base = ElementClass.Empty()
.Add( "relative" )
Expand Down Expand Up @@ -38,6 +38,20 @@ private static ElementClass GetColorStyles( ThemeColor color )
.Add( "text-info", when: color is ThemeColor.Info );
}

public static string GetStyles( LumexLinkBase link )
{
return ElementClass.Empty()
.Add( _base )
.Add( _disabled, when: link.Disabled )
.Add( GetColorStyles( link.Color ) )
.Add( link.Class )
.ToString();
}
}

[ExcludeFromCodeCoverage]
internal readonly record struct Link
{
private static ElementClass GetUnderlineStyles( Underline underline )
{
return ElementClass.Empty()
Expand All @@ -50,11 +64,26 @@ private static ElementClass GetUnderlineStyles( Underline underline )
public static string GetStyles( LumexLink link )
{
return ElementClass.Empty()
.Add( _base )
.Add( _disabled, when: link.Disabled )
.Add( GetColorStyles( link.Color ) )
.Add( LinkBase.GetStyles( link ) )
.Add( GetUnderlineStyles( link.Underline ) )
.Add( link.Class )
.ToString();
}
}

[ExcludeFromCodeCoverage]
internal readonly record struct NavLink
{
private readonly static string _active = ElementClass.Empty()
.Add( "data-[active=true]:font-semibold" )
.ToString();

public static string GetStyles( LumexNavLink navLink )
{
return ElementClass.Empty()
.Add( LinkBase.GetStyles( navLink ) )
.Add( _active )
.Add( navLink.Class )
.ToString();
}
}
17 changes: 16 additions & 1 deletion tests/LumexUI.Tests/Components/Link/LinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void Link_ShouldRenderCorrectly()
}

[Fact]
public void Link_External_ShouldHaveCorrectAttributes()
public void Link_External_ShouldSetCorrectAttributes()
{
var cut = RenderComponent<LumexLink>( p => p
.Add( p => p.External, true )
Expand All @@ -35,4 +35,19 @@ public void Link_External_ShouldHaveCorrectAttributes()
link.GetAttribute( "target" ).Should().Be( "_blank" );
link.GetAttribute( "rel" ).Should().Be( "noopener noreferrer" );
}

[Fact]
public void Link_AdditionalAttributes_ShouldSetAttributes()
{
var attributes = new Dictionary<string, object>
{
["data-custom"] = "custom-attribute-value"
};

var cut = RenderComponent<LumexLink>( p => p
.Add( p => p.AdditionalAttributes, attributes )
);

cut.Find( "a" ).GetAttribute( "data-custom" ).Should().NotBeNull();
}
}
43 changes: 43 additions & 0 deletions tests/LumexUI.Tests/Components/Link/NavLinkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 Bunit.TestDoubles;

using Microsoft.Extensions.DependencyInjection;

using TailwindMerge;

namespace LumexUI.Tests.Components;

public class NavLinkTests : TestContext
{
public NavLinkTests()
{
Services.AddSingleton<TwMerge>();
}

[Fact]
public void NavLink_ShouldRenderCorrectly()
{
var action = () => RenderComponent<LumexNavLink>();

action.Should().NotThrow();
}

[Fact]
public void NavLink_IfUrlEqualsHref_ShouldBeActive()
{
var navMan = Services.GetRequiredService<FakeNavigationManager>();
var cut = RenderComponent<LumexNavLink>( p => p
.Add( p => p.Href, "some-url" )
);

navMan.NavigateTo( "some-url" );

// faking re-render on location change (NavLink base implementation)
cut.Render();

cut.Find( "a" ).GetAttribute( "data-active" ).Should().Be( "true" );
}
}

0 comments on commit 65c34cb

Please sign in to comment.