mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-05 10:53:28 -05:00
Use HTML5 details element for the service switcher
The previous service switcher was built purely in Javascript, which meant that, for the purposes of progressive enhancement, it had to load in the open state. Setting it to the closed state with Javascript happened a fraction of a second after page load. This caused an unpleasant flicker as the whole page shifted up and down as it loaded. This commit changes the switcher to use the native HTML5 `details` and `summary` elements[1]. This commit adds a polyfill from GOV.UK Elements for browsers which don’t support `details`/`summary`. 1. http://html5doctor.com/the-details-and-summary-elements/
This commit is contained in:
199
app/assets/javascripts/detailsPolyfill.js
Normal file
199
app/assets/javascripts/detailsPolyfill.js
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copied from:
|
||||
// https://github.com/alphagov/govuk_elements/blob/4926897dc7734db2fc5e5ebb6acdc97f86e22e50/public/javascripts/vendor/details.polyfill.js
|
||||
|
||||
// When this is moved to GOV.UK Frontend Toolkit, we should import it from there
|
||||
// instead
|
||||
|
||||
// <details> polyfill
|
||||
// http://caniuse.com/#feat=details
|
||||
|
||||
// FF Support for HTML5's <details> and <summary>
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=591737
|
||||
|
||||
// http://www.sitepoint.com/fixing-the-details-element/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var NATIVE_DETAILS = typeof document.createElement('details').open === 'boolean';
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cross-modal click events
|
||||
function addClickEvent(node, callback) {
|
||||
// Prevent space(32) from scrolling the page
|
||||
addEvent(node, 'keypress', function (e, target) {
|
||||
if (target.nodeName === 'SUMMARY') {
|
||||
if (e.keyCode === 32) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
e.returnValue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// When the key comes up - check if it is enter(13) or space(32)
|
||||
addEvent(node, 'keyup', function (e, target) {
|
||||
if (e.keyCode === 13 || e.keyCode === 32) { callback(e, target); }
|
||||
});
|
||||
addEvent(node, 'mouseup', 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;
|
||||
}
|
||||
} while (node = node.parentNode);
|
||||
|
||||
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, 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);
|
||||
|
||||
})();
|
||||
@@ -1,18 +0,0 @@
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
Modules.Dropdown = function() {
|
||||
|
||||
this.start = function(component) {
|
||||
|
||||
$('.dropdown-toggle', component)
|
||||
.on(
|
||||
'click', () => $(component).toggleClass('js-closed')
|
||||
)
|
||||
.trigger('click');
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
})(window.GOVUK.Modules);
|
||||
@@ -3,56 +3,28 @@
|
||||
|
||||
position: relative;
|
||||
|
||||
.js-enabled &-toggle {
|
||||
&-toggle {
|
||||
|
||||
color: $link-colour;
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
cursor: pointer;
|
||||
outline-offset: 2px;
|
||||
|
||||
&:hover {
|
||||
color: $link-hover-colour;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '▼';
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.js-closed &-toggle::before {
|
||||
content: '▶';
|
||||
left: -1px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
padding-left: 20px;
|
||||
|
||||
.js-enabled & {
|
||||
|
||||
padding-left: 20px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.js-closed {
|
||||
|
||||
a {
|
||||
left: -9999em;
|
||||
position: absolute;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
<nav class="management-navigation">
|
||||
<div class="grid-row">
|
||||
<div class="column-half">
|
||||
<div class="dropdown" data-module="dropdown">
|
||||
<div class="dropdown-toggle">
|
||||
<details class="dropdown">
|
||||
<summary class="dropdown-toggle">
|
||||
Service name
|
||||
</div>
|
||||
</summary>
|
||||
<a href="#">Switch to A N Other service</a>
|
||||
<a href="{{ url_for('.add_service') }}">Add a new service to GOV.UK Notify</a>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="column-half management-navigation-account">
|
||||
<a href="{{ url_for('main.userprofile') }}">{{ current_user.name }}</a>
|
||||
|
||||
Reference in New Issue
Block a user