diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.TaskList.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.TaskList.cs new file mode 100644 index 00000000..ce8df501 --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.TaskList.cs @@ -0,0 +1,144 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace GovUk.Frontend.AspNetCore.HtmlGeneration +{ + public partial class ComponentGenerator + { + internal const string TaskListElement = "ul"; + internal const string TaskListTaskElement = "li"; + internal const string TaskListTaskNameElement = "span"; + internal const string TaskListHintElement = "span"; + internal const string TaskListStatusElement = "span"; + + public TagBuilder GenerateTaskList( + AttributeDictionary? attributes, + IEnumerable tasks) + { + Guard.ArgumentNotNull(nameof(tasks), tasks); + Guard.ArgumentValid(nameof(tasks), "A task list must contain at least one task", tasks.Any()); + + var tagBuilder = new TagBuilder(TaskListElement); + if (attributes != null) { tagBuilder.MergeAttributes(attributes); } + tagBuilder.MergeCssClass("govuk-task-list"); + + var taskNumber = 1; + foreach (var task in tasks) + { + Guard.ArgumentValid(nameof(tasks), "Task name cannot be null or empty", task.Name.Content != null); + + string taskId = GenerateTaskId(attributes, taskNumber, task); + var itemTagBuilder = new TagBuilder(TaskListTaskElement); + if (task.Attributes != null) { itemTagBuilder.MergeAttributes(task.Attributes); } + if (!itemTagBuilder.Attributes.ContainsKey("id")) { itemTagBuilder.MergeAttribute("id", taskId); } + itemTagBuilder.MergeCssClass("govuk-task-list__item"); + tagBuilder.InnerHtml.AppendHtml(itemTagBuilder); + + var taskNameTagBuilder = new TagBuilder(TaskListTaskNameElement); + taskNameTagBuilder.MergeAttributes(task.Name.Attributes); + taskNameTagBuilder.MergeCssClass("govuk-task-list__task-name-and-hint"); + + var statusText = TaskListTaskStatusText(task.Status.Status, task.Status.Content); + + if (!string.IsNullOrEmpty(task.Href) && task.Status.Status != TaskListTaskStatus.NotApplicable && task.Status.Status != TaskListTaskStatus.CannotStartYet) + { + var taskLinkTagBuilder = new TagBuilder("a"); + taskLinkTagBuilder.MergeAttribute("href", task.Href); + taskLinkTagBuilder.MergeCssClass("govuk-link"); + taskLinkTagBuilder.MergeCssClass("govuk-task-list__link"); + taskLinkTagBuilder.InnerHtml.AppendHtml(task.Name.Content!); + taskNameTagBuilder.InnerHtml.AppendHtml(taskLinkTagBuilder); + itemTagBuilder.MergeCssClass("govuk-task-list__item--with-link"); + + if (!string.IsNullOrEmpty(statusText)) + { + var statusId = task.Status.Attributes.ContainsKey("id") ? task.Status.Attributes["id"] : taskId + "-status"; + taskLinkTagBuilder.MergeAttribute("aria-describedby", statusId); + } + } + else + { + var unlinkedTaskTagBuilder = new TagBuilder("span"); + unlinkedTaskTagBuilder.MergeCssClass("govuk-task-list__task-no-link"); + unlinkedTaskTagBuilder.InnerHtml.AppendHtml(task.Name.Content!); + taskNameTagBuilder.InnerHtml.AppendHtml(unlinkedTaskTagBuilder); + } + + if (task.Hint.Content != null) + { + var hintTagBuilder = new TagBuilder(TaskListHintElement); + hintTagBuilder.MergeAttributes(task.Hint.Attributes); + hintTagBuilder.MergeCssClass("govuk-task-list__task_hint"); + hintTagBuilder.InnerHtml.AppendHtml(task.Hint.Content); + taskNameTagBuilder.InnerHtml.AppendHtml(hintTagBuilder); + } + + itemTagBuilder.InnerHtml.AppendHtml(taskNameTagBuilder); + + if (!string.IsNullOrEmpty(statusText)) + { + var statusOuterTagBuilder = new TagBuilder(TaskListStatusElement); + statusOuterTagBuilder.MergeCssClass("govuk-task-list__status-container"); + + var statusInnerTagBuilder = new TagBuilder("span"); + statusInnerTagBuilder.MergeAttributes(task.Status.Attributes); + statusInnerTagBuilder.MergeCssClass("govuk-task-list__status"); + if (task.Status.Status.HasValue) + { + statusInnerTagBuilder.MergeCssClass(TaskStatusCssClass(task.Status.Status.Value)); + } + if (!statusInnerTagBuilder.Attributes.ContainsKey("id")) { statusInnerTagBuilder.MergeAttribute("id", taskId + "-status"); } + + statusInnerTagBuilder.InnerHtml.AppendHtml(statusText); + statusOuterTagBuilder.InnerHtml.AppendHtml(statusInnerTagBuilder); + itemTagBuilder.InnerHtml.AppendHtml(statusOuterTagBuilder); + } + + taskNumber++; + } + + return tagBuilder; + } + + private string TaskStatusCssClass(TaskListTaskStatus status) + { + return "govuk-task-list__status-" + Regex.Replace(status.ToString(), "([A-Z])", "-$1").ToLowerInvariant(); + } + + public static string? TaskListTaskStatusText(TaskListTaskStatus? status, string? customStatus = null) + { + if (!string.IsNullOrEmpty(customStatus)) + { + return customStatus; + } + else if (status.HasValue) + { + var statusText = Regex.Replace(status.ToString()!, "([A-Z])", " $1").ToLowerInvariant().Trim(); + return statusText.Substring(0, 1).ToUpperInvariant() + statusText.Substring(1); + } + else return null; + } + + private static string GenerateTaskId(AttributeDictionary? attributes, int taskNumber, TaskListTask task) + { + string taskId = string.Empty; + if (task.Attributes != null && task.Attributes.ContainsKey("id")) + { + taskId = task.Attributes["id"]!; + } + else + { + taskId = Regex.Replace(Regex.Replace(task.Name.Content!.Value!, "<.*?>", string.Empty, RegexOptions.IgnoreCase), "[^A-Z0-9- ]", string.Empty, RegexOptions.IgnoreCase).Replace(" ", "-").ToLowerInvariant(); + } + if (string.IsNullOrEmpty(taskId)) { taskId = "task-" + taskNumber; } + if (attributes != null && attributes.ContainsKey("id")) { taskId = attributes["id"] + "-" + taskId; } + + return taskId; + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.TaskListSummary.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.TaskListSummary.cs new file mode 100644 index 00000000..3e21c5ee --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/ComponentGenerator.TaskListSummary.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System; +using System.Web; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace GovUk.Frontend.AspNetCore.HtmlGeneration +{ + public partial class ComponentGenerator + { + internal const string TaskListSummaryElement = "div"; + internal const int TaskListSummaryDefaultHeadingLevel = 2; + internal const int TaskListSummaryMinHeadingLevel = 1; + internal const int TaskListSummaryMaxHeadingLevel = 6; + public const string TaskListSummaryDefaultIncompleteStatus = "Tasks incomplete"; + public const string TaskListSummaryDefaultCompletedStatus = "Tasks completed"; + public const string TaskListSummaryDefaultTracker = "You've completed {0} of {1} tasks."; + + public TagBuilder GenerateTaskListSummary(TaskListSummary taskListSummary) + { + if (taskListSummary.HeadingLevel < TaskListSummaryMinHeadingLevel || taskListSummary.HeadingLevel > TaskListSummaryMaxHeadingLevel) + { + throw new ArgumentOutOfRangeException( + $"{nameof(taskListSummary.HeadingLevel)} must be between {TaskListSummaryMinHeadingLevel} and {TaskListSummaryMaxHeadingLevel}.", + nameof(taskListSummary.HeadingLevel)); + } + + Guard.ArgumentNotNullOrEmpty(nameof(taskListSummary.IncompleteStatus), taskListSummary.IncompleteStatus); + Guard.ArgumentNotNullOrEmpty(nameof(taskListSummary.CompletedStatus), taskListSummary.CompletedStatus); + Guard.ArgumentNotNullOrEmpty(nameof(taskListSummary.Tracker), taskListSummary.Tracker); + + var tagBuilder = new TagBuilder(TaskListSummaryElement); + if (taskListSummary.Attributes != null) { tagBuilder.MergeAttributes(taskListSummary.Attributes); } + tagBuilder.MergeCssClass("govuk-task-list-summary"); + + var statusTagBuilder = new TagBuilder($"h{taskListSummary.HeadingLevel}"); + statusTagBuilder.MergeCssClass("govuk-heading-s"); + statusTagBuilder.MergeCssClass("govuk-task-list-summary__heading"); + statusTagBuilder.InnerHtml.Append(taskListSummary.CompletedTasks == taskListSummary.TotalTasks ? taskListSummary.CompletedStatus : taskListSummary.IncompleteStatus); + tagBuilder.InnerHtml.AppendHtml(statusTagBuilder); + + var trackerTagBuilder = new TagBuilder("p"); + trackerTagBuilder.MergeCssClass("govuk-body"); + trackerTagBuilder.MergeCssClass("govuk-task-list-summary__tracker"); + trackerTagBuilder.InnerHtml.AppendHtml(string.Format(HttpUtility.HtmlEncode(taskListSummary.Tracker), + "" + taskListSummary.CompletedTasks + "", + "" + taskListSummary.TotalTasks + "")); + tagBuilder.InnerHtml.AppendHtml(trackerTagBuilder); + + return tagBuilder; + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/TaskListSummary.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/TaskListSummary.cs new file mode 100644 index 00000000..abf018b5 --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/TaskListSummary.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace GovUk.Frontend.AspNetCore.HtmlGeneration +{ + public class TaskListSummary + { + public AttributeDictionary Attributes { get; set; } + public string IncompleteStatus { get; set; } + public string CompletedStatus { get; set; } + public string Tracker { get; set; } + public int TotalTasks { get; set; } + public int CompletedTasks { get; set; } + public int HeadingLevel { get; set; } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/HtmlGeneration/TaskListTask.cs b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/TaskListTask.cs new file mode 100644 index 00000000..861119b5 --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/HtmlGeneration/TaskListTask.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace GovUk.Frontend.AspNetCore.HtmlGeneration +{ + public class TaskListTask + { + public AttributeDictionary Attributes { get; set; } + public (AttributeDictionary Attributes, HtmlString Content) Name { get; init; } + public string Href { get; set; } + public (AttributeDictionary Attributes, TaskListTaskStatus? Status, string Content) Status { get; internal set; } + public (AttributeDictionary Attributes, HtmlString Content) Hint { get; internal set; } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs b/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs index bc0e6287..18b9a394 100644 --- a/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs +++ b/src/GovUk.Frontend.AspNetCore/IGovUkHtmlGenerator.cs @@ -159,6 +159,10 @@ TagBuilder GenerateTabs( TagBuilder GenerateTag(IHtmlContent content, AttributeDictionary attributes); + TagBuilder GenerateTaskList(AttributeDictionary? attributes, IEnumerable tasks); + + TagBuilder GenerateTaskListSummary(TaskListSummary taskListSummary); + TagBuilder GenerateTextArea( bool haveError, string id, diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListContext.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListContext.cs new file mode 100644 index 00000000..f922fcc7 --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using GovUk.Frontend.AspNetCore.HtmlGeneration; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + internal class TaskListContext + { + private readonly List _tasks; + + public TaskListContext() + { + _tasks = new List(); + } + + public IReadOnlyList Tasks => _tasks; + + public void AddTask(TaskListTask task) + { + Guard.ArgumentNotNull(nameof(task), task); + + _tasks.Add(task); + } + + public void ThrowIfIncomplete() + { + if (Tasks.Count < 1) + { + throw ExceptionHelper.AChildElementMustBeProvided(TaskListTaskTagHelper.TagName); + } + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListSummaryTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListSummaryTagHelper.cs new file mode 100644 index 00000000..a74ed66f --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListSummaryTagHelper.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading.Tasks; +using GovUk.Frontend.AspNetCore.HtmlGeneration; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + /// + /// Generates a GOV.UK task list summary component. + /// + [HtmlTargetElement(TagName)] + [OutputElementHint(ComponentGenerator.TaskListSummaryElement)] + public class TaskListSummaryTagHelper : TagHelper + { + internal const string TagName = "govuk-task-list-summary"; + + private readonly IGovUkHtmlGenerator _htmlGenerator; + private int _headingLevel = ComponentGenerator.TaskListSummaryDefaultHeadingLevel; + + /// + /// The heading to display when the task list is incomplete. + /// + [HtmlAttributeName("incomplete-status")] + public string IncompleteStatus { get; set; } = ComponentGenerator.TaskListSummaryDefaultIncompleteStatus; + + /// + /// The heading to display when the task list is completed. + /// + [HtmlAttributeName("completed-status")] + public string CompletedStatus { get; set; } = ComponentGenerator.TaskListSummaryDefaultCompletedStatus; + + /// + /// Text that displays how many tasks are completed. {0} is replaced with the number of completed tasks, and {1} is replaced with the total number of tasks. + /// + [HtmlAttributeName("tracker")] + public string Tracker { get; set; } = ComponentGenerator.TaskListSummaryDefaultTracker; + + /// + /// The total number of tasks in the task list that must be completed. + /// + [HtmlAttributeName("total-tasks")] + public int TotalTasks { get; set; } = 0; + + /// + /// The number of tasks in the task list that have been completed so far. + /// + [HtmlAttributeName("completed-tasks")] + public int CompletedTasks { get; set; } = 0; + + + /// + /// The heading level. + /// + /// + /// Must be between 1 and 6 (inclusive). The default is 2. + /// + [HtmlAttributeName("heading-level")] + public int HeadingLevel + { + get => _headingLevel; + set + { + if (value < ComponentGenerator.TaskListSummaryMinHeadingLevel || + value > ComponentGenerator.TaskListSummaryMaxHeadingLevel) + { + throw new ArgumentOutOfRangeException( + nameof(value), + $"{nameof(HeadingLevel)} must be between {ComponentGenerator.TaskListSummaryMinHeadingLevel} and {ComponentGenerator.TaskListSummaryMaxHeadingLevel}."); + } + + _headingLevel = value; + } + } + + /// + /// Creates a new . + /// + public TaskListSummaryTagHelper() + : this(htmlGenerator: null) + { + } + + internal TaskListSummaryTagHelper(IGovUkHtmlGenerator htmlGenerator) + { + _htmlGenerator = htmlGenerator ?? new ComponentGenerator(); + } + + /// + public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var tagBuilder = _htmlGenerator.GenerateTaskListSummary(new TaskListSummary + { + Attributes = output.Attributes.ToAttributeDictionary(), + HeadingLevel = HeadingLevel, + IncompleteStatus = IncompleteStatus, + CompletedStatus = CompletedStatus, + Tracker = Tracker, + TotalTasks = TotalTasks, + CompletedTasks = CompletedTasks + }); + + output.TagName = tagBuilder.TagName; + output.TagMode = TagMode.StartTagAndEndTag; + + output.Attributes.Clear(); + output.MergeAttributes(tagBuilder); + output.Content.SetHtmlContent(tagBuilder.InnerHtml); + + return Task.CompletedTask; + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTagHelper.cs new file mode 100644 index 00000000..585bd4e6 --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTagHelper.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using GovUk.Frontend.AspNetCore.HtmlGeneration; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + /// + /// Generates a GOV.UK task list component. + /// + [HtmlTargetElement(TagName)] + [RestrictChildren(TaskListTaskTagHelper.TagName)] + [OutputElementHint(ComponentGenerator.TaskListElement)] + public class TaskListTagHelper : TagHelper + { + internal const string TagName = "govuk-task-list"; + + private readonly IGovUkHtmlGenerator _htmlGenerator; + + /// + /// Creates a new . + /// + public TaskListTagHelper() + : this(htmlGenerator: null) + { + } + + internal TaskListTagHelper(IGovUkHtmlGenerator htmlGenerator) + { + _htmlGenerator = htmlGenerator ?? new ComponentGenerator(); + } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var taskListContext = new TaskListContext(); + + using (context.SetScopedContextItem(taskListContext)) + { + await output.GetChildContentAsync(); + } + + taskListContext.ThrowIfIncomplete(); + + var tagBuilder = _htmlGenerator.GenerateTaskList( + output.Attributes.ToAttributeDictionary(), + taskListContext.Tasks); + + output.TagName = tagBuilder.TagName; + output.TagMode = TagMode.StartTagAndEndTag; + + output.Attributes.Clear(); + output.MergeAttributes(tagBuilder); + output.Content.SetHtmlContent(tagBuilder.InnerHtml); + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskContext.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskContext.cs new file mode 100644 index 00000000..2a81eb7e --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskContext.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + internal class TaskListTaskContext + { + public (AttributeDictionary Attributes, HtmlString Content) Name { get; internal set; } + public (AttributeDictionary Attributes, HtmlString Content) Hint { get; internal set; } + public (AttributeDictionary Attributes, TaskListTaskStatus? Status, string Content) Status { get; internal set; } + + public void ThrowIfIncomplete() + { + if (Name.Attributes == null) + { + throw ExceptionHelper.AChildElementMustBeProvided(TaskListTaskNameTagHelper.TagName); + } + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskHintTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskHintTagHelper.cs new file mode 100644 index 00000000..0625cdfc --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskHintTagHelper.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using GovUk.Frontend.AspNetCore.HtmlGeneration; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + /// + /// Represents a hint for a task in a GOV.UK task list component. + /// + [HtmlTargetElement(TagName, ParentTag = TaskListTaskTagHelper.TagName)] + [OutputElementHint(ComponentGenerator.TaskListHintElement)] + public class TaskListTaskHintTagHelper : TagHelper + { + internal const string TagName = "govuk-task-list-task-hint"; + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var taskContext = context.GetContextItem(); + + HtmlString htmlContent = null; + using (context.SetScopedContextItem(taskContext)) + { + var content = (await output.GetChildContentAsync()).GetContent(); + if (!string.IsNullOrEmpty(content)) + { + htmlContent = new HtmlString(content); + } + } + + taskContext.Hint = (output.Attributes.ToAttributeDictionary(), htmlContent); + + output.SuppressOutput(); + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskNameTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskNameTagHelper.cs new file mode 100644 index 00000000..0dfd7a40 --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskNameTagHelper.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using GovUk.Frontend.AspNetCore.HtmlGeneration; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + /// + /// Represents the name of a task in a GOV.UK task list component. + /// + [HtmlTargetElement(TagName, ParentTag = TaskListTaskTagHelper.TagName)] + [OutputElementHint(ComponentGenerator.TaskListTaskNameElement)] + public class TaskListTaskNameTagHelper : TagHelper + { + internal const string TagName = "govuk-task-list-task-name"; + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var taskContext = context.GetContextItem(); + + HtmlString htmlContent = null; + using (context.SetScopedContextItem(taskContext)) + { + var content = (await output.GetChildContentAsync()).GetContent(); + if (!string.IsNullOrEmpty(content)) + { + htmlContent = new HtmlString(content); + } + } + + taskContext.Name = (output.Attributes.ToAttributeDictionary(), htmlContent); + + output.SuppressOutput(); + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskStatusTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskStatusTagHelper.cs new file mode 100644 index 00000000..e5b2189a --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskStatusTagHelper.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using GovUk.Frontend.AspNetCore.HtmlGeneration; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + /// + /// Represents the status of a task in a GOV.UK task list component. + /// + [HtmlTargetElement(TagName, ParentTag = TaskListTaskTagHelper.TagName)] + [OutputElementHint(ComponentGenerator.TaskListStatusElement)] + public class TaskListTaskStatusTagHelper : TagHelper + { + internal const string TagName = "govuk-task-list-task-status"; + + /// + /// The status of the task. Set to null if no status is possible or the status does not fit one of the standard values. + /// + [HtmlAttributeName("status")] + public TaskListTaskStatus? Status { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var taskContext = context.GetContextItem(); + + string textContent = null; + using (context.SetScopedContextItem(taskContext)) + { + textContent = (await output.GetChildContentAsync()).GetContent(); + } + + taskContext.Status = (output.Attributes.ToAttributeDictionary(), Status, textContent); + + output.SuppressOutput(); + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskTagHelper.cs new file mode 100644 index 00000000..be287a6d --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/TaskListTaskTagHelper.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using GovUk.Frontend.AspNetCore.HtmlGeneration; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace GovUk.Frontend.AspNetCore.TagHelpers +{ + /// + /// Represents an task in a GOV.UK task list component. + /// + [HtmlTargetElement(TagName, ParentTag = TaskListTagHelper.TagName)] + [RestrictChildren(TaskListTaskNameTagHelper.TagName, TaskListTaskHintTagHelper.TagName, TaskListTaskStatusTagHelper.TagName)] + [OutputElementHint(ComponentGenerator.TaskListTaskElement)] + public class TaskListTaskTagHelper : TagHelper + { + internal const string TagName = "govuk-task-list-task"; + private const string TaskHrefAttributeName = "href"; + + /// + /// A link to the task. + /// + [HtmlAttributeName(TaskHrefAttributeName)] + public string Href { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var taskListContext = context.GetContextItem(); + + var taskContext = new TaskListTaskContext(); + + using (context.SetScopedContextItem(taskContext)) + { + await output.GetChildContentAsync(); + } + + taskContext.ThrowIfIncomplete(); + + taskListContext.AddTask(new TaskListTask + { + Attributes = output.Attributes.ToAttributeDictionary(), + Name = taskContext.Name, + Href = Href, + Status = taskContext.Status, + Hint = taskContext.Hint + }); + + output.SuppressOutput(); + } + } +} diff --git a/src/GovUk.Frontend.AspNetCore/TaskListTaskStatus.cs b/src/GovUk.Frontend.AspNetCore/TaskListTaskStatus.cs new file mode 100644 index 00000000..a670da9f --- /dev/null +++ b/src/GovUk.Frontend.AspNetCore/TaskListTaskStatus.cs @@ -0,0 +1,12 @@ +namespace GovUk.Frontend.AspNetCore +{ + public enum TaskListTaskStatus + { + CannotStartYet, + NotStarted, + Incomplete, + Completed, + NotApplicable, + Error + } +}