diff --git a/src/static/css/site.css b/src/static/css/site.css index ed409930..6c853161 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -680,7 +680,8 @@ summary.underline-white > span:hover a:not(:hover) { border-bottom: 1px solid var(--dim-color); } -.wiki-search-result:hover::before { +.wiki-search-result:hover::before, +.wiki-search-result:focus::before { display: block; background: var(--light-ghost-color); } diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 9d2cae34..c79fb837 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -69,11 +69,15 @@ export const info = { tidiedSidebar: null, collapsedDetailsForTidiness: null, + currentValue: null, + workerStatus: null, searchStage: null, stoppedTypingTimeout: null, stoppedScrollingTimeout: null, + focusFirstResultTimeout: null, + dismissChangeEventTimeout: null, indexDownloadStatuses: Object.create(null), }, @@ -102,6 +106,9 @@ export const info = { stoppedTypingDelay: 800, stoppedScrollingDelay: 200, + pressDownToFocusFirstResultLatency: 500, + dismissChangeEventAfterFocusingFirstResultLatency: 50, + maxActiveResultsStorage: 100000, }, }; @@ -304,9 +311,15 @@ export function addPageListeners() { if (!info.searchInput) return; info.searchInput.addEventListener('change', _domEvent => { - if (info.searchInput.value) { - activateSidebarSearch(info.searchInput.value); + const {state} = info; + + if (state.dismissChangeEventTimeout) { + state.dismissChangeEventTimeout = null; + clearTimeout(state.dismissChangeEventTimeout); + return; } + + activateSidebarSearch(info.searchInput.value); }); info.searchInput.addEventListener('input', _domEvent => { @@ -323,12 +336,55 @@ export function addPageListeners() { state.stoppedTypingTimeout = setTimeout(() => { + state.stoppedTypingTimeout = null; activateSidebarSearch(info.searchInput.value); }, settings.stoppedTypingDelay); + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + } }); info.searchInput.addEventListener('drop', handleDroppedIntoSearchInput); + info.searchInput.addEventListener('keydown', domEvent => { + const {settings, state} = info; + + if (domEvent.key === 'ArrowUp' || domEvent.key === 'ArrowDown') { + domEvent.preventDefault(); + } + + if (domEvent.key === 'ArrowDown') { + if (state.stoppedTypingTimeout) { + clearTimeout(state.stoppedTypingTimeout); + state.stoppedTypingTimeout = null; + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + } + + state.focusFirstResultTimeout = + setTimeout(() => { + state.focusFirstResultTimeout = null; + }, settings.pressDownToFocusFirstResultLatency); + + activateSidebarSearch(info.searchInput.value); + } else { + focusFirstSidebarSearchResult(); + } + } + }); + + document.addEventListener('selectionchange', _domEvent => { + const {state} = info; + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + } + }); + info.endSearchLink.addEventListener('click', domEvent => { domEvent.preventDefault(); clearSidebarSearch(); @@ -449,6 +505,12 @@ async function activateSidebarSearch(query) { session.resultsScrollOffset = 0; showSidebarSearchResults(results); + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + focusFirstSidebarSearchResult(); + } } function clearSidebarSearch() { @@ -775,6 +837,24 @@ function generateSidebarSearchResultTemplate(slots) { saveSidebarSearchResultsScrollOffset(); }); + link.addEventListener('keydown', domEvent => { + if (domEvent.key === 'ArrowDown') { + const elem = link.nextElementSibling; + if (elem) { + domEvent.preventDefault(); + elem.focus({focusVisible: true}); + } + } else if (domEvent.key === 'ArrowUp') { + domEvent.preventDefault(); + const elem = link.previousElementSibling; + if (elem) { + elem.focus({focusVisible: true}); + } else { + info.searchInput.focus(); + } + } + }); + return link; } @@ -790,6 +870,26 @@ function hideSidebarSearchResults() { cssProp(info.endSearchLine, 'display', 'none'); } +function focusFirstSidebarSearchResult() { + const {settings, state} = info; + + const elem = info.results.firstChild; + if (!elem?.classList.contains('wiki-search-result')) { + return; + } + + if (state.dismissChangeEventTimeout) { + clearTimeout(state.dismissChangeEventTimeout); + } + + state.dismissChangeEventTimeout = + setTimeout(() => { + state.dismissChangeEventTimeout = null; + }, settings.dismissChangeEventAfterFocusingFirstResultLatency); + + elem.focus({focusVisible: true}); +} + function saveSidebarSearchResultsScrollOffset() { const {session} = info;