Skip to content

Commit 94e58dc

Browse files
committed
Keyboard navigation to search
Resolves #83
1 parent d539af5 commit 94e58dc

File tree

2 files changed

+100
-6
lines changed

2 files changed

+100
-6
lines changed

src/css/vendor/search.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,20 @@ a.ais-Hits-item:hover {
289289
border-color: var(--nav-panel-divider-color);
290290
}
291291

292+
#hits ul li a.selected {
293+
background: #6db33f;
294+
color: #fff;
295+
}
296+
297+
#hits ul li a.selected .hit-description,
298+
#hits ul li a.selected .hit-breadcrumbs {
299+
color: #fff;
300+
}
301+
302+
#hits ul li a.selected mark {
303+
background-color: white;
304+
}
305+
292306
@media screen and (max-width: 1023.5px) {
293307
.DocSearch-Button {
294308
border-left-width: 1px;

src/js/vendor/search.bundle.js

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
const config = document.getElementById('search-script').dataset
77
const client = algoliasearch(config.appId, config.apiKey)
8+
let selected = null
89

910
const search = instantsearch({
1011
indexName: config.indexName,
@@ -45,6 +46,7 @@
4546
const { hits, showMore, widgetParams } = renderArgs
4647
const { container } = widgetParams
4748
lastRenderArgs = renderArgs
49+
selected = null
4850
if (isFirstRender) {
4951
const sentinel = document.createElement('div')
5052
container.appendChild(document.createElement('ul'))
@@ -127,19 +129,96 @@
127129
}
128130
})
129131

132+
const selectHit = (newSelected) => {
133+
const hits = document.querySelectorAll('#hits>ul>li>a')
134+
if (hits[selected]) {
135+
hits[selected].classList.remove('selected')
136+
selected = null
137+
}
138+
if (hits[newSelected]) {
139+
hits[newSelected].classList.add('selected')
140+
selected = newSelected
141+
}
142+
143+
if (selected) {
144+
hits[selected].scrollIntoView()
145+
}
146+
}
147+
148+
const openHit = (index) => {
149+
const hits = document.querySelectorAll('#hits>ul>li>a')
150+
if (hits[index]) {
151+
hits[index].click()
152+
}
153+
}
154+
155+
const renderSearchBox = (renderOptions, isFirstRender) => {
156+
const { query, refine, clear, isSearchStalled, widgetParams } = renderOptions
157+
if (isFirstRender) {
158+
const input = document.createElement('input')
159+
input.classList.add('ais-SearchBox-input')
160+
input.placeholder = `Search in the current documentation ${config.pageVersion}`
161+
const loadingIndicator = document.createElement('span')
162+
loadingIndicator.textContent = 'Loading...'
163+
const button = document.createElement('button')
164+
button.classList.add('ais-SearchBox-reset')
165+
button.innerHTML = '<svg class="ais-SearchBox-resetIcon" viewBox="0 0 20 20" width="10" height="10" aria-hidden="true"><path d="M8.114 10L.944 2.83 0 1.885 1.886 0l.943.943L10 8.113l7.17-7.17.944-.943L20 1.886l-.943.943-7.17 7.17 7.17 7.17.943.944L18.114 20l-.943-.943-7.17-7.17-7.17 7.17-.944.943L0 18.114l.943-.943L8.113 10z"></path></svg>'
166+
input.addEventListener('keydown', (event) => {
167+
switch (event.keyCode) {
168+
case 40: // Down
169+
event.preventDefault()
170+
if (selected === null) {
171+
selectHit(0)
172+
} else {
173+
selectHit(selected + 1)
174+
}
175+
break
176+
case 38: // Up
177+
event.preventDefault()
178+
if (selected === null) {
179+
selectHit(0)
180+
}
181+
selectHit(Math.max(selected - 1, 0))
182+
break
183+
case 13: // Enter
184+
event.preventDefault()
185+
if (selected !== null) {
186+
openHit(selected)
187+
}
188+
break
189+
case 9: // Tab
190+
event.preventDefault()
191+
break
192+
}
193+
})
194+
input.addEventListener('input', (event) => {
195+
refine(event.target.value)
196+
})
197+
button.addEventListener('click', () => {
198+
clear()
199+
})
200+
widgetParams.container.appendChild(input)
201+
widgetParams.container.appendChild(loadingIndicator)
202+
widgetParams.container.appendChild(button)
203+
}
204+
widgetParams.container.querySelector('input').value = query
205+
widgetParams.container.querySelector('span').hidden = !isSearchStalled
206+
}
207+
208+
const searchBox = instantsearch.connectors.connectSearchBox(
209+
renderSearchBox
210+
)
211+
212+
// selected
130213
search.addWidgets([
131214
instantsearch.widgets.configure({
132215
facetFilters: [`version:${config.pageVersion}`],
133216
attributesToSnippet: ['content'],
134217
attributesToHighlight: ['hierarchy'],
135218
distinct: true,
136219
}),
137-
instantsearch.widgets.searchBox({
138-
container: '#searchbox',
139-
autofocus: true,
140-
showSubmit: false,
141-
showReset: true,
142-
placeholder: `Search in the current documentation ${config.pageVersion}`,
220+
searchBox({
221+
container: document.querySelector('#searchbox'),
143222
}),
144223
infiniteHits({
145224
container: document.querySelector('#hits'),
@@ -149,6 +228,7 @@
149228
search.start()
150229

151230
const open = () => {
231+
selectHit(null)
152232
MicroModal.show('modal-1', {
153233
disableScroll: true,
154234
})

0 commit comments

Comments
 (0)