From 7fad79e77674b28799dc35264a00d902ac91cc9e Mon Sep 17 00:00:00 2001 From: James Gunn Date: Mon, 10 Jul 2023 16:13:35 +0100 Subject: [PATCH] Update to govuk-frontend 4.4.1 (#262) --- README.md | 2 +- docs/components/accordion.md | 6 ++ lib/govuk-frontend | 2 +- src/GovUk.Frontend.AspNetCore/Constants.cs | 2 +- .../GovUkFrontendAspNetCoreOptions.cs | 8 +- .../ComponentGenerator.Accordion.cs | 37 +++++++++ .../ComponentGenerator.Button.cs | 7 +- .../ComponentGenerator.CharacterCount.cs | 65 ++++++++++++++- .../ComponentGenerator.ErrorSummary.cs | 19 ++--- .../ComponentGenerator.NotificationBanner.cs | 7 +- .../IGovUkHtmlGenerator.cs | 21 ++++- .../PageTemplateHelper.cs | 6 +- .../TagBuilderExtensions.cs | 19 +++++ .../TagHelpers/AccordionTagHelper.cs | 62 ++++++++++++++- .../TagHelpers/ButtonTagHelper.cs | 2 +- .../TagHelpers/CharacterCountTagHelper.cs | 22 ++++-- .../TagHelpers/DateInputTagHelper.cs | 19 +---- .../TagHelpers/ErrorSummaryTagHelper.cs | 5 +- .../TagHelpers/FormErrorSummaryTagHelper.cs | 2 +- .../TagHelpers/NotificationBannerTagHelper.cs | 3 +- .../ComponentTests.Accordion.cs | 13 ++- .../ComponentTests.Button.cs | 3 +- .../ComponentTests.CharacterCount.cs | 15 +++- .../ComponentTests.ErrorSummary.cs | 2 +- .../ComponentTests.Label.cs | 4 +- .../ComponentTests.NotificationBanner.cs | 5 +- .../ComponentTests.SummaryList.cs | 2 +- .../OptionsJson/Accordion.cs | 6 ++ .../OptionsJson/CharacterCount.cs | 15 +++- .../TagHelpers/AccordionTagHelperTests.cs | 79 +++++++++++++++++++ .../CharacterCountTagHelperTests.cs | 44 ----------- .../TagHelpers/ErrorSummaryTagHelperTests.cs | 18 +++-- 32 files changed, 390 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index e2f79930..87672e97 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![ci](https://github.com/gunndabad/govuk-frontend-aspnetcore/workflows/ci/badge.svg) ![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/GovUk.Frontend.AspNetCore) -Targets [GDS Frontend v4.3.0](https://github.com/alphagov/govuk-frontend/releases/tag/v4.3.0) +Targets [GDS Frontend v4.4.1](https://github.com/alphagov/govuk-frontend/releases/tag/v4.4.1) ## Installation diff --git a/docs/components/accordion.md b/docs/components/accordion.md index 13a7f455..48a9bd94 100644 --- a/docs/components/accordion.md +++ b/docs/components/accordion.md @@ -114,6 +114,12 @@ | --- | --- | --- | | `id` | `string` | *Required* The `id` attribute for the accordion. Must be unique across the domain of your service. Cannot be `null` or empty. | | `heading-level` | `int` | The heading level. Must be between `1` and `6` (inclusive). The default is `2`. | +| `hide-all-sections-text` | `string` | The text content of the 'Hide all sections' button at the top of the accordion when all sections are expanded. | +| `hide-section-text` | `string` | The text content of the 'Hide' button within each section of the accordion, which is visible when the section is expanded. | +| `hide-section-aria-label-text` | `string` | The text made available to assistive technologies, like screen-readers, as the final part of the toggle's accessible name when the section is expanded. Defaults to 'Hide this section'. | +| `show-all-sections-text` | `string` | The text content of the 'Show all sections' button at the top of the accordion when at least one section is collapsed. | +| `show-section-text` | `string` | The text content of the 'Show' button within each section of the accordion, which is visible when the section is collapsed. | +| `hide-section-aria-label-text` | `string` | The text made available to assistive technologies, like screen-readers, as the final part of the toggle's accessible name when the section is collapsed. Defaults to 'Show this section'. | ### `` diff --git a/lib/govuk-frontend b/lib/govuk-frontend index e319f83c..f31e6f87 160000 --- a/lib/govuk-frontend +++ b/lib/govuk-frontend @@ -1 +1 @@ -Subproject commit e319f83cc2496e6814e0eb8f341ae658b05b415d +Subproject commit f31e6f87728267929733642701b7bd36016dd102 diff --git a/src/GovUk.Frontend.AspNetCore/Constants.cs b/src/GovUk.Frontend.AspNetCore/Constants.cs index 802bfb93..9c13682b 100644 --- a/src/GovUk.Frontend.AspNetCore/Constants.cs +++ b/src/GovUk.Frontend.AspNetCore/Constants.cs @@ -3,6 +3,6 @@ namespace GovUk.Frontend.AspNetCore internal static class Constants { public const string IdAttributeDotReplacement = "_"; - public const string GdsLibraryVersion = "4.3.0"; + public const string GdsLibraryVersion = "4.4.1"; } } diff --git a/src/GovUk.Frontend.AspNetCore/GovUkFrontendAspNetCoreOptions.cs b/src/GovUk.Frontend.AspNetCore/GovUkFrontendAspNetCoreOptions.cs index 6a070fbf..9ef932f6 100644 --- a/src/GovUk.Frontend.AspNetCore/GovUkFrontendAspNetCoreOptions.cs +++ b/src/GovUk.Frontend.AspNetCore/GovUkFrontendAspNetCoreOptions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using GovUk.Frontend.AspNetCore.HtmlGeneration; using GovUk.Frontend.AspNetCore.ModelBinding; using GovUk.Frontend.AspNetCore.TagHelpers; using Microsoft.AspNetCore.Http; @@ -41,10 +40,7 @@ public GovUkFrontendAspNetCoreOptions() /// /// The default value for . /// - /// - /// The default is false. - /// - public bool DefaultButtonPreventDoubleClick { get; set; } = ComponentGenerator.ButtonDefaultPreventDoubleClick; + public bool? DefaultButtonPreventDoubleClick { get; set; } /// /// A delegate for retrieving a CSP nonce for the current request. @@ -69,7 +65,7 @@ public GovUkFrontendAspNetCoreOptions() public bool PrependErrorSummaryToForms { get; set; } /// - /// Whether to prepend 'Error: ' to the <title> element when ModelState is not valid. + /// Whether to prepend "Error: " to the <title> element when ModelState is not valid. /// /// /// The default is true. diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Accordion.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Accordion.cs index 77ea2dcc..04cf3eb7 100644 --- a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Accordion.cs +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Accordion.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -19,6 +20,12 @@ public TagBuilder GenerateAccordion( string id, int headingLevel, AttributeDictionary? attributes, + string? hideAllSectionsText, + string? hideSectionText, + string? hideSectionAriaLabelText, + string? showAllSectionsText, + string? showSectionText, + string? showSectionAriaLabelText, IEnumerable items) { Guard.ArgumentNotNullOrEmpty(nameof(id), id); @@ -38,6 +45,36 @@ public TagBuilder GenerateAccordion( tagBuilder.Attributes.Add("data-module", "govuk-accordion"); tagBuilder.Attributes.Add("id", id); + if (hideAllSectionsText is not null) + { + tagBuilder.Attributes.Add("data-i18n.hide-all-sections", hideAllSectionsText); + } + + if (hideSectionText is not null) + { + tagBuilder.Attributes.Add("data-i18n.hide-section", hideSectionText); + } + + if (hideSectionAriaLabelText is not null) + { + tagBuilder.Attributes.Add("data-i18n.hide-section-aria-label", hideSectionAriaLabelText); + } + + if (showAllSectionsText is not null) + { + tagBuilder.Attributes.Add("data-i18n.show-all-sections", showAllSectionsText); + } + + if (showSectionText is not null) + { + tagBuilder.Attributes.Add("data-i18n.show-section", showSectionText); + } + + if (showSectionAriaLabelText is not null) + { + tagBuilder.Attributes.Add("data-i18n.show-section-aria-label", showSectionAriaLabelText); + } + var index = 0; foreach (var item in items) { diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Button.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Button.cs index 12c88c06..374e256f 100644 --- a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Button.cs +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.Button.cs @@ -9,14 +9,13 @@ internal partial class ComponentGenerator { internal const bool ButtonDefaultDisabled = false; internal const bool ButtonDefaultIsStartButton = false; - internal const bool ButtonDefaultPreventDoubleClick = false; internal const string ButtonElement = "button"; internal const string ButtonLinkElement = "a"; public TagBuilder GenerateButton( bool isStartButton, bool disabled, - bool preventDoubleClick, + bool? preventDoubleClick, IHtmlContent content, AttributeDictionary? attributes) { @@ -34,9 +33,9 @@ public TagBuilder GenerateButton( tagBuilder.Attributes.Add("aria-disabled", "true"); } - if (preventDoubleClick) + if (preventDoubleClick.HasValue) { - tagBuilder.Attributes.Add("data-prevent-double-click", "true"); + tagBuilder.Attributes.Add("data-prevent-double-click", preventDoubleClick.Value ? "true" : "false"); } tagBuilder.InnerHtml.AppendHtml(content); diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.CharacterCount.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.CharacterCount.cs index a92488a2..4f43e4e4 100644 --- a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.CharacterCount.cs +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.CharacterCount.cs @@ -15,11 +15,20 @@ public TagBuilder GenerateCharacterCount( int? maxWords, decimal? threshold, IHtmlContent formGroup, - AttributeDictionary? countMessageAttributes) + AttributeDictionary? countMessageAttributes, + string? textAreaDescriptionText, + (string Other, string One)? charactersUnderLimitText, + string? charactersAtLimitText, + (string Other, string One)? charactersOverLimitText, + (string Other, string One)? wordsUnderLimitText, + string? wordsAtLimitText, + (string Other, string One)? wordsOverLimitText) { Guard.ArgumentNotNull(nameof(textAreaId), textAreaId); Guard.ArgumentNotNull(nameof(formGroup), formGroup); + var hasNoLimit = maxLength is null && maxWords is null; + var tagBuilder = new TagBuilder(CharacterCountElement); tagBuilder.MergeCssClass("govuk-character-count"); tagBuilder.Attributes.Add("data-module", "govuk-character-count"); @@ -39,6 +48,53 @@ public TagBuilder GenerateCharacterCount( tagBuilder.Attributes.Add("data-maxwords", maxWords.Value.ToString()); } + if (hasNoLimit && textAreaDescriptionText is not null) + { + tagBuilder.AddPluralisedI18nAttributes("textarea-description", "other", textAreaDescriptionText); + } + + if (charactersUnderLimitText is not null) + { + tagBuilder.AddPluralisedI18nAttributes( + "characters-under-limit", + ("other", charactersUnderLimitText!.Value.Other), + ("one", charactersUnderLimitText.Value!.One)); + } + + if (charactersAtLimitText is not null) + { + tagBuilder.Attributes.Add("data-i18n.characters-at-limit", charactersAtLimitText); + } + + if (charactersOverLimitText is not null) + { + tagBuilder.AddPluralisedI18nAttributes( + "characters-over-limit", + ("other", charactersOverLimitText!.Value.Other), + ("one", charactersOverLimitText.Value!.One)); + } + + if (wordsUnderLimitText is not null) + { + tagBuilder.AddPluralisedI18nAttributes( + "words-under-limit", + ("other", wordsUnderLimitText!.Value.Other), + ("one", wordsUnderLimitText.Value!.One)); + } + + if (wordsAtLimitText is not null) + { + tagBuilder.Attributes.Add("data-i18n.words-at-limit", wordsAtLimitText); + } + + if (wordsOverLimitText is not null) + { + tagBuilder.AddPluralisedI18nAttributes( + "words-over-limit", + ("other", wordsOverLimitText!.Value.Other), + ("one", wordsOverLimitText.Value!.One)); + } + tagBuilder.InnerHtml.AppendHtml(formGroup); tagBuilder.InnerHtml.AppendHtml(GenerateHint()); @@ -48,9 +104,10 @@ IHtmlContent GenerateHint() { var hintId = $"{textAreaId}-info"; - var content = maxWords.HasValue ? - $"You can enter up to {maxWords} words" : - $"You can enter up to {maxLength} characters"; + var content = hasNoLimit ? "" : + (textAreaDescriptionText ?? $"You can enter up to %{{count}} {(maxWords.HasValue ? "words" : "characters")}") + .Replace("%{count}", (maxWords.HasValue ? maxWords : maxLength).ToString()); + var hintContent = new HtmlString(HtmlEncoder.Default.Encode(content)); var attributes = countMessageAttributes.ToAttributeDictionary(); diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.ErrorSummary.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.ErrorSummary.cs index be20dff6..a195d1a6 100644 --- a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.ErrorSummary.cs +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.ErrorSummary.cs @@ -8,11 +8,10 @@ namespace GovUk.Frontend.AspNetCore.HtmlGeneration internal partial class ComponentGenerator { internal const string ErrorSummaryDefaultTitle = "There is a problem"; - internal const bool ErrorSummaryDefaultDisableAutoFocus = false; internal const string ErrorSummaryElement = "div"; public TagBuilder GenerateErrorSummary( - bool disableAutoFocus, + bool? disableAutoFocus, IHtmlContent titleContent, AttributeDictionary titleAttributes, IHtmlContent descriptionContent, @@ -25,21 +24,21 @@ public TagBuilder GenerateErrorSummary( var tagBuilder = new TagBuilder(ErrorSummaryElement); tagBuilder.MergeAttributes(attributes); tagBuilder.MergeCssClass("govuk-error-summary"); - tagBuilder.Attributes.Add("aria-labelledby", "error-summary-title"); - tagBuilder.Attributes.Add("role", "alert"); tagBuilder.Attributes.Add("data-module", "govuk-error-summary"); - if (disableAutoFocus) + if (disableAutoFocus.HasValue) { - tagBuilder.Attributes.Add("data-disable-auto-focus", "true"); + tagBuilder.Attributes.Add("data-disable-auto-focus", disableAutoFocus.Value ? "true" : "false"); } + var alert = new TagBuilder("div"); + alert.Attributes.Add("role", "alert"); + var heading = new TagBuilder("h2"); heading.MergeAttributes(titleAttributes); heading.MergeCssClass("govuk-error-summary__title"); - heading.Attributes.Add("id", "error-summary-title"); heading.InnerHtml.AppendHtml(titleContent); - tagBuilder.InnerHtml.AppendHtml(heading); + alert.InnerHtml.AppendHtml(heading); var body = new TagBuilder("div"); body.MergeCssClass("govuk-error-summary__body"); @@ -89,7 +88,9 @@ public TagBuilder GenerateErrorSummary( body.InnerHtml.AppendHtml(ul); - tagBuilder.InnerHtml.AppendHtml(body); + alert.InnerHtml.AppendHtml(body); + + tagBuilder.InnerHtml.AppendHtml(alert); return tagBuilder; } diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.NotificationBanner.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.NotificationBanner.cs index add18d63..2e4de5ec 100644 --- a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.NotificationBanner.cs +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.NotificationBanner.cs @@ -8,7 +8,6 @@ namespace GovUk.Frontend.AspNetCore.HtmlGeneration { internal partial class ComponentGenerator { - internal const bool NotificationBannerDefaultDisableAutoFocus = false; internal const string NotificationBannerDefaultRole = "region"; internal const string NotificationBannerDefaultSuccessRole = "alert"; internal const string NotificationBannerDefaultSuccessTitle = "Success"; @@ -23,7 +22,7 @@ internal partial class ComponentGenerator public TagBuilder GenerateNotificationBanner( NotificationBannerType type, string? role, - bool disableAutoFocus, + bool? disableAutoFocus, string? titleId, int? titleHeadingLevel, IHtmlContent? titleContent, @@ -64,9 +63,9 @@ public TagBuilder GenerateNotificationBanner( tagBuilder.Attributes.Add("role", role); tagBuilder.Attributes.Add("aria-labelledby", titleId); - if (disableAutoFocus) + if (disableAutoFocus.HasValue) { - tagBuilder.Attributes.Add("data-disable-auto-focus", "true"); + tagBuilder.Attributes.Add("data-disable-auto-focus", disableAutoFocus.Value ? "true" : "false"); } tagBuilder.InnerHtml.AppendHtml(GenerateHeading()); diff --git a/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs b/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs index 18baa8f9..f56e8b5f 100644 --- a/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs +++ b/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs @@ -13,6 +13,12 @@ TagBuilder GenerateAccordion( string id, int headingLevel, AttributeDictionary attributes, + string hideAllSectionsText, + string hideSectionText, + string hideSectionAriaLabelText, + string showAllSectionsText, + string showSectionText, + string showSectionAriaLabelText, IEnumerable items); TagBuilder GenerateBackLink(IHtmlContent content, AttributeDictionary attributes); @@ -25,7 +31,7 @@ TagBuilder GenerateBreadcrumbs( TagBuilder GenerateButton( bool isStartButton, bool disabled, - bool preventDoubleClick, + bool? preventDoubleClick, IHtmlContent content, AttributeDictionary attributes); @@ -41,7 +47,14 @@ TagBuilder GenerateCharacterCount( int? maxWords, decimal? threshold, IHtmlContent formGroup, - AttributeDictionary countMessageAttributes); + AttributeDictionary countMessageAttributes, + string textAreaDescriptionText, + (string Other, string One)? charactersUnderLimitText, + string charactersAtLimitText, + (string Other, string One)? charactersOverLimitText, + (string Other, string One)? wordsUnderLimitText, + string wordsAtLimitText, + (string Other, string One)? wordsOverLimitText); TagBuilder GenerateCheckboxes( string idPrefix, @@ -73,7 +86,7 @@ TagBuilder GenerateErrorMessage( AttributeDictionary attributes); TagBuilder GenerateErrorSummary( - bool disableAutofocus, + bool? disableAutofocus, IHtmlContent titleContent, AttributeDictionary titleAttributes, IHtmlContent descriptionContent, @@ -112,7 +125,7 @@ TagBuilder GenerateLabel( TagBuilder GenerateNotificationBanner( NotificationBannerType type, string role, - bool disableAutoFocus, + bool? disableAutoFocus, string titleId, int? titleHeadingLevel, IHtmlContent titleContent, diff --git a/src/GovUk.Frontend.AspNetCore/PageTemplateHelper.cs b/src/GovUk.Frontend.AspNetCore/PageTemplateHelper.cs index 7313710a..84197b7c 100644 --- a/src/GovUk.Frontend.AspNetCore/PageTemplateHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/PageTemplateHelper.cs @@ -64,7 +64,7 @@ public IHtmlContent GenerateScriptImports(string? cspNonce = null) TagBuilder GenerateImportScript() { var tagBuilder = new TagBuilder("script"); - tagBuilder.MergeAttribute("src", "/govuk-frontend-4.3.0.min.js"); + tagBuilder.MergeAttribute("src", "/govuk-frontend-4.4.1.min.js"); return tagBuilder; } @@ -91,10 +91,10 @@ TagBuilder GenerateInitScript() /// /// containing the link tags. public IHtmlContent GenerateStyleImports() => new HtmlString(@" - + "); /// diff --git a/src/GovUk.Frontend.AspNetCore/TagBuilderExtensions.cs b/src/GovUk.Frontend.AspNetCore/TagBuilderExtensions.cs index 9f1db5dc..00682bc5 100644 --- a/src/GovUk.Frontend.AspNetCore/TagBuilderExtensions.cs +++ b/src/GovUk.Frontend.AspNetCore/TagBuilderExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -22,6 +23,24 @@ public static void MergeCssClass(this TagBuilder tagBuilder, string value) tagBuilder.Attributes.MergeCssClass(value); } + // See govukPluralisedI18nAttributes nunjucks macro + internal static void AddPluralisedI18nAttributes( + this TagBuilder tagBuilder, + string translationKey, + params (string PluralType, string Message)[] pluralForms) + { + foreach (var (pluralType, message) in pluralForms) + { + tagBuilder.Attributes.Add($"data-i18n.{translationKey}.{pluralType}", message); + } + } + + internal static void AddPluralisedI18nAttributes( + this TagBuilder tagBuilder, + string translationKey, + string pluralType, + string message) => AddPluralisedI18nAttributes(tagBuilder, translationKey, new[] { (pluralType, message) }); + internal static void MergeOptionalAttributes(this TagBuilder tagBuilder, AttributeDictionary? attributes) { if (attributes is not null) diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/AccordionTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/AccordionTagHelper.cs index 2264a341..484fe288 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/AccordionTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/AccordionTagHelper.cs @@ -18,7 +18,13 @@ public class AccordionTagHelper : TagHelper internal const string TagName = "govuk-accordion"; private const string HeadingLevelAttributeName = "heading-level"; + private const string HideAllSectionsTextAttributeName = "hide-all-sections-text"; + private const string HideSectionTextAttributeName = "hide-section-text"; + private const string HideSectionAriaLabelTextAttributeName = "hide-section-aria-label-text"; private const string IdAttributeName = "id"; + private const string ShowAllSectionsTextAttributeName = "show-all-sections-text"; + private const string ShowSectionTextAttributeName = "show-section-text"; + private const string ShowSectionAriaLabelTextAttributeName = "show-section-aria-label-text"; private readonly IGovUkHtmlGenerator _htmlGenerator; private string? _id; @@ -61,6 +67,30 @@ public int HeadingLevel } } + /// + /// The text content of the "Hide all sections" button at the top of the accordion when all sections + /// are expanded. + /// + [HtmlAttributeName(HideAllSectionsTextAttributeName)] + public string? HideAllSectionsText { get; set; } + + /// + /// The text content of the "Hide" button within each section of the accordion, which is visible when the + /// section is expanded. + /// + [HtmlAttributeName(HideSectionTextAttributeName)] + public string? HideSectionText { get; set; } + + /// + /// The text made available to assistive technologies, like screen-readers, as the final part of the toggle's + /// accessible name when the section is expanded. + /// + /// + /// Defaults to "Hide this section". + /// + [HtmlAttributeName(HideSectionAriaLabelTextAttributeName)] + public string? HideSectionAriaLabelText { get; set; } + /// /// The id attribute for the accordion. /// @@ -79,6 +109,30 @@ public string? Id } } + /// + /// The text content of the "Show all sections" button at the top of the accordion, which is visible when the + /// section is collapsed. + /// + [HtmlAttributeName(ShowAllSectionsTextAttributeName)] + public string? ShowAllSectionsText { get; set; } + + /// + /// The text content of the "Show" button within each section of the accordion, which is visible when the + /// section is collapsed. + /// + [HtmlAttributeName(ShowSectionTextAttributeName)] + public string? ShowSectionText { get; set; } + + /// + /// The text made available to assistive technologies, like screen-readers, as the final part of the toggle's + /// accessible name when the section is collapsed. + /// + /// + /// Defaults to "Show this section". + /// + [HtmlAttributeName(ShowSectionAriaLabelTextAttributeName)] + public string? ShowSectionAriaLabelText { get; set; } + /// public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { @@ -98,7 +152,13 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu Id, HeadingLevel, output.Attributes.ToAttributeDictionary(), - accordionContext.Items); + ShowAllSectionsText, + ShowSectionText, + ShowSectionAriaLabelText, + HideAllSectionsText, + HideSectionText, + HideSectionAriaLabelText, + items: accordionContext.Items); output.TagName = tagBuilder.TagName; output.TagMode = TagMode.StartTagAndEndTag; diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/ButtonTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/ButtonTagHelper.cs index b1e5011e..ead55adb 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/ButtonTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/ButtonTagHelper.cs @@ -63,7 +63,7 @@ internal ButtonTagHelper(IOptions optionsAccesso /// The default is set for the application in . /// [HtmlAttributeName(PreventDoubleClickAttributeName)] - public bool PreventDoubleClick { get; set; } + public bool? PreventDoubleClick { get; set; } /// /// The type attribute for the generated button element. diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs index 15ac1566..a87f09c9 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/CharacterCountTagHelper.cs @@ -194,11 +194,7 @@ public decimal? Threshold private protected override TagBuilder CreateTagBuilder(bool haveError, IHtmlContent content, TagHelperOutput tagHelperOutput) { - if (!MaxLength.HasValue && !MaxWords.HasValue) - { - throw new InvalidOperationException($"One of the '{MaxLengthAttributeName}' and '{MaxWordsLengthAttributeName}' attributes must be specified."); - } - else if (MaxLength.HasValue && MaxWords.HasValue) + if (MaxLength.HasValue && MaxWords.HasValue) { throw new InvalidOperationException($"Only one of the '{MaxLengthAttributeName}' or '{MaxWordsLengthAttributeName}' attributes can be specified."); } @@ -210,7 +206,21 @@ private protected override TagBuilder CreateTagBuilder(bool haveError, IHtmlCont FormGroupAttributes.ToAttributeDictionary()); var resolvedId = ResolveIdPrefix(); - return Generator.GenerateCharacterCount(resolvedId, MaxLength, MaxWords, Threshold, formGroup, CountMessageAttributes.ToAttributeDictionary()); + + return Generator.GenerateCharacterCount( + resolvedId, + MaxLength, + MaxWords, + Threshold, + formGroup, + CountMessageAttributes.ToAttributeDictionary(), + null, + null, + null, + null, + null, + null, + null); } private protected override FormGroupContext CreateFormGroupContext() => new CharacterCountContext(); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputTagHelper.cs index 0042552f..9391dfe3 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputTagHelper.cs @@ -167,22 +167,11 @@ private protected override string GetErrorFieldId(TagHelperContext context) var errorItems = GetErrorComponents(dateInputContext); Debug.Assert(errorItems != DateInputErrorComponents.None); - string suffix; + var suffix = errorItems.HasFlag(DateInputErrorComponents.Day) ? DefaultDayItemName : + errorItems.HasFlag(DateInputErrorComponents.Month) ? DefaultMonthItemName : + DefaultYearItemName; - if (errorItems.HasFlag(DateInputErrorComponents.Day)) - { - suffix = $".{DefaultDayItemName}"; - } - else if (errorItems.HasFlag(DateInputErrorComponents.Month)) - { - suffix = $".{DefaultMonthItemName}"; - } - else - { - suffix = $".{DefaultYearItemName}"; - } - - return $"{ResolveIdPrefix()}{suffix}"; + return $"{ResolveIdPrefix()}.{suffix}"; } private protected override string ResolveIdPrefix() diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/ErrorSummaryTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/ErrorSummaryTagHelper.cs index 8bde4104..8148e732 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/ErrorSummaryTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/ErrorSummaryTagHelper.cs @@ -37,11 +37,8 @@ internal ErrorSummaryTagHelper(IGovUkHtmlGenerator? htmlGenerator = null) /// /// Whether to disable the behavior that focuses the error summary when the page loads. /// - /// - /// The default is false. - /// [HtmlAttributeName(DisableAutoFocusAttributeName)] - public bool DisableAutoFocus { get; set; } = ComponentGenerator.ErrorSummaryDefaultDisableAutoFocus; + public bool? DisableAutoFocus { get; set; } /// public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/FormErrorSummaryTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/FormErrorSummaryTagHelper.cs index 97d8a76a..72557ae9 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/FormErrorSummaryTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/FormErrorSummaryTagHelper.cs @@ -75,7 +75,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu }); var errorSummary = _htmlGenerator.GenerateErrorSummary( - ComponentGenerator.ErrorSummaryDefaultDisableAutoFocus, + disableAutofocus: null, // TODO Should we have an attribute to configure this? titleContent: new HtmlString(HtmlEncoder.Default.Encode(ComponentGenerator.ErrorSummaryDefaultTitle)), titleAttributes: null, descriptionContent: null, diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/NotificationBannerTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/NotificationBannerTagHelper.cs index c74456a4..34e406d5 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/NotificationBannerTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/NotificationBannerTagHelper.cs @@ -38,10 +38,9 @@ internal NotificationBannerTagHelper(IGovUkHtmlGenerator? htmlGenerator = null) /// /// /// Only applies when is . - /// The default is false. /// [HtmlAttributeName(DisableAutoFocusAttributeName)] - public bool DisableAutoFocus { get; set; } = ComponentGenerator.NotificationBannerDefaultDisableAutoFocus; + public bool? DisableAutoFocus { get; set; } /// /// The role attribute for the notification banner. diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Accordion.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Accordion.cs index 581a1285..71cc6ea1 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Accordion.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Accordion.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Text.Encodings.Web; using GovUk.Frontend.AspNetCore.HtmlGeneration; -using GovUk.Frontend.AspNetCore.TestCommon; using Microsoft.AspNetCore.Html; using Xunit; @@ -36,7 +35,17 @@ public void Accordion(ComponentTestCaseData data) => }) .OrEmpty(); - return generator.GenerateAccordion(options.Id, headingLevel, attributes, items) + return generator.GenerateAccordion( + options.Id, + headingLevel, + attributes, + options.HideAllSectionsText, + options.HideSectionText, + options.HideSectionAriaLabelText, + options.ShowAllSectionsText, + options.ShowSectionText, + options.ShowSectionAriaLabelText, + items) .ToHtmlString(); }); } diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Button.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Button.cs index aa0b6036..62285bd6 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Button.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Button.cs @@ -1,6 +1,5 @@ using GovUk.Frontend.AspNetCore.HtmlGeneration; using GovUk.Frontend.AspNetCore.TestCommon; -using Microsoft.AspNetCore.Html; using Xunit; namespace GovUk.Frontend.AspNetCore.ConformanceTests @@ -28,7 +27,7 @@ public void Button(ComponentTestCaseData data) => var isStartButton = options.IsStartButton ?? ComponentGenerator.ButtonDefaultIsStartButton; var disabled = options.Disabled ?? ComponentGenerator.ButtonDefaultDisabled; - var preventDoubleClick = options.PreventDoubleClick ?? ComponentGenerator.ButtonDefaultPreventDoubleClick; + var preventDoubleClick = options.PreventDoubleClick; var content = TextOrHtmlHelper.GetHtmlContent(options.Text, options.Html) ?? _emptyContent; var attributes = options.Attributes.ToAttributesDictionary() diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.CharacterCount.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.CharacterCount.cs index 927723c4..dbf030b0 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.CharacterCount.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.CharacterCount.cs @@ -57,7 +57,20 @@ public void CharacterCount(ComponentTestCaseData dat attributes); })); - return generator.GenerateCharacterCount(options.Id, options.MaxLength, options.MaxWords, options.Threshold, content, countMessageAttributes) + return generator.GenerateCharacterCount( + options.Id, + options.MaxLength, + options.MaxWords, + options.Threshold, + content, + countMessageAttributes, + options.TextareaDescriptionText, + options.CharactersUnderLimitText is not null ? (options.CharactersUnderLimitText.Other, options.CharactersUnderLimitText.One) : null, + options.CharactersAtLimitText, + options.CharactersOverLimitText is not null ? (options.CharactersOverLimitText.Other, options.CharactersOverLimitText.One) : null, + options.WordsUnderLimitText is not null ? (options.WordsUnderLimitText.Other, options.WordsUnderLimitText.One) : null, + options.WordsAtLimitText, + options.WordsOverLimitText is not null ? (options.WordsOverLimitText.Other, options.WordsOverLimitText.One) : null) .ToHtmlString(); }); } diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.ErrorSummary.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.ErrorSummary.cs index fe8c5e48..05242e22 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.ErrorSummary.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.ErrorSummary.cs @@ -36,7 +36,7 @@ public void ErrorSummary(ComponentTestCaseData data) = .MergeAttribute("class", options.Classes); return generator.GenerateErrorSummary( - options.DisableAutoFocus ?? ComponentGenerator.ErrorSummaryDefaultDisableAutoFocus, + options.DisableAutoFocus, titleContent, titleAttributes: null, descriptionContent, diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Label.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Label.cs index d433baec..fbefdd49 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Label.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.Label.cs @@ -9,7 +9,8 @@ public partial class ComponentTests { [Theory] [ComponentFixtureData("label", typeof(OptionsJson.Label), exclude: "empty")] - public void Label(ComponentTestCaseData data) => + public void Label(ComponentTestCaseData data) + { CheckComponentHtmlMatchesExpectedHtml( data, (generator, options) => @@ -18,6 +19,7 @@ public void Label(ComponentTestCaseData data) => return BuildLabel(generator, labelOptions).ToHtmlString(); }); + } private static IHtmlContent BuildLabel(ComponentGenerator generator, OptionsJson.Label options) { diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.NotificationBanner.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.NotificationBanner.cs index e1a45e8e..84d040a4 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.NotificationBanner.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/ComponentTests.NotificationBanner.cs @@ -19,9 +19,6 @@ public void NotificationBanner(ComponentTestCaseData @@ -40,7 +37,7 @@ public void NotificationBanner(ComponentTestCaseData data) => .MergeAttribute("class", a.Classes) .MergeAttribute("href", a.Href), Content = TextOrHtmlHelper.GetHtmlContent(a.Text, a.Html), - VisuallyHiddenText = a.VisuallyHiddenText is string vht ? vht : null + VisuallyHiddenText = a.VisuallyHiddenText is string vht && vht != string.Empty ? vht : null })).OrEmpty().ToList(), Attributes = new AttributeDictionary().MergeAttribute("class", r.Actions?.Classes) }, diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/Accordion.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/Accordion.cs index 5a841557..5ca49a90 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/Accordion.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/Accordion.cs @@ -8,6 +8,12 @@ public record Accordion public int? HeadingLevel { get; set; } public string Classes { get; set; } public IDictionary Attributes { get; set; } + public string HideAllSectionsText { get; set; } + public string HideSectionText { get; set; } + public string HideSectionAriaLabelText { get; set; } + public string ShowAllSectionsText { get; set; } + public string ShowSectionText { get; set; } + public string ShowSectionAriaLabelText { get; set; } public IList Items { get; set; } } diff --git a/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/CharacterCount.cs b/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/CharacterCount.cs index 29a85b80..24973d4b 100644 --- a/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/CharacterCount.cs +++ b/test/GovUk.Frontend.AspNetCore.ConformanceTests/OptionsJson/CharacterCount.cs @@ -20,11 +20,24 @@ public class CharacterCount public string Classes { get; set; } public bool? Spellcheck { get; set; } public IDictionary Attributes { get; set; } - public CharacterCountCountMessage CountMessage{ get; set; } + public CharacterCountCountMessage CountMessage { get; set; } + public string TextareaDescriptionText { get; set; } + public CharacterCountLocalizedText CharactersUnderLimitText { get; set; } + public string CharactersAtLimitText { get; set; } + public CharacterCountLocalizedText CharactersOverLimitText { get; set; } + public CharacterCountLocalizedText WordsUnderLimitText { get; set; } + public string WordsAtLimitText { get; set; } + public CharacterCountLocalizedText WordsOverLimitText { get; set; } } public class CharacterCountCountMessage { public string Classes { get; set; } } + + public class CharacterCountLocalizedText + { + public string One { get; set; } + public string Other { get; set; } + } } diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/AccordionTagHelperTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/AccordionTagHelperTests.cs index c41f9588..152fa2b6 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/AccordionTagHelperTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/AccordionTagHelperTests.cs @@ -86,6 +86,85 @@ First content AssertEx.HtmlEqual(@expectedHtml, output.ToHtmlString()); } + [Fact] + public async Task ProcessAsync_WithSectionTranslationAttributes_GeneratesExpectedOutput() + { + // Arrange + var context = new TagHelperContext( + tagName: "govuk-accordion", + allAttributes: new TagHelperAttributeList(), + items: new Dictionary(), + uniqueId: "test"); + + var output = new TagHelperOutput( + "govuk-accordion", + attributes: new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => + { + var accordionContext = (AccordionContext)context.Items[typeof(AccordionContext)]; + + accordionContext.AddItem(new AccordionItem() + { + Content = new HtmlString("First content"), + Expanded = false, + HeadingContent = new HtmlString("First heading"), + SummaryContent = new HtmlString("First summary") + }); + + accordionContext.AddItem(new AccordionItem() + { + Content = new HtmlString("First content"), + Expanded = true, + HeadingContent = new HtmlString("Second heading") + }); + + var tagHelperContent = new DefaultTagHelperContent(); + return Task.FromResult(tagHelperContent); + }); + + var tagHelper = new AccordionTagHelper() + { + Id = "testaccordion", + HideAllSectionsText = "Collapse all sections", + HideSectionText = "Collapse", + HideSectionAriaLabelText = "Collapse this section", + ShowAllSectionsText = "Expand all sections", + ShowSectionText = "Expand", + ShowSectionAriaLabelText = "Expand this section" + }; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + var expectedHtml = @" +
+
+
+

+ First heading +

+
First summary
+
+
+ First content +
+
+
+
+

+ Second heading +

+
+
+ First content +
+
+
"; + + AssertEx.HtmlEqual(@expectedHtml, output.ToHtmlString()); + } + [Fact] public async Task ProcessAsync_NoId_ThrowsInvalidOperationException() { diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs index 6afd513c..37a2ed7a 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/CharacterCountTagHelperTests.cs @@ -196,50 +196,6 @@ You can enter up to 10 words AssertEx.HtmlEqual(expectedHtml, output.ToHtmlString()); } - [Fact] - public async Task ProcessAsync_NoMaxLengthOrMaxWords_ThrowsInvalidOperationException() - { - // Arrange - var context = new TagHelperContext( - tagName: "govuk-character-count", - allAttributes: new TagHelperAttributeList(), - items: new Dictionary(), - uniqueId: "test"); - - var output = new TagHelperOutput( - "govuk-character-count", - attributes: new TagHelperAttributeList(), - getChildContentAsync: (useCachedResult, encoder) => - { - var characterCountContext = context.GetContextItem(); - - characterCountContext.SetLabel( - isPageHeading: false, - attributes: null, - content: new HtmlString("The label")); - - characterCountContext.SetHint( - attributes: null, - content: new HtmlString("The hint")); - - var tagHelperContent = new DefaultTagHelperContent(); - return Task.FromResult(tagHelperContent); - }); - - var tagHelper = new CharacterCountTagHelper() - { - Id = "my-id", - Name = "my-name" - }; - - // Act - var ex = await Record.ExceptionAsync(() => tagHelper.ProcessAsync(context, output)); - - // Assert - Assert.IsType(ex); - Assert.Equal("One of the 'max-length' and 'max-words' attributes must be specified.", ex.Message); - } - [Fact] public async Task ProcessAsync_BothMaxLengthAndMaxWords_ThrowsInvalidOperationException() { diff --git a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/ErrorSummaryTagHelperTests.cs b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/ErrorSummaryTagHelperTests.cs index 7ff67700..3fca1f4b 100644 --- a/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/ErrorSummaryTagHelperTests.cs +++ b/test/GovUk.Frontend.AspNetCore.Tests/TagHelpers/ErrorSummaryTagHelperTests.cs @@ -57,14 +57,16 @@ public async Task ProcessAsync_GeneratesExpectedOutput() // Assert var expectedHtml = @" -
-

Title

-
-

Description

- +
+
+

Title

+
+

Description

+ +
";