diff --git a/app/assets/javascripts/govuk_publishing_components/components/global-bar.js b/app/assets/javascripts/govuk_publishing_components/components/global-bar.js new file mode 100644 index 0000000000..a281730d00 --- /dev/null +++ b/app/assets/javascripts/govuk_publishing_components/components/global-bar.js @@ -0,0 +1,91 @@ +//= require govuk_publishing_components/lib/GlobalBarHelper.js + +/* global parseCookie */ + +/* + Global bar + + Manages count of how many times a global bar has been seen + using cookies. +*/ +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + function GlobalBar ($module) { + this.$module = $module + } + + GlobalBar.prototype.init = function () { + var GLOBAL_BAR_SEEN_COOKIE = 'global_bar_seen' + var alwaysOn = this.$module.getAttribute('data-global-bar-permanent') + if (alwaysOn === 'false') { + alwaysOn = false // in this situation we need to convert string to boolean + } + var cookieCategory = GOVUK.getCookieCategory(GLOBAL_BAR_SEEN_COOKIE) + var cookieConsent = GOVUK.getConsentCookie()[cookieCategory] + + if (cookieConsent) { + // If the cookie is not set, let's set a basic one + if (GOVUK.getCookie(GLOBAL_BAR_SEEN_COOKIE) === null || parseCookie(GOVUK.getCookie(GLOBAL_BAR_SEEN_COOKIE)).count === undefined) { + GOVUK.setCookie('global_bar_seen', JSON.stringify({ count: 0, version: 0 }), { days: 84 }) + } + + var currentCookie = parseCookie(GOVUK.getCookie(GLOBAL_BAR_SEEN_COOKIE)) + var currentCookieVersion = currentCookie.version + var count = viewCount() + } + + this.$module.addEventListener('click', function (e) { + var target = e.target + if (target.classList.contains('dismiss')) { + hide(e) + } + }) + + // if the element is visible + if (this.$module.offsetParent !== null && !alwaysOn) { + incrementViewCount(count) + } + + function hide (event) { + var currentCookie = parseCookie(GOVUK.getCookie(GLOBAL_BAR_SEEN_COOKIE)) + var cookieVersion = currentCookieVersion + + if (currentCookie) { + cookieVersion = currentCookie.version + } + + var cookieValue = JSON.stringify({ count: 999, version: cookieVersion }) + GOVUK.setCookie(GLOBAL_BAR_SEEN_COOKIE, cookieValue, { days: 84 }) + var additional = document.querySelector('.global-bar-additional') + if (additional) { + additional.classList.remove('global-bar-additional--show') + } + var dismiss = document.querySelector('.global-bar__dismiss') + if (dismiss) { + dismiss.classList.remove('global-bar__dismiss--show') + } + event.preventDefault() + } + + function incrementViewCount (count) { + count = count + 1 + var cookieValue = JSON.stringify({ count: count, version: currentCookieVersion }) + GOVUK.setCookie(GLOBAL_BAR_SEEN_COOKIE, cookieValue, { days: 84 }) + } + + function viewCount () { + var viewCountCookie = GOVUK.getCookie(GLOBAL_BAR_SEEN_COOKIE) + var viewCount = parseInt(parseCookie(viewCountCookie).count, 10) + + if (isNaN(viewCount)) { + viewCount = 0 + } + + return viewCount + } + } + + Modules.GlobalBar = GlobalBar +})(window.GOVUK.Modules) diff --git a/app/assets/javascripts/govuk_publishing_components/lib/GlobalBarHelper.js b/app/assets/javascripts/govuk_publishing_components/lib/GlobalBarHelper.js new file mode 100644 index 0000000000..05b860d8a1 --- /dev/null +++ b/app/assets/javascripts/govuk_publishing_components/lib/GlobalBarHelper.js @@ -0,0 +1,10 @@ +function parseCookie(cookie) { + var parsedCookie = JSON.parse(cookie) + + // Tests seem to run differently on CI, and require an extra parse + if (typeof parsedCookie !== "object") { + parsedCookie = JSON.parse(parsedCookie) + } + + return parsedCookie +} diff --git a/app/assets/javascripts/govuk_publishing_components/lib/global-bar-init.js b/app/assets/javascripts/govuk_publishing_components/lib/global-bar-init.js new file mode 100644 index 0000000000..4bf86a0dd7 --- /dev/null +++ b/app/assets/javascripts/govuk_publishing_components/lib/global-bar-init.js @@ -0,0 +1,114 @@ +//= require govuk_publishing_components/lib/GlobalBarHelper.js +//= require govuk_publishing_components/lib/cookie-functions + +/* global parseCookie */ + +'use strict' +window.GOVUK = window.GOVUK || {} + +// Bump this if you are releasing a major change to the banner +// This will reset the view count so all users will see the banner, even if previously seen +var BANNER_VERSION = 8 +var GLOBAL_BAR_SEEN_COOKIE = 'global_bar_seen' + +var globalBarInit = { + getBannerVersion: function () { + return BANNER_VERSION + }, + + getLatestCookie: function () { + var currentCookie = window.GOVUK.getCookie(GLOBAL_BAR_SEEN_COOKIE) + + return currentCookie + }, + + urlBlockList: function () { + var paths = [ + '^/coronavirus/.*$', + '^/brexit(.cy)?$', + '^/transition-check/.*$', + '^/eubusiness(\\..*)?$', + '^/account/.*$' + ] + + var ctaLink = document.querySelector('.js-call-to-action') + if (ctaLink) { + var ctaPath = '^' + ctaLink.getAttribute('href') + '$' + paths.push(ctaPath) + } + + return new RegExp(paths.join('|')).test(window.location.pathname) + }, + + setBannerCookie: function () { + var cookieCategory = window.GOVUK.getCookieCategory(GLOBAL_BAR_SEEN_COOKIE) + var cookieConsent = GOVUK.getConsentCookie() + var value + + if (cookieConsent && cookieConsent[cookieCategory]) { + // Coronavirus banner - auto hide after user has been on landing page + if (window.location.pathname === '/coronavirus') { + value = JSON.stringify({ count: 999, version: globalBarInit.getBannerVersion() }) + } else { + value = JSON.stringify({ count: 0, version: globalBarInit.getBannerVersion() }) + } + + window.GOVUK.setCookie(GLOBAL_BAR_SEEN_COOKIE, value, { days: 84 }) + } + }, + + makeBannerVisible: function () { + document.documentElement.className = document.documentElement.className.concat(' show-global-bar') + var globalBarEl = document.querySelector('#global-bar') + if (globalBarEl) { + globalBarEl.setAttribute('data-ga4-global-bar', '') + } + }, + + init: function () { + var currentCookieVersion + + if (!globalBarInit.urlBlockList()) { + if (globalBarInit.getLatestCookie() === null) { + globalBarInit.setBannerCookie() + globalBarInit.makeBannerVisible() + } else { + currentCookieVersion = parseCookie(globalBarInit.getLatestCookie()).version + + if (currentCookieVersion !== globalBarInit.getBannerVersion()) { + globalBarInit.setBannerCookie() + } + + var newCookieCount = parseCookie(globalBarInit.getLatestCookie()).count + + // If banner has been manually dismissed, hide the additional info + if (newCookieCount === 999) { + var globalBarAdditional = document.querySelector('.global-bar-additional') + if (globalBarAdditional) { + globalBarAdditional.classList.remove('global-bar-additional--show') + } + var globarBarDismiss = document.querySelector('.global-bar__dismiss') + if (globarBarDismiss) { + globarBarDismiss.classList.remove('global-bar__dismiss--show') + } + } + + globalBarInit.makeBannerVisible() + } + } else { + // If on a url in the blocklist, set cookie but don't show the banner + if (globalBarInit.getLatestCookie() === null) { + globalBarInit.setBannerCookie() + } else { + currentCookieVersion = parseCookie(globalBarInit.getLatestCookie()).version + + if (currentCookieVersion !== globalBarInit.getBannerVersion()) { + globalBarInit.setBannerCookie() + } + } + } + } +} + +window.GOVUK.globalBarInit = globalBarInit +window.GOVUK.globalBarInit.init() diff --git a/app/assets/stylesheets/govuk_publishing_components/components/_global-bar.scss b/app/assets/stylesheets/govuk_publishing_components/components/_global-bar.scss new file mode 100644 index 0000000000..956e224795 --- /dev/null +++ b/app/assets/stylesheets/govuk_publishing_components/components/_global-bar.scss @@ -0,0 +1,86 @@ +@import "govuk_publishing_components/individual_component_support"; + +// stylelint-disable selector-max-id +.show-global-bar #global-header-bar { + display: none; +} + +$covid-yellow: #fff500; +$covid-grey: #262828; + +.gem-c-global-bar { + @include govuk-font(19); + background-color: #d9e7f2; + border-top: govuk-spacing(2) solid govuk-colour("blue"); + display: none; + + .show-global-bar & { + display: block; + } + + .govuk-link, + .govuk-link:link { + color: govuk-colour("black"); + + &:visited { + color: govuk-colour("black"); + } + + &:focus { + color: govuk-colour("black"); + } + } +} + +.gem-c-global-bar-message { + margin-bottom: 0; + margin-top: 0; + padding: govuk-spacing(4) 0; +} + +.gem-c-global-bar-title { + font-weight: 700; + margin-right: govuk-spacing(2); + margin-bottom: govuk-spacing(1); + + &:only-child { + margin: 0; + } +} + +.gem-c-global-bar-title, +.gem-c-global-bar-text { + color: govuk-colour("black"); +} + +.gem-c-global-bar-title__nowrap { + white-space: nowrap; +} + +.gem-c-global-bar-dismiss-wrapper { + margin-top: govuk-spacing(4); +} + +.gem-c-global-bar__dismiss { + display: none; +} + +.gem-c-global-bar__dismiss--show { + display: inline-block; +} + +// [TODO: don't allow cross component styling like this ] +.gem-c-govspeak .gem-c-global-bar__heading { + @include govuk-font(19, $weight: bold); + margin-top: 0; + margin-bottom: govuk-spacing(1); +} + +.gem-c-govspeak .gem-c-global-bar__list { + margin-top: 0; +} +// [/end TODO] + +.gem-c-global-bar__list { + margin-top: 0; +} diff --git a/app/helpers/timed_update_helper.rb b/app/helpers/timed_update_helper.rb new file mode 100644 index 0000000000..3dbc6a37bf --- /dev/null +++ b/app/helpers/timed_update_helper.rb @@ -0,0 +1,5 @@ +module TimedUpdateHelper + def before_update_time?(year:, month:, day:, hour:, minute:) + Time.zone.now.before? Time.zone.local(year, month, day, hour, minute) + end +end diff --git a/app/views/govuk_publishing_components/components/_global_bar.html.erb b/app/views/govuk_publishing_components/components/_global_bar.html.erb new file mode 100644 index 0000000000..455fe2d8af --- /dev/null +++ b/app/views/govuk_publishing_components/components/_global_bar.html.erb @@ -0,0 +1,70 @@ +<% + add_gem_component_stylesheet("global-bar") + + if before_update_time?(year: 2024, month: 12, day: 30, hour: 22, minute: 0) + show_global_bar ||= true # Toggles the appearance of the global bar + title = "Bring photo ID to vote" + title_href = "/how-to-vote/photo-id-youll-need" + link_text = "Check what photo ID you'll need to vote in person in the General Election on 4 July." + else + show_global_bar = false + title = nil + title_href = nil + link_text = nil + end + + link_href = false + + # Toggles banner being permanently visible + # If true, banner is always_visible & does not disappear automatically after 3 pageviews + # Regardless of value, banner is always manually dismissable by users + always_visible = true + + global_bar_classes = %w(gem-c-global-bar govuk-!-display-none-print) + + title_classes = %w(gem-c-global-bar-title) + title_classes << "js-call-to-action" if title_href + title_classes << "govuk-link" if title_href + + ga4_data = { + event_name: "navigation", + type: "global bar", + section: title, + }.to_json + +-%> + +<% if show_global_bar %> + +
data-nosnippet> +

+ <% if title %> + <% if title_href %> + <%= title %> + <% else %> + <%= title %> + <% end %> + <% end %> + + <% if link_text %> + + <% if link_href %> + <%= link_to( + link_text, + link_href, + rel: "external noreferrer", + class: "govuk-link js-call-to-action", + data: { + module: "ga4-link-tracker", + ga4_link: ga4_data, + }, + ) %> + <% else %> + <%= link_text %> + <% end %> + + <% end %> +

+
+ +<% end %> diff --git a/app/views/govuk_publishing_components/components/docs/global_bar.yml b/app/views/govuk_publishing_components/components/docs/global_bar.yml new file mode 100644 index 0000000000..92712c7787 --- /dev/null +++ b/app/views/govuk_publishing_components/components/docs/global_bar.yml @@ -0,0 +1,8 @@ +name: Global banner +description: A site-wide banner used to convey important information +body: | + See the [opsmanual](https://docs.publishing.service.gov.uk/manual/global-banner.html) for information about what the Global banner is and when it should be activated. +shared_accessibility_criteria: +- link +examples: + default: {} diff --git a/spec/javascripts/global-bar-init.spec.js b/spec/javascripts/global-bar-init.spec.js new file mode 100644 index 0000000000..426bf83bc9 --- /dev/null +++ b/spec/javascripts/global-bar-init.spec.js @@ -0,0 +1,86 @@ +/* global globalBarInit, parseCookie, expectGlobalBarToShow, expectGlobalBarToBeHidden, expectGa4AttributeToExist, expectGa4AttributeToNotExist */ + +describe('Global bar initialize', function () { + beforeAll(function () { + $('html').append('
') + }) + + beforeEach(function () { + deleteAllCookies() + spyOn(globalBarInit, 'getBannerVersion').and.returnValue(5) + $('html').removeClass('show-global-bar') + $('#global-bar').removeAttr('data-ga4-global-bar') + + window.GOVUK.setConsentCookie({ settings: true }) + }) + + it('does not show the banner on a blocked URL', function () { + spyOn(globalBarInit, 'urlBlockList').and.returnValue(true) + GOVUK.globalBarInit.init() + + // The cookie should still be set, but the banner should not be visible + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(0) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(5) + expectGlobalBarToBeHidden() + expectGa4AttributeToNotExist() + }) + + it('sets global_bar_seen cookie', function () { + GOVUK.globalBarInit.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(0) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(5) + expectGlobalBarToShow() + expectGa4AttributeToExist() + }) + + it('sets cookie to default value if current cookie is old (prior to versioning mechanism)', function () { + GOVUK.setCookie('global_bar_seen', 1) + GOVUK.globalBarInit.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(0) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(5) + + expectGlobalBarToShow() + expectGa4AttributeToExist() + }) + + it('resets cookie if version number is out of date, if count below 3', function () { + GOVUK.setCookie('global_bar_seen', JSON.stringify({ count: 1, version: 1 })) + GOVUK.globalBarInit.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(0) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(5) + expectGlobalBarToShow() + expectGa4AttributeToExist() + }) + + it('resets cookie if version number is out of date, if count above 3', function () { + GOVUK.setCookie('global_bar_seen', JSON.stringify({ count: 10, version: 1 })) + GOVUK.globalBarInit.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(0) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(5) + expectGlobalBarToShow() + expectGa4AttributeToExist() + }) + + it('makes banner visible if view count is less than 3', function () { + GOVUK.setCookie('global_bar_seen', JSON.stringify({ count: 1, version: 5 })) + GOVUK.globalBarInit.init() + + expectGlobalBarToShow() + expectGa4AttributeToExist() + }) +}) + +function deleteAllCookies () { + var cookies = document.cookie.split(';') + + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i] + var eqPos = cookie.indexOf('=') + var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT' + } +} diff --git a/spec/javascripts/helpers/GlobalBarHelper.js b/spec/javascripts/helpers/GlobalBarHelper.js new file mode 100644 index 0000000000..439070ebcc --- /dev/null +++ b/spec/javascripts/helpers/GlobalBarHelper.js @@ -0,0 +1,26 @@ +//= require govuk_publishing_components/lib/GlobalBarHelper.js + +/* eslint-disable no-unused-vars */ +function expectGlobalBarToShow () { + expect($('html').hasClass('show-global-bar')).toBe(true) +} + +function expectGlobalBarToBeHidden () { + expect($('html').hasClass('show-global-bar')).toBe(false) +} + +function expectGa4AttributeToExist () { + expect($('#global-bar').attr('data-ga4-global-bar')).toBe('') +} + +function expectGa4AttributeToNotExist () { + expect($('#global-bar').attr('data-ga4-global-bar')).toBe(undefined) +} + +function expectAdditionalSectionToBeVisible () { + expect($('.global-bar-additional').hasClass('global-bar-additional--show')).toBe(true) +} + +function expectAdditionalSectionToBeHidden () { + expect($('.global-bar-additional').hasClass('global-bar-additional--show')).toBe(false) +} diff --git a/spec/javascripts/modules/global-bar.spec.js b/spec/javascripts/modules/global-bar.spec.js new file mode 100644 index 0000000000..2b290d8d68 --- /dev/null +++ b/spec/javascripts/modules/global-bar.spec.js @@ -0,0 +1,144 @@ +/* global parseCookie, expectAdditionalSectionToBeHidden */ +describe('Global bar module', function () { + 'use strict' + + var globalBar + var element + + beforeEach(function () { + window.GOVUK.setConsentCookie({ settings: true }) + document.cookie = 'global_bar_seen=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' + }) + + afterEach(function () { + window.GOVUK.setConsentCookie({ settings: null }) + $('#global-bar').remove() + }) + + describe('global banner default', function () { + beforeEach(function () { + element = $( + '
' + + 'Register to Vote' + + 'Hide message' + + '
This is some additional content
' + + '
' + ) + + document.cookie = 'global_bar_seen=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' + }) + + it('sets basic global_bar_seen cookie if not already set', function () { + expect(GOVUK.getCookie('global_bar_seen')).toBeNull() + + globalBar = new GOVUK.Modules.GlobalBar(element[0]) + globalBar.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(0) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(0) + }) + + it('sets basic global_bar_seen cookie if existing one is malformed', function () { + GOVUK.setCookie('global_bar_seen', 1) + + globalBar = new GOVUK.Modules.GlobalBar(element[0]) + globalBar.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(0) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(0) + }) + }) + + describe('global banner interactions', function () { + beforeEach(function () { + element = $( + '
' + + 'Register to Vote' + + 'Hide message' + + '
' + ) + + $(document.body).append(element) + + document.cookie = 'global_bar_seen=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' + }) + + it('increments view count', function () { + GOVUK.setCookie('global_bar_seen', JSON.stringify({ count: 1, version: 0 })) + + globalBar = new GOVUK.Modules.GlobalBar(element[0]) + globalBar.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(2) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(0) + }) + + it('hides additional information section when dismiss link is clicked', function () { + globalBar = new GOVUK.Modules.GlobalBar(element[0]) + globalBar.init() + + $(element).find('.dismiss')[0].click() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(999) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(0) + + expectAdditionalSectionToBeHidden() + }) + + it('hides dismiss link once dismiss link is clicked', function () { + globalBar = new GOVUK.Modules.GlobalBar(element[0]) + globalBar.init() + + $(element).find('.dismiss')[0].click() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(999) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(0) + + expect($('.global-bar-dismiss').hasClass('global-bar-dismiss--show')).toBe(false) + }) + }) + + describe('always on', function () { + beforeEach(function () { + document.cookie = 'global_bar_seen=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' + }) + + it('does not increment view count when on', function () { + element = $( + '
' + + 'Register to Vote' + + 'Hide message' + + '
' + ) + + $(document.body).append(element) + + GOVUK.setCookie('global_bar_seen', JSON.stringify({ count: 2, version: 0 })) + + globalBar = new GOVUK.Modules.GlobalBar(element[0]) + globalBar.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(2) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(0) + }) + + it('continues to increment view count when off', function () { + element = $( + '
' + + 'Register to Vote' + + 'Hide message' + + '
' + ) + + $(document.body).append(element) + + GOVUK.setCookie('global_bar_seen', JSON.stringify({ count: 2, version: 0 })) + + globalBar = new GOVUK.Modules.GlobalBar(element[0]) + globalBar.init() + + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).count).toBe(3) + expect(parseCookie(GOVUK.getCookie('global_bar_seen')).version).toBe(0) + }) + }) +})