@ministryofjustice/frontend 2.2.5 → 3.0.1
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/moj/all.jquery.min.js +11 -3
- package/moj/all.js +327 -138
- package/moj/components/button-menu/_button-menu.scss +71 -109
- package/moj/components/button-menu/button-menu.js +292 -123
- package/moj/components/button-menu/button-menu.spec.js +363 -0
- package/moj/components/button-menu/template.njk +32 -6
- package/moj/components/date-picker/date-picker.js +29 -15
- package/moj/components/date-picker/date-picker.spec.js +571 -0
- package/moj/components/identity-bar/_identity-bar.scss +5 -9
- package/moj/components/identity-bar/template.njk +17 -22
- package/moj/components/page-header-actions/_page-header-actions.scss +9 -39
- package/moj/components/page-header-actions/template.njk +16 -16
- package/moj/objects/_all.scss +1 -0
- package/moj/objects/_button-group.scss +37 -0
- package/package.json +1 -1
|
@@ -1,95 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
========================================================================== */
|
|
1
|
+
$moj-button-hover-colour: #767676;
|
|
2
|
+
$moj-datepicker-mid-grey: #949494;
|
|
4
3
|
|
|
5
4
|
.moj-button-menu {
|
|
6
5
|
display: inline-block;
|
|
7
6
|
position: relative;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/* TOGGLE BUTTON */
|
|
11
7
|
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
margin-right: govuk-spacing(2);
|
|
15
|
-
margin-bottom: govuk-spacing(2);
|
|
16
|
-
width: auto; // Override GDS’s 100% width
|
|
17
|
-
|
|
18
|
-
&:last-child {
|
|
19
|
-
margin-right: 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
&:after {
|
|
23
|
-
background-repeat: no-repeat;
|
|
24
|
-
background-image: url(#{$moj-images-path}icon-arrow-white-down.svg);
|
|
25
|
-
content: '';
|
|
26
|
-
display: inline-block;
|
|
27
|
-
height: 5px;
|
|
28
|
-
margin-left: govuk-spacing(2);
|
|
29
|
-
width: 10px;
|
|
30
|
-
vertical-align: middle;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.moj-button-menu__toggle-button:focus {
|
|
35
|
-
&:after {
|
|
36
|
-
background-image: url(#{$moj-images-path}icon-arrow-black-down.svg);
|
|
8
|
+
> .govuk-button {
|
|
9
|
+
margin-bottom: 0;
|
|
37
10
|
}
|
|
38
11
|
}
|
|
39
12
|
|
|
40
|
-
.moj-button-menu__toggle-button
|
|
41
|
-
|
|
42
|
-
background-image: url(#{$moj-images-path}icon-arrow-black-up.svg);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.moj-button-menu__toggle-button:hover {
|
|
47
|
-
&:after {
|
|
48
|
-
background-image: url(#{$moj-images-path}icon-arrow-white-down.svg);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.moj-button-menu__toggle-button[aria-expanded="true"]:hover {
|
|
53
|
-
&:after {
|
|
54
|
-
background-image: url(#{$moj-images-path}icon-arrow-white-up.svg);
|
|
55
|
-
}
|
|
13
|
+
.moj-button-menu__toggle-button {
|
|
14
|
+
display: inline;
|
|
56
15
|
}
|
|
57
16
|
|
|
58
|
-
.moj-button-menu__toggle-button
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
17
|
+
.moj-button-menu__toggle-button span {
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 8px;
|
|
62
21
|
}
|
|
63
22
|
|
|
64
|
-
.moj-button-menu__toggle-button
|
|
65
|
-
|
|
66
|
-
margin-
|
|
67
|
-
&:after {
|
|
68
|
-
background-image: url(#{$moj-images-path}icon-arrow-black-down.svg);
|
|
69
|
-
}
|
|
23
|
+
.moj-button-menu__toggle-button svg {
|
|
24
|
+
transform: rotate(180deg);
|
|
25
|
+
margin-top: 2px;
|
|
70
26
|
}
|
|
71
27
|
|
|
72
|
-
.moj-button-menu__toggle-button
|
|
73
|
-
|
|
74
|
-
background-image: url(#{$moj-images-path}icon-arrow-black-up.svg);
|
|
75
|
-
}
|
|
28
|
+
.moj-button-menu__toggle-button[aria-expanded="true"] svg {
|
|
29
|
+
transform: rotate(0deg);
|
|
76
30
|
}
|
|
77
31
|
|
|
78
|
-
.moj-button-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
32
|
+
.moj-button-menu__wrapper {
|
|
33
|
+
list-style: none;
|
|
34
|
+
position: absolute;
|
|
35
|
+
margin: 0;
|
|
36
|
+
padding: 0;
|
|
37
|
+
width: 200px;
|
|
38
|
+
top: 43px; //38px button height, 2px shadow, 3px gap
|
|
39
|
+
z-index: 10;
|
|
83
40
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
background-image: url(#{$moj-images-path}icon-arrow-black-up.svg);
|
|
41
|
+
&--right {
|
|
42
|
+
right: 0;
|
|
87
43
|
}
|
|
88
44
|
}
|
|
89
45
|
|
|
90
|
-
|
|
91
|
-
/* MENU ITEM */
|
|
92
|
-
|
|
46
|
+
/* Menu items with no JS */
|
|
93
47
|
.moj-button-menu__item {
|
|
94
48
|
display: inline-block;
|
|
95
49
|
margin-right: govuk-spacing(2);
|
|
@@ -100,57 +54,65 @@
|
|
|
100
54
|
}
|
|
101
55
|
}
|
|
102
56
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
57
|
+
/* Menu items with JS */
|
|
58
|
+
.moj-button-menu li > .moj-button-menu__item {
|
|
59
|
+
$button-shadow-size: 0;
|
|
60
|
+
@include govuk-font($size: 19, $line-height: 19px);
|
|
61
|
+
|
|
107
62
|
box-sizing: border-box;
|
|
108
|
-
display: block;
|
|
63
|
+
display: inline-block;
|
|
64
|
+
position: relative;
|
|
65
|
+
width: 100%;
|
|
66
|
+
margin-top: 0;
|
|
67
|
+
margin-right: 0;
|
|
68
|
+
margin-left: 0;
|
|
109
69
|
margin-bottom: 0;
|
|
110
70
|
padding: govuk-spacing(2);
|
|
71
|
+
border: $govuk-border-width-form-element solid transparent;
|
|
72
|
+
border-radius: 0;
|
|
73
|
+
border-bottom: 1px solid $moj-datepicker-mid-grey;
|
|
74
|
+
color: $govuk-text-colour;
|
|
75
|
+
background-color: govuk-colour("light-grey");
|
|
111
76
|
text-align: left;
|
|
112
|
-
|
|
113
|
-
|
|
77
|
+
vertical-align: top;
|
|
78
|
+
cursor: pointer;
|
|
114
79
|
-webkit-appearance: none;
|
|
80
|
+
appearance: none;
|
|
115
81
|
|
|
116
82
|
&:link,
|
|
117
|
-
&:visited
|
|
83
|
+
&:visited,
|
|
84
|
+
&:active,
|
|
85
|
+
&:hover {
|
|
86
|
+
color: $govuk-text-colour;
|
|
118
87
|
text-decoration: none;
|
|
119
|
-
color: govuk-colour("black");
|
|
120
88
|
}
|
|
121
89
|
|
|
90
|
+
&:active,
|
|
122
91
|
&:hover {
|
|
123
|
-
|
|
92
|
+
color: govuk-colour("white");
|
|
124
93
|
}
|
|
125
94
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* MENU WRAPPER */
|
|
135
|
-
|
|
136
|
-
.moj-button-menu__wrapper {
|
|
137
|
-
font-size: 0; /* Hide whitespace between elements */
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.moj-button-menu__wrapper--right {
|
|
141
|
-
right: 0;
|
|
142
|
-
}
|
|
95
|
+
// Fix unwanted button padding in Firefox
|
|
96
|
+
&::-moz-focus-inner {
|
|
97
|
+
padding: 0;
|
|
98
|
+
border: 0;
|
|
99
|
+
}
|
|
143
100
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
z-index: 10;
|
|
148
|
-
}
|
|
101
|
+
&:hover {
|
|
102
|
+
background-color: $moj-button-hover-colour;
|
|
103
|
+
}
|
|
149
104
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
105
|
+
&:focus {
|
|
106
|
+
border-color: $govuk-focus-colour;
|
|
107
|
+
outline: $govuk-focus-width solid transparent;
|
|
108
|
+
box-shadow: inset 0 0 0 1px $govuk-focus-colour;
|
|
109
|
+
z-index: 10;
|
|
110
|
+
}
|
|
153
111
|
|
|
154
|
-
|
|
155
|
-
|
|
112
|
+
&:focus:not(:active):not(:hover) {
|
|
113
|
+
border-color: $govuk-focus-colour;
|
|
114
|
+
color: $govuk-focus-text-colour;
|
|
115
|
+
background-color: $govuk-focus-colour;
|
|
116
|
+
box-shadow: 0 2px 0 $govuk-focus-text-colour;
|
|
117
|
+
}
|
|
156
118
|
}
|
|
@@ -1,155 +1,324 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
this.menu.attr('role', 'menu');
|
|
8
|
-
this.mq = params.mq;
|
|
9
|
-
this.buttonText = params.buttonText;
|
|
10
|
-
this.buttonClasses = params.buttonClasses || '';
|
|
11
|
-
this.keys = { esc: 27, up: 38, down: 40, tab: 9 };
|
|
12
|
-
this.menu.on('keydown', '[role=menuitem]', $.proxy(this, 'onButtonKeydown'));
|
|
13
|
-
this.createToggleButton();
|
|
14
|
-
this.setupResponsiveChecks();
|
|
15
|
-
$(document).on('click', $.proxy(this, 'onDocumentClick'));
|
|
16
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ButtonMenuConfig
|
|
3
|
+
* @property {string} [buttonText=Actions] - Label for the toggle button
|
|
4
|
+
* @property {"left" | "right"} [alignMenu=left] - the alignment of the menu
|
|
5
|
+
* @property {string} [buttonClasses=govuk-button--secondary] - css classes applied to the toggle button
|
|
6
|
+
*/
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {HTMLElement} $module
|
|
10
|
+
* @param {ButtonMenuConfig} config
|
|
11
|
+
* @constructor
|
|
12
|
+
*/
|
|
13
|
+
MOJFrontend.ButtonMenu = function ($module, config = {}) {
|
|
14
|
+
if (!$module) {
|
|
15
|
+
return this;
|
|
21
16
|
}
|
|
22
|
-
};
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
const schema = Object.freeze({
|
|
19
|
+
properties: {
|
|
20
|
+
buttonText: { type: "string" },
|
|
21
|
+
buttonClasses: { type: "string" },
|
|
22
|
+
alignMenu: { type: "string" },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const defaults = {
|
|
27
|
+
buttonText: "Actions",
|
|
28
|
+
alignMenu: "left",
|
|
29
|
+
buttonClasses: "",
|
|
30
|
+
};
|
|
31
|
+
// data attributes override JS config, which overrides defaults
|
|
32
|
+
this.config = this.mergeConfigs(
|
|
33
|
+
defaults,
|
|
34
|
+
config,
|
|
35
|
+
this.parseDataset(schema, $module.dataset),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
this.$module = $module;
|
|
28
39
|
};
|
|
29
40
|
|
|
30
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
MOJFrontend.ButtonMenu.prototype.init = function () {
|
|
42
|
+
// If only one button is provided, don't initiate a menu and toggle button
|
|
43
|
+
// if classes have been provided for the toggleButton, apply them to the single item
|
|
44
|
+
if (this.$module.children.length == 1) {
|
|
45
|
+
const button = this.$module.children[0];
|
|
46
|
+
button.classList.forEach((className) => {
|
|
47
|
+
if (className.startsWith("govuk-button-")) {
|
|
48
|
+
button.classList.remove(className);
|
|
49
|
+
}
|
|
50
|
+
button.classList.remove("moj-button-menu__item")
|
|
51
|
+
});
|
|
52
|
+
if (this.config.buttonClasses) {
|
|
53
|
+
button.classList.add(...this.config.buttonClasses.split(" "));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Otherwise intialise a button menu
|
|
57
|
+
if (this.$module.children.length > 1) {
|
|
58
|
+
this.initMenu();
|
|
59
|
+
}
|
|
34
60
|
};
|
|
35
61
|
|
|
36
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
MOJFrontend.ButtonMenu.prototype.initMenu = function () {
|
|
63
|
+
this.$menu = this.createMenu();
|
|
64
|
+
this.$module.insertAdjacentHTML("afterbegin", this.toggleTemplate());
|
|
65
|
+
this.setupMenuItems();
|
|
66
|
+
|
|
67
|
+
this.$menuToggle = this.$module.querySelector(":scope > button");
|
|
68
|
+
this.items = this.$menu.querySelectorAll("a, button");
|
|
69
|
+
|
|
70
|
+
this.$menuToggle.addEventListener("click", (event) => {
|
|
71
|
+
this.toggleMenu(event);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.$module.addEventListener("keydown", (event) => {
|
|
75
|
+
this.handleKeyDown(event);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
document.addEventListener("click", (event) => {
|
|
79
|
+
if (!this.$module.contains(event.target)) {
|
|
80
|
+
this.closeMenu(false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
42
83
|
};
|
|
43
84
|
|
|
44
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
MOJFrontend.ButtonMenu.prototype.createMenu = function () {
|
|
86
|
+
const $menu = document.createElement("ul");
|
|
87
|
+
$menu.setAttribute("role", "list");
|
|
88
|
+
$menu.hidden = true;
|
|
89
|
+
$menu.classList.add("moj-button-menu__wrapper");
|
|
90
|
+
if (this.config.alignMenu == "right") {
|
|
91
|
+
$menu.classList.add("moj-button-menu__wrapper--right");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.$module.appendChild($menu);
|
|
95
|
+
while (this.$module.firstChild !== $menu) {
|
|
96
|
+
$menu.appendChild(this.$module.firstChild);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return $menu;
|
|
50
100
|
};
|
|
51
101
|
|
|
52
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
102
|
+
MOJFrontend.ButtonMenu.prototype.setupMenuItems = function () {
|
|
103
|
+
Array.from(this.$menu.children).forEach((item) => {
|
|
104
|
+
// wrap item in li tag
|
|
105
|
+
const listItem = document.createElement("li");
|
|
106
|
+
this.$menu.insertBefore(listItem, item);
|
|
107
|
+
listItem.appendChild(item);
|
|
108
|
+
|
|
109
|
+
item.setAttribute("tabindex", -1);
|
|
110
|
+
|
|
111
|
+
if (item.tagName == "BUTTON") {
|
|
112
|
+
item.setAttribute("type", "button");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
item.classList.forEach((className) => {
|
|
116
|
+
if (className.startsWith("govuk-button")) {
|
|
117
|
+
item.classList.remove(className);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// add a slight delay after click before closing the menu, makes it *feel* better
|
|
122
|
+
item.addEventListener("click", (event) => {
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
this.closeMenu(false);
|
|
125
|
+
}, 50);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
58
128
|
};
|
|
59
129
|
|
|
60
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
$(el).removeClass('govuk-button');
|
|
71
|
-
});
|
|
130
|
+
MOJFrontend.ButtonMenu.prototype.toggleTemplate = function () {
|
|
131
|
+
return `
|
|
132
|
+
<button type="button" class="govuk-button moj-button-menu__toggle-button ${this.config.buttonClasses || ""}" aria-haspopup="true" aria-expanded="false">
|
|
133
|
+
<span>
|
|
134
|
+
${this.config.buttonText}
|
|
135
|
+
<svg width="11" height="5" viewBox="0 0 11 5" xmlns="http://www.w3.org/2000/svg">
|
|
136
|
+
<path d="M5.5 0L11 5L0 5L5.5 0Z" fill="currentColor"/>
|
|
137
|
+
</svg>
|
|
138
|
+
</span>
|
|
139
|
+
</button>`;
|
|
72
140
|
};
|
|
73
141
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if($(el).attr('data-warning') == 'true') {
|
|
80
|
-
$(el).addClass('govuk-button--warning');
|
|
81
|
-
}
|
|
82
|
-
$(el).addClass('govuk-button');
|
|
83
|
-
});
|
|
142
|
+
/**
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
MOJFrontend.ButtonMenu.prototype.isOpen = function () {
|
|
146
|
+
return this.$menuToggle.getAttribute("aria-expanded") === "true";
|
|
84
147
|
};
|
|
85
148
|
|
|
86
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
87
|
-
|
|
149
|
+
MOJFrontend.ButtonMenu.prototype.toggleMenu = function (event) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
|
|
152
|
+
// If menu is triggered with mouse don't move focus to first item
|
|
153
|
+
const keyboardEvent = event.detail == 0;
|
|
154
|
+
const focusIndex = keyboardEvent ? 0 : -1;
|
|
155
|
+
|
|
156
|
+
if (this.isOpen()) {
|
|
157
|
+
this.closeMenu();
|
|
158
|
+
} else {
|
|
159
|
+
this.openMenu(focusIndex);
|
|
160
|
+
}
|
|
88
161
|
};
|
|
89
162
|
|
|
90
|
-
|
|
91
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Opens the menu and optionally sets the focus to the item with given index
|
|
165
|
+
*
|
|
166
|
+
* @param {number} focusIndex - The index of the item to focus
|
|
167
|
+
*/
|
|
168
|
+
MOJFrontend.ButtonMenu.prototype.openMenu = function (focusIndex = 0) {
|
|
169
|
+
this.$menu.hidden = false;
|
|
170
|
+
this.$menuToggle.setAttribute("aria-expanded", "true");
|
|
171
|
+
if (focusIndex !== -1) {
|
|
172
|
+
this.focusItem(focusIndex);
|
|
173
|
+
}
|
|
92
174
|
};
|
|
93
175
|
|
|
94
|
-
|
|
95
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Closes the menu and optionally returns focus back to menuToggle
|
|
178
|
+
*
|
|
179
|
+
* @param {boolean} moveFocus - whether to return focus to the toggle button
|
|
180
|
+
*/
|
|
181
|
+
MOJFrontend.ButtonMenu.prototype.closeMenu = function (moveFocus = true) {
|
|
182
|
+
this.$menu.hidden = true;
|
|
183
|
+
this.$menuToggle.setAttribute("aria-expanded", "false");
|
|
184
|
+
if (moveFocus) {
|
|
185
|
+
this.$menuToggle.focus();
|
|
186
|
+
}
|
|
96
187
|
};
|
|
97
188
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Focuses the menu item at the specified index
|
|
191
|
+
*
|
|
192
|
+
* @param {number} index - the index of the item to focus
|
|
193
|
+
*/
|
|
194
|
+
MOJFrontend.ButtonMenu.prototype.focusItem = function (index) {
|
|
195
|
+
if (index >= this.items.length) index = 0;
|
|
196
|
+
if (index < 0) index = this.items.length - 1;
|
|
197
|
+
|
|
198
|
+
this.items.item(index)?.focus();
|
|
106
199
|
};
|
|
107
200
|
|
|
108
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
201
|
+
MOJFrontend.ButtonMenu.prototype.currentFocusIndex = function () {
|
|
202
|
+
const activeElement = document.activeElement;
|
|
203
|
+
const menuItems = Array.from(this.items);
|
|
204
|
+
|
|
205
|
+
return menuItems.indexOf(activeElement);
|
|
114
206
|
};
|
|
115
207
|
|
|
116
|
-
MOJFrontend.ButtonMenu.prototype.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
208
|
+
MOJFrontend.ButtonMenu.prototype.handleKeyDown = function (event) {
|
|
209
|
+
if (event.target == this.$menuToggle) {
|
|
210
|
+
switch (event.key) {
|
|
211
|
+
case "ArrowDown":
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
this.openMenu();
|
|
214
|
+
break;
|
|
215
|
+
case "ArrowUp":
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
this.openMenu(this.items.length - 1);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.$menu.contains(event.target) && this.isOpen()) {
|
|
223
|
+
switch (event.key) {
|
|
224
|
+
case "ArrowDown":
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
if (this.currentFocusIndex() !== -1) {
|
|
227
|
+
this.focusItem(this.currentFocusIndex() + 1);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
case "ArrowUp":
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
if (this.currentFocusIndex() !== -1) {
|
|
233
|
+
this.focusItem(this.currentFocusIndex() - 1);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
case "Home":
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
this.focusItem(0);
|
|
239
|
+
break;
|
|
240
|
+
case "End":
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
this.focusItem(this.items.length - 1);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (event.key == "Escape" && this.isOpen()) {
|
|
248
|
+
this.closeMenu();
|
|
249
|
+
}
|
|
250
|
+
if (event.key == "Tab" && this.isOpen()) {
|
|
251
|
+
this.closeMenu(false);
|
|
252
|
+
}
|
|
137
253
|
};
|
|
138
254
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Parse dataset
|
|
257
|
+
*
|
|
258
|
+
* Loop over an object and normalise each value using {@link normaliseString},
|
|
259
|
+
* optionally expanding nested `i18n.field`
|
|
260
|
+
*
|
|
261
|
+
* @param {{ schema: Schema }} Component - Component class
|
|
262
|
+
* @param {DOMStringMap} dataset - HTML element dataset
|
|
263
|
+
* @returns {Object} Normalised dataset
|
|
264
|
+
*/
|
|
265
|
+
MOJFrontend.ButtonMenu.prototype.parseDataset = function (schema, dataset) {
|
|
266
|
+
const parsed = {};
|
|
267
|
+
|
|
268
|
+
for (const [field, attributes] of Object.entries(schema.properties)) {
|
|
269
|
+
if (field in dataset) {
|
|
270
|
+
if (dataset[field]) {
|
|
271
|
+
parsed[field] = dataset[field];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return parsed;
|
|
146
277
|
};
|
|
147
278
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Config merging function
|
|
281
|
+
*
|
|
282
|
+
* Takes any number of objects and combines them together, with
|
|
283
|
+
* greatest priority on the LAST item passed in.
|
|
284
|
+
*
|
|
285
|
+
* @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
|
|
286
|
+
* @returns {{ [key: string]: unknown }} A merged config object
|
|
287
|
+
*/
|
|
288
|
+
MOJFrontend.ButtonMenu.prototype.mergeConfigs = function (...configObjects) {
|
|
289
|
+
const formattedConfigObject = {};
|
|
290
|
+
|
|
291
|
+
// Loop through each of the passed objects
|
|
292
|
+
for (const configObject of configObjects) {
|
|
293
|
+
for (const key of Object.keys(configObject)) {
|
|
294
|
+
const option = formattedConfigObject[key];
|
|
295
|
+
const override = configObject[key];
|
|
296
|
+
|
|
297
|
+
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
298
|
+
// keys with object values will be merged, otherwise the new value will
|
|
299
|
+
// override the existing value.
|
|
300
|
+
if (typeof option === "object" && typeof override === "object") {
|
|
301
|
+
// @ts-expect-error Index signature for type 'string' is missing
|
|
302
|
+
formattedConfigObject[key] = this.mergeConfigs(option, override);
|
|
303
|
+
} else {
|
|
304
|
+
formattedConfigObject[key] = override;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return formattedConfigObject;
|
|
155
310
|
};
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Schema for component config
|
|
314
|
+
*
|
|
315
|
+
* @typedef {object} Schema
|
|
316
|
+
* @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
|
|
317
|
+
*/
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Schema property for component config
|
|
321
|
+
*
|
|
322
|
+
* @typedef {object} SchemaProperty
|
|
323
|
+
* @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
|
|
324
|
+
*/
|