From b615c0c85469c94041a5e68b9d8b68dcf799f9f1 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Sun, 9 May 2021 12:56:21 -0700 Subject: [PATCH] rustdoc: use focus for search navigation Rather than keeping track of highlighted element inside the JS, take advantage of `.focus()` and the :focus CSS pseudo-class. This required wrapping each row of results in one big tag (because anchors can be focused, but table rows cannot). That in turn required moving from a table layout to a div layout with float. This makes it so Ctrl+Enter opens links in new tabs, and using the arrow keys to navigate off the bottom of the page scrolls the rest of the page into view. It also simplifies the keyboard event handling. It eliminates the need for click handlers on the search results, and for tracking mouse movements. This changes the UI treatment of mouse hovering. A hovered element now gets a light grey background, but does not change the focused element. It's possible to have two highlighted search results: one that is focused (via keyboard) and one that is hovered (via mouse). Pressing enter will activate the focused link; clicking will activate the hovered link. This matches up with how Firefox and Chrome handle suggestions in their URL bar, and avoids stray mouse movements changing the focus. Selecting tabs is now done with left/right arrows while any search result is focused. The visibility of results on each search tab is controlled with the "active" class, rather than by setting display: none directly. Note that the old code kept track of highlighted search element when tabbing back and forth. The new code doesn't. --- src/librustdoc/html/static/main.js | 9 +- src/librustdoc/html/static/rustdoc.css | 32 ++-- src/librustdoc/html/static/search.js | 186 ++++++++------------ src/librustdoc/html/static/themes/ayu.css | 50 ++++-- src/librustdoc/html/static/themes/dark.css | 50 +++--- src/librustdoc/html/static/themes/light.css | 46 ++--- 6 files changed, 178 insertions(+), 195 deletions(-) diff --git a/src/librustdoc/html/static/main.js b/src/librustdoc/html/static/main.js index ebc9ada4451e7..fcb06bc796ee0 100644 --- a/src/librustdoc/html/static/main.js +++ b/src/librustdoc/html/static/main.js @@ -170,7 +170,8 @@ function hideThemeButtonState() { // 1 for "In Parameters" // 2 for "In Return Types" currentTab: 0, - mouseMovedAfterSearch: true, + // tab and back preserves the element that was focused. + focusedByTab: [null, null, null], clearInputTimeout: function() { if (searchState.timeout !== null) { clearTimeout(searchState.timeout); @@ -262,10 +263,6 @@ function hideThemeButtonState() { search_input.placeholder = searchState.input.origPlaceholder; }); - document.addEventListener("mousemove", function() { - searchState.mouseMovedAfterSearch = true; - }); - search_input.removeAttribute('disabled'); // `crates{version}.js` should always be loaded before this script, so we can use it @@ -1070,7 +1067,7 @@ function hideThemeButtonState() { ["T", "Focus the theme picker menu"], ["↑", "Move up in search results"], ["↓", "Move down in search results"], - ["ctrl + ↑ / ↓", "Switch result tab"], + ["← / →", "Switch result tab (when results focused)"], ["⏎", "Go to active search result"], ["+", "Expand all sections"], ["-", "Collapse all sections"], diff --git a/src/librustdoc/html/static/rustdoc.css b/src/librustdoc/html/static/rustdoc.css index 1b7eff4604f5f..b03e657dce0f4 100644 --- a/src/librustdoc/html/static/rustdoc.css +++ b/src/librustdoc/html/static/rustdoc.css @@ -144,7 +144,7 @@ h4.type.trait-impl, h4.associatedconstant.trait-impl, h4.associatedtype.trait-im } h1, h2, h3, h4, -.sidebar, a.source, .search-input, .content table td:first-child > a, +.sidebar, a.source, .search-input, .search-results .result-name, div.item-list .out-of-band, #source-sidebar, #sidebar-toggle, details.rustdoc-toggle > summary::before, @@ -748,6 +748,15 @@ a { outline: 0; } +.search-results { + display: none; + padding-bottom: 2em; +} + +.search-results.active { + display: block; +} + .search-results .desc { white-space: nowrap; text-overflow: ellipsis; @@ -756,22 +765,14 @@ a { } .search-results a { + /* A little margin ensures the browser's outlining of focused links has room to display. */ + margin-left: 2px; + margin-right: 2px; display: block; } -.content .search-results td:first-child { - padding-right: 0; +.result-name { width: 50%; -} -.content .search-results td:first-child a { - padding-right: 10px; -} -.content .search-results td:first-child a:after { - clear: both; - content: ""; - display: block; -} -.content .search-results td:first-child a span { float: left; } @@ -1134,6 +1135,11 @@ pre.rust { .search-failed { text-align: center; margin-top: 20px; + display: none; +} + +.search-failed.active { + display: block; } .search-failed > ul { diff --git a/src/librustdoc/html/static/search.js b/src/librustdoc/html/static/search.js index a09d3eb1796b3..4de24d7328370 100644 --- a/src/librustdoc/html/static/search.js +++ b/src/librustdoc/html/static/search.js @@ -48,9 +48,9 @@ function printTab(nb) { }); onEachLazy(document.getElementById("results").childNodes, function(elem) { if (nb === 0) { - elem.style.display = ""; + addClass(elem, "active"); } else { - elem.style.display = "none"; + removeClass(elem, "active"); } nb -= 1; }); @@ -875,106 +875,22 @@ window.initSearch = function(rawSearchIndex) { }; } - function initSearchNav() { - var hoverTimeout; - - var click_func = function(e) { - var el = e.target; - // to retrieve the real "owner" of the event. - while (el.tagName !== "TR") { - el = el.parentNode; - } - var dst = e.target.getElementsByTagName("a"); - if (dst.length < 1) { - return; - } - dst = dst[0]; - if (window.location.pathname === dst.pathname) { - searchState.hideResults(); - document.location.href = dst.href; - } - }; - var mouseover_func = function(e) { - if (searchState.mouseMovedAfterSearch) { - var el = e.target; - // to retrieve the real "owner" of the event. - while (el.tagName !== "TR") { - el = el.parentNode; - } - clearTimeout(hoverTimeout); - hoverTimeout = setTimeout(function() { - onEachLazy(document.getElementsByClassName("search-results"), function(e) { - onEachLazy(e.getElementsByClassName("result"), function(i_e) { - removeClass(i_e, "highlighted"); - }); - }); - addClass(el, "highlighted"); - }, 20); - } - }; - onEachLazy(document.getElementsByClassName("search-results"), function(e) { - onEachLazy(e.getElementsByClassName("result"), function(i_e) { - i_e.onclick = click_func; - i_e.onmouseover = mouseover_func; - }); - }); - - searchState.input.onkeydown = function(e) { - // "actives" references the currently highlighted item in each search tab. - // Each array in "actives" represents a tab. - var actives = [[], [], []]; - // "current" is used to know which tab we're looking into. - var current = 0; - onEachLazy(document.getElementById("results").childNodes, function(e) { - onEachLazy(e.getElementsByClassName("highlighted"), function(h_e) { - actives[current].push(h_e); - }); - current += 1; - }); - var SHIFT = 16; - var CTRL = 17; - var ALT = 18; + function nextTab(direction) { + var next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; + searchState.focusedByTab[searchState.currentTab] = document.activeElement; + printTab(next); + focusSearchResult(); + } - var currentTab = searchState.currentTab; - if (e.which === 38) { // up - if (e.ctrlKey) { // Going through result tabs. - printTab(currentTab > 0 ? currentTab - 1 : 2); - } else { - if (!actives[currentTab].length || - !actives[currentTab][0].previousElementSibling) { - return; - } - addClass(actives[currentTab][0].previousElementSibling, "highlighted"); - removeClass(actives[currentTab][0], "highlighted"); - } - e.preventDefault(); - } else if (e.which === 40) { // down - if (e.ctrlKey) { // Going through result tabs. - printTab(currentTab > 1 ? 0 : currentTab + 1); - } else if (!actives[currentTab].length) { - var results = document.getElementById("results").childNodes; - if (results.length > 0) { - var res = results[currentTab].getElementsByClassName("result"); - if (res.length > 0) { - addClass(res[0], "highlighted"); - } - } - } else if (actives[currentTab][0].nextElementSibling) { - addClass(actives[currentTab][0].nextElementSibling, "highlighted"); - removeClass(actives[currentTab][0], "highlighted"); - } - e.preventDefault(); - } else if (e.which === 13) { // return - if (actives[currentTab].length) { - var elem = actives[currentTab][0].getElementsByTagName("a")[0]; - document.location.href = elem.href; - } - } else if ([SHIFT, CTRL, ALT].indexOf(e.which) !== -1) { - // Does nothing, it's just to avoid losing "focus" on the highlighted element. - } else if (actives[currentTab].length > 0) { - removeClass(actives[currentTab][0], "highlighted"); - } - }; + // focus the first search result on the active tab, or the result that + // was focused last time this tab was active. + function focusSearchResult() { + var target = searchState.focusedByTab[searchState.currentTab] || + document.querySelectorAll(".search-results.active a").item(0) || + document.querySelectorAll("#titles > button").item(searchState.currentTab); + if (target) { + target.focus(); + } } function buildHrefAndPath(item) { @@ -1044,16 +960,16 @@ window.initSearch = function(rawSearchIndex) { } function addTab(array, query, display) { - var extraStyle = ""; - if (display === false) { - extraStyle = " style=\"display: none;\""; + var extraClass = ""; + if (display === true) { + extraClass = " active"; } var output = ""; var duplicates = {}; var length = 0; if (array.length > 0) { - output = ""; + output = "
"; array.forEach(function(item) { var name, type; @@ -1069,20 +985,19 @@ window.initSearch = function(rawSearchIndex) { } length += 1; - output += "
"; + " "; }); - output += "
" + - "" + + output += "" + + "" + - "" + + name + "
"; + output += ""; } else { - output = "
No results :(
" + + output = "
No results :(
" + "Try on
DuckDuckGo?

" + @@ -1118,7 +1033,7 @@ window.initSearch = function(rawSearchIndex) { { var elem = document.createElement("a"); elem.href = results.others[0].href; - elem.style.display = "none"; + removeClass(elem, "active"); // For firefox, we need the element to be in the DOM so it can be clicked. document.body.appendChild(elem); elem.click(); @@ -1159,7 +1074,6 @@ window.initSearch = function(rawSearchIndex) { search.innerHTML = output; searchState.showResults(search); - initSearchNav(); var elems = document.getElementById("titles").childNodes; elems[0].onclick = function() { printTab(0); }; elems[1].onclick = function() { printTab(1); }; @@ -1437,6 +1351,50 @@ window.initSearch = function(rawSearchIndex) { }; searchState.input.onpaste = searchState.input.onchange; + searchState.outputElement().addEventListener("keydown", function(e) { + // We only handle unmodified keystrokes here. We don't want to interfere with, + // for instance, alt-left and alt-right for history navigation. + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + // up and down arrow select next/previous search result, or the + // search box if we're already at the top. + if (e.which === 38) { // up + var previous = document.activeElement.previousElementSibling; + if (previous) { + console.log("previousElementSibling", previous); + previous.focus(); + } else { + searchState.focus(); + } + e.preventDefault(); + } else if (e.which === 40) { // down + var next = document.activeElement.nextElementSibling; + if (next) { + next.focus(); + } + var rect = document.activeElement.getBoundingClientRect(); + if (window.innerHeight - rect.bottom < rect.height) { + window.scrollBy(0, rect.height); + } + e.preventDefault(); + } else if (e.which === 37) { // left + nextTab(-1); + e.preventDefault(); + } else if (e.which === 39) { // right + nextTab(1); + e.preventDefault(); + } + }); + + searchState.input.addEventListener("keydown", function(e) { + if (e.which === 40) { // down + focusSearchResult(); + e.preventDefault(); + } + }); + + var selectCrate = document.getElementById("crate-search"); if (selectCrate) { selectCrate.onchange = function() { diff --git a/src/librustdoc/html/static/themes/ayu.css b/src/librustdoc/html/static/themes/ayu.css index 13d1a475e4b2a..cf3b4b4b9361f 100644 --- a/src/librustdoc/html/static/themes/ayu.css +++ b/src/librustdoc/html/static/themes/ayu.css @@ -151,13 +151,16 @@ pre, .rustdoc.source .example-wrap { color: #c5c5c5; } -.content .highlighted { +.content a:hover { + background-color: #777; +} + +.content a:focus { color: #000 !important; background-color: #c6afb3; } -.content .highlighted a, .content .highlighted span { color: #000 !important; } -.content .highlighted { - background-color: #c6afb3; +.content a:focus { + color: #000 !important; } .search-results a { color: #0096cf; @@ -432,31 +435,21 @@ individually rather than as a group) */ /* FIXME: these rules should be at the bottom of the file but currently must be above the `@media (max-width: 700px)` rules due to a bug in the css checker */ /* see https://github.com/rust-lang/rust/pull/71237#issuecomment-618170143 */ -.content .highlighted.mod, .content .highlighted.externcrate {} .search-input:focus {} .content span.attr,.content a.attr,.block a.current.attr,.content span.derive,.content a.derive, .block a.current.derive,.content span.macro,.content a.macro,.block a.current.macro {} -.content .highlighted.trait {} .content span.struct,.content a.struct,.block a.current.struct {} #titles>button:hover,#titles>button.selected {} -.content .highlighted.traitalias {} .content span.type,.content a.type,.block a.current.type {} .content span.union,.content a.union,.block a.current.union {} -.content .highlighted.foreigntype {} pre.rust .lifetime {} -.content .highlighted.primitive {} -.content .highlighted.constant,.content .highlighted.static {} .stab.unstable {} -.content .highlighted.fn,.content .highlighted.method,.content .highlighted.tymethod {} h2,h3:not(.impl):not(.method):not(.type):not(.tymethod),h4:not(.method):not(.type):not(.tymethod) {} .content span.enum,.content a.enum,.block a.current.enum {} .content span.constant,.content a.constant,.block a.current.constant,.content span.static, -.content a.static,.block a.current.static {} +.content a.static, .block a.current.static {} .content span.keyword,.content a.keyword,.block a.current.keyword {} pre.rust .comment {} -.content .highlighted.enum {} -.content .highlighted.struct {} -.content .highlighted.keyword {} .content span.traitalias,.content a.traitalias,.block a.current.traitalias {} .content span.fn,.content a.fn,.block a.current.fn,.content span.method,.content a.method, .block a.current.method,.content span.tymethod,.content a.tymethod,.block a.current.tymethod, @@ -467,15 +460,36 @@ pre.rust .attribute .ident {} .content span.foreigntype,.content a.foreigntype,.block a.current.foreigntype {} pre.rust .doccomment {} .stab.deprecated {} -.content .highlighted.attr,.content .highlighted.derive,.content .highlighted.macro {} +.content a.attr,.content a.derive,.content a.macro {} .stab.portability {} -.content .highlighted.union {} .content span.primitive,.content a.primitive,.block a.current.primitive {} .content span.externcrate,.content span.mod,.content a.mod,.block a.current.mod {} -.content .highlighted.type {} pre.rust .kw-2,pre.rust .prelude-ty {} .content span.trait,.content a.trait,.block a.current.trait {} +.search-results a:focus span {} +a.result-trait:focus {} +a.result-traitalias:focus {} +a.result-mod:focus, +a.result-externcrate:focus {} +a.result-mod:focus {} +a.result-externcrate:focus {} +a.result-enum:focus {} +a.result-struct:focus {} +a.result-union:focus {} +a.result-fn:focus, +a.result-method:focus, +a.result-tymethod:focus {} +a.result-type:focus {} +a.result-foreigntype:focus {} +a.result-attr:focus, +a.result-derive:focus, +a.result-macro:focus {} +a.result-constant:focus, +a.result-static:focus {} +a.result-primitive:focus {} +a.result-keyword:focus {} + @media (max-width: 700px) { .sidebar-menu { background-color: #14191f; diff --git a/src/librustdoc/html/static/themes/dark.css b/src/librustdoc/html/static/themes/dark.css index 945ca9b252e5f..f66f9bbbc893d 100644 --- a/src/librustdoc/html/static/themes/dark.css +++ b/src/librustdoc/html/static/themes/dark.css @@ -109,32 +109,36 @@ pre, .rustdoc.source .example-wrap { color: #ddd; } -.content .highlighted { +.content a:hover { + background-color: #777; +} + +.content a:focus { color: #eee !important; background-color: #616161; } -.content .highlighted a, .content .highlighted span { color: #eee !important; } -.content .highlighted.trait { background-color: #013191; } -.content .highlighted.traitalias { background-color: #013191; } -.content .highlighted.mod, -.content .highlighted.externcrate { background-color: #afc6e4; } -.content .highlighted.mod { background-color: #803a1b; } -.content .highlighted.externcrate { background-color: #396bac; } -.content .highlighted.enum { background-color: #5b4e68; } -.content .highlighted.struct { background-color: #194e9f; } -.content .highlighted.union { background-color: #b7bd49; } -.content .highlighted.fn, -.content .highlighted.method, -.content .highlighted.tymethod { background-color: #4950ed; } -.content .highlighted.type { background-color: #38902c; } -.content .highlighted.foreigntype { background-color: #b200d6; } -.content .highlighted.attr, -.content .highlighted.derive, -.content .highlighted.macro { background-color: #217d1c; } -.content .highlighted.constant, -.content .highlighted.static { background-color: #0063cc; } -.content .highlighted.primitive { background-color: #00708a; } -.content .highlighted.keyword { background-color: #884719; } +.search-results a:focus span { color: #eee !important; } +a.result-trait:focus { background-color: #013191; } +a.result-traitalias:focus { background-color: #013191; } +a.result-mod:focus, +a.result-externcrate:focus { background-color: #afc6e4; } +a.result-mod:focus { background-color: #803a1b; } +a.result-externcrate:focus { background-color: #396bac; } +a.result-enum:focus { background-color: #5b4e68; } +a.result-struct:focus { background-color: #194e9f; } +a.result-union:focus { background-color: #b7bd49; } +a.result-fn:focus, +a.result-method:focus, +a.result-tymethod:focus { background-color: #4950ed; } +a.result-type:focus { background-color: #38902c; } +a.result-foreigntype:focus { background-color: #b200d6; } +a.result-attr:focus, +a.result-derive:focus, +a.result-macro:focus { background-color: #217d1c; } +a.result-constant:focus, +a.result-static:focus { background-color: #0063cc; } +a.result-primitive:focus { background-color: #00708a; } +a.result-keyword:focus { background-color: #884719; } .content .item-info::before { color: #ccc; } diff --git a/src/librustdoc/html/static/themes/light.css b/src/librustdoc/html/static/themes/light.css index 2ed638bdd4fa1..872160863b290 100644 --- a/src/librustdoc/html/static/themes/light.css +++ b/src/librustdoc/html/static/themes/light.css @@ -109,30 +109,34 @@ pre, .rustdoc.source .example-wrap { color: #4E4C4C; } -.content .highlighted { +.content a:hover { + background-color: #ddd; +} + +.content a:focus { color: #000 !important; background-color: #ccc; } -.content .highlighted a, .content .highlighted span { color: #000 !important; } -.content .highlighted.trait { background-color: #c7b6ff; } -.content .highlighted.traitalias { background-color: #c7b6ff; } -.content .highlighted.mod, -.content .highlighted.externcrate { background-color: #afc6e4; } -.content .highlighted.enum { background-color: #b4d1b9; } -.content .highlighted.struct { background-color: #e7b1a0; } -.content .highlighted.union { background-color: #b7bd49; } -.content .highlighted.fn, -.content .highlighted.method, -.content .highlighted.tymethod { background-color: #c6afb3; } -.content .highlighted.type { background-color: #ffc891; } -.content .highlighted.foreigntype { background-color: #f5c4ff; } -.content .highlighted.attr, -.content .highlighted.derive, -.content .highlighted.macro { background-color: #8ce488; } -.content .highlighted.constant, -.content .highlighted.static { background-color: #c3e0ff; } -.content .highlighted.primitive { background-color: #9aecff; } -.content .highlighted.keyword { background-color: #f99650; } +.search-results a:focus span { color: #000 !important; } +a.result-trait:focus { background-color: #c7b6ff; } +a.result-traitalias:focus { background-color: #c7b6ff; } +a.result-mod:focus, +a.result-externcrate:focus { background-color: #afc6e4; } +a.result-enum:focus { background-color: #b4d1b9; } +a.result-struct:focus { background-color: #e7b1a0; } +a.result-union:focus { background-color: #b7bd49; } +a.result-fn:focus, +a.result-method:focus, +a.result-tymethod:focus { background-color: #c6afb3; } +a.result-type:focus { background-color: #ffc891; } +a.result-foreigntype:focus { background-color: #f5c4ff; } +a.result-attr:focus, +a.result-derive:focus, +a.result-macro:focus { background-color: #8ce488; } +a.result-constant:focus, +a.result-static:focus { background-color: #c3e0ff; } +a.result-primitive:focus { background-color: #9aecff; } +a.result-keyword:focus { background-color: #f99650; } .content .item-info::before { color: #ccc; }