decidim-core 0.31.6 → 0.31.7

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/card_metadata_cell.rb +1 -1
  3. data/app/cells/decidim/comments_button_cell.rb +9 -1
  4. data/app/cells/decidim/data_consent/category.erb +20 -33
  5. data/app/cells/decidim/footer_topics/show.erb +5 -8
  6. data/app/cells/decidim/participatory_space_dropdown_metadata/links.erb +1 -1
  7. data/app/cells/decidim/share_widget/qr_code_modal.erb +1 -1
  8. data/app/events/decidim/amendable/amendment_base_event.rb +7 -1
  9. data/app/helpers/decidim/menu_helper.rb +0 -2
  10. data/app/models/decidim/user.rb +2 -2
  11. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +196 -42
  12. data/app/packs/src/decidim/controllers/accordion/controller.js +93 -17
  13. data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
  14. data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -0
  15. data/app/packs/src/decidim/controllers/password_toggler/controller.js +22 -1
  16. data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
  17. data/app/packs/src/decidim/direct_uploads/upload_modal.js +2 -1
  18. data/app/packs/src/decidim/map/controller.js +10 -1
  19. data/app/packs/src/decidim/refactor/moved/focus_guard.js +12 -12
  20. data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
  21. data/app/packs/stylesheets/decidim/_cards.scss +1 -1
  22. data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
  23. data/app/packs/stylesheets/decidim/_documents.scss +1 -1
  24. data/app/packs/stylesheets/decidim/_footer.scss +33 -1
  25. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +1 -1
  26. data/app/presenters/decidim/footer_menu_presenter.rb +2 -4
  27. data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -0
  28. data/app/views/decidim/searches/_filters.html.erb +1 -1
  29. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  30. data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
  31. data/config/locales/ar.yml +0 -20
  32. data/config/locales/bg.yml +0 -19
  33. data/config/locales/ca-IT.yml +28 -23
  34. data/config/locales/ca.yml +28 -23
  35. data/config/locales/cs.yml +13 -21
  36. data/config/locales/de.yml +0 -25
  37. data/config/locales/el.yml +0 -18
  38. data/config/locales/en.yml +28 -23
  39. data/config/locales/es-MX.yml +28 -23
  40. data/config/locales/es-PY.yml +28 -23
  41. data/config/locales/es.yml +28 -23
  42. data/config/locales/eu.yml +27 -24
  43. data/config/locales/fi-plain.yml +28 -23
  44. data/config/locales/fi.yml +28 -23
  45. data/config/locales/fr-CA.yml +7 -19
  46. data/config/locales/fr.yml +7 -19
  47. data/config/locales/gl.yml +0 -19
  48. data/config/locales/hu.yml +0 -17
  49. data/config/locales/id-ID.yml +0 -18
  50. data/config/locales/is-IS.yml +0 -6
  51. data/config/locales/it.yml +0 -17
  52. data/config/locales/ja.yml +28 -23
  53. data/config/locales/lb.yml +0 -18
  54. data/config/locales/lt.yml +0 -19
  55. data/config/locales/lv.yml +0 -18
  56. data/config/locales/nl.yml +0 -18
  57. data/config/locales/no.yml +0 -18
  58. data/config/locales/pl.yml +2 -19
  59. data/config/locales/pt-BR.yml +0 -25
  60. data/config/locales/pt.yml +0 -18
  61. data/config/locales/ro-RO.yml +0 -18
  62. data/config/locales/ru.yml +0 -8
  63. data/config/locales/sk.yml +0 -25
  64. data/config/locales/sv.yml +4 -25
  65. data/config/locales/tr-TR.yml +0 -18
  66. data/config/locales/uk.yml +0 -6
  67. data/config/locales/zh-CN.yml +0 -16
  68. data/config/locales/zh-TW.yml +0 -19
  69. data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
  70. data/decidim-core.gemspec +3 -0
  71. data/lib/decidim/core/test/shared_examples/amendable/amendment_event_examples.rb +2 -2
  72. data/lib/decidim/core/test/shared_examples/amendable/amendment_promoted_event_examples.rb +3 -3
  73. data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
  74. data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +2 -1
  75. data/lib/decidim/core/test/shared_examples/resource_liked_event_examples.rb +3 -3
  76. data/lib/decidim/core/version.rb +1 -1
  77. data/lib/decidim/core.rb +1 -1
  78. data/lib/decidim/map/dynamic_map.rb +3 -1
  79. metadata +27 -6
@@ -142,6 +142,34 @@ describe("DropdownController", () => {
142
142
  });
143
143
  });
144
144
 
145
+ describe("aria-expanded initialization", () => {
146
+ it("sets aria-expanded to false when missing and isOpen is false", () => {
147
+ const testController = createController(element);
148
+
149
+ testController.connect();
150
+
151
+ expect(element.getAttribute("aria-expanded")).toBe("false");
152
+ });
153
+
154
+ it("sets aria-expanded to true when missing and data-open is true", () => {
155
+ element.setAttribute("data-open", "true");
156
+ const testController = createController(element);
157
+
158
+ testController.connect();
159
+
160
+ expect(element.getAttribute("aria-expanded")).toBe("true");
161
+ });
162
+
163
+ it("does not override existing aria-expanded attribute", () => {
164
+ element.setAttribute("aria-expanded", "true");
165
+ const testController = createController(element);
166
+
167
+ testController.connect();
168
+
169
+ expect(element.getAttribute("aria-expanded")).toBe("true");
170
+ });
171
+ });
172
+
145
173
  describe("data-add-aria-roles option", () => {
146
174
  it("keeps role menu when data-add-aria-roles is true", () => {
147
175
  element.setAttribute("data-add-aria-roles", "true");
@@ -55,6 +55,18 @@ export default class extends Controller {
55
55
  hide: icon("eye-off-line")
56
56
  }
57
57
 
58
+ const existingWrapper = this.input.closest(".input-group__password");
59
+ if (existingWrapper) {
60
+ const existingButton = existingWrapper.querySelector('button[type="button"]');
61
+ const existingStatusText = existingWrapper.querySelector('span.sr-only[aria-live="polite"]');
62
+ if (existingButton && existingStatusText) {
63
+ this.button = existingButton;
64
+ this.statusText = existingStatusText;
65
+ this.bindListeners();
66
+ return;
67
+ }
68
+ }
69
+
58
70
  this.init();
59
71
  }
60
72
 
@@ -81,8 +93,17 @@ export default class extends Controller {
81
93
  */
82
94
  init() {
83
95
  this.createControls();
96
+ this.bindListeners();
97
+ }
98
+
99
+ bindListeners() {
100
+ if (this.button && this.boundToggleVisibility) {
101
+ this.button.removeEventListener("click", this.boundToggleVisibility);
102
+ }
103
+ if (this.form && this.boundHidePassword) {
104
+ this.form.removeEventListener("submit", this.boundHidePassword);
105
+ }
84
106
 
85
- // Bind methods to maintain correct context
86
107
  this.boundToggleVisibility = this.toggleVisibility.bind(this);
87
108
  this.boundHidePassword = this.hidePassword.bind(this);
88
109
 
@@ -22,17 +22,7 @@ describe("PasswordToggler", () => {
22
22
  container = document.createElement("div");
23
23
  container.innerHTML = `
24
24
  <div data-controller="password-toggler" class="flex flex-row items-center gap-x-2" data-show-password="Show secret" data-hide-password="Hide Secret" data-hidden-password="Secret is hidden" data-shown-password="Secret is shown">
25
- <div class="input-group__password">
26
- <div class="input-group__password">
27
- <button type="button" aria-controls="token_162" aria-label="Show password">
28
- <svg width="0.75em" height="0.75em" role="img" aria-hidden="true">
29
- <title>eye-line</title>
30
- <use href="/decidim-packs/media/images/remixicon.symbol-e643e553623ffcd49f94.svg#ri-eye-line"></use>
31
- </svg>
32
- </button>
33
- <input type="password" id="token_162" value="AXXXXXXXX" class="w-full" autocomplete="off">
34
- </div>
35
- </div>
25
+ <input type="password" id="token_162" value="AXXXXXXXX" class="w-full" autocomplete="off">
36
26
  <div class="basis-1/4">
37
27
  <button type="button" class="button button__sm button__text-primary" data-clipboard-copy="#token_162">
38
28
  <span>Copy secret</span>
@@ -204,22 +194,23 @@ describe("PasswordToggler", () => {
204
194
  });
205
195
  });
206
196
 
207
- describe("integration with existing markup", () => {
208
- it("works with existing button structure", () => {
209
- const existingButton = passwordElement.querySelector('button[aria-controls="token_162"]');
210
- expect(existingButton).toBeTruthy();
197
+ describe("idempotency", () => {
198
+ it("does not duplicate controls on reconnect", () => {
199
+ controller.disconnect();
200
+ controller.connect();
211
201
 
212
- controller.init();
202
+ const wrappers = passwordElement.querySelectorAll(".input-group__password");
203
+ const buttons = passwordElement.querySelectorAll('button[aria-controls="token_162"]');
213
204
 
214
- // Should work alongside existing button
215
- expect(controller.button).toBeDefined();
216
- expect(controller.input.getAttribute("type")).toBe("password");
205
+ expect(wrappers.length).toBe(1);
206
+ expect(buttons.length).toBe(1);
217
207
  });
208
+ });
218
209
 
210
+ describe("integration with existing markup", () => {
219
211
  it("handles password input with existing value", () => {
220
212
  expect(controller.input.value).toBe("AXXXXXXXX");
221
213
 
222
- controller.init();
223
214
  controller.showPassword();
224
215
 
225
216
  expect(controller.input.getAttribute("type")).toBe("text");
@@ -253,7 +253,8 @@ export default class UploadModal {
253
253
  }
254
254
 
255
255
  setProgressBar(name, value) {
256
- const item = Array.from(this.uploadItems.querySelectorAll("[data-filename]")).find((el) => el.dataset.filename === name);
256
+ const items = Array.from(this.uploadItems.querySelectorAll("[data-filename]"));
257
+ const item = items.filter((el) => el.dataset.filename === name).pop();
257
258
  if (item) {
258
259
  item.querySelector("progress").value = value;
259
260
  }
@@ -29,7 +29,16 @@ export default class MapController {
29
29
  }
30
30
 
31
31
  load() {
32
- this.map = L.map(this.mapId);
32
+ const zoomControl = L.control.zoom({
33
+ zoomInTitle: this.config.zoomInText || "Zoom in",
34
+ zoomOutTitle: this.config.zoomOutText || "Zoom out"
35
+ });
36
+
37
+ this.map = L.map(this.mapId, {
38
+ zoomControl: false
39
+ });
40
+
41
+ this.map.addControl(zoomControl);
33
42
 
34
43
  this.map.scrollWheelZoom.disable();
35
44
 
@@ -5,14 +5,12 @@ const focusableDisableableNodes = ["BUTTON", "INPUT", "TEXTAREA", "SELECT"];
5
5
  export default class FocusGuard {
6
6
  constructor(container) {
7
7
  this.container = container;
8
- this.guardedElement = null;
9
- this.triggerElement = null;
8
+ this.stack = [];
10
9
  }
11
10
 
12
11
  trap(element, trigger) {
13
12
  this.enable();
14
- this.guardedElement = element;
15
- this.triggerElement = trigger;
13
+ this.stack.push({ element, trigger });
16
14
  }
17
15
 
18
16
  enable() {
@@ -45,14 +43,15 @@ export default class FocusGuard {
45
43
  }
46
44
 
47
45
  disable() {
48
- const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
49
- guards.forEach((guard) => guard.remove());
46
+ const current = this.stack.pop();
50
47
 
51
- this.guardedElement = null;
48
+ if (this.stack.length === 0) {
49
+ const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
50
+ guards.forEach((guard) => guard.remove());
51
+ }
52
52
 
53
- if (this.triggerElement) {
54
- this.triggerElement.focus();
55
- this.triggerElement = null;
53
+ if (current && current.trigger) {
54
+ current.trigger.focus();
56
55
  }
57
56
  }
58
57
 
@@ -67,12 +66,13 @@ export default class FocusGuard {
67
66
  };
68
67
 
69
68
  handleContainerFocus(guard) {
70
- if (!this.guardedElement) {
69
+ const current = this.stack[this.stack.length - 1];
70
+ if (!current || !current.element) {
71
71
  guard.blur();
72
72
  return;
73
73
  }
74
74
 
75
- const visibleNodes = Array.from(this.guardedElement.querySelectorAll("*")).filter((item) => {
75
+ const visibleNodes = Array.from(current.element.querySelectorAll("*")).filter((item) => {
76
76
  return this.isVisible(item);
77
77
  });
78
78
 
@@ -0,0 +1,136 @@
1
+ /* global jest */
2
+
3
+ import FocusGuard from "src/decidim/refactor/moved/focus_guard"
4
+
5
+ describe("FocusGuard", () => {
6
+ let focusGuard = null;
7
+
8
+ beforeEach(() => {
9
+ document.body.innerHTML = "";
10
+ focusGuard = new FocusGuard(document.body);
11
+ });
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = "";
15
+ });
16
+
17
+ describe("single dialog", () => {
18
+ it("creates focus guards on trap", () => {
19
+ const dialog = document.createElement("div");
20
+ const trigger = document.createElement("button");
21
+ document.body.appendChild(dialog);
22
+ document.body.appendChild(trigger);
23
+
24
+ focusGuard.trap(dialog, trigger);
25
+ const guards = document.body.querySelectorAll(".focusguard");
26
+ expect(guards.length).toBe(2);
27
+ });
28
+
29
+ it("removes focus guards and returns focus on disable", () => {
30
+ const dialog = document.createElement("div");
31
+ const trigger = document.createElement("button");
32
+ document.body.appendChild(dialog);
33
+ document.body.appendChild(trigger);
34
+
35
+ focusGuard.trap(dialog, trigger);
36
+ focusGuard.disable();
37
+
38
+ const guards = document.body.querySelectorAll(".focusguard");
39
+ expect(guards.length).toBe(0);
40
+ expect(document.activeElement).toBe(trigger);
41
+ });
42
+ });
43
+
44
+ describe("nested dialogs", () => {
45
+ it("keeps guards when inner dialog is disabled", () => {
46
+ const outerDialog = document.createElement("div");
47
+ const outerTrigger = document.createElement("button");
48
+ const innerDialog = document.createElement("div");
49
+ const innerTrigger = document.createElement("button");
50
+
51
+ document.body.appendChild(outerDialog);
52
+ document.body.appendChild(outerTrigger);
53
+ document.body.appendChild(innerDialog);
54
+ document.body.appendChild(innerTrigger);
55
+
56
+ focusGuard.trap(outerDialog, outerTrigger);
57
+ focusGuard.trap(innerDialog, innerTrigger);
58
+
59
+ expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
60
+
61
+ focusGuard.disable();
62
+
63
+ expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
64
+ expect(document.activeElement).toBe(innerTrigger);
65
+ });
66
+
67
+ it("removes guards when outer dialog is disabled last", () => {
68
+ const outerDialog = document.createElement("div");
69
+ const outerTrigger = document.createElement("button");
70
+ const innerDialog = document.createElement("div");
71
+ const innerTrigger = document.createElement("button");
72
+
73
+ document.body.appendChild(outerDialog);
74
+ document.body.appendChild(outerTrigger);
75
+ document.body.appendChild(innerDialog);
76
+ document.body.appendChild(innerTrigger);
77
+
78
+ focusGuard.trap(outerDialog, outerTrigger);
79
+ focusGuard.trap(innerDialog, innerTrigger);
80
+
81
+ focusGuard.disable();
82
+ focusGuard.disable();
83
+
84
+ expect(document.body.querySelectorAll(".focusguard").length).toBe(0);
85
+ expect(document.activeElement).toBe(outerTrigger);
86
+ });
87
+
88
+ it("cycles focus to the current top dialog when a guard receives focus", () => {
89
+ jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
90
+
91
+ const outerDialog = document.createElement("div");
92
+ const outerInput = document.createElement("input");
93
+ outerDialog.appendChild(outerInput);
94
+
95
+ const innerDialog = document.createElement("div");
96
+ const innerInput = document.createElement("input");
97
+ innerDialog.appendChild(innerInput);
98
+
99
+ document.body.appendChild(outerDialog);
100
+ document.body.appendChild(innerDialog);
101
+
102
+ focusGuard.trap(outerDialog, null);
103
+ focusGuard.trap(innerDialog, null);
104
+
105
+ const endGuard = document.body.querySelector('.focusguard[data-position="end"]');
106
+ endGuard.focus();
107
+
108
+ expect(document.activeElement).toBe(innerInput);
109
+ });
110
+
111
+ it("cycles focus backward to the current top dialog when the start guard receives focus", () => {
112
+ jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
113
+
114
+ const outerDialog = document.createElement("div");
115
+ const outerInput = document.createElement("input");
116
+ outerDialog.appendChild(outerInput);
117
+
118
+ const innerDialog = document.createElement("div");
119
+ const innerInput = document.createElement("input");
120
+ const innerButton = document.createElement("button");
121
+ innerDialog.appendChild(innerInput);
122
+ innerDialog.appendChild(innerButton);
123
+
124
+ document.body.appendChild(outerDialog);
125
+ document.body.appendChild(innerDialog);
126
+
127
+ focusGuard.trap(outerDialog, null);
128
+ focusGuard.trap(innerDialog, null);
129
+
130
+ const startGuard = document.body.querySelector('.focusguard[data-position="start"]');
131
+ startGuard.focus();
132
+
133
+ expect(document.activeElement).toBe(innerButton);
134
+ });
135
+ });
136
+ });
@@ -55,7 +55,7 @@
55
55
  }
56
56
 
57
57
  &-content {
58
- @apply flex flex-col gap-2.5;
58
+ @apply flex flex-col gap-2.5 min-w-0 break-words;
59
59
  }
60
60
 
61
61
  &-title {
@@ -65,17 +65,25 @@
65
65
  &-panel {
66
66
  @apply divide-y divide-gray-3 [&>*]:py-4 [&>p]:text-gray-2;
67
67
 
68
- &__tr {
69
- @apply flex items-center justify-between rounded first-of-type:bg-background-4;
68
+ table {
69
+ @apply w-full border-collapse;
70
70
  }
71
71
 
72
- &__th {
73
- @apply w-1/4 p-2 bg-background-4 text-gray-2 font-bold text-sm;
72
+ thead tr {
73
+ @apply bg-background-4;
74
74
  }
75
75
 
76
- &__td {
76
+ th {
77
+ @apply w-1/4 p-2 text-gray-2 font-bold text-sm text-left;
78
+ }
79
+
80
+ td {
77
81
  @apply w-1/4 p-2 text-gray-2 text-xs;
78
82
  }
83
+
84
+ tbody tr {
85
+ @apply border-b border-gray-3;
86
+ }
79
87
  }
80
88
 
81
89
  &-toggle {
@@ -3,7 +3,7 @@
3
3
  @apply divide-y divide-background [&>*]:py-4 first:[&>*]:pt-0 last:[&>*]:pb-0;
4
4
 
5
5
  > * {
6
- @apply flex items-center justify-between gap-4;
6
+ @apply grid sm:flex items-center justify-between gap-4;
7
7
  }
8
8
  }
9
9
 
@@ -3,12 +3,44 @@ footer {
3
3
  @apply bg-gray-4;
4
4
 
5
5
  &__top {
6
- @apply hidden lg:flex flex-row gap-8 container py-10;
6
+ @apply flex flex-col lg:flex-row gap-0 lg:gap-8 container py-10;
7
7
 
8
8
  .prose a,
9
9
  .prose strong {
10
10
  @apply text-white;
11
11
  }
12
+
13
+ .footer__nav {
14
+ @apply border-b border-white/20 lg:border-0;
15
+
16
+ ul {
17
+ @apply space-y-4 break-inside-avoid mb-4;
18
+
19
+ li a {
20
+ @apply font-semibold underline decoration-1 underline-offset-1;
21
+ }
22
+ }
23
+
24
+ > [data-controls] {
25
+ @apply flex items-center justify-between w-full py-4 lg:pointer-events-none lg:cursor-default text-left text-white font-semibold;
26
+
27
+ svg {
28
+ @apply w-4 h-4 fill-current transition-transform lg:hidden;
29
+ }
30
+
31
+ &[aria-expanded="true"] svg {
32
+ @apply rotate-180;
33
+ }
34
+ }
35
+
36
+ [id*="panel"][aria-hidden="true"] {
37
+ @apply hidden lg:block;
38
+ }
39
+
40
+ [id*="panel"][aria-hidden="false"] {
41
+ @apply block;
42
+ }
43
+ }
12
44
  }
13
45
 
14
46
  &__down {
@@ -49,7 +49,7 @@
49
49
  }
50
50
 
51
51
  &__collection-content {
52
- @apply w-auto ml-6 my-4 pl-4 lg:pl-8 border-l border-[#D3D5D9];
52
+ @apply w-auto sm:ml-6 sm:my-4 sm:pl-4 lg:pl-8 sm:border-l sm:border-[#D3D5D9];
53
53
 
54
54
  p {
55
55
  @apply pt-4;
@@ -3,10 +3,8 @@
3
3
  module Decidim
4
4
  # A presenter to render footer menu
5
5
  class FooterMenuPresenter < MenuPresenter
6
- def render
7
- content_tag(:nav, :role => "navigation", "aria-label" => @options[:label]) do
8
- safe_join([content_tag(:h2, @options[:label], class: "h5 mb-4"), render_menu])
9
- end
6
+ def render_menu_items
7
+ render_menu
10
8
  end
11
9
  end
12
10
  end
@@ -79,6 +79,9 @@ module Decidim
79
79
  image_url = image_element["src"]
80
80
  next if image_url.blank?
81
81
 
82
+ blob = find_blob_by_id(image_url.split("/").last)
83
+ return blob if blob.present?
84
+
82
85
  blob = find_blob_by_key(image_url.split("/").second_to_last)
83
86
  return blob if blob.present?
84
87
  end
@@ -120,6 +123,10 @@ module Decidim
120
123
  ActiveStorage::Blob.find_signed(blob_key) if blob_key.present?
121
124
  end
122
125
 
126
+ def find_blob_by_id(blob_id)
127
+ ActiveStorage::Blob.find_by(id: blob_id) if blob_id.present?
128
+ end
129
+
123
130
  def blob_from_attached_file(file)
124
131
  return unless file.respond_to?(:attached?) && file.attached?
125
132
 
@@ -16,7 +16,7 @@
16
16
  </button>
17
17
  <% end %>
18
18
  </div>
19
- <button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true" aria-expanded="true">
19
+ <button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true">
20
20
  <%= content_tag :span, t("decidim.searches.filters_small_view.filter_by"), class: "#{"is-active" if params.dig(:filter, :with_resource_type) == nil}" %>
21
21
  <% @blocks.each do |elements| %>
22
22
  <% elements.each do |type, results| %>
@@ -3,7 +3,7 @@
3
3
  <div class="lg:w-1/3">
4
4
  <%= render partial: "layouts/decidim/footer/main_intro" %>
5
5
  </div>
6
- <div class="lg:w-2/3 grid grid-cols-1 gap-x-4 gap-y-10 lg:grid-cols-4 text-md text-white">
6
+ <div class="lg:w-2/3 grid grid-cols-1 gap-x-4 lg:grid-cols-4 text-md text-white">
7
7
  <%= render partial: "layouts/decidim/footer/main_links" %>
8
8
  </div>
9
9
  </div>
@@ -1,31 +1,59 @@
1
- <%= footer_menu.render %>
1
+ <div data-controller="accordion" data-multiselectable="true" class="contents">
2
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.decidim_title") %>">
3
+ <button type="button" id="panel-footer-menu-trigger" data-controls="panel-footer-menu" data-open-lg="true">
4
+ <span class="h4"><%= t("layouts.decidim.footer.decidim_title") %></span>
5
+ <%= icon "arrow-down-s-line" %>
6
+ </button>
7
+ <div id="panel-footer-menu" aria-hidden="true">
8
+ <%= footer_menu.render_menu_items %>
9
+ </div>
10
+ </nav>
2
11
 
3
- <nav role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
4
- <h2 class="h4 mb-4"><%= t("layouts.decidim.user_menu.profile") %></h2>
5
- <ul class="space-y-4 break-inside-avoid">
6
- <% if current_user %>
7
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.configuration"), decidim.account_path %></li>
8
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.public_profile"), decidim.profile_path(current_user.nickname) %></li>
9
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.notifications"), decidim.notifications_path %></li>
10
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.conversations"), decidim.conversations_path %></li>
11
- <% else %>
12
- <% if current_organization.sign_up_enabled? %>
13
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.sign_up"), decidim.new_user_registration_path %></li>
14
- <% end %>
15
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.log_in"), decidim.new_user_session_path %></li>
16
- <% end %>
17
- </ul>
18
- </nav>
12
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
13
+ <button type="button" id="panel-profile-trigger" data-controls="panel-profile" data-open-lg="true">
14
+ <span class="h4"><%= t("layouts.decidim.user_menu.profile") %></span>
15
+ <%= icon "arrow-down-s-line" %>
16
+ </button>
17
+ <div id="panel-profile" aria-hidden="true">
18
+ <ul>
19
+ <% if current_user %>
20
+ <li><%= link_to t("layouts.decidim.user_menu.configuration"), decidim.account_path %></li>
21
+ <li><%= link_to t("layouts.decidim.user_menu.public_profile"), decidim.profile_path(current_user.nickname) %></li>
22
+ <li><%= link_to t("layouts.decidim.user_menu.notifications"), decidim.notifications_path %></li>
23
+ <li><%= link_to t("layouts.decidim.user_menu.conversations"), decidim.conversations_path %></li>
24
+ <% else %>
25
+ <% if current_organization.sign_up_enabled? %>
26
+ <li><%= link_to t("layouts.decidim.footer.sign_up"), decidim.new_user_registration_path %></li>
27
+ <% end %>
28
+ <li><%= link_to t("layouts.decidim.footer.log_in"), decidim.new_user_session_path %></li>
29
+ <% end %>
30
+ </ul>
31
+ </div>
32
+ </nav>
19
33
 
20
- <nav role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
21
- <h2 class="h4 mb-4"><%= t("layouts.decidim.footer.resources") %></h2>
22
- <ul class="space-y-4 break-inside-avoid">
23
- <li class="font-semibold underline"><%= link_to t("decidim.profiles.show.activity"), decidim.last_activities_path %></li>
24
- <% if Decidim.module_installed?(:meetings) %>
25
- <li class="font-semibold underline"><%= link_to t("decidim.pages.home.extended.meetings"), Decidim::Meetings::DirectoryEngine.routes.url_helpers.root_path %></li>
26
- <% end %>
27
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.open_data"), decidim.open_data_path %></li>
28
- </ul>
29
- </nav>
34
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
35
+ <button type="button" id="panel-resources-trigger" data-controls="panel-resources" data-open-lg="true">
36
+ <span class="h4"><%= t("layouts.decidim.footer.resources") %></span>
37
+ <%= icon "arrow-down-s-line" %>
38
+ </button>
39
+ <div id="panel-resources" aria-hidden="true">
40
+ <ul>
41
+ <li><%= link_to t("decidim.profiles.show.activity"), decidim.last_activities_path %></li>
42
+ <% if Decidim.module_installed?(:meetings) %>
43
+ <li><%= link_to t("decidim.pages.home.extended.meetings"), Decidim::Meetings::DirectoryEngine.routes.url_helpers.root_path %></li>
44
+ <% end %>
45
+ <li><%= link_to t("layouts.decidim.footer.open_data"), decidim.open_data_path %></li>
46
+ </ul>
47
+ </div>
48
+ </nav>
30
49
 
31
- <%= cell("decidim/footer_topics", nil) %>
50
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.help") %>">
51
+ <button type="button" id="panel-help-trigger" data-controls="panel-help" data-open-lg="true">
52
+ <span class="h4"><%= t("layouts.decidim.footer.help") %></span>
53
+ <%= icon "arrow-down-s-line" %>
54
+ </button>
55
+ <div id="panel-help" aria-hidden="true">
56
+ <%= cell("decidim/footer_topics", nil) %>
57
+ </div>
58
+ </nav>
59
+ </div>