decidim-core 0.30.6 → 0.30.8
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.
- checksums.yaml +4 -4
- data/app/cells/decidim/content_blocks/highlighted_elements_with_cell_for_list_cell.rb +5 -1
- data/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb +2 -2
- data/app/cells/decidim/report_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_button/flag_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/flag_modal.erb +1 -1
- data/app/cells/decidim/statistic/show.erb +4 -4
- data/app/cells/decidim/upload_modal/files.erb +9 -5
- data/app/cells/decidim/upload_modal_cell.rb +14 -1
- data/app/commands/decidim/multiple_attachments_methods.rb +20 -3
- data/app/helpers/decidim/mailer_helper.rb +36 -0
- data/app/helpers/decidim/menu_helper.rb +2 -1
- data/app/helpers/decidim/newsletters_helper.rb +4 -22
- data/app/jobs/decidim/find_and_update_descendants_job.rb +8 -2
- data/app/jobs/decidim/update_search_indexes_job.rb +2 -2
- data/app/mailers/decidim/application_mailer.rb +4 -0
- data/app/packs/src/decidim/a11y.js +29 -0
- data/app/packs/src/decidim/a11y.test.js +81 -0
- data/app/packs/src/decidim/confirm.js +8 -1
- data/app/packs/src/decidim/confirm.test.js +225 -0
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +26 -0
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -1
- data/app/packs/src/decidim/datepicker/generate_timepicker.js +9 -1
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
- data/app/packs/src/decidim/direct_uploads/upload_field.js +1 -1
- data/app/packs/src/decidim/focus_guard.js +4 -4
- data/app/packs/stylesheets/decidim/_cards.scss +12 -4
- data/app/packs/stylesheets/decidim/_flash.scss +1 -1
- data/app/packs/stylesheets/decidim/_modal_update.scss +1 -1
- data/app/presenters/decidim/menu_item_presenter.rb +7 -1
- data/app/views/decidim/devise/invitations/edit.html.erb +3 -3
- data/app/views/decidim/devise/registrations/new.html.erb +1 -0
- data/app/views/decidim/devise/shared/_tos_fields.html.erb +3 -3
- data/app/views/decidim/notification_mailer/event_received.html.erb +3 -3
- data/config/initializers/devise.rb +6 -0
- data/config/locales/ar.yml +3 -3
- data/config/locales/bg.yml +0 -4
- data/config/locales/ca-IT.yml +7 -6
- data/config/locales/ca.yml +7 -6
- data/config/locales/cs.yml +8 -8
- data/config/locales/de.yml +17 -8
- data/config/locales/el.yml +0 -2
- data/config/locales/en.yml +5 -4
- data/config/locales/es-MX.yml +8 -7
- data/config/locales/es-PY.yml +8 -7
- data/config/locales/es.yml +10 -9
- data/config/locales/eu.yml +8 -6
- data/config/locales/fi-plain.yml +10 -4
- data/config/locales/fi.yml +11 -5
- data/config/locales/fr-CA.yml +7 -5
- data/config/locales/fr.yml +8 -7
- data/config/locales/gl.yml +0 -2
- data/config/locales/hu.yml +5 -9
- data/config/locales/id-ID.yml +0 -2
- data/config/locales/it.yml +1 -3
- data/config/locales/ja.yml +7 -8
- data/config/locales/lb.yml +0 -2
- data/config/locales/lt.yml +1 -3
- data/config/locales/lv.yml +0 -2
- data/config/locales/nl.yml +0 -2
- data/config/locales/no.yml +0 -2
- data/config/locales/pl.yml +0 -4
- data/config/locales/pt-BR.yml +4 -5
- data/config/locales/pt.yml +0 -2
- data/config/locales/ro-RO.yml +1 -5
- data/config/locales/ru.yml +0 -2
- data/config/locales/sk.yml +0 -4
- data/config/locales/sv.yml +8 -7
- data/config/locales/tr-TR.yml +17 -5
- data/config/locales/zh-CN.yml +0 -2
- data/config/locales/zh-TW.yml +1 -3
- data/lib/decidim/assets/tailwind/tailwind.config.js.erb +1 -1
- data/lib/decidim/content_parsers/blob_parser.rb +2 -2
- data/lib/decidim/content_renderers/blob_renderer.rb +2 -2
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/form_builder.rb +25 -1
- data/lib/decidim/maintenance/taxonomy_importer.rb +1 -1
- data/lib/decidim/searchable.rb +4 -4
- metadata +10 -6
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
jest.mock("src/decidim/icon", () => () => "<svg></svg>");
|
|
4
|
+
|
|
5
|
+
describe("Confirm dialog for button[type='button']", () => {
|
|
6
|
+
let mockRails = null;
|
|
7
|
+
let mockDecidim = null;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
document.body.innerHTML = `
|
|
12
|
+
<div id="confirm-modal" style="display: none;">
|
|
13
|
+
<div data-dialog-title></div>
|
|
14
|
+
<div class="confirm-modal-icon"></div>
|
|
15
|
+
<div data-confirm-modal-content></div>
|
|
16
|
+
<button data-confirm-ok>Confirm</button>
|
|
17
|
+
<button data-confirm-cancel>Cancel</button>
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
mockRails = {
|
|
22
|
+
linkClickSelector: "a[data-confirm]",
|
|
23
|
+
buttonClickSelector: "button[data-confirm]:not([form])",
|
|
24
|
+
formInputClickSelector: 'form button[type="submit"], form button:not([type])',
|
|
25
|
+
inputChangeSelector: "input[data-confirm], select[data-confirm]",
|
|
26
|
+
formSubmitSelector: "form[data-confirm]",
|
|
27
|
+
stopEverything: jest.fn(),
|
|
28
|
+
fire: jest.fn((el, event) => {
|
|
29
|
+
const evt = new CustomEvent(event);
|
|
30
|
+
el.dispatchEvent(evt);
|
|
31
|
+
return true;
|
|
32
|
+
}),
|
|
33
|
+
matches: function(element, selector) {
|
|
34
|
+
if (element instanceof Element) {
|
|
35
|
+
return element.matches(selector);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
mockDecidim = {
|
|
42
|
+
currentDialogs: {
|
|
43
|
+
"confirm-modal": {
|
|
44
|
+
open: jest.fn(),
|
|
45
|
+
close: jest.fn()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
window.Rails = mockRails;
|
|
51
|
+
window.Decidim = mockDecidim;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
document.body.innerHTML = "";
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("selector matching for button[type='button'] with data-confirm", () => {
|
|
59
|
+
it("matches button[data-confirm][type='button'] selector", () => {
|
|
60
|
+
const button = document.createElement("button");
|
|
61
|
+
button.type = "button";
|
|
62
|
+
button.setAttribute("data-confirm", "Are you sure?");
|
|
63
|
+
|
|
64
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("matches form button[data-confirm] selector", () => {
|
|
68
|
+
const form = document.createElement("form");
|
|
69
|
+
const button = document.createElement("button");
|
|
70
|
+
button.setAttribute("data-confirm", "Are you sure?");
|
|
71
|
+
form.appendChild(button);
|
|
72
|
+
|
|
73
|
+
expect(button.matches("form button[data-confirm]")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not match regular button without data-confirm", () => {
|
|
77
|
+
const button = document.createElement("button");
|
|
78
|
+
button.type = "button";
|
|
79
|
+
|
|
80
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not match button[type='submit'] with the type='button' selector", () => {
|
|
84
|
+
const button = document.createElement("button");
|
|
85
|
+
button.type = "submit";
|
|
86
|
+
button.setAttribute("data-confirm", "Are you sure?");
|
|
87
|
+
|
|
88
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("matches button[type='button'] inside form", () => {
|
|
92
|
+
document.body.innerHTML = `
|
|
93
|
+
<form>
|
|
94
|
+
<button type="button" data-confirm="Test message">Click me</button>
|
|
95
|
+
</form>
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const button = document.querySelector('button[type="button"]');
|
|
99
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(true);
|
|
100
|
+
expect(button.matches("form button[data-confirm]")).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not match button[type='button'] without data-confirm inside form", () => {
|
|
104
|
+
document.body.innerHTML = `
|
|
105
|
+
<form>
|
|
106
|
+
<button type="button">Click me</button>
|
|
107
|
+
</form>
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const button = document.querySelector('button[type="button"]');
|
|
111
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(false);
|
|
112
|
+
expect(button.matches("form button[data-confirm]")).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("initializeConfirm - selectors registration", () => {
|
|
117
|
+
it("adds click event listener with proper selectors including button[type='button'] support", async () => {
|
|
118
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
119
|
+
|
|
120
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
121
|
+
|
|
122
|
+
initializeConfirm();
|
|
123
|
+
|
|
124
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith("click", expect.any(Function));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("adds change event listener for input change selector", async () => {
|
|
128
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
129
|
+
|
|
130
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
131
|
+
|
|
132
|
+
initializeConfirm();
|
|
133
|
+
|
|
134
|
+
const changeHandlerCalls = addEventListenerSpy.mock.calls.filter(
|
|
135
|
+
(call) => call[0] === "change"
|
|
136
|
+
);
|
|
137
|
+
expect(changeHandlerCalls.length).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("adds submit event listener for form submit selector", async () => {
|
|
141
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
142
|
+
|
|
143
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
144
|
+
|
|
145
|
+
initializeConfirm();
|
|
146
|
+
|
|
147
|
+
const submitHandlerCalls = addEventListenerSpy.mock.calls.filter(
|
|
148
|
+
(call) => call[0] === "submit"
|
|
149
|
+
);
|
|
150
|
+
expect(submitHandlerCalls.length).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("adds DOMContentLoaded event listener for Foundation Abide compatibility", async () => {
|
|
154
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
155
|
+
|
|
156
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
157
|
+
|
|
158
|
+
initializeConfirm();
|
|
159
|
+
|
|
160
|
+
const turboLoadCalls = addEventListenerSpy.mock.calls.filter(
|
|
161
|
+
(call) => call[0] === "DOMContentLoaded"
|
|
162
|
+
);
|
|
163
|
+
expect(turboLoadCalls.length).toBeGreaterThan(0);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("handleDocumentEvent with button[type='button'] support", () => {
|
|
168
|
+
it("handles click on button[type='button'] with data-confirm and form attribute", async () => {
|
|
169
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
170
|
+
|
|
171
|
+
document.body.innerHTML = `
|
|
172
|
+
<form id="my-form">
|
|
173
|
+
<button type="button" form="my-form" data-confirm="Are you sure?">Click me</button>
|
|
174
|
+
</form>
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const button = document.querySelector('button[type="button"]');
|
|
178
|
+
const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open");
|
|
179
|
+
|
|
180
|
+
initializeConfirm();
|
|
181
|
+
|
|
182
|
+
button.click();
|
|
183
|
+
|
|
184
|
+
expect(openSpy).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("handles click on button[type='button'] with data-confirm outside form", async () => {
|
|
188
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
189
|
+
|
|
190
|
+
document.body.innerHTML = `
|
|
191
|
+
<button type="button" data-confirm="Are you sure?">Click me</button>
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const button = document.querySelector('button[type="button"]');
|
|
195
|
+
const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open");
|
|
196
|
+
|
|
197
|
+
initializeConfirm();
|
|
198
|
+
|
|
199
|
+
button.click();
|
|
200
|
+
|
|
201
|
+
expect(openSpy).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("does not trigger confirm for button without data-confirm attribute", async () => {
|
|
205
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
206
|
+
|
|
207
|
+
document.body.innerHTML = `
|
|
208
|
+
<form>
|
|
209
|
+
<button type="button">Click me</button>
|
|
210
|
+
</form>
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const button = document.querySelector('button[type="button"]');
|
|
214
|
+
const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open");
|
|
215
|
+
|
|
216
|
+
initializeConfirm();
|
|
217
|
+
|
|
218
|
+
button.click();
|
|
219
|
+
|
|
220
|
+
expect(openSpy).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/* dummy end */
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
// Utility helper functions for the date and time picker functionality
|
|
2
2
|
|
|
3
|
+
export const adjustPickerPosition = (input, datePickerContainer, selector) => {
|
|
4
|
+
const parent = input.closest(selector);
|
|
5
|
+
|
|
6
|
+
if (getComputedStyle(parent).position === "static") {
|
|
7
|
+
parent.style.position = "relative";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const rect = input.getBoundingClientRect();
|
|
11
|
+
const calendarHeight = datePickerContainer.offsetHeight;
|
|
12
|
+
const spaceAbove = rect.top;
|
|
13
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
14
|
+
const openBelow = spaceBelow >= calendarHeight || spaceBelow >= spaceAbove;
|
|
15
|
+
|
|
16
|
+
if (openBelow) {
|
|
17
|
+
// Open below
|
|
18
|
+
datePickerContainer.style.top = `${input.offsetHeight}px`;
|
|
19
|
+
datePickerContainer.style.bottom = "";
|
|
20
|
+
} else {
|
|
21
|
+
// Open above
|
|
22
|
+
datePickerContainer.style.top = "";
|
|
23
|
+
datePickerContainer.style.bottom = `${input.offsetHeight}px`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
datePickerContainer.style.right = "0px";
|
|
27
|
+
};
|
|
28
|
+
|
|
3
29
|
export const setHour = (value, format) => {
|
|
4
30
|
const hour = value.split(":")[0];
|
|
5
31
|
if (format === 12) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable require-jsdoc */
|
|
2
2
|
import icon from "src/decidim/icon"
|
|
3
|
-
import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos } from "src/decidim/datepicker/datepicker_functions"
|
|
3
|
+
import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
|
|
4
4
|
import { dateKeyDownListener, dateBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners"
|
|
5
5
|
import { getDictionary } from "src/decidim/i18n"
|
|
6
6
|
|
|
@@ -130,6 +130,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
130
130
|
};
|
|
131
131
|
pickedDate = null;
|
|
132
132
|
datePickerContainer.style.display = "block";
|
|
133
|
+
adjustPickerPosition(date, datePickerContainer, ".datepicker__date-column");
|
|
133
134
|
|
|
134
135
|
document.addEventListener("click", datePickerDisplay);
|
|
135
136
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* eslint-disable require-jsdoc */
|
|
2
|
+
/* eslint max-lines: ["error", 310] */
|
|
3
|
+
|
|
2
4
|
import icon from "src/decidim/icon"
|
|
3
|
-
import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue } from "src/decidim/datepicker/datepicker_functions"
|
|
5
|
+
import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
|
|
4
6
|
import { timeKeyDownListener, timeBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners";
|
|
5
7
|
import { getDictionary } from "src/decidim/i18n";
|
|
6
8
|
|
|
@@ -21,6 +23,10 @@ export default function generateTimePicker(input, row, formats) {
|
|
|
21
23
|
clock.setAttribute("type", "button");
|
|
22
24
|
clock.setAttribute("aria-label", input.dataset.buttonTimeLabel);
|
|
23
25
|
|
|
26
|
+
if (input.attributes.disabled) {
|
|
27
|
+
clock.setAttribute("disabled", input.attributes.disabled);
|
|
28
|
+
};
|
|
29
|
+
|
|
24
30
|
timeColumn.appendChild(time);
|
|
25
31
|
timeColumn.appendChild(clock);
|
|
26
32
|
|
|
@@ -270,6 +276,8 @@ export default function generateTimePicker(input, row, formats) {
|
|
|
270
276
|
event.preventDefault();
|
|
271
277
|
timePicker.style.display = "block";
|
|
272
278
|
document.addEventListener("click", timePickerDisplay);
|
|
279
|
+
adjustPickerPosition(time, timePicker, ".datepicker__time-column")
|
|
280
|
+
|
|
273
281
|
hours.value = hourDisplay(hour);
|
|
274
282
|
minutes.value = minuteDisplay(minute);
|
|
275
283
|
});
|
data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import { adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions";
|
|
4
|
+
|
|
5
|
+
describe("adjustDatePickerPosition", () => {
|
|
6
|
+
let input = null;
|
|
7
|
+
let parent = null;
|
|
8
|
+
let datePickerContainer = null;
|
|
9
|
+
|
|
10
|
+
let originalInnerHeight = window.innerHeight;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Setup DOM structure
|
|
14
|
+
parent = document.createElement("div");
|
|
15
|
+
parent.className = "datepicker__date-column";
|
|
16
|
+
document.body.appendChild(parent);
|
|
17
|
+
|
|
18
|
+
input = document.createElement("input");
|
|
19
|
+
Reflect.defineProperty(input, "offsetHeight", {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: 40
|
|
22
|
+
});
|
|
23
|
+
parent.appendChild(input);
|
|
24
|
+
|
|
25
|
+
datePickerContainer = document.createElement("div");
|
|
26
|
+
datePickerContainer.className = "datepicker__container";
|
|
27
|
+
parent.appendChild(datePickerContainer);
|
|
28
|
+
|
|
29
|
+
// Mock offsetHeight for calendar
|
|
30
|
+
Reflect.defineProperty(datePickerContainer, "offsetHeight", {
|
|
31
|
+
configurable: true,
|
|
32
|
+
value: 300
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// store original viewport height
|
|
36
|
+
originalInnerHeight = window.innerHeight;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
document.body.removeChild(parent);
|
|
41
|
+
|
|
42
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
43
|
+
writable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
value: originalInnerHeight
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
jest.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("sets parent position to relative when static", () => {
|
|
52
|
+
parent.style.position = "static";
|
|
53
|
+
|
|
54
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
55
|
+
|
|
56
|
+
expect(parent.style.position).toBe("relative");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not change parent position when already positioned", () => {
|
|
60
|
+
parent.style.position = "absolute";
|
|
61
|
+
|
|
62
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
63
|
+
|
|
64
|
+
expect(parent.style.position).toBe("absolute");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("opens below when sufficient space below", () => {
|
|
68
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
69
|
+
top: 100,
|
|
70
|
+
bottom: 140
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
74
|
+
writable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
value: 800
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
80
|
+
|
|
81
|
+
expect(datePickerContainer.style.top).toBe("40px");
|
|
82
|
+
expect(datePickerContainer.style.bottom).toBe("");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("opens above when insufficient space below", () => {
|
|
86
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
87
|
+
top: 400,
|
|
88
|
+
bottom: 440
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
92
|
+
writable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
value: 500
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
98
|
+
|
|
99
|
+
expect(datePickerContainer.style.top).toBe("");
|
|
100
|
+
expect(datePickerContainer.style.bottom).toBe("40px");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("prefers opening below when space is equal above and below", () => {
|
|
104
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
105
|
+
top: 250,
|
|
106
|
+
bottom: 290
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
110
|
+
writable: true,
|
|
111
|
+
configurable: true,
|
|
112
|
+
value: 540
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
116
|
+
|
|
117
|
+
expect(datePickerContainer.style.top).toBe("40px");
|
|
118
|
+
expect(datePickerContainer.style.bottom).toBe("");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("always sets right position to 0px", () => {
|
|
122
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
123
|
+
top: 100,
|
|
124
|
+
bottom: 140
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
128
|
+
|
|
129
|
+
expect(datePickerContainer.style.right).toBe("0px");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
describe("adjustTimePickerPosition", () => {
|
|
135
|
+
let input = null;
|
|
136
|
+
let parent = null;
|
|
137
|
+
let timePicker = null;
|
|
138
|
+
|
|
139
|
+
let originalInnerHeight = window.innerHeight;
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
parent = document.createElement("div");
|
|
143
|
+
parent.className = "datepicker__time-column";
|
|
144
|
+
document.body.appendChild(parent);
|
|
145
|
+
|
|
146
|
+
input = document.createElement("input");
|
|
147
|
+
Reflect.defineProperty(input, "offsetHeight", {
|
|
148
|
+
configurable: true,
|
|
149
|
+
value: 30
|
|
150
|
+
});
|
|
151
|
+
parent.appendChild(input);
|
|
152
|
+
|
|
153
|
+
timePicker = document.createElement("div");
|
|
154
|
+
timePicker.className = "timepicker__container";
|
|
155
|
+
parent.appendChild(timePicker);
|
|
156
|
+
|
|
157
|
+
Reflect.defineProperty(timePicker, "offsetHeight", {
|
|
158
|
+
configurable: true,
|
|
159
|
+
value: 200
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// store original value before any test mutates it
|
|
163
|
+
originalInnerHeight = window.innerHeight;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
// restore DOM
|
|
168
|
+
document.body.removeChild(parent);
|
|
169
|
+
|
|
170
|
+
// restore window.innerHeight (fix for CodeRabbit warning)
|
|
171
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
172
|
+
writable: true,
|
|
173
|
+
configurable: true,
|
|
174
|
+
value: originalInnerHeight
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
jest.restoreAllMocks();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("sets parent position to relative when static", () => {
|
|
181
|
+
parent.style.position = "static";
|
|
182
|
+
|
|
183
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
184
|
+
|
|
185
|
+
expect(parent.style.position).toBe("relative");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("opens below when there is enough space", () => {
|
|
189
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
190
|
+
top: 100,
|
|
191
|
+
bottom: 130
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
195
|
+
writable: true,
|
|
196
|
+
configurable: true,
|
|
197
|
+
value: 700
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
201
|
+
|
|
202
|
+
expect(timePicker.style.top).toBe("30px");
|
|
203
|
+
expect(timePicker.style.bottom).toBe("");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("opens above when there is not enough space below", () => {
|
|
207
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
208
|
+
top: 400,
|
|
209
|
+
bottom: 430
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
213
|
+
writable: true,
|
|
214
|
+
configurable: true,
|
|
215
|
+
value: 500
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
219
|
+
|
|
220
|
+
expect(timePicker.style.top).toBe("");
|
|
221
|
+
expect(timePicker.style.bottom).toBe("30px");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("always aligns to the right", () => {
|
|
225
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
226
|
+
top: 100,
|
|
227
|
+
bottom: 130
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
231
|
+
|
|
232
|
+
expect(timePicker.style.right).toBe("0px");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -79,7 +79,7 @@ const updateActiveUploads = (modal) => {
|
|
|
79
79
|
const template = `
|
|
80
80
|
<div ${attachmentIdOrHiddenField} data-filename="${escapeQuotes(file.name)}" data-title="${escapeQuotes(title)}">
|
|
81
81
|
${(/image/).test(file.type) && "<div><img src=\"data:,\" role=\"presentation\" /></div>" || ""}
|
|
82
|
-
<
|
|
82
|
+
<p>${escapeHtml(title)}</p>
|
|
83
83
|
${hidden}
|
|
84
84
|
</div>
|
|
85
85
|
`
|
|
@@ -78,16 +78,16 @@ export default class FocusGuard {
|
|
|
78
78
|
|
|
79
79
|
let target = null;
|
|
80
80
|
if (guard.dataset.position === "start") {
|
|
81
|
-
// Focus at the start guard, so focus the
|
|
82
|
-
for (let ind =
|
|
81
|
+
// Focus at the start guard, so focus the last focusable element (cycle forward to end)
|
|
82
|
+
for (let ind = visibleNodes.length - 1; ind >= 0; ind -= 1) {
|
|
83
83
|
if (!this.isFocusGuard(visibleNodes[ind]) && this.isFocusable(visibleNodes[ind])) {
|
|
84
84
|
target = visibleNodes[ind];
|
|
85
85
|
break;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
} else {
|
|
89
|
-
// Focus at the end guard, so focus the first focusable element
|
|
90
|
-
for (let ind =
|
|
89
|
+
// Focus at the end guard, so focus the first focusable element (cycle back to start)
|
|
90
|
+
for (let ind = 0; ind < visibleNodes.length; ind += 1) {
|
|
91
91
|
if (!this.isFocusGuard(visibleNodes[ind]) && this.isFocusable(visibleNodes[ind])) {
|
|
92
92
|
target = visibleNodes[ind];
|
|
93
93
|
break;
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
&__calendar {
|
|
119
|
-
@apply w-
|
|
119
|
+
@apply w-20 flex flex-col justify-start rounded overflow-hidden bg-background text-center;
|
|
120
120
|
|
|
121
121
|
/* overwrite defaults */
|
|
122
122
|
&-list__reset {
|
|
@@ -128,17 +128,25 @@
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
&-day {
|
|
131
|
-
@apply text-black text-
|
|
131
|
+
@apply text-black text-xl font-semibold;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
&-year {
|
|
135
|
-
@apply text-black text-xs;
|
|
135
|
+
@apply text-black text-xs mb-0.5;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
&-month,
|
|
139
139
|
&-day,
|
|
140
140
|
&-year {
|
|
141
|
-
@apply inline-flex items-center justify-
|
|
141
|
+
@apply inline-flex items-center justify-center empty:[&>div]:hidden;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&-separator {
|
|
145
|
+
@apply mx-2 font-normal text-sm;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.card__list-content {
|
|
149
|
+
@apply mt-0.5;
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -26,7 +26,7 @@ module Decidim
|
|
|
26
26
|
delegate :content_tag, :safe_join, :link_to, :active_link_to_class, :is_active_link?, :icon, to: :@view
|
|
27
27
|
|
|
28
28
|
def render
|
|
29
|
-
content_tag :li, role:
|
|
29
|
+
content_tag :li, role: menuitem_role, class: link_wrapper_classes do
|
|
30
30
|
output = if url == "#"
|
|
31
31
|
[content_tag(:span, composed_label, class: "sidebar-menu__item-disabled")]
|
|
32
32
|
else
|
|
@@ -62,6 +62,12 @@ module Decidim
|
|
|
62
62
|
[@options.element_class, active_class].compact.join(" ")
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
def menuitem_role
|
|
66
|
+
return if @options.role == false
|
|
67
|
+
|
|
68
|
+
@options.role || :menuitem
|
|
69
|
+
end
|
|
70
|
+
|
|
65
71
|
def active_class
|
|
66
72
|
active_link_to_class(
|
|
67
73
|
url,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<h1 class="title-decorator inline-block text-left mb-12"><%= t "devise.invitations.edit.header" %></h1>
|
|
5
5
|
|
|
6
6
|
<p class="text-lg text-gray-2">
|
|
7
|
-
<%= t("devise.invitations.edit.subtitle").html_safe %>
|
|
7
|
+
<%= current_organization.users_registration_mode_disabled? ? t("devise.invitations.edit.subtitle_no_password").html_safe : t("devise.invitations.edit.subtitle").html_safe %>
|
|
8
8
|
</p>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
<%= f.text_field :nickname, help_text: t("devise.invitations.edit.nickname_help", organization: current_organization_name), required: "required", autocomplete: "nickname" %>
|
|
18
18
|
|
|
19
|
-
<%
|
|
20
|
-
<%= render partial: "decidim/account/password_fields", locals: { form: f, user: :user } %>
|
|
19
|
+
<% unless current_organization.users_registration_mode_disabled? %>
|
|
20
|
+
<%= render partial: "decidim/account/password_fields", locals: { form: f, user: resource.admin? ? :admin : :user } %>
|
|
21
21
|
<% end %>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
<%= f.text_field :name, help_text: t("decidim.devise.registrations.new.username_help"), autocomplete: "name", placeholder: "John Doe" %>
|
|
35
35
|
|
|
36
36
|
<%= f.email_field :email, autocomplete: "email", placeholder: t("placeholder_email", scope: "decidim.devise.shared") %>
|
|
37
|
+
<span class="sr-only"><%= t("placeholder_email", scope: "decidim.devise.shared") %></span>
|
|
37
38
|
|
|
38
39
|
<%= render partial: "decidim/account/password_fields", locals: { form: f, user: :user } %>
|
|
39
40
|
</div>
|