Skip to content

Commit 45e8d6a

Browse files
jensensthet
authored andcommitted
fix(pat-plone-modal): Evaluate focusable elements on each keypress.
Make modal focus trap dynamic: re-query visible focusable elements on each Tab press so that AJAX-loaded content (occurrence lists) and dynamically shown/hidden fields are always reachable via keyboard. Ref: plone/Products.CMFPlone#4272
1 parent 73ca787 commit 45e8d6a

1 file changed

Lines changed: 55 additions & 35 deletions

File tree

src/pat/modal/modal.js

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -763,50 +763,68 @@ export default Base.extend({
763763
activateFocusTrap: function () {
764764
var self = this;
765765
const modal_el = self.$modal[0];
766-
var inputsBody = modal_el
767-
.querySelector(`.${self.options.templateOptions.classBodyName}`)
768-
.querySelectorAll(`select, input:not([type="hidden"]), textarea, button, a`);
769-
var inputsFooter = modal_el
770-
.querySelector(`.${self.options.templateOptions.classFooterName}`)
771-
.querySelectorAll(`select, input:not([type="hidden"]), textarea, button, a`);
772-
var inputs = [];
773-
774-
for (const el of [...inputsBody, ...inputsFooter]) {
775-
if (dom.is_visible(el)) {
776-
inputs.push(el);
766+
const focusable_selector = `select, input:not([type="hidden"]), textarea, button, a`;
767+
768+
// Re-query visible focusable elements on each Tab press so that
769+
// dynamically loaded content (e.g. AJAX-loaded occurrence lists)
770+
// is always reachable via keyboard.
771+
function getVisibleInputs() {
772+
var bodyEl = modal_el.querySelector(
773+
`.${self.options.templateOptions.classBodyName}`
774+
);
775+
var footerEl = modal_el.querySelector(
776+
`.${self.options.templateOptions.classFooterName}`
777+
);
778+
var inputsBody = bodyEl
779+
? bodyEl.querySelectorAll(focusable_selector)
780+
: [];
781+
var inputsFooter = footerEl
782+
? footerEl.querySelectorAll(focusable_selector)
783+
: [];
784+
var inputs = [];
785+
for (const el of [...inputsBody, ...inputsFooter]) {
786+
if (dom.is_visible(el)) {
787+
inputs.push(el);
788+
}
789+
}
790+
if (inputs.length === 0) {
791+
inputs = [...modal_el.querySelectorAll(".modal-title")];
777792
}
793+
return inputs;
778794
}
779795

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

787-
modal_el.addEventListener(
788-
"keydown",
789-
(e) => {
790-
if (e.key === "Tab") {
791-
e.preventDefault();
798+
// Remove previous focus trap listener to prevent duplicates
799+
// when activateFocusTrap is called multiple times (e.g. redraw).
800+
if (self._focusTrapHandler) {
801+
modal_el.removeEventListener("keydown", self._focusTrapHandler);
802+
}
803+
self._focusTrapHandler = (e) => {
804+
if (e.key === "Tab") {
805+
e.preventDefault();
792806

793-
var target = e.target;
794-
var currentIndex = inputs.indexOf(target);
795-
if (currentIndex >= 0 && currentIndex < inputs.length) {
796-
var nextIndex = currentIndex + (e.shiftKey ? -1 : 1);
797-
if (nextIndex < 0 || nextIndex >= inputs.length) {
798-
closeInput.focus();
799-
} else {
800-
inputs[nextIndex].focus();
801-
}
802-
} else if (e.shiftKey && lastInput) {
803-
lastInput.focus();
804-
} else if (firstInput) {
805-
firstInput.focus();
807+
var inputs = getVisibleInputs();
808+
var firstInput = inputs.length !== 0 ? inputs[0] : null;
809+
var lastInput = inputs.length !== 0 ? inputs[inputs.length - 1] : null;
810+
var target = e.target;
811+
var currentIndex = inputs.indexOf(target);
812+
if (currentIndex >= 0 && currentIndex < inputs.length) {
813+
var nextIndex = currentIndex + (e.shiftKey ? -1 : 1);
814+
if (nextIndex < 0 || nextIndex >= inputs.length) {
815+
closeInput.focus();
816+
} else {
817+
inputs[nextIndex].focus();
806818
}
819+
} else if (e.shiftKey && lastInput) {
820+
lastInput.focus();
821+
} else if (firstInput) {
822+
firstInput.focus();
807823
}
808824
}
809-
);
825+
};
826+
modal_el.addEventListener("keydown", self._focusTrapHandler);
827+
810828
if (self.options.backdropOptions.closeOnClick === true) {
811829
modal_el.addEventListener("click", (e) => {
812830
if (!e.target.closest(`.${self.options.templateOptions.classModal}`)) {
@@ -815,6 +833,8 @@ export default Base.extend({
815833
});
816834
}
817835

836+
var inputs = getVisibleInputs();
837+
var firstInput = inputs.length !== 0 ? inputs[0] : null;
818838
if (firstInput && ["INPUT", "SELECT", "TEXTAREA"].includes(firstInput.nodeName)) {
819839
// autofocus first element when opening a modal with a form
820840
firstInput.focus();

0 commit comments

Comments
 (0)