decidim-core 0.30.6 → 0.30.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.
- 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/jobs/decidim/find_and_update_descendants_job.rb +8 -2
- data/app/jobs/decidim/update_search_indexes_job.rb +2 -2
- 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/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/views/decidim/devise/invitations/edit.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 +5 -8
- data/config/locales/de.yml +4 -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 +5 -6
- data/config/locales/fi-plain.yml +5 -4
- data/config/locales/fi.yml +6 -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/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 +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5110ff22ed67aef95bf387023ec7d18d9602a92944c82c2ab9373e786c04393c
|
|
4
|
+
data.tar.gz: b754a2b2ddb4a72ae07dcd384203d61454ced7f6a3a41a4f80a5bb501b3dc310
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3659a2a34d2531735e1a03710329261952c724361d0661bb9cf02c7a2acc76c2516266e8030a82e4dcc40789361d67845f1421addfcbf0f1fe4e0743a1288572
|
|
7
|
+
data.tar.gz: 9ec948751168cd3f4a8cc2b768be6a118cd22b47aecb0712fa64a1c01d58a5cdf2b7f2f20963fdd0eec2384a41753bbaec558016f533bfab7176227df6f3cadd
|
|
@@ -19,10 +19,14 @@ module Decidim
|
|
|
19
19
|
@list_cell ||= cell(
|
|
20
20
|
list_cell_path,
|
|
21
21
|
published_components.one? ? published_components.first : published_components,
|
|
22
|
-
**model.settings.to_h, see_all_path:
|
|
22
|
+
**model.settings.to_h, **extra_list_cell_options, see_all_path:
|
|
23
23
|
)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def extra_list_cell_options
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
|
|
26
30
|
def see_all_path; end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<% metadata_valued_items.each do |item| %>
|
|
3
3
|
<div class="participatory-space__metadata-item">
|
|
4
4
|
<div class="participatory-space__metadata-item-title">
|
|
5
|
-
<
|
|
5
|
+
<p><%= item[:title] %></p>
|
|
6
6
|
</div>
|
|
7
|
-
<
|
|
7
|
+
<p><%= item[:value] %></p>
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= decidim_modal id: modal_id, class: "flag-modal" do %>
|
|
2
2
|
<div data-dialog-container>
|
|
3
3
|
<%= icon "flag-line" %>
|
|
4
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
4
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
5
5
|
<div>
|
|
6
6
|
<div class="form__wrapper flag-modal__form">
|
|
7
7
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_modal.already_reported") %></p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<%= form_for report_form, builder:, url: report_path, method: :post, html: { id: nil } do |f| %>
|
|
3
3
|
<div data-dialog-container>
|
|
4
4
|
<%= icon "flag-line" %>
|
|
5
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
5
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
6
6
|
<div>
|
|
7
7
|
<div class="form__wrapper flag-modal__form">
|
|
8
8
|
<p id="dialog-desc-<%= modal_id %>" class="flag-modal__form-description"><%= t("decidim.shared.flag_modal.description") %></p>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= decidim_modal id: modal_id, class: "flag-user-modal" do %>
|
|
2
2
|
<div data-dialog-container>
|
|
3
3
|
<%= icon "flag-line" %>
|
|
4
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
4
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
5
5
|
<div>
|
|
6
6
|
<div class="form__wrapper flag-modal__form">
|
|
7
7
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_user_modal.already_reported") %></p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<%= form_for report_form, builder:, url: report_path, method: :post, html: { id: nil } do |f| %>
|
|
3
3
|
<div data-dialog-container>
|
|
4
4
|
<%= icon "flag-line" %>
|
|
5
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
5
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
6
6
|
<div>
|
|
7
7
|
<div class="form__wrapper flag-modal__form">
|
|
8
8
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_user_modal.description") %></p>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<div class="statistic <%= stat_dom_class %>" data-statistic>
|
|
2
|
-
<
|
|
2
|
+
<p class="statistic__title" title="<%= stat_title %>">
|
|
3
3
|
<%= stat_title %>
|
|
4
|
-
</
|
|
5
|
-
<
|
|
4
|
+
</p>
|
|
5
|
+
<p class="statistic__number">
|
|
6
6
|
<%= stat_number %>
|
|
7
|
-
</
|
|
7
|
+
</p>
|
|
8
8
|
</div>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div class="upload-modal__files-container upload-container-for-<%= attribute %> <%= with_title %>">
|
|
2
2
|
<div>
|
|
3
|
-
<%= label %>
|
|
3
|
+
<%= options[:paragraph] == true ? paragraph : label %>
|
|
4
4
|
|
|
5
5
|
<% if options[:help_text].present? %>
|
|
6
|
-
<
|
|
6
|
+
<p class="help-text"><%= options[:help_text] %></p>
|
|
7
7
|
<% end %>
|
|
8
8
|
|
|
9
9
|
<%# NOTE: this block is about wrapping a default image for the avatar with the new styles,
|
|
@@ -33,16 +33,20 @@
|
|
|
33
33
|
<% end %>
|
|
34
34
|
|
|
35
35
|
<% if has_title? %>
|
|
36
|
-
<
|
|
36
|
+
<p><%= title_for(attachment) %></p>
|
|
37
37
|
<% else %>
|
|
38
38
|
<% if attachment_blob&.image? %>
|
|
39
|
-
<
|
|
39
|
+
<p><%= title_for(attachment) %></p>
|
|
40
40
|
<% else %>
|
|
41
41
|
<%= link_to title_for(attachment), file_attachment_path(attachment), class: "w-full break-all mb-2" %>
|
|
42
42
|
<% end %>
|
|
43
43
|
<% end %>
|
|
44
44
|
<% if attachment_blob.present? %>
|
|
45
|
-
|
|
45
|
+
<% if is_persisted_attachment %>
|
|
46
|
+
<%= form.hidden_field attribute, value: attachment.id, id: "hidden_#{attribute}_#{attachment.id}" %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<%= form.hidden_field attribute, value: attachment_blob.signed_id, id: "hidden_#{attribute}_#{attachment_blob.id}" %>
|
|
49
|
+
<% end %>
|
|
46
50
|
<% end %>
|
|
47
51
|
</div>
|
|
48
52
|
<% end %>
|
|
@@ -30,6 +30,10 @@ module Decidim
|
|
|
30
30
|
form.send(:custom_label, attribute, options[:label], { required: required?, for: nil })
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def paragraph
|
|
34
|
+
form.send(:custom_paragraph, attribute, options[:label], { required: required? })
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def button_label
|
|
34
38
|
return button_edit_label if attachments.count.positive?
|
|
35
39
|
|
|
@@ -124,7 +128,16 @@ module Decidim
|
|
|
124
128
|
@attachments = begin
|
|
125
129
|
attachments = options[:attachments] || form.object.send(attribute)
|
|
126
130
|
attachments = Array(attachments).compact_blank
|
|
127
|
-
attachments.map
|
|
131
|
+
attachments.map do |attachment|
|
|
132
|
+
case attachment
|
|
133
|
+
when String
|
|
134
|
+
ActiveStorage::Blob.find_signed(attachment)
|
|
135
|
+
when Integer
|
|
136
|
+
Decidim::Attachment.find_by(id: attachment)
|
|
137
|
+
else
|
|
138
|
+
attachment
|
|
139
|
+
end
|
|
140
|
+
end.compact
|
|
128
141
|
end
|
|
129
142
|
end
|
|
130
143
|
|
|
@@ -41,8 +41,9 @@ module Decidim
|
|
|
41
41
|
|
|
42
42
|
def create_attachments(first_weight: 0)
|
|
43
43
|
weight = first_weight
|
|
44
|
-
# Add the weights first to the old
|
|
45
|
-
|
|
44
|
+
# Add the weights first to the old documents
|
|
45
|
+
document_ids = keep_ids
|
|
46
|
+
Decidim::Attachment.where(id: document_ids).each do |document|
|
|
46
47
|
document.update!(weight:)
|
|
47
48
|
weight += 1
|
|
48
49
|
end
|
|
@@ -59,7 +60,7 @@ module Decidim
|
|
|
59
60
|
documents = include_all_attachments ? documents_attached_to.attachments.with_attached_file : documents_attached_to.documents
|
|
60
61
|
|
|
61
62
|
documents.each do |document|
|
|
62
|
-
document.destroy!
|
|
63
|
+
document.destroy! unless keep_ids.include?(document.id)
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
documents_attached_to.reload
|
|
@@ -98,5 +99,21 @@ module Decidim
|
|
|
98
99
|
def blob(signed_id)
|
|
99
100
|
ActiveStorage::Blob.find_signed(signed_id)
|
|
100
101
|
end
|
|
102
|
+
|
|
103
|
+
def keep_ids
|
|
104
|
+
documents_array = Array(@form.documents)
|
|
105
|
+
documents_array.map do |doc|
|
|
106
|
+
case doc
|
|
107
|
+
when Decidim::Attachment
|
|
108
|
+
doc.id
|
|
109
|
+
when Integer
|
|
110
|
+
doc
|
|
111
|
+
when String
|
|
112
|
+
doc.match?(/\A\d+\z/) ? doc.to_i : nil
|
|
113
|
+
when Hash
|
|
114
|
+
(doc[:id] || doc["id"]).to_i
|
|
115
|
+
end
|
|
116
|
+
end.compact
|
|
117
|
+
end
|
|
101
118
|
end
|
|
102
119
|
end
|
|
@@ -4,8 +4,14 @@ module Decidim
|
|
|
4
4
|
# Update search indexes for each descendants of a given element
|
|
5
5
|
class FindAndUpdateDescendantsJob < ApplicationJob
|
|
6
6
|
queue_as :default
|
|
7
|
+
MAX_DEPTH = 5
|
|
8
|
+
|
|
9
|
+
def perform(element, current_depth = 0)
|
|
10
|
+
if current_depth >= MAX_DEPTH
|
|
11
|
+
Rails.logger.warn "Max depth of #{MAX_DEPTH} reached for element #{element.class.name} with id #{element.id}. Stopping recursion."
|
|
12
|
+
return
|
|
13
|
+
end
|
|
7
14
|
|
|
8
|
-
def perform(element)
|
|
9
15
|
descendants_collector = components_for(element)
|
|
10
16
|
descendants_collector << element.comments.to_a if element.respond_to?(:comments)
|
|
11
17
|
|
|
@@ -14,7 +20,7 @@ module Decidim
|
|
|
14
20
|
descendants_collector.each do |descendants|
|
|
15
21
|
next if descendants.blank?
|
|
16
22
|
|
|
17
|
-
Decidim::UpdateSearchIndexesJob.perform_later(descendants)
|
|
23
|
+
Decidim::UpdateSearchIndexesJob.perform_later(descendants, current_depth + 1)
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -4,8 +4,8 @@ module Decidim
|
|
|
4
4
|
class UpdateSearchIndexesJob < ApplicationJob
|
|
5
5
|
queue_as :default
|
|
6
6
|
|
|
7
|
-
def perform(elements)
|
|
8
|
-
elements.each { |element| element.try(:try_update_index_for_search_resource) }
|
|
7
|
+
def perform(elements, current_depth = 0)
|
|
8
|
+
elements.each { |element| element.try(:try_update_index_for_search_resource, current_depth) }
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -124,6 +124,13 @@ const createDropdown = (component) => {
|
|
|
124
124
|
* @return {void}
|
|
125
125
|
*/
|
|
126
126
|
const createDialog = (component) => {
|
|
127
|
+
const getFocusableElements = (container) => {
|
|
128
|
+
const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
|
|
129
|
+
return Array.from(container.querySelectorAll(selectors)).filter(
|
|
130
|
+
(el) => el.offsetParent !== null
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
127
134
|
const {
|
|
128
135
|
dataset: { dialog, ...attrs }
|
|
129
136
|
} = component;
|
|
@@ -145,11 +152,33 @@ const createDialog = (component) => {
|
|
|
145
152
|
backdropSelector: `[data-dialog="${dialog}"]`,
|
|
146
153
|
enableAutoFocus: false,
|
|
147
154
|
onOpen: (params, trigger) => {
|
|
155
|
+
const keyHandler = (event) => {
|
|
156
|
+
if (event.key !== "Tab") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const focusable = getFocusableElements(params);
|
|
160
|
+
if (focusable.length === 0) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (event.shiftKey && document.activeElement === focusable[0]) {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
focusable[focusable.length - 1].focus({ preventScroll: true });
|
|
166
|
+
} else if (!event.shiftKey && document.activeElement === focusable[focusable.length - 1]) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
focusable[0].focus({ preventScroll: true });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
params._focusTrapHandler = keyHandler;
|
|
172
|
+
params.addEventListener("keydown", keyHandler);
|
|
148
173
|
setFocusOnTitle(params);
|
|
149
174
|
window.focusGuard.trap(params, trigger);
|
|
150
175
|
params.dispatchEvent(new CustomEvent("open.dialog"));
|
|
151
176
|
},
|
|
152
177
|
onClose: (params) => {
|
|
178
|
+
if (params._focusTrapHandler) {
|
|
179
|
+
params.removeEventListener("keydown", params._focusTrapHandler);
|
|
180
|
+
Reflect.deleteProperty(params, "_focusTrapHandler");
|
|
181
|
+
}
|
|
153
182
|
window.focusGuard.disable();
|
|
154
183
|
params.dispatchEvent(new CustomEvent("close.dialog"));
|
|
155
184
|
},
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import { createDialog } from "src/decidim/a11y"
|
|
4
|
+
|
|
5
|
+
describe("a11y dialog focus trap", () => {
|
|
6
|
+
const dialogHtml = `
|
|
7
|
+
<button id="open-btn" data-dialog-open="testDialog">Open</button>
|
|
8
|
+
<div id="test-dialog" data-dialog="testDialog">
|
|
9
|
+
<div data-dialog-container>
|
|
10
|
+
<h2 id="dialog-title-testDialog" tabindex="-1" data-dialog-title>Test Dialog</h2>
|
|
11
|
+
<a href="#">Link 1</a>
|
|
12
|
+
<button>Button 1</button>
|
|
13
|
+
<input type="text" />
|
|
14
|
+
<button>Button 2</button>
|
|
15
|
+
<a href="#">Link 2</a>
|
|
16
|
+
</div>
|
|
17
|
+
<div data-dialog-actions>
|
|
18
|
+
<button data-dialog-close="testDialog">Close</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
document.body.innerHTML = dialogHtml;
|
|
25
|
+
window.Decidim = {
|
|
26
|
+
currentDialogs: {}
|
|
27
|
+
};
|
|
28
|
+
window.focusGuard = {
|
|
29
|
+
trap: jest.fn(),
|
|
30
|
+
disable: jest.fn()
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("keydown handler", () => {
|
|
35
|
+
let dialogEl = null;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
const component = document.querySelector("[data-dialog]");
|
|
39
|
+
createDialog(component);
|
|
40
|
+
dialogEl = document.querySelector("[data-dialog='testDialog']");
|
|
41
|
+
// Get the dialog from Decidim.currentDialogs and open it
|
|
42
|
+
const dialog = window.Decidim.currentDialogs.testDialog;
|
|
43
|
+
dialog.open();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("adds keydown handler on open", () => {
|
|
47
|
+
expect(dialogEl._focusTrapHandler).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("handles Tab key", () => {
|
|
51
|
+
const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
|
|
52
|
+
const tabbableElements = Array.from(dialogEl.querySelectorAll(selectors)).filter(
|
|
53
|
+
(el) => el.offsetParent || el.offsetParent === null
|
|
54
|
+
);
|
|
55
|
+
tabbableElements[tabbableElements.length - 1].focus();
|
|
56
|
+
|
|
57
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true });
|
|
58
|
+
const preventDefault = jest.fn();
|
|
59
|
+
event.preventDefault = preventDefault;
|
|
60
|
+
|
|
61
|
+
dialogEl.dispatchEvent(event);
|
|
62
|
+
|
|
63
|
+
expect(preventDefault).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("onClose cleanup", () => {
|
|
68
|
+
it("removes the keydown handler on close", () => {
|
|
69
|
+
const component = document.querySelector("[data-dialog]");
|
|
70
|
+
createDialog(component);
|
|
71
|
+
const dialogEl = document.querySelector("[data-dialog='testDialog']");
|
|
72
|
+
|
|
73
|
+
const dialog = window.Decidim.currentDialogs.testDialog;
|
|
74
|
+
dialog.open();
|
|
75
|
+
expect(dialogEl._focusTrapHandler).toBeDefined();
|
|
76
|
+
|
|
77
|
+
dialog.close();
|
|
78
|
+
expect(dialogEl._focusTrapHandler).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -162,7 +162,9 @@ export const initializeConfirm = () => {
|
|
|
162
162
|
return handleDocumentEvent(ev, [
|
|
163
163
|
Rails.linkClickSelector,
|
|
164
164
|
Rails.buttonClickSelector,
|
|
165
|
-
Rails.formInputClickSelector
|
|
165
|
+
Rails.formInputClickSelector,
|
|
166
|
+
'button[data-confirm][type="button"]',
|
|
167
|
+
"form button[data-confirm]"
|
|
166
168
|
]);
|
|
167
169
|
});
|
|
168
170
|
document.addEventListener("change", (ev) => {
|
|
@@ -180,6 +182,11 @@ export const initializeConfirm = () => {
|
|
|
180
182
|
$(Rails.formInputClickSelector).on("click.confirm", (ev) => {
|
|
181
183
|
handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector));
|
|
182
184
|
});
|
|
185
|
+
|
|
186
|
+
// Handle button[type="button"] with data-confirm inside forms
|
|
187
|
+
$('button[data-confirm][type="button"]').on("click.confirm", (ev) => {
|
|
188
|
+
handleConfirm(ev, ev.currentTarget);
|
|
189
|
+
});
|
|
183
190
|
});
|
|
184
191
|
};
|
|
185
192
|
|
|
@@ -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 */
|
|
@@ -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;
|