feat(palette): press any button, link or field on the page by name

This commit is contained in:
hh committed 2026-10-16 14:51:02 +00:00
1 parent a1e7489ad4
commit 0ace2919aa
1 file changed
+69 -1
+69 -1
View File
@@ -17,9 +17,75 @@
run: () => void;
}
const CLICKABLE =
"a[href], button, summary, select, textarea, input:not([type='hidden']), [role='button'], [role='tab'], [role='link'], [role='menuitem']";
const FIELD =
"select, textarea, input:not([type='checkbox'], [type='radio'])";
const SKIP = "tbody, [aria-hidden='true'], [inert]";
const MAX_LABEL = 80;
const WHITESPACE = /\s+/g;
let dialog = $state<HTMLDialogElement>();
let input = $state<HTMLInputElement>();
let cursor = $state(0);
let pageEntries = $state.raw<Entry[]>([]);
function labelOf(element: HTMLElement): string {
const field = element as HTMLInputElement;
const text =
element.getAttribute("aria-label") ||
field.labels?.[0]?.innerText ||
element.innerText ||
element.getAttribute("title") ||
field.placeholder ||
"";
return text.replace(WHITESPACE, " ").trim().slice(0, MAX_LABEL);
}
function press(element: HTMLElement, field: boolean) {
requestAnimationFrame(() => {
element.focus();
if (!field) {
element.click();
}
});
}
function scanPage(): Entry[] {
const seen = new Set<string>();
const found: Entry[] = [];
const elements =
document
.querySelector("main")
?.querySelectorAll<HTMLElement>(CLICKABLE) ?? [];
for (const element of elements) {
const hidden =
element.closest(SKIP) !== null ||
element.matches(":disabled") ||
element.getClientRects().length === 0;
const label = hidden ? "" : labelOf(element);
const id = `${label}|${element.getAttribute("href") ?? ""}`;
if (!label || seen.has(id)) {
continue;
}
seen.add(id);
const field = element.matches(FIELD);
let hint = "button";
if (field) {
hint = "field";
} else if (element.matches("a")) {
hint = "link";
}
found.push({
group: "On this page",
hint,
id: `dom:${found.length}`,
label,
run: () => press(element, field),
});
}
return found;
}
const accounts = createQuery(() => ({
...lists.accounts("all"),
@@ -149,6 +215,7 @@
]);
const entries = $derived([
...pageEntries,
...staticEntries,
...commandEntries,
...entityEntries,
@@ -175,6 +242,7 @@
$effect(() => {
if (ui.palette && dialog && !dialog.open) {
pageEntries = scanPage();
cursor = 0;
dialog.showModal();
input?.select();
@@ -244,7 +312,7 @@
aria-expanded="true"
class="h-12 flex-1 bg-transparent text-[15px] outline-none placeholder:text-cladd-fg-softer"
{onkeydown}
placeholder="Go to page, account, budget, category, tag, or run a command"
placeholder="Press anything on this page, go to a page, account, budget, tag, or run a command"
role="combobox"
bind:this={input}
bind:value={ui.paletteQuery}