Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,102 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
});

// `j` / `k` move the selection between search result items, `Enter`
// activates the selected one. Selection is tracked with a class instead
// of browser focus, since the result `<li>`s are not focusable by default
// and the global `*:focus { outline-width: 0 }` rule would hide any focus
// ring anyway.
const SELECTED_CLASS = "result-selected";
let selectedResultId = null;

function isTypingTarget(target) {
return (
target &&
target.matches &&
target.matches(
"input, textarea, select, [contenteditable=true], [contenteditable='']",
)
);
}

function applySelection() {
document.querySelectorAll("." + SELECTED_CLASS).forEach((el) => {
if (el.id !== selectedResultId) {
el.classList.remove(SELECTED_CLASS);
}
});
if (!selectedResultId) {
return;
}
const el = document.getElementById(selectedResultId);
if (el && !el.classList.contains(SELECTED_CLASS)) {
el.classList.add(SELECTED_CLASS);
}
}

// Elm's VDOM diff replaces the `class` attribute on re-render (e.g. when
// toggling a result's expanded view), which would otherwise wipe our
// `result-selected` class. Re-apply it whenever the DOM changes.
new MutationObserver(applySelection).observe(document.body, {
Comment on lines +70 to +73
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suggest it make more sense to implement this functionality in Elm instead of in js?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case that seems worse tbh

subtree: true,
childList: true,
attributes: true,
attributeFilter: ["class"],
});

document.addEventListener("keydown", (event) => {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
return;
}
if (isTypingTarget(event.target)) {
return;
}

if (event.key === "Enter") {
if (!selectedResultId) {
return;
}
const current = document.getElementById(selectedResultId);
if (!current) {
return;
}
// The toggle anchor on result titles is rendered with `href=""`
// (a real `href` would be a navigation link, e.g. the flake source
// URL shown alongside the title for flake results).
const link =
current.querySelector('a[href=""]') || current.querySelector("a");
if (link) {
event.preventDefault();
link.click();
}
return;
}

if (event.key !== "j" && event.key !== "k") {
return;
}

const items = Array.from(document.querySelectorAll('[id^="result-"]'));
if (items.length === 0) {
return;
}

const currentIndex = selectedResultId
? items.findIndex((el) => el.id === selectedResultId)
: -1;

let nextIndex;
if (event.key === "j") {
nextIndex =
currentIndex < 0 ? 0 : Math.min(currentIndex + 1, items.length - 1);
} else {
nextIndex = currentIndex < 0 ? 0 : Math.max(currentIndex - 1, 0);
}

const next = items[nextIndex];
selectedResultId = next.id;
applySelection();
event.preventDefault();
next.scrollIntoView({ block: "nearest" });
});
5 changes: 5 additions & 0 deletions frontend/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
outline-offset: 0 !important;
}

// `j` / `k` keyboard navigation between search results.
.result-selected {
@include outline-visible;
}

.label-warning {
background: transparent;
color: var(--text-color-warning);
Expand Down
Loading