From 1a3e280ca4c33ec7e746f9fe51e28fdce512246b Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Sat, 8 Nov 2025 19:17:45 -0500 Subject: [PATCH] Internal: Improve forms keyboard shortcuts - refs #6997 --- assets/css/app.scss | 35 ++++++ assets/js/legacy/app.js | 242 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) diff --git a/assets/css/app.scss b/assets/css/app.scss index b66717650b1..55f55caee91 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -985,6 +985,41 @@ img.course-tool__icon { } .app-breadcrumb .p-breadcrumb-separator { padding-inline: .25rem; } +@layer base { + .form-control:focus, + input:focus, + select:focus, + textarea:focus { + outline: 0 !important; + border-color: #1d4ed8 !important; + box-shadow: 0 0 0 2px rgba(29, 78, 216, .35) !important; + border-radius: .5rem; + transition: box-shadow .12s ease, border-color .12s ease; + } + input[type="checkbox"]:focus, + input[type="radio"]:focus { + outline: 3px solid #1d4ed8 !important; + outline-offset: 2px; + box-shadow: 0 0 0 2px rgba(29, 78, 216, .35) !important; + border-radius: 4px; + } +} +@layer components { + .form-group:focus-within, + .field:focus-within, + fieldset:focus-within { + box-shadow: none !important; + outline: 0 !important; + } + .select2-container--default .select2-selection:focus, + .select2-container--default.select2-container--focus .select2-selection { + outline: 0 !important; + border-color: #1d4ed8 !important; + box-shadow: 0 0 0 2px rgba(29, 78, 216, .35) !important; + border-radius: .5rem !important; + } +} + @import "~@fancyapps/fancybox/dist/jquery.fancybox.css"; @import "~timepicker/jquery.timepicker.min.css"; @import "~qtip2/dist/jquery.qtip.min.css"; diff --git a/assets/js/legacy/app.js b/assets/js/legacy/app.js index 7f3b28bc75d..3442889c4b2 100644 --- a/assets/js/legacy/app.js +++ b/assets/js/legacy/app.js @@ -431,6 +431,248 @@ $(document).scroll(function () { } }) +// focus first meaningful field + Enter=submit (any form, any container) +;(function () { + // Avoid double-install + if (window.__A11Y_INSTALLED__) { + return + } + window.__A11Y_INSTALLED__ = true + + const NS = "[A11Y]" + const boundForms = new WeakSet() + const TEXT_TYPES = new Set([ + "text", + "email", + "password", + "search", + "url", + "tel", + "number", + "date", + "datetime-local", + "month", + "time", + "week", + "color", + ]) + + const isVisible = (el) => { + if (!el) return false + const s = getComputedStyle(el) + if (s.visibility === "hidden" || s.display === "none") return false + const r = el.getBoundingClientRect() + return r.width > 0 && r.height > 0 + } + + const inViewport = (el) => { + if (!el) return false + const r = el.getBoundingClientRect() + const h = window.innerHeight || document.documentElement.clientHeight + return r.top < h && r.bottom > 0 + } + + function listFocusable(root) { + const nodes = Array.from( + root.querySelectorAll( + [ + 'input:not([type="hidden"]):not([disabled])', + "textarea:not([disabled])", + "select:not([disabled])", + '[contenteditable="true"]', + ].join(","), + ), + ) + return nodes.filter((el) => { + if (!isVisible(el)) return false + if (el.tagName === "INPUT") { + const type = (el.getAttribute("type") || "text").toLowerCase() + if (!TEXT_TYPES.has(type)) return false + if (el.readOnly) return false + } + return true + }) + } + + function pickFocusTarget(form) { + // explicit markers + const explicit = form.querySelector("[autofocus], [data-autofocus]") + if (explicit && isVisible(explicit)) return explicit + + // 'title' or 'name' + const all = listFocusable(form) + const match = all.find((el) => { + const id = (el.id || "").toLowerCase() + const name = (el.name || "").toLowerCase() + return id.includes("title") || name.includes("title") || id === "name" || name === "name" + }) + return match || all[0] || null + } + + function focusWithRetries(el, attempt = 0) { + if (!el || !isVisible(el)) { + if (attempt === 0) console.log(NS, "No visible element to focus.") + return + } + + // If Select2 hid the