@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.
- package/README.md +4 -10
- package/govuk-prototype-kit.config.json +5 -16
- package/moj/all.jquery.min.js +77 -3
- package/moj/all.js +2022 -1444
- package/moj/all.scss +2 -0
- package/moj/all.spec.js +15 -13
- package/moj/components/_all.scss +1 -0
- package/moj/components/action-bar/_action-bar.scss +4 -6
- package/moj/components/add-another/_add-another.scss +9 -7
- package/moj/components/add-another/add-another.js +90 -69
- package/moj/components/add-another/add-another.spec.js +165 -0
- package/moj/components/alert/README.md +0 -0
- package/moj/components/alert/_alert.scss +142 -0
- package/moj/components/alert/alert.js +247 -0
- package/moj/components/alert/alert.spec.helper.js +67 -0
- package/moj/components/alert/alert.spec.js +229 -0
- package/moj/components/alert/macro.njk +3 -0
- package/moj/components/alert/template.njk +83 -0
- package/moj/components/badge/_badge.scss +3 -4
- package/moj/components/banner/_banner.scss +5 -10
- package/moj/components/button-menu/_button-menu.scss +10 -9
- package/moj/components/button-menu/button-menu.js +139 -136
- package/moj/components/button-menu/button-menu.spec.js +295 -296
- package/moj/components/cookie-banner/_cookie-banner.scss +6 -5
- package/moj/components/currency-input/_currency-input.scss +4 -4
- package/moj/components/date-picker/README.md +14 -17
- package/moj/components/date-picker/_date-picker.scss +122 -106
- package/moj/components/date-picker/date-picker.js +473 -471
- package/moj/components/date-picker/date-picker.spec.js +962 -914
- package/moj/components/filter/README.md +1 -1
- package/moj/components/filter/_filter.scss +53 -75
- package/moj/components/filter-toggle-button/filter-toggle-button.js +71 -67
- package/moj/components/filter-toggle-button/filter-toggle-button.spec.js +203 -205
- package/moj/components/form-validator/form-validator.js +117 -109
- package/moj/components/header/_header.scss +17 -19
- package/moj/components/identity-bar/_identity-bar.scss +5 -5
- package/moj/components/interruption-card/_interruption-card.scss +9 -2
- package/moj/components/messages/_messages.scss +12 -19
- package/moj/components/multi-file-upload/README.md +1 -1
- package/moj/components/multi-file-upload/_multi-file-upload.scss +34 -30
- package/moj/components/multi-file-upload/multi-file-upload.js +188 -152
- package/moj/components/multi-file-upload/multi-file-upload.spec.js +510 -0
- package/moj/components/multi-select/_multi-select.scss +4 -3
- package/moj/components/multi-select/multi-select.js +55 -50
- package/moj/components/multi-select/multi-select.spec.js +128 -0
- package/moj/components/notification-badge/_notification-badge.scss +12 -12
- package/moj/components/organisation-switcher/_organisation-switcher.scss +1 -1
- package/moj/components/page-header-actions/_page-header-actions.scss +3 -2
- package/moj/components/pagination/_pagination.scss +26 -31
- package/moj/components/password-reveal/_password-reveal.scss +1 -2
- package/moj/components/password-reveal/password-reveal.js +22 -21
- package/moj/components/password-reveal/password-reveal.spec.js +39 -37
- package/moj/components/primary-navigation/_primary-navigation.scss +26 -29
- package/moj/components/progress-bar/_progress-bar.scss +21 -26
- package/moj/components/rich-text-editor/_rich-text-editor.scss +17 -16
- package/moj/components/rich-text-editor/rich-text-editor.js +117 -103
- package/moj/components/search/_search.scss +6 -4
- package/moj/components/search-toggle/search-toggle.js +29 -30
- package/moj/components/search-toggle/search-toggle.scss +21 -15
- package/moj/components/search-toggle/search-toggle.spec.js +129 -0
- package/moj/components/side-navigation/_side-navigation.scss +12 -21
- package/moj/components/sortable-table/_sortable-table.scss +25 -23
- package/moj/components/sortable-table/sortable-table.js +139 -117
- package/moj/components/sortable-table/sortable-table.spec.js +362 -0
- package/moj/components/sub-navigation/_sub-navigation.scss +24 -28
- package/moj/components/tag/_tag.scss +8 -9
- package/moj/components/task-list/_task-list.scss +8 -7
- package/moj/components/ticket-panel/_ticket-panel.scss +14 -6
- package/moj/components/timeline/_timeline.scss +18 -20
- package/moj/filters/all.js +28 -30
- package/moj/filters/prototype-kit-13-filters.js +2 -1
- package/moj/helpers/_all.scss +1 -0
- package/moj/helpers/_hidden.scss +1 -1
- package/moj/helpers/_links.scss +20 -0
- package/moj/helpers.js +160 -31
- package/moj/helpers.spec.js +235 -0
- package/moj/init.js +2 -2
- package/moj/moj-frontend.min.css +2 -2
- package/moj/moj-frontend.min.js +77 -3
- package/moj/namespace.js +2 -1
- package/moj/objects/_filter-layout.scss +11 -10
- package/moj/objects/_scrollable-pane.scss +11 -14
- package/moj/settings/_colours.scss +5 -0
- package/moj/settings/_measurements.scss +0 -2
- package/moj/utilities/_hidden.scss +3 -3
- package/moj/utilities/_width-container.scss +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
background-size: 20px 16px;
|
|
105
103
|
|
|
106
104
|
&:focus {
|
|
107
105
|
color: govuk-colour("black"); // Focus colour on yellow should really be black.
|
package/moj/filters/all.js
CHANGED
|
@@ -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
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* @type {object}
|
|
9
10
|
*/
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
})
|
package/moj/helpers/_all.scss
CHANGED
package/moj/helpers/_hidden.scss
CHANGED
|
@@ -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
|
-
|
|
1
|
+
MOJFrontend.removeAttributeValue = function (el, attr, value) {
|
|
2
|
+
let re, m
|
|
3
3
|
if (el.getAttribute(attr)) {
|
|
4
|
-
if (el.getAttribute(attr)
|
|
5
|
-
el.removeAttribute(attr)
|
|
4
|
+
if (el.getAttribute(attr) === value) {
|
|
5
|
+
el.removeAttribute(attr)
|
|
6
6
|
} else {
|
|
7
|
-
re = new RegExp(
|
|
8
|
-
m = el.getAttribute(attr).match(re)
|
|
9
|
-
if (m && m.length
|
|
10
|
-
el.setAttribute(
|
|
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
|
-
|
|
19
|
+
MOJFrontend.addAttributeValue = function (el, attr, value) {
|
|
20
|
+
let re
|
|
18
21
|
if (!el.getAttribute(attr)) {
|
|
19
|
-
el.setAttribute(attr, value)
|
|
20
|
-
}
|
|
21
|
-
|
|
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)
|
|
26
|
+
el.setAttribute(attr, `${el.getAttribute(attr)} ${value}`)
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
}
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
MOJFrontend.dragAndDropSupported = function() {
|
|
30
|
-
|
|
31
|
-
return typeof div.ondrop
|
|
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
|
|
36
|
-
}
|
|
36
|
+
MOJFrontend.formDataSupported = function () {
|
|
37
|
+
return typeof FormData === 'function'
|
|
38
|
+
}
|
|
37
39
|
|
|
38
|
-
MOJFrontend.fileApiSupported = function() {
|
|
39
|
-
|
|
40
|
-
input.type = 'file'
|
|
41
|
-
return typeof input.files
|
|
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 (
|
|
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
|
+
})
|