Merge pull request #3225 from alphagov/fix-details

Add GOV.UK Frontend details component - second attempt
This commit is contained in:
Tom Byers
2019-12-23 09:47:56 +00:00
committed by GitHub
12 changed files with 145 additions and 274 deletions

View File

@@ -743,6 +743,7 @@ def add_template_filters(application):
format_phone_number_human_readable,
format_thousands,
id_safe,
convert_to_boolean,
]:
application.add_template_filter(fn)

View File

@@ -1,196 +0,0 @@
// From
// https://github.com/alphagov/govuk_elements/blob/4926897dc7734db2fc5e5ebb6acdc97f86e22e50/public/javascripts/vendor/details.polyfill.js
//
// ---
//
// <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
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);
})();

View File

@@ -7,6 +7,21 @@
// Exported items will be added to the window.GOVUK namespace.
// For example, `export { Frontend }` will assign `Frontend` to `window.Frontend`
import Header from 'govuk-frontend/components/header/header';
import Details from 'govuk-frontend/components/details/details';
/**
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
* This seems to fail in IE8, requires more investigation.
* See: https://github.com/imagitama/nodelist-foreach-polyfill
*/
function nodeListForEach (nodes, callback) {
if (window.NodeList.prototype.forEach) {
return nodes.forEach(callback)
}
for (var i = 0; i < nodes.length; i++) {
callback.call(window, nodes[i], i, nodes);
}
}
// Copy of the initAll function from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/all.js
// except it only includes, and initialises, the components used by this application.
@@ -18,6 +33,12 @@ function initAll (options) {
// Defaults to the entire document if nothing is set.
var scope = typeof options.scope !== 'undefined' ? options.scope : document
// Find all global details elements to enhance.
var $details = scope.querySelectorAll('details')
nodeListForEach($details, function ($detail) {
new Details($detail).init()
})
// Find first header module to enhance.
var $toggleButton = scope.querySelector('[data-module="header"]')
new Header($toggleButton).init()
@@ -26,6 +47,7 @@ function initAll (options) {
// Create separate namespace for GOVUK Frontend.
var Frontend = {
"Header": Header,
"Details": Details,
"initAll": initAll
}

View File

@@ -17,6 +17,7 @@ $govuk-assets-path: "/static/";
@import 'components/header/_header';
@import 'components/footer/_footer';
@import 'components/back-link/_back-link';
@import 'components/details/_details';
@import "utilities/all";
@import "overrides/all";

View File

@@ -1,6 +1,8 @@
.api-notifications {
font-family: monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
border-bottom: 1px solid $border-colour;
&-item {
@@ -8,38 +10,59 @@
border-top: 1px solid $border-colour;
padding: 10px 0 0 0;
&-title {
color: $link-colour;
&__heading,
&__data,
&__view {
font-family: monospace;
}
&__heading {
display: block;
margin-bottom: $gutter-half;
&::before {
top: -1.3em;
}
}
&__meta {
display: block;
color: $secondary-text-colour;
text-decoration: none;
display: block;
&-key,
&-time {
color: $secondary-text-colour;
display: inline-block;
width: auto;
}
@include govuk-media-query($from: tablet) {
&-key,
&-time {
width: 50%;
}
&-time {
text-align: right;
}
}
}
&-recipient {
display: inline;
}
&__data {
&-meta {
display: block;
color: $secondary-text-colour;
}
border-left: none;
padding-left: 25px;
&-time {
text-align: right;
}
&-name {
color: $secondary-text-colour;
}
&-key {
display: inline-block;
padding-left: 46px;
}
&-data {
padding-left: 31px;
color: $secondary-text-colour;
&-item {
padding-bottom: 15px;
&-value {
color: $text-colour;
padding-bottom: 15px;
}
}

View File

@@ -18,6 +18,9 @@
.govuk-header__container { border-color: {{header_colour}} }
</style>
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
{% block meta_format_detection %}
<meta name="format-detection" content="telephone=no">
{% endblock %}
{% block meta %}
{% endblock %}
{% endblock %}

View File

@@ -39,43 +39,45 @@
<div class="api-notifications">
{% if not api_notifications.notifications %}
<div class="api-notifications-item">
<p class="api-notifications-item-meta">
<p class="api-notifications-item__meta">
When you send messages via the API theyll appear here.
</p>
<p class="api-notifications-item-meta">
<p class="api-notifications-item__meta">
Notify deletes messages after 7 days.
</p>
</div>
{% endif %}
{% for notification in api_notifications.notifications %}
<details class="api-notifications-item">
<summary class="api-notifications-item-title">
<h3 class="api-notifications-item-recipient">
<details class="api-notifications-item govuk-details govuk-!-margin-bottom-0" data-module="govuk-details">
<summary class="govuk-details__summary govuk-clearfix api-notifications-item__heading">
<h3>
<span class="govuk-details__summary-text">
{{ notification.to }}
</span>
<span class="govuk-grid-row api-notifications-item__meta">
<span class="govuk-grid-column-one-half api-notifications-item__meta-key">
{{notification.key_name}}
</span>
<span class="govuk-grid-column-one-half api-notifications-item__meta-time">
<time class="timeago" datetime="{{ notification.created_at }}">
{{ notification.created_at|format_delta }}
</time>
</span>
</span>
</h3>
<span class="grid-row api-notifications-item-meta">
<span class="column-half api-notifications-item-key">
{{notification.key_name}}
</span>
<span class="column-half api-notifications-item-time">
<time class="timeago" datetime="{{ notification.created_at }}">
{{ notification.created_at|format_delta }}
</time>
</span>
</span>
</summary>
<div>
<dl id="notification-{{ notification.id }}" class="api-notifications-item-data bottom-gutter-1-2">
<div class="govuk-details__text api-notifications-item__data govuk-!-padding-top-0">
<dl id="notification-{{ notification.id }}">
{% for key in [
'id', 'client_reference', 'notification_type', 'created_at', 'updated_at', 'sent_at', 'status'
] %}
{% if notification[key] %}
<dt>{{ key }}:</dt>
<dd class="api-notifications-item-data-item">{{ notification[key] }}</dd>
<dt class="api-notifications-item__data-name">{{ key }}:</dt>
<dd class="api-notifications-item__data-value">{{ notification[key] }}</dd>
{% endif %}
{% endfor %}
{% if notification.status not in ('pending-virus-check', 'virus-scan-failed') %}
<a href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=notification.id) }}">View {{ message_count_label(1, notification.template.template_type, suffix='') }}</a>
<a class="api-notifications-item__view" href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=notification.id) }}">View {{ message_count_label(1, notification.template.template_type, suffix='') }}</a>
{% endif %}
</dl>
</div>
@@ -84,11 +86,11 @@
{% if api_notifications.notifications %}
<div class="api-notifications-item">
{% if api_notifications.notifications|length == 50 %}
<p class="api-notifications-item-meta">
<p class="api-notifications-item__meta">
Only showing the first 50 messages.
</p>
{% endif %}
<p class="api-notifications-item-meta">
<p class="api-notifications-item__meta">
Notify deletes messages after 7 days.
</p>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "content_template.html" %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% from "components/sub-navigation.html" import sub_navigation %}
{% from "components/details/macro.njk" import govukDetails %}
{% block per_page_title %}
Get started
@@ -14,21 +15,22 @@
<li class="get-started-list__item">
<h2 class="get-started-list__heading">Check if GOV.UK Notify is right for you</h2>
<p>Read about our <a href="{{ url_for('main.features') }}">features</a>, <a href="{{ url_for('.pricing') }}">pricing</a> and <a href="{{ url_for('main.roadmap') }}">roadmap</a>.</p>
<details>
<summary>Organisations that can use Notify</summary>
<div id="eligible-organisations">
<p>Notify is available to:</p>
<ul class="list list-bullet">
<li>central government departments</li>
<li>local authorities</li>
<li>state-funded schools</li>
<li>housing associations</li>
<li>the NHS</li>
<li>companies owned by local or central government that deliver services on their behalf</li>
</ul>
<p>Notify is not currently available to charities.</p>
</div>
</details>
{{ govukDetails({
"summaryText": "Organisations that can use Notify",
"html": '''
<div id="eligible-organisations">
<p>Notify is available to:</p>
<ul class="list list-bullet">
<li>central government departments</li>
<li>local authorities</li>
<li>state-funded schools</li>
<li>housing associations</li>
<li>the NHS</li>
<li>companies owned by local or central government that deliver services on their behalf</li>
</ul>
<p>Notify is not currently available to charities.</p>
</div>'''
}) }}
</li>
<li class="get-started-list__item">

View File

@@ -4,6 +4,7 @@
{% from "components/message-count-label.html" import message_count_label %}
{% from "components/status-box.html" import status_box %}
{% from "components/form.html" import form_wrapper %}
{% from "components/details/macro.njk" import govukDetails %}
{% block per_page_title %}
Platform admin
@@ -14,15 +15,21 @@
<h1 class="heading-large">
Summary
</h1>
<details {% if form.errors %}open{% endif %}>
<summary>Apply filters</summary>
{% set details_content %}
{% call form_wrapper(method="get") %}
{{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }}
{{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }}
</br>
<button type="submit" class="button">Filter</button>
{% endcall %}
</details>
{% endset %}
{{ govukDetails({
"summaryText": "Apply filters",
"html": details_content,
"open": form.errors | convert_to_boolean
}) }}
<div class="grid-row bottom-gutter">
{% for noti_type in global_stats %}

View File

@@ -6,6 +6,7 @@
{% from "components/message-count-label.html" import message_count_label %}
{% from "components/table.html" import mapping_table, field, stats_fields, row_group, row, right_aligned_field_heading, hidden_field_heading, text_field %}
{% from "components/form.html" import form_wrapper %}
{% from "components/details/macro.njk" import govukDetails %}
{% macro stats_fields(channel, data) -%}
@@ -101,16 +102,21 @@
{{ page_title|capitalize }}
</h1>
<details>
<summary>Apply filters</summary>
{% call form_wrapper(method="get") %}
{{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }}
{{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }}
{{ checkbox(form.include_from_test_key) }}
</br>
<button type="submit" class="button">Filter</button>
{% endcall %}
</details>
{% set details_content %}
{% call form_wrapper(method="get") %}
{{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }}
{{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }}
{{ checkbox(form.include_from_test_key) }}
</br>
<button type="submit" class="button">Filter</button>
{% endcall %}
{% endset %}
{{ govukDetails({
"summaryText": "Apply filters",
"html": details_content
}) }}
{% include "views/platform-admin/_global_stats.html" %}

View File

@@ -63,7 +63,8 @@ const copy = {
'skip-link',
'header',
'footer',
'back-link'
'back-link',
'details'
];
let done = 0;
@@ -143,7 +144,6 @@ const javascripts = () => {
paths.src + 'javascripts/govuk/cookie-functions.js',
paths.src + 'javascripts/cookieMessage.js',
paths.src + 'javascripts/stick-to-window-when-scrolling.js',
paths.src + 'javascripts/detailsPolyfill.js',
paths.src + 'javascripts/apiKey.js',
paths.src + 'javascripts/autofocus.js',
paths.src + 'javascripts/enhancedTextbox.js',

View File

@@ -37,7 +37,7 @@ def test_should_show_api_page(
rows = page.find_all('details')
assert len(rows) == 5
for row in rows:
assert row.find('h3').string.strip() == '07123456789'
assert row.select('h3 .govuk-details__summary-text')[0].string.strip() == '07123456789'
def test_should_show_api_page_with_lots_of_notifications(