diff --git a/app/assets/images/icon-plus-minus-small.png b/app/assets/images/icon-plus-minus-small.png new file mode 100644 index 000000000..aa3c067bf Binary files /dev/null and b/app/assets/images/icon-plus-minus-small.png differ diff --git a/app/assets/images/icon-plus-minus.png b/app/assets/images/icon-plus-minus.png new file mode 100644 index 000000000..c08c39fb6 Binary files /dev/null and b/app/assets/images/icon-plus-minus.png differ diff --git a/app/assets/images/icon-plus-minus.svg b/app/assets/images/icon-plus-minus.svg new file mode 100644 index 000000000..579aad27a --- /dev/null +++ b/app/assets/images/icon-plus-minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/icon-plus-minus_orig.png b/app/assets/images/icon-plus-minus_orig.png new file mode 100644 index 000000000..40abf0662 Binary files /dev/null and b/app/assets/images/icon-plus-minus_orig.png differ diff --git a/app/assets/javascripts/current-location.js b/app/assets/javascripts/current-location.js new file mode 100644 index 000000000..f1b047e25 --- /dev/null +++ b/app/assets/javascripts/current-location.js @@ -0,0 +1,10 @@ +// used by the tasklist component + +(function(root) { + "use strict"; + window.GOVUK = window.GOVUK || {}; + + GOVUK.getCurrentLocation = function(){ + return root.location; + }; +}(window)); diff --git a/app/assets/javascripts/govuk-component/tasklist.js b/app/assets/javascripts/govuk-component/tasklist.js new file mode 100644 index 000000000..5fc8cd07d --- /dev/null +++ b/app/assets/javascripts/govuk-component/tasklist.js @@ -0,0 +1,333 @@ +// Most of this is originally from the service manual but has changed considerably since then + +(function (Modules) { + "use strict"; + window.GOVUK = window.GOVUK || {}; + + Modules.Tasklist = function () { + + var bulkActions = { + openAll: { + buttonText: "Open all", + eventLabel: "Open All" + }, + closeAll: { + buttonText: "Close all", + eventLabel: "Close All" + } + }; + + var rememberOpenSection = false; + + this.start = function ($element) { + + $(window).unload(storeScrollPosition); + + // Indicate that js has worked + $element.addClass('pub-c-task-list--active'); + + // Prevent FOUC, remove class hiding content + $element.removeClass('js-hidden'); + + rememberOpenSection = !!$element.filter('[data-remember]').length; + var $sections = $element.find('.js-section'); + var $sectionHeaders = $element.find('.js-toggle-panel'); + var totalSections = $element.find('.js-panel').length; + + var $openOrCloseAllButton; + + var tasklistTracker = new TasklistTracker(totalSections); + + addButtonstoSections(); + addOpenCloseAllButton(); + addIconsToSections(); + addAriaControlsAttrForOpenCloseAllButton(); + + closeAllSections(); + openLinkedSection(); + + bindToggleForSections(tasklistTracker); + bindToggleOpenCloseAllButton(tasklistTracker); + + // When navigating back in browser history to the tasklist, the browser will try to be "clever" and return + // the user to their previous scroll position. However, since we collapse all but the currently-anchored + // section, the content length changes and the user is returned to the wrong position (often the footer). + // In order to correct this behaviour, as the user leaves the page, we anticipate the correct height we wish the + // user to return to by forcibly scrolling them to that height, which becomes the height the browser will return + // them to. + // If we can't find an element to return them to, then reset the scroll to the top of the page. This handles + // the case where the user has expanded all sections, so they are not returned to a particular section, but + // still could have scrolled a long way down the page. + function storeScrollPosition() { + closeAllSections(); + var $section = getSectionForAnchor(); + + document.body.scrollTop = $section && $section.length + ? $section.offset().top + : 0; + } + + function addOpenCloseAllButton() { + $element.prepend('
'); + } + + function addIconsToSections() { + $sectionHeaders.append(''); + $sectionHeaders.append(''); + } + + function addAriaControlsAttrForOpenCloseAllButton() { + var ariaControlsValue = ""; + var $sectionPanels = $element.find('.js-panel') + for (var i = 0; i < totalSections; i++) { + ariaControlsValue += $sectionPanels[i].id + " " + } + + $openOrCloseAllButton = $element.find('.js-section-controls-button'); + $openOrCloseAllButton.attr('aria-controls', ariaControlsValue); + } + + function closeAllSections() { + setAllSectionsOpenState(false); + } + + function setAllSectionsOpenState(isOpen) { + $.each($sections, function () { + var sectionView = new SectionView($(this)); + sectionView.preventHashUpdate(); + sectionView.setIsOpen(isOpen); + }); + } + + function openLinkedSection() { + var $section; + if (rememberOpenSection) { + $section = getSectionForAnchor(); + } + else { + $section = $sections.filter('[data-open]'); + } + + if ($section && $section.length) { + var sectionView = new SectionView($section); + sectionView.open(); + } + } + + function getSectionForAnchor() { + var anchor = getActiveAnchor(); + + return anchor.length + ? $element.find('#' + escapeSelector(anchor.substr(1))) + : null; + } + + function getActiveAnchor() { + return GOVUK.getCurrentLocation().hash; + } + + function addButtonstoSections() { + $.each($sections, function () { + var $section = $(this); + var $title = $section.find('.js-section-title'); + var contentId = $section.find('.js-panel').first().attr('id'); + + $title.wrapInner( + '' ); + }); + } + + function bindToggleForSections(tasklistTracker) { + $element.find('.js-toggle-panel').click(function (event) { + preventLinkFollowingForCurrentTab(event); + + var sectionView = new SectionView($(this).closest('.js-section')); + sectionView.toggle(); + + var toggleClick = new SectionToggleClick(sectionView, $sections, tasklistTracker); + toggleClick.track(); + + setOpenCloseAllText(); + }); + } + + function preventLinkFollowingForCurrentTab(event) { + // If the user is holding the ⌘ or Ctrl key, they're trying + // to open the link in a new window, so let the click happen + if (event.metaKey || event.ctrlKey) { + return; + } + + event.preventDefault(); + } + + function bindToggleOpenCloseAllButton(tasklistTracker) { + $openOrCloseAllButton = $element.find('.js-section-controls-button'); + $openOrCloseAllButton.on('click', function () { + var shouldOpenAll; + + if ($openOrCloseAllButton.text() == bulkActions.openAll.buttonText) { + $openOrCloseAllButton.text(bulkActions.closeAll.buttonText); + shouldOpenAll = true; + + tasklistTracker.track('pageElementInteraction', 'tasklistAllOpened', { + label: bulkActions.openAll.eventLabel + }); + } else { + $openOrCloseAllButton.text(bulkActions.openAll.buttonText); + shouldOpenAll = false; + + tasklistTracker.track('pageElementInteraction', 'tasklistAllClosed', { + label: bulkActions.closeAll.eventLabel + }); + } + + setAllSectionsOpenState(shouldOpenAll); + $openOrCloseAllButton.attr('aria-expanded', shouldOpenAll); + setOpenCloseAllText(); + setHash(null); + + return false; + }); + } + + function setOpenCloseAllText() { + var openSections = $element.find('.section-is-open').length; + // Find out if the number of is-opens == total number of sections + if (openSections === totalSections) { + $openOrCloseAllButton.text(bulkActions.closeAll.buttonText); + } else { + $openOrCloseAllButton.text(bulkActions.openAll.buttonText); + } + } + + // Ideally we'd use jQuery.escapeSelector, but this is only available from v3 + // See https://github.com/jquery/jquery/blob/2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/selector-native.js#L46 + function escapeSelector(s) { + var cssMatcher = /([\x00-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + return s.replace(cssMatcher, "\\$&"); + } + }; + + function SectionView($sectionElement) { + var $titleLink = $sectionElement.find('.js-section-title-button'); + var $sectionContent = $sectionElement.find('.js-panel'); + var shouldUpdateHash = rememberOpenSection; + + this.title = $sectionElement.find('.js-section-title').text(); + this.href = $titleLink.attr('href'); + this.element = $sectionElement; + + this.open = open; + this.close = close; + this.toggle = toggle; + this.setIsOpen = setIsOpen; + this.isOpen = isOpen; + this.isClosed = isClosed; + this.preventHashUpdate = preventHashUpdate; + this.numberOfContentItems = numberOfContentItems; + + function open() { + setIsOpen(true); + } + + function close() { + setIsOpen(false); + } + + function toggle() { + setIsOpen(isClosed()); + } + + function setIsOpen(isOpen) { + $sectionElement.toggleClass('section-is-open', isOpen); + $sectionContent.toggleClass('js-hidden', !isOpen); + $titleLink.attr("aria-expanded", isOpen); + + if (shouldUpdateHash) { + updateHash($sectionElement); + } + } + + function isOpen() { + return $sectionElement.hasClass('section-is-open'); + } + + function isClosed() { + return !isOpen(); + } + + function preventHashUpdate() { + shouldUpdateHash = false; + } + + function numberOfContentItems() { + return $sectionContent.find('li').length; + } + } + + function updateHash($sectionElement) { + var sectionView = new SectionView($sectionElement); + var hash = sectionView.isOpen() && '#' + $sectionElement.attr('id'); + setHash(hash) + } + + // Sets the hash for the page. If a falsy value is provided, the hash is cleared. + function setHash(hash) { + if (!GOVUK.support.history()) { + return; + } + + var newLocation = hash || GOVUK.getCurrentLocation().pathname; + history.replaceState({}, '', newLocation); + } + + function SectionToggleClick(sectionView, $sections, tasklistTracker) { + this.track = trackClick; + + function trackClick() { + var tracking_options = {label: trackingLabel(), dimension28: sectionView.numberOfContentItems().toString()} + tasklistTracker.track('pageElementInteraction', trackingAction(), tracking_options); + + if (!sectionView.isClosed()) { + tasklistTracker.track( + 'navtasklistLinkClicked', + String(sectionIndex()), + { + label: sectionView.href, + dimension28: String(sectionView.numberOfContentItems()), + dimension29: sectionView.title + } + ) + } + } + + function trackingLabel() { + return sectionIndex() + '. ' + sectionView.title; + } + + function sectionIndex() { + return $sections.index(sectionView.element) + 1; + } + + function trackingAction() { + return (sectionView.isClosed() ? 'tasklistClosed' : 'tasklistOpened'); + } + } + + // A helper that sends a custom event request to Google Analytics if + // the GOVUK module is setup + function TasklistTracker(totalSections) { + this.track = function(category, action, options) { + if (GOVUK.analytics && GOVUK.analytics.trackEvent) { + options = options || {}; + options["dimension28"] = options["dimension28"] || totalSections.toString(); + GOVUK.analytics.trackEvent(category, action, options); + } + } + } + }; +})(window.GOVUK.Modules); diff --git a/app/assets/javascripts/header-footer-only.js b/app/assets/javascripts/header-footer-only.js index 4ad585f03..354e3863d 100644 --- a/app/assets/javascripts/header-footer-only.js +++ b/app/assets/javascripts/header-footer-only.js @@ -11,3 +11,5 @@ //= require govuk-component/govspeak-convert-html-pub-charts //= require govuk-component/option-select //= require govuk/shim-links-with-button-role +//= require history-support +//= require current-location diff --git a/app/assets/javascripts/history-support.js b/app/assets/javascripts/history-support.js new file mode 100644 index 000000000..b850fbb59 --- /dev/null +++ b/app/assets/javascripts/history-support.js @@ -0,0 +1,8 @@ +// used by the tasklist component + +if(typeof window.GOVUK === 'undefined'){ window.GOVUK = {}; } +if(typeof window.GOVUK.support === 'undefined'){ window.GOVUK.support = {}; } + +window.GOVUK.support.history = function() { + return window.history && window.history.pushState && window.history.replaceState; +} diff --git a/app/assets/javascripts/start-modules.js b/app/assets/javascripts/start-modules.js index b096b4c4c..f8f484356 100644 --- a/app/assets/javascripts/start-modules.js +++ b/app/assets/javascripts/start-modules.js @@ -3,6 +3,7 @@ // = require modules/toggle // = require modules/toggle-input-class-on-focus // = require modules/track-click +// = require govuk-component/tasklist $(document).ready(function () { GOVUK.modules.start() diff --git a/app/assets/stylesheets/govuk-component/_component-print.scss b/app/assets/stylesheets/govuk-component/_component-print.scss index e3c920c11..bd52ce720 100644 --- a/app/assets/stylesheets/govuk-component/_component-print.scss +++ b/app/assets/stylesheets/govuk-component/_component-print.scss @@ -6,3 +6,4 @@ @import "metadata-print"; @import "related-items-print"; @import "title-print"; +@import "task-list-print"; diff --git a/app/assets/stylesheets/govuk-component/_component.scss b/app/assets/stylesheets/govuk-component/_component.scss index 8b22e534d..9d8cc5a9a 100644 --- a/app/assets/stylesheets/govuk-component/_component.scss +++ b/app/assets/stylesheets/govuk-component/_component.scss @@ -25,3 +25,4 @@ @import "search"; @import "button"; @import "lead-paragraph"; +@import "task-list"; diff --git a/app/assets/stylesheets/govuk-component/_task-list-print.scss b/app/assets/stylesheets/govuk-component/_task-list-print.scss new file mode 100644 index 000000000..03ea42cfe --- /dev/null +++ b/app/assets/stylesheets/govuk-component/_task-list-print.scss @@ -0,0 +1,20 @@ +// scss-lint:disable SelectorFormat + +.pub-c-tasklist__controls, +.pub-c-tasklist__number { + display: none; +} + +.pub-c-tasklist__button--title { + @include bold-19; + + padding: 0; + border: 0; + background: none; +} + +.pub-c-tasklist__panel-link--active { + font-weight: bold; +} + +// scss-lint:enable SelectorFormat diff --git a/app/assets/stylesheets/govuk-component/_task-list.scss b/app/assets/stylesheets/govuk-component/_task-list.scss new file mode 100644 index 000000000..f7e954346 --- /dev/null +++ b/app/assets/stylesheets/govuk-component/_task-list.scss @@ -0,0 +1,380 @@ +.pub-c-task-list { + position: relative; + margin-top: $gutter-half; + margin-left: $gutter-half; + margin-bottom: $gutter; +} + +// scss-lint:disable SelectorFormat + +// when js is enabled +.pub-c-task-list--active { + margin-top: 0; +} + +.pub-c-task-list__list { + padding: 0; +} + +$number-circle-size: 1.5em; +$line-width: 3px; + +$first-indent: 28px; +$second-indent: 26px; +$first-indent-small: 15px; +$second-indent-small: 18px; + +// put all the indentation stuff here so it's easier to manage +// desktop version on mobile should be same size as small version +// large class only applies to tablet and above +// component defaults to having the large class +// active state requires more indent to make space for the plus/minus + +.pub-c-task-list__header { + padding-left: $first-indent-small; + + .pub-c-task-list--large & { + @include media(tablet) { + padding-left: $first-indent; + } + } +} + +.pub-c-task-list__title { + .pub-c-task-list--active & { + padding-left: $second-indent-small; + } + + .pub-c-task-list--active.pub-c-task-list--large & { + @include media(tablet) { + padding-left: $second-indent; + } + } +} + +.pub-c-task-list__panel-content, +.pub-c-task-list__panel-link { + padding-left: $first-indent-small; + + .pub-c-task-list--active & { + padding-left: $first-indent-small + $second-indent-small; + } + + .pub-c-task-list--large & { + @include media(tablet) { + padding-left: $first-indent; + } + } + + .pub-c-task-list--active.pub-c-task-list--large & { + @include media(tablet) { + padding-left: $first-indent + $second-indent; + } + } +} + +.pub-c-task-list__number { + @include bold-19; + @include box-sizing(border-box); + + position: absolute; + z-index: 2; + top: 0; + left: 0; + width: $number-circle-size; + height: $number-circle-size; + margin-top: -($number-circle-size / 2); + margin-left: -($number-circle-size / 2); + color: $black; + background: $white; + border: solid 3px $grey-2; + border-radius: 100px; + text-align: center; + + .pub-c-task-list--large & { + @include media(tablet) { + @include bold-24($number-circle-size * 0.9); + } + } +} + +.pub-c-task-list__step { + position: relative; + list-style: none; + border-top: 1px solid $grey-2; + + // tasklist vertical line + &:before { + @include core-19; + + content: ""; + position: absolute; + z-index: 1; + width: $line-width; + height: 100%; + left: 0; + top: 0; + margin-left: -($line-width / 2); + background: $grey-2; + } + + // 'dash' at end of tasklist vertical line + &:last-child { + &:after { + @include core-19; + + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: $number-circle-size / 2; + height: $line-width; + margin-left: -($number-circle-size / 4); + background: $grey-2; + } + } + + .pub-c-task-list--large & { + @include media(tablet) { + &:before { + @include core-24; + } + + &:last-child:after { + @include core-24; + + width: $number-circle-size / 2.5; + margin-left: -($number-circle-size / 5); + } + } + } +} + +.pub-c-task-list__step--active { + .pub-c-task-list__number { + border-color: $black; + } + + &:before, + &:last-child:after { + background: $black; + } +} + +.pub-c-task-list__section { + &:after { + content: ""; + display: block; + margin-left: $first-indent-small; + border-bottom: 1px solid $border-colour; + } + + &:last-child:after { + border-bottom: 0; + } + + .pub-c-task-list--large & { + &:after { + margin-left: $first-indent; + } + } +} + +.pub-c-task-list__header { + position: relative; + padding-top: 0.9em; + padding-bottom: 0.9em; + + .pub-c-task-list--active & { + cursor: pointer; + + &:hover { + background: $grey-3; + } + } +} + +// having two icons makes overriding the styles for the small tasklist simpler + +$icon-size: 10px; +$icon-size-large: 12px; + +.pub-c-task-list__icon { + position: absolute; + top: 50%; + left: $first-indent-small; + height: $icon-size; + width: $icon-size; + margin-top: -($icon-size / 2) - 2; + background-image: image-url('icon-plus-minus-small.png'); // PNG fallback for SVG + background: image-url("icon-plus-minus.svg"), linear-gradient(transparent, transparent); // http://pauginer.com/post/36614680636/invisible-gradient-technique + background-repeat: no-repeat; + + .pub-c-task-list--large & { + @include media(tablet) { + left: $first-indent; + height: $icon-size-large; + width: $icon-size-large; + margin-top: -($icon-size-large / 2) - 2; + // use a different PNG for IE8, which doesn't support background size + background-image: image-url('icon-plus-minus.png'); // PNG fallback for SVG + background: image-url("icon-plus-minus.svg"), linear-gradient(transparent, transparent); // http://pauginer.com/post/36614680636/invisible-gradient-technique + } + } +} + +.pub-c-task-list__icon--plus { + background-position: 0 -#{$icon-size}; + background-size: $icon-size; + + .section-is-open & { + display: none; + } + + .pub-c-task-list--large & { + @include media(tablet) { + background-position: 0 -#{$icon-size-large}; + background-size: $icon-size-large; + } + } +} + +.pub-c-task-list__icon--minus { + display: none; + background-position: 0 0; + background-size: $icon-size; + height: $icon-size - 1; // -1 is to avoid an irregular bug in Chrome where the plus part of the graphic is visible at the bottom of the element + + .section-is-open & { + display: block; + } + + .pub-c-task-list--large & { + @include media(tablet) { + background-size: $icon-size-large; + height: $icon-size-large - 1; + } + } +} + +.pub-c-task-list__title { + @include bold-19; + + .pub-c-task-list--large & { + @include media(tablet) { + @include bold-24; + } + } +} + +.pub-c-task-list__panel { + @include core-19; + + padding-bottom: $gutter-half; + + @include media(tablet) { + @include core-16; + } + + .pub-c-task-list--large & { + @include core-19; + } + + .section-is-open & { + @include media(tablet) { + padding-top: 5px; + padding-bottom: $gutter; + } + } +} + +.pub-c-task-list__button { + color: $link-colour; + cursor: pointer; + background: none; + border: 0; + + // removes extra dotted outline from buttons in Firefox + // on focus (standard yellow outline unaffected) + &::-moz-focus-inner { + border: 0; + } + + &--title { + @include bold-19; + + display: inline-block; + padding: 0; + text-align: left; + + .pub-c-task-list--large & { + @include media(tablet) { + @include bold-24; + } + } + } + + &--controls { + @include core-16; + + float: right; + position: relative; + z-index: 1; // this and relative position stops focus outline underlap with border of accordion + padding: 0.5em 0; + } +} + +.pub-c-task-list__controls { + @extend %contain-floats; +} + +.pub-c-task-list__panel-description { + padding: 0; + margin: 0; + margin-bottom: 1em; + font-size: inherit; //task list is used in a lot of apps and sometimes the default p size breaks it +} + +.pub-c-task-list__panel-links { + padding: 0; +} + +.pub-c-task-list__panel-link { + position: relative; + padding: 0 0 0.5em; +} + +.pub-c-task-list__panel-link-item { + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + +$active-dot-size: 16px; + +.pub-c-task-list__panel-link--active { + &:before { + @include box-sizing(border-box); + + content: ""; + position: absolute; + z-index: 3; + top: 50%; + left: 0; + width: $active-dot-size; + height: $active-dot-size; + margin-left: -($active-dot-size / 2); + margin-top: -($active-dot-size / 2); + background: $black; + border-radius: 100px; + border: solid 2px $white; + } + + .pub-c-task-list__panel-link-item--active { + color: $black; + } +} + +// scss-lint:enable SelectorFormat diff --git a/app/views/govuk_component/docs/task_list.yml b/app/views/govuk_component/docs/task_list.yml new file mode 100644 index 000000000..909e55983 --- /dev/null +++ b/app/views/govuk_component/docs/task_list.yml @@ -0,0 +1,354 @@ +name: Task list +description: Numbered steps containing expanding/collapsing sections +body: | + Task lists are designed to show a sequence of steps towards a specific goal, such as 'learning to drive'. Each step of a task list can contain one or more sections. Each section can contain one or more links to pages. User research suggested that each section should be collapsed by default so that users are not overwhelmed with information. + + If JavaScript is disabled the task list expands fully. All of the task list functionality (including the icons and aria attributes) are added using JavaScript. + + The task list is based on the accordion component in collections, but has been altered. This is for two reasons. + + - task lists are needed throughout GOV.UK and extending the accordion to be the task list would require promoting it to static, which would break integration testing + - creating a new component allows further iteration without impacting the accordion + + Changes in the task list code that are different from the accordion: + + - functionality to 'remember' the last opened section in the URL is disabled by default + - a section can be open by default + - visual changes (see below) including plus/minus control being on the left, not the right +accessibility_criteria: | + The task list must: + + - indicate to users that each section can be expanded and collapsed + - inform the user when a section has been expanded or collapsed + - be usable with a keyboard + - allow users to open or close all sections at once + - inform the user which step a link belongs to + - inform the user which step the current page is in, particularly where there are multiple sections in one step + + Section headings must use a button element: + + - so that sections can be toggled with the space and enter keys + - so that sections can't be opened in a new tab or window + + When JavaScript is unavailable the task list must: + + - be fully expanded + - not be marked as expandable +shared_accessibility_criteria: + - link +examples: + default: + data: + steps: [ + [ + { + title: 'First header', + panel: 'First panel' + } + ], + [ + { + title: 'Second header', + panel: 'Second panel' + } + ], + [ + { + title: 'Third header lets see how long we can make this and what that does to the layout of this component', + panel: 'Third panel also containing some deliberately long content to show what happens if long content is included and the response the component has to it.' + } + ] + ] + with_custom_section_ids: + description: IDs for sections are automatically generated based on the section title, but can also be passed manually. Passed IDs are converted to lowercase and have spaces replaced with hyphens. + data: + steps: [ + [ + { + title: 'First header', + id: 'something1', + panel: 'First panel' + } + ], + [ + { + title: 'Second header', + id: 'something2', + panel: 'Second panel' + } + ] + ] + with_different_heading_level: + description: Sections have a H2 by default, but this can be changed. The heading level does not change any styling. + data: + heading_level: 3 + steps: [ + [ + { + title: 'This is a heading 3', + panel: 'Some content
' + } + ], + [ + { + title: 'This is also a heading 3', + panel: 'Some more content' + } + ] + ] + open_a_section_by_default: + description: Pass the index of the accordion to open by default. This is the nth accordion section, regardless of step number. + data: + open_section: 3 + steps: [ + [ + { + title: 'Closed by default', + panel: 'Well, open now, obviously.' + } + ], + [ + { + title: 'Also closed by default', + panel: 'Hello.' + }, + { + title: 'Open by default', + panel: 'Hello!' + } + ] + ] + remember_last_opened_section: + description: If a section is opened its ID is appended to the end of the URL, so that if the URL is shared or the 'back' button is used, that section will be opened on page load. This was historically part of the behaviour of the accordion. By default this is turned off and cannot be used together with the option to open a section by default. + data: + remember_last_section: true + steps: [ + [ + { + title: 'Remember this', + panel: 'Panel content' + } + ], + [ + { + title: 'Or this', + panel: 'More panel content' + } + ] + ] + with_multiple_sections_in_one_step: + description: Some task lists contain non-sequential steps that can be combined into a single step, but kept as separate accordion sections. + data: + steps: [ + [ + { + title: 'First step first section header', + panel: 'First step first section panel' + } + ], + [ + { + title: 'Second step first section header', + panel: 'Second step first section panel' + }, + { + title: 'Second step second section header', + panel: 'Second step second section panel' + } + ], + [ + { + title: 'Third step first section header', + panel: 'Third step first section panel' + }, + { + title: 'Third step second section header', + panel: 'Third step second section panel' + }, + { + title: 'Third step third section header', + panel: 'Third step third section panel' + } + ] + ] + with_panel_descriptions: + description: Panels can have paragraphs added into them that are styled by the component. + data: + steps: [ + [ + { + title: 'Panel descriptions 1', + panel_descriptions: [ + 'This is a panel description.', + 'This is also a panel description. It has been filled with words such as CONTENT and ACCORDION in order to fill the horizontal space and wrap onto a second line.' + ] + } + ], + [ + { + title: 'Panel descriptions 2', + panel: 'You can also insert non-component specific content as before.', + panel_descriptions: [ + 'This is the panel description.' + ] + } + ] + ] + with_styled_lists: + description: Panels can contain lists of links that are styled by the component. A link can be marked as 'active', which will visually highlight the step and the link itself. Appropriate aria attributes are added and the active link will be rendered as a link to the top of the page content (this may not work in the component guide). + data: + steps: [ + [ + { + title: 'Panel links', + panel_links: [ + { + href: '/notalink', + text: 'First item' + }, + { + href: '/notalink', + text: 'Second item', + active: true + }, + { + href: '/notalink', + text: 'Third item' + } + ] + } + ], + [ + { + title: 'More panel links', + panel_links: [ + { + href: '/notalink', + text: 'First item' + }, + { + href: '/notalink', + text: 'Second item', + }, + { + href: '/notalink', + text: 'Third item' + } + ] + } + ] + ] + with_styled_lists_in_steps_with_multiple_sections: + description: If a link in a section is marked as active, the entire step is highlighted. + data: + steps: [ + [ + { + title: 'Panel links 1', + panel: 'You can also insert non-component specific content as before.', + panel_descriptions: [ + 'You can also insert panel descriptions as before.' + ] + } + ], + [ + { + title: 'Panel links 2', + panel_descriptions: [ + 'This is a panel description' + ], + panel_links: [ + { + href: '/notalink', + text: 'First item' + }, + { + href: '/notalink', + text: 'Second item' + }, + { + href: '/notalink', + text: 'Third item', + active: true + }, + { + href: '/notalink', + text: 'Fourth item' + } + ] + }, + { + title: 'Panel links 3', + panel: 'content' + } + ] + ] + small: + description: Designed to fit in a smaller space, specifically the sidebar of a page. + data: + small: true + steps: [ + [ + { + title: 'Small header 1', + panel: 'First panel' + } + ], + [ + { + title: 'Small header 2', + panel: 'Second panel' + }, + { + title: 'Small header 3', + panel: 'Third panel' + } + ], + [ + { + title: 'Small header 4', + panel: 'Fourth panel' + } + ] + ] + small_with_styled_lists: + description: The intended use of 'active' task list items is in small task lists. + data: + small: true + steps: [ + [ + { + title: 'Small panel links 1', + panel: 'You can also insert non-component specific content as before.', + panel_descriptions: [ + 'You can also insert panel descriptions as before.' + ] + } + ], + [ + { + title: 'Small panel links 2', + panel_descriptions: [ + 'This is a panel description.', + 'This is also a panel description. It has been filled with words such as CONTENT and ACCORDION in order to fill the horizontal space and wrap onto a second line.' + ], + panel_links: [ + { + href: '/notalink', + text: 'First item', + active: true + }, + { + href: '/notalink', + text: 'Second item' + } + ] + } + ], + [ + { + title: 'Small panel links 3', + panel: 'content' + } + ] + ] diff --git a/app/views/govuk_component/task_list.raw.html.erb b/app/views/govuk_component/task_list.raw.html.erb new file mode 100644 index 000000000..8210daba9 --- /dev/null +++ b/app/views/govuk_component/task_list.raw.html.erb @@ -0,0 +1,84 @@ +<% + steps ||= false + small ||= false + heading_level ||= 2 + open_section ||= false + remember_last_section ||= false + + section_count = 0 +%> +<% if steps %> ++ <%= panel_description %> +
+ <% end %> +Section 1 description in here
\ +Section 2 description in here
\ +First panel
', + panel_descriptions: [ + 'First step first section first panel description', + 'First step first section second panel description' + ], + panel_links: [ + { + href: '/notalink1', + text: 'First step first section first panel link' + }, + { + href: '/notalink2', + text: 'First step first section second panel link' + } + ] + }, + { + title: 'First step second section', + panel: 'Second panel
', + panel_descriptions: [ + 'First step second section first panel description', + 'First step second section second panel description' + ], + panel_links: [ + { + href: '/notalink3', + text: 'First step second section first panel link' + }, + { + href: '/notalink4', + text: 'First step second section second panel link', + active: true + } + ] + } + ], + [ + { + title: 'Second step first section', + panel: 'Third panel
', + panel_descriptions: [ + 'Second step first section first panel description', + 'Second step first section second panel description' + ], + panel_links: [ + { + href: '/notalink5', + text: 'Second step first section first panel link' + }, + { + href: '/notalink6', + text: 'Second step first section second panel link' + } + ] + } + ] + ] + end + + step1 = ".pub-c-task-list__step:nth-child(1)" + step2 = ".pub-c-task-list__step:nth-child(2)" + + test "renders nothing without passed content" do + assert_empty render_component({}) + end + + test "renders a simple tasklist correctly" do + render_component(steps: simple_tasklist) + assert_select ".pub-c-task-list" + + assert_select step1 + " .pub-c-task-list__section#first-header" + assert_select step1 + " .pub-c-task-list__title", text: "First header" + assert_select step1 + " .pub-c-task-list__panel", text: "First panel" + + assert_select step2 + " .pub-c-task-list__section#second-header" + assert_select step2 + " .pub-c-task-list__title", text: "Second header" + assert_select step2 + " .pub-c-task-list__panel", text: "Second panel" + end + + test "renders a simple tasklist with custom section ids" do + ids_accordion = simple_tasklist + ids_accordion[0][0][:id] = "first-section-id" + + render_component(steps: ids_accordion) + + assert_select step1, id: "first-section-id" + assert_select step2, id: "2nd-header" + end + + test "renders a tasklist with different heading levels" do + render_component(steps: simple_tasklist, heading_level: 4) + + assert_select step1 + " .pub-c-task-list__section#first-header h4.pub-c-task-list__title", text: "First header" + assert_select step2 + " .pub-c-task-list__section#second-header h4.pub-c-task-list__title", text: "Second header" + end + + test "opens a section by default" do + render_component(steps: simple_tasklist, open_section: 2) + + assert_select step2 + " .pub-c-task-list__section[data-open]" + end + + test "remembers last opened section" do + render_component(steps: simple_tasklist, remember_last_section: true) + + assert_select ".pub-c-task-list[data-remember]" + end + + test "renders a complex tasklist" do + render_component(steps: complex_tasklist) + + step1_section1 = step1 + " .pub-c-task-list__section#first-step-first-section" + step1_section2 = step1 + " .pub-c-task-list__section#first-step-second-section" + + assert_select step1 + ".pub-c-task-list__step--active[aria-current=?]", 'step' + assert_select step2 + ".pub-c-task-list__step--active", false + + assert_select step1_section1 + " .firstpanel", text: 'First panel' + assert_select step1_section1 + " .pub-c-task-list__panel-description:nth-child(2)", text: 'First step first section first panel description' + assert_select step1_section1 + " .pub-c-task-list__panel-description:nth-child(3)", text: 'First step first section second panel description' + assert_select step1_section1 + " .pub-c-task-list__panel-links .pub-c-task-list__panel-link:nth-child(1) a[href=?]", '/notalink1', text: 'First step first section first panel link' + assert_select step1_section1 + " .pub-c-task-list__panel-links .pub-c-task-list__panel-link:nth-child(2) a[href=?]", '/notalink2', text: 'First step first section second panel link' + + assert_select step1_section2 + " .secondpanel", text: 'Second panel' + assert_select step1_section2 + " .pub-c-task-list__panel-description:nth-child(2)", text: 'First step second section first panel description' + assert_select step1_section2 + " .pub-c-task-list__panel-description:nth-child(3)", text: 'First step second section second panel description' + assert_select step1_section2 + " .pub-c-task-list__panel-links .pub-c-task-list__panel-link:nth-child(1) a[href=?]", '/notalink3', text: 'First step second section first panel link' + assert_select step1_section2 + " .pub-c-task-list__panel-links .pub-c-task-list__panel-link--active:nth-child(2) .visuallyhidden", text: 'You are currently viewing:' + assert_select step1_section2 + " .pub-c-task-list__panel-links .pub-c-task-list__panel-link--active:nth-child(2)", text: 'You are currently viewing: First step second section second panel link' + end + + test "renders a small tasklist" do + render_component(steps: simple_tasklist, small: true) + + assert_select ".pub-c-task-list" + assert_select ".pub-c-task-list.pub-c-task-list--large", false + end +end