diff --git a/public/combobox-autocomplete.js b/public/combobox-autocomplete.js index 6dd765c..c159f09 100644 --- a/public/combobox-autocomplete.js +++ b/public/combobox-autocomplete.js @@ -198,15 +198,18 @@ ComboboxAutocomplete.prototype.setCurrentOptionStyle = function (option) { var opt = this.filteredOptions[i]; if (opt === option) { opt.setAttribute('aria-selected', 'true'); + // Keep the visible highlight ('active') on the option the combobox marks current; + // the legacy Swiftype handler otherwise drifts out of sync at the list boundaries. + opt.classList.add('active'); if ((this.listboxNode.scrollTop + this.listboxNode.offsetHeight) < (opt.offsetTop + opt.offsetHeight)) { this.listboxNode.scrollTop = opt.offsetTop + opt.offsetHeight - this.listboxNode.offsetHeight; } else if (this.listboxNode.scrollTop > (opt.offsetTop + 2)) { this.listboxNode.scrollTop = opt.offsetTop; } - // Focus stays on the input (aria-activedescendant), and the listbox is not an - // internal scroll container, so the browser will not auto-scroll to the active - // option. Scroll it into view so it is never obscured (WCAG 2.4.11). + // The listbox is capped and scrolls internally (see #search-results in + // autocomplete.css), so the scrollTop above keeps the focused suggestion visible + // (WCAG 2.4.11). scrollIntoView is a no-op-when-visible fallback. if (typeof opt.scrollIntoView === 'function') { try { opt.scrollIntoView({ block: 'nearest', inline: 'nearest' }); @@ -219,6 +222,7 @@ ComboboxAutocomplete.prototype.setCurrentOptionStyle = function (option) { } else { opt.removeAttribute('aria-selected'); + opt.classList.remove('active'); } } }; diff --git a/public/css/autocomplete.css b/public/css/autocomplete.css index 46b7c6c..47f99f3 100644 --- a/public/css/autocomplete.css +++ b/public/css/autocomplete.css @@ -52,6 +52,16 @@ form input.st-search-input { text-align: left; } +/* WCAG 2.4.11 Focus Not Obscured: cap the suggestion list to the viewport and scroll it + internally instead of scrolling the whole page, so the keyboard-focused suggestion stays + visible. 'position: relative' makes each option's offsetTop relative to this list so + setCurrentOptionStyle's listboxNode.scrollTop can reach it. No scrollbar when all fit. */ +#search-results { + position: relative; + max-height: calc(100vh - 60px); + overflow-y: auto; +} + .swiftype-widget .autocomplete li > a:hover { text-decoration: none; }