@ministryofjustice/frontend 3.3.0 → 3.4.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 (87) hide show
  1. package/README.md +4 -10
  2. package/govuk-prototype-kit.config.json +5 -16
  3. package/moj/all.jquery.min.js +77 -3
  4. package/moj/all.js +2022 -1444
  5. package/moj/all.scss +2 -0
  6. package/moj/all.spec.js +15 -13
  7. package/moj/components/_all.scss +1 -0
  8. package/moj/components/action-bar/_action-bar.scss +4 -6
  9. package/moj/components/add-another/_add-another.scss +9 -7
  10. package/moj/components/add-another/add-another.js +90 -69
  11. package/moj/components/add-another/add-another.spec.js +165 -0
  12. package/moj/components/alert/README.md +0 -0
  13. package/moj/components/alert/_alert.scss +142 -0
  14. package/moj/components/alert/alert.js +247 -0
  15. package/moj/components/alert/alert.spec.helper.js +67 -0
  16. package/moj/components/alert/alert.spec.js +229 -0
  17. package/moj/components/alert/macro.njk +3 -0
  18. package/moj/components/alert/template.njk +83 -0
  19. package/moj/components/badge/_badge.scss +3 -4
  20. package/moj/components/banner/_banner.scss +5 -10
  21. package/moj/components/button-menu/_button-menu.scss +10 -9
  22. package/moj/components/button-menu/button-menu.js +139 -136
  23. package/moj/components/button-menu/button-menu.spec.js +295 -296
  24. package/moj/components/cookie-banner/_cookie-banner.scss +6 -5
  25. package/moj/components/currency-input/_currency-input.scss +4 -4
  26. package/moj/components/date-picker/README.md +14 -17
  27. package/moj/components/date-picker/_date-picker.scss +122 -106
  28. package/moj/components/date-picker/date-picker.js +473 -471
  29. package/moj/components/date-picker/date-picker.spec.js +962 -914
  30. package/moj/components/filter/README.md +1 -1
  31. package/moj/components/filter/_filter.scss +53 -75
  32. package/moj/components/filter-toggle-button/filter-toggle-button.js +71 -67
  33. package/moj/components/filter-toggle-button/filter-toggle-button.spec.js +203 -205
  34. package/moj/components/form-validator/form-validator.js +117 -109
  35. package/moj/components/header/_header.scss +17 -19
  36. package/moj/components/identity-bar/_identity-bar.scss +5 -5
  37. package/moj/components/interruption-card/_interruption-card.scss +9 -2
  38. package/moj/components/messages/_messages.scss +12 -19
  39. package/moj/components/multi-file-upload/README.md +1 -1
  40. package/moj/components/multi-file-upload/_multi-file-upload.scss +34 -30
  41. package/moj/components/multi-file-upload/multi-file-upload.js +188 -152
  42. package/moj/components/multi-file-upload/multi-file-upload.spec.js +510 -0
  43. package/moj/components/multi-select/_multi-select.scss +4 -3
  44. package/moj/components/multi-select/multi-select.js +55 -50
  45. package/moj/components/multi-select/multi-select.spec.js +128 -0
  46. package/moj/components/notification-badge/_notification-badge.scss +12 -12
  47. package/moj/components/organisation-switcher/_organisation-switcher.scss +1 -1
  48. package/moj/components/page-header-actions/_page-header-actions.scss +3 -2
  49. package/moj/components/pagination/_pagination.scss +26 -31
  50. package/moj/components/password-reveal/_password-reveal.scss +1 -2
  51. package/moj/components/password-reveal/password-reveal.js +22 -21
  52. package/moj/components/password-reveal/password-reveal.spec.js +39 -37
  53. package/moj/components/primary-navigation/_primary-navigation.scss +26 -29
  54. package/moj/components/progress-bar/_progress-bar.scss +21 -26
  55. package/moj/components/rich-text-editor/_rich-text-editor.scss +17 -16
  56. package/moj/components/rich-text-editor/rich-text-editor.js +117 -103
  57. package/moj/components/search/_search.scss +6 -4
  58. package/moj/components/search-toggle/search-toggle.js +29 -30
  59. package/moj/components/search-toggle/search-toggle.scss +21 -15
  60. package/moj/components/search-toggle/search-toggle.spec.js +129 -0
  61. package/moj/components/side-navigation/_side-navigation.scss +12 -21
  62. package/moj/components/sortable-table/_sortable-table.scss +25 -23
  63. package/moj/components/sortable-table/sortable-table.js +139 -117
  64. package/moj/components/sortable-table/sortable-table.spec.js +362 -0
  65. package/moj/components/sub-navigation/_sub-navigation.scss +24 -28
  66. package/moj/components/tag/_tag.scss +8 -9
  67. package/moj/components/task-list/_task-list.scss +8 -7
  68. package/moj/components/ticket-panel/_ticket-panel.scss +14 -6
  69. package/moj/components/timeline/_timeline.scss +18 -20
  70. package/moj/filters/all.js +28 -30
  71. package/moj/filters/prototype-kit-13-filters.js +2 -1
  72. package/moj/helpers/_all.scss +1 -0
  73. package/moj/helpers/_hidden.scss +1 -1
  74. package/moj/helpers/_links.scss +20 -0
  75. package/moj/helpers.js +160 -31
  76. package/moj/helpers.spec.js +235 -0
  77. package/moj/init.js +2 -2
  78. package/moj/moj-frontend.min.css +2 -2
  79. package/moj/moj-frontend.min.js +77 -3
  80. package/moj/namespace.js +2 -1
  81. package/moj/objects/_filter-layout.scss +11 -10
  82. package/moj/objects/_scrollable-pane.scss +11 -14
  83. package/moj/settings/_colours.scss +5 -0
  84. package/moj/settings/_measurements.scss +0 -2
  85. package/moj/utilities/_hidden.scss +3 -3
  86. package/moj/utilities/_width-container.scss +1 -1
  87. package/package.json +1 -1
@@ -3,44 +3,43 @@
3
3
  ========================================================================== */
4
4
 
5
5
  .moj-timeline {
6
+ position: relative;
6
7
  margin-bottom: govuk-spacing(4);
7
8
  overflow: hidden;
8
- position: relative;
9
9
 
10
- &:before {
11
- background-color: $govuk-brand-colour;
10
+ &::before {
12
11
  content: "";
13
- height: 100%;
14
- left: 0;
15
12
  position: absolute;
16
13
  top: govuk-spacing(2);
14
+ left: 0;
17
15
  width: 5px;
16
+ height: 100%;
17
+ background-color: $govuk-brand-colour;
18
18
  }
19
-
20
19
  }
21
20
 
22
21
  .moj-timeline--full {
23
22
  margin-bottom: 0;
24
- &:before {
23
+
24
+ &::before {
25
25
  height: calc(100% - 75px);
26
26
  }
27
27
  }
28
28
 
29
29
  .moj-timeline__item {
30
+ position: relative;
30
31
  padding-bottom: govuk-spacing(6);
31
32
  padding-left: govuk-spacing(4);
32
- position: relative;
33
33
 
34
- &:before {
35
- background-color: $govuk-brand-colour;
34
+ &::before {
36
35
  content: "";
37
- height: 5px;
38
- left: 0;
39
36
  position: absolute;
40
37
  top: govuk-spacing(2);
38
+ left: 0;
41
39
  width: 15px;
40
+ height: 5px;
41
+ background-color: $govuk-brand-colour;
42
42
  }
43
-
44
43
  }
45
44
 
46
45
  .moj-timeline__title {
@@ -50,9 +49,9 @@
50
49
 
51
50
  .moj-timeline__byline {
52
51
  @include govuk-font($size: 19);
53
- color: $govuk-secondary-text-colour;
54
52
  display: inline;
55
53
  margin: 0;
54
+ color: $govuk-secondary-text-colour;
56
55
  }
57
56
 
58
57
  .moj-timeline__date {
@@ -71,9 +70,9 @@
71
70
  ========================================================================== */
72
71
 
73
72
  .moj-timeline__documents {
74
- list-style: none;
75
73
  margin-bottom: 0;
76
74
  padding-left: 0;
75
+ list-style: none;
77
76
  }
78
77
 
79
78
  .moj-timeline__document-item {
@@ -82,14 +81,13 @@
82
81
  &:last-child {
83
82
  margin-bottom: 0;
84
83
  }
85
-
86
84
  }
87
85
 
88
86
  .moj-timeline__document-icon {
89
- float: left;
90
87
  margin-top: 4px;
91
88
  margin-right: 4px;
92
- fill: currentColor;
89
+ float: left;
90
+ fill: currentcolor;
93
91
 
94
92
  @media screen and (forced-colors: active) {
95
93
  fill: linkText;
@@ -97,11 +95,11 @@
97
95
  }
98
96
 
99
97
  .moj-timeline__document-link {
98
+ padding-left: govuk-spacing(5);
100
99
  background-image: url(#{$moj-images-path}icon-document.svg);
101
100
  background-repeat: no-repeat;
102
- background-size: 20px 16px;
103
101
  background-position: 0 50%;
104
- padding-left: govuk-spacing(5);
102
+ background-size: 20px 16px;
105
103
 
106
104
  &:focus {
107
105
  color: govuk-colour("black"); // Focus colour on yellow should really be black.
@@ -1,41 +1,41 @@
1
- const moment = require('moment');
1
+ const moment = require('moment')
2
2
 
3
3
  module.exports = function () {
4
4
  /**
5
5
  * Instantiate object used to store the methods registered as a
6
6
  * 'filter' (of the same name) within nunjucks. You can override
7
7
  * gov.uk core filters by creating filter methods of the same name.
8
- * @type {Object}
8
+ *
9
+ * @type {object}
9
10
  */
10
- let filters = {}
11
+ const filters = {}
11
12
 
12
13
  /* ------------------------------------------------------------------
13
14
  date filter for use in Nunjucks
14
15
  example: {{ params.date | date("DD/MM/YYYY") }}
15
16
  outputs: 01/01/1970
16
17
  ------------------------------------------------------------------ */
17
- filters.date = function(timestamp, format) {
18
- return moment(timestamp).format(format);
18
+ filters.date = function (timestamp, format) {
19
+ return moment(timestamp).format(format)
19
20
  }
20
21
 
21
22
  /* ------------------------------------------------------------------
22
23
  utility functions for use in mojDate function/filter
23
24
  ------------------------------------------------------------------ */
24
25
  function govDate(timestamp) {
25
- return moment(timestamp).format('D MMMM YYYY');
26
+ return moment(timestamp).format('D MMMM YYYY')
26
27
  }
27
-
28
+
28
29
  function govShortDate(timestamp) {
29
- return moment(timestamp).format('D MMM YYYY');
30
+ return moment(timestamp).format('D MMM YYYY')
30
31
  }
31
-
32
+
32
33
  function govTime(timestamp) {
33
- let t = moment(timestamp);
34
- if(t.minutes() > 0) {
35
- return t.format('h:mma');
36
- } else {
37
- return t.format('ha');
34
+ const t = moment(timestamp)
35
+ if (t.minutes() > 0) {
36
+ return t.format('h:mma')
38
37
  }
38
+ return t.format('ha')
39
39
  }
40
40
 
41
41
  /* ------------------------------------------------------------------
@@ -43,27 +43,25 @@ module.exports = function () {
43
43
  example: {{ params.date | mojDate("datetime") }}
44
44
  outputs: 1 Jan 1970 at 1:32pm
45
45
  ------------------------------------------------------------------ */
46
- filters.mojDate = function(timestamp, type) {
47
-
48
- switch(type) {
49
- case "datetime":
50
- return govDate(timestamp) + " at " + govTime(timestamp);
51
- case "shortdatetime":
52
- return govShortDate(timestamp) + " at " + govTime(timestamp);
53
- case "date":
54
- return govDate(timestamp);
55
- case "shortdate":
56
- return govShortDate(timestamp);
57
- case "time":
58
- return govTime(timestamp);
46
+ filters.mojDate = function (timestamp, type) {
47
+ switch (type) {
48
+ case 'datetime':
49
+ return `${govDate(timestamp)} at ${govTime(timestamp)}`
50
+ case 'shortdatetime':
51
+ return `${govShortDate(timestamp)} at ${govTime(timestamp)}`
52
+ case 'date':
53
+ return govDate(timestamp)
54
+ case 'shortdate':
55
+ return govShortDate(timestamp)
56
+ case 'time':
57
+ return govTime(timestamp)
59
58
  default:
60
- return timestamp;
59
+ return timestamp
61
60
  }
62
-
63
61
  }
64
62
 
65
63
  /* ------------------------------------------------------------------
66
64
  keep the following line to return your filters to the app
67
65
  ------------------------------------------------------------------ */
68
- return filters;
66
+ return filters
69
67
  }
@@ -1,8 +1,9 @@
1
1
  const { addFilter } = require('govuk-prototype-kit').views
2
+
2
3
  const getAllFilters = require('./all')
3
4
 
4
5
  const allFilters = getAllFilters()
5
6
 
6
- Object.keys(allFilters).forEach(name => {
7
+ Object.keys(allFilters).forEach((name) => {
7
8
  addFilter(name, allFilters[name])
8
9
  })
@@ -1 +1,2 @@
1
1
  @import "hidden";
2
+ @import "links";
@@ -1,3 +1,3 @@
1
1
  @mixin moj-hidden() {
2
2
  display: none;
3
- }
3
+ }
@@ -0,0 +1,20 @@
1
+ @import '../settings/colours';
2
+
3
+ @mixin moj-link-style-warning {
4
+ &:link,
5
+ &:visited {
6
+ color: $moj-warning-link-colour;
7
+ }
8
+
9
+ &:hover {
10
+ color: scale-color($moj-warning-link-colour, $lightness: -30%);
11
+ }
12
+
13
+ &:active {
14
+ color: $moj-warning-colour;
15
+ }
16
+
17
+ &:focus {
18
+ color: $govuk-focus-text-colour;
19
+ }
20
+ }
package/moj/helpers.js CHANGED
@@ -1,51 +1,180 @@
1
- MOJFrontend.removeAttributeValue = function(el, attr, value) {
2
- var re, m;
1
+ MOJFrontend.removeAttributeValue = function (el, attr, value) {
2
+ let re, m
3
3
  if (el.getAttribute(attr)) {
4
- if (el.getAttribute(attr) == value) {
5
- el.removeAttribute(attr);
4
+ if (el.getAttribute(attr) === value) {
5
+ el.removeAttribute(attr)
6
6
  } else {
7
- re = new RegExp('(^|\\s)' + value + '(\\s|$)');
8
- m = el.getAttribute(attr).match(re);
9
- if (m && m.length == 3) {
10
- el.setAttribute(attr, el.getAttribute(attr).replace(re, (m[1] && m[2])?' ':''))
7
+ re = new RegExp(`(^|\\s)${value}(\\s|$)`)
8
+ m = el.getAttribute(attr).match(re)
9
+ if (m && m.length === 3) {
10
+ el.setAttribute(
11
+ attr,
12
+ el.getAttribute(attr).replace(re, m[1] && m[2] ? ' ' : '')
13
+ )
11
14
  }
12
15
  }
13
16
  }
14
17
  }
15
18
 
16
- MOJFrontend.addAttributeValue = function(el, attr, value) {
17
- var re;
19
+ MOJFrontend.addAttributeValue = function (el, attr, value) {
20
+ let re
18
21
  if (!el.getAttribute(attr)) {
19
- el.setAttribute(attr, value);
20
- }
21
- else {
22
- re = new RegExp('(^|\\s)' + value + '(\\s|$)');
22
+ el.setAttribute(attr, value)
23
+ } else {
24
+ re = new RegExp(`(^|\\s)${value}(\\s|$)`)
23
25
  if (!re.test(el.getAttribute(attr))) {
24
- el.setAttribute(attr, el.getAttribute(attr) + ' ' + value);
26
+ el.setAttribute(attr, `${el.getAttribute(attr)} ${value}`)
25
27
  }
26
28
  }
27
- };
29
+ }
28
30
 
29
- MOJFrontend.dragAndDropSupported = function() {
30
- var div = document.createElement('div');
31
- return typeof div.ondrop != 'undefined';
32
- };
31
+ MOJFrontend.dragAndDropSupported = function () {
32
+ const div = document.createElement('div')
33
+ return typeof div.ondrop !== 'undefined'
34
+ }
33
35
 
34
- MOJFrontend.formDataSupported = function() {
35
- return typeof FormData == 'function';
36
- };
36
+ MOJFrontend.formDataSupported = function () {
37
+ return typeof FormData === 'function'
38
+ }
37
39
 
38
- MOJFrontend.fileApiSupported = function() {
39
- var input = document.createElement('input');
40
- input.type = 'file';
41
- return typeof input.files != 'undefined';
42
- };
40
+ MOJFrontend.fileApiSupported = function () {
41
+ const input = document.createElement('input')
42
+ input.type = 'file'
43
+ return typeof input.files !== 'undefined'
44
+ }
43
45
 
44
- MOJFrontend.nodeListForEach = function(nodes, callback) {
46
+ MOJFrontend.nodeListForEach = function (nodes, callback) {
45
47
  if (window.NodeList.prototype.forEach) {
46
48
  return nodes.forEach(callback)
47
49
  }
48
- for (var i = 0; i < nodes.length; i++) {
50
+ for (let i = 0; i < nodes.length; i++) {
49
51
  callback.call(window, nodes[i], i, nodes)
50
52
  }
51
- };
53
+ }
54
+
55
+ /**
56
+ * Find an elements next sibling
57
+ *
58
+ * Utility function to find an elements next sibling matching the provided
59
+ * selector.
60
+ *
61
+ * @param {HTMLElement} element - Element to find siblings for
62
+ * @param {string} selector - selector for required sibling
63
+ */
64
+ MOJFrontend.getNextSibling = function ($element, selector) {
65
+ if (!$element) return
66
+ // Get the next sibling element
67
+ let $sibling = $element.nextElementSibling
68
+
69
+ // If there's no selector, return the first sibling
70
+ if (!selector) return $sibling
71
+
72
+ // If the sibling matches our selector, use it
73
+ // If not, jump to the next sibling and continue the loop
74
+ while ($sibling) {
75
+ if ($sibling.matches(selector)) return $sibling
76
+ $sibling = $sibling.nextElementSibling
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Find an elements preceding sibling
82
+ *
83
+ * Utility function to find an elements previous sibling matching the provided
84
+ * selector.
85
+ *
86
+ * @param {HTMLElement} element - Element to find siblings for
87
+ * @param {string} selector - selector for required sibling
88
+ */
89
+ MOJFrontend.getPreviousSibling = function ($element, selector) {
90
+ if (!$element) return
91
+ // Get the previous sibling element
92
+ let $sibling = $element.previousElementSibling
93
+
94
+ // If there's no selector, return the first sibling
95
+ if (!selector) return $sibling
96
+
97
+ // If the sibling matches our selector, use it
98
+ // If not, jump to the next sibling and continue the loop
99
+ while ($sibling) {
100
+ if ($sibling.matches(selector)) return $sibling
101
+ $sibling = $sibling.previousElementSibling
102
+ }
103
+ }
104
+
105
+ MOJFrontend.findNearestMatchingElement = function ($element, selector) {
106
+ // If no element or selector is provided, return null
107
+ if (!$element) return
108
+ if (!selector) return
109
+
110
+ // Start with the current element
111
+ let $currentElement = $element
112
+
113
+ while ($currentElement) {
114
+ // First check the current element
115
+ if ($currentElement.matches(selector)) {
116
+ return $currentElement
117
+ }
118
+
119
+ // Check all previous siblings
120
+ let $sibling = $currentElement.previousElementSibling
121
+ while ($sibling) {
122
+ // Check if the sibling itself is a heading
123
+ if ($sibling.matches(selector)) {
124
+ return $sibling
125
+ }
126
+ $sibling = $sibling.previousElementSibling
127
+ }
128
+
129
+ // If no match found in siblings, move up to parent
130
+ $currentElement = $currentElement.parentElement
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Move focus to element
136
+ *
137
+ * Sets tabindex to -1 to make the element programmatically focusable,
138
+ * but removes it on blur as the element doesn't need to be focused again.
139
+ *
140
+ * @param {HTMLElement} $element - HTML element
141
+ * @param {object} [options] - Handler options
142
+ * @param {function(this: HTMLElement): void} [options.onBeforeFocus] - Callback before focus
143
+ * @param {function(this: HTMLElement): void} [options.onBlur] - Callback on blur
144
+ */
145
+ MOJFrontend.setFocus = function ($element, options = {}) {
146
+ const isFocusable = $element.getAttribute('tabindex')
147
+
148
+ if (!isFocusable) {
149
+ $element.setAttribute('tabindex', '-1')
150
+ }
151
+
152
+ /**
153
+ * Handle element focus
154
+ */
155
+ function onFocus() {
156
+ $element.addEventListener('blur', onBlur, { once: true })
157
+ }
158
+
159
+ /**
160
+ * Handle element blur
161
+ */
162
+ function onBlur() {
163
+ if (options.onBlur) {
164
+ options.onBlur.call($element)
165
+ }
166
+
167
+ if (!isFocusable) {
168
+ $element.removeAttribute('tabindex')
169
+ }
170
+ }
171
+
172
+ // Add listener to reset element on blur, after focus
173
+ $element.addEventListener('focus', onFocus, { once: true })
174
+
175
+ // Focus element
176
+ if (options.onBeforeFocus) {
177
+ options.onBeforeFocus.call($element)
178
+ }
179
+ $element.focus()
180
+ }
@@ -0,0 +1,235 @@
1
+ require('./helpers.js')
2
+
3
+ describe('helpers', () => {
4
+ describe('getNextSibling', () => {
5
+ beforeEach(() => {
6
+ const html = `
7
+ <div id="container">
8
+ <h1 id="title">Heading 1</h1>
9
+ <p>this is some text</p>
10
+ <ul id="list">
11
+ <li id="item-1" class="item">item 1</li>
12
+ <li id="item-2" class="item">item 2</li>
13
+ <li id="item-3" class="selected">item 3</li>
14
+ <li id="item-4" class="item">item 4</li>
15
+ </ul>
16
+ </div>`
17
+
18
+ document.body.insertAdjacentHTML('afterbegin', html)
19
+ })
20
+
21
+ afterEach(() => {
22
+ document.body.innerHTML = ''
23
+ })
24
+
25
+ test('returns undefined with no element', () => {
26
+ const result = MOJFrontend.getNextSibling()
27
+
28
+ expect(result).toBeUndefined()
29
+ })
30
+
31
+ test('returns null with no selector if no sibling', () => {
32
+ const element = document.querySelector('#item-4')
33
+ const result = MOJFrontend.getNextSibling(element)
34
+
35
+ expect(result).toBeNull()
36
+ })
37
+
38
+ test('returns first sibling with no selector', () => {
39
+ const element = document.querySelector('#item-2')
40
+ const expected = document.querySelector('#item-3')
41
+ const result = MOJFrontend.getNextSibling(element)
42
+
43
+ expect(result).toBe(expected)
44
+ })
45
+
46
+ test('returns undefined if no sibling matches selector', () => {
47
+ const element = document.querySelector('#item-1')
48
+ const result = MOJFrontend.getNextSibling(element, '#not-present')
49
+
50
+ expect(result).toBeUndefined()
51
+ })
52
+
53
+ test('returns matching sibling', () => {
54
+ const element = document.querySelector('#item-1')
55
+ const expected = document.querySelector('#item-3')
56
+ const result = MOJFrontend.getNextSibling(element, '.selected')
57
+
58
+ expect(result).toBe(expected)
59
+ })
60
+
61
+ test('returns first matching sibling', () => {
62
+ const element = document.querySelector('#item-1')
63
+ const expected = document.querySelector('#item-2')
64
+ const result = MOJFrontend.getNextSibling(element, '.item')
65
+
66
+ expect(result).toBe(expected)
67
+ })
68
+ })
69
+
70
+ describe('getPreviousSibling', () => {
71
+ beforeEach(() => {
72
+ const html = `
73
+ <div id="container">
74
+ <h1 id="title">Heading 1</h1>
75
+ <p>this is some text</p>
76
+ <ul id="list">
77
+ <li id="item-1" class="item">item 1</li>
78
+ <li id="item-2" class="item">item 2</li>
79
+ <li id="item-3" class="selected">item 3</li>
80
+ <li id="item-4" class="item">item 4</li>
81
+ </ul>
82
+ </div>`
83
+
84
+ document.body.insertAdjacentHTML('afterbegin', html)
85
+ })
86
+
87
+ afterEach(() => {
88
+ document.body.innerHTML = ''
89
+ })
90
+
91
+ test('returns undefined with no element', () => {
92
+ const result = MOJFrontend.getPreviousSibling()
93
+
94
+ expect(result).toBeUndefined()
95
+ })
96
+
97
+ test('returns undefined with no selector if no sibling', () => {
98
+ const element = document.querySelector('#item-1')
99
+ const result = MOJFrontend.getPreviousSibling(element)
100
+
101
+ expect(result).toBeNull()
102
+ })
103
+
104
+ test('returns first sibling with no selector', () => {
105
+ const element = document.querySelector('#item-3')
106
+ const expected = document.querySelector('#item-2')
107
+ const result = MOJFrontend.getPreviousSibling(element)
108
+
109
+ expect(result).toBe(expected)
110
+ })
111
+
112
+ test('returns undefined if no sibling matches selector', () => {
113
+ const element = document.querySelector('#item-4')
114
+ const result = MOJFrontend.getPreviousSibling(element, '#not-present')
115
+
116
+ expect(result).toBeUndefined()
117
+ })
118
+
119
+ test('returns matching sibling', () => {
120
+ const element = document.querySelector('#item-4')
121
+ const expected = document.querySelector('#item-3')
122
+ const result = MOJFrontend.getPreviousSibling(element, '.selected')
123
+
124
+ expect(result).toBe(expected)
125
+ })
126
+
127
+ test('returns first matching sibling', () => {
128
+ const element = document.querySelector('#item-4')
129
+ const expected = document.querySelector('#item-2')
130
+ const result = MOJFrontend.getPreviousSibling(element, '.item')
131
+
132
+ expect(result).toBe(expected)
133
+ })
134
+ })
135
+
136
+ describe('findNearestMatchingElement', () => {
137
+ beforeEach(() => {
138
+ const html = `
139
+ <div id="container">
140
+ <h1 id="title">Heading 1</h1>
141
+ <p>this is some text</p>
142
+ <ul id="list">
143
+ <li id="item-1" class="item">item 1</li>
144
+ <li id="item-2" class="item">item 2</li>
145
+ <li id="item-3" class="selected">item 3</li>
146
+ <li id="item-4" class="item">item 4</li>
147
+ </ul>
148
+ </div>`
149
+
150
+ document.body.insertAdjacentHTML('afterbegin', html)
151
+ })
152
+
153
+ afterEach(() => {
154
+ document.body.innerHTML = ''
155
+ })
156
+
157
+ test('returns undefined with no element', () => {
158
+ const result = MOJFrontend.findNearestMatchingElement()
159
+
160
+ expect(result).toBeUndefined()
161
+ })
162
+
163
+ test('returns undefined with no selector', () => {
164
+ const element = document.querySelector('#item-1')
165
+ const result = MOJFrontend.findNearestMatchingElement(element)
166
+
167
+ expect(result).toBeUndefined()
168
+ })
169
+
170
+ test('returns undefined if element not found', () => {
171
+ const element = document.querySelector('#item-1')
172
+ const result = MOJFrontend.findNearestMatchingElement(
173
+ element,
174
+ '#not-present'
175
+ )
176
+
177
+ expect(result).toBeUndefined()
178
+ })
179
+
180
+ test("doesn't find next siblings", () => {
181
+ const element = document.querySelector('#item-2')
182
+ const result = MOJFrontend.findNearestMatchingElement(element, '#item-3')
183
+
184
+ expect(result).toBeUndefined()
185
+ })
186
+
187
+ test('finds previous sibling', () => {
188
+ const element = document.querySelector('#item-2')
189
+ const expected = document.querySelector('#item-1')
190
+ const result = MOJFrontend.findNearestMatchingElement(element, '#item-1')
191
+
192
+ expect(result).toBe(expected)
193
+ })
194
+
195
+ test('finds first matching sibling', () => {
196
+ const element = document.querySelector('#item-3')
197
+ const expected = document.querySelector('#item-2')
198
+ const result = MOJFrontend.findNearestMatchingElement(element, '.item')
199
+
200
+ expect(result).toBe(expected)
201
+ })
202
+
203
+ test('returns element if matching', () => {
204
+ const element = document.querySelector('#item-3')
205
+ const expected = document.querySelector('#item-3')
206
+ const result = MOJFrontend.findNearestMatchingElement(element, 'li')
207
+
208
+ expect(result).toBe(expected)
209
+ })
210
+
211
+ test('if no sibling, finds matching ancestor', () => {
212
+ const element = document.querySelector('#item-3')
213
+ const expected = document.querySelector('#list')
214
+ const result = MOJFrontend.findNearestMatchingElement(element, 'ul')
215
+
216
+ expect(result).toBe(expected)
217
+ })
218
+
219
+ test('return an ancestor sibling if matched', () => {
220
+ const element = document.querySelector('#item-3')
221
+ const expected = document.querySelector('#title')
222
+ const result = MOJFrontend.findNearestMatchingElement(element, 'h1')
223
+
224
+ expect(result).toBe(expected)
225
+ })
226
+
227
+ test('continues traversing until match is found', () => {
228
+ const element = document.querySelector('#item-3')
229
+ const expected = document.querySelector('#container')
230
+ const result = MOJFrontend.findNearestMatchingElement(element, 'div')
231
+
232
+ expect(result).toBe(expected)
233
+ })
234
+ })
235
+ })