Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 55 additions & 35 deletions src/pat/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -763,50 +763,68 @@ export default Base.extend({
activateFocusTrap: function () {
var self = this;
const modal_el = self.$modal[0];
var inputsBody = modal_el
.querySelector(`.${self.options.templateOptions.classBodyName}`)
.querySelectorAll(`select, input:not([type="hidden"]), textarea, button, a`);
var inputsFooter = modal_el
.querySelector(`.${self.options.templateOptions.classFooterName}`)
.querySelectorAll(`select, input:not([type="hidden"]), textarea, button, a`);
var inputs = [];

for (const el of [...inputsBody, ...inputsFooter]) {
if (dom.is_visible(el)) {
inputs.push(el);
const focusable_selector = `select, input:not([type="hidden"]), textarea, button, a`;

// Re-query visible focusable elements on each Tab press so that
// dynamically loaded content (e.g. AJAX-loaded occurrence lists)
// is always reachable via keyboard.
function getVisibleInputs() {
var bodyEl = modal_el.querySelector(
`.${self.options.templateOptions.classBodyName}`
);
var footerEl = modal_el.querySelector(
`.${self.options.templateOptions.classFooterName}`
);
var inputsBody = bodyEl
? bodyEl.querySelectorAll(focusable_selector)
: [];
var inputsFooter = footerEl
? footerEl.querySelectorAll(focusable_selector)
: [];
var inputs = [];
for (const el of [...inputsBody, ...inputsFooter]) {
if (dom.is_visible(el)) {
inputs.push(el);
}
}
if (inputs.length === 0) {
inputs = [...modal_el.querySelectorAll(".modal-title")];
}
return inputs;
}

if (inputs.length === 0) {
inputs = modal_el.querySelectorAll(".modal-title");
}
var firstInput = inputs.length !== 0 ? inputs[0] : null;
var lastInput = inputs.length !== 0 ? inputs[inputs.length - 1] : null;
var closeInput = modal_el.querySelector(".modal-close");

modal_el.addEventListener(
"keydown",
(e) => {
if (e.key === "Tab") {
e.preventDefault();
// Remove previous focus trap listener to prevent duplicates
// when activateFocusTrap is called multiple times (e.g. redraw).
if (self._focusTrapHandler) {
modal_el.removeEventListener("keydown", self._focusTrapHandler);
}
self._focusTrapHandler = (e) => {
if (e.key === "Tab") {
e.preventDefault();

var target = e.target;
var currentIndex = inputs.indexOf(target);
if (currentIndex >= 0 && currentIndex < inputs.length) {
var nextIndex = currentIndex + (e.shiftKey ? -1 : 1);
if (nextIndex < 0 || nextIndex >= inputs.length) {
closeInput.focus();
} else {
inputs[nextIndex].focus();
}
} else if (e.shiftKey && lastInput) {
lastInput.focus();
} else if (firstInput) {
firstInput.focus();
var inputs = getVisibleInputs();
var firstInput = inputs.length !== 0 ? inputs[0] : null;
var lastInput = inputs.length !== 0 ? inputs[inputs.length - 1] : null;
var target = e.target;
var currentIndex = inputs.indexOf(target);
if (currentIndex >= 0 && currentIndex < inputs.length) {
var nextIndex = currentIndex + (e.shiftKey ? -1 : 1);
if (nextIndex < 0 || nextIndex >= inputs.length) {
closeInput.focus();
} else {
inputs[nextIndex].focus();
}
} else if (e.shiftKey && lastInput) {
lastInput.focus();
} else if (firstInput) {
firstInput.focus();
}
}
);
};
modal_el.addEventListener("keydown", self._focusTrapHandler);

if (self.options.backdropOptions.closeOnClick === true) {
modal_el.addEventListener("click", (e) => {
if (!e.target.closest(`.${self.options.templateOptions.classModal}`)) {
Expand All @@ -815,6 +833,8 @@ export default Base.extend({
});
}

var inputs = getVisibleInputs();
var firstInput = inputs.length !== 0 ? inputs[0] : null;
if (firstInput && ["INPUT", "SELECT", "TEXTAREA"].includes(firstInput.nodeName)) {
// autofocus first element when opening a modal with a form
firstInput.focus();
Expand Down
24 changes: 12 additions & 12 deletions src/pat/recurrence/recurrence.js
Original file line number Diff line number Diff line change
Expand Up @@ -717,15 +717,15 @@ const RecurrenceInput = function (conf, textarea) {
<span class="rlabel">${conf.localization.additionalDate}</span>
</span>
<span class="action">
<a date="${datevalue}" href="#" class="btn btn-sm btn-secondary rdate" >
<button type="button" date="${datevalue}" class="btn btn-sm btn-secondary rdate">
${conf.icons.remove}
</a>
</button>
</span>
</div>`);
$newdate.hide();
self.$modalForm.find("div.rioccurrences").prepend($newdate);
$newdate.slideDown();
$newdate.find("a.rdate").on("click", occurrenceDelete);
$newdate.find("button.rdate").on("click", occurrenceDelete);
} else {
errorarea.text(conf.localization.alreadyAdded).show();
}
Expand Down Expand Up @@ -805,13 +805,13 @@ const RecurrenceInput = function (conf, textarea) {
// Add the delete/undelete actions:
if (!readonly) {
element
.find(".rioccurrences .action a.rrule")
.find(".rioccurrences .action button.rrule")
.on("click", occurrenceExclude);
element
.find(".rioccurrences .action a.exdate")
.find(".rioccurrences .action button.exdate")
.on("click", occurrenceInclude);
element
.find(".rioccurrences .action a.rdate")
.find(".rioccurrences .action button.rdate")
.on("click", occurrenceDelete);
}
},
Expand Down Expand Up @@ -982,8 +982,8 @@ const RecurrenceInput = function (conf, textarea) {
if (startdate !== null) {
loadOccurrences(startdate, RFC5545.result, 0, true);
}
self.display.find('a[name="riedit"]').text(conf.localization.edit_rules);
self.display.find('a[name="ridelete"]').show();
self.display.find('button[name="riedit"]').text(conf.localization.edit_rules);
self.display.find('button[name="ridelete"]').show();
}

function recurrenceOff() {
Expand All @@ -998,8 +998,8 @@ const RecurrenceInput = function (conf, textarea) {
textarea.innerHTML = "";
$textarea.trigger("change"); // Clear the textarea.
self.display.find(".rioccurrences").hide();
self.display.find('a[name="riedit"]').text(conf.localization.add_rules);
self.display.find('a[name="ridelete"]').hide();
self.display.find('button[name="riedit"]').text(conf.localization.add_rules);
self.display.find('button[name="ridelete"]').hide();
}

function checkFields(form) {
Expand Down Expand Up @@ -1223,13 +1223,13 @@ const RecurrenceInput = function (conf, textarea) {
*/

// When you click "Delete...", the recurrence rules should be cleared.
self.display.find('a[name="ridelete"]').on("click", function (e) {
self.display.find('button[name="ridelete"]').on("click", function (e) {
e.preventDefault();
recurrenceOff();
});

// Show form modal when you click on the "Edit..." link
self.display.find('a[name="riedit"]').on("click", function (e) {
self.display.find('button[name="riedit"]').on("click", function (e) {
// Load the form to set up the right fields to show, etc.
e.preventDefault();
self.modal.show();
Expand Down
7 changes: 6 additions & 1 deletion src/pat/recurrence/recurrence.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ div.ridisplay label.ridisplay-label {
font-weight: 300;
}

div.ridisplay .rimain a {
div.ridisplay .rimain button {
margin-right: 0.5em;
}

div.rioccurrences .occurrence .action button:focus-visible {
outline: 2px solid var(--bs-primary, #0d6efd);
outline-offset: 2px;
}

div.rioccurrences .occurrence.rdate {
background: #ffffe0;
}
47 changes: 47 additions & 0 deletions src/pat/recurrence/recurrence.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,53 @@ describe("Recurrence", function () {
expect(add_occurrence).toBeTruthy();
});

it("Uses button elements for display widget controls.", async function () {
document.body.innerHTML = `
<textarea class="pat-recurrence"></textarea>
`;

registry.scan(document.body);
await utils.timeout(1);

const editBtn = document.querySelector("[name=riedit]");
const deleteBtn = document.querySelector("[name=ridelete]");
expect(editBtn.tagName).toEqual("BUTTON");
expect(deleteBtn.tagName).toEqual("BUTTON");
expect(editBtn.type).toEqual("button");
expect(deleteBtn.type).toEqual("button");
});

it("Uses button elements for added date actions.", async function () {
document.body.innerHTML = `
<input name="start" type="date" value="2026-01-01" />
<textarea class="pat-recurrence"
data-pat-recurrence='{
"startField": "[name=start]",
"allowAdditionalDates": true
}'
></textarea>
`;

registry.scan(document.body);
await utils.timeout(1);

const edit_btn = document.querySelector("[name=riedit]");
edit_btn.click();

// Add a date via the form
const add_date = document.querySelector(".modal #adddate");
add_date.value = "2026-02-15";
const add_date_btn = document.querySelector(".modal #addaction");
add_date_btn.click();

// The dynamically added rdate action should be a button
const rdate_action = document.querySelector(
".modal .rioccurrences .action button.rdate",
);
expect(rdate_action).toBeTruthy();
expect(rdate_action.tagName).toEqual("BUTTON");
});

it("Adds EXDATES as expected by RFC5545.", async function () {
// This fixes a problem described in
// https://github.com/plone/plone.formwidget.recurrence/issues/48
Expand Down
4 changes: 2 additions & 2 deletions src/pat/recurrence/templates/display.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<div class="ridisplay">
<div class="rimain">
<% if(!readOnly) { %>
<a class="btn btn-primary" name="riedit" href="#"><%= localization.add_rules %></a>
<a class="btn btn-danger" name="ridelete" style="display:none" href="#"><%= localization.delete_rules %></a>
<button type="button" class="btn btn-primary" name="riedit"><%= localization.add_rules %></button>
<button type="button" class="btn btn-danger" name="ridelete" style="display:none"><%= localization.delete_rules %></button>
<% } %>
<label class="ridisplay-label"><%= localization.displayUnactivate %></label>
</div>
Expand Down
12 changes: 6 additions & 6 deletions src/pat/recurrence/templates/occurrence.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@
<% if(!readOnly) { %>
<span class="action">
<% if(occurrence.type === "rrule") { %>
<a date="<%= occurrence.date %>" href="#"
<button type="button" date="<%= occurrence.date %>"
class="btn btn-sm btn-outline-secondary <%= occurrence.type %>" title="<%= localization.exclude %>">
<%= icons.exclude %>
</a>
</button>
<% } %>
<% if(occurrence.type === "rdate") { %>
<a date="<%= occurrence.date %>" href="#"
<button type="button" date="<%= occurrence.date %>"
class="btn btn-sm btn-outline-secondary <%= occurrence.type %>" title="<%= localization.remove %>">
<%= icons.remove %>
</a>
</button>
<% } %>
<% if(occurrence.type === "exdate") { %>
<a date="<%= occurrence.date %>" href="#"
<button type="button" date="<%= occurrence.date %>"
class="btn btn-sm btn-outline-secondary <%= occurrence.type %>" title="<%= localization.include %>">
<%= icons.include %>
</a>
</button>
<% } %>
</span>
<% } %>
Expand Down