govuk_publishing_components 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/Rakefile +1 -9
  4. data/app/assets/javascripts/components/task-list.js +471 -0
  5. data/app/assets/javascripts/current-location.js +10 -0
  6. data/app/assets/javascripts/govuk_publishing_components/application.js +4 -0
  7. data/app/assets/javascripts/history-support.js +8 -0
  8. data/app/assets/stylesheets/components/_task-list-header-print.scss +15 -0
  9. data/app/assets/stylesheets/components/_task-list-header.scss +36 -0
  10. data/app/assets/stylesheets/components/_task-list-print.scss +35 -0
  11. data/app/assets/stylesheets/components/_task-list-related.scss +39 -0
  12. data/app/assets/stylesheets/components/_task-list.scss +454 -0
  13. data/app/assets/stylesheets/govuk_publishing_components/application.scss +3 -0
  14. data/app/assets/stylesheets/govuk_publishing_components/print.scss +4 -0
  15. data/app/controllers/govuk_publishing_components/component_guide_controller.rb +5 -0
  16. data/app/models/govuk_publishing_components/component_doc.rb +10 -3
  17. data/app/models/govuk_publishing_components/component_doc_resolver.rb +19 -5
  18. data/app/views/components/_task_list.html.erb +143 -0
  19. data/app/views/components/_task_list_header.html.erb +38 -0
  20. data/app/views/components/_task_list_related.html.erb +39 -0
  21. data/app/views/components/docs/task_list.yml +1032 -0
  22. data/app/views/components/docs/task_list_header.yml +33 -0
  23. data/app/views/components/docs/task_list_related.yml +54 -0
  24. data/app/views/govuk_publishing_components/component_guide/index.html.erb +14 -3
  25. data/app/views/govuk_publishing_components/component_guide/show.html.erb +1 -1
  26. data/app/views/layouts/govuk_publishing_components/application.html.erb +1 -4
  27. data/config/initializers/assets.rb +1 -0
  28. data/lib/govuk_publishing_components/config.rb +5 -1
  29. data/lib/govuk_publishing_components/version.rb +1 -1
  30. metadata +33 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e4dc58d36544b1596c32214fd0f900a58e3f9eca
4
- data.tar.gz: 3c758cf643c55aed354257f7ff90bea0bb2ec3ba
3
+ metadata.gz: fd686b764227170cd5029b2e357499afb254e157
4
+ data.tar.gz: c9789a0a73bf3dd79b242e300495790d253641a3
5
5
  SHA512:
6
- metadata.gz: a88f615ce939922932f1e09a7e11dca0c314263a0cf5f28b75eb99cf98746119ea6537819de7c96284373e5d30f52b3d70ca4e66c2772a59b84a1f4f3981080f
7
- data.tar.gz: d5ed207ce42a0c003d499e9b806a521ccd23f75b71ef400f43515d2841e8ec4ae6e89d8d80caed66f2e78c7cfb97b664f5f69f785b1efe97e551991c91392dbc
6
+ metadata.gz: 7148c05e82ebe78bd28e8ff6469e9c5ad0a23820118d23c519de2ab0f4ecc6574d25c8f077834137363beb2f2fe31d0e3991ccf51da4506c27dec0face3ff6d1
7
+ data.tar.gz: 6005d721d3bd60a2c7494c059b9dc8624e03f77f4ea42849ee81ac4dfd73f2ae6cab7087f3378b6760d5cb4306ee0e7aa3e173b98213afd0c86187142bb6bff1
data/README.md CHANGED
@@ -118,7 +118,7 @@ Sometimes you will have a component that will throw an error due to it being in
118
118
 
119
119
  For this case you can add `accessibility_excluded_rules` to your components' documentation yml file with the rules you want to exclude. These rules can be found in brackets in the error messages displayed.
120
120
 
121
- For an example of this check [test/.../docs/test-component-with-duplicate-ids.yml](test/dummy/app/views/components/docs/test-component-with-duplicate-ids.yml)
121
+ For an example of this check [test/.../docs/test-component-with-duplicate-ids.yml](spec/dummy/app/views/components/docs/test-component-with-duplicate-ids.yml)
122
122
 
123
123
 
124
124
  ## Component generator
data/Rakefile CHANGED
@@ -1,20 +1,12 @@
1
1
  require 'rspec/core/rake_task'
2
2
  RSpec::Core::RakeTask.new(:spec)
3
3
 
4
- APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
4
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
5
5
 
6
6
  load 'rails/tasks/engine.rake'
7
7
  load 'rails/tasks/statistics.rake'
8
8
 
9
9
  require 'bundler/gem_tasks'
10
- require 'rake/testtask'
11
-
12
- Rake::TestTask.new(:test) do |t|
13
- t.libs << 'lib'
14
- t.libs << 'test'
15
- t.pattern = 'test/**/*_test.rb'
16
- t.verbose = false
17
- end
18
10
 
19
11
  namespace :assets do
20
12
  desc "Test precompiling assets through dummy application"
@@ -0,0 +1,471 @@
1
+ // Most of this is originally from the service manual but has changed considerably since then
2
+
3
+ (function (Modules) {
4
+ "use strict";
5
+ window.GOVUK = window.GOVUK || {};
6
+
7
+ Modules.Gemtasklist = function () {
8
+
9
+ var actions = {
10
+ showLinkText: "Show",
11
+ hideLinkText: "Hide"
12
+ };
13
+
14
+ var bulkActions = {
15
+ showAll: {
16
+ buttonText: "Show all",
17
+ eventLabel: "Show All",
18
+ linkText: "Show"
19
+ },
20
+ hideAll: {
21
+ buttonText: "Hide all",
22
+ eventLabel: "Hide All",
23
+ linkText: "Hide"
24
+ }
25
+ };
26
+
27
+ var rememberShownStep = false;
28
+ var taskListSize;
29
+ var sessionStoreLink = 'govuk-task-list-active-link';
30
+ var activeLinkClass = 'gem-c-task-list__link-item--active';
31
+ var activeLinkHref = '#content';
32
+
33
+ this.start = function ($element) {
34
+
35
+ $(window).unload(storeScrollPosition);
36
+
37
+ // Indicate that js has worked
38
+ $element.addClass('gem-c-task-list--active');
39
+
40
+ // Prevent FOUC, remove class hiding content
41
+ $element.removeClass('js-hidden');
42
+
43
+ rememberShownStep = !!$element.filter('[data-remember]').length;
44
+ taskListSize = $element.hasClass('gem-c-task-list--large') ? 'Big' : 'Small';
45
+ var $groups = $element.find('.gem-c-task-list__group');
46
+ var $steps = $element.find('.js-step');
47
+ var $stepHeaders = $element.find('.js-toggle-panel');
48
+ var totalSteps = $element.find('.js-panel').length;
49
+ var totalLinks = $element.find('.gem-c-task-list__link-item').length;
50
+
51
+ var $showOrHideAllButton;
52
+
53
+ var tasklistTracker = new TasklistTracker(totalSteps, totalLinks);
54
+
55
+ addButtonstoSteps();
56
+ addShowHideAllButton();
57
+ addShowHideToggle();
58
+ addAriaControlsAttrForShowHideAllButton();
59
+
60
+ hideAllSteps();
61
+ showLinkedStep();
62
+ ensureOnlyOneActiveLink();
63
+
64
+ bindToggleForSteps(tasklistTracker);
65
+ bindToggleShowHideAllButton(tasklistTracker);
66
+ bindComponentLinkClicks(tasklistTracker);
67
+
68
+ // When navigating back in browser history to the tasklist, the browser will try to be "clever" and return
69
+ // the user to their previous scroll position. However, since we collapse all but the currently-anchored
70
+ // step, the content length changes and the user is returned to the wrong position (often the footer).
71
+ // In order to correct this behaviour, as the user leaves the page, we anticipate the correct height we wish the
72
+ // user to return to by forcibly scrolling them to that height, which becomes the height the browser will return
73
+ // them to.
74
+ // If we can't find an element to return them to, then reset the scroll to the top of the page. This handles
75
+ // the case where the user has expanded all steps, so they are not returned to a particular step, but
76
+ // still could have scrolled a long way down the page.
77
+ function storeScrollPosition() {
78
+ hideAllSteps();
79
+ var $step = getStepForAnchor();
80
+
81
+ document.body.scrollTop = $step && $step.length
82
+ ? $step.offset().top
83
+ : 0;
84
+ }
85
+
86
+ function addShowHideAllButton() {
87
+ $element.prepend('<div class="gem-c-task-list__controls"><button aria-expanded="false" class="gem-c-task-list__button gem-c-task-list__button--controls js-step-controls-button">' + bulkActions.showAll.buttonText + '</button></div>');
88
+ }
89
+
90
+ function addShowHideToggle() {
91
+ $stepHeaders.each(function() {
92
+ var linkText = actions.showLinkText;
93
+
94
+ if (headerIsOpen($(this))) {
95
+ linkText = actions.hideLinkText;
96
+ }
97
+
98
+ $(this).append('<span class="gem-c-task-list__toggle-link js-toggle-link">' + linkText + '</span>');
99
+ });
100
+ }
101
+
102
+ function headerIsOpen($stepHeader) {
103
+ return (typeof $stepHeader.closest('.js-step').data('show') !== 'undefined');
104
+ }
105
+
106
+ function addAriaControlsAttrForShowHideAllButton() {
107
+ var ariaControlsValue = $element.find('.js-panel').first().attr('id');
108
+
109
+ $showOrHideAllButton = $element.find('.js-step-controls-button');
110
+ $showOrHideAllButton.attr('aria-controls', ariaControlsValue);
111
+ }
112
+
113
+ function hideAllSteps() {
114
+ setAllStepsShownState(false);
115
+ }
116
+
117
+ function setAllStepsShownState(isShown) {
118
+ $.each($steps, function () {
119
+ var stepView = new StepView($(this));
120
+ stepView.preventHashUpdate();
121
+ stepView.setIsShown(isShown);
122
+ });
123
+ }
124
+
125
+ function showLinkedStep() {
126
+ var $step;
127
+ if (rememberShownStep) {
128
+ $step = getStepForAnchor();
129
+ } else {
130
+ $step = $steps.filter('[data-show]');
131
+ }
132
+
133
+ if ($step && $step.length) {
134
+ var stepView = new StepView($step);
135
+ stepView.show();
136
+ }
137
+ }
138
+
139
+ function getStepForAnchor() {
140
+ var anchor = getActiveAnchor();
141
+
142
+ return anchor.length
143
+ ? $element.find('#' + escapeSelector(anchor.substr(1)))
144
+ : null;
145
+ }
146
+
147
+ function getActiveAnchor() {
148
+ return GOVUK.getCurrentLocation().hash;
149
+ }
150
+
151
+ function addButtonstoSteps() {
152
+ $.each($steps, function () {
153
+ var $step = $(this);
154
+ var $title = $step.find('.js-step-title');
155
+ var contentId = $step.find('.js-panel').first().attr('id');
156
+
157
+ $title.wrapInner(
158
+ '<button ' +
159
+ 'class="gem-c-task-list__button gem-c-task-list__button--title js-step-title-button" ' +
160
+ 'aria-expanded="false" aria-controls="' + contentId + '">' +
161
+ '</button>' );
162
+ });
163
+ }
164
+
165
+ function bindToggleForSteps(tasklistTracker) {
166
+ $element.find('.js-toggle-panel').click(function (event) {
167
+ preventLinkFollowingForCurrentTab(event);
168
+
169
+ var stepView = new StepView($(this).closest('.js-step'));
170
+ stepView.toggle();
171
+
172
+ var toggleClick = new StepToggleClick(event, stepView, $steps, tasklistTracker, $groups);
173
+ toggleClick.track();
174
+
175
+ var toggleLink = $(this).find('.js-toggle-link');
176
+ toggleLink.text(toggleLink.text() == actions.showLinkText ? actions.hideLinkText : actions.showLinkText);
177
+
178
+ setShowHideAllText();
179
+ });
180
+ }
181
+
182
+ // tracking click events on links in step content
183
+ function bindComponentLinkClicks(tasklistTracker) {
184
+ $element.find('.js-link').click(function (event) {
185
+ var linkClick = new componentLinkClick(event, tasklistTracker, $(this).attr('data-position'));
186
+ linkClick.track();
187
+ var thisLinkHref = $(this).attr('href');
188
+
189
+ if ($(this).attr('rel') !== 'external') {
190
+ saveToSessionStorage(sessionStoreLink, $(this).data('position'));
191
+ }
192
+
193
+ if (thisLinkHref == activeLinkHref) {
194
+ setOnlyThisLinkActive($(this));
195
+ }
196
+ });
197
+ }
198
+
199
+ function saveToSessionStorage(key, value) {
200
+ sessionStorage.setItem(key, value);
201
+ }
202
+
203
+ function loadFromSessionStorage(key) {
204
+ return sessionStorage.getItem(key);
205
+ }
206
+
207
+ function removeFromSessionStorage(key) {
208
+ sessionStorage.removeItem(key);
209
+ }
210
+
211
+ function setOnlyThisLinkActive(clicked) {
212
+ $element.find('.' + activeLinkClass).removeClass(activeLinkClass);
213
+ clicked.addClass(activeLinkClass);
214
+ }
215
+
216
+ function ensureOnlyOneActiveLink() {
217
+ var $activeLinks = $element.find('.js-link.' + activeLinkClass);
218
+
219
+ if ($activeLinks.length <= 1) {
220
+ return;
221
+ }
222
+
223
+ var lastClicked = loadFromSessionStorage(sessionStoreLink);
224
+
225
+ if (lastClicked) {
226
+ removeActiveStateFromAllButCurrent($activeLinks, lastClicked);
227
+ removeFromSessionStorage(sessionStoreLink);
228
+ } else {
229
+ var activeLinkInActiveGroup = $element.find('.gem-c-task-list__group--active').find('.' + activeLinkClass).first();
230
+
231
+ if (activeLinkInActiveGroup.length) {
232
+ $activeLinks.removeClass(activeLinkClass);
233
+ activeLinkInActiveGroup.addClass(activeLinkClass);
234
+ } else {
235
+ $activeLinks.slice(1).removeClass(activeLinkClass);
236
+ }
237
+ }
238
+ }
239
+
240
+ function removeActiveStateFromAllButCurrent($links, current) {
241
+ $links.each(function() {
242
+ if ($(this).data('position') !== current) {
243
+ $(this).removeClass(activeLinkClass);
244
+ }
245
+ });
246
+ }
247
+
248
+ function preventLinkFollowingForCurrentTab(event) {
249
+ // If the user is holding the ⌘ or Ctrl key, they're trying
250
+ // to open the link in a new window, so let the click happen
251
+ if (event.metaKey || event.ctrlKey) {
252
+ return;
253
+ }
254
+
255
+ event.preventDefault();
256
+ }
257
+
258
+ function bindToggleShowHideAllButton(tasklistTracker) {
259
+ $showOrHideAllButton = $element.find('.js-step-controls-button');
260
+ $showOrHideAllButton.on('click', function () {
261
+ var shouldshowAll;
262
+
263
+ if ($showOrHideAllButton.text() == bulkActions.showAll.buttonText) {
264
+ $showOrHideAllButton.text(bulkActions.hideAll.buttonText);
265
+ $element.find('.js-toggle-link').text(actions.hideLinkText)
266
+ shouldshowAll = true;
267
+
268
+ tasklistTracker.track('pageElementInteraction', 'tasklistAllShown', {
269
+ label: bulkActions.showAll.eventLabel + ": " + taskListSize
270
+ });
271
+ } else {
272
+ $showOrHideAllButton.text(bulkActions.showAll.buttonText);
273
+ $element.find('.js-toggle-link').text(actions.showLinkText);
274
+ shouldshowAll = false;
275
+
276
+ tasklistTracker.track('pageElementInteraction', 'tasklistAllHidden', {
277
+ label: bulkActions.hideAll.eventLabel + ": " + taskListSize
278
+ });
279
+ }
280
+
281
+ setAllStepsShownState(shouldshowAll);
282
+ $showOrHideAllButton.attr('aria-expanded', shouldshowAll);
283
+ setShowHideAllText();
284
+ setHash(null);
285
+
286
+ return false;
287
+ });
288
+ }
289
+
290
+ function setShowHideAllText() {
291
+ var shownSteps = $element.find('.step-is-shown').length;
292
+ // Find out if the number of is-opens == total number of steps
293
+ if (shownSteps === totalSteps) {
294
+ $showOrHideAllButton.text(bulkActions.hideAll.buttonText);
295
+ } else {
296
+ $showOrHideAllButton.text(bulkActions.showAll.buttonText);
297
+ }
298
+ }
299
+
300
+ // Ideally we'd use jQuery.escapeSelector, but this is only available from v3
301
+ // See https://github.com/jquery/jquery/blob/2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/selector-native.js#L46
302
+ function escapeSelector(s) {
303
+ var cssMatcher = /([\x00-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;
304
+ return s.replace(cssMatcher, "\\$&");
305
+ }
306
+ };
307
+
308
+ function StepView($stepElement) {
309
+ var $titleLink = $stepElement.find('.js-step-title-button');
310
+ var $stepContent = $stepElement.find('.js-panel');
311
+ var shouldUpdateHash = rememberShownStep;
312
+
313
+ this.title = $stepElement.find('.js-step-title').text();
314
+ this.href = $titleLink.attr('href');
315
+ this.element = $stepElement;
316
+
317
+ this.show = show;
318
+ this.hide = hide;
319
+ this.toggle = toggle;
320
+ this.setIsShown = setIsShown;
321
+ this.isShown = isShown;
322
+ this.isHidden = isHidden;
323
+ this.preventHashUpdate = preventHashUpdate;
324
+ this.numberOfContentItems = numberOfContentItems;
325
+
326
+ function show() {
327
+ setIsShown(true);
328
+ }
329
+
330
+ function hide() {
331
+ setIsShown(false);
332
+ }
333
+
334
+ function toggle() {
335
+ setIsShown(isHidden());
336
+ }
337
+
338
+ function setIsShown(isShown) {
339
+ $stepElement.toggleClass('step-is-shown', isShown);
340
+ $stepContent.toggleClass('js-hidden', !isShown);
341
+ $titleLink.attr("aria-expanded", isShown);
342
+
343
+ if (shouldUpdateHash) {
344
+ updateHash($stepElement);
345
+ }
346
+ }
347
+
348
+ function isShown() {
349
+ return $stepElement.hasClass('step-is-shown');
350
+ }
351
+
352
+ function isHidden() {
353
+ return !isShown();
354
+ }
355
+
356
+ function preventHashUpdate() {
357
+ shouldUpdateHash = false;
358
+ }
359
+
360
+ function numberOfContentItems() {
361
+ return $stepContent.find('.js-link').length;
362
+ }
363
+ }
364
+
365
+ function updateHash($stepElement) {
366
+ var stepView = new StepView($stepElement);
367
+ var hash = stepView.isShown() && '#' + $stepElement.attr('id');
368
+ setHash(hash)
369
+ }
370
+
371
+ // Sets the hash for the page. If a falsy value is provided, the hash is cleared.
372
+ function setHash(hash) {
373
+ if (!GOVUK.support.history()) {
374
+ return;
375
+ }
376
+
377
+ var newLocation = hash || GOVUK.getCurrentLocation().pathname;
378
+ history.replaceState({}, '', newLocation);
379
+ }
380
+
381
+ function StepToggleClick(event, stepView, $steps, tasklistTracker, $groups) {
382
+ this.track = trackClick;
383
+ var $target = $(event.target);
384
+ var $thisGroup = stepView.element.closest('.gem-c-task-list__group');
385
+ var $thisGroupSteps = $thisGroup.find('.gem-c-task-list__step');
386
+
387
+ function trackClick() {
388
+ var tracking_options = {label: trackingLabel(), dimension28: stepView.numberOfContentItems().toString()}
389
+ tasklistTracker.track('pageElementInteraction', trackingAction(), tracking_options);
390
+
391
+ if (!stepView.isHidden()) {
392
+ tasklistTracker.track(
393
+ 'tasklistLinkClicked',
394
+ String(stepIndex()),
395
+ {
396
+ label: stepView.href,
397
+ dimension28: String(stepView.numberOfContentItems()),
398
+ dimension29: stepView.title
399
+ }
400
+ )
401
+ }
402
+ }
403
+
404
+ function trackingLabel() {
405
+ return $target.closest('.js-toggle-panel').attr('data-position') + ' - ' + stepView.title + ' - ' + locateClickElement() + ": " + taskListSize;
406
+ }
407
+
408
+ // returns index of the clicked step in the overall number of accordion steps, regardless of how many per group
409
+ function stepIndex() {
410
+ return $steps.index(stepView.element) + 1;
411
+ }
412
+
413
+ function trackingAction() {
414
+ return (stepView.isHidden() ? 'tasklistHidden' : 'tasklistShown');
415
+ }
416
+
417
+ function locateClickElement() {
418
+ if (clickedOnIcon()) {
419
+ return iconType() + ' click';
420
+ } else if (clickedOnHeading()) {
421
+ return 'Heading click';
422
+ } else {
423
+ return 'Elsewhere click';
424
+ }
425
+ }
426
+
427
+ function clickedOnIcon() {
428
+ return $target.hasClass('js-toggle-link');
429
+ }
430
+
431
+ function clickedOnHeading() {
432
+ return $target.hasClass('js-step-title-button');
433
+ }
434
+
435
+ function iconType() {
436
+ return (stepView.isHidden() ? 'Minus' : 'Plus');
437
+ }
438
+ }
439
+
440
+ function componentLinkClick(event, tasklistTracker, linkPosition) {
441
+ this.track = trackClick;
442
+
443
+ function trackClick() {
444
+ var tracking_options = {label: $(event.target).attr('href') + " : " + taskListSize};
445
+ var dimension28 = $(event.target).closest('.gem-c-task-list__links').attr('data-length');
446
+
447
+ if (dimension28) {
448
+ tracking_options['dimension28'] = dimension28;
449
+ }
450
+
451
+ tasklistTracker.track('taskAccordionLinkClicked', linkPosition, tracking_options);
452
+ }
453
+ }
454
+
455
+ // A helper that sends a custom event request to Google Analytics if
456
+ // the GOVUK module is setup
457
+ function TasklistTracker(totalSteps, totalLinks) {
458
+ this.track = function(category, action, options) {
459
+ // dimension26 records the total number of expand/collapse steps in this tasklist
460
+ // dimension27 records the total number of links in this tasklist
461
+ // dimension28 records the number of links in the step that was shown/hidden (handled in click event)
462
+ if (GOVUK.analytics && GOVUK.analytics.trackEvent) {
463
+ options = options || {};
464
+ options["dimension26"] = options["dimension26"] || totalSteps.toString();
465
+ options["dimension27"] = options["dimension27"] || totalLinks.toString();
466
+ GOVUK.analytics.trackEvent(category, action, options);
467
+ }
468
+ }
469
+ }
470
+ };
471
+ })(window.GOVUK.Modules);