diff --git a/CHANGELOG.md b/CHANGELOG.md index e81af06d..280e7084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [1.26.0] - 2021-02-19 + +## Added + +- HMRC user research banner + ## [1.25.0] - 2021-02-17 ## Updated diff --git a/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_0_phone.png b/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_0_phone.png index 1840d423..4c2707ab 100644 Binary files a/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_0_phone.png and b/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_0_phone.png differ diff --git a/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_1_tablet.png b/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_1_tablet.png index 05fef688..e95a4f8b 100644 Binary files a/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_1_tablet.png and b/backstop_data/bitmaps_reference/backstop_default_HMRC_Header_0_document_1_tablet.png differ diff --git a/backstop_data/bitmaps_reference/backstop_default_HMRC_User_Research_Banner_0_document_0_phone.png b/backstop_data/bitmaps_reference/backstop_default_HMRC_User_Research_Banner_0_document_0_phone.png new file mode 100644 index 00000000..0fac019f Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_default_HMRC_User_Research_Banner_0_document_0_phone.png differ diff --git a/backstop_data/bitmaps_reference/backstop_default_HMRC_User_Research_Banner_0_document_1_tablet.png b/backstop_data/bitmaps_reference/backstop_default_HMRC_User_Research_Banner_0_document_1_tablet.png new file mode 100644 index 00000000..801f7fb6 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_default_HMRC_User_Research_Banner_0_document_1_tablet.png differ diff --git a/check-compatibility.js b/check-compatibility.js index d1a0c8ec..f5299a34 100644 --- a/check-compatibility.js +++ b/check-compatibility.js @@ -14,7 +14,7 @@ if (!knownPrototypeKitNames.includes(consumerPackageJson.name)) { } const compatibility = { - 1.25: { + 1.26: { 'prototype-kit': ['9.12', '9.11', '9.10', '9.9', '9.8', '9.7', '9.6', '9.5', '9.4', '9.3', '9.2', '9.1', '9.0', '9.9', '9.10', '9.11'], }, 0.6: { diff --git a/package-lock.json b/package-lock.json index 9ea0480a..e20f181b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14129,6 +14129,12 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, + "mockdate": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.2.tgz", + "integrity": "sha512-ldfYSUW1ocqSHGTK6rrODUiqAFPGAg0xaHqYJ5tvj1hQyFsjuHpuWgWFTZWwDVlzougN/s2/mhDr8r5nY5xDpA==", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index c069893d..3551e434 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hmrc-frontend", - "version": "1.25.0", + "version": "1.26.0", "description": "Design patterns for HMRC frontends", "scripts": { "start": "gulp dev", @@ -10,7 +10,7 @@ "build:dist": "gulp build:dist --destination 'dist' && npm run test:build:dist", "build": "npm run build:dist && npm run build:package", "lint": "stylelint 'src/**/*.scss' && eslint .", - "lint:fix": "stylelint 'src/**/*.scss' && eslint --fix .", + "lint:fix": "stylelint 'src/**/*.scss' --fix && eslint --fix .", "test": "npm run build:dist && jest src && npm run lint", "test:update-snapshots": "jest src -u", "test:compatibility": "jest __tests__/check-compatibility.test.js", @@ -72,6 +72,7 @@ "js-yaml": "^3.14.1", "merge-stream": "^1.0.1", "mkdirp": "^1.0.4", + "mockdate": "^3.0.2", "node-sass": "^5.0.0", "nodemon": "^2.0.3", "nunjucks": "^3.2.1", diff --git a/src/all.js b/src/all.js index be99332f..1530472d 100644 --- a/src/all.js +++ b/src/all.js @@ -1,5 +1,6 @@ import AccountMenu from './components/account-menu/account-menu'; import TimeoutDialog from './components/timeout-dialog/timeout-dialog'; +import UserResearchBanner from './components/user-research-banner/user-research-banner'; function initAll() { const $AccountMenuSelector = '[data-module="hmrc-account-menu"]'; @@ -11,10 +12,16 @@ function initAll() { if ($TimeoutDialog) { new TimeoutDialog($TimeoutDialog).init(); } + + const $UserResearchBanner = document.querySelector('[data-module="hmrc-user-research-banner"]'); + if ($UserResearchBanner) { + new UserResearchBanner($UserResearchBanner).init(); + } } export default { initAll, AccountMenu, TimeoutDialog, + UserResearchBanner, }; diff --git a/src/components/_all.scss b/src/components/_all.scss index 2c6c8a5b..083a4d1d 100644 --- a/src/components/_all.scss +++ b/src/components/_all.scss @@ -9,3 +9,4 @@ @import "add-to-a-list/add-to-a-list"; @import "timeout-dialog/timeout-dialog"; @import "status-tags-in-task-list-pages/status-tags-in-task-list-pages"; +@import "user-research-banner/user-research-banner"; diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss index 151f8912..5e147cb3 100644 --- a/src/components/header/_header.scss +++ b/src/components/header/_header.scss @@ -52,3 +52,9 @@ } } } + +// Shift user research banner up 10px to compensate for govuk header border +// only when adjacent to govuk header +.hmrc-header + .hmrc-user-research-banner { + top: -10px; +} diff --git a/src/components/header/header.yaml b/src/components/header/header.yaml index c697fd9b..cae452ee 100644 --- a/src/components/header/header.yaml +++ b/src/components/header/header.yaml @@ -64,7 +64,45 @@ params: type: string required: false description: Either "en" for english or "cy" for welsh. - +- name: displayHmrcBanner + type: boolean + required: false + description: Display the HMRC banner or not +- name: userResearchBanner + type: object + required: false + description: User research banner parameters. If not supplied, the user research banner will not be displayed. + params: + - name: url + type: string + required: true + description: The URL the user research banner should link to +- name: phaseBanner + type: object + required: false + description: Phase banner parameters. If not supplied, the phase banner will not be displayed. + params: + - name: text + type: string + required: true + description: If `html` is set, this is not required. Text to use within the phase banner. If `html` is provided, the `text` argument will be ignored. + - name: html + type: string + required: true + description: If `text` is set, this is not required. HTML to use within the phase banner. If `html` is provided, the `text` argument will be ignored. + - name: tag + type: object + required: false + description: Options for the tag component. + isComponent: true + - name: classes + type: string + required: false + description: Classes to add to the phase banner container. + - name: attributes + type: object + required: false + description: HTML attributes (for example data attributes) to add to the phase banner container. previewLayout: full-width accessibilityCriteria: | Text and links in the Header must: @@ -89,7 +127,13 @@ examples: - name: default description: The standard header as used on information pages on GOV.UK data: - {} + userResearchBanner: + url: '/sign-up-for-user-research' + phaseBanner: + tag: + text: alpha + html: This is a new service – your feedback will help us to improve it. + displayHmrcBanner: true - name: with service name description: If your service is more than a few pages long, you can help users understand where they are by adding the service name. diff --git a/src/components/header/template.njk b/src/components/header/template.njk index ad47ce0b..4c30e63d 100644 --- a/src/components/header/template.njk +++ b/src/components/header/template.njk @@ -1,5 +1,7 @@ {% set language = params.language | default('en') %} {% from "../banner/macro.njk" import hmrcBanner %} +{% from "../user-research-banner/macro.njk" import hmrcUserResearchBanner %} + {% from "govuk/components/phase-banner/macro.njk" import govukPhaseBanner %}
+ {% if params.userResearchBanner %} + {{ hmrcUserResearchBanner({language: params.language, url: params.userResearchBanner.url }) }} + {% endif %} + {% if params.phaseBanner %} +
+ {{ govukPhaseBanner(params.phaseBanner) }} +
+ {% endif %} {% if params.displayHmrcBanner %}
{{ hmrcBanner({language: params.language}) }} diff --git a/src/components/user-research-banner/README.md b/src/components/user-research-banner/README.md new file mode 100644 index 00000000..07039863 --- /dev/null +++ b/src/components/user-research-banner/README.md @@ -0,0 +1 @@ +# HMRC User Research Banner diff --git a/src/components/user-research-banner/_user-research-banner.scss b/src/components/user-research-banner/_user-research-banner.scss new file mode 100644 index 00000000..9b435ee2 --- /dev/null +++ b/src/components/user-research-banner/_user-research-banner.scss @@ -0,0 +1,58 @@ +@import "node_modules/govuk-frontend/govuk/helpers/typography"; + +@mixin hmrc-white-link() { + color: govuk-colour("white"); + + &:link, + &:hover, + &:visited { + color: govuk-colour("white"); + } + + &:active, + &:focus { + color: govuk-colour("black"); + } +} + +.hmrc-user-research-banner { + display: none; + position: relative; + color: govuk-colour("white"); + background-color: $govuk-brand-colour; + box-sizing: border-box; + + @include govuk-responsive-padding(6, "top"); + @include govuk-responsive-padding(6, "bottom"); + @include govuk-font($size: 19); + + &__container { + position: relative; + } + + &__title { + @include govuk-responsive-margin(1, "bottom"); + } + + &__link { + @include hmrc-white-link(); + } + + &--show { + display: block; + } + + &__close { + @include hmrc-white-link(); + @include govuk-responsive-margin(2, "top"); + + outline: 0; + border: 0; + background: none; + text-decoration: underline; + box-shadow: none; + padding: 0; + cursor: pointer; + font-size: inherit; + } +} diff --git a/src/components/user-research-banner/macro.njk b/src/components/user-research-banner/macro.njk new file mode 100644 index 00000000..9c9b192e --- /dev/null +++ b/src/components/user-research-banner/macro.njk @@ -0,0 +1,3 @@ +{% macro hmrcUserResearchBanner(params) %} + {%- include "./template.njk" -%} +{% endmacro %} diff --git a/src/components/user-research-banner/template.njk b/src/components/user-research-banner/template.njk new file mode 100644 index 00000000..fdf759dd --- /dev/null +++ b/src/components/user-research-banner/template.njk @@ -0,0 +1,27 @@ +
+
+
+
+ {% if params.language == 'cy' %} + Helpwch i wella gwasanaethau CThEM + {% else %} + Help improve HMRC services + {% endif %} +
+ + {% if params.language == 'cy' %} + Cofrestrwch i gymryd rhan mewn ymchwil defnyddwyr (yn agor tab newydd) + {% else %} + Sign up to take part in user research (opens in new tab) + {% endif %} + +
+ +
+
diff --git a/src/components/user-research-banner/template.test.js b/src/components/user-research-banner/template.test.js new file mode 100644 index 00000000..4fe506ee --- /dev/null +++ b/src/components/user-research-banner/template.test.js @@ -0,0 +1,38 @@ +/* eslint-env jest */ + +const axe = require('../../../lib/axe-helper'); + +const { render, getExamples } = require('../../../lib/jest-helpers'); + +const examples = getExamples('user-research-banner'); + +describe('User Research Banner', () => { + describe('by default', () => { + it('passes accessibility tests', async () => { + const $ = render('user-research-banner', examples.default); + + const results = await axe($.html(), { + rules: { + region: { enabled: false }, + }, + }); + + expect(results).toHaveNoViolations(); + }); + + it('should render the correct URL', () => { + const $ = render('user-research-banner', examples.default); + expect($('.hmrc-user-research-banner__link').attr('href')).toEqual('https://www.example.com/user-research'); + }); + + it('should have English text by default', () => { + const $ = render('user-research-banner', examples.default); + expect($('.hmrc-user-research-banner__title').text().trim()).toEqual('Help improve HMRC services'); + }); + + it('should have Welsh text when specified', () => { + const $ = render('user-research-banner', examples.welsh); + expect($('.hmrc-user-research-banner__title').text().trim()).toEqual('Helpwch i wella gwasanaethau CThEM'); + }); + }); +}); diff --git a/src/components/user-research-banner/user-research-banner.js b/src/components/user-research-banner/user-research-banner.js new file mode 100644 index 00000000..91fe51de --- /dev/null +++ b/src/components/user-research-banner/user-research-banner.js @@ -0,0 +1,28 @@ +import { getCookie, setCookie } from '../../utils/cookies'; + +function UserResearchBanner($module) { + this.$module = $module; + this.$closeLink = this.$module.querySelector('.hmrc-user-research-banner__close'); + this.cookieName = 'mdtpurr'; + this.cookieExpiryDays = 28; +} + +UserResearchBanner.prototype.init = function init() { + const cookieData = getCookie(this.cookieName); + + if (cookieData == null) { + this.$module.classList.add('hmrc-user-research-banner--show'); + this.$closeLink.addEventListener('click', this.eventHandlers.noThanksClick.bind(this)); + } +}; + +UserResearchBanner.prototype.eventHandlers = { + noThanksClick(event) { + event.preventDefault(); + + setCookie(this.cookieName, 'suppress_for_all_services', { days: this.cookieExpiryDays }); + this.$module.classList.remove('hmrc-user-research-banner--show'); + }, +}; + +export default UserResearchBanner; diff --git a/src/components/user-research-banner/user-research-banner.test.js b/src/components/user-research-banner/user-research-banner.test.js new file mode 100644 index 00000000..cbed6538 --- /dev/null +++ b/src/components/user-research-banner/user-research-banner.test.js @@ -0,0 +1,72 @@ +/** + * @jest-environment ./lib/puppeteer/environment.js + */ +/* eslint-env jest */ + +const configPaths = require('../../../config/paths.json'); + +describe('/components/user-research-banner', () => { + let page; + const url = `http://localhost:${configPaths.ports.test}/components/user-research-banner/default/preview`; + const dayInSeconds = 24 * 60 * 60; + const expiryTimeInSeconds = 28 * dayInSeconds; + + const getExpectedExpiry = () => Math.trunc( + (new Date().getTime() + (expiryTimeInSeconds * 1000)) / 1000, + ); + + beforeAll(async () => { + // eslint-disable-next-line no-underscore-dangle + page = await global.__BROWSER__.newPage(); + }); + + beforeEach(async () => { + await page.goto(url); + }); + + afterEach(async () => { + await page.deleteCookie({ name: 'mdtpurr' }); + }); + + afterAll(async () => { + await page.close(); + }); + + describe('When a page with the user research banner is loaded', () => { + it('should display', async () => { + await page.waitForSelector('.hmrc-user-research-banner', { + visible: true, + }); + }); + + it('should close when No thanks is clicked', async () => { + await page.waitForSelector('.hmrc-user-research-banner', { + visible: true, + }); + + const closeLink = await page.$('.hmrc-user-research-banner__close'); + await closeLink.click(); + + await page.waitForSelector('.hmrc-user-research-banner', { + hidden: true, + }); + }); + + it('should set the mdtpurr cookie with the correct properties when No thanks is clicked', async () => { + expect(await page.cookies()).toHaveLength(0); + + const closeLink = await page.$('.hmrc-user-research-banner__close'); + + const earliestExpectedExpiry = getExpectedExpiry(); + await closeLink.click(); + const latestExpectedExpiry = getExpectedExpiry(); + + const cookies = await page.cookies(); + expect(cookies).toHaveLength(1); + expect(cookies[0].name).toEqual('mdtpurr'); + expect(cookies[0].value).toEqual('suppress_for_all_services'); + expect(cookies[0].expires).toBeGreaterThanOrEqual(earliestExpectedExpiry); + expect(cookies[0].expires).toBeGreaterThanOrEqual(latestExpectedExpiry); + }); + }); +}); diff --git a/src/components/user-research-banner/user-research-banner.yaml b/src/components/user-research-banner/user-research-banner.yaml new file mode 100644 index 00000000..44ffabaa --- /dev/null +++ b/src/components/user-research-banner/user-research-banner.yaml @@ -0,0 +1,22 @@ +params: +- name: url + type: string + required: true + description: The url to link to from the user research banner +- name: language + type: string + required: false + description: Either "en" for English or "cy" for Welsh + +previewLayout: full-width + +examples: +- name: default + description: The standard user research banner + data: + url: 'https://www.example.com/user-research' + language: en +- name: welsh + description: The standard user research banner for Welsh + data: + language: cy diff --git a/src/utils/__tests__/cookies.test.js b/src/utils/__tests__/cookies.test.js new file mode 100644 index 00000000..c5392023 --- /dev/null +++ b/src/utils/__tests__/cookies.test.js @@ -0,0 +1,70 @@ +/* eslint-env jest */ + +import MockDate from 'mockdate'; +import { getCookie, setCookie } from '../cookies'; + +describe('cookies', () => { + describe('setCookie', () => { + beforeEach(() => { + delete window.location; + window.location = { + protocol: 'http:', + }; + + // Clear any cookies that exist + document.cookie = 'baz=; Max-Age=-99999999;'; + document.cookie = 'foo=; Max-Age=-99999999;'; + document.cookie = 'bam=; Max-Age=-99999999;'; + document.cookie = 'faz=; Max-Age=-99999999;'; + }); + + it('should set a cookie', () => { + const cookieString = setCookie('baz', 'foo'); + + expect(cookieString).toEqual('baz=foo; path=/'); + expect(document.cookie).toEqual('baz=foo'); + }); + + it('should set multiple cookies', () => { + setCookie('baz', 'foo'); + setCookie('faz', 'bar'); + + expect(document.cookie).toEqual('baz=foo; faz=bar'); + }); + + it('should set a cookie with an expiry date', () => { + MockDate.set('1/1/2050'); + + const cookieString = setCookie('foo', 'bar', { days: 1 }); + + expect(cookieString).toEqual('foo=bar; path=/; expires=Sun, 02 Jan 2050 00:00:00 GMT'); + expect(document.cookie).toEqual('foo=bar'); + }); + + it('should set a secure cookie', () => { + window.location.protocol = 'https:'; + const cookieString = setCookie('foo', 'bar'); + + expect(cookieString).toEqual('foo=bar; path=/; Secure'); + }); + }); + + describe('getCookie', () => { + it('should correctly retrieve a cookie', () => { + document.cookie = 'foo=bar'; + expect(getCookie('foo')).toEqual('bar'); + }); + + it('should correctly retrieve a cookie if more than one exists', () => { + document.cookie = 'foo=bar'; + document.cookie = 'bam=bat'; + + expect(getCookie('bam')).toEqual('bat'); + expect(getCookie('foo')).toEqual('bar'); + }); + + it("should return null if the cookie doesn't exist", () => { + expect(getCookie('nonexistent')).toEqual(null); + }); + }); +}); diff --git a/src/utils/cookies.js b/src/utils/cookies.js new file mode 100644 index 00000000..95c21b0d --- /dev/null +++ b/src/utils/cookies.js @@ -0,0 +1,30 @@ +// Based on https://github.com/alphagov/govuk_template_jinja +export const setCookie = (name, value, options = {}) => { + let cookieString = `${name}=${value}; path=/`; + if (options.days) { + const date = new Date(); + date.setTime(date.getTime() + (options.days * 24 * 60 * 60 * 1000)); + cookieString = `${cookieString}; expires=${date.toGMTString()}`; + } + if (window.location.protocol === 'https:') { + cookieString += '; Secure'; + } + document.cookie = cookieString; + + return cookieString; +}; + +export const getCookie = (name) => { + const nameEQ = `${name}=`; + const cookies = document.cookie.split(';'); + for (let i = 0, len = cookies.length; i < len; i += 1) { + let cookie = cookies[i]; + while (cookie.charAt(0) === ' ') { + cookie = cookie.substring(1, cookie.length); + } + if (cookie.indexOf(nameEQ) === 0) { + return decodeURIComponent(cookie.substring(nameEQ.length)); + } + } + return null; +}; diff --git a/tasks/gulp/backstop-config.js b/tasks/gulp/backstop-config.js index 3fb50621..f828a4ef 100644 --- a/tasks/gulp/backstop-config.js +++ b/tasks/gulp/backstop-config.js @@ -76,6 +76,11 @@ module.exports = ({ host, port }) => ({ url: `http://${host}:${port}/components/timeout-dialog/preview`, delay: 2000, }, + { + label: 'HMRC User Research Banner', + url: `http://${host}:${port}/components/user-research-banner/preview`, + delay: 2000, + }, ], paths: { bitmaps_reference: 'backstop_data/bitmaps_reference',