-
-
Notifications
You must be signed in to change notification settings - Fork 933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stimulus for search field autocomplete #4468
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { Controller } from "@hotwired/stimulus" | ||
|
||
// TODO: Add suggest help text and aria-live | ||
// https://accessibility.huit.harvard.edu/technique-aria-autocomplete | ||
export default class extends Controller { | ||
static targets = ["query", "suggestions", "template", "item"] | ||
static classes = ["selected"] | ||
|
||
connect() { | ||
this.indexNumber = -1; | ||
this.suggestLength = 0; | ||
} | ||
|
||
disconnect() { clear() } | ||
|
||
clear() { | ||
this.suggestionsTarget.innerHTML = "" | ||
this.suggestionsTarget.removeAttribute('tabindex'); | ||
this.suggestionsTarget.removeAttribute('aria-activedescendant'); | ||
} | ||
|
||
hide(e) { | ||
// Allows adjusting the cursor in the input without hiding the suggestions. | ||
if (!this.queryTarget.contains(e.target)) this.clear() | ||
} | ||
|
||
next() { | ||
this.indexNumber++; | ||
if (this.indexNumber >= this.suggestLength) this.indexNumber = 0; | ||
this.focusItem(this.itemTargets[this.indexNumber]); | ||
} | ||
|
||
prev() { | ||
martinemde marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.indexNumber--; | ||
if (this.indexNumber < 0) this.indexNumber = this.suggestLength - 1; | ||
this.focusItem(this.itemTargets[this.indexNumber]); | ||
} | ||
|
||
// On mouseover, highlight the item, shifting the index, | ||
// but don't change the input because it causes an undesireable feedback loop. | ||
highlight(e) { | ||
this.indexNumber = this.itemTargets.indexOf(e.currentTarget) | ||
this.focusItem(e.currentTarget, false) | ||
} | ||
|
||
choose(e) { | ||
martinemde marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.clear(); | ||
this.queryTarget.value = e.target.textContent; | ||
this.queryTarget.form.submit(); | ||
} | ||
|
||
async suggest(e) { | ||
const el = e.currentTarget; | ||
const term = el.value.trim(); | ||
|
||
if (term.length >= 2) { | ||
el.classList.remove('autocomplete-done'); | ||
el.classList.add('autocomplete-loading'); | ||
const query = new URLSearchParams({ query: term }) | ||
|
||
try { | ||
const response = await fetch('/api/v1/search/autocomplete?' + query, { method: 'GET' }) | ||
const data = await response.json() | ||
this.showSuggestions(data.slice(0, 10)) | ||
} catch (error) { } | ||
el.classList.remove('autocomplete-loading'); | ||
el.classList.add('autocomplete-done'); | ||
} else { | ||
this.clear() | ||
} | ||
} | ||
|
||
showSuggestions(items) { | ||
this.clear(); | ||
if (items.length === 0) { | ||
return; | ||
} | ||
items.forEach((item, idx) => this.appendItem(item, idx)); | ||
this.suggestionsTarget.setAttribute('tabindex', 0); | ||
this.suggestionsTarget.setAttribute('role', 'listbox'); | ||
|
||
this.suggestLength = items.length; | ||
this.indexNumber = -1; | ||
}; | ||
|
||
appendItem(text, idx) { | ||
const clone = this.templateTarget.content.cloneNode(true); | ||
const li = clone.querySelector('li') | ||
li.textContent = text; | ||
li.id = `suggest-${idx}`; | ||
this.suggestionsTarget.appendChild(clone) | ||
} | ||
|
||
focusItem(el, change = true) { | ||
this.itemTargets.forEach(el => el.classList.remove(this.selectedClass)) | ||
el.classList.add(this.selectedClass); | ||
this.suggestionsTarget.setAttribute('aria-activedescendant', el.id); | ||
if (change) { | ||
this.queryTarget.value = el.textContent; | ||
this.queryTarget.focus(); | ||
} | ||
} | ||
} |
This file was deleted.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be a partial instead of a layout? or a phlex component maybe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you say "partial instead of layout" I'm confused. It's a partial in the layouts dir. should it move somewhere else? Phlex components seem cool but they're also an extra barrier to entry. This is mostly code though, so it is reasonable. I thought about just making it a helper. I'd be really happy to watch you run through the component stuff you made so I could start using it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍 I agree on this. Should we open discussion somewhere to get in sync on this topic? Meanwhile let's not block template changes because of this if possible. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<% home = current_page?(root_path) || current_page?(advanced_search_path) %> | ||
<div class="<%= home ? "home__search-wrap" : "header__search-wrap" %>" role="search"> | ||
<%= form_tag search_path, method: :get, data: { controller: "autocomplete", autocomplete_selected_class: "selected", } do %> | ||
<%= search_field(home: home) %> | ||
|
||
<ul class="suggest-list" role="listbox" data-autocomplete-target="suggestions"></ul> | ||
|
||
<template id="suggestion" data-autocomplete-target="template"> | ||
<li class="menu-item" role="option" tabindex="-1" data-autocomplete-target="item" data-action="click->autocomplete#choose mouseover->autocomplete#highlight"></li> | ||
</template> | ||
|
||
<%= label_tag :query, id: "querylabel" do %> | ||
<span class="t-hidden"><%= t('layouts.application.header.search_gem_html') %></span> | ||
<% end %> | ||
|
||
<%= submit_tag '⌕', id: "search_submit", name: nil, class: home ? "home__search__icon" : "header__search__icon", aria: { labelledby: "querylabel" } %> | ||
|
||
<% if home %> | ||
<center> | ||
<%= link_to t("advanced_search"), advanced_search_path, class: "home__advanced__search t-link--has-arrow"%> | ||
</center> | ||
<% end %> | ||
<% end %> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this TODO for later (other PR)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's kind of a burden to add this here and requires some extra style and i18n work.