From 2e6769add82133147e4de408ce1e508b191008aa Mon Sep 17 00:00:00 2001 From: Gemma Leigh Date: Mon, 12 Jun 2017 11:35:28 +0100 Subject: [PATCH] Add details component and border utility mixin (#58) * Add a details component * Add vars for border widths * Add a set of mixins to output borders * Use the border mixin * Copy details polyfill js * Remove the defaults for the border mixin This way it can be used for borders - inset text and within the details component and also for error message borders. * Make params explicit for the border mixin --- src/components/details/README.md | 3 + src/components/details/_details.scss | 54 ++++++ src/components/details/details.html | 15 ++ src/components/details/details.js | 215 +++++++++++++++++++++++ src/globals/scss/_vars.scss | 3 + src/globals/scss/govuk-frontend.scss | 2 +- src/globals/scss/utilities/_borders.scss | 26 +++ 7 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 src/components/details/README.md create mode 100644 src/components/details/_details.scss create mode 100644 src/components/details/details.html create mode 100644 src/components/details/details.js create mode 100644 src/globals/scss/utilities/_borders.scss diff --git a/src/components/details/README.md b/src/components/details/README.md new file mode 100644 index 0000000000..3dc4c04a93 --- /dev/null +++ b/src/components/details/README.md @@ -0,0 +1,3 @@ +# Component example + +Description of component. diff --git a/src/components/details/_details.scss b/src/components/details/_details.scss new file mode 100644 index 0000000000..fdf738feb8 --- /dev/null +++ b/src/components/details/_details.scss @@ -0,0 +1,54 @@ +@import "../../globals/scss/import-once"; +@import "../../globals/scss/colours"; +@import "../../globals/scss/utilities/borders"; + +@include exports("details") { + .govuk-c-details { + display: block; + clear: both; + font-family: $govuk-font-stack; + @include font-smoothing; + @include core-19; + } + + .govuk-c-details__summary { + display: inline-block; + color: $govuk-blue; + cursor: pointer; + position: relative; + margin-bottom: em(5, 19); + } + + .govuk-c-details__summary:hover { + color: $govuk-link-hover-colour; + } + + .govuk-c-details__summary:focus { + outline: $govuk-outline-width-focus solid $govuk-focus-colour; + } + + // Underline only summary text (not the arrow) + .govuk-c-details__summary-text { + text-decoration: underline; + } + + // Match fallback arrow spacing with -webkit default + .govuk-c-details__arrow { + margin-right: .35em; + font-style: normal; + } + + .govuk-c-details__text { + padding: em(15, 19); + margin-bottom: em(15, 19); + @include govuk-u-border-left($govuk-border-width, $govuk-border-colour); + } + + .govuk-c-details__text p { + margin-top: 0; + } + + .govuk-c-details__text p:last-child { + margin-bottom: 0; + } +} diff --git a/src/components/details/details.html b/src/components/details/details.html new file mode 100644 index 0000000000..6d1a7b44a1 --- /dev/null +++ b/src/components/details/details.html @@ -0,0 +1,15 @@ +
+ + Help with nationality + +
+
+

+ If you’re not sure about your nationality, try to find out from an official document like a passport or national ID card. +

+

+ We need to know your nationality so we can work out which elections you’re entitled to vote in. If you can’t provide your nationality, you’ll have to send copies of identity documents through the post. +

+
+
+
diff --git a/src/components/details/details.js b/src/components/details/details.js new file mode 100644 index 0000000000..b53ebe3780 --- /dev/null +++ b/src/components/details/details.js @@ -0,0 +1,215 @@ +//
polyfill +// http://caniuse.com/#feat=details + +// FF Support for HTML5's
and +// https://bugzilla.mozilla.org/show_bug.cgi?id=591737 + +;(function () { + 'use strict' + + var NATIVE_DETAILS = typeof document.createElement('details').open === 'boolean' + var KEY_ENTER = 13 + var KEY_SPACE = 32 + + // Add event construct for modern browsers or IE + // which fires the callback with a pre-converted target reference + function addEvent (node, type, callback) { + if (node.addEventListener) { + node.addEventListener(type, function (e) { + callback(e, e.target) + }, false) + } else if (node.attachEvent) { + node.attachEvent('on' + type, function (e) { + callback(e, e.srcElement) + }) + } + } + + // Cross-browser character code / key pressed + function charCode (e) { + return (typeof e.which === 'number') ? e.which : e.keyCode + } + + // Cross-browser preventing default action + function preventDefault (e) { + if (e.preventDefault) { + e.preventDefault() + } else { + e.returnValue = false + } + } + + // Handle cross-modal click events + function addClickEvent (node, callback) { + addEvent(node, 'keypress', function (e, target) { + // When the key gets pressed - check if it is enter or space + if (charCode(e) === KEY_ENTER || charCode(e) === KEY_SPACE) { + if (target.nodeName.toLowerCase() === 'summary') { + // Prevent space from scrolling the page + // and enter from submitting a form + preventDefault(e) + // Click to let the click event do all the necessary action + if (target.click) { + target.click() + } else { + // except Safari 5.1 and under don't support .click() here + callback(e, target) + } + } + } + }) + + // Prevent keyup to prevent clicking twice in Firefox when using space key + addEvent(node, 'keyup', function (e, target) { + if (charCode(e) === KEY_SPACE) { + if (target.nodeName === 'SUMMARY') { + preventDefault(e) + } + } + }) + + addEvent(node, 'click', function (e, target) { + callback(e, target) + }) + } + + // Get the nearest ancestor element of a node that matches a given tag name + function getAncestor (node, match) { + do { + if (!node || node.nodeName.toLowerCase() === match) { + break + } + node = node.parentNode + } while (node) + + return node + } + + // Create a started flag so we can prevent the initialisation + // function firing from both DOMContentLoaded and window.onload + var started = false + + // Initialisation function + function addDetailsPolyfill (list) { + // If this has already happened, just return + // else set the flag so it doesn't happen again + if (started) { + return + } + started = true + + // Get the collection of details elements, but if that's empty + // then we don't need to bother with the rest of the scripting + if ((list = document.getElementsByTagName('details')).length === 0) { + return + } + + // else iterate through them to apply their initial state + var n = list.length + var i = 0 + for (i; i < n; i++) { + var details = list[i] + + // Save shortcuts to the inner summary and content elements + details.__summary = details.getElementsByTagName('summary').item(0) + details.__content = details.getElementsByTagName('div').item(0) + + // If the content doesn't have an ID, assign it one now + // which we'll need for the summary's aria-controls assignment + if (!details.__content.id) { + details.__content.id = 'details-content-' + i + } + + // Add ARIA role="group" to details + details.setAttribute('role', 'group') + + // Add role=button to summary + details.__summary.setAttribute('role', 'button') + + // Add aria-controls + details.__summary.setAttribute('aria-controls', details.__content.id) + + // Set tabIndex so the summary is keyboard accessible for non-native elements + // http://www.saliences.com/browserBugs/tabIndex.html + if (!NATIVE_DETAILS) { + details.__summary.tabIndex = 0 + } + + // Detect initial open state + var openAttr = details.getAttribute('open') !== null + if (openAttr === true) { + details.__summary.setAttribute('aria-expanded', 'true') + details.__content.setAttribute('aria-hidden', 'false') + } else { + details.__summary.setAttribute('aria-expanded', 'false') + details.__content.setAttribute('aria-hidden', 'true') + if (!NATIVE_DETAILS) { + details.__content.style.display = 'none' + } + } + + // Create a circular reference from the summary back to its + // parent details element, for convenience in the click handler + details.__summary.__details = details + + // If this is not a native implementation, create an arrow + // inside the summary + if (!NATIVE_DETAILS) { + var twisty = document.createElement('i') + + if (openAttr === true) { + twisty.className = 'arrow arrow-open' + twisty.appendChild(document.createTextNode('\u25bc')) + } else { + twisty.className = 'arrow arrow-closed' + twisty.appendChild(document.createTextNode('\u25ba')) + } + + details.__summary.__twisty = details.__summary.insertBefore(twisty, details.__summary.firstChild) + details.__summary.__twisty.setAttribute('aria-hidden', 'true') + } + } + + // Define a statechange function that updates aria-expanded and style.display + // Also update the arrow position + function statechange (summary) { + var expanded = summary.__details.__summary.getAttribute('aria-expanded') === 'true' + var hidden = summary.__details.__content.getAttribute('aria-hidden') === 'true' + + summary.__details.__summary.setAttribute('aria-expanded', (expanded ? 'false' : 'true')) + summary.__details.__content.setAttribute('aria-hidden', (hidden ? 'false' : 'true')) + + if (!NATIVE_DETAILS) { + summary.__details.__content.style.display = (expanded ? 'none' : '') + + var hasOpenAttr = summary.__details.getAttribute('open') !== null + if (!hasOpenAttr) { + summary.__details.setAttribute('open', 'open') + } else { + summary.__details.removeAttribute('open') + } + } + + if (summary.__twisty) { + summary.__twisty.firstChild.nodeValue = (expanded ? '\u25ba' : '\u25bc') + summary.__twisty.setAttribute('class', (expanded ? 'arrow arrow-closed' : 'arrow arrow-open')) + } + + return true + } + + // Bind a click event to handle summary elements + addClickEvent(document, function (e, summary) { + if (!(summary = getAncestor(summary, 'summary'))) { + return true + } + return statechange(summary) + }) + } + + // Bind two load events for modern and older browsers + // If the first one fires it will set a flag to block the second one + // but if it's not supported then the second one will fire + addEvent(document, 'DOMContentLoaded', addDetailsPolyfill) + addEvent(window, 'load', addDetailsPolyfill) +})() diff --git a/src/globals/scss/_vars.scss b/src/globals/scss/_vars.scss index 19e7cd5e88..e2b3b785d5 100644 --- a/src/globals/scss/_vars.scss +++ b/src/globals/scss/_vars.scss @@ -5,6 +5,9 @@ $govuk-border-width-tablet: 5px; $govuk-border-width-form-element: 2px; $govuk-border-width-error: 4px; +$govuk-border-width: 5px; +$govuk-border-width-wide: 10px; + $govuk-outline-width-focus: 3px; $govuk-site-width: 960px; $govuk-full-width: 100%; diff --git a/src/globals/scss/govuk-frontend.scss b/src/globals/scss/govuk-frontend.scss index a4e99faf88..d9563c1c75 100644 --- a/src/globals/scss/govuk-frontend.scss +++ b/src/globals/scss/govuk-frontend.scss @@ -16,4 +16,4 @@ @import "../../components/checkbox/checkbox"; @import "../../components/radio/radio"; @import "../../components/file-upload/file-upload"; - +@import "../../components/details/details"; diff --git a/src/globals/scss/utilities/_borders.scss b/src/globals/scss/utilities/_borders.scss new file mode 100644 index 0000000000..fb34a3292c --- /dev/null +++ b/src/globals/scss/utilities/_borders.scss @@ -0,0 +1,26 @@ +@import "../vars"; +@import "../colours"; + +// Mixin to output a border +// @param $govuk-u-border-width - width of border +// @param $govuk-u-border-colour - colour of border + +// Adds a top border to an element +@mixin govuk-u-border-top($govuk-u-border-width, $govuk-u-border-colour) { + border-top: $govuk-u-border-width solid $govuk-u-border-colour; +} + +// Adds a right border to an element +@mixin govuk-u-border-right($govuk-u-border-width, $govuk-u-border-colour) { + border-right: $govuk-u-border-width solid $govuk-border-colour; +} + +// Adds a bottom border to an element +@mixin govuk-u-border-bottom($govuk-u-border-width, $govuk-u-border-colour) { + border-bottom: $govuk-u-border-width solid $govuk-border-colour; +} + +// Adds a left border to an element +@mixin govuk-u-border-left($govuk-u-border-width, $govuk-u-border-colour) { + border-left: $govuk-u-border-width solid $govuk-border-colour; +}