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
+ }
+}