Skip to content

Commit 956eec6

Browse files
committed
Port Skip link to IComponentGenerator
1 parent 621cb33 commit 956eec6

File tree

10 files changed

+85
-128
lines changed

10 files changed

+85
-128
lines changed

docs/components/skip-link.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020

2121
The content is the HTML to use within the generated link.
2222

23-
| Attribute | Type | Description |
24-
| --- | --- | --- |
25-
| `href` | `string` | The 'href' attribute for the link. The default is '#content'. Cannot be `null` or empty. |
23+
| Attribute | Type | Description |
24+
| --- | --- |-------------------------------------------------------------------------------|
25+
| `href` | `string` | The 'href' attribute for the link. If not specified, '#content' will be used. |
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using Microsoft.AspNetCore.Html;
3+
4+
namespace GovUk.Frontend.AspNetCore.ComponentGeneration;
5+
6+
public partial class DefaultComponentGenerator
7+
{
8+
internal const string SkipLinkDefaultHref = "#content";
9+
internal const string SkipLinkElement = "a";
10+
11+
/// <inheritdoc/>
12+
public virtual HtmlTagBuilder GenerateSkipLink(SkipLinkOptions options)
13+
{
14+
ArgumentNullException.ThrowIfNull(options);
15+
options.Validate();
16+
17+
return new HtmlTagBuilder(SkipLinkElement)
18+
.WithAttribute("href", options.Href ?? new HtmlString(SkipLinkDefaultHref))
19+
.WithCssClass("govuk-skip-link")
20+
.WithCssClasses(ExplodeClasses(options.Classes?.ToHtmlString()))
21+
.WithAttributes(options.Attributes)
22+
.WithAttribute("data-module", "govuk-skip-link", encodeValue: false)
23+
.WithAppendedHtml(GetEncodedTextOrHtml(options.Text, options.Html)!);
24+
}
25+
}

src/GovUk.Frontend.AspNetCore/ComponentGeneration/IComponentGenerator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ public interface IComponentGenerator
9595
/// <returns>An <see cref="HtmlTagBuilder"/> with the component's HTML.</returns>
9696
HtmlTagBuilder GeneratePhaseBanner(PhaseBannerOptions options);
9797

98+
/// <summary>
99+
/// Generates a skip link component.
100+
/// </summary>
101+
/// <returns>An <see cref="HtmlTagBuilder"/> with the component's HTML.</returns>
102+
HtmlTagBuilder GenerateSkipLink(SkipLinkOptions options);
103+
98104
/// <summary>
99105
/// Generates a tag component.
100106
/// </summary>

src/GovUk.Frontend.AspNetCore/ComponentGeneration/SkipLinkOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,12 @@ public class SkipLinkOptions
1111
public IHtmlContent? Href { get; set; }
1212
public IHtmlContent? Classes { get; set; }
1313
public EncodedAttributesDictionary? Attributes { get; set; }
14+
15+
internal void Validate()
16+
{
17+
if (Html.NormalizeEmptyString() is null && Text.NormalizeEmptyString() is null)
18+
{
19+
throw new InvalidOptionsException(GetType(), $"{nameof(Html)} or {nameof(Text)} must be specified.");
20+
}
21+
}
1422
}

src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.SkipLink.cs

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,6 @@ TagBuilder GenerateSelect(
115115
IEnumerable<SelectItem> items,
116116
AttributeDictionary attributes);
117117

118-
TagBuilder GenerateSkipLink(string href, IHtmlContent content, AttributeDictionary attributes);
119-
120118
TagBuilder GenerateSummaryCard(SummaryCardTitle title, SummaryListActions actions, IHtmlContent summaryList, AttributeDictionary attributes);
121119

122120
TagBuilder GenerateSummaryList(IEnumerable<SummaryListRow> rows, AttributeDictionary attributes);
Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
using System;
12
using System.Threading.Tasks;
2-
using GovUk.Frontend.AspNetCore.HtmlGeneration;
3-
using Microsoft.AspNetCore.Mvc.TagHelpers;
3+
using GovUk.Frontend.AspNetCore.ComponentGeneration;
44
using Microsoft.AspNetCore.Razor.TagHelpers;
55

66
namespace GovUk.Frontend.AspNetCore.TagHelpers;
@@ -9,61 +9,54 @@ namespace GovUk.Frontend.AspNetCore.TagHelpers;
99
/// Generates a GDS skip link component.
1010
/// </summary>
1111
[HtmlTargetElement(TagName)]
12-
[OutputElementHint(ComponentGenerator.SkipLinkElement)]
12+
[OutputElementHint(DefaultComponentGenerator.SkipLinkElement)]
1313
public class SkipLinkTagHelper : TagHelper
1414
{
1515
internal const string TagName = "govuk-skip-link";
1616

1717
private const string HrefAttributeName = "href";
1818

19-
private readonly IGovUkHtmlGenerator _htmlGenerator;
20-
21-
private string _href = ComponentGenerator.SkipLinkDefaultHref;
19+
private readonly IComponentGenerator _componentGenerator;
2220

2321
/// <summary>
2422
/// Creates a new <see cref="BackLinkTagHelper"/>.
2523
/// </summary>
26-
public SkipLinkTagHelper()
27-
: this(htmlGenerator: null)
28-
{
29-
}
30-
31-
internal SkipLinkTagHelper(IGovUkHtmlGenerator? htmlGenerator)
24+
public SkipLinkTagHelper(IComponentGenerator componentGenerator)
3225
{
33-
_htmlGenerator = htmlGenerator ?? new ComponentGenerator();
26+
ArgumentNullException.ThrowIfNull(componentGenerator);
27+
_componentGenerator = componentGenerator;
3428
}
3529

3630
/// <summary>
3731
/// The <c>href</c> attribute for the link.
3832
/// </summary>
3933
/// <remarks>
40-
/// The default is <c>&quot;#content&quot;</c>.
41-
/// Cannot be <c>null</c> or empty.
34+
/// If not specified, <c>#content</c> will be used.
4235
/// </remarks>
4336
[HtmlAttributeName(HrefAttributeName)]
44-
public string Href
45-
{
46-
get => _href;
47-
set => _href = Guard.ArgumentNotNullOrEmpty(nameof(value), value);
48-
}
37+
public string? Href { get; set; }
4938

5039
/// <inheritdoc/>
5140
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
5241
{
53-
var childContent = await output.GetChildContentAsync();
42+
var childContent = (await output.GetChildContentAsync()).Snapshot();
5443

5544
if (output.Content.IsModified)
5645
{
5746
childContent = output.Content;
5847
}
5948

60-
var tagBuilder = _htmlGenerator.GenerateSkipLink(Href, childContent, output.Attributes.ToAttributeDictionary());
49+
var attributes = new EncodedAttributesDictionary(output.Attributes);
50+
attributes.Remove("class", out var classes);
6151

62-
output.TagName = tagBuilder.TagName;
63-
output.TagMode = TagMode.StartTagAndEndTag;
52+
var component = _componentGenerator.GenerateSkipLink(new SkipLinkOptions
53+
{
54+
Html = childContent,
55+
Href = Href.ToHtmlContent(),
56+
Classes = classes,
57+
Attributes = attributes
58+
});
6459

65-
output.Attributes.Clear();
66-
output.MergeAttributes(tagBuilder);
67-
output.Content.SetHtmlContent(tagBuilder.InnerHtml);
60+
component.WriteTo(output);
6861
}
6962
}

tests/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.SkipLink.cs

Lines changed: 0 additions & 28 deletions
This file was deleted.

tests/GovUk.Frontend.AspNetCore.Tests/ComponentGeneration/ComponentTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ public void Pagination(ComponentTestCaseData<PaginationOptions> data) =>
135135
data,
136136
(generator, options) => generator.GeneratePagination(options).ToHtmlString());
137137

138+
[Theory]
139+
[ComponentFixtureData("skip-link", typeof(SkipLinkOptions))]
140+
public void SkipLink(ComponentTestCaseData<SkipLinkOptions> data) =>
141+
CheckComponentHtmlMatchesExpectedHtml(
142+
data,
143+
(generator, options) => generator.GenerateSkipLink(options).ToHtmlString());
144+
138145
[Theory]
139146
[ComponentFixtureData("task-list", typeof(TaskListOptions))]
140147
public void TaskList(ComponentTestCaseData<TaskListOptions> data) =>
Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
using System.Collections.Generic;
22
using System.Threading.Tasks;
3+
using GovUk.Frontend.AspNetCore.ComponentGeneration;
34
using GovUk.Frontend.AspNetCore.TagHelpers;
45
using GovUk.Frontend.AspNetCore.TestCommon;
56
using Microsoft.AspNetCore.Razor.TagHelpers;
7+
using Moq;
68
using Xunit;
79

810
namespace GovUk.Frontend.AspNetCore.Tests.TagHelpers;
911

1012
public class SkipLinkTagHelperTests
1113
{
1214
[Fact]
13-
public async Task ProcessAsync_WithHrefSpecified_GeneratesExpectedOutput()
15+
public async Task ProcessAsync_InvokesComponentGeneratorWithExpectedOptions()
1416
{
1517
// Arrange
18+
var href = "#main";
19+
var content = "Link content";
20+
1621
var context = new TagHelperContext(
1722
tagName: "govuk-skip-link",
1823
allAttributes: new TagHelperAttributeList(),
@@ -25,53 +30,25 @@ public async Task ProcessAsync_WithHrefSpecified_GeneratesExpectedOutput()
2530
getChildContentAsync: (useCachedResult, encoder) =>
2631
{
2732
var tagHelperContent = new DefaultTagHelperContent();
28-
tagHelperContent.SetContent("Link content");
33+
tagHelperContent.SetContent(content);
2934
return Task.FromResult<TagHelperContent>(tagHelperContent);
3035
});
3136

32-
var tagHelper = new SkipLinkTagHelper()
37+
var componentGeneratorMock = new Mock<DefaultComponentGenerator>() { CallBase = true };
38+
SkipLinkOptions? actualOptions = null;
39+
componentGeneratorMock.Setup(mock => mock.GenerateSkipLink(It.IsAny<SkipLinkOptions>())).Callback<SkipLinkOptions>(o => actualOptions = o);
40+
41+
var tagHelper = new SkipLinkTagHelper(componentGeneratorMock.Object)
3342
{
34-
Href = "#main"
43+
Href = href
3544
};
3645

3746
// Act
3847
await tagHelper.ProcessAsync(context, output);
3948

4049
// Assert
41-
var expectedHtml = @"
42-
<a class=""govuk-skip-link"" href=""#main"" data-module=""govuk-skip-link"">Link content</a>";
43-
44-
AssertEx.HtmlEqual(expectedHtml, output.ToHtmlString());
45-
}
46-
47-
[Fact]
48-
public async Task ProcessAsync_WithNoHrefSpecified_UsesDefaultHref()
49-
{
50-
// Arrange
51-
var context = new TagHelperContext(
52-
tagName: "govuk-skip-link",
53-
allAttributes: new TagHelperAttributeList(),
54-
items: new Dictionary<object, object>(),
55-
uniqueId: "test");
56-
57-
var output = new TagHelperOutput(
58-
"govuk-skip-link",
59-
attributes: new TagHelperAttributeList(),
60-
getChildContentAsync: (useCachedResult, encoder) =>
61-
{
62-
var tagHelperContent = new DefaultTagHelperContent();
63-
tagHelperContent.SetContent("Link content");
64-
return Task.FromResult<TagHelperContent>(tagHelperContent);
65-
});
66-
67-
var tagHelper = new SkipLinkTagHelper();
68-
69-
// Act
70-
await tagHelper.ProcessAsync(context, output);
71-
72-
// Assert
73-
var element = output.RenderToElement();
74-
75-
Assert.Equal("#content", element.GetAttribute("href"));
50+
Assert.NotNull(actualOptions);
51+
Assert.Equal(href, actualOptions.Href?.ToHtmlString());
52+
Assert.Equal(content, actualOptions.Html?.ToHtmlString());
7653
}
7754
}

0 commit comments

Comments
 (0)