From 64cf539eca28933449e8c8df4118600a05ea4593 Mon Sep 17 00:00:00 2001 From: leo60228 Date: Wed, 11 Sep 2024 17:36:24 -0400 Subject: [PATCH 01/13] use search result hover styling for focus too --- src/static/css/site.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/static/css/site.css b/src/static/css/site.css index ed409930..7eb3605c 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -655,6 +655,7 @@ summary.underline-white > span:hover a:not(:hover) { position: relative; display: flex; padding: 4px 3px 4px 6px; + outline: none; } .wiki-search-result:hover { @@ -680,7 +681,7 @@ 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); } From 21de3c57d4a5364228fc95ffc3d79dae5ea16586 Mon Sep 17 00:00:00 2001 From: leo60228 Date: Wed, 11 Sep 2024 17:48:20 -0400 Subject: [PATCH 02/13] search result keyboard focus --- src/static/js/client/sidebar-search.js | 36 +++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 9d2cae34..917b9c12 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -329,6 +329,16 @@ export function addPageListeners() { info.searchInput.addEventListener('drop', handleDroppedIntoSearchInput); + info.searchInput.addEventListener('keydown', domEvent => { + if (domEvent.key === 'ArrowDown' && info.searchInput.value.length === info.searchInput.selectionStart) { + const elem = info.results?.firstChild; + if (elem && !elem.classList.contains('wiki-search-no-results')) { + domEvent.preventDefault(); + elem.focus(); + } + } + }); + info.endSearchLink.addEventListener('click', domEvent => { domEvent.preventDefault(); clearSidebarSearch(); @@ -593,7 +603,7 @@ function showSidebarSearchResults(results) { } for (const result of flatResults) { - const el = generateSidebarSearchResult(result); + const el = generateSidebarSearchResult(result, info); if (!el) continue; info.results.appendChild(el); @@ -609,7 +619,7 @@ function showSidebarSearchResults(results) { restoreSidebarSearchResultsScrollOffset(); } -function generateSidebarSearchResult(result) { +function generateSidebarSearchResult(result, info) { const preparedSlots = { color: result.data.color ?? null, @@ -680,7 +690,7 @@ function generateSidebarSearchResult(result) { return null; } - return generateSidebarSearchResultTemplate(preparedSlots); + return generateSidebarSearchResultTemplate(preparedSlots, info); } function getSearchResultImageSource(result) { @@ -693,7 +703,7 @@ function getSearchResultImageSource(result) { 'rebaseThumb')); } -function generateSidebarSearchResultTemplate(slots) { +function generateSidebarSearchResultTemplate(slots, info) { const link = document.createElement('a'); link.classList.add('wiki-search-result'); @@ -775,6 +785,24 @@ function generateSidebarSearchResultTemplate(slots) { saveSidebarSearchResultsScrollOffset(); }); + link.addEventListener('keydown', domEvent => { + if (domEvent.key === 'ArrowDown') { + const elem = link.nextElementSibling; + if (elem) { + domEvent.preventDefault(); + elem.focus(); + } + } else if (domEvent.key === 'ArrowUp') { + domEvent.preventDefault(); + const elem = link.previousElementSibling; + if (elem) { + elem.focus(); + } else { + info.searchInput.focus(); + } + } + }); + return link; } From 55dad23c4b65f9040b6b3e97c05813263b009ef7 Mon Sep 17 00:00:00 2001 From: leo60228 Date: Wed, 11 Sep 2024 18:19:50 -0400 Subject: [PATCH 03/13] address review --- src/static/css/site.css | 1 - src/static/js/client/sidebar-search.js | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/static/css/site.css b/src/static/css/site.css index 7eb3605c..b2a64c76 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -655,7 +655,6 @@ summary.underline-white > span:hover a:not(:hover) { position: relative; display: flex; padding: 4px 3px 4px 6px; - outline: none; } .wiki-search-result:hover { diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 917b9c12..34d2161f 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -330,11 +330,11 @@ export function addPageListeners() { info.searchInput.addEventListener('drop', handleDroppedIntoSearchInput); info.searchInput.addEventListener('keydown', domEvent => { - if (domEvent.key === 'ArrowDown' && info.searchInput.value.length === info.searchInput.selectionStart) { + if (domEvent.key === 'ArrowDown') { const elem = info.results?.firstChild; if (elem && !elem.classList.contains('wiki-search-no-results')) { domEvent.preventDefault(); - elem.focus(); + elem.focus({focusVisible: true}); } } }); @@ -790,13 +790,13 @@ function generateSidebarSearchResultTemplate(slots, info) { const elem = link.nextElementSibling; if (elem) { domEvent.preventDefault(); - elem.focus(); + elem.focus({focusVisible: true}); } } else if (domEvent.key === 'ArrowUp') { domEvent.preventDefault(); const elem = link.previousElementSibling; if (elem) { - elem.focus(); + elem.focus({focusVisible: true}); } else { info.searchInput.focus(); } From ca03d7becf6eff26664d3aab7e43a14f5f6c62c9 Mon Sep 17 00:00:00 2001 From: leo60228 Date: Wed, 11 Sep 2024 18:28:06 -0400 Subject: [PATCH 04/13] avoid replacing results after focusing --- src/static/js/client/sidebar-search.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 34d2161f..ae18c930 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -303,15 +303,20 @@ export function mutatePageContent() { export function addPageListeners() { if (!info.searchInput) return; + let prevValue = null; info.searchInput.addEventListener('change', _domEvent => { - if (info.searchInput.value) { + if (info.searchInput.value && info.searchInput.value !== prevValue) { activateSidebarSearch(info.searchInput.value); } + prevValue = info.searchInput.value; }); info.searchInput.addEventListener('input', _domEvent => { const {settings, state} = info; + if (prevValue === info.searchInput.value) return; + prevValue = info.searchInput.value; + if (!info.searchInput.value) { clearSidebarSearch(); return; From 13f8b8fe7679161a272db67ebb06d75c86d6041d Mon Sep 17 00:00:00 2001 From: leo60228 Date: Sat, 14 Sep 2024 17:44:20 -0400 Subject: [PATCH 05/13] address review 2 --- src/static/js/client/sidebar-search.js | 30 ++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index ae18c930..f46ce8e8 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -76,6 +76,8 @@ export const info = { stoppedScrollingTimeout: null, indexDownloadStatuses: Object.create(null), + + currentValue: null, }, session: { @@ -300,22 +302,32 @@ export function mutatePageContent() { info.searchBox.appendChild(info.endSearchLine); } +function trackSidebarSearchInputChanged() { + const {state} = info; + + const newValue = info.searchInput.value; + + if (newValue === state.currentValue) { + return false; + } else { + state.currentValue = newValue; + return !!newValue; + } +} + export function addPageListeners() { if (!info.searchInput) return; - let prevValue = null; info.searchInput.addEventListener('change', _domEvent => { - if (info.searchInput.value && info.searchInput.value !== prevValue) { + if (trackSidebarSearchInputChanged()) { activateSidebarSearch(info.searchInput.value); } - prevValue = info.searchInput.value; }); info.searchInput.addEventListener('input', _domEvent => { const {settings, state} = info; - if (prevValue === info.searchInput.value) return; - prevValue = info.searchInput.value; + trackSidebarSearchInputChanged(); if (!info.searchInput.value) { clearSidebarSearch(); @@ -608,7 +620,7 @@ function showSidebarSearchResults(results) { } for (const result of flatResults) { - const el = generateSidebarSearchResult(result, info); + const el = generateSidebarSearchResult(result); if (!el) continue; info.results.appendChild(el); @@ -624,7 +636,7 @@ function showSidebarSearchResults(results) { restoreSidebarSearchResultsScrollOffset(); } -function generateSidebarSearchResult(result, info) { +function generateSidebarSearchResult(result) { const preparedSlots = { color: result.data.color ?? null, @@ -695,7 +707,7 @@ function generateSidebarSearchResult(result, info) { return null; } - return generateSidebarSearchResultTemplate(preparedSlots, info); + return generateSidebarSearchResultTemplate(preparedSlots); } function getSearchResultImageSource(result) { @@ -708,7 +720,7 @@ function getSearchResultImageSource(result) { 'rebaseThumb')); } -function generateSidebarSearchResultTemplate(slots, info) { +function generateSidebarSearchResultTemplate(slots) { const link = document.createElement('a'); link.classList.add('wiki-search-result'); From e7f9ff5a305e049ca7204995e9377c32162da889 Mon Sep 17 00:00:00 2001 From: leo60228 Date: Sat, 14 Sep 2024 18:04:18 -0400 Subject: [PATCH 06/13] address review 3 --- src/static/css/site.css | 3 ++- src/static/js/client/sidebar-search.js | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/static/css/site.css b/src/static/css/site.css index b2a64c76..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:focus::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 f46ce8e8..48bc8d08 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -69,6 +69,8 @@ export const info = { tidiedSidebar: null, collapsedDetailsForTidiness: null, + currentValue: null, + workerStatus: null, searchStage: null, @@ -76,8 +78,6 @@ export const info = { stoppedScrollingTimeout: null, indexDownloadStatuses: Object.create(null), - - currentValue: null, }, session: { @@ -348,8 +348,8 @@ export function addPageListeners() { info.searchInput.addEventListener('keydown', domEvent => { if (domEvent.key === 'ArrowDown') { - const elem = info.results?.firstChild; - if (elem && !elem.classList.contains('wiki-search-no-results')) { + const elem = info.results.firstChild; + if (elem?.classList?.contains?.('wiki-search-result')) { domEvent.preventDefault(); elem.focus({focusVisible: true}); } From 135811201a999ec21d60a2380ddd870311679704 Mon Sep 17 00:00:00 2001 From: leo60228 Date: Sat, 14 Sep 2024 18:11:52 -0400 Subject: [PATCH 07/13] cleaner optional chaining --- src/static/js/client/sidebar-search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 48bc8d08..2d346b41 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -349,7 +349,7 @@ export function addPageListeners() { info.searchInput.addEventListener('keydown', domEvent => { if (domEvent.key === 'ArrowDown') { const elem = info.results.firstChild; - if (elem?.classList?.contains?.('wiki-search-result')) { + if (elem?.classList.contains('wiki-search-result')) { domEvent.preventDefault(); elem.focus({focusVisible: true}); } From 54143b573079250b441834df980a6e04a22e5517 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 10 Jan 2025 17:40:54 -0400 Subject: [PATCH 08/13] client: always capture up/down in search input --- src/static/js/client/sidebar-search.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 2d346b41..5c1b54b8 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -347,10 +347,13 @@ export function addPageListeners() { info.searchInput.addEventListener('drop', handleDroppedIntoSearchInput); info.searchInput.addEventListener('keydown', domEvent => { + if (domEvent.key === 'ArrowUp' || domEvent.key === 'ArrowDown') { + domEvent.preventDefault(); + } + if (domEvent.key === 'ArrowDown') { const elem = info.results.firstChild; if (elem?.classList.contains('wiki-search-result')) { - domEvent.preventDefault(); elem.focus({focusVisible: true}); } } From af518f1236a8ae22b45d2ab05ecb44430398dde2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 10 Jan 2025 17:51:48 -0400 Subject: [PATCH 09/13] client: activate search when pressing down --- src/static/js/client/sidebar-search.js | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 5c1b54b8..ef64a259 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -76,6 +76,7 @@ export const info = { stoppedTypingTimeout: null, stoppedScrollingTimeout: null, + focusFirstResultTimeout: null, indexDownloadStatuses: Object.create(null), }, @@ -104,6 +105,8 @@ export const info = { stoppedTypingDelay: 800, stoppedScrollingDelay: 200, + pressDownToFocusFirstResultLatency: 200, + maxActiveResultsStorage: 100000, }, }; @@ -340,6 +343,7 @@ export function addPageListeners() { state.stoppedTypingTimeout = setTimeout(() => { + state.stoppedTypingTimeout = null; activateSidebarSearch(info.searchInput.value); }, settings.stoppedTypingDelay); }); @@ -347,11 +351,27 @@ export function addPageListeners() { 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 (domEvent.key === 'ArrowDown') handleDown: { + if (state.stoppedTypingTimeout) { + clearTimeout(state.stoppedTypingTimeout); + state.stoppedTypingTimeout = null; + + state.focusFirstResultTimeout = + setTimeout(() => { + state.focusFirstResultTimeout = null; + }, settings.pressDownToFocusFirstResultLatency); + + activateSidebarSearch(info.searchInput.value); + + break handleDown; + } + const elem = info.results.firstChild; if (elem?.classList.contains('wiki-search-result')) { elem.focus({focusVisible: true}); @@ -479,6 +499,14 @@ async function activateSidebarSearch(query) { session.resultsScrollOffset = 0; showSidebarSearchResults(results); + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + if (!state.stoppedTypingTimeout) { + info.results.firstChild?.focus(); + } + } } function clearSidebarSearch() { From c7d233c4bc1f7bd8f8c3205b6683022c31a43b5f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 10 Jan 2025 18:24:34 -0400 Subject: [PATCH 10/13] client: factor out focusFirstSidebarSearchResult --- src/static/js/client/sidebar-search.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index ef64a259..4f0c2d8f 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -357,7 +357,7 @@ export function addPageListeners() { domEvent.preventDefault(); } - if (domEvent.key === 'ArrowDown') handleDown: { + if (domEvent.key === 'ArrowDown') { if (state.stoppedTypingTimeout) { clearTimeout(state.stoppedTypingTimeout); state.stoppedTypingTimeout = null; @@ -368,13 +368,8 @@ export function addPageListeners() { }, settings.pressDownToFocusFirstResultLatency); activateSidebarSearch(info.searchInput.value); - - break handleDown; - } - - const elem = info.results.firstChild; - if (elem?.classList.contains('wiki-search-result')) { - elem.focus({focusVisible: true}); + } else { + focusFirstSidebarSearchResult(); } } }); @@ -504,7 +499,7 @@ async function activateSidebarSearch(query) { clearTimeout(state.focusFirstResultTimeout); state.focusFirstResultTimeout = null; if (!state.stoppedTypingTimeout) { - info.results.firstChild?.focus(); + focusFirstSidebarSearchResult(); } } } @@ -866,6 +861,15 @@ function hideSidebarSearchResults() { cssProp(info.endSearchLine, 'display', 'none'); } +function focusFirstSidebarSearchResult() { + const elem = info.results.firstChild; + if (!elem?.classList.contains('wiki-search-result')) { + return; + } + + elem.focus({focusVisible: true}); +} + function saveSidebarSearchResultsScrollOffset() { const {session} = info; From 1eaa19ecac9affb7f6a7c0e6c6363c125ece91cf Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 10 Jan 2025 18:25:08 -0400 Subject: [PATCH 11/13] client: dismiss upcoming change event ...rather than tracking value changes, which is finnicky and broke how we detected pressing enter/return to acivate search (on 'change' event) --- src/static/js/client/sidebar-search.js | 42 +++++++++++++++----------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 4f0c2d8f..2e34baad 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -77,6 +77,7 @@ export const info = { stoppedTypingTimeout: null, stoppedScrollingTimeout: null, focusFirstResultTimeout: null, + dismissChangeEventTimeout: null, indexDownloadStatuses: Object.create(null), }, @@ -106,6 +107,7 @@ export const info = { stoppedScrollingDelay: 200, pressDownToFocusFirstResultLatency: 200, + dismissChangeEventAfterFocusingFirstResultLatency: 50, maxActiveResultsStorage: 100000, }, @@ -305,33 +307,24 @@ export function mutatePageContent() { info.searchBox.appendChild(info.endSearchLine); } -function trackSidebarSearchInputChanged() { - const {state} = info; - - const newValue = info.searchInput.value; - - if (newValue === state.currentValue) { - return false; - } else { - state.currentValue = newValue; - return !!newValue; - } -} - export function addPageListeners() { if (!info.searchInput) return; info.searchInput.addEventListener('change', _domEvent => { - if (trackSidebarSearchInputChanged()) { - 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 => { const {settings, state} = info; - trackSidebarSearchInputChanged(); - if (!info.searchInput.value) { clearSidebarSearch(); return; @@ -362,6 +355,10 @@ export function addPageListeners() { clearTimeout(state.stoppedTypingTimeout); state.stoppedTypingTimeout = null; + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + } + state.focusFirstResultTimeout = setTimeout(() => { state.focusFirstResultTimeout = null; @@ -862,11 +859,22 @@ function hideSidebarSearchResults() { } 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}); } From b05383e49bc1de21997321afb468905eb7cefb1d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 10 Jan 2025 18:44:56 -0400 Subject: [PATCH 12/13] client: simplify logic to cancel focus first result --- src/static/js/client/sidebar-search.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 2e34baad..10ccf1b5 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -106,7 +106,7 @@ export const info = { stoppedTypingDelay: 800, stoppedScrollingDelay: 200, - pressDownToFocusFirstResultLatency: 200, + pressDownToFocusFirstResultLatency: 500, dismissChangeEventAfterFocusingFirstResultLatency: 50, maxActiveResultsStorage: 100000, @@ -339,6 +339,11 @@ export function addPageListeners() { state.stoppedTypingTimeout = null; activateSidebarSearch(info.searchInput.value); }, settings.stoppedTypingDelay); + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + } }); info.searchInput.addEventListener('drop', handleDroppedIntoSearchInput); @@ -495,9 +500,7 @@ async function activateSidebarSearch(query) { if (state.focusFirstResultTimeout) { clearTimeout(state.focusFirstResultTimeout); state.focusFirstResultTimeout = null; - if (!state.stoppedTypingTimeout) { - focusFirstSidebarSearchResult(); - } + focusFirstSidebarSearchResult(); } } From 085ab0e889f1a3182dffb9f48cf33aff013f22ec Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 10 Jan 2025 18:45:44 -0400 Subject: [PATCH 13/13] client: cancel focus first search result when text cursor moves HTMLInputElement: selectionchange still not available, boo hoo although this is probably appropriate wrt selection moving literally anywhere else - should also cancel focus-first-result --- src/static/js/client/sidebar-search.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 10ccf1b5..c79fb837 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -376,6 +376,15 @@ export function addPageListeners() { } }); + document.addEventListener('selectionchange', _domEvent => { + const {state} = info; + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + } + }); + info.endSearchLink.addEventListener('click', domEvent => { domEvent.preventDefault(); clearSidebarSearch();