From 4df77f224e1c82af8da035de7c1b41e87816116f Mon Sep 17 00:00:00 2001 From: James Gunn Date: Wed, 7 Jun 2023 14:41:45 +0100 Subject: [PATCH] Add label-class attribute to form field tag helpers It's relatively common to have the label child tag helper specified just to add an additional CSS class. This removes the need to do that by adding a `label-class` attribute at the top-level. --- docs/components/character-count.md | 1 + docs/components/file-upload.md | 1 + docs/components/select.md | 1 + docs/components/text-input.md | 1 + docs/components/textarea.md | 1 + .../AttributeDictionaryExtensions.cs | 2 +- .../TagHelpers/CharacterCountTagHelper.cs | 9 ++++++++- .../TagHelpers/FileUploadTagHelper.cs | 9 ++++++++- .../TagHelpers/FormGroupTagHelperBase.cs | 6 ++++-- .../TagHelpers/SelectTagHelper.cs | 9 ++++++++- .../TagHelpers/TextAreaTagHelper.cs | 9 ++++++++- .../TagHelpers/TextInputTagHelper.cs | 13 ++++++++++--- .../TagHelpers/CharacterCountTagHelperTests.cs | 5 +++-- .../TagHelpers/FileUploadTagHelperTests.cs | 5 +++-- .../TagHelpers/FormGroupTagHelperBaseTests.cs | 10 +++++----- .../TagHelpers/SelectTagHelperTests.cs | 5 +++-- .../TagHelpers/TextAreaTagHelperTests.cs | 5 +++-- .../TagHelpers/TextInputTagHelperTests.cs | 5 +++-- 18 files changed, 72 insertions(+), 25 deletions(-) diff --git a/docs/components/character-count.md b/docs/components/character-count.md index bd97660b..377610ba 100644 --- a/docs/components/character-count.md +++ b/docs/components/character-count.md @@ -33,6 +33,7 @@ Check out the [max words validator](../validation/maxwords.md) for adding server | `form-group-*` | | Additional attributes to add to the generated form-group wrapper element. | | `id` | `string` | The `id` attribute for the generated `textarea` element. If not specified then a value is generated from the `name` attribute. | | `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. | +| `label-class` | `string` | Additional classes for the generated `label` element. | | `max-length` | `int?` | The maximum number of characters the generated `textarea` may contain. Required unless the `max-words` attribute is specified. | | `max-words` | `int?` | The maximum number of words the generated `textarea` may contain. Required unless the `max-length` attribute is specified. | | `name` | `string` | The `name` attribute for the generated `textarea` element. Required unless the `asp-for` attribute is specified. | diff --git a/docs/components/file-upload.md b/docs/components/file-upload.md index 7bfc9d03..377b1923 100644 --- a/docs/components/file-upload.md +++ b/docs/components/file-upload.md @@ -35,6 +35,7 @@ | `id` | `string` | The `id` attribute for the generated `input` element. If not specified then a value is generated from the `name` attribute. | | `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. | | `input-*` | | Additional attributes to add to the generated `input` element. | +| `label-class` | `string` | Additional classes for the generated `label` element. | | `name` | `string` | The `name` attribute for the generated `input` element. Required unless the `asp-for` attribute is specified. | ### `` diff --git a/docs/components/select.md b/docs/components/select.md index ab4d79c8..3b44ebf5 100644 --- a/docs/components/select.md +++ b/docs/components/select.md @@ -27,6 +27,7 @@ | `disabled` | `bool` | Whether the element should be disabled. The default is `false`. | | `id` | `string` | The `id` attribute for the generated `select` element. If not specified then a value is generated from the `name` attribute. | | `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. | +| `label-class` | `string` | Additional classes for the generated `label` element. | | `name` | `string` | The `name` attribute for the generated `select` element. Required unless the `asp-for` attribute is specified. | | `select-*` | | Additional attributes to add to the generated `select` element. | diff --git a/docs/components/text-input.md b/docs/components/text-input.md index 100dd6b2..cac871c8 100644 --- a/docs/components/text-input.md +++ b/docs/components/text-input.md @@ -53,6 +53,7 @@ The content is the HTML to use within the generated component. | `id` | `string` | The `id` attribute for the generated `input` element. If not specified then a value is generated from the `name` attribute. | | `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. | | `input-*` | | Additional attributes to add to the generated `input` element. | +| `label-class` | `string` | Additional classes for the generated `label` element. | | `name` | `string` | The `name` attribute for the generated `input` element. Required unless the `asp-for` attribute is specified. | | `inputmode` | `string` | The `inputmode` attribute for the generated `input` element. | | `pattern` | `string` | The `pattern` attribute for the generated `input` element. | diff --git a/docs/components/textarea.md b/docs/components/textarea.md index a6e21c67..18c1ad23 100644 --- a/docs/components/textarea.md +++ b/docs/components/textarea.md @@ -29,6 +29,7 @@ | `disabled` | `bool` | Whether the textarea should be disabled. The default is `false`. | | `id` | `string` | The `id` attribute for the generated `textarea` element. If not specified then a value is generated from the `name` attribute. | | `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. | +| `label-class` | `string` | Additional classes for the generated `label` element. | | `name` | `string` | The `name` attribute for the generated `textarea` element. Required unless the `asp-for` attribute is specified. | | `rows` | `int` | The `rows` attribute for the generated `textarea` element. The default is `5`. | | `spellcheck` | `bool?` | The `spellcheck` attribute for the generated `textarea` element. The default is `null`. | diff --git a/src/GovUk.Frontend.AspNetCore/AttributeDictionaryExtensions.cs b/src/GovUk.Frontend.AspNetCore/AttributeDictionaryExtensions.cs index af90c72f..a090fdf1 100644 --- a/src/GovUk.Frontend.AspNetCore/AttributeDictionaryExtensions.cs +++ b/src/GovUk.Frontend.AspNetCore/AttributeDictionaryExtensions.cs @@ -16,7 +16,7 @@ public static class AttributeDictionaryExtensions /// /// The to add the CSS class name to. /// The CSS class name to add. - public static void MergeCssClass(this AttributeDictionary attributeDictionary, string value) + public static void MergeCssClass(this AttributeDictionary attributeDictionary, string? value) { Guard.ArgumentNotNull(nameof(attributeDictionary), attributeDictionary); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs index 6043d6bd..15ac1566 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs @@ -26,6 +26,7 @@ public class CharacterCountTagHelper : FormGroupTagHelperBase private const string DisabledAttributeName = "disabled"; private const string FormGroupAttributesPrefix = "form-group-"; private const string IdAttributeName = "id"; + private const string LabelClassAttributeName = "label-class"; private const string MaxLengthAttributeName = "max-length"; private const string MaxWordsLengthAttributeName = "max-words"; private const string NameAttributeName = "name"; @@ -86,6 +87,12 @@ internal CharacterCountTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IMod [HtmlAttributeName(IdAttributeName)] public string? Id { get; set; } + /// + /// Additional classes for the generated label element. + /// + [HtmlAttributeName(LabelClassAttributeName)] + public string? LabelClass { get; set; } + /// /// The maximum number of characters the generated textarea may contain. /// @@ -217,7 +224,7 @@ private protected override IHtmlContent GenerateFormGroupContent( { var contentBuilder = new HtmlContentBuilder(); - var label = GenerateLabel(formGroupContext); + var label = GenerateLabel(formGroupContext, LabelClass); contentBuilder.AppendHtml(label); var hint = GenerateHint(tagHelperContext, formGroupContext); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/FileUploadTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/FileUploadTagHelper.cs index 8d6cf715..299ff9d9 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/FileUploadTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/FileUploadTagHelper.cs @@ -20,6 +20,7 @@ public class FileUploadTagHelper : FormGroupTagHelperBase private const string AttributesPrefix = "input-"; private const string IdAttributeName = "id"; + private const string LabelClassAttributeName = "label-class"; private const string NameAttributeName = "name"; /// @@ -52,6 +53,12 @@ internal FileUploadTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHe [HtmlAttributeName(DictionaryAttributePrefix = AttributesPrefix)] public IDictionary InputAttributes { get; set; } = new Dictionary(); + /// + /// Additional classes for the generated label element. + /// + [HtmlAttributeName(LabelClassAttributeName)] + public string? LabelClass { get; set; } + /// /// The name attribute for the generated input element. /// @@ -72,7 +79,7 @@ private protected override IHtmlContent GenerateFormGroupContent( { var contentBuilder = new HtmlContentBuilder(); - var label = GenerateLabel(formGroupContext); + var label = GenerateLabel(formGroupContext, LabelClass); contentBuilder.AppendHtml(label); var hint = GenerateHint(tagHelperContext, formGroupContext); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupTagHelperBase.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupTagHelperBase.cs index 58907940..31034d3f 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupTagHelperBase.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupTagHelperBase.cs @@ -193,7 +193,7 @@ void AddErrorToFormErrorContext() } } - internal IHtmlContent GenerateLabel(FormGroupContext formGroupContext) + internal IHtmlContent GenerateLabel(FormGroupContext formGroupContext, string? labelClass) { // We need some content for the label; if AspFor is null then label content must have been specified if (AspFor == null && formGroupContext.Label?.Content == null) @@ -206,7 +206,9 @@ internal IHtmlContent GenerateLabel(FormGroupContext formGroupContext) var isPageHeading = formGroupContext.Label?.IsPageHeading ?? ComponentGenerator.LabelDefaultIsPageHeading; var content = formGroupContext.Label?.Content; - var attributes = formGroupContext.Label?.Attributes; + + var attributes = formGroupContext.Label?.Attributes?.ToAttributeDictionary() ?? new(); + attributes.MergeCssClass(labelClass); var resolvedContent = content ?? new HtmlString(HtmlEncoder.Default.Encode(ModelHelper.GetDisplayName(ViewContext!, AspFor!.ModelExplorer, AspFor.Name) ?? string.Empty)); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/SelectTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/SelectTagHelper.cs index 95cbf530..909d0f1d 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/SelectTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/SelectTagHelper.cs @@ -23,6 +23,7 @@ public class SelectTagHelper : FormGroupTagHelperBase private const string DescribedByAttributeName = "described-by"; private const string DisabledAttributeName = "disabled"; private const string IdAttributeName = "id"; + private const string LabelClassAttributeName = "label-class"; private const string NameAttributeName = "name"; /// @@ -65,6 +66,12 @@ internal SelectTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHelper [HtmlAttributeName(IdAttributeName)] public string? Id { get; set; } + /// + /// Additional classes for the generated label element. + /// + [HtmlAttributeName(LabelClassAttributeName)] + public string? LabelClass { get; set; } + /// /// The name attribute for the generated select element. /// @@ -93,7 +100,7 @@ private protected override IHtmlContent GenerateFormGroupContent( var contentBuilder = new HtmlContentBuilder(); - var label = GenerateLabel(formGroupContext); + var label = GenerateLabel(formGroupContext, LabelClass); contentBuilder.AppendHtml(label); var hint = GenerateHint(tagHelperContext, formGroupContext); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TextAreaTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TextAreaTagHelper.cs index 2f6a7612..e91a2576 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/TextAreaTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TextAreaTagHelper.cs @@ -23,6 +23,7 @@ public class TextAreaTagHelper : FormGroupTagHelperBase private const string AutocompleteAttributeName = "autocomplete"; private const string DisabledAttributeName = "disabled"; private const string IdAttributeName = "id"; + private const string LabelClassAttributeName = "label-class"; private const string NameAttributeName = "name"; private const string RowsAttributeName = "rows"; private const string SpellcheckAttributeName = "spellcheck"; @@ -64,6 +65,12 @@ internal TextAreaTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHelp [HtmlAttributeName(IdAttributeName)] public string? Id { get; set; } + /// + /// Additional classes for the generated label element. + /// + [HtmlAttributeName(LabelClassAttributeName)] + public string? LabelClass { get; set; } + /// /// The name attribute for the generated textarea element. /// @@ -105,7 +112,7 @@ private protected override IHtmlContent GenerateFormGroupContent( { var contentBuilder = new HtmlContentBuilder(); - var label = GenerateLabel(formGroupContext); + var label = GenerateLabel(formGroupContext, LabelClass); contentBuilder.AppendHtml(label); var hint = GenerateHint(tagHelperContext, formGroupContext); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TextInputTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TextInputTagHelper.cs index be6c86ee..91753894 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/TextInputTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TextInputTagHelper.cs @@ -26,6 +26,7 @@ public class TextInputTagHelper : FormGroupTagHelperBase private const string DisabledAttributeName = "disabled"; private const string IdAttributeName = "id"; private const string InputModeAttributeName = "inputmode"; + private const string LabelClassAttributeName = "label-class"; private const string NameAttributeName = "name"; private const string PatternAttributeName = "pattern"; private const string SpellcheckAttributeName = "spellcheck"; @@ -46,8 +47,8 @@ public TextInputTagHelper() internal TextInputTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHelper? modelHelper = null) : base( - htmlGenerator ?? new ComponentGenerator(), - modelHelper ?? new DefaultModelHelper()) + htmlGenerator ?? new ComponentGenerator(), + modelHelper ?? new DefaultModelHelper()) { } @@ -88,6 +89,12 @@ internal TextInputTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHel [HtmlAttributeName(DictionaryAttributePrefix = AttributesPrefix)] public IDictionary? InputAttributes { get; set; } = new Dictionary(); + /// + /// Additional classes for the generated label element. + /// + [HtmlAttributeName(LabelClassAttributeName)] + public string? LabelClass { get; set; } + /// /// The name attribute for the generated input element. /// @@ -160,7 +167,7 @@ private protected override IHtmlContent GenerateFormGroupContent( var contentBuilder = new HtmlContentBuilder(); - var label = GenerateLabel(formGroupContext); + var label = GenerateLabel(formGroupContext, LabelClass); contentBuilder.AppendHtml(label); var hint = GenerateHint(tagHelperContext, formGroupContext); diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs index 0ebdc11f..6afd513c 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs @@ -51,7 +51,8 @@ public async Task ProcessAsync_WithMaxWords_GeneratesExpectedOutput() Id = "my-id", Name = "my-name", MaxWords = 10, - Threshold = 90 + Threshold = 90, + LabelClass = "additional-label-class" }; // Act @@ -61,7 +62,7 @@ public async Task ProcessAsync_WithMaxWords_GeneratesExpectedOutput() var expectedHtml = @"
- +
The hint
diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FileUploadTagHelperTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FileUploadTagHelperTests.cs index a3b0ac91..c6ba9471 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FileUploadTagHelperTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FileUploadTagHelperTests.cs @@ -45,7 +45,8 @@ public async Task ProcessAsync_GeneratesExpectedOutput() { Id = "my-id", DescribedBy = "describedby", - Name = "my-id" + Name = "my-id", + LabelClass = "additional-label-class" }; // Act @@ -54,7 +55,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput() // Assert var expectedHtml = @"
- +
The hint
"; diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FormGroupTagHelperBaseTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FormGroupTagHelperBaseTests.cs index 66e63dd7..527fc4fa 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FormGroupTagHelperBaseTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/FormGroupTagHelperBaseTests.cs @@ -511,7 +511,7 @@ public void GenerateLabel_NoLabelContentOnContextOrAspFor_ThrowsInvalidOperation }; // Act - var ex = Record.Exception(() => tagHelper.GenerateLabel(formGroupContext)); + var ex = Record.Exception(() => tagHelper.GenerateLabel(formGroupContext, labelClass: null)); // Assert Assert.IsType(ex); @@ -533,7 +533,7 @@ public void GenerateLabel_LabelContentOnContext_ReturnsContentFromContext() }; // Act - var label = tagHelper.GenerateLabel(formGroupContext); + var label = tagHelper.GenerateLabel(formGroupContext, labelClass: null); // Assert Assert.NotNull(label); @@ -562,7 +562,7 @@ public void GenerateLabel_NoLabelContentOnContext_ReturnsContentFromAspFor() }; // Act - var label = tagHelper.GenerateLabel(formGroupContext); + var label = tagHelper.GenerateLabel(formGroupContext, labelClass: null); // Assert Assert.NotNull(label); @@ -592,7 +592,7 @@ public void GenerateLabel_LabelContentOnContextAndAspFor_ReturnsContentFromConte }; // Act - var label = tagHelper.GenerateLabel(formGroupContext); + var label = tagHelper.GenerateLabel(formGroupContext, labelClass: null); // Assert Assert.NotNull(label); @@ -751,7 +751,7 @@ private protected override IHtmlContent GenerateFormGroupContent( { var contentBuilder = new HtmlContentBuilder(); - var label = GenerateLabel(formGroupContext); + var label = GenerateLabel(formGroupContext, labelClass: null); contentBuilder.AppendHtml(label); var hint = GenerateHint(tagHelperContext, formGroupContext); diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/SelectTagHelperTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/SelectTagHelperTests.cs index 95e14804..3389584c 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/SelectTagHelperTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/SelectTagHelperTests.cs @@ -70,7 +70,8 @@ public async Task ProcessAsync_GeneratesExpectedOutput() { Id = "my-id", DescribedBy = "describedby", - Name = "my-name" + Name = "my-name", + LabelClass = "additional-label-class" }; // Act @@ -79,7 +80,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput() // Assert var expectedHtml = @"
- +
The hint
"; diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/TextInputTagHelperTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/TextInputTagHelperTests.cs index 6c49510b..49de0efc 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/TextInputTagHelperTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/TextInputTagHelperTests.cs @@ -50,7 +50,8 @@ public async Task ProcessAsync_GeneratesExpectedOutput() InputMode = "numeric", Pattern = "[0-9]*", Type = "number", - Value = "42" + Value = "42", + LabelClass = "additional-label-class" }; // Act @@ -59,7 +60,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput() // Assert var expectedHtml = @"
- +
The hint