diff --git a/sphinx_searchlite/static/engine/searchlite.js b/sphinx_searchlite/static/engine/searchlite.js
index be31485..2c1eb70 100644
--- a/sphinx_searchlite/static/engine/searchlite.js
+++ b/sphinx_searchlite/static/engine/searchlite.js
@@ -196,6 +196,12 @@
return [];
})
.then(function (records) {
+ // Record urls are relative to the documentation root, but a page
+ // nested below it would resolve them against its own directory.
+ var root = new URL("../", new URL(url, document.baseURI));
+ records.forEach(function (record) {
+ record.u = new URL(record.u, root).href;
+ });
index = build(records);
return index;
});
diff --git a/sphinx_searchlite/static/ui/searchlite-ui.js b/sphinx_searchlite/static/ui/searchlite-ui.js
index f2a17c1..99d692a 100644
--- a/sphinx_searchlite/static/ui/searchlite-ui.js
+++ b/sphinx_searchlite/static/ui/searchlite-ui.js
@@ -142,8 +142,15 @@
seen.add(field);
field.classList.add("searchlite-adopted");
field.readOnly = true;
- field.addEventListener("focus", open);
field.addEventListener("click", open);
+ // Opening on `focus` traps the user: closing the dialog restores focus
+ // to the field that opened it, which would immediately reopen it.
+ field.addEventListener("keydown", function (event) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ open();
+ }
+ });
var form = field.closest("form");
if (form) {
form.addEventListener("submit", function (event) {
@@ -174,6 +181,11 @@
event.preventDefault();
window.location.href = active.href;
}
+ } else if (event.key === "Escape") {
+ // `` swallows the first Escape to clear itself, which
+ // would leave the dialog needing two presses despite the `Esc` hint.
+ event.preventDefault();
+ dialog.close();
}
});
diff --git a/sphinx_searchlite/tests/test_searchlite.py b/sphinx_searchlite/tests/test_searchlite.py
index df1cdb9..54370eb 100644
--- a/sphinx_searchlite/tests/test_searchlite.py
+++ b/sphinx_searchlite/tests/test_searchlite.py
@@ -117,6 +117,14 @@ def test_index_is_still_emitted(self, built):
assert (built / "_static" / "searchlite-index.json").is_file()
+class TestResultLinks:
+ def test_urls_resolve_against_the_documentation_root(self, built):
+ js = (built / "_static" / "searchlite.js").read_text()
+ # Records store root-relative urls, so a page nested below the root
+ # would otherwise resolve them against its own directory.
+ assert 'new URL("../", new URL(url, document.baseURI))' in js
+
+
class TestThemeSearchAdoption:
def test_adoption_is_advertised_to_the_ui_script(self, built):
assert 'data-searchlite-adopt="true"' in (built / "guide.html").read_text()
@@ -125,6 +133,17 @@ def test_adoption_can_be_switched_off(self, tmp_path_factory):
out = _build(tmp_path_factory, CONF + "\nsearchlite_adopt_theme_search = False\n", "src_no_adopt")
assert 'data-searchlite-adopt="false"' in (out / "guide.html").read_text()
+ def test_adopted_fields_do_not_reopen_the_dialog_on_focus(self, built):
+ js = (built / "_static" / "searchlite-ui.js").read_text()
+ # Closing the dialog restores focus to the field that opened it, so an
+ # opener bound to `focus` would reopen it and trap the reader.
+ assert 'field.addEventListener("focus", open)' not in js
+
+ def test_escape_closes_the_dialog_on_the_first_press(self, built):
+ js = (built / "_static" / "searchlite-ui.js").read_text()
+ # `` eats the first Escape to clear itself.
+ assert 'event.key === "Escape"' in js
+
def test_styles_no_longer_hardcode_a_dark_palette(self, built):
css = (built / "_static" / "searchlite.css").read_text()
# Colours are adopted from the host page instead, since themes signal