From 10d0e81099f27f8aeedc5eb99685086d3b9c32e1 Mon Sep 17 00:00:00 2001 From: vamsi-krish-chiguruwada Date: Wed, 29 Jul 2026 18:58:27 -0400 Subject: [PATCH] Show a hint for documentation-scoped search --- .../javascripts/views/search/search_scope.js | 38 ++++++++++++++++++ assets/stylesheets/components/_header.scss | 25 ++++++++++++ test/assets/search_hash_test.js | 39 +++++++++++++++++++ views/app.erb | 3 +- 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/assets/javascripts/views/search/search_scope.js b/assets/javascripts/views/search/search_scope.js index 45984bc176..e17555d5a8 100644 --- a/assets/javascripts/views/search/search_scope.js +++ b/assets/javascripts/views/search/search_scope.js @@ -3,11 +3,15 @@ app.views.SearchScope = class SearchScope extends app.View { static elements = { input: "._search-input", + hint: "._search-hint", + hintKey: "._search-hint-key", + hintDoc: "._search-hint-doc", tag: "._search-tag", }; static events = { click: "onClick", + input: "onInput", keydown: "onKeydown", textInput: "onTextInput", }; @@ -24,6 +28,12 @@ app.views.SearchScope = class SearchScope extends app.View { max_results: 1, }); this.searcher.on("results", (results) => this.onResults(results)); + + this.hintSearcher = new app.SynchronousSearcher({ + fuzzy_min_length: 2, + max_results: 1, + }); + this.hintSearcher.on("results", (results) => this.onHintResults(results)); } getScope() { @@ -70,12 +80,39 @@ app.views.SearchScope = class SearchScope extends app.View { } } + onHintResults(results) { + const doc = results[0]; + if (!doc) { + return; + } + this.hintKey.textContent = app.isMobile() ? "Space" : "Tab"; + this.hintDoc.textContent = doc.fullName; + this.hint.style.display = "block"; + this.input.style.paddingRight = this.hint.offsetWidth + 28 + "px"; + } + + hideHint() { + this.hint.style.display = "none"; + this.hintDoc.textContent = ""; + this.input.style.paddingRight = ""; + } + + onInput() { + this.hideHint(); + if (this.doc || app.isSingleDoc() || !this.input.value) { + return; + } + const value = this.input.value.slice(0, this.input.selectionStart); + this.hintSearcher.find(app.docs.all(), "text", value); + } + selectDoc(doc) { const previousDoc = this.doc; if (doc === previousDoc) { return; } this.doc = doc; + this.hideHint(); this.tag.textContent = doc.fullName; this.tag.style.display = "block"; @@ -95,6 +132,7 @@ app.views.SearchScope = class SearchScope extends app.View { } reset() { + this.hideHint(); if (!this.doc) { return; } diff --git a/assets/stylesheets/components/_header.scss b/assets/stylesheets/components/_header.scss index bc33d39c62..12516683af 100644 --- a/assets/stylesheets/components/_header.scss +++ b/assets/stylesheets/components/_header.scss @@ -206,6 +206,31 @@ ._search-active > & { display: block; } } +._search-hint { + display: none; + position: absolute; + z-index: 2; + top: .875rem; + right: 2rem; + max-width: 62%; + padding-left: .25rem; + overflow: hidden; + line-height: 1.25rem; + white-space: nowrap; + text-overflow: ellipsis; + font-size: .75rem; + color: var(--textColorLighter); + background: var(--contentBackground); + pointer-events: none; + + > kbd { + padding: 0 .25rem; + color: var(--textColorLight); + background: var(--searchTagBackground); + border-radius: 2px; + } +} + ._search-tag { display: none; position: absolute; diff --git a/test/assets/search_hash_test.js b/test/assets/search_hash_test.js index 276f3df387..e6031b9c5a 100644 --- a/test/assets/search_hash_test.js +++ b/test/assets/search_hash_test.js @@ -48,3 +48,42 @@ test("URL search hash preserves encoded literal plus signs in the query", () => assert.equal(search.getHashValue(), "operator+"); }); + +test("search scope shows a hint for the matching enabled documentation", () => { + const scope = Object.create(context.app.views.SearchScope.prototype); + const docs = [{ fullName: "Ruby 3" }]; + let searchArgs; + + context.app.docs = { all: () => docs }; + context.app.isMobile = () => false; + context.app.isSingleDoc = () => false; + + scope.input = { + value: "ruby", + selectionStart: 4, + style: {}, + }; + scope.hint = { offsetWidth: 140, style: {} }; + scope.hintKey = {}; + scope.hintDoc = {}; + scope.hintSearcher = { + find: (...args) => { + searchArgs = args; + }, + }; + + scope.onInput(); + assert.deepEqual(searchArgs, [docs, "text", "ruby"]); + + scope.onHintResults(docs); + assert.equal(scope.hintKey.textContent, "Tab"); + assert.equal(scope.hintDoc.textContent, "Ruby 3"); + assert.equal(scope.hint.style.display, "block"); + assert.equal(scope.input.style.paddingRight, "168px"); + + scope.doc = docs[0]; + scope.onInput(); + assert.equal(scope.hint.style.display, "none"); + assert.equal(scope.hintDoc.textContent, ""); + assert.equal(scope.input.style.paddingRight, ""); +}); diff --git a/views/app.erb b/views/app.erb index 13890a5b0d..b1925e4696 100644 --- a/views/app.erb +++ b/views/app.erb @@ -5,7 +5,8 @@