From 78668ad8753b1eae1d9d876e22d2fb7e02e0522c Mon Sep 17 00:00:00 2001 From: Simon Whatley <23444+simonwhatley@users.noreply.github.com> Date: Tue, 9 Jun 2020 09:38:47 +0100 Subject: [PATCH] Add step by step navigation component (#81) --- .../step-by-step-navigation/index.html | 240 ++++++++++ app/views/index.html | 1 + app/views/layouts/base.html | 1 + src/govuk-pub/components/_all.scss | 1 + .../step-by-step-navigation/README.md | 135 ++++++ .../_step-by-step-navigation.scss | 430 +++++++++++++++++ .../step-by-step-navigation/macro.njk | 3 + .../step-by-step-navigation.js | 432 ++++++++++++++++++ .../step-by-step-navigation/template.njk | 41 ++ 9 files changed, 1284 insertions(+) create mode 100644 app/views/components/step-by-step-navigation/index.html create mode 100644 src/govuk-pub/components/step-by-step-navigation/README.md create mode 100644 src/govuk-pub/components/step-by-step-navigation/_step-by-step-navigation.scss create mode 100644 src/govuk-pub/components/step-by-step-navigation/macro.njk create mode 100644 src/govuk-pub/components/step-by-step-navigation/step-by-step-navigation.js create mode 100644 src/govuk-pub/components/step-by-step-navigation/template.njk diff --git a/app/views/components/step-by-step-navigation/index.html b/app/views/components/step-by-step-navigation/index.html new file mode 100644 index 0000000..40c2880 --- /dev/null +++ b/app/views/components/step-by-step-navigation/index.html @@ -0,0 +1,240 @@ +{% extends "layouts/base.html" %} + +{% block pageTitle %} + Step by step navigation component – GOV.UK Publishing Frontend +{% endblock %} + +{% block pageScripts %} + + +{% endblock %} + +{% block beforeContent %} +{{ govukBackLink({ + text: "Back", + href: "../" +}) }} +{% endblock %} + +{% block content %} + +
+
+

+ Components + Step by step navigation +

+ +

A series of expanding/collapsing steps designed to navigate a user through a process.

+ +

View a list of live step by steps

+ +

GOV.UK component guide

+ +

View the Nunjucks macro options on GitHub

+ +

Examples

+ +

Example 1

+ + {{ govukPubStepByStepNavigation({ + id: 'step-by-step-navigation-1', + steps: [{ + heading: { + text: 'Step 1' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, + { + text: 'Vivamus nec pharetra ipsum. Duis euismod augue nisl, sit amet dictum velit malesuada at.' + }] + }, + { + heading: { + text: 'Step 2' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }], + logic: 'and' + }, + { + heading: { + text: 'Step 3' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }] + }) }} + +

Example 2

+ +

With an optional step

+ + {{ govukPubStepByStepNavigation({ + id: 'step-by-step-navigation-2', + steps: [{ + heading: { + text: 'Step 1' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, + { + text: 'Vivamus nec pharetra ipsum. Duis euismod augue nisl, sit amet dictum velit malesuada at.' + }] + }, + { + heading: { + text: 'Step 2' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }], + logic: 'or' + }, + { + heading: { + text: 'Step 3' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }] + }) }} + +

Example 3

+ +

With a step open by default

+ + {{ govukPubStepByStepNavigation({ + id: 'step-by-step-navigation-3', + steps: [{ + heading: { + text: 'Step 1 (Open by default)' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, + { + text: 'Vivamus nec pharetra ipsum. Duis euismod augue nisl, sit amet dictum velit malesuada at.' + }], + expanded: true + }, + { + heading: { + text: 'Step 2' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }, + { + heading: { + text: 'Step 3' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }] + }) }} + +

Example 4

+ +

With an active step

+ + {{ govukPubStepByStepNavigation({ + id: 'step-by-step-navigation-4', + steps: [{ + heading: { + text: 'Step 1' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, + { + text: 'Vivamus nec pharetra ipsum. Duis euismod augue nisl, sit amet dictum velit malesuada at.' + }], + active: true + }, + { + heading: { + text: 'Step 2' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }], + logic: 'or' + }, + { + heading: { + text: 'Step 3' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }] + }) }} + +

Example 5

+ +

Small step by step for use in a sidebar

+ + {{ govukPubStepByStepNavigation({ + id: 'step-by-step-navigation-5', + small: true, + steps: [{ + heading: { + text: 'Step 1' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, + { + text: 'Vivamus nec pharetra ipsum. Duis euismod augue nisl, sit amet dictum velit malesuada at.' + }] + }, + { + heading: { + text: 'Step 2' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }, + { + heading: { + text: 'Step 3' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }] + }) }} + +
+
+ +{% endblock %} diff --git a/app/views/index.html b/app/views/index.html index e60480c..134b3b0 100755 --- a/app/views/index.html +++ b/app/views/index.html @@ -23,6 +23,7 @@

Components

  • Print link
  • Related navigation
  • Share links
  • +
  • Step by step navigation
  • Subscription links
  • diff --git a/app/views/layouts/base.html b/app/views/layouts/base.html index 5536628..3d1b615 100755 --- a/app/views/layouts/base.html +++ b/app/views/layouts/base.html @@ -33,6 +33,7 @@ {%- from "govuk-pub/components/print-link/macro.njk" import govukPubPrintLink %} {%- from "govuk-pub/components/related-navigation/macro.njk" import govukPubRelatedNavigation %} {%- from "govuk-pub/components/share-links/macro.njk" import govukPubShareLinks %} +{%- from "govuk-pub/components/step-by-step-navigation/macro.njk" import govukPubStepByStepNavigation %} {%- from "govuk-pub/components/subscription-links/macro.njk" import govukPubSubscriptionLinks %} diff --git a/src/govuk-pub/components/_all.scss b/src/govuk-pub/components/_all.scss index 578bab5..922b8f1 100644 --- a/src/govuk-pub/components/_all.scss +++ b/src/govuk-pub/components/_all.scss @@ -8,4 +8,5 @@ @import "print-link/print-link"; @import "related-navigation/related-navigation"; @import "share-links/share-links"; +@import "step-by-step-navigation/step-by-step-navigation"; @import "subscription-links/subscription-links"; diff --git a/src/govuk-pub/components/step-by-step-navigation/README.md b/src/govuk-pub/components/step-by-step-navigation/README.md new file mode 100644 index 0000000..e5e53db --- /dev/null +++ b/src/govuk-pub/components/step-by-step-navigation/README.md @@ -0,0 +1,135 @@ +# Step by step navigation + +Displays a series of expanding/collapsing steps designed to navigate a user through a process. + +Step by step navigation shows a sequence of steps towards a specific goal, such as ‘learning to drive’. Each step can contain one or more links to pages. User research suggested that each step should be collapsed by default so that users are not overwhelmed with information. + +If JavaScript is disabled the step by step navigation expands fully. All of the functionality (including the icons and aria attributes) are added using JavaScript. + +This component is based on the accordion component in collections. + +[Preview the component](https://govuk-publishing-frontend.herokuapp.com/components/step-by-step-navigation/) + +[View a list of live step by steps](https://live-stuff.herokuapp.com/document-types/step-by-step-nav) + +## Example usage + +**Macro** +``` +{{ govukPubStepByStepNavigation({ + id: 'step-by-step-navigation', + steps: [{ + heading: { + text: 'Step 1' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, + { + text: 'Vivamus nec pharetra ipsum. Duis euismod augue nisl, sit amet dictum velit malesuada at.' + }], + active: true + }, + { + heading: { + text: 'Step 2' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }], + logic: 'and' + }, + { + heading: { + text: 'Step 3' + }, + contents: [{ + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }] + }] +}) }} +``` + +**JavaScript** +```html + + +``` + +## Accessibility acceptance criteria + +The step by step navigation must: + +- indicate to users that each step can be expanded and collapsed +- inform the user when a step has been expanded or collapsed +- be usable with a keyboard +- allow users to show or hide all steps at once +- inform the user which step a link belongs to +- inform the user which step the current page is in +- be readable when only the text of the page is zoomed - achieved for the numbers and logic elements by their text being wrapped in several elements that give them a white background and ensure the text when zoomed expands to the left, not to the right, where it would obscure the step titles + +The show/hide all button only needs to list the first panel id in the aria-controls attribute, rather than all of them. + +Step headings must use a button element: + +- so that steps can be toggled with the space and enter keys +- so that steps can’t be opened in a new tab or window + +When JavaScript is unavailable the component must: + +- be fully expanded +- not be marked as expandable + +Links in the component must: + +- accept focus +- be focusable with a keyboard +- be usable with a keyboard +- indicate when they have focus +- change in appearance when touched (in the touch-down state) +- change in appearance when hovered +- be usable with touch +- be usable with [voice commands](https://www.w3.org/WAI/perspectives/voice.html) +- have visible text + +## Arguments + +This component accepts the following arguments. + +|Name|Type|Required|Description| +|---|---|---|---| +|id|string|No|| +|steps|array|Yes|| +|small|boolean|No|| +|attributes|object|No|HTML attributes (for example data attributes) to add to the container.| + +### Steps + +|Name|Type|Required|Description| +|---|---|---|---| +|heading|object|Yes|| +|contents|array|Yes|| +|active|boolean|No|| +|logic|string|No|| + +#### Heading + +|Name|Type|Required|Description| +|---|---|---|---| +|text|string|Yes|If `html` is set, this is not required. Text to use within the heading. If `html` is provided, the `text` argument will be ignored.| +|html|string|Yes|If `text` is set, this is not required. HTML to use within the heading. If `html` is provided, the `text` argument will be ignored.| +|headingLevel|numeric|No|A number for the heading level. Defaults to 2 (`

    `)| + +#### Content + +|Name|Type|Required|Description| +|---|---|---|---| +|text|string|Yes|If `html` is set, this is not required. Text to use within the content. If `html` is provided, the `text` argument will be ignored.| +|html|string|Yes|If `text` is set, this is not required. HTML to use within the content. If `html` is provided, the `text` argument will be ignored.| + + +*Warning: If you’re using Nunjucks macros in production be aware that using HTML arguments, or ones ending with `.html` can be at risk from [cross-site scripting](https://en.wikipedia.org/wiki/Cross-site_scripting) attacks. More information about security vulnerabilities can be found in the [Nunjucks documentation](https://mozilla.github.io/nunjucks/api.html#user-defined-templates-warning).* diff --git a/src/govuk-pub/components/step-by-step-navigation/_step-by-step-navigation.scss b/src/govuk-pub/components/step-by-step-navigation/_step-by-step-navigation.scss new file mode 100644 index 0000000..439ecd0 --- /dev/null +++ b/src/govuk-pub/components/step-by-step-navigation/_step-by-step-navigation.scss @@ -0,0 +1,430 @@ +$stroke-width: 2px; +$stroke-width-large: 3px; +$number-circle-size: 26px; +$number-circle-size-large: 35px; +$top-border: solid 2px govuk-colour("mid-grey", $legacy: "grey-3"); + +@mixin step-nav-vertical-line ($line-style: solid) { + content: ""; + position: absolute; + z-index: 2; + width: 0; + height: 100%; + border-left: $line-style $stroke-width govuk-colour("mid-grey", $legacy: "grey-2"); + background: govuk-colour("white"); +} + +@mixin step-nav-line-position { + left: 0; + margin-left: ($number-circle-size / 2) - ($stroke-width / 2); +} + +@mixin step-nav-line-position-large { + left: 0; + margin-left: ($number-circle-size-large / 2) - ($stroke-width-large / 2); + border-width: $stroke-width-large; +} + +// custom mixin as govuk-font does undesirable things at different breakpoints +// we want to ensure that both large and small step navs have the same size font on mobile +// this will stop text resizing if compatibility mode is turned off +@mixin step-nav-font($size, $tablet-size: $size, $weight: normal, $line-height: 1.3, $tablet-line-height: $line-height) { + @include govuk-typography-common; + font-size: $size + px; + font-weight: $weight; + line-height: $line-height; + + @include govuk-media-query($from: tablet) { + font-size: $tablet-size + px; + line-height: $tablet-line-height; + } +} + +.govuk-pub-step-nav { + margin-bottom: govuk-spacing(6); + + &.govuk-pub-step-nav--large { + @include govuk-media-query($from: tablet) { + margin-bottom: govuk-spacing(9); + } + } + + .js-enabled &.js-hidden { + display: none; + } +} + +.govuk-pub-step-nav__controls { + padding: 3px 3px 0 0; + text-align: right; +} + +.govuk-pub-step-nav__button { + margin: 0; + border: 0; + color: $govuk-link-colour; + background: none; + cursor: pointer; + + &:focus { + @include govuk-focused-text; + } +} + +// removes extra dotted outline from buttons in Firefox +// on focus (standard yellow outline unaffected) +.govuk-pub-step-nav__button::-moz-focus-inner { + border: 0; +} + +.govuk-pub-step-nav__button--title { + @include step-nav-font(19, $weight: bold, $line-height: 1.4); + display: inline-block; + padding: 0; + color: govuk-colour("black"); + text-align: left; + + .govuk-pub-step-nav--large & { + @include step-nav-font(19, $tablet-size: 24, $weight: bold, $line-height: 1.4); + } +} + +.govuk-pub-step-nav__button--controls { + @include step-nav-font(14, $line-height: 1); + position: relative; + z-index: 1; // this and relative position stops focus outline underlap with border of accordion + padding: .5em 0; + text-decoration: underline; + + .govuk-pub-step-nav--large & { + @include step-nav-font(14, $tablet-size: 16, $line-height: 1); + } +} + +.govuk-pub-step-nav__steps { + margin: 0; + padding: 0; +} + +.govuk-pub-step-nav__step { + position: relative; + padding-left: govuk-spacing(6) + govuk-spacing(3); + list-style: none; + + // line down the side of a step + &:after { + @include step-nav-vertical-line; + @include step-nav-line-position; + top: govuk-spacing(3); + } + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + padding-left: govuk-spacing(9); + + &:after { + @include step-nav-line-position-large; + top: govuk-spacing(6); + } + } + } +} + +.govuk-pub-step-nav__step:last-child { + // little dash at the bottom of the line + &:before { + content: ""; + position: absolute; + z-index: 6; + bottom: 0; + left: 0; + width: $number-circle-size / 2; + height: 0; + margin-left: $number-circle-size / 4; + border-bottom: solid $stroke-width govuk-colour("mid-grey", $legacy: "grey-2"); + } + + &:after { + // scss-lint:disable DuplicateProperty + // sass-lint:disable no-duplicate-properties + height: -webkit-calc(100% - #{govuk-spacing(3)}); // fallback for iphone 4 + height: calc(100% - #{govuk-spacing(3)}); + // sass-lint:enable no-duplicate-properties + // scss-lint:enable DuplicateProperty + } + + .govuk-pub-step-nav__help:after { + height: 100%; + } + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + &:before { + width: $number-circle-size-large / 2; + margin-left: $number-circle-size-large / 4; + border-width: $stroke-width-large; + } + + &:after { + height: calc(100% - #{govuk-spacing(6)}); + } + } + } +} + +.govuk-pub-step-nav__step--active { + &:last-child:before, + .govuk-pub-step-nav__circle--number, + &:after, + .govuk-pub-step-nav__help:after { + border-color: govuk-colour("black"); + } +} + +.govuk-pub-step-nav__circle { + box-sizing: border-box; + position: absolute; + z-index: 5; + top: govuk-spacing(3); + left: 0; + width: $number-circle-size; + height: $number-circle-size; + border-radius: 100px; + color: govuk-colour("black"); + background: govuk-colour("white"); + text-align: center; + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + top: govuk-spacing(6); + width: $number-circle-size-large; + height: $number-circle-size-large; + } + } +} + +.govuk-pub-step-nav__circle--number { + @include step-nav-font(16, $weight: bold, $line-height: 23px); + border: solid $stroke-width govuk-colour("mid-grey", $legacy: "grey-2"); + + .govuk-pub-step-nav--large & { + @include step-nav-font(16, $tablet-size: 19, $weight: bold, $line-height: 23px, $tablet-line-height: 30px); + + @include govuk-media-query($from: tablet) { + border-width: $stroke-width-large; + } + } +} + +.govuk-pub-step-nav__circle--logic { + @include step-nav-font(16, $weight: bold, $line-height: 28px); + + .govuk-pub-step-nav--large & { + @include step-nav-font(16, $tablet-size: 19, $weight: bold, $line-height: 28px, $tablet-line-height: 34px); + } +} + +// makes sure logic text expands to the left if text size is zoomed, preventing overlap +.govuk-pub-step-nav__circle-inner { + min-width: 100%; + float: right; +} + +.govuk-pub-step-nav__circle-background { + $shadow-offset: .1em; + $shadow-colour: govuk-colour("white"); + + // to make numbers readable for users zooming text only in browsers such as Firefox + text-shadow: 0 -#{$shadow-offset} 0 $shadow-colour, $shadow-offset 0 0 $shadow-colour, 0 $shadow-offset 0 $shadow-colour, -#{$shadow-offset} 0 0 $shadow-colour; +} + +.govuk-pub-step-nav__circle-step-label, +.govuk-pub-step-nav__circle-step-colon { + @include govuk-visually-hidden; +} + +.govuk-pub-step-nav__header { + padding: govuk-spacing(3) 0; + border-top: $top-border; + + .govuk-pub-step-nav--active & { + cursor: pointer; + } + + .govuk-pub-step-nav__button { + &:focus { + @include govuk-focused-text; + + .govuk-pub-step-nav__toggle-link { + @include govuk-focused-text; + } + } + } + + &:hover { + .govuk-pub-step-nav__button, + .govuk-pub-step-nav__circle { + color: $govuk-link-colour; + } + + .govuk-pub-step-nav__toggle-link { + text-decoration: underline; + } + } + + &:focus { + .govuk-pub-step-nav__button { + color: $govuk-focus-text-colour; + } + } + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + padding: govuk-spacing(6) 0; + } + } +} + +.govuk-pub-step-nav__title { + @include govuk-text-colour; + @include step-nav-font(19, $weight: bold, $line-height: 1.4); + margin: 0; + + .govuk-pub-step-nav--large & { + @include step-nav-font(19, $tablet-size: 24, $weight: bold, $line-height: 1.4); + } +} + +.govuk-pub-step-nav__toggle-link { + @include step-nav-font(14, $line-height: 1.2); + display: block; + color: $govuk-link-colour; + text-transform: capitalize; + + .govuk-pub-step-nav--large & { + @include step-nav-font(14, $tablet-size: 16, $line-height: 1.2); + } +} + +.govuk-pub-step-nav__panel { + @include govuk-text-colour; + @include step-nav-font(16); + + .govuk-pub-step-nav--large & { + @include step-nav-font(16, $tablet-size: 19); + } + + .js-enabled &.js-hidden { + display: none; + } +} + +// contents of the steps, such as paragraphs and links + +.govuk-pub-step-nav__paragraph { + margin: 0; + padding-bottom: govuk-spacing(3); + font-size: inherit; + + + .govuk-pub-step-nav__list { + margin-top: -5px; + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + margin-top: -govuk-spacing(3); + } + } + } + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + padding-bottom: govuk-spacing(6); + } + } +} + +.govuk-pub-step-nav__list { + padding: 0; + padding-bottom: 10px; + list-style: none; + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + padding-bottom: 20px; + } + } +} + +.govuk-pub-step-nav__list--choice { + $links-margin: 20px; + + margin-left: $links-margin; + list-style: disc; + + .govuk-pub-step-nav__list-item--active:before { + left: -(govuk-spacing(6) + govuk-spacing(3)) - $links-margin; + } + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + .govuk-pub-step-nav__list-item--active:before { + left: -(govuk-spacing(9)) - $links-margin; + } + } + } +} + +.govuk-pub-step-nav__list-item { + margin-bottom: 10px; +} + +.govuk-pub-step-nav__link { + @include govuk-link-common; + @include govuk-link-style-default; +} + +.govuk-pub-step-nav__link-active-context { + @include govuk-visually-hidden; +} + +.govuk-pub-step-nav__list-item--active { + position: relative; + + &:before { + content: ""; + box-sizing: border-box; + position: absolute; + z-index: 5; + top: .6em; // position the dot to align with the first row of text in the link + left: -(govuk-spacing(6) + govuk-spacing(3)); + width: $number-circle-size / 2; + height: $stroke-width; + margin-top: -($stroke-width / 2); + margin-left: ($number-circle-size / 2); + background: govuk-colour("black"); + } + + .govuk-pub-step-nav--large & { + @include govuk-media-query($from: tablet) { + &:before { + left: -(govuk-spacing(9)); + height: $stroke-width-large; + margin-left: ($number-circle-size-large / 2); + } + } + } + + .govuk-pub-step-nav__link { + @include govuk-link-style-text; + } +} + +.govuk-pub-step-nav__context { + display: inline-block; + color: govuk-colour("dark-grey", $legacy: "grey-1"); + font-weight: normal; + + &:before { + content: " \2013\00a0"; // dash followed by   + } +} diff --git a/src/govuk-pub/components/step-by-step-navigation/macro.njk b/src/govuk-pub/components/step-by-step-navigation/macro.njk new file mode 100644 index 0000000..62eb26c --- /dev/null +++ b/src/govuk-pub/components/step-by-step-navigation/macro.njk @@ -0,0 +1,3 @@ +{% macro govukPubStepByStepNavigation(params) %} + {%- include "./template.njk" -%} +{% endmacro %} diff --git a/src/govuk-pub/components/step-by-step-navigation/step-by-step-navigation.js b/src/govuk-pub/components/step-by-step-navigation/step-by-step-navigation.js new file mode 100644 index 0000000..355f94e --- /dev/null +++ b/src/govuk-pub/components/step-by-step-navigation/step-by-step-navigation.js @@ -0,0 +1,432 @@ +/* eslint-env jquery */ + +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + 'use strict' + + Modules.StepByStepNavigation = function () { + var actions = {} // stores text for JS appended elements 'show' and 'hide' on steps, and 'show/hide all' button + var rememberShownStep = false + var stepNavSize + var sessionStoreLink = 'govuk-step-nav-active-link' + var activeLinkClass = 'govuk-pub-step-nav__list-item--active' + var activeStepClass = 'govuk-pub-step-nav__step--active' + var activeLinkHref = '#content' + var uniqueId + + this.start = function ($element) { + // Indicate that js has worked + $element.addClass('govuk-pub-step-nav--active') + + // Prevent FOUC, remove class hiding content + $element.removeClass('js-hidden') + + stepNavSize = $element.hasClass('govuk-pub-step-nav--large') ? 'Big' : 'Small' + rememberShownStep = !!$element.filter('[data-remember]').length && stepNavSize === 'Big' + var $steps = $element.find('.js-step') + var $stepHeaders = $element.find('.js-toggle-panel') + var totalSteps = $element.find('.js-panel').length + var totalLinks = $element.find('.govuk-pub-step-nav__link').length + var $showOrHideAllButton + + uniqueId = $element.data('id') || false + + if (uniqueId) { + sessionStoreLink = sessionStoreLink + '_' + uniqueId + } + + var stepNavTracker = new StepNavTracker(totalSteps, totalLinks, uniqueId) + + getTextForInsertedElements() + addButtonstoSteps() + addShowHideAllButton() + addShowHideToggle() + addAriaControlsAttrForShowHideAllButton() + + ensureOnlyOneActiveLink() + showPreviouslyOpenedSteps() + + bindToggleForSteps(stepNavTracker) + bindToggleShowHideAllButton(stepNavTracker) + bindComponentLinkClicks(stepNavTracker) + + function getTextForInsertedElements () { + actions.showText = $element.attr('data-show-text') + actions.hideText = $element.attr('data-hide-text') + actions.showAllText = $element.attr('data-show-all-text') + actions.hideAllText = $element.attr('data-hide-all-text') + } + + function addShowHideAllButton () { + $element.prepend('
    ') + } + + function addShowHideToggle () { + $stepHeaders.each(function () { + var linkText = actions.showText // eslint-disable-line no-unused-vars + + if (headerIsOpen($(this))) { + linkText = actions.hideText + } + + if (!$(this).find('.js-toggle-link').length) { + $(this).find('.js-step-title-button').append( + '' + ) + } + }) + } + + function headerIsOpen ($stepHeader) { + return (typeof $stepHeader.closest('.js-step').data('show') !== 'undefined') + } + + function addAriaControlsAttrForShowHideAllButton () { + var ariaControlsValue = $element.find('.js-panel').first().attr('id') + + $showOrHideAllButton = $element.find('.js-step-controls-button') + $showOrHideAllButton.attr('aria-controls', ariaControlsValue) + } + + // called by show all/hide all, sets all steps accordingly + function setAllStepsShownState (isShown) { + var data = [] + + $.each($steps, function () { + var stepView = new StepView($(this)) + stepView.setIsShown(isShown) + + if (isShown) { + data.push($(this).attr('id')) + } + }) + + if (isShown) { + saveToSessionStorage(uniqueId, JSON.stringify(data)) + } else { + removeFromSessionStorage(uniqueId) + } + } + + // called on load, determines whether each step should be open or closed + function showPreviouslyOpenedSteps () { + var data = loadFromSessionStorage(uniqueId) || [] + + $.each($steps, function () { + var id = $(this).attr('id') + var stepView = new StepView($(this)) + + // show the step if it has been remembered or if it has the 'data-show' attribute + if ((rememberShownStep && data.indexOf(id) > -1) || typeof $(this).attr('data-show') !== 'undefined') { + stepView.setIsShown(true) + } else { + stepView.setIsShown(false) + } + }) + + if (data.length > 0) { + $showOrHideAllButton.attr('aria-expanded', true) + setShowHideAllText() + } + } + + function addButtonstoSteps () { + $.each($steps, function () { + var $step = $(this) + var $title = $step.find('.js-step-title') + var contentId = $step.find('.js-panel').first().attr('id') + + $title.wrapInner( + '' + ) + + $title.wrapInner( + '' + ) + }) + } + + function bindToggleForSteps (stepNavTracker) { + $element.find('.js-toggle-panel').click(function (event) { + var $step = $(this).closest('.js-step') + + var stepView = new StepView($step) + stepView.toggle() + + var stepIsOptional = typeof $step.data('optional') !== 'undefined' + var toggleClick = new StepToggleClick(event, stepView, $steps, stepNavTracker, stepIsOptional) + toggleClick.track() + + setShowHideAllText() + rememberStepState($step) + }) + } + + // if the step is open, store its id in session store + // if the step is closed, remove its id from session store + function rememberStepState ($step) { + if (rememberShownStep) { + var data = JSON.parse(loadFromSessionStorage(uniqueId)) || [] + var thisstep = $step.attr('id') + var shown = $step.hasClass('step-is-shown') + + if (shown) { + data.push(thisstep) + } else { + var i = data.indexOf(thisstep) + if (i > -1) { + data.splice(i, 1) + } + } + saveToSessionStorage(uniqueId, JSON.stringify(data)) + } + } + + // tracking click events on links in step content + function bindComponentLinkClicks (stepNavTracker) { + $element.find('.js-link').click(function (event) { + var linkClick = new componentLinkClick(event, stepNavTracker, $(this).attr('data-position')) // eslint-disable-line no-new, new-cap + linkClick.track() + var thisLinkHref = $(this).attr('href') + + if ($(this).attr('rel') !== 'external') { + saveToSessionStorage(sessionStoreLink, $(this).attr('data-position')) + } + + if (thisLinkHref === activeLinkHref) { + setOnlyThisLinkActive($(this)) + setActiveStepClass() + } + }) + } + + function saveToSessionStorage (key, value) { + window.sessionStorage.setItem(key, value) + } + + function loadFromSessionStorage (key) { + return window.sessionStorage.getItem(key) + } + + function removeFromSessionStorage (key) { + window.sessionStorage.removeItem(key) + } + + function setOnlyThisLinkActive (clicked) { + $element.find('.' + activeLinkClass).removeClass(activeLinkClass) + clicked.parent().addClass(activeLinkClass) + } + + // if a link occurs more than once in a step nav, the backend doesn't know which one to highlight + // so it gives all those links the 'active' attribute and highlights the last step containing that link + // if the user clicked on one of those links previously, it will be in the session store + // this code ensures only that link and its corresponding step have the highlighting + // otherwise it accepts what the backend has already passed to the component + function ensureOnlyOneActiveLink () { + var $activeLinks = $element.find('.js-list-item.' + activeLinkClass) + + if ($activeLinks.length <= 1) { + return + } + + var lastClicked = loadFromSessionStorage(sessionStoreLink) || $element.find('.' + activeLinkClass).first().attr('data-position') + + // it's possible for the saved link position value to not match any of the currently duplicate highlighted links + // so check this otherwise it'll take the highlighting off all of them + if (!$element.find('.js-link[data-position="' + lastClicked + '"]').parent().hasClass(activeLinkClass)) { + lastClicked = $element.find('.' + activeLinkClass).first().find('.js-link').attr('data-position') + } + removeActiveStateFromAllButCurrent($activeLinks, lastClicked) + setActiveStepClass() + } + + function removeActiveStateFromAllButCurrent ($activeLinks, current) { + $activeLinks.each(function () { + if ($(this).find('.js-link').attr('data-position').toString() !== current.toString()) { + $(this).removeClass(activeLinkClass) + $(this).find('.visuallyhidden').remove() + } + }) + } + + function setActiveStepClass () { + $element.find('.' + activeStepClass).removeClass(activeStepClass).removeAttr('data-show') + $element.find('.' + activeLinkClass).closest('.govuk-pub-step-nav__step').addClass(activeStepClass).attr('data-show', '') + } + + function bindToggleShowHideAllButton (stepNavTracker) { + $showOrHideAllButton = $element.find('.js-step-controls-button') + $showOrHideAllButton.on('click', function () { + var shouldshowAll + + if ($showOrHideAllButton.text() === actions.showAllText) { + $showOrHideAllButton.text(actions.hideAllText) + $element.find('.js-toggle-link').html(actions.hideText) + shouldshowAll = true + + stepNavTracker.track('pageElementInteraction', 'stepNavAllShown', { + label: actions.showAllText + ': ' + stepNavSize + }) + } else { + $showOrHideAllButton.text(actions.showAllText) + $element.find('.js-toggle-link').html(actions.showText) + shouldshowAll = false + + stepNavTracker.track('pageElementInteraction', 'stepNavAllHidden', { + label: actions.hideAllText + ': ' + stepNavSize + }) + } + + setAllStepsShownState(shouldshowAll) + $showOrHideAllButton.attr('aria-expanded', shouldshowAll) + setShowHideAllText() + + return false + }) + } + + function setShowHideAllText () { + var shownSteps = $element.find('.step-is-shown').length + // Find out if the number of is-opens == total number of steps + if (shownSteps === totalSteps) { + $showOrHideAllButton.text(actions.hideAllText) + } else { + $showOrHideAllButton.text(actions.showAllText) + } + } + } + + function StepView ($stepElement) { + var $titleLink = $stepElement.find('.js-step-title-button') + var $stepContent = $stepElement.find('.js-panel') + + this.title = $stepElement.find('.js-step-title-text').text().trim() + this.element = $stepElement + + this.show = show + this.hide = hide + this.toggle = toggle + this.setIsShown = setIsShown + this.isShown = isShown + this.isHidden = isHidden + this.numberOfContentItems = numberOfContentItems + + function show () { + setIsShown(true) + } + + function hide () { + setIsShown(false) + } + + function toggle () { + setIsShown(isHidden()) + } + + function setIsShown (isShown) { + $stepElement.toggleClass('step-is-shown', isShown) + $stepContent.toggleClass('js-hidden', !isShown) + $titleLink.attr('aria-expanded', isShown) + $stepElement.find('.js-toggle-link').html(isShown ? actions.hideText : actions.showText) + } + + function isShown () { + return $stepElement.hasClass('step-is-shown') + } + + function isHidden () { + return !isShown() + } + + function numberOfContentItems () { + return $stepContent.find('.js-link').length + } + } + + function StepToggleClick (event, stepView, $steps, stepNavTracker, stepIsOptional) { + this.track = trackClick + var $target = $(event.target) + + function trackClick () { + var trackingOptions = { label: trackingLabel(), dimension28: stepView.numberOfContentItems().toString() } + stepNavTracker.track('pageElementInteraction', trackingAction(), trackingOptions) + } + + function trackingLabel () { + return $target.closest('.js-toggle-panel').attr('data-position') + ' - ' + stepView.title + ' - ' + locateClickElement() + ': ' + stepNavSize + isOptional() + } + + // returns index of the clicked step in the overall number of steps + function stepIndex () { // eslint-disable-line no-unused-vars + return $steps.index(stepView.element) + 1 + } + + function trackingAction () { + return (stepView.isHidden() ? 'stepNavHidden' : 'stepNavShown') + } + + function locateClickElement () { + if (clickedOnIcon()) { + return iconType() + ' click' + } else if (clickedOnHeading()) { + return 'Heading click' + } else { + return 'Elsewhere click' + } + } + + function clickedOnIcon () { + return $target.hasClass('js-toggle-link') + } + + function clickedOnHeading () { + return $target.hasClass('js-step-title-text') + } + + function iconType () { + return (stepView.isHidden() ? 'Minus' : 'Plus') + } + + function isOptional () { + return (stepIsOptional ? ' ; optional' : '') + } + } + + function componentLinkClick (event, stepNavTracker, linkPosition) { + this.track = trackClick + + function trackClick () { + var trackingOptions = { label: $(event.target).attr('href') + ' : ' + stepNavSize } + var dimension28 = $(event.target).closest('.govuk-pub-step-nav__list').attr('data-length') + + if (dimension28) { + trackingOptions['dimension28'] = dimension28 + } + + stepNavTracker.track('stepNavLinkClicked', linkPosition, trackingOptions) + } + } + + // A helper that sends a custom event request to Google Analytics if + // the GOVUK module is setup + function StepNavTracker (totalSteps, totalLinks, uniqueId) { + this.track = function (category, action, options) { + // dimension26 records the total number of expand/collapse steps in this step nav + // dimension27 records the total number of links in this step nav + // dimension28 records the number of links in the step that was shown/hidden (handled in click event) + if (window.GOVUK.analytics && window.GOVUK.analytics.trackEvent) { + options = options || {} + options['dimension26'] = options['dimension26'] || totalSteps.toString() + options['dimension27'] = options['dimension27'] || totalLinks.toString() + options['dimension96'] = options['dimension96'] || uniqueId + window.GOVUK.analytics.trackEvent(category, action, options) + } + } + } + } +})(window.GOVUK.Modules) diff --git a/src/govuk-pub/components/step-by-step-navigation/template.njk b/src/govuk-pub/components/step-by-step-navigation/template.njk new file mode 100644 index 0000000..5b85689 --- /dev/null +++ b/src/govuk-pub/components/step-by-step-navigation/template.njk @@ -0,0 +1,41 @@ +{%- if params.steps %} +
    +
      + {%- set position = 1 -%} + {%- for step in params.steps %} +
    1. +
      + + + + + {%- if step.logic %} + {{- step.logic -}} + {% else %} + Step {{ position }} + {% endif -%} + + + + + {{- step.heading.html | safe if step.heading.html else step.heading.text -}} + + +
      +
      + {%- for content in step.contents %} + {%- if content.html %} + {{- content.html | safe if content.html -}} + {% else %} +

      {{- content.text | safe if content.text -}}

      + {% endif -%} + {% endfor -%} +
      +
    2. + {%- if not step.logic %} + {%- set position = position + 1 -%} + {% endif -%} + {% endfor -%} +
    +
    +{% endif -%}