diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a1708b0d..5f119b9047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ ### New features +#### Task list component added + +A new component has been added which creates lists of tasks that users need to complete. + +Each task in the list can have a title, status, link and an optional hint. When a link is added, the whole row is clickable. + +This change was made in [pull request #2261: Task list component](https://github.com/alphagov/govuk-frontend/pull/2261). + #### Tag design changes The design of the tag component has changed to improve accessibility and readability. diff --git a/packages/govuk-frontend-review/src/views/full-page-examples/task-list/index.njk b/packages/govuk-frontend-review/src/views/full-page-examples/task-list/index.njk new file mode 100644 index 0000000000..4127ea35f0 --- /dev/null +++ b/packages/govuk-frontend-review/src/views/full-page-examples/task-list/index.njk @@ -0,0 +1,247 @@ +--- +scenario: >- + You want to apply for a teacher training course + + + Things to try: + + 1. Making sure all content is visible without overlapping on narrower screens. + +notes: The buttons and links on this page are not functional. +--- + +{# This example is based of the live "Apply for teacher training" service at: https://www.apply-for-teacher-training.service.gov.uk/candidate/application #} +{% extends "layouts/full-page-example.njk" %} + +{% from "govuk/components/task-list/macro.njk" import govukTaskList %} +{% from "govuk/components/button/macro.njk" import govukButton %} + +{% set pageTitle = "Apply for teacher training" %} +{% block pageTitle %}{{ pageTitle }} - GOV.UK{% endblock %} + + +{% block content %} +

Your application

+

Last saved on 15 May 2023 at 2:21pm

+ + + +
+
+ +

Personal details

+ {{ govukTaskList({ + items: [ + { + title: { text: "Personal information" }, + href: "#", + status: { + text: "Completed" + } + }, + { + title: { text: "Contact information" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + })}} + +

Courses

+

You can apply for up to 4 courses.

+ {{ govukTaskList({ + items: [ + { + title: { text: "Choose your courses" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + })}} + +

Qualifications

+ {{ govukTaskList({ + items: [ + { + title: { text: "English GCSE or equivalent" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + }, + { + title: { text: "Maths GCSE or equivalent" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + }, + { + title: { text: "A levels and other qualifications" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + }, + { + title: { text: "Degree" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + })}} + +

Work experience

+ {{ govukTaskList({ + items: [ + { + title: { text: "Work history" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + }, + { + title: { text: "Unpaid experience" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + })}} + +

Personal statement

+ {{ govukTaskList({ + items: [ + { + title: { text: "Your personal statement" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + })}} + +

Adjustments

+ {{ govukTaskList({ + items: [ + { + title: { text: "Ask for support if you’re disabled" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + }, + { + title: { text: "Interview needs" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + })}} + +

Safeguarding

+ {{ govukTaskList({ + items: [ + { + title: { text: "References to be requested if you accept an offer" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + }, + { + title: { text: "Declare any safeguarding issues" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + })}} + +

Equality and diversity

+

Training providers will only see your answers to this section if you accept an offer from them.

+ {{ govukTaskList({ + items: [ + { + title: { text: "Equality and diversity questions" }, + href: "#", + status: { + tag: { + text: "Incomplete", + classes: "govuk-tag--blue" + } + } + } + ] + }) }} + +

Check and submit

+ + {{ govukButton({ text: "Check and submit your applicaitons"}) }} + +
+ +
+ +
+
+{% endblock %} diff --git a/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs b/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs index 740bf40133..d2117be96c 100644 --- a/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs +++ b/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs @@ -147,6 +147,10 @@ describe('GOV.UK Prototype Kit config', () => { importFrom: 'govuk/components/tag/macro.njk', macroName: 'govukTag' }, + { + importFrom: 'govuk/components/task-list/macro.njk', + macroName: 'govukTaskList' + }, { importFrom: 'govuk/components/textarea/macro.njk', macroName: 'govukTextarea' diff --git a/packages/govuk-frontend/src/govuk/components/_all.scss b/packages/govuk-frontend/src/govuk/components/_all.scss index e09c5d82b0..7761ada353 100644 --- a/packages/govuk-frontend/src/govuk/components/_all.scss +++ b/packages/govuk-frontend/src/govuk/components/_all.scss @@ -30,5 +30,6 @@ @import "skip-link/index"; @import "summary-list/index"; @import "table/index"; +@import "task-list/index"; @import "textarea/index"; @import "warning-text/index"; diff --git a/packages/govuk-frontend/src/govuk/components/task-list/README.md b/packages/govuk-frontend/src/govuk/components/task-list/README.md new file mode 100644 index 0000000000..042e52d145 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/README.md @@ -0,0 +1,15 @@ +# Task list + +## Installation + +See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component. + +## Guidance and Examples + +Find out when to use the task list component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/task-list). + +## Component options + +Use options to customise the appearance, content and behaviour of a component when using a macro, for example, changing the text. + +See [options table](https://design-system.service.gov.uk/components/task-list/#options-task-list-example) for details. diff --git a/packages/govuk-frontend/src/govuk/components/task-list/_index.scss b/packages/govuk-frontend/src/govuk/components/task-list/_index.scss new file mode 100644 index 0000000000..94c20ef621 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/_index.scss @@ -0,0 +1,73 @@ +@include govuk-exports("govuk/component/task-list") { + $govuk-task-list-hover-colour: govuk-colour("light-grey"); + + .govuk-task-list { + @include govuk-font($size: 19); + margin-top: 0; + @include govuk-responsive-margin(6, "bottom"); + padding: 0; + list-style-type: none; + } + + // This uses table layout so that the task name and status always appear side-by-side, with the width of + // each 'column' being flexible depending upon the length of the task names and statuses. + // + // The position is set to 'relative' so than an absolutely-positioned transparent element box + // can be added within the link so that the whole row can be clickable. + .govuk-task-list__item { + display: table; + position: relative; + width: 100%; + margin-bottom: 0; + padding-top: govuk-spacing(2); + padding-bottom: govuk-spacing(2); + border-bottom: 1px solid $govuk-border-colour; + } + + .govuk-task-list__item:first-child { + border-top: 1px solid $govuk-border-colour; + } + + // This class is added to the
  • elements where the task name is a link. + // The background hover colour is added to help indicate that the whole row is clickable, rather + // than just the visible link text. + .govuk-task-list__item--with-link:hover { + background: $govuk-task-list-hover-colour; + } + + .govuk-task-list__task-name-and-hint { + display: table-cell; + vertical-align: top; + @include govuk-text-colour; + } + + .govuk-task-list__status { + display: table-cell; + padding-left: govuk-spacing(2); + text-align: right; + vertical-align: top; + @include govuk-text-colour; + } + + .govuk-task-list__status--cannot-start-yet { + color: $govuk-secondary-text-colour; + } + + // This adds an empty transparent box covering the whole row, including the task status and + // any hint text. Because this is generated within the link element, this allows the whole area + // to be clickable. + .govuk-task-list__link::after { + content: ""; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + .govuk-task-list__task_hint { + margin-top: govuk-spacing(1); + color: $govuk-secondary-text-colour; + } +} diff --git a/packages/govuk-frontend/src/govuk/components/task-list/_task-list.scss b/packages/govuk-frontend/src/govuk/components/task-list/_task-list.scss new file mode 100644 index 0000000000..bfabb03440 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/_task-list.scss @@ -0,0 +1,2 @@ +@import "../../base"; +@import "./index"; diff --git a/packages/govuk-frontend/src/govuk/components/task-list/accessibility.test.mjs b/packages/govuk-frontend/src/govuk/components/task-list/accessibility.test.mjs new file mode 100644 index 0000000000..a55b7ab2e9 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/accessibility.test.mjs @@ -0,0 +1,22 @@ +import { axe, goToComponent } from 'govuk-frontend-helpers/puppeteer' +import { getExamples } from 'govuk-frontend-lib/files' + +describe('/components/task-list', () => { + describe('component examples', () => { + let exampleNames + + beforeAll(async () => { + exampleNames = Object.keys(await getExamples('task-list')) + }) + + it('passes accessibility tests', async () => { + for (const name of exampleNames) { + const exampleName = name.replace(/ /g, '-') + + // Navigation to example, create report + await goToComponent(page, 'task-list', { exampleName }) + await expect(axe(page)).resolves.toHaveNoViolations() + } + }, 60000) + }) +}) diff --git a/packages/govuk-frontend/src/govuk/components/task-list/macro.njk b/packages/govuk-frontend/src/govuk/components/task-list/macro.njk new file mode 100644 index 0000000000..abf049abc8 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/macro.njk @@ -0,0 +1,3 @@ +{% macro govukTaskList(params) %} + {%- include "./template.njk" -%} +{% endmacro %} diff --git a/packages/govuk-frontend/src/govuk/components/task-list/task-list.yaml b/packages/govuk-frontend/src/govuk/components/task-list/task-list.yaml new file mode 100644 index 0000000000..e769451c25 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/task-list.yaml @@ -0,0 +1,283 @@ +params: + - name: items + type: array + required: true + description: Array of tasks within the task list. + params: + - name: title + type: object + required: true + description: Object containing the main title for the task. + params: + - name: text + type: string + required: true + description: Text to use within the title. If `html` is provided, the `text` argument will be ignored. + - name: html + type: string + required: true + description: HTML to use within the title. If `html` is provided, the `text` argument will be ignored. + - name: classes + type: string + required: false + description: Classes to add to the title wrapper. + - name: hint + type: object + required: false + description: Object containing a hint for the task. + params: + - name: text + type: string + required: true + description: Text to use within the hint. If `html` is provided, the `text` argument will be ignored. + - name: html + type: string + required: true + description: HTML to use within the hint. If `html` is provided, the `text` argument will be ignored. + - name: status + type: object + required: true + description: Object containing the status of the task. + params: + - name: tag + type: object + required: false + descrption: Object containing the options for a tag that acts as the status for the task. + params: + - name: text + type: string + required: true + description: Text to use within the tag. If `html` is provided, the `text` argument will be ignored. + - name: html + type: string + required: true + description: HTML to use within the tag. If `html` is provided, the `text` argument will be ignored. + - name: classes + type: string + required: false + description: Classes to add to the tag. + - name: text + required: false + type: string + description: Text to use for the status, as an alternative to using a tag. If `html` or `tag` is provided, the `text` argument will be ignored. + - name: html + required: false + type: string + description: HTML to use for the status, as an alternative to using a tag. If `html` or `tag` is provided, the `text` argument will be ignored. + - name: classes + type: string + required: false + description: Classes to add to the status container. + - name: href + type: string + required: false + description: The value of the link’s `href` attribute for the task list item. + - name: classes + type: string + required: false + description: Classes to add to the item `div`. + - name: classes + type: string + required: false + description: Classes to add to the `ul` container for the task list. + - name: attributes + type: object + required: false + description: HTML attributes (for example data attributes) to add to the `ul` container for the task list. + - name: idPrefix + type: string + required: false + description: String to prefix ID for the tag and hint for each task list item. If `idPrefix` is not passed, fallback to using the `task-list` string instead. + +examples: + - name: default + data: + idPrefix: 'task-list-example' + items: + - title: + text: Company Directors + href: '#' + status: + text: Completed + classes: govuk-tag--black + + - title: + text: Registered company details + href: '#' + status: + tag: + text: Incomplete + classes: govuk-tag--blue + + - title: + text: Business plan + href: '#' + status: + tag: + text: Incomplete + classes: govuk-tag--blue + + - name: example with 3 states + data: + idPrefix: 'task-list-example' + items: + - title: + text: Company Directors + href: '#' + status: + text: Completed + - title: + text: Registered company details + href: '#' + status: + tag: + text: Not started + classes: govuk-tag--light-blue + - title: + text: Business plan + href: '#' + status: + tag: + text: In progress + classes: govuk-tag--blue + - title: + text: Documentation + href: '#' + status: + tag: + text: Not started + classes: govuk-tag--light-blue + + - name: example with hint text and additional states + data: + idPrefix: 'task-list-example' + items: + - title: + text: Company Directors + href: '#' + status: + text: Completed + - title: + text: Registered company details + href: '#' + status: + tag: + text: Not started + classes: govuk-tag--light-blue + - title: + text: Business plan + href: '#' + hint: + text: Ensure the plan covers objectives, strategies, sales, marketing and financial forecasts. + status: + tag: + text: Review + classes: govuk-tag--pink + - title: + text: Documentation + href: '#' + status: + tag: + text: In progress + classes: govuk-tag--blue + - title: + text: Charitable status + href: '#' + status: + tag: + text: Error + classes: govuk-tag--red + - classes: app-task-list__item--no-link + title: + text: Payment + hint: + text: It will cost between £15 and £75 + status: + text: Cannot start yet + classes: govuk-task-list__status--cannot-start-yet + + - name: example with all possible colours + data: + idPrefix: 'task-list-example' + items: + - title: + text: Task A + href: '#' + status: + text: Text colour + - title: + text: Task B + href: '#' + status: + text: Secondary text colour + classes: govuk-task-list__status--cannot-start-yet + - title: + text: Task C + href: '#' + status: + tag: + text: Grey + classes: govuk-tag--grey + - title: + text: Task D + href: '#' + status: + tag: + text: Blue + classes: govuk-tag--blue + - title: + text: Task E + href: '#' + status: + tag: + text: Light blue + classes: govuk-tag--light-blue + - title: + text: Task F + href: '#' + status: + tag: + text: Turquoise + classes: govuk-tag--turquoise + - title: + text: Task G + href: '#' + status: + tag: + text: Green + classes: govuk-tag--green + - title: + text: Task H + href: '#' + status: + tag: + text: Purple + classes: govuk-tag--purple + - title: + text: Task I + href: '#' + status: + tag: + text: Pink + classes: govuk-tag--pink + - title: + text: Task J + href: '#' + status: + tag: + text: Red + classes: govuk-tag--red + - title: + text: Task K + href: '#' + status: + tag: + text: Orange + classes: govuk-tag--orange + - title: + text: Task L + href: '#' + status: + tag: + text: Yellow + classes: govuk-tag--yellow diff --git a/packages/govuk-frontend/src/govuk/components/task-list/template.njk b/packages/govuk-frontend/src/govuk/components/task-list/template.njk new file mode 100644 index 0000000000..db1a2ebc91 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/template.njk @@ -0,0 +1,36 @@ +{% from "../tag/macro.njk" import govukTag -%} + +{% set idPrefix = params.idPrefix if params.idPrefix else "task-list" %} + diff --git a/packages/govuk-frontend/src/govuk/components/task-list/template.test.js b/packages/govuk-frontend/src/govuk/components/task-list/template.test.js new file mode 100644 index 0000000000..9b19183d20 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/task-list/template.test.js @@ -0,0 +1,74 @@ +const { render } = require('govuk-frontend-helpers/nunjucks') +const { getExamples } = require('govuk-frontend-lib/files') + +describe('Task List', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('task-list') + }) + + describe('default example', () => { + it('renders the default example', () => { + const $ = render('task-list', examples.default) + + const $component = $('.govuk-task-list') + expect($component.get(0).tagName).toEqual('ul') + + const $items = $component.find('.govuk-task-list__item') + expect($items.length).toEqual(3) + + expect($items.get(0).tagName).toEqual('li') + expect($items.hasClass('govuk-task-list__item--with-link')).toBeTruthy() + }) + + it('associates the task name link with the status using aria', async () => { + const $ = render('task-list', examples.default) + + const $component = $('.govuk-task-list') + + const $itemLink = $component.find('.govuk-task-list__link') + expect($itemLink.get(0).tagName).toEqual('a') + expect($itemLink.attr('href')).toEqual('#') + + const statusId = 'task-list-example-1-status' + expect($itemLink.attr('aria-describedby')).toEqual(statusId) + + const $statusWithId = $component.find(`#${statusId}`) + expect($statusWithId.get(0).tagName).toEqual('div') + + expect($statusWithId.text()).toContain('Completed') + expect($statusWithId.hasClass('govuk-task-list__status')).toBeTruthy() + }) + }) + + describe('example with no link, hint text and additional states', () => { + it('doesn’t include a link in the item with no href', () => { + const $ = render('task-list', examples['example with hint text and additional states']) + + const $itemWithNoLink = $('.app-task-list__item--no-link') + expect($itemWithNoLink.get(0).tagName).toEqual('li') + + const $itemWithNoLinkTitle = $itemWithNoLink.find('div') + expect($itemWithNoLinkTitle.text()).toContain('Payment') + }) + + it('renders hint text', () => { + const $ = render('task-list', examples['example with hint text and additional states']) + + const $hintText = $('.govuk-task-list__task_hint') + expect($hintText.get(0).tagName).toEqual('div') + expect($hintText.text()).toContain('Ensure the plan covers objectives, strategies, sales, marketing and financial forecasts.') + }) + + it('associates the hint text with the task link using aria', () => { + const $ = render('task-list', examples['example with hint text and additional states']) + + const $hintText = $('.govuk-task-list__task_hint') + expect($hintText.attr('id')).toEqual('task-list-example-3-hint') + + const $itemAssociatedWithHint = $(`.govuk-task-list__link[aria-describedby~="${$hintText.attr('id')}"]`) + expect($itemAssociatedWithHint.text()).toContain('Business plan') + }) + }) +})