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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/content_blocks/highlighted_elements_with_cell_for_list_cell.rb +5 -1
  3. data/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb +2 -2
  4. data/app/cells/decidim/report_button/already_reported_modal.erb +1 -1
  5. data/app/cells/decidim/report_button/flag_modal.erb +1 -1
  6. data/app/cells/decidim/report_user_button/already_reported_modal.erb +1 -1
  7. data/app/cells/decidim/report_user_button/flag_modal.erb +1 -1
  8. data/app/cells/decidim/statistic/show.erb +4 -4
  9. data/app/cells/decidim/upload_modal/files.erb +9 -5
  10. data/app/cells/decidim/upload_modal_cell.rb +14 -1
  11. data/app/commands/decidim/multiple_attachments_methods.rb +20 -3
  12. data/app/helpers/decidim/mailer_helper.rb +36 -0
  13. data/app/helpers/decidim/menu_helper.rb +2 -1
  14. data/app/helpers/decidim/newsletters_helper.rb +4 -22
  15. data/app/jobs/decidim/find_and_update_descendants_job.rb +8 -2
  16. data/app/jobs/decidim/update_search_indexes_job.rb +2 -2
  17. data/app/mailers/decidim/application_mailer.rb +4 -0
  18. data/app/packs/src/decidim/a11y.js +29 -0
  19. data/app/packs/src/decidim/a11y.test.js +81 -0
  20. data/app/packs/src/decidim/confirm.js +8 -1
  21. data/app/packs/src/decidim/confirm.test.js +225 -0
  22. data/app/packs/src/decidim/datepicker/datepicker_functions.js +26 -0
  23. data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -1
  24. data/app/packs/src/decidim/datepicker/generate_timepicker.js +9 -1
  25. data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
  26. data/app/packs/src/decidim/direct_uploads/upload_field.js +1 -1
  27. data/app/packs/src/decidim/focus_guard.js +4 -4
  28. data/app/packs/stylesheets/decidim/_cards.scss +12 -4
  29. data/app/packs/stylesheets/decidim/_flash.scss +1 -1
  30. data/app/packs/stylesheets/decidim/_modal_update.scss +1 -1
  31. data/app/presenters/decidim/menu_item_presenter.rb +7 -1
  32. data/app/views/decidim/devise/invitations/edit.html.erb +3 -3
  33. data/app/views/decidim/devise/registrations/new.html.erb +1 -0
  34. data/app/views/decidim/devise/shared/_tos_fields.html.erb +3 -3
  35. data/app/views/decidim/notification_mailer/event_received.html.erb +3 -3
  36. data/config/initializers/devise.rb +6 -0
  37. data/config/locales/ar.yml +3 -3
  38. data/config/locales/bg.yml +0 -4
  39. data/config/locales/ca-IT.yml +7 -6
  40. data/config/locales/ca.yml +7 -6
  41. data/config/locales/cs.yml +8 -8
  42. data/config/locales/de.yml +17 -8
  43. data/config/locales/el.yml +0 -2
  44. data/config/locales/en.yml +5 -4
  45. data/config/locales/es-MX.yml +8 -7
  46. data/config/locales/es-PY.yml +8 -7
  47. data/config/locales/es.yml +10 -9
  48. data/config/locales/eu.yml +8 -6
  49. data/config/locales/fi-plain.yml +10 -4
  50. data/config/locales/fi.yml +11 -5
  51. data/config/locales/fr-CA.yml +7 -5
  52. data/config/locales/fr.yml +8 -7
  53. data/config/locales/gl.yml +0 -2
  54. data/config/locales/hu.yml +5 -9
  55. data/config/locales/id-ID.yml +0 -2
  56. data/config/locales/it.yml +1 -3
  57. data/config/locales/ja.yml +7 -8
  58. data/config/locales/lb.yml +0 -2
  59. data/config/locales/lt.yml +1 -3
  60. data/config/locales/lv.yml +0 -2
  61. data/config/locales/nl.yml +0 -2
  62. data/config/locales/no.yml +0 -2
  63. data/config/locales/pl.yml +0 -4
  64. data/config/locales/pt-BR.yml +4 -5
  65. data/config/locales/pt.yml +0 -2
  66. data/config/locales/ro-RO.yml +1 -5
  67. data/config/locales/ru.yml +0 -2
  68. data/config/locales/sk.yml +0 -4
  69. data/config/locales/sv.yml +8 -7
  70. data/config/locales/tr-TR.yml +17 -5
  71. data/config/locales/zh-CN.yml +0 -2
  72. data/config/locales/zh-TW.yml +1 -3
  73. data/lib/decidim/assets/tailwind/tailwind.config.js.erb +1 -1
  74. data/lib/decidim/content_parsers/blob_parser.rb +2 -2
  75. data/lib/decidim/content_renderers/blob_renderer.rb +2 -2
  76. data/lib/decidim/core/version.rb +1 -1
  77. data/lib/decidim/form_builder.rb +25 -1
  78. data/lib/decidim/maintenance/taxonomy_importer.rb +1 -1
  79. data/lib/decidim/searchable.rb +4 -4
  80. 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
  });
@@ -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
- <span>${escapeHtml(title)}</span>
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 first focusable element after that
82
- for (let ind = 0; ind < visibleNodes.length; ind += 1) {
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 after that
90
- for (let ind = visibleNodes.length - 1; ind >= 0; ind -= 1) {
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-14 flex flex-col justify-start rounded overflow-hidden bg-background text-center;
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-2xl font-bold;
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-evenly empty:[&>div]:hidden;
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
 
@@ -1,5 +1,5 @@
1
1
  .flash {
2
- @apply flex justify-start gap-4 border-l-4 border-secondary my-4 p-4 bg-secondary/5 break-all;
2
+ @apply flex justify-start gap-4 border-l-4 border-secondary my-4 p-4 bg-secondary/5;
3
3
 
4
4
  &__icon {
5
5
  svg {
@@ -131,7 +131,7 @@
131
131
  @apply w-full rounded bg-background flex items-center justify-center py-4 [&_img]:object-cover [&_img]:h-[200px];
132
132
  }
133
133
 
134
- span {
134
+ p {
135
135
  @apply text-sm text-gray-2 mx-auto w-full break-all mb-2;
136
136
  }
137
137
  }
@@ -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: :menuitem, class: link_wrapper_classes do
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
- <% if f.object.class.require_password_on_accepting %>
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>