Skip to content
Merged
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
2 changes: 2 additions & 0 deletions site/src/components/starlight/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ const isActive = (href: string) => {

<script>
import { initDocsHeader } from '../../scripts/docs-header';
import { initDocsSearchKeys } from '../../scripts/docs-search-keys';
initDocsHeader();
initDocsSearchKeys();
</script>

<style>
Expand Down
65 changes: 65 additions & 0 deletions site/src/scripts/docs-search-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { onDispose } from './docs-header-disposal';

/**
* Arrow-key navigation for the docs search results.
*
* Pagefind's default UI leaves focus in the input and does nothing with
* ArrowDown/ArrowUp, so the results were reachable only by tabbing. Tab and
* Enter did work — search was usable — but the down arrow is the key almost
* everyone reaches for first, and pressing it appeared to do nothing at all.
*
* This moves real DOM focus rather than painting a highlight and tracking
* `aria-activedescendant`. The results are anchors, so focus gets Enter,
* middle-click and screen-reader announcement for free, and there is no
* second notion of "selected" to keep in sync with the rendered list.
*
* Results are queried on every keypress. Pagefind re-renders the list as the
* query changes, so any cached node reference would be stale by the next
* keystroke.
*/
const RESULT_SELECTOR = '.pagefind-ui__result-link';
const INPUT_SELECTOR = '.pagefind-ui__search-input';

export function initDocsSearchKeys(): void {
function handleKeyDown(event: KeyboardEvent) {
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;

const dialog = document.querySelector<HTMLDialogElement>('site-search dialog[open]');
if (!dialog) return;

const target = event.target;
if (!(target instanceof HTMLElement) || !dialog.contains(target)) return;

const input = dialog.querySelector<HTMLInputElement>(INPUT_SELECTOR);
const results = [...dialog.querySelectorAll<HTMLAnchorElement>(RESULT_SELECTOR)];
if (results.length === 0) return;

const index = results.indexOf(target.closest<HTMLAnchorElement>(RESULT_SELECTOR) as HTMLAnchorElement);
const onInput = target === input;

// Only act from the input or from a result. Anything else in the dialog
// (the close button, filters) keeps its native behaviour.
if (!onInput && index === -1) return;

let next: HTMLElement | null = null;
if (event.key === 'ArrowDown') {
next = onInput ? results[0] : (results[index + 1] ?? results[0]);
} else if (onInput) {
// Up from the input wraps to the last result, matching how most
// command palettes behave.
next = results[results.length - 1];
} else {
// Up from the first result returns to the input, so the query is
// always one keypress away rather than needing Shift+Tab.
next = index === 0 ? input : (results[index - 1] ?? input);
}

if (!next) return;
event.preventDefault();
next.focus();
if (next !== input) next.scrollIntoView({ block: 'nearest' });
}

document.addEventListener('keydown', handleKeyDown);
onDispose(() => document.removeEventListener('keydown', handleKeyDown));
}
14 changes: 13 additions & 1 deletion site/src/styles/starlight/20-docs-shell-closeout.css
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,21 @@ html[data-theme='light'] .right-sidebar-panel :where(a[aria-current='location'])
padding: 0 !important;
border: 1px solid rgba(140, 149, 166, 0.22) !important;
border-radius: 8px !important;
/* Opaque, and theme-aware.
*
* This was a hard-coded `rgba(27, 32, 40, 0.96)`, which failed twice
* over. The 4% let the page read straight through the open menu -- the
* page heading and body text crossing the menu labels. And because the
* colour was hard-coded rather than tokenised, the panel stayed dark in
* light theme, so a dark drawer sat over a #fbfbfb page with white
* bleeding through it.
*
* `--sl-color-bg-sidebar` is #111 in dark and #fbfbfb in light, so the
* panel now follows the theme and hides what is behind it. The green
* wash stays; it is decorative and reads on both. */
background:
linear-gradient(180deg, rgba(10, 174, 122, 0.04), transparent 11rem),
rgba(27, 32, 40, 0.96) !important;
var(--sl-color-bg-sidebar) !important;
box-shadow: 18px 0 46px rgba(0, 0, 0, 0.36) !important;
overflow: auto !important;
overscroll-behavior: contain !important;
Expand Down
Loading